├── .codecov.yml ├── .env.template ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── pull_request_template.md └── workflows │ └── CI.yaml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── buildenv.sh ├── cmd ├── root.go ├── root_test.go └── version.go ├── common ├── cache.go ├── cache_test.go ├── common.go ├── common_test.go ├── darkside.go ├── generatecerts.go ├── logging │ ├── logging.go │ └── logging_test.go └── mempool.go ├── docgen.sh ├── docker-compose.yml ├── docker ├── cert.key ├── cert.pem ├── gen_cert.sh ├── grafana │ └── provisioning │ │ └── datasources │ │ ├── loki.yaml │ │ └── prometheus.yaml ├── prometheus │ └── config.yml └── zcash.conf ├── docs ├── architecture.md ├── darksidewalletd.md ├── docker-compose-setup.md ├── docker-run.md ├── images │ ├── grafana-configure.png │ ├── grafana-explore-2.png │ ├── grafana-explore-3.png │ ├── grafana-explore-4.png │ ├── grafana-explore.png │ ├── grafana-import-1.png │ ├── grafana-import-2.png │ ├── grafana-login.png │ ├── grafana-manage.png │ └── grafana-zcashd-dashboard.png ├── integration-tests.md ├── release-notes │ └── release-notes-0.4.1.md └── rtd │ └── index.html ├── frontend ├── frontend_test.go ├── rpc_client.go └── service.go ├── go.mod ├── go.sum ├── kubernetes └── tekton │ ├── README.md │ ├── pipeline.yml │ ├── pipelinerun.yml │ ├── serviceaccount.yml │ └── triggers.yml ├── lightwalletd-example.yml ├── main.go ├── parser ├── block.go ├── block_header.go ├── block_header_test.go ├── block_test.go ├── fuzz.go ├── internal │ └── bytestring │ │ ├── bytestring.go │ │ └── bytestring_test.go ├── transaction.go ├── transaction_test.go ├── util.go └── util_test.go ├── smoke-test.bash ├── tekton ├── resources.yml ├── taskruns.yml └── triggerbinding.yml ├── testclient ├── main.go └── stress.sh ├── testdata ├── badblocks ├── blocks ├── compact_blocks.json ├── corpus │ ├── block0 │ ├── block1 │ ├── block2 │ └── block3 ├── mainnet_genesis ├── tx_v5.json ├── zip143_raw_tx └── zip243_raw_tx ├── testtools ├── genblocks │ └── main.go └── zap │ └── main.go ├── utils ├── pullblocks.sh └── submitblocks.sh └── walletrpc ├── compact_formats.pb.go ├── compact_formats.proto ├── darkside.pb.go ├── darkside.proto ├── darkside_grpc.pb.go ├── generate.go ├── service.pb.go ├── service.proto ├── service_grpc.pb.go └── walletrpc_test.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | #used to disable false alarm in codecov/patch github status check 2 | 3 | coverage: 4 | status: 5 | patch: off 6 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | ZCASHD_RPCUSER=zcashrpc 2 | ZCASHD_RPCPASSWORD=${PASSWORD_ZCASHD} 3 | ZCASHD_RPCPORT=38232 4 | ZCASHD_ALLOWIP=0.0.0.0/0 5 | ZCASHD_DATADIR=/srv/zcashd/.zcash 6 | ZCASHD_PARMDIR=/srv/zcashd/.zcash-params 7 | ZCASHD_GEN=0 8 | GF_SECURITY_ADMIN_USER=admin 9 | GF_SECURITY_ADMIN_PASSWORD=${PASSWORD_GRAFANA} 10 | LWD_GRPC_PORT=9067 11 | LWD_HTTP_PORT=9068 12 | ZCASHD_CONF_PATH=/srv/lightwalletd/zcash.conf 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Tell us what's wrong. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is the bug?** 11 | What happened, and why was it unexpected? If applicable, add screenshots to help explain your problem. 12 | 13 | **Additional context** 14 | Add any other context about the problem here. Device, OS, App versions, and other details appreciated. 15 | 16 | **Solution** 17 | Add fix suggestions here, or what you’d like to see. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea. 4 | title: '' 5 | labels: 'use case' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is your feature request?** 11 | A description of your feature request. References or screenshots appreciated. 12 | 13 | **How would this feature help you?** 14 | A clear and concise description of what this feature would help with, or do. 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Please ensure this checklist is followed for any pull requests for this repo. This checklist must be checked by both the PR creator and by anyone who reviews the PR. 2 | * [ ] Relevant documentation for this PR has to be completed before the PR can be merged 3 | * [ ] A test plan for the PR must be documented in the PR notes and included in the test plan for the next regular release 4 | 5 | As a note, all CI tests need to be passing and all appropriate code reviews need to be done before this PR can be merged 6 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | release: 8 | types: [published] 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | go-version: ['>=1.23.0'] 19 | 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v2 23 | 24 | - name: Set up Go ${{ matrix.go-version }} 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | id: go 29 | 30 | - name: Build ${{ matrix.go-version }} 31 | run: | 32 | make 33 | make test 34 | 35 | - name: Race conditions test 36 | run: make race 37 | 38 | - name: test Build Docker image 39 | run: docker build . 40 | 41 | docker_set_env: 42 | needs: [build] 43 | if: github.event_name == 'release' 44 | uses: zcash/.github/.github/workflows/standard-versioning-for-docker.yaml@main 45 | 46 | docker_build_push: 47 | uses: zcash/.github/.github/workflows/build-and-push-docker-hub.yaml@main 48 | needs: [build, docker_set_env] 49 | if: github.event_name == 'release' 50 | with: 51 | image_name: ${{ github.event.repository.name }} 52 | image_tags: ${{ needs.docker_set_env.outputs.tags }} 53 | dockerfile: ./Dockerfile 54 | context: . 55 | build-args: "" 56 | secrets: 57 | dockerhub_registry: ${{ secrets.DOCKERHUB_REGISTRY }} 58 | dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} 59 | dockerhub_password: ${{ secrets.DOCKERHUB_PASSWORD }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | first-make-timestamp 2 | lightwalletd 3 | server.log 4 | db/ 5 | coverage.out 6 | test-log 7 | lwd-api.html 8 | *.orig 9 | __debug_bin 10 | .vscode 11 | */unittestcache/ 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this library will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this library adheres to Rust's notion of 6 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | The most recent changes are listed first. 8 | 9 | ## [Unreleased] 10 | 11 | ### Added 12 | 13 | - Add debug logging to gRPC entry and exit points. 14 | 15 | - Add smoke test 16 | 17 | - lightwalletd node operators can export a donation address in the 18 | GetLightdInfo gRPC. 19 | 20 | - Add the ability to not create and maintain a compact block cache. 21 | 22 | 23 | ### Changed 24 | 25 | - The `RawTransaction` values returned from a call to `GetMempoolStream` 26 | now report a `Height` value of `0`, in order to be consistent with 27 | the results of calls to `GetTransaction`. See the documentation of 28 | `RawTransaction` in `walletrpc/service.proto` for more details on 29 | the semantics of this field. 30 | 31 | ### Fixed 32 | 33 | - GetLatestBlock should report latest block hash in little-endian 34 | format, not big-endian. 35 | 36 | - Support empty block range end in `getaddresstxids` calls. 37 | 38 | - Filter out mined transactions in `refreshMempoolTxns` 39 | 40 | - Uniformly return height 0 for mempool `RawTransaction` results. 41 | 42 | - Reduce lightwalletd startup time. 43 | 44 | - Parsing of `getrawtransaction` results is now platform-independent. 45 | Previously, values of `-1` returned for the transaction height would 46 | be converted to different `RawTransaction.Height` values depending 47 | upon whether `lightwalletd` was being run on a 32-bit or 64-bit 48 | platform. 49 | 50 | ## [Prior Releases] 51 | 52 | This changelog was not created until after the release of v0.4.17 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer (see below). All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | If you wish to contact specific maintainers directly, the following have made 45 | themselves available for conduct issues: 46 | 47 | - Marshall Gaucher (marshall@z.cash) 48 | - Larry Ruane (larry@z.cash) 49 | 50 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 51 | version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html 52 | 53 | [homepage]: https://www.contributor-covenant.org 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development Workflow 2 | This document describes the standard workflows and terminology for developers at Zcash. It is intended to provide procedures that will allow users to contribute to the open-source code base. Below are common workflows users will encounter: 3 | 4 | 1. Fork lightwalletd Repository 5 | 2. Create Branch 6 | 3. Make & Commit Changes 7 | 4. Create Pull Request 8 | 5. Discuss / Review PR 9 | 6. Deploy / Merge PR 10 | 11 | Before continuing, please ensure you have an existing GitHub or GitLab account. If not, visit [GitHub](https://github.com) or [GitLab](https://gitlab.com) to create an account. 12 | 13 | ## Fork Repository 14 | This step assumes you are starting with a new GitHub/GitLab environment. If you have already forked the lightwalletd repository, please continue to [Create Branch] section. Otherwise, open up a terminal and issue the below commands: 15 | 16 | Note: Please replace `your_username`, with your actual GitHub username 17 | 18 | ```bash 19 | git clone git@github.com:your_username/lightwalletd.git 20 | cd lightwalletd 21 | git remote set-url origin git@github.com:your_username/lightwalletd.git 22 | git remote add upstream git@github.com:zcash/lightwalletd.git 23 | git remote set-url --push upstream DISABLED 24 | git fetch upstream 25 | git branch -u upstream/master master 26 | ``` 27 | After issuing the above commands, your `.git/config` file should look similar to the following: 28 | 29 | ```bash 30 | [core] 31 | repositoryformatversion = 0 32 | filemode = true 33 | bare = false 34 | logallrefupdates = true 35 | [remote "origin"] 36 | url = git@github.com:your_username/lightwalletd.git 37 | fetch = +refs/heads/*:refs/remotes/origin/* 38 | [branch "master"] 39 | remote = upstream 40 | merge = refs/heads/master 41 | [remote "upstream"] 42 | url = git@github.com:zcash/lightalletd.git 43 | fetch = +refs/heads/*:refs/remotes/upstream/* 44 | pushurl = DISABLED 45 | ``` 46 | This setup provides a single cloned environment to develop for lightwalletd. There are alternative methods using multiple clones, but this document does not cover that process. 47 | 48 | ## Create Branch 49 | While working on the lightwalletd project, you are going to have bugs, features, and ideas to work on. Branching exists to aid these different tasks while you write code. Below are some conventions of branching at Zcash: 50 | 51 | 1. `master` branch is **ALWAYS** deployable 52 | 2. Branch names **MUST** be descriptive: 53 | * General format: `issue#_short_description` 54 | 55 | To create a new branch (assuming you are in `lightwalletd` directory): 56 | 57 | ```bash 58 | git checkout -b [new_branch_name] 59 | ``` 60 | Note: Even though you have created a new branch, until you `git push` this local branch, it will not show up in your lightwalletd fork on GitHub (e.g. https://github.com/your_username/lightwalletd) 61 | 62 | To checkout an existing branch (assuming you are in `lightwalletd` directory): 63 | 64 | ```bash 65 | git checkout [existing_branch_name] 66 | ``` 67 | If you are fixing a bug or implementing a new feature, you likely will want to create a new branch. If you are reviewing code or working on existing branches, you likely will checkout an existing branch. To view the list of current lightwalletd GitHub issues, click [here](https://github.com/zcash/lightwalletd/issues). 68 | 69 | ## Make & Commit Changes 70 | If you have created a new branch or checked out an existing one, it is time to make changes to your local source code. Below are some formalities for commits: 71 | 72 | 1. Commit messages **MUST** be clear 73 | 2. Commit messages **MUST** be descriptive 74 | 3. Commit messages **MUST** be clean (see squashing commits for details) 75 | 76 | While continuing to do development on a branch, keep in mind that other approved commits are getting merged into `master`. In order to ensure there are minimal to no merge conflicts, we need `rebase` with master. 77 | 78 | If you are new to this process, please sanity check your remotes: 79 | 80 | ``` 81 | git remote -v 82 | ``` 83 | ```bash 84 | origin git@github.com:your_username/lightwalletd.git (fetch) 85 | origin git@github.com:your_username/lightwalletd.git (push) 86 | upstream git@github.com:zcash/lightwalletd.git (fetch) 87 | upstream DISABLED (push) 88 | ``` 89 | This output should be consistent with your `.git/config`: 90 | 91 | ```bash 92 | [branch "master"] 93 | remote = upstream 94 | merge = refs/heads/master 95 | [remote "origin"] 96 | url = git@github.com:your_username/lightwalletd.git 97 | fetch = +refs/heads/*:refs/remotes/origin/* 98 | [remote "upstream"] 99 | url = git@github.com:zcash/lightwalletd.git 100 | fetch = +refs/heads/*:refs/remotes/upstream/* 101 | pushurl = DISABLED 102 | ``` 103 | Once you have confirmed your branch/remote is valid, issue the following commands (assumes you have **NO** existing uncommitted changes): 104 | 105 | ```bash 106 | git fetch upstream 107 | git rebase upstream/master 108 | git push -f 109 | ``` 110 | If you have uncommitted changes, use `git stash` to preserve them: 111 | 112 | ```bash 113 | git stash 114 | git fetch upstream 115 | git rebase upstream/master 116 | git push -f 117 | git stash pop 118 | ``` 119 | Using `git stash` allows you to temporarily store your changes while you rebase with `master`. Without this, you will rebase with master and lose your local changes. 120 | 121 | Before committing changes, ensure your commit messages follow these guidelines: 122 | 123 | 1. Separate subject from body with a blank line 124 | 2. Limit the subject line to 50 characters 125 | 3. Capitalize the subject line 126 | 4. Do not end the subject line with a period 127 | 5. Wrap the body at 72 characters 128 | 6. Use the body to explain *what* and *why* vs. *how* 129 | 130 | Once synced with `master`, let's commit our changes: 131 | 132 | ```bash 133 | git add [files...] # default is all files, be careful not to add unintended files 134 | git commit -m 'Message describing commit' 135 | git push 136 | ``` 137 | Now that all the files changed have been committed, let's continue to Create Pull Request section. 138 | 139 | ## Create Pull Request 140 | On your GitHub page (e.g. https://github.com/your_username/lightwalletd), you will notice a newly created banner containing your recent commit with a big green `Compare & pull request`. Click on it. 141 | 142 | First, write a brief summary comment for your PR -- this first comment should be no more than a few lines because it ends up in the merge commit message. This comment should mention the issue number preceded by a hash symbol (for example, #2984). 143 | 144 | Add a second comment if more explanation is needed. It's important to explain why this pull request should be accepted. State whether the proposed change fixes part of the problem or all of it; if the change is temporary (a workaround) or permanent; if the problem also exists upstream (Bitcoin) and, if so, if and how it was fixed there. 145 | 146 | If you click on `Commits`, you should see the diff of that commit; it's advisable to verify it's what you expect. You can also click on the small plus signs that appear when you hover over the lines on either the left or right side and add a comment specific to that part of the code. This is very helpful, as you don't have to tell the reviewers (in a general comment) that you're referring to a certain line in a certain file. 147 | 148 | Add comments **before** adding reviewers, otherwise they will get a separate email for each comment you add. Once you're happy with the documentation you've added to your PR, select reviewers along the right side. For a trivial change (like the example here), one reviewer is enough, but generally you should have at least two reviewers, at least one of whom should be experienced. It may be good to add one less experienced engineer as a learning experience for that person. 149 | 150 | ## Discuss / Review PR 151 | In order to merge your PR with `master`, you will need to convince the reviewers of the intentions of your code. 152 | 153 | **IMPORTANT:** If your PR introduces code that does not have existing tests to ensure it operates gracefully, you **MUST** also create these tests to accompany your PR. 154 | 155 | Reviewers will investigate your PR and provide feedback. Generally the comments are explicitly requesting code changes or clarifying implementations. Otherwise Reviewers will reply with PR terminology: 156 | 157 | > **Concept ACK** - Agree with the idea and overall direction, but have neither reviewed nor tested the code changes. 158 | 159 | > **utACK (untested ACK)**- Reviewed and agree with the code changes but haven't actually tested them. 160 | 161 | > **Tested ACK** - Reviewed the code changes and have verified the functionality or bug fix. 162 | 163 | > **ACK** - A loose ACK can be confusing. It's best to avoid them unless it's a documentation/comment only change in which case there is nothing to test/verify; therefore the tested/untested distinction is not there. 164 | 165 | > **NACK** - Disagree with the code changes/concept. Should be accompanied by an explanation. 166 | 167 | ### Squashing Commits 168 | Before your PR is accepted, you might be requested to squash your commits to clean up the logs. This can be done using the following approach: 169 | 170 | ```bash 171 | git checkout branch_name 172 | git rebase -i HEAD~4 173 | ``` 174 | The integer value after `~` represents the number of commits you would like to interactively rebase. You can pick a value that makes sense for your situation. A template will pop-up in your terminal requesting you to specify what commands you would like to do with each prior commit: 175 | 176 | ```bash 177 | Commands: 178 | p, pick = use commit 179 | r, reword = use commit, but edit the commit message 180 | e, edit = use commit, but stop for amending 181 | s, squash = use commit, but meld into previous commit 182 | f, fixup = like "squash", but discard this commit's log message 183 | x, exec = run command (the rest of the line) using shell 184 | ``` 185 | Modify each line with the according command, followed by the hash of the commit. For example, if I wanted to squash my last 4 commits into the most recent commit for this PR: 186 | 187 | ```bash 188 | p 1fc6c95 Final commit message 189 | s 6b2481b Third commit message 190 | s dd1475d Second commit message 191 | s c619268 First commit message 192 | ``` 193 | ```bash 194 | git push origin branch-name --force 195 | ``` 196 | 197 | ## Deploy / Merge PR 198 | Once your PR/MR has been properly reviewed, it will be ran in the build pipeline to ensure it is valid to merge with master. 199 | 200 | Sometimes there will be times when your PR is waiting for some portion of the above process. If you are requested to rebase your PR, in order to gracefully merge into `master`, please do the following: 201 | 202 | ```bash 203 | git checkout branch_name 204 | git fetch upstream 205 | git rebase upstream/master 206 | git push -f 207 | ``` 208 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 The Zcash developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | The MIT software license (https://www.opensource.org/licenses/mit-license.php) 23 | above applies to the code directly included in this source distribution, with 24 | the exception of certain Autoconf macros. Dependencies downloaded as part of 25 | the build process may be covered by other open-source licenses. The MIT-licensed 26 | source code is not considered a derived work of these Autoconf macros or of the 27 | dependencies. For further details see 'contrib/debian/copyright'. 28 | 29 | 30 | This product includes software developed by the OpenSSL Project for use in the 31 | OpenSSL Toolkit (https://www.openssl.org/). This product includes cryptographic 32 | software written by Eric Young (eay@cryptsoft.com). 33 | 34 | 35 | Although almost all of the Zcash code is licensed under "permissive" open source 36 | licenses, users and distributors should note that when built using the default 37 | build options, Zcash depends on Oracle Berkeley DB 6.2.x, which is licensed 38 | under the GNU Affero General Public License. 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 AS lightwalletd_base 2 | 3 | ADD . /go/src/github.com/zcash/lightwalletd 4 | WORKDIR /go/src/github.com/zcash/lightwalletd 5 | 6 | RUN make \ 7 | && /usr/bin/install -c ./lightwalletd /usr/local/bin/ \ 8 | && mkdir -p /var/lib/lightwalletd/db \ 9 | && chown 2002:2002 /var/lib/lightwalletd/db 10 | 11 | ARG LWD_USER=lightwalletd 12 | ARG LWD_UID=2002 13 | 14 | RUN useradd --home-dir "/srv/$LWD_USER" \ 15 | --shell /bin/bash \ 16 | --create-home \ 17 | --uid "$LWD_UID" \ 18 | "$LWD_USER" 19 | 20 | WORKDIR "/srv/$LWD_USER" 21 | 22 | ENTRYPOINT ["lightwalletd"] 23 | CMD ["--help"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Electric Coin Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # /************************************************************************ 2 | # File: Makefile 3 | # Author: mdr0id 4 | # Date: 7/16/2019 5 | # Description: Used for local and container dev in CI deployments 6 | # Usage: make 7 | # 8 | # Copyright (c) 2020 The Zcash developers 9 | # Distributed under the MIT software license, see the accompanying 10 | # file COPYING or https://www.opensource.org/licenses/mit-license.php . 11 | # 12 | # Known bugs/missing features: 13 | # 1. make msan is not stable as of 9/20/2019 14 | # 15 | # ************************************************************************/ 16 | PROJECT_NAME := "lightwalletd" 17 | GO_TEST_FILES := $(shell find . -name '*_test.go' -type f | rev | cut -d "/" -f2- | rev | sort -u) 18 | 19 | VERSION := `git describe --tags` 20 | GITCOMMIT := `git rev-parse HEAD` 21 | BUILDDATE := `date +%Y-%m-%d` 22 | BUILDUSER := `whoami` 23 | LDFLAGSSTRING :=-X github.com/zcash/lightwalletd/common.Version=$(VERSION) 24 | LDFLAGSSTRING +=-X github.com/zcash/lightwalletd/common.GitCommit=$(GITCOMMIT) 25 | LDFLAGSSTRING +=-X github.com/zcash/lightwalletd/common.Branch=$(BRANCH) 26 | LDFLAGSSTRING +=-X github.com/zcash/lightwalletd/common.BuildDate=$(BUILDDATE) 27 | LDFLAGSSTRING +=-X github.com/zcash/lightwalletd/common.BuildUser=$(BUILDUSER) 28 | LDFLAGS :=-ldflags "$(LDFLAGSSTRING)" 29 | 30 | # There are some files that are generated but are also in source control 31 | # (so that the average clone - build doesn't need the required tools) 32 | GENERATED_FILES := docs/rtd/index.html walletrpc/compact_formats.pb.go walletrpc/service.pb.go walletrpc/darkside.pb.go 33 | 34 | PWD := $(shell pwd) 35 | 36 | .PHONY: all dep build clean test coverage lint doc simpledoc proto 37 | 38 | all: first-make-timestamp build $(GENERATED_FILES) 39 | 40 | # Ensure that the generated files that are also in git source control are 41 | # initially more recent than the files they're generated from (so we don't try 42 | # to rebuild them); this isn't perfect because it depends on doing a make before 43 | # editing a .proto file; also, "make -jn" may trigger remake if n > 1. 44 | first-make-timestamp: 45 | touch $(GENERATED_FILES) $@ 46 | 47 | # Lint golang files 48 | lint: 49 | golint -set_exit_status 50 | 51 | show_tests: 52 | @echo ${GO_TEST_FILES} 53 | 54 | # Run unittests 55 | test: 56 | go test -v ./... 57 | 58 | # Run data race detector 59 | race: 60 | go test -v -race -short ./... 61 | 62 | # Run memory sanitizer (need to ensure proper build flag is set) 63 | msan: 64 | go test -v -msan -short ${GO_TEST_FILES} 65 | 66 | # Generate global code coverage report, ignore generated *.pb.go files 67 | 68 | coverage: 69 | go test -coverprofile=coverage.out ./... 70 | sed -i '/\.pb\.go/d' coverage.out 71 | 72 | # Generate code coverage report 73 | coverage_report: coverage 74 | go tool cover -func=coverage.out 75 | 76 | # Generate code coverage report in HTML 77 | coverage_html: coverage 78 | go tool cover -html=coverage.out 79 | 80 | # Generate documents, requires docker, see https://github.com/pseudomuto/protoc-gen-doc 81 | doc: docs/rtd/index.html 82 | 83 | docs/rtd/index.html: walletrpc/compact_formats.proto walletrpc/service.proto walletrpc/darkside.proto 84 | docker run --rm -v $(PWD)/docs/rtd:/out -v $(PWD)/walletrpc:/protos pseudomuto/protoc-gen-doc 85 | 86 | proto: walletrpc/service.pb.go walletrpc/darkside.pb.go walletrpc/compact_formats.pb.go 87 | 88 | walletrpc/service.pb.go: walletrpc/service.proto 89 | cd walletrpc && protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative service.proto 90 | 91 | walletrpc/darkside.pb.go: walletrpc/darkside.proto 92 | cd walletrpc && protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative darkside.proto 93 | 94 | walletrpc/compact_formats.pb.go: walletrpc/compact_formats.proto 95 | cd walletrpc && protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative compact_formats.proto 96 | 97 | # Generate documents using a very simple wrap-in-html approach (not ideal) 98 | simpledoc: lwd-api.html 99 | 100 | lwd-api.html: walletrpc/compact_formats.proto walletrpc/service.proto 101 | ./docgen.sh $^ >lwd-api.html 102 | 103 | # Generate docker image 104 | docker_img: 105 | docker build -t zcash_lwd_base . 106 | 107 | # Run the above docker image in a container 108 | docker_img_run: 109 | docker run -i --name zcashdlwd zcash_lwd_base 110 | 111 | # Execute a bash process on zcashdlwdcontainer 112 | docker_img_bash: 113 | docker exec -it zcashdlwd bash 114 | 115 | # Start the zcashd process in the zcashdlwd container 116 | docker_img_run_zcashd: 117 | docker exec -i zcashdlwd zcashd -printtoconsole 118 | 119 | # Stop the zcashd process in the zcashdlwd container 120 | docker_img_stop_zcashd: 121 | docker exec -i zcashdlwd zcash-cli stop 122 | 123 | # Start the lightwalletd server in the zcashdlwd container 124 | docker_img_run_lightwalletd_insecure_server: 125 | docker exec -i zcashdlwd server --no-tls-very-insecure=true --conf-file /home/zcash/.zcash/zcash.conf --log-file /logs/server.log --bind-addr 127.0.0.1:18232 126 | 127 | # Remove and delete ALL images and containers in Docker; assumes containers are stopped 128 | docker_remove_all: 129 | docker system prune -f 130 | 131 | # Get dependencies 132 | dep: 133 | @go get -v -d ./... 134 | 135 | # Build binary 136 | build: 137 | go build $(LDFLAGS) 138 | 139 | build_rel: 140 | go build $(LDFLAGS) 141 | 142 | # Install binaries into Go path 143 | install: 144 | go install ./... 145 | 146 | # Update your protoc, protobufs, grpc, .pb.go files 147 | update-grpc: 148 | go get -u google.golang.org/protobuf 149 | go get -u google.golang.org/grpc 150 | cd walletrpc && protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative service.proto 151 | cd walletrpc && protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative darkside.proto 152 | cd walletrpc && protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative compact_formats.proto 153 | go mod tidy 154 | 155 | clean: 156 | @echo "clean project..." 157 | #rm -f $(PROJECT_NAME) 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![pipeline status](https://gitlab.com/zcash/lightwalletd/badges/master/pipeline.svg)](https://gitlab.com/zcash/lightwalletd/commits/master) 3 | [![codecov](https://codecov.io/gh/zcash/lightwalletd/branch/master/graph/badge.svg)](https://codecov.io/gh/zcash/lightwalletd) 4 | 5 | # Security Disclaimer 6 | 7 | lightwalletd is under active development, some features are more stable than 8 | others. The code has not been subjected to a thorough review by an external 9 | auditor, and recent code changes have not yet received security review from 10 | Electric Coin Company's security team. 11 | 12 | Developers should familiarize themselves with the [wallet app threat 13 | model](https://zcash.readthedocs.io/en/latest/rtd_pages/wallet_threat_model.html), 14 | since it contains important information about the security and privacy 15 | limitations of light wallets that use lightwalletd. 16 | 17 | --- 18 | 19 | # Overview 20 | 21 | [lightwalletd](https://github.com/zcash/lightwalletd) is a backend service that provides a bandwidth-efficient interface to the Zcash blockchain. Currently, lightwalletd supports the Sapling protocol version and beyond as its primary concern. The intended purpose of lightwalletd is to support the development and operation of mobile-friendly shielded light wallets. 22 | 23 | lightwalletd is a backend service that provides a bandwidth-efficient interface to the Zcash blockchain for mobile and other wallets, such as [Zashi](https://github.com/Electric-Coin-Company/zashi-android) and [Ywallet](https://github.com/hhanh00/zwallet). 24 | 25 | To view status of [CI pipeline](https://gitlab.com/zcash/lightwalletd/pipelines) 26 | 27 | To view detailed [Codecov](https://codecov.io/gh/zcash/lightwalletd) report 28 | 29 | Documentation for lightwalletd clients (the gRPC interface) is in `docs/rtd/index.html`. The current version of this file corresponds to the two `.proto` files; if you change these files, please regenerate the documentation by running `make doc`, which requires docker to be installed. 30 | # Local/Developer docker-compose Usage 31 | 32 | [docs/docker-compose-setup.md](./docs/docker-compose-setup.md) 33 | 34 | # Local/Developer Usage 35 | 36 | ## Zcashd 37 | 38 | You must start a local instance of `zcashd`, and its `.zcash/zcash.conf` file must include the following entries 39 | (set the user and password strings accordingly): 40 | ``` 41 | txindex=1 42 | lightwalletd=1 43 | experimentalfeatures=1 44 | rpcuser=xxxxx 45 | rpcpassword=xxxxx 46 | ``` 47 | 48 | The `zcashd` can be configured to run `mainnet` or `testnet` (or `regtest`). If you stop `zcashd` and restart it on a different network (switch from `testnet` to `mainnet`, for example), you must also stop and restart lightwalletd. 49 | 50 | It's necessary to run `zcashd --reindex` one time for these options to take effect. This typically takes several hours, and requires more space in the `.zcash` data directory. 51 | 52 | Lightwalletd uses the following `zcashd` RPCs: 53 | - `getinfo` 54 | - `getblockchaininfo` 55 | - `getbestblockhash` 56 | - `z_gettreestate` 57 | - `getblock` 58 | - `getrawtransaction` 59 | - `sendrawtransaction` 60 | - `getrawmempool` 61 | - `getaddresstxids` 62 | - `getaddressbalance` 63 | - `getaddressutxos` 64 | 65 | ## Lightwalletd 66 | 67 | First, install [Go](https://golang.org/dl/#stable) version 1.17 or later. You can see your current version by running `go version`. 68 | 69 | Clone the [current repository](https://github.com/zcash/lightwalletd) into a local directory that is _not_ within any component of 70 | your `$GOPATH` (`$HOME/go` by default), then build the lightwalletd server binary by running `make`. 71 | 72 | ## To run SERVER 73 | 74 | Assuming you used `make` to build the server, here's a typical developer invocation: 75 | 76 | ``` 77 | ./lightwalletd --no-tls-very-insecure --zcash-conf-path ~/.zcash/zcash.conf --data-dir . --log-file /dev/stdout 78 | ``` 79 | Type `./lightwalletd help` to see the full list of options and arguments. 80 | 81 | # Production Usage 82 | 83 | Run a local instance of `zcashd` (see above), except do _not_ specify `--no-tls-very-insecure`. 84 | Ensure [Go](https://golang.org/dl/#stable) version 1.17 or later is installed. 85 | 86 | **x509 Certificates** 87 | You will need to supply an x509 certificate that connecting clients will have good reason to trust (hint: do not use a self-signed one, our SDK will reject those unless you distribute them to the client out-of-band). We suggest that you be sure to buy a reputable one from a supplier that uses a modern hashing algorithm (NOT md5 or sha1) and that uses Certificate Transparency (OID 1.3.6.1.4.1.11129.2.4.2 will be present in the certificate). 88 | 89 | To check a given certificate's (cert.pem) hashing algorithm: 90 | ``` 91 | openssl x509 -text -in certificate.crt | grep "Signature Algorithm" 92 | ``` 93 | 94 | To check if a given certificate (cert.pem) contains a Certificate Transparency OID: 95 | ``` 96 | echo "1.3.6.1.4.1.11129.2.4.2 certTransparency Certificate Transparency" > oid.txt 97 | openssl asn1parse -in cert.pem -oid ./oid.txt | grep 'Certificate Transparency' 98 | ``` 99 | 100 | To use Let's Encrypt to generate a free certificate for your frontend, one method is to: 101 | 1) Install certbot 102 | 2) Open port 80 to your host 103 | 3) Point some forward dns to that host (some.forward.dns.com) 104 | 4) Run 105 | ``` 106 | certbot certonly --standalone --preferred-challenges http -d some.forward.dns.com 107 | ``` 108 | 5) Pass the resulting certificate and key to frontend using the -tls-cert and -tls-key options. 109 | 110 | ## To run production SERVER 111 | 112 | Example using server binary built from Makefile: 113 | 114 | ``` 115 | ./lightwalletd --tls-cert cert.pem --tls-key key.pem --zcash-conf-path /home/zcash/.zcash/zcash.conf --log-file /logs/server.log 116 | ``` 117 | 118 | ## Block cache 119 | 120 | lightwalletd caches all blocks from Sapling activation up to the 121 | most recent block, which takes about an hour the first time you run 122 | lightwalletd. During this syncing, lightwalletd is fully available, 123 | but block fetches are slower until the download completes. 124 | 125 | After syncing, lightwalletd will start almost immediately, 126 | because the blocks are cached in local files (by default, within 127 | `/var/lib/lightwalletd/db`; you can specify a different location using 128 | the `--data-dir` command-line option). 129 | 130 | lightwalletd checks the consistency of these files at startup and during 131 | operation as these files may be damaged by, for example, an unclean shutdown. 132 | If the server detects corruption, it will automatically re-downloading blocks 133 | from `zcashd` from that height, requiring up to an hour again (no manual 134 | intervention is required). But this should occur rarely. 135 | 136 | If lightwalletd detects corruption in these cache files, it will log 137 | a message containing the string `CORRUPTION` and also indicate the 138 | nature of the corruption. 139 | 140 | ## Darksidewalletd & Testing 141 | 142 | lightwalletd now supports a mode that enables integration testing of itself and 143 | wallets that connect to it. See the [darksidewalletd 144 | docs](docs/darksidewalletd.md) for more information. 145 | 146 | # Pull Requests 147 | 148 | We welcome pull requests! We like to keep our Go code neatly formatted in a standard way, 149 | which the standard tool [gofmt](https://golang.org/cmd/gofmt/) can do. Please consider 150 | adding the following to the file `.git/hooks/pre-commit` in your clone: 151 | 152 | ``` 153 | #!/bin/sh 154 | 155 | modified_go_files=$(git diff --cached --name-only -- '*.go') 156 | if test "$modified_go_files" 157 | then 158 | need_formatting=$(gofmt -l $modified_go_files) 159 | if test "$need_formatting" 160 | then 161 | echo files need formatting (then don't forget to git add): 162 | echo gofmt -w $need_formatting 163 | exit 1 164 | fi 165 | fi 166 | ``` 167 | 168 | You'll also need to make this file executable: 169 | 170 | ``` 171 | $ chmod +x .git/hooks/pre-commit 172 | ``` 173 | 174 | Doing this will prevent commits that break the standard formatting. Simply run the 175 | `gofmt` command as indicated and rerun the `git add` and `git commit` commands. 176 | -------------------------------------------------------------------------------- /buildenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in "$@" 4 | do 5 | case $i in 6 | -h|--help) 7 | echo HELP 8 | exit 0 9 | ;; 10 | -n=*|--network=*) 11 | NETWORK="${i#*=}" 12 | shift 13 | ;; 14 | *) 15 | echo Unknown option. Use -h for help. 16 | exit -1 17 | ;; 18 | esac 19 | done 20 | 21 | if [ "$NETWORK" == "" ] 22 | then 23 | echo ZCASHD_NETWORK=testnet 24 | else 25 | echo ZCASHD_NETWORK=$NETWORK 26 | fi 27 | 28 | # sanity check openssl first... 29 | 30 | if [ `openssl rand -base64 32 | wc -c` != 45 ] 31 | then 32 | echo Openssl password generation failed. 33 | exit 1 34 | fi 35 | 36 | PASSWORD_GRAFANA=`openssl rand -base64 32` 37 | PASSWORD_ZCASHD=`openssl rand -base64 32` 38 | 39 | while read TEMPLATE 40 | do 41 | eval echo $TEMPLATE 42 | done < .env.template 43 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package cmd 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestFileExists(t *testing.T) { 11 | if fileExists("nonexistent-file") { 12 | t.Fatal("fileExists unexpected success") 13 | } 14 | // If the path exists but is a directory, should return false 15 | if fileExists(".") { 16 | t.Fatal("fileExists unexpected success") 17 | } 18 | // The following file should exist, it's what's being tested 19 | if !fileExists("root.go") { 20 | t.Fatal("fileExists failed") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/zcash/lightwalletd/common" 8 | ) 9 | 10 | // versionCmd represents the version command 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Display lightwalletd version", 14 | Long: `Display lightwalletd version.`, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Println("lightwalletd version: ", common.Version) 17 | fmt.Println("from commit: ", common.GitCommit) 18 | fmt.Println("on: ", common.BuildDate) 19 | fmt.Println("by: ", common.BuildUser) 20 | 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /common/cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package common 5 | 6 | import ( 7 | "encoding/hex" 8 | "encoding/json" 9 | "os" 10 | "testing" 11 | 12 | "github.com/zcash/lightwalletd/parser" 13 | "github.com/zcash/lightwalletd/walletrpc" 14 | ) 15 | 16 | var compacts []*walletrpc.CompactBlock 17 | var cache *BlockCache 18 | 19 | const ( 20 | unitTestPath = "unittestcache" 21 | unitTestChain = "unittestnet" 22 | ) 23 | 24 | func TestCache(t *testing.T) { 25 | type compactTest struct { 26 | BlockHeight int `json:"block"` 27 | BlockHash string `json:"hash"` 28 | PrevHash string `json:"prev"` 29 | Full string `json:"full"` 30 | Compact string `json:"compact"` 31 | } 32 | var compactTests []compactTest 33 | 34 | blockJSON, err := os.ReadFile("../testdata/compact_blocks.json") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | err = json.Unmarshal(blockJSON, &compactTests) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | // Derive compact blocks from file data (setup, not part of the test). 45 | for _, test := range compactTests { 46 | blockData, _ := hex.DecodeString(test.Full) 47 | block := parser.NewBlock() 48 | blockData, err = block.ParseFromSlice(blockData) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | if len(blockData) > 0 { 53 | t.Error("Extra data remaining") 54 | } 55 | compacts = append(compacts, block.ToCompact()) 56 | } 57 | 58 | // Pretend Sapling starts at 289460. 59 | os.RemoveAll(unitTestPath) 60 | cache = NewBlockCache(unitTestPath, unitTestChain, 289460, 0) 61 | 62 | // Initially cache is empty. 63 | if cache.GetLatestHeight() != -1 { 64 | t.Fatal("unexpected GetLatestHeight") 65 | } 66 | if cache.firstBlock != 289460 { 67 | t.Fatal("unexpected initial firstBlock") 68 | } 69 | if cache.nextBlock != 289460 { 70 | t.Fatal("unexpected initial nextBlock") 71 | } 72 | fillCache(t) 73 | reorgCache(t) 74 | fillCache(t) 75 | 76 | // Simulate a restart to ensure the db files are read correctly. 77 | cache = NewBlockCache(unitTestPath, unitTestChain, 289460, -1) 78 | 79 | // Should still be 6 blocks. 80 | if cache.nextBlock != 289466 { 81 | t.Fatal("unexpected nextBlock height") 82 | } 83 | reorgCache(t) 84 | 85 | // Reorg to before the first block moves back to only the first block 86 | cache.Reorg(289459) 87 | if cache.latestHash != nil { 88 | t.Fatal("unexpected latestHash, should be nil") 89 | } 90 | if cache.nextBlock != 289460 { 91 | t.Fatal("unexpected nextBlock: ", cache.nextBlock) 92 | } 93 | 94 | // Clean up the test files. 95 | cache.Close() 96 | os.RemoveAll(unitTestPath) 97 | } 98 | 99 | func reorgCache(t *testing.T) { 100 | // Simulate a reorg by adding a block whose height is lower than the latest; 101 | // we're replacing the second block, so there should be only two blocks. 102 | cache.Reorg(289461) 103 | err := cache.Add(289461, compacts[1]) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | if cache.firstBlock != 289460 { 108 | t.Fatal("unexpected firstBlock height") 109 | } 110 | if cache.nextBlock != 289462 { 111 | t.Fatal("unexpected nextBlock height") 112 | } 113 | if len(cache.starts) != 3 { 114 | t.Fatal("unexpected len(cache.starts)") 115 | } 116 | 117 | // some "black-box" tests (using exported interfaces) 118 | if cache.GetLatestHeight() != 289461 { 119 | t.Fatal("unexpected GetLatestHeight") 120 | } 121 | if int(cache.Get(289461).Height) != 289461 { 122 | t.Fatal("unexpected block contents") 123 | } 124 | 125 | // Make sure we can go forward from here 126 | err = cache.Add(289462, compacts[2]) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | if cache.firstBlock != 289460 { 131 | t.Fatal("unexpected firstBlock height") 132 | } 133 | if cache.nextBlock != 289463 { 134 | t.Fatal("unexpected nextBlock height") 135 | } 136 | if len(cache.starts) != 4 { 137 | t.Fatal("unexpected len(cache.starts)") 138 | } 139 | 140 | if cache.GetLatestHeight() != 289462 { 141 | t.Fatal("unexpected GetLatestHeight") 142 | } 143 | if int(cache.Get(289462).Height) != 289462 { 144 | t.Fatal("unexpected block contents") 145 | } 146 | } 147 | 148 | // Whatever the state of the cache, add 6 blocks starting at the 149 | // pretend Sapling height, 289460 (this could cause a reorg). 150 | func fillCache(t *testing.T) { 151 | next := 289460 152 | cache.Reorg(next) 153 | for i, compact := range compacts { 154 | err := cache.Add(next, compact) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | next++ 159 | 160 | // some "white-box" checks 161 | if cache.firstBlock != 289460 { 162 | t.Fatal("unexpected firstBlock height") 163 | } 164 | if cache.nextBlock != 289460+i+1 { 165 | t.Fatal("unexpected nextBlock height") 166 | } 167 | if len(cache.starts) != i+2 { 168 | t.Fatal("unexpected len(cache.starts)") 169 | } 170 | 171 | // some "black-box" tests (using exported interfaces) 172 | if cache.GetLatestHeight() != 289460+i { 173 | t.Fatal("unexpected GetLatestHeight") 174 | } 175 | b := cache.Get(289460 + i) 176 | if b == nil { 177 | t.Fatal("unexpected Get failure") 178 | } 179 | if int(b.Height) != 289460+i { 180 | t.Fatal("unexpected block contents") 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /common/generatecerts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package common 5 | 6 | import ( 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/tls" 10 | "crypto/x509" 11 | "crypto/x509/pkix" 12 | "encoding/pem" 13 | "math/big" 14 | "time" 15 | ) 16 | 17 | // GenerateCerts create self signed certificate for local development use 18 | // (and, if using grpcurl, specify the -insecure argument option) 19 | func GenerateCerts() *tls.Certificate { 20 | 21 | privKey, err := rsa.GenerateKey(rand.Reader, 2048) 22 | if err != nil { 23 | Log.Fatal("Failed to generate key") 24 | } 25 | publicKey := &privKey.PublicKey 26 | 27 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 28 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 29 | if err != nil { 30 | Log.Fatal("Failed to generate serial number:", err) 31 | } 32 | 33 | template := x509.Certificate{ 34 | SerialNumber: serialNumber, 35 | Subject: pkix.Name{ 36 | Organization: []string{"Lighwalletd developer"}, 37 | }, 38 | NotBefore: time.Now(), 39 | NotAfter: time.Now().Local().Add(time.Hour * 24 * 365), 40 | 41 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 42 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 43 | BasicConstraintsValid: true, 44 | } 45 | 46 | // List of hostnames and IPs for the cert 47 | template.DNSNames = append(template.DNSNames, "localhost") 48 | 49 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privKey) 50 | if err != nil { 51 | Log.Fatal("Failed to create certificate:", err) 52 | } 53 | 54 | // PEM encode the certificate (this is a standard TLS encoding) 55 | b := pem.Block{Type: "CERTIFICATE", Bytes: certDER} 56 | certPEM := pem.EncodeToMemory(&b) 57 | 58 | // PEM encode the private key 59 | privBytes, err := x509.MarshalPKCS8PrivateKey(privKey) 60 | if err != nil { 61 | Log.Fatal("Unable to marshal private key:", err) 62 | } 63 | keyPEM := pem.EncodeToMemory(&pem.Block{ 64 | Type: "RSA PRIVATE KEY", Bytes: privBytes, 65 | }) 66 | 67 | // Create a TLS cert using the private key and certificate 68 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 69 | if err != nil { 70 | Log.Fatal("invalid key pair:", err) 71 | } 72 | 73 | return &tlsCert 74 | } 75 | -------------------------------------------------------------------------------- /common/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/peer" 10 | ) 11 | 12 | var LogToStderr bool 13 | 14 | func LoggingInterceptor() grpc.ServerOption { 15 | return grpc.UnaryInterceptor(LogInterceptor) 16 | } 17 | 18 | func loggerFromContext(ctx context.Context) *logrus.Entry { 19 | // TODO: anonymize the addresses. cryptopan? 20 | if peerInfo, ok := peer.FromContext(ctx); ok { 21 | return logrus.WithFields(logrus.Fields{"peer_addr": peerInfo.Addr}) 22 | } 23 | return logrus.WithFields(logrus.Fields{"peer_addr": "unknown"}) 24 | } 25 | 26 | func LogInterceptor( 27 | ctx context.Context, 28 | req interface{}, 29 | info *grpc.UnaryServerInfo, 30 | handler grpc.UnaryHandler, 31 | ) (interface{}, error) { 32 | reqLog := loggerFromContext(ctx) 33 | start := time.Now() 34 | 35 | resp, err := handler(ctx, req) 36 | 37 | if LogToStderr { 38 | entry := reqLog.WithFields(logrus.Fields{ 39 | "method": info.FullMethod, 40 | "duration": time.Since(start), 41 | "error": err, 42 | }) 43 | 44 | if err != nil { 45 | entry.Error("call failed") 46 | } else { 47 | entry.Info("method called") 48 | } 49 | } 50 | 51 | return resp, err 52 | } 53 | -------------------------------------------------------------------------------- /common/logging/logging_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package logging 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "testing" 11 | 12 | "errors" 13 | "github.com/sirupsen/logrus" 14 | "github.com/zcash/lightwalletd/common" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/peer" 17 | ) 18 | 19 | var step int 20 | 21 | func testhandler(ctx context.Context, req interface{}) (interface{}, error) { 22 | step++ 23 | switch step { 24 | case 1: 25 | return nil, errors.New("test error") 26 | case 2: 27 | return nil, nil 28 | } 29 | return nil, nil 30 | } 31 | 32 | func TestLogInterceptor(t *testing.T) { 33 | output, err := os.OpenFile("test-log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 34 | if err != nil { 35 | os.Stderr.WriteString(fmt.Sprint("Cannot open test-log:", err)) 36 | os.Exit(1) 37 | } 38 | logger := logrus.New() 39 | logger.SetOutput(output) 40 | common.Log = logger.WithFields(logrus.Fields{ 41 | "app": "test", 42 | }) 43 | var req interface{} 44 | resp, err := LogInterceptor(peer.NewContext(context.Background(), &peer.Peer{}), 45 | &req, &grpc.UnaryServerInfo{}, testhandler) 46 | if err == nil { 47 | t.Fatal("unexpected success") 48 | } 49 | if resp != nil { 50 | t.Fatal("unexpected response", resp) 51 | } 52 | resp, err = LogInterceptor(context.Background(), &req, &grpc.UnaryServerInfo{}, testhandler) 53 | if err != nil { 54 | t.Fatal("unexpected error", err) 55 | } 56 | if resp != nil { 57 | t.Fatal("unexpected response", resp) 58 | } 59 | os.Remove("test-log") 60 | step = 0 61 | } 62 | -------------------------------------------------------------------------------- /common/mempool.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "time" 7 | 8 | "github.com/zcash/lightwalletd/walletrpc" 9 | ) 10 | 11 | type txid string 12 | 13 | var ( 14 | // Set of mempool txids that have been seen during the current block interval. 15 | // The zcashd RPC `getrawmempool` returns the entire mempool each time, so 16 | // this allows us to ignore the txids that we've already seen. 17 | g_txidSeen map[txid]struct{} = map[txid]struct{}{} 18 | 19 | // List of transactions during current block interval, in order received. Each 20 | // client thread can keep an index into this slice to record which transactions 21 | // it's sent back to the client (everything before that index). The g_txidSeen 22 | // map allows this list to not contain duplicates. 23 | g_txList []*walletrpc.RawTransaction 24 | 25 | // The most recent absolute time that we fetched the mempool and the latest 26 | // (tip) block hash (so we know when a new block has been mined). 27 | g_lastTime time.Time 28 | 29 | // The most recent zcashd getblockchaininfo reply, for height and best block 30 | // hash (tip) which is used to detect when a new block arrives. 31 | g_lastBlockChainInfo *ZcashdRpcReplyGetblockchaininfo = &ZcashdRpcReplyGetblockchaininfo{} 32 | 33 | // Mutex to protect the above variables. 34 | g_lock sync.Mutex 35 | ) 36 | 37 | func GetMempool(sendToClient func(*walletrpc.RawTransaction) error) error { 38 | g_lock.Lock() 39 | index := 0 40 | // Stay in this function until the tip block hash changes. 41 | stayHash := g_lastBlockChainInfo.BestBlockHash 42 | 43 | // Wait for more transactions to be added to the list 44 | for { 45 | // Don't fetch the mempool more often than every 2 seconds. 46 | now := Time.Now() 47 | if now.After(g_lastTime.Add(2 * time.Second)) { 48 | blockChainInfo, err := GetBlockChainInfo() 49 | if err != nil { 50 | g_lock.Unlock() 51 | return err 52 | } 53 | if g_lastBlockChainInfo.BestBlockHash != blockChainInfo.BestBlockHash { 54 | // A new block has arrived 55 | g_lastBlockChainInfo = blockChainInfo 56 | // We're the first thread to notice, clear cached state. 57 | g_txidSeen = map[txid]struct{}{} 58 | g_txList = []*walletrpc.RawTransaction{} 59 | g_lastTime = time.Time{} 60 | break 61 | } 62 | if err = refreshMempoolTxns(); err != nil { 63 | g_lock.Unlock() 64 | return err 65 | } 66 | g_lastTime = now 67 | } 68 | // Send transactions we haven't sent yet, best to not do so while 69 | // holding the mutex, since this call may get flow-controlled. 70 | toSend := g_txList[index:] 71 | index = len(g_txList) 72 | g_lock.Unlock() 73 | for _, tx := range toSend { 74 | if err := sendToClient(tx); err != nil { 75 | return err 76 | } 77 | } 78 | Time.Sleep(200 * time.Millisecond) 79 | g_lock.Lock() 80 | if g_lastBlockChainInfo.BestBlockHash != stayHash { 81 | break 82 | } 83 | } 84 | g_lock.Unlock() 85 | return nil 86 | } 87 | 88 | // RefreshMempoolTxns gets all new mempool txns and sends any new ones to waiting clients 89 | func refreshMempoolTxns() error { 90 | params := []json.RawMessage{} 91 | result, rpcErr := RawRequest("getrawmempool", params) 92 | if rpcErr != nil { 93 | return rpcErr 94 | } 95 | var mempoolList []string 96 | err := json.Unmarshal(result, &mempoolList) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // Fetch all new mempool txns and add them into `newTxns` 102 | for _, txidstr := range mempoolList { 103 | if _, ok := g_txidSeen[txid(txidstr)]; ok { 104 | // We've already fetched this transaction 105 | continue 106 | } 107 | 108 | // We haven't fetched this transaction already. 109 | g_txidSeen[txid(txidstr)] = struct{}{} 110 | txidJSON, err := json.Marshal(txidstr) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | params := []json.RawMessage{txidJSON, json.RawMessage("1")} 116 | result, rpcErr := RawRequest("getrawtransaction", params) 117 | if rpcErr != nil { 118 | // Not an error; mempool transactions can disappear 119 | continue 120 | } 121 | 122 | rawtx, err := ParseRawTransaction(result) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | // Skip any transaction that has been mined since the list of txids 128 | // was retrieved. 129 | if (rawtx.Height != 0) { 130 | continue; 131 | } 132 | 133 | g_txList = append(g_txList, rawtx) 134 | } 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /docgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # read argument files, construct simple html 4 | echo '' 5 | echo '' 6 | echo 'Lightwalletd reference API' 7 | echo '' 8 | echo '' 9 | echo '

Lightwalletd API reference

' 10 | for f 11 | do 12 | echo "

$f

" 13 | echo '
'
14 |     # list of reserved words https://developers.google.com/protocol-buffers/docs/proto3
15 |     sed <$f '
16 |         s/\/\/.*/&<\/font>/
17 |         s/\(^\|[^a-zA-Z_.]\)\(message\|service\|enum\)\($\|[^a-zA-Z_0-9]\)/\1\2<\/font>\3/
18 |         s/\(^\|[^a-zA-Z_.]\)\(rpc\|reserved\|repeated\|enum|stream\)\($\|[^a-zA-Z_0-9]\)/\1\2\3<\/font>\3/
19 |         s/\(^\|[^a-zA-Z_.]\)\(double\|float\|int32\|int64\|uint32\|uint64\|sint32\|sint64\|fixed32\|fixed64\|sfixed32\|sfixed64\|bool\|string\|bytes\)\($\|[^a-zA-Z_0-9]\)/\1\2<\/font>\3/'
20 |     echo '
' 21 | done 22 | echo '' 23 | echo '' 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | 4 | services: 5 | lightwalletd: 6 | build: . 7 | env_file: 8 | - .env 9 | #entrypoint: ["/bin/bash", "-c", "sleep infinity"] 10 | command: 11 | - --grpc-bind-addr=0.0.0.0:$LWD_GRPC_PORT 12 | - --http-bind-addr=0.0.0.0:$LWD_HTTP_PORT 13 | - --zcash-conf-path=$ZCASHD_CONF_PATH 14 | - --log-file=/dev/stdout 15 | - --log-level=7 16 | ports: 17 | - "127.0.0.1:$LWD_GRPC_PORT:$LWD_GRPC_PORT" 18 | - "127.0.0.1:$LWD_HTTP_PORT:$LWD_HTTP_PORT" 19 | volumes: 20 | - ./docker/:/srv/lightwalletd 21 | - lightwalletd_cache:/var/lib/lightwalletd 22 | logging: 23 | driver: loki 24 | options: 25 | loki-url: 'http://localhost:3100/api/prom/push' 26 | 27 | zcashd: 28 | image: electriccoinco/zcashd:latest 29 | volumes: 30 | - $ZCASHD_DATADIR:/srv/zcashd/.zcash 31 | - $ZCASHD_PARMDIR:/srv/zcashd/.zcash-params 32 | env_file: 33 | - .env 34 | mem_limit: 4G 35 | logging: 36 | driver: loki 37 | options: 38 | loki-url: 'http://localhost:3100/api/prom/push' 39 | 40 | zcashd_exporter: 41 | image: electriccoinco/zcashd_exporter:latest 42 | environment: 43 | - ZCASHD_RPCUSER=$ZCASHD_RPCUSER 44 | - ZCASHD_RPCPASSWORD=$ZCASHD_RPCPASSWORD 45 | command: 46 | - --rpc.host=zcashd 47 | - --rpc.port=$ZCASHD_RPCPORT 48 | - --rpc.user=$ZCASHD_RPCUSER 49 | - --rpc.password=$ZCASHD_RPCPASSWORD 50 | ports: 51 | - "127.0.0.1:9100:9100" 52 | logging: 53 | driver: loki 54 | options: 55 | loki-url: 'http://localhost:3100/api/prom/push' 56 | 57 | grafana: 58 | image: grafana/grafana:6.4.3 59 | entrypoint: 60 | - bash 61 | - -c 62 | - grafana-cli plugins install grafana-piechart-panel && /run.sh 63 | ports: 64 | - "127.0.0.1:3000:3000" 65 | env_file: 66 | - .env 67 | volumes: 68 | - ./docker/grafana/provisioning/:/etc/grafana/provisioning/ 69 | logging: 70 | driver: loki 71 | options: 72 | loki-url: 'http://localhost:3100/api/prom/push' 73 | 74 | prometheus: 75 | image: prom/prometheus:v2.13.1 76 | ports: 77 | - "127.0.0.1:9090:9090" 78 | volumes: 79 | - ./docker/prometheus/config.yml:/etc/prometheus/prometheus.yml 80 | - promethus_data:/promethus_data 81 | mem_limit: 2G 82 | logging: 83 | driver: loki 84 | options: 85 | loki-url: 'http://localhost:3100/api/prom/push' 86 | 87 | loki: 88 | image: grafana/loki:master 89 | ports: 90 | - "127.0.0.1:3100:3100" 91 | command: -config.file=/etc/loki/local-config.yaml 92 | logging: 93 | driver: loki 94 | options: 95 | loki-url: 'http://localhost:3100/api/prom/push' 96 | 97 | volumes: 98 | promethus_data: 99 | lightwalletd_cache: -------------------------------------------------------------------------------- /docker/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCertVgQ0jHcqW9 3 | NvPszU9Mwi5jsin6edmCIBFLEDdOBBCWd9fkDkVHcaLNgq2uf1DYnhaWcSHiUiP2 4 | D7sUrtFdR9JZc9REwOwaOInk+ughiGPQ06nnOlhxbHH4TF3hwXLBe1LvEVL7SP3s 5 | liBi88oITC1YrTQ2BXi3vKwL7FsTuCLn8+hlw/l5WNt+JZyA9GvXjwLLr8yf3Xi+ 6 | 8HRn3vRyIwy6rxFf2xYmTK0+5pBGBGhkuKuz/V3mJO9hyw8702ccdc7Vi+yETvIr 7 | I6BjmQgH8AzM3J2JjZ/4Jbc78WjTqC8/P7ve4YW19rKmJKvqeY73x2CXC80PQZ8J 8 | 2nVMtlu7AgMBAAECggEAXBGi2jSP4LEjevKMeuPw09+C3PN9qcfTLM4AWqYxAIIZ 9 | QcSNLdQd3EMRq93befiC5vxqrKU9fLweA2HDMU/xSAcpBB/RlIa/NsOBNqthzjr9 10 | dyeoV/IhaMX8Jo3gluEP/TTZvL43gHcsZX4BkohSu5e/Y8kzFvj7vteol05u1bRB 11 | 607DW6ONNKCK+upbTIvY3FvaeQ+Y8sZLT1ceQGzmwkNFb0l5qnwmKm3w2jy8y7Ls 12 | lGkVTOXuDpUm1rFYomh1vUetTB1E/B8FokbLlz8shRq3nmWKx/tycW305fqA8j0h 13 | 5XWxApARF8LptEXCGTyaqWdZDTMwRrUP4ORqklyf2QKBgQDSU1y4AvRbzDL+dfb7 14 | 3WgNQjkQEmWDDb50hKxK9hat1aOhPa/gFwNKLWPyJpAFNBOczyCHVux6eOYPCH+t 15 | cxf81FfbNHbfTlnfM1yXg1/5r2Mp3selUjun2aSIHQmmI0r+MjpLmFCV/TFDRDRx 16 | IeZlea7adU62XmhIZbD9hw44jwKBgQDBJH9/+U7W4WZc0RocxZDzlW6R2E5R7hBo 17 | F4mfeTUHrPVR5r5Tdxv94eyGqW5ZTRfty9U4btg/t8rOhb2YW9P0LT7BPQ/K/PXu 18 | RjNR12sgZn3XY89/IwitY+SV/8kPq0fQtnjc1YdHGJ7cqW76b2SAGuFSY7bzw4W8 19 | 9fV08+TIFQKBgAxI+j11Trid8MyUL1z+zbkYiSS7Llq9TsaXiUjHnwOAWxJr+/3m 20 | 2jZW+GOIhRkItayPHKNLHHz62tU99dc3xcrqzEbthZP9i5pR8bKX5d87s1savCaX 21 | 6wwe1lFtAMdHgHXgkS8hMnPQWjRHo5iIFmEO/nucJoDYetbfubrVTKtZAoGAPsmX 22 | rUmlyJMjzL6pR3sugREuDbmM1HOY383vDmm/xIwEgCiL7ORGtEUSuEAyQFOgmMxv 23 | t1XJdQVRp8uwc+w+Ph3LTdSE4s9TP6+QlWV7TOAkvrWSydjgxEU6FU0+1pou0XnQ 24 | VrIPtRwa4M8v5bf6qu6SG0+RNTN1sZUfw3JaCHUCgYBb9wCYDktOzcUTo0xK2j8q 25 | gZotpmKT+1wbZmL+vTRmq9juo3qGjlcRSNJMvFCe1zwC24E3bei+Chld626Do3mN 26 | f2hkrrLIVqsXP2CSOOTZtONsO3VRPaNjdLzreR5V6Oc9StOD/2CPcvaHcaRMaoDl 27 | ylpUUJpnIUaIIRZQvN9GlA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /docker/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDizCCAnOgAwIBAgIUevPATPP74qY3JPE6s3axxnC8rxwwDQYJKoZIhvcNAQEL 3 | BQAwVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQKDAtNeU9yZywg 4 | SW5jLjEjMCEGA1UEAwwabGlnaHR3YWxsZXRkLnRlc3RuZXQubG9jYWwwHhcNMTkx 5 | MjE4MTQyNDU0WhcNMjkxMjE1MTQyNDU0WjBVMQswCQYDVQQGEwJVUzELMAkGA1UE 6 | CAwCQ0ExFDASBgNVBAoMC015T3JnLCBJbmMuMSMwIQYDVQQDDBpsaWdodHdhbGxl 7 | dGQudGVzdG5ldC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 8 | AJ6u1WBDSMdypb028+zNT0zCLmOyKfp52YIgEUsQN04EEJZ31+QORUdxos2Cra5/ 9 | UNieFpZxIeJSI/YPuxSu0V1H0llz1ETA7Bo4ieT66CGIY9DTqec6WHFscfhMXeHB 10 | csF7Uu8RUvtI/eyWIGLzyghMLVitNDYFeLe8rAvsWxO4Iufz6GXD+XlY234lnID0 11 | a9ePAsuvzJ/deL7wdGfe9HIjDLqvEV/bFiZMrT7mkEYEaGS4q7P9XeYk72HLDzvT 12 | Zxx1ztWL7IRO8isjoGOZCAfwDMzcnYmNn/gltzvxaNOoLz8/u97hhbX2sqYkq+p5 13 | jvfHYJcLzQ9BnwnadUy2W7sCAwEAAaNTMFEwHQYDVR0OBBYEFFNfTC+B2/frL5N+ 14 | h85UjP4Ijq/dMB8GA1UdIwQYMBaAFFNfTC+B2/frL5N+h85UjP4Ijq/dMA8GA1Ud 15 | EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHHE3+/Hn6q5N7P1ImyZwB2s 16 | a6y98BPbmgDpb3iWVGhiPRw2vE7/iW8aJwCPQLl9RJZsyGM447CdxsGbw4U5hBkl 17 | NlFFRBX4b7YLuDrDeTonaIiceWaDbKYqQHggIMZMoXdRnmQQcvuJUUsDnrk1GhQQ 18 | jI58LT4tPl8Xz780NB1+ZjGuVYPnyjk0NRHGSytNEr2KddpCrfJDRoGPRUIMuc7b 19 | gRDQCJ5tw0heJ1HYJcyX1LzZP3u2CX3TqCvYvTSHVwbXgJ6LLOJffXR0b3FwldMa 20 | YwKriZ9NC6wdEdEGtwN0I0rSMOaHMK0+3jMv2Lg9NCubF0p4l19KF9yANdXc+bs= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /docker/gen_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## In debian 4 | # apt-get update && apt-get install -y openssl 5 | 6 | openssl req -x509 -nodes -newkey rsa:2048 -keyout ./cert.key -out ./cert.pem -days 3650 -subj "/C=US/ST=CA/O=MyOrg, Inc./CN=lightwalletd.testnet.local" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:lightwalletd.testnet.local,DNS:127.0.0.1,DNS:localhost")) 7 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/datasources/loki.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: Loki 4 | type: loki 5 | access: proxy 6 | orgId: 1 7 | url: http://loki:3100 8 | version: 1 9 | editable: false 10 | jsonData: 11 | maxLines: 1000 12 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: Prometheus 4 | type: prometheus 5 | access: proxy 6 | orgId: 1 7 | url: http://prometheus:9090 8 | version: 1 9 | editable: false 10 | -------------------------------------------------------------------------------- /docker/prometheus/config.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | - job_name: 'zcashd_exporter' 23 | static_configs: 24 | - targets: ['zcashd_exporter:9100'] 25 | - job_name: 'lightwalletd' 26 | static_configs: 27 | - targets: ['lightwalletd:9068'] 28 | -------------------------------------------------------------------------------- /docker/zcash.conf: -------------------------------------------------------------------------------- 1 | rpcuser=zcashrpc 2 | rpcpassword=notsecure 3 | rpcbind=zcashd 4 | rpcport=3434 5 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Definitions 2 | 3 | A **light wallet** is not a full participant in the network of Zcash peers. It can send and receive payments, but does not store or validate a copy of the blockchain. 4 | 5 | A **compact transaction** is a representation of a Zcash Sapling transaction that contains only the information necessary to detect that a given Sapling payment output is for you and to spend a note. 6 | 7 | A **compact block** is a collection of compact transactions along with certain metadata (such as the block header) from their source block. 8 | 9 | # Architecture 10 | 11 | ``` 12 | +----------+ 13 | | zcashd | +----------+ +-------+ 14 | +----+-----+ +------->+ frontend +--->+ | 15 | | | +----------+ | L +<----Client 16 | | raw blocks +----+----+ | O B | 17 | v | | | A A | 18 | +----+-----+ | | +----------+ | D L +<---Client 19 | | ingester +-------->+ storage +-->+ frontend +--->+ A | 20 | +----------+ compact | | +----------+ | N +<-------Client 21 | blocks | | | C | 22 | +----+----+ | E +<----Client 23 | | +----------+ | R | 24 | +------->+ frontend +--->+ +<------Client 25 | +----------+ +-------+ 26 | ``` 27 | 28 | ## Ingester 29 | 30 | The ingester is the component responsible for transforming raw Zcash block data into a compact block. 31 | 32 | The ingester is a modular component. Anything that can retrieve the necessary data and put it into storage can fulfill this role. Currently, the only ingester available communicated to zcashd through RPCs and parses that raw block data. 33 | 34 | **How do I run it?** 35 | 36 | ⚠️ This section literally describes how to execute the binaries from source code. This is suitable only for testing, not production deployment. See section Production for cleaner instructions. 37 | 38 | ⚠️ Bringing up a fresh compact block database can take several hours of uninterrupted runtime. 39 | 40 | First, install [Go >= 1.11](https://golang.org/dl/#stable). Older versions of Go may work but are not actively supported at this time. Note that the version of Go packaged by Debian stable (or anything prior to Buster) is far too old to work. 41 | 42 | Now clone this repo and start the ingester. The first run will start slow as Go builds the sqlite C interface: 43 | 44 | ``` 45 | $ git clone https://github.com/zcash/lightwalletd 46 | $ cd lightwalletd 47 | $ go run cmd/ingest/main.go --conf-file --db-path 48 | ``` 49 | 50 | To see the other command line options, run `go run cmd/ingest/main.go --help`. 51 | 52 | ## Frontend 53 | 54 | The frontend is the component that talks to clients. 55 | 56 | It exposes an API that allows a client to query for current blockheight, request ranges of compact block data, request specific transaction details, and send new Zcash transactions. 57 | 58 | The API is specified in [Protocol Buffers](https://developers.google.com/protocol-buffers/) and implemented using [gRPC](https://grpc.io). You can find the exact details in [these files](https://github.com/zcash/lightwalletd/tree/master/walletrpc). 59 | 60 | **How do I run it?** 61 | 62 | ⚠️ This section literally describes how to execute the binaries from source code. This is suitable only for testing, not production deployment. See section Production for cleaner instructions. 63 | 64 | First, install [Go >= 1.11](https://golang.org/dl/#stable). Older versions of Go may work but are not actively supported at this time. Note that the version of Go packaged by Debian stable (or anything prior to Buster) is far too old to work. 65 | 66 | Now clone this repo and start the frontend. The first run will start slow as Go builds the sqlite C interface: 67 | 68 | ``` 69 | $ git clone https://github.com/zcash/lightwalletd 70 | $ cd lightwalletd 71 | $ go run cmd/server/main.go --db-path --bind-addr 0.0.0.0:9067 72 | ``` 73 | 74 | To see the other command line options, run `go run cmd/server/main.go --help`. 75 | 76 | **What should I watch out for?** 77 | 78 | x509 Certificates! This software relies on the confidentiality and integrity of a modern TLS connection between incoming clients and the front-end. Without an x509 certificate that incoming clients accurately authenticate, the security properties of this software are lost. 79 | 80 | Otherwise, not much! This is a very simple piece of software. Make sure you point it at the same storage as the ingester. See the "Production" section for some caveats. 81 | 82 | Support for users sending transactions will require the ability to make JSON-RPC calls to a zcashd instance. By default the frontend tries to pull RPC credentials from your zcashd.conf file, but you can specify other credentials via command line flag. In the future, it should be possible to do this with environment variables [(#2)](https://github.com/zcash/lightwalletd/issues/2). 83 | 84 | ## Storage 85 | 86 | The storage provider is the component that caches compact blocks and their metadata for the frontend to retrieve and serve to clients. 87 | 88 | It currently assumes a SQL database. The schema can be found [here](https://github.com/zcash/lightwalletd/blob/d53507cc39e8da52e14d08d9c63fee96d3bd16c3/storage/sqlite3.go#L15), but they're extremely provisional. We expect that anyone deploying lightwalletd at scale will adapt it to their own existing data infrastructure. 89 | 90 | **How do I run it?** 91 | 92 | It's not necessary to explicitly run anything. Both the ingester and the frontend code know how to use a generic SQL database via Go's [database/sql](https://golang.org/pkg/database/sql/) package. It should be possible to swap out for MySQL or Postgres by changing the driver import and connection string. 93 | 94 | **What should I watch out for?** 95 | 96 | sqlite is extremely reliable for what it is, but it isn't good at high concurrency. Because sqlite uses a global write lock, the code limits the number of open database connections to *one* and currently makes no distinction between read-only (frontend) and read/write (ingester) connections. It will probably begin to exhibit lock contention at low user counts, and should be improved or replaced with your own data store in production. 97 | 98 | ## Production 99 | 100 | ⚠️ This is informational documentation about a piece of alpha software. It has not yet undergone audits or been subject to rigorous testing. It lacks some affordances necessary for production-level reliability. We do not recommend using it to handle customer funds at this time (March 2019). 101 | 102 | **x509 Certificates** 103 | You will need to supply an x509 certificate that connecting clients will have good reason to trust (hint: do not use a self-signed one, our SDK will reject those unless you distribute them to the client out-of-band). We suggest that you be sure to buy a reputable one from a supplier that uses a modern hashing algorithm (NOT md5 or sha1) and that uses Certificate Transparency (OID 1.3.6.1.4.1.11129.2.4.2 will be present in the certificate). 104 | 105 | To check a given certificate's (cert.pem) hashing algorithm: 106 | ``` 107 | openssl x509 -text -in certificate.crt | grep "Signature Algorithm" 108 | ``` 109 | 110 | To check if a given certificate (cert.pem) contains a Certificate Transparency OID: 111 | ``` 112 | echo "1.3.6.1.4.1.11129.2.4.2 certTransparency Certificate Transparency" > oid.txt 113 | openssl asn1parse -in cert.pem -oid ./oid.txt | grep 'Certificate Transparency' 114 | ``` 115 | 116 | To use Let's Encrypt to generate a free certificate for your frontend, one method is to: 117 | 1) Install certbot 118 | 2) Open port 80 to your host 119 | 3) Point some forward dns to that host (some.forward.dns.com) 120 | 4) Run 121 | ``` 122 | certbot certonly --standalone --preferred-challenges http -d some.forward.dns.com 123 | ``` 124 | 5) Pass the resulting certificate and key to frontend using the -tls-cert and -tls-key options. 125 | 126 | **Dependencies** 127 | 128 | The first-order dependencies of this code are: 129 | 130 | - Go (>= 1.11 suggested; older versions are currently unsupported) 131 | - libsqlite3-dev (used by our sqlite interface library; optional with another datastore) 132 | 133 | **Containers** 134 | 135 | This software was designed to be container-friendly! We highly recommend that you package and deploy the software in this manner. We've created an example Docker environment that is likewise new and minimally tested, but it's functional. 136 | 137 | **What's missing?** 138 | 139 | lightwalletd currently lacks several things that you'll want in production. Caveats include: 140 | 141 | - There are no monitoring / metrics endpoints yet. You're on your own to notice if it goes down or check on its performance. 142 | - Logging coverage is patchy and inconsistent. However, what exists emits structured JSON compatible with various collectors. 143 | - Logging may capture identifiable user data. It hasn't received any privacy analysis yet and makes no attempt at sanitization. 144 | - The only storage provider we've implemented is sqlite. sqlite is [likely not appropriate](https://sqlite.org/whentouse.html) for the number of concurrent requests we expect to handle. Because sqlite uses a global write lock, the code limits the number of open database connections to *one* and currently makes no distinction between read-only (frontend) and read/write (ingester) connections. It will probably begin to exhibit lock contention at low user counts, and should be improved or replaced with your own data store in production. 145 | - [Load-balancing with gRPC](https://grpc.io/blog/loadbalancing) may not work quite like you're used to. A full explanation is beyond the scope of this document, but we recommend looking into [Envoy](https://www.envoyproxy.io/), [nginx](https://nginx.com), or [haproxy](https://www.haproxy.org) depending on your existing infrastructure. 146 | -------------------------------------------------------------------------------- /docs/docker-compose-setup.md: -------------------------------------------------------------------------------- 1 | # Installation and setup 2 | 3 | ## Install requirements 4 | - [docker](https://docs.docker.com/install/) 5 | - [docker-compose](https://docs.docker.com/compose/install/) 6 | - loki plugin for docker logs 7 | ``` 8 | docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions 9 | ``` 10 | 11 | ## Setup .env file 12 | 13 | Copy `.env.example` to `.env` and change any required parameters. 14 | 15 | | Variable | Usage | 16 | | ------------- |:-------------:| 17 | | `GF_SECURITY_ADMIN_USER` | Grafana admin user name | 18 | | `ZCASHD_RPCUSER` | zcashd rpc user | 19 | | `ZCASHD_RPCPASSWORD` | zcashd rpc password | 20 | | `ZCASHD_RPCPORT` | zcashd rpc port | 21 | |`ZCASHD_ALLOWIP`| zcashd rpc allowed IPs (don't change unless you know what you're doing)| 22 | |`ZCASHD_DATADIR`| local location of zcashd data directory. `uid` 2001 needs write access| 23 | |`ZCASHD_PARMDIR`| local location of zcashd data directory. `uid` 2001 needs read access| 24 | |`ZCASHD_NETWORK`| zcashd network to use, `testnet` or `mainnet`| 25 | |`ZCASHD_GEN`| should zcashd mine? `0` or `1` 26 | |`LWD_PORT`| port for lightwalletd to bind to| 27 | |`ZCASHD_CONF_PATH`| path for lightwalletd to pick up configuration| 28 | 29 | 30 | ## Populate secret env vars with random values 31 | 32 | ``` 33 | ./buildenv.sh | tee .env 34 | ``` 35 | 36 | ## Edit the two zcash.conf files 37 | There are two zcash.conf files; one read by zcashd, one read by lightwalletd. 38 | 39 | ### `$ZCASHD_DATADIR/zcash.conf`—read by zcashd 40 | The zcashd's `zcash.conf` needs to look like: 41 | ``` 42 | rpcuser=zcashrpc 43 | rpcpassword=TODO INSERT A RANDOM PASSWORD HERE 44 | experimentalfeatures=1 45 | lightwalletd=1 46 | ``` 47 | 48 | Replace `TODO INSERT A RANDOM PASSWORD HERE` with a random password, e.g. the output of `head -c 16 /dev/urandom | base64`. 49 | 50 | `rpcuser` and `rpcpassword` must be set, as lightwalletd doesn't work with RPC cookies (see the [rpcpassword](https://zcash.readthedocs.io/en/latest/rtd_pages/zcash_conf_guide.html) documentation) for authentication. 51 | 52 | `rpcuser` and `rpcpassword` in `.env` are only used by zcashd_exporter, but they also must be the same values as in `$ZCASHD_DATADIR/zcash.conf` 53 | 54 | ### `lightwalletd/docker/zcash.conf`—read by lightwalletd 55 | The other `zcashd.conf`—the one read by lightwalletd—needs to have `rpcbind` (the address of the zcashd it will connect to) set to `zcashd`, and then docker-compose networking will make it resolve to the right IP address. Also, it needs to have the same `rpcuser` and `rpcpassword` values that are listed in `$ZCASHD_DATADIR/zcash.conf` to be able to authenticate. 56 | 57 | 58 | ## Build initial local docker image 59 | 60 | `docker-compose build` 61 | 62 | ## Start the project 63 | 64 | ``` 65 | docker-compose up -d 66 | ``` 67 | 68 | # Setup and use Grafana 69 | 70 | Open a browser to http://localhost:3000 71 | 72 | ![grafana-login](./images/grafana-login.png) 73 | 74 | 75 | Login with the user (`GF_SECURITY_ADMIN_USER`) and password (`GF_SECURITY_ADMIN_PASSWORD`). 76 | The values can be found in your `.env` file 77 | 78 | Open the `Dashboard Manage` menu on the left 79 | 80 | ![grafana-manage](./images/grafana-manage.png) 81 | 82 | Select `Import` 83 | 84 | ![grafana-import](./images/grafana-import-1.png) 85 | 86 | Enter `11325` for the `Grafana.com Dashboard` 87 | 88 | ![grafana-import](./images/grafana-import-2.png) 89 | 90 | On the next screen, select the `Prometheus` and `Loki` values (there should only be 1 to select) 91 | 92 | ![grafana-configure](./images/grafana-configure.png) 93 | 94 | Click `Import` 95 | 96 | 97 | This should then be taken to the `Zcashd node exporter` dashboard. 98 | 99 | ![grafana-zcashd-dashboard](./images/grafana-zcashd-dashboard.png) 100 | 101 | If all goes as planned, the dashboard should start populating data from the container services. 102 | 103 | If there are any issues, you can view all the `docker-compose` services under the `Explore` section. 104 | 105 | # Viewing container logs 106 | 107 | Open the `Explore` menu entry 108 | 109 | ![grafana-explore.png](./images/grafana-explore.png) 110 | 111 | Make sure `Loki` is selected as the datasource at the top. 112 | 113 | ![grafana-explore2](./images/grafana-explore-2.png) 114 | 115 | Then choose the container to view it's logs. 116 | 117 | ![grafana-explore3](./images/grafana-explore-3.png) 118 | 119 | Loki as a rich query syntax to help with log in many ways, for example combine 2 container logs entries: 120 | 121 | ![grafana-explore4](./images/grafana-explore-4.png) 122 | 123 | See more here: https://github.com/grafana/loki/blob/master/docs/sources/query/_index.md#logql-log-query-language 124 | 125 | 126 | # Exposing `lightwalletd` to the network 127 | 128 | Edit `docker-compose.yml` to look like 129 | 130 | ``` 131 | ports: 132 | #- "127.0.0.1:$LWD_GRPC_PORT:$LWD_GRPC_PORT" 133 | #- "127.0.0.1:$LWD_HTTP_PORT:$LWD_HTTP_PORT" 134 | - "0.0.0.0:$LWD_GRPC_PORT:$LWD_GRPC_PORT" 135 | - "0.0.0.0:$LWD_HTTP_PORT:$LWD_HTTP_PORT" 136 | ``` 137 | 138 | When you edit these lines in `docker-compose.yml`, stopping/starting the individual `lightwalletd` container doesn't actually make the changes happen—you have to stop/start the whole `docker-compose` ensemble of containers because the ports/network config stuff lives at that level and doesn't seem to be affected by individual container stop/starts. Also if you want to expose `lightwalletd` to the whole internet, you don't need to specify an IP address, `0.0.0.0` works as it should. 139 | -------------------------------------------------------------------------------- /docs/docker-run.md: -------------------------------------------------------------------------------- 1 | # Docker images 2 | Docker images are available on Docker Hub at [electriccoinco/lightwalletd](https://hub.docker.com/repository/docker/electriccoinco/lightwalletd). 3 | 4 | ## Using command line options 5 | 6 | Already have a Zcash node running with an exposed RPC endpoint? 7 | 8 | Try the docker container with command lines flags like: 9 | ``` 10 | docker run --rm -p 9067:9067 \ 11 | electriccoinco/lightwalletd:v0.4.2 \ 12 | --grpc-bind-addr 0.0.0.0:9067 \ 13 | --no-tls-very-insecure \ 14 | --rpchost 192.168.86.46 \ 15 | --rpcport 38237 \ 16 | --rpcuser zcashrpc \ 17 | --rpcpassword notsecure \ 18 | --log-file /dev/stdout 19 | ``` 20 | 21 | ## Preserve the compactblocks database between runs 22 | 23 | Like the first example, but this will preserve the lightwalletd compactblocks database for use between runs. 24 | 25 | 26 | Create a directory somewhere and change the `uid` to `2002`. 27 | The is the id of the restricted lightwalletd user inside of the container. 28 | 29 | ``` 30 | mkdir ./lightwalletd_db_volume 31 | sudo chown 2002 ./lightwalletd_db_volume 32 | ``` 33 | 34 | Now add a `--volume` mapping from the local file path to where we want it to show up inside the container. 35 | 36 | Then, add the `--data-dir` to the lightwalletd command with the value of path mapping as viewed from inside the container. 37 | 38 | ``` 39 | docker run --rm -p 9067:9067 \ 40 | --volume $(pwd)/lightwalletd_db_volume:/srv/lightwalletd/db_volume \ 41 | electriccoinco/lightwalletd:v0.4.2 \ 42 | --grpc-bind-addr 0.0.0.0:9067 \ 43 | --no-tls-very-insecure \ 44 | --rpchost 192.168.86.46 \ 45 | --rpcport 38237 \ 46 | --rpcuser zcashrpc \ 47 | --rpcpassword notsecure \ 48 | --data-dir /srv/lightwalletd/db_volume \ 49 | --log-file /dev/stdout 50 | ``` 51 | 52 | 53 | ## Using a YAML config file 54 | 55 | When using a configuration file with the docker image, you must create the configuration file and then map it into the container. Finally, provide a command line option referencing the mapped file location. 56 | 57 | Create a configuration file: 58 | ``` 59 | cat <lightwalletd.yml 60 | no-tls-very-insecure: true 61 | log-file: /dev/stdout 62 | rpcuser: zcashrpc 63 | rpcpassword: notsecure 64 | rpchost: 192.168.86.46 65 | rpcport: 38237 66 | grpc-bind-addr: 0.0.0.0:9067 67 | EOF 68 | ``` 69 | 70 | Use it with the docker container 71 | ``` 72 | docker run --rm \ 73 | -p 9067:9067 \ 74 | -v $(pwd)/lightwalletd.yml:/tmp/lightwalletd.yml \ 75 | electriccoinco/lightwalletd:v0.4.2 \ 76 | --config /tmp/lightwalletd.yml 77 | ``` 78 | 79 | ## Using docker-compose for a full stack 80 | 81 | Don't have an existing Zcash node? Check out the [docker-compose](./docker-compose-setup.md) for examples of multi-container usage. 82 | -------------------------------------------------------------------------------- /docs/images/grafana-configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-configure.png -------------------------------------------------------------------------------- /docs/images/grafana-explore-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-explore-2.png -------------------------------------------------------------------------------- /docs/images/grafana-explore-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-explore-3.png -------------------------------------------------------------------------------- /docs/images/grafana-explore-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-explore-4.png -------------------------------------------------------------------------------- /docs/images/grafana-explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-explore.png -------------------------------------------------------------------------------- /docs/images/grafana-import-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-import-1.png -------------------------------------------------------------------------------- /docs/images/grafana-import-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-import-2.png -------------------------------------------------------------------------------- /docs/images/grafana-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-login.png -------------------------------------------------------------------------------- /docs/images/grafana-manage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-manage.png -------------------------------------------------------------------------------- /docs/images/grafana-zcashd-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcash/lightwalletd/c5ffd6bcf70207eadb78c2f9bd3fa1071d1f4ef3/docs/images/grafana-zcashd-dashboard.png -------------------------------------------------------------------------------- /docs/integration-tests.md: -------------------------------------------------------------------------------- 1 | # Wallet ⟷ Lightwalletd Integration Tests 2 | 3 | ## High-priority tests 4 | Funds are at risk if these tests fail. 5 | 6 | **Reorged-Away Transaction** 7 | A transparent/shielded transaction is sent to the wallet in block N containing value v. There's a reorg to height N-k for some k >= 1, and after the reorg the original transaction is no longer there but there is a new transaction with a different value u. Before the reorg, the wallet should detect the transaction and show unconfirmed balance v. After the reorg, the wallet should show unconfirmed balance u. Some number of blocks later, the balance is marked as confirmed. 8 | 9 | Consequences if this test fails: An attacker could take advantage of regular/accidental reorgs to confuse the wallet about its balance. 10 | 11 | **Dropped from Mempool** 12 | Similar to the reorged-away transaction test, except the transaction only enters the mempool and is never mined. 13 | 14 | Consequences: An attacker could confuse wallets about their balance by arranging for a transaction to enter the mempool but not be mined. 15 | 16 | **Transparent TXID Malleated** 17 | The wallet sends a transparent transaction. Its transaction ID is malleated to change its transaction ID, and then mined. After sending the transaction, the wallet’s balance should be reduced by the value of the transaction. 100 blocks after the transparent transaction was mined, the wallet’s balance should still be reduced by that amount. 18 | 19 | Consequences if this test fails: An attacker could malleate one of the wallet’s transparent transactions, and if it times out thinking it was never mined, the wallet would think it has balance when it doesn’t. 20 | 21 | **Transaction Never Mined** 22 | The wallet broadcasts a transparent/shielded transaction optionally with an expiry height. For 100 blocks (or at least until the expiry height), the transaction is never mined. After sending the transaction, the wallet’s balance should be reduced by the value of the transaction, and it should stay reduced by that amount until the expiry height (if any). 23 | 24 | Consequences if this test fails: If the wallet concludes the transaction will never be mined before the expiry height, then an attacker can delay mining the transaction to cause the wallet to think it has more funds than it does. 25 | 26 | **Transaction Created By Other Wallet** 27 | A seed is imported into three wallets with transparent/shielded funds. Wallet A sends a payment to some address. At the same time, Wallet B sends a payment of a different amount using some of the same UTXOs or notes. Wallet C does not send any payments. Wallet B’s transaction gets mined instead of Wallet A’s. The balances of all three wallets are decreased by the value of Wallet B’s transaction. 28 | 29 | Consequences if this test fails: A user importing their seed into multiple wallets and making simultaneous transactions could lead them to be confused about their balance. 30 | 31 | **Anchor Invalidation** 32 | A wallet broadcasts a sapling transaction using a recent anchor. A reorg occurs which invalidates that anchor, i.e. some of the previous shielded transactions changed. (Depending on how we want to handle this) the wallet either detects this and re-broadcasts the transaction or marks the transaction as failed and the funds become spendable again. 33 | 34 | Consequences if this test fails: Wallets might get confused about their balance if this ever occurs. 35 | 36 | **Secret Transactions** 37 | Lightwalletd has some shielded/transparent funds. It creates a real transaction sending these funds to the wallet, such that if the transaction were broadcast on the Zcash network, the wallet really would get the funds. However, instead of broadcasting the transaction, the lightwalletd operator includes the transaction in a compact block, but does not broadcast the transaction to the actual network. The wallet should detect that the transaction has not really been mined by miners on the Zcash network, and not show an increased balance. 38 | 39 | (Currently, this test will fail, since the wallet is not checking the PoW or block headers at all. Worse, even with PoW/header checks, lightwalletd could mine down the difficulty to have their wallets follow an invalid chain. To comabt this wallets would need to reach out to multiple independent lightwalletds to verify it has the highest PoW chain. Or Larry’s idea, to warn when the PoW is below a certain threshold (chosen to be above what most attackers could do but low enough we’d legitimately want to warn users if it drops that low), I like a lot better.) 40 | 41 | Consequences if this test fails: lightwalletd can make it appear as though a wallet received funds when it didn’t. 42 | 43 | ## Medium-priority tests 44 | Funds aren’t at risk if these fail but there’s a severe problem. 45 | 46 | **Normal Payments** 47 | Wallet A sends a shielded/transparent transaction to Wallet B. Wallet B receives the transaction and sends half back to wallet A. Wallet A receives the transaction, and B receives change. All of the balances end up as expected. 48 | 49 | Consequences if this test fails: Normal functionality of the wallet is broken. 50 | 51 | **Mempool DoS** 52 | The transactions in the mempool constantly churn. The wallet should limit its bandwidth used to fetch new transactions in the mempool, rather than using an unlimited amount. 53 | 54 | Consequences if this test fails: It’s possible to run up the bandwidth bills of wallet users. 55 | 56 | 57 | ## Low-priority tests 58 | These won’t occur unless lightwalletd is evil. 59 | 60 | **High Block Number** 61 | Lightwalletd announces that the latest block is some very large number, much larger than the actual block height. The wallet syncs up to that point (with lightwalletd providing fake blocks all the way). Lightwalletd then stops lying about the block height and blocks. This should trigger the wallet’s reorg limit and the wallet should be unusable. 62 | 63 | **Repeated Note** 64 | A shielded transaction is sent to the wallet. Lightwalletd simply repeats the transaction in a compact block sent to the wallet. The wallet should not think it has twice as much money. From this point, no shielded transactions the wallet sends can be mined, since they will use invalid anchors. 65 | 66 | **Invalid Note** 67 | Same as repeated note above, but random data. The results should be exactly the same. 68 | 69 | **Omitted Note** 70 | A shielded transaction is sent to the wallet. Lightwalletd simply does not send the transaction to the wallet (omits it from the compact block). From this point, no shielded transactions the wallet sends can be mined, since they will use invalid anchors. 71 | 72 | -------------------------------------------------------------------------------- /docs/release-notes/release-notes-0.4.1.md: -------------------------------------------------------------------------------- 1 | 0.4.1 Release Notes 2 | =============================== 3 | 4 | Lightwalletd version 0.4.1 is now available from: 5 | 6 | 7 | 8 | Or cloned from: 9 | 10 | 11 | 12 | Lightwalletd must be built from source code (there are no binary releases 13 | at this time). 14 | 15 | This minor release includes various bug fixes, performance 16 | improvements, and test code improvements. 17 | 18 | Please report bugs using the issue tracker at GitHub: 19 | 20 | 21 | 22 | How to Upgrade 23 | ============== 24 | 25 | If you are running an older version, shut it down. Run `make` to generate 26 | the `./lightwalletd` executable. Run `./lightwalletd version` to verify 27 | that you're running the correct version (v0.4.0). Some of the command-line 28 | arguments (options) have changed since the previous release; please 29 | run `./lightwalletd help` to view them. 30 | 31 | Compatibility 32 | ============== 33 | 34 | Lightwalletd is supported and extensively tested on operating systems using 35 | the Linux kernel, and to a lesser degree macOS. It is not recommended 36 | to use Lightwalletd on unsupported systems. 37 | 38 | 0.4.1 change log 39 | ================= 40 | 41 | ### Infrastructure 42 | - #161 Add docker-compose 43 | - #227 Added tekton for Docker image build 44 | - #236 Add http endpoint and prometheus metrics framework 45 | 46 | ### Tests and QA 47 | - #234 darksidewalletd 48 | 49 | ### Documentation 50 | - #107 Reorg documents for updates and upcoming new details 51 | - #188 add documentation for lightwalletd APIs and data types 52 | - #195 add simple gRPC test client 53 | - #270 add issue and PR templates 54 | 55 | Credits 56 | ======= 57 | 58 | Thanks to everyone who directly contributed to this release: 59 | 60 | - adityapk00 61 | - Marshall Gaucher 62 | - Kevin Gorhan 63 | - Taylor Hornby 64 | - Linda Lee 65 | - Brad Miller 66 | - Charlie O'Keefe 67 | - Larry Ruane 68 | - Za Wilcox 69 | - Ben Wilson 70 | -------------------------------------------------------------------------------- /frontend/rpc_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | package frontend 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "path/filepath" 12 | 13 | "github.com/BurntSushi/toml" 14 | "github.com/btcsuite/btcd/rpcclient" 15 | "github.com/zcash/lightwalletd/common" 16 | ini "gopkg.in/ini.v1" 17 | ) 18 | 19 | // NewZRPCFromConf reads the zcashd configuration file. 20 | func NewZRPCFromConf(confPath string) (*rpcclient.Client, error) { 21 | connCfg, err := connFromConf(confPath) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return rpcclient.New(connCfg, nil) 26 | } 27 | 28 | // NewZRPCFromFlags gets zcashd rpc connection information from provided flags. 29 | func NewZRPCFromFlags(opts *common.Options) (*rpcclient.Client, error) { 30 | // Connect to local Zcash RPC server using HTTP POST mode. 31 | connCfg := &rpcclient.ConnConfig{ 32 | Host: net.JoinHostPort(opts.RPCHost, opts.RPCPort), 33 | User: opts.RPCUser, 34 | Pass: opts.RPCPassword, 35 | HTTPPostMode: true, // Zcash only supports HTTP POST mode 36 | DisableTLS: true, // Zcash does not provide TLS by default 37 | } 38 | return rpcclient.New(connCfg, nil) 39 | } 40 | 41 | func connFromConf(confPath string) (*rpcclient.ConnConfig, error) { 42 | if filepath.Ext(confPath) == ".toml" { 43 | return connFromToml(confPath) 44 | } else { 45 | return connFromIni(confPath) 46 | } 47 | } 48 | 49 | func connFromIni(confPath string) (*rpcclient.ConnConfig, error) { 50 | cfg, err := ini.Load(confPath) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to read config file in .conf format: %w", err) 53 | } 54 | 55 | rpcaddr := cfg.Section("").Key("rpcbind").String() 56 | if rpcaddr == "" { 57 | rpcaddr = "127.0.0.1" 58 | } 59 | rpcport := cfg.Section("").Key("rpcport").String() 60 | if rpcport == "" { 61 | rpcport = "8232" // default mainnet 62 | testnet, _ := cfg.Section("").Key("testnet").Int() 63 | regtest, _ := cfg.Section("").Key("regtest").Int() 64 | if testnet > 0 || regtest > 0 { 65 | rpcport = "18232" 66 | } 67 | } 68 | username := cfg.Section("").Key("rpcuser").String() 69 | password := cfg.Section("").Key("rpcpassword").String() 70 | 71 | if password == "" { 72 | return nil, errors.New("rpcpassword not found (or empty), please add rpcpassword= to zcash.conf") 73 | } 74 | 75 | // Connect to local Zcash RPC server using HTTP POST mode. 76 | connCfg := &rpcclient.ConnConfig{ 77 | Host: net.JoinHostPort(rpcaddr, rpcport), 78 | User: username, 79 | Pass: password, 80 | HTTPPostMode: true, // Zcash only supports HTTP POST mode 81 | DisableTLS: true, // Zcash does not provide TLS by default 82 | } 83 | // Notice the notification parameter is nil since notifications are 84 | // not supported in HTTP POST mode. 85 | return connCfg, nil 86 | } 87 | 88 | // If passed a string, interpret as a path, open and read; if passed 89 | // a byte slice, interpret as the config file content (used in testing). 90 | func connFromToml(confPath string) (*rpcclient.ConnConfig, error) { 91 | var tomlConf struct { 92 | Rpc struct { 93 | Listen_addr string 94 | RPCUser string 95 | RPCPassword string 96 | } 97 | } 98 | _, err := toml.DecodeFile(confPath, &tomlConf) 99 | if err != nil { 100 | return nil, fmt.Errorf("failed to read config file in .toml format: %w", err) 101 | } 102 | conf := rpcclient.ConnConfig{ 103 | Host: tomlConf.Rpc.Listen_addr, 104 | User: tomlConf.Rpc.RPCUser, 105 | Pass: tomlConf.Rpc.RPCPassword, 106 | HTTPPostMode: true, // Zcash only supports HTTP POST mode 107 | DisableTLS: true, // Zcash does not provide TLS by default 108 | } 109 | 110 | // Notice the notification parameter is nil since notifications are 111 | // not supported in HTTP POST mode. 112 | return &conf, nil 113 | } 114 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zcash/lightwalletd 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/BurntSushi/toml v0.3.1 9 | github.com/btcsuite/btcd v0.24.2 10 | github.com/golang/protobuf v1.5.3 11 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 12 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 13 | github.com/prometheus/client_golang v1.18.0 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/spf13/cobra v1.8.0 16 | github.com/spf13/viper v1.18.2 17 | google.golang.org/grpc v1.61.0 18 | google.golang.org/protobuf v1.33.0 19 | gopkg.in/ini.v1 v1.67.0 20 | ) 21 | 22 | require ( 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect 25 | github.com/btcsuite/btcd/btcutil v1.1.5 // indirect 26 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect 27 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 28 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 29 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 30 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 31 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect 32 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 33 | github.com/fsnotify/fsnotify v1.7.0 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 36 | github.com/magiconair/properties v1.8.7 // indirect 37 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 38 | github.com/mitchellh/mapstructure v1.5.0 // indirect 39 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 40 | github.com/prometheus/client_model v0.5.0 // indirect 41 | github.com/prometheus/common v0.45.0 // indirect 42 | github.com/prometheus/procfs v0.12.0 // indirect 43 | github.com/sagikazarmark/locafero v0.4.0 // indirect 44 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 45 | github.com/sourcegraph/conc v0.3.0 // indirect 46 | github.com/spf13/afero v1.11.0 // indirect 47 | github.com/spf13/cast v1.6.0 // indirect 48 | github.com/spf13/pflag v1.0.5 // indirect 49 | github.com/subosito/gotenv v1.6.0 // indirect 50 | go.uber.org/atomic v1.9.0 // indirect 51 | go.uber.org/multierr v1.9.0 // indirect 52 | golang.org/x/crypto v0.36.0 // indirect 53 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 54 | golang.org/x/net v0.38.0 // indirect 55 | golang.org/x/sys v0.31.0 // indirect 56 | golang.org/x/text v0.23.0 // indirect 57 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect 58 | gopkg.in/yaml.v3 v3.0.1 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /kubernetes/tekton/README.md: -------------------------------------------------------------------------------- 1 | # Tekton CI 2 | The configurations in this directory are for automating lightwalletd operations using Tekton. 3 | 4 | Currently new tags will trigger a docker build and push to https://hub.docker.com/r/electriccoinco/lightwalletd 5 | 6 | ## Testing 7 | 8 | ### Requirements 9 | - `kind` installed 10 | - `docker` installed 11 | - A Docker Hub account (create a new one if you want, its free!) 12 | 13 | ### Setup 14 | 15 | #### Log into Docker Hub 16 | Just run the command: 17 | ``` 18 | docker login 19 | ``` 20 | This creates a `config.json` file that we're going to send to tekton and contains your Docker Hub password! 21 | 22 | More info: https://github.com/tektoncd/pipeline/blob/master/docs/auth.md 23 | 24 | #### Create a kind cluster 25 | ``` 26 | kind create cluster --name tekton-testing-zcashd_exporter 27 | ``` 28 | #### Create a Kubernetes secret containing your Docker hub creds 29 | ``` 30 | kubectl create secret generic dockerhub-creds \ 31 | --from-file=.dockerconfigjson=~/.docker/config.json \ 32 | --type=kubernetes.io/dockerconfigjson 33 | ``` 34 | 35 | #### Create a service account to use those creds 36 | ``` 37 | kubectl apply -f serviceaccount.yml 38 | ``` 39 | 40 | #### Install Tekton 41 | ``` 42 | kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.15.2/release.yaml 43 | kubectl apply -f https://github.com/tektoncd/dashboard/releases/download/v0.9.0/tekton-dashboard-release.yaml 44 | ``` 45 | #### Install the Tekton Catalog tasks 46 | These are predefined tasks from the `tektoncd/catalog` collection on github. 47 | 48 | ``` 49 | kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/master/task/git-clone/0.2/git-clone.yaml 50 | kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/master/task/kaniko/0.1/kaniko.yaml 51 | ``` 52 | 53 | #### Create the pipeline 54 | ``` 55 | kubectl apply -f pipeline.yml 56 | ``` 57 | 58 | #### Edit the `PipelineRun` 59 | 60 | This object holds all of your pipeline instance parameters. 61 | 62 | You **must** edit (unless somehow you got access to my Docker Hub account) 63 | Change `electriccoinco` to your Docker Hub account name. 64 | ``` 65 | - name: dockerHubRepo 66 | value: electriccoinco/lightwalletd 67 | ``` 68 | You can also change the `gitTag` and `gitRepositoryURL` values if you want to try building off some other commit, or you fork the code to your own repo. 69 | 70 | ### Run the pipeline! 71 | 72 | You can do this as many times as you want, each one will create a new pipelinerun. 73 | ``` 74 | kubectl create -f pipelinerun.yml 75 | ``` 76 | 77 | 78 | ### View the dashboard for status 79 | 80 | Forward a port from inside the kubernetes cluster to your laptop. 81 | ``` 82 | kubectl --namespace tekton-pipelines port-forward svc/tekton-dashboard 9097:9097 & 83 | ``` 84 | 85 | The browse to http://localhost:9097/#/namespaces/default/pipelineruns -------------------------------------------------------------------------------- /kubernetes/tekton/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1beta1 3 | kind: Pipeline 4 | metadata: 5 | name: lightwalletd-tag-pipeline 6 | spec: 7 | params: 8 | - name: gitTag 9 | - name: gitRepositoryURL 10 | - name: dockerHubRepo 11 | workspaces: 12 | - name: source 13 | tasks: 14 | - name: git-clone 15 | taskRef: 16 | name: git-clone 17 | workspaces: 18 | - name: output 19 | workspace: source 20 | params: 21 | - name: url 22 | value: $(params.gitRepositoryURL) 23 | - name: revision 24 | value: $(params.gitTag) 25 | - name: refspec 26 | value: +refs/tags/*:refs/remotes/origin/tags/* +refs/heads/*:refs/heads/* 27 | - name: verbose 28 | value: "true" 29 | - name: kaniko-build 30 | taskRef: 31 | name: kaniko 32 | runAfter: 33 | - git-clone 34 | params: 35 | - name: IMAGE 36 | value: $(params.dockerHubRepo):$(params.gitTag) 37 | workspaces: 38 | - name: source 39 | workspace: source -------------------------------------------------------------------------------- /kubernetes/tekton/pipelinerun.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1beta1 3 | kind: PipelineRun 4 | metadata: 5 | generateName: lightwalletd-tag-pipeline- 6 | spec: 7 | serviceAccountName: ecc-tekton 8 | pipelineRef: 9 | name: lightwalletd-tag-pipeline 10 | workspaces: 11 | - name: source 12 | volumeClaimTemplate: 13 | spec: 14 | accessModes: 15 | - ReadWriteOnce 16 | resources: 17 | requests: 18 | storage: 1Gi 19 | params: 20 | - name: gitTag 21 | value: GetMempoolTx 22 | - name: gitRepositoryURL 23 | value: https://github.com/zcash/lightwalletd.git 24 | - name: dockerHubRepo 25 | value: electriccoinco/lightwalletd 26 | -------------------------------------------------------------------------------- /kubernetes/tekton/serviceaccount.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: ecc-tekton 6 | secrets: 7 | - name: dockerhub-creds -------------------------------------------------------------------------------- /kubernetes/tekton/triggers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: triggers.tekton.dev/v1alpha1 3 | kind: TriggerBinding 4 | metadata: 5 | name: lightwalletd-tag-binding 6 | spec: 7 | params: 8 | - name: gitTag 9 | value: $(body.tag) 10 | - name: gitRepositoryURL 11 | value: $(body.repository.git_http_url) 12 | - name: dockerHubRepo 13 | value: electriccoinco/zcashd_exporter 14 | --- 15 | apiVersion: triggers.tekton.dev/v1alpha1 16 | kind: TriggerTemplate 17 | metadata: 18 | name: lightwalletd-tag-pipeline-template 19 | spec: 20 | params: 21 | - name: gitTag 22 | description: Git tag 23 | - name: gitRepositoryURL 24 | description: Git repo url 25 | - name: dockerHubRepo 26 | description: Docker Hub repository name 27 | resourcetemplates: 28 | - apiVersion: tekton.dev/v1beta1 29 | kind: PipelineRun 30 | metadata: 31 | generateName: lightwalletd-tag-pipeline- 32 | spec: 33 | serviceAccountName: ecc-tekton 34 | pipelineRef: 35 | name: lightwalletd-tag-pipeline 36 | workspaces: 37 | - name: source 38 | volumeClaimTemplate: 39 | spec: 40 | accessModes: 41 | - ReadWriteOnce 42 | resources: 43 | requests: 44 | storage: 1Gi 45 | params: 46 | - name: gitRepositoryURL 47 | value: $(params.gitRepositoryURL) 48 | - name: gitTag 49 | value: $(params.gitTag) 50 | - name: dockerHubRepo 51 | value: $(params.dockerHubRepo) 52 | -------------------------------------------------------------------------------- /lightwalletd-example.yml: -------------------------------------------------------------------------------- 1 | bind-addr: 0.0.0.0:9067 2 | cache-size: 10 3 | log-file: /dev/stdout 4 | log-level: 10 5 | tls-cert: /secrets/lightwallted/cert.pem 6 | tls-key: /secrets/lightwallted/cert.key 7 | zcash-conf-path: /srv/zcashd/zcash.conf -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/zcash/lightwalletd/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /parser/block.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | // Package parser deserializes blocks from zcashd. 6 | package parser 7 | 8 | import ( 9 | "fmt" 10 | "errors" 11 | 12 | "github.com/zcash/lightwalletd/parser/internal/bytestring" 13 | "github.com/zcash/lightwalletd/walletrpc" 14 | ) 15 | 16 | // Block represents a full block (not a compact block). 17 | type Block struct { 18 | hdr *BlockHeader 19 | vtx []*Transaction 20 | height int 21 | } 22 | 23 | // NewBlock constructs a block instance. 24 | func NewBlock() *Block { 25 | return &Block{height: -1} 26 | } 27 | 28 | // GetVersion returns a block's version number (current 4) 29 | func (b *Block) GetVersion() int { 30 | return int(b.hdr.Version) 31 | } 32 | 33 | // GetTxCount returns the number of transactions in the block, 34 | // including the coinbase transaction (minimum 1). 35 | func (b *Block) GetTxCount() int { 36 | return len(b.vtx) 37 | } 38 | 39 | // Transactions returns the list of the block's transactions. 40 | func (b *Block) Transactions() []*Transaction { 41 | // TODO: these should NOT be mutable 42 | return b.vtx 43 | } 44 | 45 | // GetDisplayHash returns the block hash in big-endian display order. 46 | func (b *Block) GetDisplayHash() []byte { 47 | return b.hdr.GetDisplayHash() 48 | } 49 | 50 | // TODO: encode hash endianness in a type? 51 | 52 | // GetEncodableHash returns the block hash in little-endian wire order. 53 | func (b *Block) GetEncodableHash() []byte { 54 | return b.hdr.GetEncodableHash() 55 | } 56 | 57 | // GetDisplayPrevHash returns the block's previous hash in big-endian format. 58 | func (b *Block) GetDisplayPrevHash() []byte { 59 | return b.hdr.GetDisplayPrevHash() 60 | } 61 | 62 | // HasSaplingTransactions indicates if the block contains any Sapling tx. 63 | func (b *Block) HasSaplingTransactions() bool { 64 | for _, tx := range b.vtx { 65 | if tx.HasShieldedElements() { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | 72 | // see https://github.com/zcash/lightwalletd/issues/17#issuecomment-467110828 73 | const genesisTargetDifficulty = 520617983 74 | 75 | // GetHeight extracts the block height from the coinbase transaction. See 76 | // BIP34. Returns block height on success, or -1 on error. 77 | func (b *Block) GetHeight() int { 78 | if b.height != -1 { 79 | return b.height 80 | } 81 | coinbaseScript := bytestring.String(b.vtx[0].transparentInputs[0].ScriptSig) 82 | var heightNum int64 83 | if !coinbaseScript.ReadScriptInt64(&heightNum) { 84 | return -1 85 | } 86 | if heightNum < 0 { 87 | return -1 88 | } 89 | // uint32 should last us a while (Nov 2018) 90 | if heightNum > int64(^uint32(0)) { 91 | return -1 92 | } 93 | blockHeight := uint32(heightNum) 94 | 95 | if blockHeight == genesisTargetDifficulty { 96 | blockHeight = 0 97 | } 98 | 99 | b.height = int(blockHeight) 100 | return int(blockHeight) 101 | } 102 | 103 | // GetPrevHash returns the hash of the block's previous block (little-endian). 104 | func (b *Block) GetPrevHash() []byte { 105 | return b.hdr.HashPrevBlock 106 | } 107 | 108 | // ToCompact returns the compact representation of the full block. 109 | func (b *Block) ToCompact() *walletrpc.CompactBlock { 110 | compactBlock := &walletrpc.CompactBlock{ 111 | //TODO ProtoVersion: 1, 112 | Height: uint64(b.GetHeight()), 113 | PrevHash: b.hdr.HashPrevBlock, 114 | Hash: b.GetEncodableHash(), 115 | Time: b.hdr.Time, 116 | ChainMetadata: &walletrpc.ChainMetadata{}, 117 | } 118 | 119 | // Only Sapling transactions have a meaningful compact encoding 120 | saplingTxns := make([]*walletrpc.CompactTx, 0, len(b.vtx)) 121 | for idx, tx := range b.vtx { 122 | if tx.HasShieldedElements() { 123 | saplingTxns = append(saplingTxns, tx.ToCompact(idx)) 124 | } 125 | } 126 | compactBlock.Vtx = saplingTxns 127 | return compactBlock 128 | } 129 | 130 | // ParseFromSlice deserializes a block from the given data stream 131 | // and returns a slice to the remaining data. The caller should verify 132 | // there is no remaining data if none is expected. 133 | func (b *Block) ParseFromSlice(data []byte) (rest []byte, err error) { 134 | hdr := NewBlockHeader() 135 | data, err = hdr.ParseFromSlice(data) 136 | if err != nil { 137 | return nil, fmt.Errorf("parsing block header: %w", err) 138 | } 139 | 140 | s := bytestring.String(data) 141 | var txCount int 142 | if !s.ReadCompactSize(&txCount) { 143 | return nil, errors.New("could not read tx_count") 144 | } 145 | data = []byte(s) 146 | 147 | vtx := make([]*Transaction, 0, txCount) 148 | var i int 149 | for i = 0; i < txCount && len(data) > 0; i++ { 150 | tx := NewTransaction() 151 | data, err = tx.ParseFromSlice(data) 152 | if err != nil { 153 | return nil, fmt.Errorf("error parsing transaction %d: %w", i, err) 154 | } 155 | vtx = append(vtx, tx) 156 | } 157 | if i < txCount { 158 | return nil, errors.New("parsing block transactions: not enough data") 159 | } 160 | b.hdr = hdr 161 | b.vtx = vtx 162 | return data, nil 163 | } 164 | -------------------------------------------------------------------------------- /parser/block_header.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | // Package parser deserializes the block header from zcashd. 6 | package parser 7 | 8 | import ( 9 | "bytes" 10 | "crypto/sha256" 11 | "encoding/binary" 12 | "errors" 13 | "math/big" 14 | 15 | "github.com/zcash/lightwalletd/parser/internal/bytestring" 16 | ) 17 | 18 | const ( 19 | serBlockHeaderMinusEquihashSize = 140 // size of a serialized block header minus the Equihash solution 20 | equihashSizeMainnet = 1344 // size of a mainnet / testnet Equihash solution in bytes 21 | ) 22 | 23 | // RawBlockHeader implements the block header as defined in version 24 | // 2018.0-beta-29 of the Zcash Protocol Spec. 25 | type RawBlockHeader struct { 26 | // The block version number indicates which set of block validation rules 27 | // to follow. The current and only defined block version number for Zcash 28 | // is 4. 29 | Version int32 30 | 31 | // A SHA-256d hash in internal byte order of the previous block's header. This 32 | // ensures no previous block can be changed without also changing this block's 33 | // header. 34 | HashPrevBlock []byte 35 | 36 | // A SHA-256d hash in internal byte order. The merkle root is derived from 37 | // the hashes of all transactions included in this block, ensuring that 38 | // none of those transactions can be modified without modifying the header. 39 | HashMerkleRoot []byte 40 | 41 | // [Pre-Sapling] A reserved field which should be ignored. 42 | // [Sapling onward] The root LEBS2OSP_256(rt) of the Sapling note 43 | // commitment tree corresponding to the final Sapling treestate of this 44 | // block. 45 | HashFinalSaplingRoot []byte 46 | 47 | // The block time is a Unix epoch time (UTC) when the miner started hashing 48 | // the header (according to the miner). 49 | Time uint32 50 | 51 | // An encoded version of the target threshold this block's header hash must 52 | // be less than or equal to, in the same nBits format used by Bitcoin. 53 | NBitsBytes []byte 54 | 55 | // An arbitrary field that miners can change to modify the header hash in 56 | // order to produce a hash less than or equal to the target threshold. 57 | Nonce []byte 58 | 59 | // The Equihash solution. In the wire format, this is a 60 | // CompactSize-prefixed value. 61 | Solution []byte 62 | } 63 | 64 | // BlockHeader extends RawBlockHeader by adding a cache for the block hash. 65 | type BlockHeader struct { 66 | *RawBlockHeader 67 | cachedHash []byte 68 | } 69 | 70 | // CompactLengthPrefixedLen calculates the total number of bytes needed to 71 | // encode 'length' bytes. 72 | func CompactLengthPrefixedLen(length int) int { 73 | if length < 253 { 74 | return 1 + length 75 | } else if length <= 0xffff { 76 | return 1 + 2 + length 77 | } else if length <= 0xffffffff { 78 | return 1 + 4 + length 79 | } else { 80 | return 1 + 8 + length 81 | } 82 | } 83 | 84 | // WriteCompactLengthPrefixedLen writes the given length to the stream. 85 | func WriteCompactLengthPrefixedLen(buf *bytes.Buffer, length int) { 86 | if length < 253 { 87 | binary.Write(buf, binary.LittleEndian, uint8(length)) 88 | } else if length <= 0xffff { 89 | binary.Write(buf, binary.LittleEndian, byte(253)) 90 | binary.Write(buf, binary.LittleEndian, uint16(length)) 91 | } else if length <= 0xffffffff { 92 | binary.Write(buf, binary.LittleEndian, byte(254)) 93 | binary.Write(buf, binary.LittleEndian, uint32(length)) 94 | } else { 95 | binary.Write(buf, binary.LittleEndian, byte(255)) 96 | binary.Write(buf, binary.LittleEndian, uint64(length)) 97 | } 98 | } 99 | 100 | func writeCompactLengthPrefixed(buf *bytes.Buffer, val []byte) { 101 | WriteCompactLengthPrefixedLen(buf, len(val)) 102 | binary.Write(buf, binary.LittleEndian, val) 103 | } 104 | 105 | func (hdr *RawBlockHeader) getSize() int { 106 | return serBlockHeaderMinusEquihashSize + CompactLengthPrefixedLen(len(hdr.Solution)) 107 | } 108 | 109 | // MarshalBinary returns the block header in serialized form 110 | func (hdr *RawBlockHeader) MarshalBinary() ([]byte, error) { 111 | headerSize := hdr.getSize() 112 | backing := make([]byte, 0, headerSize) 113 | buf := bytes.NewBuffer(backing) 114 | binary.Write(buf, binary.LittleEndian, hdr.Version) 115 | binary.Write(buf, binary.LittleEndian, hdr.HashPrevBlock) 116 | binary.Write(buf, binary.LittleEndian, hdr.HashMerkleRoot) 117 | binary.Write(buf, binary.LittleEndian, hdr.HashFinalSaplingRoot) 118 | binary.Write(buf, binary.LittleEndian, hdr.Time) 119 | binary.Write(buf, binary.LittleEndian, hdr.NBitsBytes) 120 | binary.Write(buf, binary.LittleEndian, hdr.Nonce) 121 | writeCompactLengthPrefixed(buf, hdr.Solution) 122 | return backing[:headerSize], nil 123 | } 124 | 125 | // NewBlockHeader return a pointer to a new block header instance. 126 | func NewBlockHeader() *BlockHeader { 127 | return &BlockHeader{ 128 | RawBlockHeader: new(RawBlockHeader), 129 | } 130 | } 131 | 132 | // ParseFromSlice parses the block header struct from the provided byte slice, 133 | // advancing over the bytes read. If successful it returns the rest of the 134 | // slice, otherwise it returns the input slice unaltered along with an error. 135 | func (hdr *BlockHeader) ParseFromSlice(in []byte) (rest []byte, err error) { 136 | s := bytestring.String(in) 137 | 138 | // Primary parsing layer: sort the bytes into things 139 | 140 | if !s.ReadInt32(&hdr.Version) { 141 | return in, errors.New("could not read header version") 142 | } 143 | 144 | if !s.ReadBytes(&hdr.HashPrevBlock, 32) { 145 | return in, errors.New("could not read HashPrevBlock") 146 | } 147 | 148 | if !s.ReadBytes(&hdr.HashMerkleRoot, 32) { 149 | return in, errors.New("could not read HashMerkleRoot") 150 | } 151 | 152 | if !s.ReadBytes(&hdr.HashFinalSaplingRoot, 32) { 153 | return in, errors.New("could not read HashFinalSaplingRoot") 154 | } 155 | 156 | if !s.ReadUint32(&hdr.Time) { 157 | return in, errors.New("could not read timestamp") 158 | } 159 | 160 | if !s.ReadBytes(&hdr.NBitsBytes, 4) { 161 | return in, errors.New("could not read NBits bytes") 162 | } 163 | 164 | if !s.ReadBytes(&hdr.Nonce, 32) { 165 | return in, errors.New("could not read Nonce bytes") 166 | } 167 | 168 | if !s.ReadCompactLengthPrefixed((*bytestring.String)(&hdr.Solution)) { 169 | return in, errors.New("could not read CompactSize-prefixed Equihash solution") 170 | } 171 | 172 | // TODO: interpret the bytes 173 | //hdr.targetThreshold = parseNBits(hdr.NBitsBytes) 174 | 175 | return []byte(s), nil 176 | } 177 | 178 | func parseNBits(b []byte) *big.Int { 179 | byteLen := int(b[0]) 180 | 181 | targetBytes := make([]byte, byteLen) 182 | copy(targetBytes, b[1:]) 183 | 184 | // If high bit set, return a negative result. This is in the Bitcoin Core 185 | // test vectors even though Bitcoin itself will never produce or interpret 186 | // a difficulty lower than zero. 187 | if b[1]&0x80 != 0 { 188 | targetBytes[0] &= 0x7F 189 | target := new(big.Int).SetBytes(targetBytes) 190 | target.Neg(target) 191 | return target 192 | } 193 | 194 | return new(big.Int).SetBytes(targetBytes) 195 | } 196 | 197 | // GetDisplayHash returns the bytes of a block hash in big-endian order. 198 | func (hdr *BlockHeader) GetDisplayHash() []byte { 199 | if hdr.cachedHash != nil { 200 | return hdr.cachedHash 201 | } 202 | 203 | serializedHeader, err := hdr.MarshalBinary() 204 | if err != nil { 205 | return nil 206 | } 207 | 208 | // SHA256d 209 | digest := sha256.Sum256(serializedHeader) 210 | digest = sha256.Sum256(digest[:]) 211 | 212 | // Convert to big-endian 213 | hdr.cachedHash = Reverse(digest[:]) 214 | return hdr.cachedHash 215 | } 216 | 217 | // GetEncodableHash returns the bytes of a block hash in little-endian wire order. 218 | func (hdr *BlockHeader) GetEncodableHash() []byte { 219 | serializedHeader, err := hdr.MarshalBinary() 220 | 221 | if err != nil { 222 | return nil 223 | } 224 | 225 | // SHA256d 226 | digest := sha256.Sum256(serializedHeader) 227 | digest = sha256.Sum256(digest[:]) 228 | 229 | return digest[:] 230 | } 231 | 232 | // GetDisplayPrevHash returns the block hash in big-endian order. 233 | func (hdr *BlockHeader) GetDisplayPrevHash() []byte { 234 | return Reverse(hdr.HashPrevBlock) 235 | } 236 | -------------------------------------------------------------------------------- /parser/block_header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package parser 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "encoding/hex" 10 | "math/big" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | // https://bitcoin.org/en/developer-reference#target-nbits 16 | var nbitsTests = []struct { 17 | bytes []byte 18 | target string 19 | }{ 20 | { 21 | []byte{0x18, 0x1b, 0xc3, 0x30}, 22 | "1bc330000000000000000000000000000000000000000000", 23 | }, 24 | { 25 | []byte{0x01, 0x00, 0x34, 0x56}, 26 | "00", 27 | }, 28 | { 29 | []byte{0x01, 0x12, 0x34, 0x56}, 30 | "12", 31 | }, 32 | { 33 | []byte{0x02, 0x00, 0x80, 00}, 34 | "80", 35 | }, 36 | { 37 | []byte{0x05, 0x00, 0x92, 0x34}, 38 | "92340000", 39 | }, 40 | { 41 | []byte{0x04, 0x92, 0x34, 0x56}, 42 | "-12345600", 43 | }, 44 | { 45 | []byte{0x04, 0x12, 0x34, 0x56}, 46 | "12345600", 47 | }, 48 | } 49 | 50 | func TestParseNBits(t *testing.T) { 51 | for i, tt := range nbitsTests { 52 | target := parseNBits(tt.bytes) 53 | expected, _ := new(big.Int).SetString(tt.target, 16) 54 | if target.Cmp(expected) != 0 { 55 | t.Errorf("NBits parsing failed case %d:\nwant: %x\nhave: %x", i, expected, target) 56 | } 57 | } 58 | } 59 | 60 | func TestBlockHeader(t *testing.T) { 61 | testBlocks, err := os.Open("../testdata/blocks") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer testBlocks.Close() 66 | 67 | lastBlockTime := uint32(0) 68 | 69 | scan := bufio.NewScanner(testBlocks) 70 | var prevHash []byte 71 | for scan.Scan() { 72 | blockDataHex := scan.Text() 73 | blockData, err := hex.DecodeString(blockDataHex) 74 | if err != nil { 75 | t.Error(err) 76 | continue 77 | } 78 | 79 | blockHeader := NewBlockHeader() 80 | _, err = blockHeader.ParseFromSlice(blockData) 81 | if err != nil { 82 | t.Error(err) 83 | continue 84 | } 85 | 86 | // Some basic sanity checks 87 | if blockHeader.Version != 4 { 88 | t.Error("Read wrong version in a test block.") 89 | break 90 | } 91 | 92 | if blockHeader.Time < lastBlockTime { 93 | t.Error("Block times not increasing.") 94 | break 95 | } 96 | lastBlockTime = blockHeader.Time 97 | 98 | if len(blockHeader.Solution) != equihashSizeMainnet { 99 | t.Error("Got wrong Equihash solution size.") 100 | break 101 | } 102 | 103 | // Re-serialize and check for consistency 104 | serializedHeader, err := blockHeader.MarshalBinary() 105 | if err != nil { 106 | t.Errorf("Error serializing header: %v", err) 107 | break 108 | } 109 | 110 | if !bytes.Equal(serializedHeader, blockData[:serBlockHeaderMinusEquihashSize+3+equihashSizeMainnet]) { 111 | offset := 0 112 | length := 0 113 | for i := 0; i < len(serializedHeader); i++ { 114 | if serializedHeader[i] != blockData[i] { 115 | if offset == 0 { 116 | offset = i 117 | } 118 | length++ 119 | } 120 | } 121 | t.Errorf( 122 | "Block header failed round-trip:\ngot\n%x\nwant\n%x\nfirst diff at %d", 123 | serializedHeader[offset:offset+length], 124 | blockData[offset:offset+length], 125 | offset, 126 | ) 127 | break 128 | } 129 | 130 | hash := blockHeader.GetDisplayHash() 131 | // test caching 132 | if !bytes.Equal(hash, blockHeader.GetDisplayHash()) { 133 | t.Error("caching is broken") 134 | } 135 | 136 | // This is not necessarily true for anything but our current test cases. 137 | for _, b := range hash[:1] { 138 | if b != 0 { 139 | t.Errorf("Hash lacked leading zeros: %x", hash) 140 | } 141 | } 142 | if prevHash != nil && !bytes.Equal(blockHeader.GetDisplayPrevHash(), prevHash) { 143 | t.Errorf("Previous hash mismatch") 144 | } 145 | prevHash = hash 146 | } 147 | } 148 | 149 | func TestBadBlockHeader(t *testing.T) { 150 | testBlocks, err := os.Open("../testdata/badblocks") 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | defer testBlocks.Close() 155 | 156 | scan := bufio.NewScanner(testBlocks) 157 | 158 | // the first "block" contains an illegal hex character 159 | { 160 | scan.Scan() 161 | blockDataHex := scan.Text() 162 | _, err := hex.DecodeString(blockDataHex) 163 | if err == nil { 164 | t.Error("unexpected success parsing illegal hex bad block") 165 | } 166 | } 167 | // these bad blocks are short in various ways 168 | for i := 1; scan.Scan(); i++ { 169 | blockDataHex := scan.Text() 170 | blockData, err := hex.DecodeString(blockDataHex) 171 | if err != nil { 172 | t.Error(err) 173 | continue 174 | } 175 | 176 | blockHeader := NewBlockHeader() 177 | _, err = blockHeader.ParseFromSlice(blockData) 178 | if err == nil { 179 | t.Errorf("unexpected success parsing bad block %d", i) 180 | } 181 | } 182 | } 183 | 184 | var compactLengthPrefixedLenTests = []struct { 185 | length int 186 | returnLength int 187 | }{ 188 | /* 00 */ {0, 1}, 189 | /* 01 */ {1, 1 + 1}, 190 | /* 02 */ {2, 1 + 2}, 191 | /* 03 */ {252, 1 + 252}, 192 | /* 04 */ {253, 1 + 2 + 253}, 193 | /* 05 */ {0xffff, 1 + 2 + 0xffff}, 194 | /* 06 */ {0x10000, 1 + 4 + 0x10000}, 195 | /* 07 */ {0x10001, 1 + 4 + 0x10001}, 196 | /* 08 */ {0xffffffff, 1 + 4 + 0xffffffff}, 197 | /* 09 */ {0x100000000, 1 + 8 + 0x100000000}, 198 | /* 10 */ {0x100000001, 1 + 8 + 0x100000001}, 199 | } 200 | 201 | func TestCompactLengthPrefixedLen(t *testing.T) { 202 | for i, tt := range compactLengthPrefixedLenTests { 203 | returnLength := CompactLengthPrefixedLen(tt.length) 204 | if returnLength != tt.returnLength { 205 | t.Errorf("TestCompactLengthPrefixedLen case %d: want: %v have %v", 206 | i, tt.returnLength, returnLength) 207 | } 208 | } 209 | } 210 | 211 | var writeCompactLengthPrefixedTests = []struct { 212 | argLen int 213 | returnLength int 214 | header []byte 215 | }{ 216 | /* 00 */ {0, 1, []byte{0}}, 217 | /* 01 */ {1, 1, []byte{1}}, 218 | /* 02 */ {2, 1, []byte{2}}, 219 | /* 03 */ {252, 1, []byte{252}}, 220 | /* 04 */ {253, 1 + 2, []byte{253, 253, 0}}, 221 | /* 05 */ {254, 1 + 2, []byte{253, 254, 0}}, 222 | /* 06 */ {0xffff, 1 + 2, []byte{253, 0xff, 0xff}}, 223 | /* 07 */ {0x10000, 1 + 4, []byte{254, 0x00, 0x00, 0x01, 0x00}}, 224 | /* 08 */ {0x10003, 1 + 4, []byte{254, 0x03, 0x00, 0x01, 0x00}}, 225 | /* 09 */ {0xffffffff, 1 + 4, []byte{254, 0xff, 0xff, 0xff, 0xff}}, 226 | /* 10 */ {0x100000000, 1 + 8, []byte{255, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}}, 227 | /* 11 */ {0x100000007, 1 + 8, []byte{255, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}}, 228 | } 229 | 230 | func TestWriteCompactLengthPrefixedLen(t *testing.T) { 231 | for i, tt := range writeCompactLengthPrefixedTests { 232 | var b bytes.Buffer 233 | WriteCompactLengthPrefixedLen(&b, tt.argLen) 234 | if b.Len() != tt.returnLength { 235 | t.Fatalf("TestWriteCompactLengthPrefixed case %d: unexpected length", i) 236 | } 237 | // check the header (tag and length) 238 | r := make([]byte, len(tt.header)) 239 | b.Read(r) 240 | if !bytes.Equal(r, tt.header) { 241 | t.Fatalf("TestWriteCompactLengthPrefixed case %d: incorrect header", i) 242 | } 243 | if b.Len() > 0 { 244 | t.Fatalf("TestWriteCompactLengthPrefixed case %d: unexpected data remaining", i) 245 | } 246 | } 247 | } 248 | 249 | func TestWriteCompactLengthPrefixed(t *testing.T) { 250 | var b bytes.Buffer 251 | val := []byte{22, 33, 44} 252 | writeCompactLengthPrefixed(&b, val) 253 | r := make([]byte, 4) 254 | b.Read(r) 255 | expected := []byte{3, 22, 33, 44} 256 | if !bytes.Equal(r, expected) { 257 | t.Fatal("TestWriteCompactLengthPrefixed incorrect result") 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /parser/block_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package parser 5 | 6 | import ( 7 | "bytes" 8 | "encoding/hex" 9 | "encoding/json" 10 | "fmt" 11 | "os" 12 | "testing" 13 | 14 | protobuf "github.com/golang/protobuf/proto" 15 | ) 16 | 17 | func TestCompactBlocks(t *testing.T) { 18 | type compactTest struct { 19 | BlockHeight int `json:"block"` 20 | BlockHash string `json:"hash"` 21 | PrevHash string `json:"prev"` 22 | Full string `json:"full"` 23 | Compact string `json:"compact"` 24 | } 25 | var compactTests []compactTest 26 | 27 | blockJSON, err := os.ReadFile("../testdata/compact_blocks.json") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | err = json.Unmarshal(blockJSON, &compactTests) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | for _, test := range compactTests { 38 | blockData, _ := hex.DecodeString(test.Full) 39 | block := NewBlock() 40 | blockData, err = block.ParseFromSlice(blockData) 41 | if err != nil { 42 | t.Error(fmt.Errorf("error parsing testnet block %d: %w", test.BlockHeight, err)) 43 | continue 44 | } 45 | if len(blockData) > 0 { 46 | t.Error("Extra data remaining") 47 | } 48 | if block.GetHeight() != test.BlockHeight { 49 | t.Errorf("incorrect block height in testnet block %d", test.BlockHeight) 50 | continue 51 | } 52 | if hex.EncodeToString(block.GetDisplayHash()) != test.BlockHash { 53 | t.Errorf("incorrect block hash in testnet block %x", test.BlockHash) 54 | continue 55 | } 56 | if hex.EncodeToString(block.GetDisplayPrevHash()) != test.PrevHash { 57 | t.Errorf("incorrect block prevhash in testnet block %x", test.BlockHash) 58 | continue 59 | } 60 | if !bytes.Equal(block.GetPrevHash(), block.hdr.HashPrevBlock) { 61 | t.Error("block and block header prevhash don't match") 62 | } 63 | 64 | compact := block.ToCompact() 65 | marshaled, err := protobuf.Marshal(compact) 66 | if err != nil { 67 | t.Errorf("could not marshal compact testnet block %d", test.BlockHeight) 68 | continue 69 | } 70 | encodedCompact := hex.EncodeToString(marshaled) 71 | if encodedCompact != test.Compact { 72 | t.Errorf("wrong data for compact testnet block %d\nhave: %s\nwant: %s\n", test.BlockHeight, encodedCompact, test.Compact) 73 | break 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /parser/fuzz.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | // +build gofuzz 5 | 6 | package parser 7 | 8 | func Fuzz(data []byte) int { 9 | block := NewBlock() 10 | _, err := block.ParseFromSlice(data) 11 | if err != nil { 12 | return 0 13 | } 14 | return 1 15 | } 16 | -------------------------------------------------------------------------------- /parser/internal/bytestring/bytestring.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | // Package bytestring provides a cryptobyte-inspired API specialized to the 6 | // needs of parsing Zcash transactions. 7 | package bytestring 8 | 9 | import ( 10 | "io" 11 | ) 12 | 13 | const maxCompactSize uint64 = 0x02000000 14 | 15 | const ( 16 | op0 uint8 = 0x00 17 | op1Negate uint8 = 0x4f 18 | op1 uint8 = 0x51 19 | op16 uint8 = 0x60 20 | ) 21 | 22 | // String represents a string of bytes and provides methods for parsing values 23 | // from it. 24 | type String []byte 25 | 26 | // read advances the string by n bytes and returns them. If fewer than n bytes 27 | // remain, it returns nil. 28 | func (s *String) read(n int) []byte { 29 | if len(*s) < n { 30 | return nil 31 | } 32 | 33 | out := (*s)[:n] 34 | (*s) = (*s)[n:] 35 | return out 36 | } 37 | 38 | // Read reads the next len(p) bytes from the string, or the remainder of the 39 | // string if len(*s) < len(p). It returns the number of bytes read as n. If the 40 | // string is empty it returns an io.EOF error, or a nil error if len(p) == 0. 41 | // Read satisfies io.Reader. 42 | func (s *String) Read(p []byte) (n int, err error) { 43 | if s.Empty() { 44 | if len(p) == 0 { 45 | return 0, nil 46 | } 47 | return 0, io.EOF 48 | } 49 | 50 | n = copy(p, *s) 51 | s.Skip(n) 52 | return n, nil 53 | } 54 | 55 | // Empty reports whether or not the string is empty. 56 | func (s *String) Empty() bool { 57 | return len(*s) == 0 58 | } 59 | 60 | // Skip advances the string by n bytes and reports whether it was successful. 61 | func (s *String) Skip(n int) bool { 62 | if len(*s) < n { 63 | return false 64 | } 65 | (*s) = (*s)[n:] 66 | return true 67 | } 68 | 69 | // ReadByte reads a single byte into out and advances over it. It reports if 70 | // the read was successful. 71 | func (s *String) ReadByte(out *byte) bool { 72 | v := s.read(1) 73 | if v == nil { 74 | return false 75 | } 76 | *out = v[0] 77 | return true 78 | } 79 | 80 | // ReadBytes reads n bytes into out and advances over them. It reports if the 81 | // read was successful. 82 | func (s *String) ReadBytes(out *[]byte, n int) bool { 83 | v := s.read(n) 84 | if v == nil { 85 | return false 86 | } 87 | *out = v 88 | return true 89 | } 90 | 91 | // ReadCompactSize reads and interprets a Bitcoin-custom compact integer 92 | // encoding used for length-prefixing and count values. If the values fall 93 | // outside the expected canonical ranges, it returns false. 94 | func (s *String) ReadCompactSize(size *int) bool { 95 | *size = 0 96 | lenBytes := s.read(1) 97 | if lenBytes == nil { 98 | return false 99 | } 100 | lenByte := lenBytes[0] 101 | 102 | var lenLen int 103 | var length, minSize uint64 104 | 105 | switch { 106 | case lenByte < 253: 107 | length = uint64(lenByte) 108 | case lenByte == 253: 109 | lenLen = 2 110 | minSize = 253 111 | case lenByte == 254: 112 | lenLen = 4 113 | minSize = 0x10000 114 | case lenByte == 255: 115 | // this case is not currently usable, beyond maxCompactSize; 116 | // also, this is not possible if sizeof(int) is 4 bytes 117 | // lenLen = 8; minSize = 0x100000000 118 | return false 119 | } 120 | 121 | if lenLen > 0 { 122 | // expect little endian uint of varying size 123 | lenBytes := s.read(lenLen) 124 | if len(lenBytes) < lenLen { 125 | return false 126 | } 127 | for i := lenLen - 1; i >= 0; i-- { 128 | length <<= 8 129 | length = length | uint64(lenBytes[i]) 130 | } 131 | } 132 | 133 | if length > maxCompactSize || length < minSize { 134 | return false 135 | } 136 | *size = int(length) 137 | return true 138 | } 139 | 140 | // ReadCompactLengthPrefixed reads data prefixed by a CompactSize-encoded 141 | // length field into out. It reports whether the read was successful. 142 | func (s *String) ReadCompactLengthPrefixed(out *String) bool { 143 | var length int 144 | if !s.ReadCompactSize(&length) { 145 | return false 146 | } 147 | 148 | v := s.read(length) 149 | if v == nil { 150 | return false 151 | } 152 | 153 | *out = v 154 | return true 155 | } 156 | 157 | // SkipCompactLengthPrefixed skips a CompactSize-encoded 158 | // length field. 159 | func (s *String) SkipCompactLengthPrefixed() bool { 160 | var length int 161 | if !s.ReadCompactSize(&length) { 162 | return false 163 | } 164 | return s.Skip(length) 165 | } 166 | 167 | // ReadInt32 decodes a little-endian 32-bit value into out, treating it as 168 | // signed, and advances over it. It reports whether the read was successful. 169 | func (s *String) ReadInt32(out *int32) bool { 170 | var tmp uint32 171 | if !s.ReadUint32(&tmp) { 172 | return false 173 | } 174 | 175 | *out = int32(tmp) 176 | return true 177 | } 178 | 179 | // ReadInt64 decodes a little-endian 64-bit value into out, treating it as 180 | // signed, and advances over it. It reports whether the read was successful. 181 | func (s *String) ReadInt64(out *int64) bool { 182 | var tmp uint64 183 | if !s.ReadUint64(&tmp) { 184 | return false 185 | } 186 | 187 | *out = int64(tmp) 188 | return true 189 | } 190 | 191 | // ReadUint16 decodes a little-endian, 16-bit value into out and advances over 192 | // it. It reports whether the read was successful. 193 | func (s *String) ReadUint16(out *uint16) bool { 194 | v := s.read(2) 195 | if v == nil { 196 | return false 197 | } 198 | *out = 0 199 | for i := 1; i >= 0; i-- { 200 | *out <<= 8 201 | *out |= uint16(v[i]) 202 | } 203 | return true 204 | } 205 | 206 | // ReadUint32 decodes a little-endian, 32-bit value into out and advances over 207 | // it. It reports whether the read was successful. 208 | func (s *String) ReadUint32(out *uint32) bool { 209 | v := s.read(4) 210 | if v == nil { 211 | return false 212 | } 213 | *out = 0 214 | for i := 3; i >= 0; i-- { 215 | *out <<= 8 216 | *out |= uint32(v[i]) 217 | } 218 | return true 219 | } 220 | 221 | // ReadUint64 decodes a little-endian, 64-bit value into out and advances over 222 | // it. It reports whether the read was successful. 223 | func (s *String) ReadUint64(out *uint64) bool { 224 | v := s.read(8) 225 | if v == nil { 226 | return false 227 | } 228 | *out = 0 229 | for i := 7; i >= 0; i-- { 230 | *out <<= 8 231 | *out |= uint64(v[i]) 232 | } 233 | return true 234 | } 235 | 236 | // ReadScriptInt64 reads and interprets a Bitcoin-custom compact integer 237 | // encoding used for int64 numbers in scripts. 238 | // 239 | // Serializer in zcashd: 240 | // https://github.com/zcash/zcash/blob/4df60f4b334dd9aee5df3a481aee63f40b52654b/src/script/script.h#L363-L378 241 | // 242 | // Partial parser in zcashd: 243 | // https://github.com/zcash/zcash/blob/4df60f4b334dd9aee5df3a481aee63f40b52654b/src/script/interpreter.cpp#L308-L335 244 | func (s *String) ReadScriptInt64(num *int64) bool { 245 | // First byte is either an integer opcode, or the number of bytes in the 246 | // number. 247 | *num = 0 248 | firstBytes := s.read(1) 249 | if firstBytes == nil { 250 | return false 251 | } 252 | firstByte := firstBytes[0] 253 | 254 | var number uint64 255 | 256 | if firstByte == op1Negate { 257 | *num = -1 258 | return true 259 | } else if firstByte == op0 { 260 | number = 0 261 | } else if firstByte >= op1 && firstByte <= op16 { 262 | number = uint64(firstByte) - uint64(op1-1) 263 | } else { 264 | numLen := int(firstByte) 265 | // expect little endian int of varying size 266 | numBytes := s.read(numLen) 267 | if numBytes == nil { 268 | return false 269 | } 270 | for i := numLen - 1; i >= 0; i-- { 271 | number <<= 8 272 | number = number | uint64(numBytes[i]) 273 | } 274 | } 275 | 276 | *num = int64(number) 277 | return true 278 | } 279 | -------------------------------------------------------------------------------- /parser/transaction_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package parser 5 | 6 | import ( 7 | "encoding/hex" 8 | "encoding/json" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | // Some of these values may be "null" (which translates to nil in Go) in 14 | // the test data, so we have *_set variables to indicate if the corresponding 15 | // variable is non-null. (There is an "optional" package we could use for 16 | // these but it doesn't seem worth pulling it in.) 17 | type TxTestData struct { 18 | Tx string 19 | Txid string 20 | Version int 21 | NVersionGroupId int 22 | NConsensusBranchId int 23 | Tx_in_count int 24 | Tx_out_count int 25 | NSpendsSapling int 26 | NoutputsSapling int 27 | NActionsOrchard int 28 | } 29 | 30 | // https://jhall.io/posts/go-json-tricks-array-as-structs/ 31 | func (r *TxTestData) UnmarshalJSON(p []byte) error { 32 | var t []interface{} 33 | if err := json.Unmarshal(p, &t); err != nil { 34 | return err 35 | } 36 | r.Tx = t[0].(string) 37 | r.Txid = t[1].(string) 38 | r.Version = int(t[2].(float64)) 39 | r.NVersionGroupId = int(t[3].(float64)) 40 | r.NConsensusBranchId = int(t[4].(float64)) 41 | r.Tx_in_count = int(t[7].(float64)) 42 | r.Tx_out_count = int(t[8].(float64)) 43 | r.NSpendsSapling = int(t[9].(float64)) 44 | r.NoutputsSapling = int(t[10].(float64)) 45 | r.NActionsOrchard = int(t[14].(float64)) 46 | return nil 47 | } 48 | 49 | func TestV5TransactionParser(t *testing.T) { 50 | // The raw data are stored in a separate file because they're large enough 51 | // to make the test table difficult to scroll through. They are in the same 52 | // order as the test table above. If you update the test table without 53 | // adding a line to the raw file, this test will panic due to index 54 | // misalignment. 55 | s, err := os.ReadFile("../testdata/tx_v5.json") 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | var testdata []json.RawMessage 61 | err = json.Unmarshal(s, &testdata) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | if len(testdata) < 3 { 66 | t.Fatal("tx_vt.json has too few lines") 67 | } 68 | testdata = testdata[2:] 69 | for _, onetx := range testdata { 70 | var txtestdata TxTestData 71 | 72 | err = json.Unmarshal(onetx, &txtestdata) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | t.Logf("txid %s", txtestdata.Txid) 77 | rawTxData, _ := hex.DecodeString(txtestdata.Tx) 78 | 79 | tx := NewTransaction() 80 | rest, err := tx.ParseFromSlice(rawTxData) 81 | if err != nil { 82 | t.Fatalf("%v", err) 83 | } 84 | if len(rest) != 0 { 85 | t.Fatalf("Test did not consume entire buffer, %d remaining", len(rest)) 86 | } 87 | // Currently, we can't check the txid because we get that from 88 | // zcashd (getblock rpc) rather than computing it ourselves. 89 | // https://github.com/zcash/lightwalletd/issues/392 90 | if tx.version != uint32(txtestdata.Version) { 91 | t.Fatal("version miscompare") 92 | } 93 | if tx.nVersionGroupID != uint32(txtestdata.NVersionGroupId) { 94 | t.Fatal("nVersionGroupId miscompare") 95 | } 96 | if tx.consensusBranchID != uint32(txtestdata.NConsensusBranchId) { 97 | t.Fatal("consensusBranchID miscompare") 98 | } 99 | if len(tx.transparentInputs) != int(txtestdata.Tx_in_count) { 100 | t.Fatal("tx_in_count miscompare") 101 | } 102 | if len(tx.transparentOutputs) != int(txtestdata.Tx_out_count) { 103 | t.Fatal("tx_out_count miscompare") 104 | } 105 | if len(tx.shieldedSpends) != int(txtestdata.NSpendsSapling) { 106 | t.Fatal("NSpendsSapling miscompare") 107 | } 108 | if len(tx.shieldedOutputs) != int(txtestdata.NoutputsSapling) { 109 | t.Fatal("NOutputsSapling miscompare") 110 | } 111 | if len(tx.orchardActions) != int(txtestdata.NActionsOrchard) { 112 | t.Fatal("NActionsOrchard miscompare") 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /parser/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | package parser 6 | 7 | // Reverse the given byte slice, returning a slice pointing to new data; 8 | // the input slice is unchanged. 9 | func Reverse(a []byte) []byte { 10 | r := make([]byte, len(a), len(a)) 11 | for left, right := 0, len(a)-1; left <= right; left, right = left+1, right-1 { 12 | r[left], r[right] = a[right], a[left] 13 | } 14 | return r 15 | } 16 | -------------------------------------------------------------------------------- /parser/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | package parser 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestReverse(t *testing.T) { 12 | s := make([]byte, 32, 32) 13 | for i := 0; i < 32; i++ { 14 | s[i] = byte(i) 15 | } 16 | r := Reverse(s) 17 | for i := 0; i < 32; i++ { 18 | if r[i] != byte(32-1-i) { 19 | t.Fatal("mismatch") 20 | } 21 | } 22 | } 23 | 24 | // Currently, Reverse() isn't called for odd-length slices, but 25 | // it should work. 26 | func TestReverseOdd(t *testing.T) { 27 | s := make([]byte, 5, 5) 28 | for i := 0; i < 5; i++ { 29 | s[i] = byte(i) 30 | } 31 | r := Reverse(s) 32 | for i := 0; i < 5; i++ { 33 | if r[i] != byte(5-1-i) { 34 | t.Fatal("mismatch") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tekton/resources.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1alpha1 3 | kind: PipelineResource 4 | metadata: 5 | name: lightwalletd-image 6 | spec: 7 | type: image 8 | params: 9 | - name: url 10 | value: electriccoinco/lightwalletd 11 | --- 12 | apiVersion: tekton.dev/v1alpha1 13 | kind: PipelineResource 14 | metadata: 15 | name: lightwalletd-git 16 | spec: 17 | type: git 18 | params: 19 | - name: revision 20 | value: master 21 | - name: url 22 | value: https://github.com/zcash/lightwalletd.git -------------------------------------------------------------------------------- /tekton/taskruns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1alpha1 3 | kind: TaskRun 4 | metadata: 5 | generateName: lightwalletd-dockerbuild- 6 | spec: 7 | serviceAccountName: zcashsysadmin-service 8 | taskRef: 9 | name: build-docker-image-from-git-source 10 | inputs: 11 | resources: 12 | - name: docker-source 13 | resourceRef: 14 | name: lightwalletd-git 15 | params: 16 | - name: pathToDockerFile 17 | value: /workspace/docker-source/Dockerfile 18 | - name: pathToContext 19 | value: /workspace/docker-source/ 20 | outputs: 21 | resources: 22 | - name: builtImage 23 | resourceRef: 24 | name: lightwalletd-image 25 | - name: notification 26 | resourceRef: 27 | name: event-to-cloudlog -------------------------------------------------------------------------------- /tekton/triggerbinding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1alpha1 3 | kind: TriggerBinding 4 | metadata: 5 | name: lightwalletd-master-binding 6 | spec: 7 | params: 8 | - name: newSHA 9 | value: $(body.after) 10 | --- 11 | apiVersion: tekton.dev/v1alpha1 12 | kind: TriggerTemplate 13 | metadata: 14 | name: lightwalletd-dockerbuild-template 15 | spec: 16 | params: 17 | - name: newSHA 18 | description: The git repository HEAD sha 19 | resourcetemplates: 20 | - apiVersion: tekton.dev/v1alpha1 21 | kind: TaskRun 22 | metadata: 23 | generateName: lightwalletd-dockerbuild- 24 | spec: 25 | serviceAccountName: zcashsysadmin-service 26 | taskRef: 27 | name: build-docker-image-from-git-source 28 | inputs: 29 | resources: 30 | - name: docker-source 31 | resourceRef: 32 | name: lightwalletd-git 33 | params: 34 | - name: pathToDockerFile 35 | value: /workspace/docker-source/Dockerfile 36 | - name: pathToContext 37 | value: /workspace/docker-source/ 38 | outputs: 39 | resources: 40 | - name: builtImage 41 | resourceRef: 42 | name: lightwalletd-image 43 | - name: notification 44 | resourceRef: 45 | name: event-to-cloudlog -------------------------------------------------------------------------------- /testclient/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2015 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | // Package main implements a gRPC test client for lightwalletd. 19 | // This file adapted from: 20 | // https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go 21 | // For now at least, all it does is generate a load for performance and stress testing. 22 | package main 23 | 24 | import ( 25 | "context" 26 | "flag" 27 | "io" 28 | "log" 29 | "strconv" 30 | "sync" 31 | "time" 32 | 33 | pb "github.com/zcash/lightwalletd/walletrpc" 34 | "google.golang.org/grpc" 35 | ) 36 | 37 | const ( 38 | address = "localhost:9067" 39 | ) 40 | 41 | type Options struct { 42 | concurrency int `json:"concurrency"` 43 | iterations int `json:"iterations"` 44 | op string `json:"op"` 45 | verbose *bool `json:"v"` 46 | } 47 | 48 | func main() { 49 | opts := &Options{} 50 | flag.IntVar(&opts.concurrency, "concurrency", 1, "number of threads") 51 | flag.IntVar(&opts.iterations, "iterations", 1, "number of iterations") 52 | flag.StringVar(&opts.op, "op", "ping", "operation(ping|getlightdinfo|getblock|getblockrange)") 53 | opts.verbose = flag.Bool("v", false, "verbose (print operation results)") 54 | flag.Parse() 55 | 56 | // Remaining args are all integers (at least for now) 57 | args := make([]int64, flag.NArg()) 58 | for i := 0; i < flag.NArg(); i++ { 59 | var err error 60 | if args[i], err = strconv.ParseInt(flag.Arg(i), 10, 64); err != nil { 61 | log.Fatalf("argument %v is not an int64: %v", flag.Arg(i), err) 62 | } 63 | } 64 | 65 | // Set up a connection to the server. 66 | conn, err := grpc.Dial(address, grpc.WithInsecure(), 67 | grpc.WithConnectParams(grpc.ConnectParams{MinConnectTimeout: 30 * time.Second})) 68 | if err != nil { 69 | log.Fatalf("did not connect: %v", err) 70 | } 71 | defer conn.Close() 72 | c := pb.NewCompactTxStreamerClient(conn) 73 | 74 | ctx, cancel := context.WithTimeout(context.Background(), 100000*time.Second) 75 | defer cancel() 76 | var wg sync.WaitGroup 77 | wg.Add(opts.concurrency) 78 | for i := 0; i < opts.concurrency; i++ { 79 | go func(i int) { 80 | for j := 0; j < opts.iterations; j++ { 81 | switch opts.op { 82 | case "ping": 83 | var a pb.Duration 84 | a.IntervalUs = 8 * 1000 * 1000 // default 8 seconds 85 | if len(args) > 0 { 86 | a.IntervalUs = args[0] 87 | } 88 | r, err := c.Ping(ctx, &a) 89 | if err != nil { 90 | log.Fatalf("Ping failed: %v", err) 91 | } 92 | if *opts.verbose { 93 | log.Println("thr:", i, "entry:", r.Entry, "exit:", r.Exit) 94 | } 95 | case "getlightdinfo": 96 | r, err := c.GetLightdInfo(ctx, &pb.Empty{}) 97 | if err != nil { 98 | log.Fatalf("GetLightwalletdInfo failed: %v", err) 99 | } 100 | if *opts.verbose { 101 | log.Println("thr:", i, r) 102 | } 103 | case "getblock": 104 | blockid := &pb.BlockID{Height: 748400} // default (arbitrary) 105 | if len(args) > 0 { 106 | blockid.Height = uint64(args[0]) 107 | } 108 | r, err := c.GetBlock(ctx, blockid) 109 | if err != nil { 110 | log.Fatalf("GetLightwalletdInfo failed: %v", err) 111 | } 112 | // Height is enough to see if it's working 113 | if *opts.verbose { 114 | log.Println("thr:", i, r.Height) 115 | } 116 | case "getblockrange": 117 | blockrange := &pb.BlockRange{ // defaults (arbitrary) 118 | Start: &pb.BlockID{Height: 738100}, 119 | End: &pb.BlockID{Height: 738199}, 120 | } 121 | if len(args) > 0 { 122 | blockrange.Start.Height = uint64(args[0]) 123 | blockrange.End.Height = uint64(args[1]) 124 | } 125 | stream, err := c.GetBlockRange(ctx, blockrange) 126 | if err != nil { 127 | log.Fatalf("GetLightwalletdInfo failed: %v", err) 128 | } 129 | for { 130 | // each call to Recv returns a compact block 131 | r, err := stream.Recv() 132 | if err == io.EOF { 133 | break 134 | } 135 | if err != nil { 136 | log.Fatal(err) 137 | } 138 | // Height is enough to see if it's working 139 | if *opts.verbose { 140 | log.Println("thr:", i, r.Height) 141 | } 142 | } 143 | default: 144 | log.Fatalf("unknown op %s", opts.op) 145 | } 146 | } 147 | wg.Done() 148 | }(i) 149 | } 150 | wg.Wait() 151 | } 152 | -------------------------------------------------------------------------------- /testclient/stress.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Create a CSV file with various performance measurements 4 | # 5 | set -e 6 | test $# -eq 0 && { echo "usage: $0 iterations op(getlighdinfo|getblock|getblockrange)";exit 1;} 7 | iterations=$1 8 | op=$2 9 | export p=`pidof server` 10 | test -z $p && { echo 'is the server running?';exit 1;} 11 | set -- $p 12 | test $# -ne 1 && { echo 'server pid is not unique';exit 1;} 13 | echo "concurrency,iterations per thread,utime before (ticks),stime before (ticks),memory before (pages),time (sec),utime after (ticks),stime after (ticks),memory after (pages)" 14 | for i in 1 200 400 600 800 1000 15 | do 16 | csv="$i,$iterations" 17 | csv="$csv,`cat /proc/$p/stat|field 14`" # utime in 10ms ticks 18 | csv="$csv,`cat /proc/$p/stat|field 15`" # stime in 10ms ticks 19 | csv="$csv,`cat /proc/$p/statm|field 2`" # resident size in pages (8k) 20 | csv="$csv,`/usr/bin/time -f '%e' testclient/main -concurrency $i -iterations $iterations -op $op 2>&1`" 21 | csv="$csv,`cat /proc/$p/stat|field 14`" # utime in 10ms ticks 22 | csv="$csv,`cat /proc/$p/stat|field 15`" # stime in 10ms ticks 23 | csv="$csv,`cat /proc/$p/statm|field 2`" 24 | echo $csv 25 | done 26 | -------------------------------------------------------------------------------- /testdata/badblocks: -------------------------------------------------------------------------------- 1 | 0400x0 2 | 040000 3 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e016090000 4 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f 5 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000 6 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000000069cb 7 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000000069cb7d5b0f1d0a 8 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000000069cb7d5b0f1d0a1c000000 9 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000000069cb7d5b0f1d0a1c0000000000000000004d6cdd939a4900000000000000000000000000310c50b3fd 10 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000000069cb7d5b0f1d0a1c0000000000000000004d6cdd939a4900000000000000000000000000310c50b3fd40 11 | -------------------------------------------------------------------------------- /testdata/blocks: -------------------------------------------------------------------------------- 1 | 0400000006785624481f381e68b4506ba5d6cc53ddd6dc876cbc97e45e3c7c4a626a19008c450aa9112b2e900eba30c2e3b8428c23fb2a30bb7fa34a853c97f02fb1a2200d86a7943df2dc92fb8b131b0698db24dd01a12d19696c20443f5f639df3ab5aecdc295c172c191f870000790c975f8c9f0a77f12ef1e021c6045c3c9acb7c432c2aa7ed42ad0000fd4005005db35c547608bbf32bd1b90369e6e0bc1e275a0d064f092b235835cb6dfc9158c6146a0e5cfbdf2d2f04f71478f7d9212bdeeb938029a5cc217ebf1f517711dbc7cb35d084328b8f7613e878f69e27b2b40e8103292cb87845006943b761bbbe43d94cfbf971011617965a7de1e846a586ad81f2ad138eb0fd74729efc04720badc95a1f1383cc70e890db91c143591ab96526263ec6b8a6186568dda29b9015078d19e87cbb2c00b26edee3e4b227ad6fa090db2d290191ef1043e2037fc0bd225e6a1364d16223b0234311e8c5ff455a0cc2e0c1f84944dfab49864f57e5ec2a2b5e91df5229449ee399b4c721abab63c534602abdf64d37f60905bea71cd59be759bae5967f6f3e1bf732753d4d9c1c11dcb5b6949bc4f63a06a3b0db5c2deebab040c70fc55fc648bb2559fe2da2b4a4bd53ccbca7aaf03262cdf31c5bab90cdc151b686d3e5f8a29103b9346602b5394b245dc9655116d33c2ed9d31563bb74950d0927895a024c119d30d4f2c760c346b11108ab29db2931123aa36937cbd152756758e3d341ec40db1d1b539fcbefa2e76af9827f575025c58569fbec1f806607e03a6eabc228a900a18144e310ab608a930ee6a10d5e6fc7ecc9934fb42b51f0a6552e2a1ba1d627b51df2d27889e14b87f093452b79bce3e2a74398f4b748eda48495b30437bef13527a231ce724f24d5bcf4040cc77f56dab50ff30ce12039ca968eef3c5ab44b2cdd8f7fecb36ed39bc624807a4da49d5b2f53abeb1a027676fb500fb89d7092eef849f254dfd3a7325e4b810687b8567d7db8e796480836bbeab2efff44bb0421f4e3d5d1493bedc2c331f75b7ba931e93495dc379fa22404a2e60919fb2644ee4e5c3bc2025fb17540dce5dede2fdc4fc6b87494ae26ae89bbe954ee8458740601a665126129c9962052683661efcfffce8303211fe3bf0adac14936927ce2f4b30a22227c39b4128616dfbfcaeae8f4a3b30255d215d1b9ccd9bff2173b5a94ffa5473bc49097030e782d81c1dc4f8c4a25803c6dd9a9e775646084ed2bbbf7457386ece058032128bc87c7b7772be513f79176241de701580b5c04f88ee81e231583528b514f0c478382e60d5741bd0b5582c2ece2767931f538d050498886814c5b27541764bda9734d5c46f2b3c80f2649d40374e45c053c050e33e83ed578f6d5c19435b7f495261e7e522d5225eaead374497fed7c30e322d6acbf512751b58417120bf8d97fcfdaa5e3be842fc7beab85235ff7e9ee459b03ef2ae01ceff42e0621ddce403e65521505ed0b25fca1a0747263e44c72c5e1e95d85119a7956852c629fcb903b9589e768a6bcbec5358a5332368a6913ffb9b9880e8d3701c6a366bec102bab8c6d9e57b2287ede4774d69fe81af83dbd423c905f666170bfc03f360314edf3098fe2036634ad90f326a71faa5b70f245acbdf8f7d4481283819d8dae7823ac31bafef129b94822a4d94b8ea5773b7477269be9dbe3bc8c11886a2d2a8176658fe30e1ba1bdcf320873d1d2325096223af6e142818a2c370b8447db8e6e865fef2074596bc9e679b8c37c51e99c4f35ac8431303fac507193d1d162d309a3d8d6e57cf4cd429b34ce69f69ef2f1e728d7b9347051b46d4ba5a7e7f9d43963e960d0bbaf63eeb69396dc75b325f08e9243db0c678aa192eb3db32d9cf65a0a38bd5726e31380f2de23ea9a019d8496ab511df2d955978fcee6e331aeea5df0a1826639323180b87bbbf6a039e34f482a5e4eabc6d9a139c40e2840b40eb53dea3d549b54405ba7e1b392f308df2abfd6e97a3eb44f540b272a6d97c085be2142445ea27349bc7a5493397ceee4c8016a38457480f4de263cd2c2416296a2d86c19ded93cf431bfdd1e3010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603e0ce050103ffffffff0200ca9a3b000000001976a914f4fbba801ad64c6539e64a1478392c57c9a5e7a688ac80b2e60e0000000017a91466a04fe5f6f1b5bb1149e6cd0079167f570880148700000000000000000000000000000000000000 2 | 04000000913b07faeb835a29bd3a1727876fe1c65aaeb10c7cde36ccd038b2b3445e0a001e1ecc75a33e350c7763ccf055402a3082f28209c643810a2ef17df4b14486cb0d86a7943df2dc92fb8b131b0698db24dd01a12d19696c20443f5f639df3ab5a15dd295c21c8181f15000813d02ae5e889354eb9b7b62cca4f139ca6364fb63646ff32f2b4700000fd400500356a828c88fb46eaaa21d759e99109375d79b61557348f830c71fd4dabb126ae794d403af814bfd3fc01b76c2fc110525507f6728bec686ea9a432eda0ef1c8fc97d912923a9ab0d06f8aeef2ba272a3b7125e0e5079f131e4706d59c4f34a2775cfed3fca2cba4a23795351a38abfacf67ae7772e6fd14326efde46192105f2c3b0989a15aedfd22063529005723c8c8ea756ea876e0ae7846d4556e57fdb4b7ae1d2125456130653604eff1a6e632e20350a88b0fece49fa57079818d28c0064764979ed2e75766767a7ddce8578a23712274fac056571619b9ef4371f6924068fd5dcf25112da1ffe4255fe60b851e50b704eef0577967a867a0fbd7ed06f0d93f0f0ae31830ab5eaee5b20757e96233c21f96d6eb8f5b5ff03e88d7a31ced8037a52d94ce82fb719764841c06016ba82e29acb9d521cf9d74e1a1a90175c19ff365b763db1d3fbb6332499a5f90903d26af753812bca3eb2b142992bbd87140c3a0c29732d648c650e6bdfda63a56c6184570bf2bb4d700ba30d7800cecac92f67647bbb282d85228fedbdae1e1e3d56e9cb7743a58f02df45f658cb3b627f6bb311618d1697a217ede3f763d8fbf1fba96f697a62b321d397d492594b477029b23565f8ee80e3df1a74d81be2eaf203e3c9215c72c5fa6ec5f0d20cff3f151d7131ff6a3021f489f87819245d6af34f12dc5d1bd70d2a692be0da4827ed3f81a4b63ee58223bcb53474471a4bfcab99ca08d7dfc6c1cefda57f836d1e58731b4eccbea60c0606dc42945b26b17a3599be385b0b64519f6160adb65fd1da795cf27ea25e9884783834130be24ec10d549eb21ca32188f4221d3ee358f2894099930d165ca4d78985e6c13ceb9ea340ad5c116118c6a2d7851781e554a0e1b968540151e3715513f5476ff6618a5de0498e4f5632a6d5ffea8579ddd0be010b10204499c82540d521397b9a39a079acccaaae32add4d7991db22dcfcb04f86730cbef5b8abb0fa31137abab148457325eb161fd0ebe033094c324f6da261fc31476619753fe95c63a326fd14dbe6e7f83c501b77d4ce5602877b5164973dd55c04f7a891e6f3b423ee425b992d73bdf7e2735e4f52cbedceed9afde086f923208dc1abf1ddce86f9e7c467a8b0b79740510900bef0351faffdda005bd35e5a819b96c1622ca06399cc62f01d08d64c4236970e6c119380df5c48d11d57a8908e76b6d8ac3517f459ec83af139986abf135ea4be1886054eeb0087996aff4ace796b765175166f46af49d6ba10f8b5a455f7bc04ad82a53ac8d71529a6d0dd138642b68a518b61be13761b33bf60133cf97c9c2a0fab6cbfaf3601c0642b6d918b142caf1da2da75d52523739a62984f34f3016ed17cfe03522e71e332079920eb33c0b41de3c3f41e92c0382f7804195e251c0db4d2b6db83566cb4a767a9a85d4d674b226732bbe298ab5df3d5fe2239b82dca3fef254d1a7e3fa9c3545a23eb49136fcd21308280715c7d26601eb40fc49e025b33a6f676b16ae215bce5d6281e0bee2d779549ca7fe32811335e404c10e142ec50c5442afbb03ce0f179848765998ef1a3b60062fd187733919a34999d8bc5724d354421e699e25c39d416c74eb3e2c79229656b54af9a3cb0e71a52e8a9fefd08043f69ad8a8e536b0728e2a039ee80c999d4bd86340c97dea3cb2be33ff333e11dddf0ddd482d99844df08f7bc2ac351987b71e802aec4266b372f2a1e86780b89be04b444801e52c2a4cbf0cc2049e2b3d2c6180e7a28ba0e459de1ddbd016e0c3538f137dc98e2ee1729d5d5b0879073eb421183da7b55866ba8b79c911792a4a6b80b9ed15dfc4249d4c88cf229bc16491f5b8f1afcf272622bf782396aa55fe527a5a67d6327010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603e1ce050103ffffffff0200ca9a3b000000001976a91403e105ea5dbb243cddada1ec31cebd92266cd22588ac80b2e60e0000000017a91466a04fe5f6f1b5bb1149e6cd0079167f570880148700000000000000000000000000000000000000 3 | 040000003893091e180992ed3ddbdca17b69bb32636fa24e139463cc3fcc390f72f00100152337b21d88dbabe7993aeda34e575edc0a10d8425ebf675ebc4a83b3ebc20c0d86a7943df2dc92fb8b131b0698db24dd01a12d19696c20443f5f639df3ab5a39dd295c4699181f1e007d867f82f93d39b4107a1265900058b7565dd1b2945241a4ef63bc690000fd4005004b556303cc38ed6fccb33b93a81c0f8f05dd0e8006d64e34868ac9fe5c22863eff3928f6f2831b502400bed03b98c17215ca8a50612273461d43f857d192154f4ad5db69f067aa48d19503523a2f0602fbccbc01bceeeeb256d5690efa33a583efbf397481afaa5745976f397bd7c0cd270674f2de48ded99778fb0da11068c770ac303637c1ecb1b53e6f1be4f89130a7b45a49edcf081959f37121190d42d668b6bd5c1d353609479c1a598c2c43dd0661c9d51222bda25437f3fd1e952aea271a02c0f19a92f4a11faa20ccef67046f1a4036f38b299e73692e61aa67d526f1b7251da0914ac33d1cddd7a4bfeefada21c8f38ceedc1557d0710bbba0f4932c839177bac34abcf402e168303ed8021dc449b3ca0940448bff3217c0bc2730b19190c6811c420629a1878f6e7021171e9359002e4bc636a5322b8056733e3299bdf0c0f4545aba5372ca4a5b337300f3e8c43e89819a563df0e385229405479e1c12a50b57cc1b8e8e2f8b1db9b3aeca20724244f21c2997270e316e37528f7f302d95d9a2b71d7f0319fb4afc4cb25ea17f1a6fc3b7fbf68d71b65139fbee9997c404c2c76611cbb97750d7e5aae23d2ac26de83ec81d2774caf7279e40737f7b1502a4e8ad15f88a7bcc660a148b1c8c68fe4d891514a7a0d7b6b9e9e4561cf510a9c10bdd586fa57a9683afc9db809d93777c435e0780404f5e05a91ab84d6a63abd77e33e1881f2e341c6a7cae0b8d0507dd6bab56ba688cef5910ff18a816a3bb38fa5c6307eea5221569cdc90cde855083b12e189c39cc9052c328d1e654a3ea11371a6f5ad6ad07d396227464e23b4ae1e1aa1956567e000558473a29f34c53dcd8d4b8d3df0410d8af663de3f177565409f30a2ee236d6edbffc355609b4a60643f6f7db0b30a111d9269d6df96d1d487a6ae2d672ce37fbe0ef02ef64ea5793778f67af238fe45ff15ad1609bdead8fe39d445b2644254cf0acc5effc84ff8786fcbd041b7d3a42085d5c2f1cea86f2817d9b1e21c252e6f02404c7699ed8c6c14b979371d1a726451599fe312b10653fa69fc6c132e30bc299af2e92f9fb4495229f1e49ce927fadcbb19ebee2cb5ceae56981201d7b73168997bde9b287abb25d86d43562839a0b30be80f02c4ee3d2021afb990364264e78cf5b07097dfe860a047191ece343378f63d88136a6509c511f4b785b48187e1bde4f8fcabec22451c19eb9af4a6b743c289426cbc2b508bd4bf9ebb382fe131928292ed0d5113f3e86c3dd8656638f4c0ee5b500ef57d19aaa4e00ab17dcf7950954d017f648aa09bbd5a87aab8c9f42a01c810bc93790ed6791d3a2462a571320bae636632b2d3d77611c6cdff96afff421a534df351910f3ffe92eaae18fc6d4a50908be160ffc5dca626d2777625205d07f18eae60d9fe1b9e32f56fd3e92f98a1aa5c728130249a24cb47556d2c40f2029b17217807e3b3a11afbf8d981df73122d61199854f2414b9ec3f687a2a021fec118b38e6756e67aec56eb726fcab9b69ae09d77d3b55559c10d8afd242836bab39f8a93d1a5f16ba0e0c76565a2731a467af0a490a8e3baa3b3bcd36fba75c7d5c3985355dc38f2ff73f9728903f02274126e39301b1ee8599ccb9fd9955696f08f79a035f09d9ac570225ad05666a2900b56c97cfaa985f3a5814c46ad2a5d998c7f18be2dc1e23c540cd208b3a11124de8965dd8b645746cf351544d0371cc798ec6552d329980050c2a04812f66219677507591470d9a5d103d011b901f2a234395f363a59cc8d18561f7807f1d26e0fe5d2bd38365c174b2886758eeccb73f958c1550df9016863c87aa5a94daabb03dc34208bd682854ebaebe90954ddfc2a4974a02566c15fc5976d38c010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603e2ce050101ffffffff0200ca9a3b000000001976a9141fb09ecb821dd7ce9cc8ee1ed54df0d1863f824188ac80b2e60e0000000017a91466a04fe5f6f1b5bb1149e6cd0079167f570880148700000000000000000000000000000000000000 4 | 04000000d91995f491c208a7a8c1d358417120e91451158db34ab4ef30a1c026e2d6070094eda3d052afd42b6d45e541a1d97184636bb767ccbb7d8f91584693a80233250d86a7943df2dc92fb8b131b0698db24dd01a12d19696c20443f5f639df3ab5ab4dd295ca942181f1a0006ea1556b0c6c73177cd09700bb72e6b2457fed3174f1d69d4e6e3190000fd4005002f434e5adb994d86d3d45d4fc9584f3d70dba6ee34005bbd47169aa751d4c4d06bf9476a26cfb2fdc905c663c77186e370765413a5d0d25be15e4d50746c232c0fde2d09e01c5cf1e53b70db14c9d889fcd1ad128add562edc91339d81d1a4a78e3eed31d4d16d5b3209ae0571569d590114f68e85fd37b26b0c7724531d14a72bbacd02a309cf039521ce4d5537069326fd1fec07eda692ff09f941a8f8c0f543372ec8fa85c20416161dd719e3f4d3f0e663eeefc4da0feb31eace0e54f9df1fd5714aebb4d306636d0b61357e343e9c04c7f60402a9066bdf11b6817ced3f39e539ff98e4282b63ead466a6e15d5c85b76affa63a4e8e935f060a074f30cf9bcc89ef3872f80268ffb4cdc5aa67fb10b745e3b856fbb4b95d439fe8c8933b50eb5dc0091ec9c41c5ddc998b477c5302c0aede64eecf70b35c1f7d7983e1e052039dea18529752ea7e5ae67f4f51009908fd530968726e0fc9dd79d71022d2eb5749f1055e96fa98b35b79e7e32a92de750036c29bbaf38c0d30587b1f87653bcbb979142b658396d4cb7c2496566154d160ba44dbe97165f909c0c5c2e99077bb71041313603d852e22f0c560ab1f0738ddf74f105aa13e26ad8baa139f98f29ed71b85c80085f324f7db0b0a6e9e1d4584af6c592483254378654903adf6b4b70b77be24e817b053ac55016f29ac78a218095509e902936166f7923ceaabda692442ce1da6fd8e3a6b860cf8ecf7770ad2d77afbb0d15de5fc6084ff491a370baeb547171415e1be7f38e5edcc1283bc573f5522286f3d92208d568896dcf51c1566cdb583f1f0d87902e8fc27c6d1c43b1b3bb051cf568e426b551608cc1d27aa72fc9ff34f2479e609a8fc68a6899598f2d713065789f95983f78a3b746798bb19a989381ae0a531178d735c11e1cfa856b40d05b1f959115608e66800e95c067be6ebbd6ac008193ced38e24bc6db67a210d9a6ed0eddbbcdfb0f8439ab66cd4e27ca32f01a095212f36cd6b1c13c17c4ad3327c255b770dc66e03c797b1d39177365c08af59e723825596f024c7dba0a6a71657f84ab7ef5b345c11ad19191bd25d725cd1660d4e0e793c453bf9aac0494f4942f75481f577d0fe5860ffe6c776fbbb801bc6794c11c9d81ec91d32a2e162c69651c8deb52d2bd4bcdf9e0f18254549c041230266cadaf43cb09f113967492286faa3ce2c50a261ec8e847956ddce763e1df446b4164f2594bc40a1c6a756a451022381fd618bb432b9dbfbcb92d79865e3e9beee8a7277359188b6ef0956b59b47e023c06cd3f99c002fed0fb9a7cb63578a4ab4be39d24981caf13d1d4d580e4d48183aca7ae8a3230011b83d1097b33d5b7a3cf89f61d6702f5ec79e66fcd93e5bd4a5183189fae5123cb56791bcf64b9425e473cfdd2073b30e551703823d848e4adcf32673270b7d4b91511b38efd3a60937f2b90c7accaf7102e0b3c5e12c907b62acf098fe2abe7d073f4a4ba248623757adcd12f27ec2cedb749c7e280452b7f2cc085beceb495040cdb00ef434bc9ab78f8b0f79e8bb90cfaf7f8577743dd5a96c8264aa9b19ca5b7e46ed1a74b3d3fa1f02eb0164b62297b61e6bd951bea4562fdf8b3355ea95ad9a72ad0a35affe08f683bf77f781212cb53f9130b672e7e6a5ed3b31bf0a1089f2e694ed7875b9b5d11bc2c6ce1db7dc770d3b6a4e872a572285bfaff5c1f7a2202d8891b3ee2f6bbe4bd633ab700815c6ed9229e19b49856d72da66f865b9bcda32e5af81fd54c122c79ca1fd7a666ea4ef38b5a668b9532e675d8c0561aeba9c5f29a8f9cfc66918bda81e63ecf792705298699e526a10d4bdcc293e93b21513b8cb7dd486030145c81942a84038001873665f93d8ef80bf942f0020400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603e3ce050104ffffffff0208d99a3b000000001976a9142943b34e1bbdefc6f6a5c501264404eac228ba5b88ac80b2e60e0000000017a91466a04fe5f6f1b5bb1149e6cd0079167f5708801487000000000000000000000000000000000000000400008085202f8901bd114c7474dc9bddc943f99e3cd8af97332b4c899fbdebc44f105fe6b760bf41010000006b483045022100b73c1baacf61e8b4bd915ee1c007a51366a7cc718c5469de9f035aec7f92fdda0220595ad4cadd2617be7ce65ca5aa10b6d86ae27fb86c960686d7e1c668469afe12012103f9e72f0713a4d4a980309a14a2ba563e0b1125ad067818e77553a1eefbfc5be7ffffffff02f0ba0400000000001976a914d78f41784821c3d9929fa56e85267eae0bb09ffd88ac486e1500000000001976a9144faeeb51bcd0b49f238b323e5f1c6c8bf11ae02a88ac00000000e6ce05000000000000000000000000 5 | -------------------------------------------------------------------------------- /testdata/corpus/block0: -------------------------------------------------------------------------------- 1 | 040000008a024cebb99e30ff83d5b9f50cc5303351923da95a8dc7fda3e0160900000000226b1c86e101f1fac09aa533c246bd6843ee6b444ad3ff251df0a401f16f0981000000000000000000000000000000000000000000000000000000000000000069cb7d5b0f1d0a1c0000000000000000004d6cdd939a4900000000000000000000000000310c50b3fd4005008f78a11b4b81126dec31cba6ede2e131ab376a8611237b04d20cd8bb4253f1e8721e0941970e0f58c206f09c8c86c70b2f73b3b5994571c389a90fadf6b812cebba5afe622b3e4aab20a88c805e1f3925e31300df11c09e3d4b7acebbfb156b75e213e36f65d88f023feae5d37ab2f13a1215299155b7d9a1f4cbb333212e8ef9959785adfcd2506d33ac1021207fc32584022080f4a7ba67157ea99f6fc645737a2a5c6f5d99700ff0c1a92f2845dba3b022d24de26691c0b56977b13e94d5f93ebe423d3d422a478f2e6d9f0b113b3fa0434fe432c173013b1a641e884f74922a3e6fc6cde4dedcd8c2458be13fb91f5285ffbf8a5dd207dad3b011b832444a85e27da43c1f94864ad7b066559d5210bf495565005f9297f6204ad7b4c395d5faa19c2530427ad4889555aefd272185f825564525f6d3e0bb62b18cce8ca120a6f636ee36f18612fede045bae81403752f8154e7c3bf4fef7134591379389155360cd15328c731c1ed3b57f439994b0350799e91f91f53e30ce9bed6ea9ce01b3f74f6041b739d85965d789b9c43ba65cf4f99808ef03c2ac46461ba0eedc05e77400815af9d8049027bb7fef6646142d86ef35adcca060e8a656c6c6cc811f65231b4ac531fa2083ddffbab26d19409cc357b89ed7da3d25476cb9d9a9c939c343d23fa35a09c52d593fb03f609dafcc28579fdd35e044168e33e747757bfdf5123080b6799d2527368f90de7f126304610765670214b6e9b6f497a491650db139129da19964461c368e2524aa1524248ed92561b3e94aec38d5ff4adcab5f73565dc7626477b1d56620ad1bb49000d1cd915a260c0913960c493edb9770d2fefae76dae63963e147331a51b1c66d5ff3ecf87f141306d575e60ca3b34fd26cb0b1d735dbbb1977db519a7a9d345ccc77121b688c7470975ee9dbfc489800f25a41d406ccbcbe1f01c629dfcbd59ea5dcd00334cc6af8718e08631c3a83d5e6395b4acc3afab48b145b73a064904176c30c2cd9a876ebab5f333b6ebd17c10e31ce6f009daad792c31fce13dd6d401120eb08d66348c13735ffc667e653ccf80bda54d0773473177433406a0c2001ec9f534c54e667c5a3cff3bca72625b5fee94a51dfc2b5b3fc73353b2b2f3a9e708932003e771dd21441e9cd075765e33ab5fe0db05b4b087bb04608d5fe5bd32f8272752cb1e59f856eab0e366a935a9651be4584f8886649c66bb6bff29ac82e0bf749c94e46cc6a42988196c15d3b2a6376185b990e653cbc8a77e56fd3e74378bcd54f4540afdf39650950fd18192198bd743708fcd948ec57aab07d71211157758ca83509cbc0094158b75bd840810fb266f91565df4f4013aaa4e414b3a56bf8442938189441ae24f8e2417b7bb729f9cf27a2ee5da291fed793afcf09a1c63f14af0dab7aa6b34b1be3d545356653f14a2d31173cf50fa6aed549933a5743cd46809bf6e64824111cb5032130536943f359ffe5cc7ad5b4dcce3f2d078f7ca751fb08f8a5d9ac3970cb9f2cf31ed1a713cec3e8bac8586ce10c74e215c3d691c58e176b39382da1fe4ff0edc7f23373f594ab00c987c772f92317575d4d199e79f5b95fe1986ff0dd4d4a339a797b3b366e737bf8befbf80fe58f0d5b6f35c831d00ebd0c47ddcec914cf879ca685c0724b9036f98abacd96102657148a55e6e7896f33d51daa97190a233a4d68278b51c2bd26dd23916c54e605b6578a8e3657359c36a9ee2e4c0f650a95a3f4b61c4133ddaa901154d20c807a6ab85211bda537f26a4fe4b53a4a64f5522d13eb75cd2bab7669ef47d1c7fd43416182bcde9370105383fa2c0c574d4895d2fea59889068e66f83c8f15118c7ce7adb41fd8de61c2afd7277d6ac31346aa01030000807082c403010000000000000000000000000000000000000000000000000000000000000000ffffffff0503e0ce0500ffffffff0200ca9a3b000000001976a914361683f47d7dfc6a8d17f8f7b9413ff1a27ec62988ac80b2e60e0000000017a91414c884180589f6d0fdc7c400181afb1bb10c9fe487000000000000000000 2 | -------------------------------------------------------------------------------- /testdata/corpus/block1: -------------------------------------------------------------------------------- 1 | 0400000015bea7ff6272a28e0dd187b5160d8fae6f2ad6316d652fdd26b92204000000008ede1b5093bbb6ae7c883dbd4b8bfc9e97290ac6ba5a617c880d3aef1bc41d9200000000000000000000000000000000000000000000000000000000000000009bcb7d5ba51e0a1c0000000000000000002c476f1d2c08000000000000000000000000002c27173bfd4005001a40fe9c1795f9617212aa1ade76cd93f85db2e5254a8ffd5aab68218adb36065f6947477a423c26c719803ba9c01555a1d81ef1cbd4ab0e7a7c9c7aeba622c7c1614f16409f5b0f757a93d12923027fbe875e01d4003525150ba313f43148bf39e7836a3d5fae611de2de0eebd8a3d1b163e4ad7b3ad0bbc00a3f4b8d029ee0e23f6354cbc381711eec9df770f31149ac1e0eb55229d10d7ba106edb12834296cfd241f5a62a6014ac0726dc4543f05a88138ad2d7654b707aa799115886e467957d5f195fda7a0a0d7908e084b974a1e0565e222acc9670f4f7191b4052f84595457315647468f532773a0755b0b3c7649aad0ce1e9da9597819022f7c951ddf47978c5985bb3f4350b1b91f2eff7f149ba3992316712d0bb2a3b6fbb3dee54b516d22b21f60c23f87e3513d2ae3447191b55b17f3671f9e3629da0eb3b8261cb7eccc675761f95452435bf6b6f800a4e5a2deeee86b8ae3d026a11eaed463373c378b079a68b51eed25b596fd519a2530f9414684d71dfa427c461ab170137fcd343af2fef3122bb037df784e647b6386823340abffa60668eb7a5729fdda35d42602729b2fa822d11da41c304ce27530bc17e717fdd10a2f221cf793e4db842ee2f6f04b2a74d3945214ec0d1819005f0e42f68c8183e9a0a87011180ed87eb3205e7f01d7a760d95b874ae2d17e915b267cbd0e4e06e99982744e7473f74af2ff766730ee01f7528478116633f7d2eebd87d8e134429e4d29b92144fd0ffb1628add40f98c4df2856632055a3d2c70a2abf69851929b6aa5a9de2af2148ea564a7520f6d95e5724b40c2a3c9d39c995b2f3e131795f2299029c215b07cd14bce9e10fd05296a8a646302ff4c45e85a4bc3bec12fedc1d1f60da8f6aac22f746d43a11bd08be8e52153c60ab71061d35284872c427e3aaff0d8a3a82af016db8d42937f04be1ccb021e7152aa82a068b0a1b04cbe51b904ffc00f3bd46cedc3c58ba378355360e11982c9981c6a5ae7c6d21fa3b2e54b8e01d49a0f618df191ffbd3274b7ac3c5f066623a15c1e07eb64e1db18f37cf4d4452c441c63b8075ca220d0131a2d022da9305700b8e0d236ab4dfd17c2f453c3f17fd722925de3cdfac190bdec92340db74ad46ff693f79c92bad961d021e3a01dcf16445d66713e53bc37b89720c887088d26831e5a4a7011366eb3b6920df10a3316eb41dfbe9ef6f697bfb673be8bce2a64c16baa1ed1cce0a0bc32595bbc22f42ba8d2dbdba3f6097cf7b27cf4717d7187bab684e364a17c69e29c49ff3aab10d8f9d3002149c72d90cf115723ca3aee3c8b8014918896bf63a2746d7a486a496b274b21765dcdeb83b387252b876129b65ce91e613783f55aa1ca85c07a448b4eb36d95681c1851f7532a47e8056eacbd8c45d01b61dda489c744f267111294fef34a98c5391d6ff31491f974fdcbee1cfcd740c714abd9a616878d604076dc376afd53826da95c4b81ad0f991642a6bd3d518feec5a021e20a73be5924147203586c75b3fa9c51111119bd39061fdeedbc1a1a92aeafe834dff67071438f21b247a5713e66bf156ab70f04995ba70f4f0129da42ee3d5ca8da38c35c2b8d33d19e79618a0061ac8fdae6a58250725ab51dbda4a6541d7fe1cd31403ad8d6cfde6badbb85ff162bc1f08b109866f8aaa1688b2de068fa272a3a6371f3876101a37191dc9c308eff5bbe41512b7a749668f2a35f43e2dc67c91ed13e39bc4ce5783b795d1a16ba03886ca053dbd6a4006e0f5091e6955d7abdc154373d10d4d60ecee597c0a8d71dfbb2e3b4fa83554d459c0a68d655c9b28ff0cdd2ace655bdd8dc2322394a34255b1d5373dc39c310244fa0fa1c557d7caa3d029ad36966d0d7606e301030000807082c403010000000000000000000000000000000000000000000000000000000000000000ffffffff0503e1ce0500ffffffff0200ca9a3b000000001976a9146d17c0ca63e020d639f5fba181ca4f5da48b8f7088ac80b2e60e0000000017a91414c884180589f6d0fdc7c400181afb1bb10c9fe487000000000000000000 2 | -------------------------------------------------------------------------------- /testdata/corpus/block2: -------------------------------------------------------------------------------- 1 | 040000003723935953489b860d1de1b8fc8240ced04625f0ffd23f16519b8e0700000000f066ce1a8f2b97d05dc15ee51e1eedac93ba6057a953eb81d3f856e9f4b7ae270000000000000000000000000000000000000000000000000000000000000000c8cb7d5b13520a1c00000000020ff5b32c230200000000000000000000000000000000000d5e39cefd4005003eed282b472cfdcb0540b4f4245ed479c5357ff11989ef70be9700eff5987318f727dafa0b467302aa016e5e956c5a5d23f0f9e4a5623b9dda30e0f1b3c47e8c3d151e686cabd4ac89fc627e5cded736d707780c3dc341370bf7af4cb291878a1c4236cc207f39f081ef26b50fe099bdb90c8a9462663d1aaf079fefa010c2a8c29211f08b8debf2f90e4afade29bc53d74253c3fbdf265759fee75e1ab46adc90f38bba5f73e80e518def9b61e8b714d6e6a1dd585539ebd1f5f2040e8e73498455bec4fd333747f96823c66fb859177d13f3ba05868918255bb022cc32dd0a099438fdb1b31b15e2de9acc341066d6b20e4c1d18f56397dfe97919a2eb988e114e3cc015c1c209d906e494c46b9dac34a99ad0595868b4f84a1474ff532202149ef12acc1db1f9307213f49efcb0a408fcc1cc6124e637adb22ee4368da8d0a78b696956e7e4c9feea5d2e198f0e00bdd16ca49956c32ff793e3ff6e635efe57788fbf11a8b2a771d1981aeed6462fdd74a6a9f5d813d86616923f5add19a41d507ba22b69a0caf4a608f8b7673bd605d7f2b022b3d8436448aeb34e491c12addc3911564ecf5de8b1355f1762dc402e37915c35cb69de2a9d575a765737ff92c653eed1eb871270589c2dff15f827ba45b19ab3d25da28fe3176895a8cc32a943391276625d991492ed315ab22a7192fbcc5f5eb01611640c96585ac4db590777a87444c3dadc653bc2bb279474ad76ac00d5972b92e026d45fdd0ed84b099a13d08c00eea952c5a2a78218e06ff989f85b52801d203f82d395310f67e80f4646f3f29c5b0c2f9c3ce31530b766c12f3253a6f3e2aed5b66efd9dcdb6f1de1e601a36b00fa2b7c4d6241e46afc746cfb73f1b2f19c60f81472bd15bdaff21afb0c44514915bfbdb5a20172209c4c9c281ba14950b07b22d097bc0cce5fa017ef344aec83bca60db20f4241ab3e1d29f7f200a24b801aa1e2c638d77ac13c0732da3561e3e1bcc6f2ac78fcf21e7f113b56d83c0343a1cb6d8b69eb12975891c1bb27885f1efc8c97a0af71176f716fe36b61262e23b48e7266f6551821948e4c44d48153df60a1a921faa5766e4a99239c2f36231bdadc93e749db715ff7e8ced9ef6cb28e7a5a8aa7603de56e73ea5fd1d79be8974a4c071ed1b969382da51d68b11f4722303d74f82849cb05d07c0c313d9a0d5eccbb853f85f0c7002c67eeb93737060180a3f557582d3849b73bf13807ff3f4065faacd7562487fc96a20fcf2cc892e16ad74b813caed4f72f11293243912c976a9f2035b099b9b9debc968aec54031bb9f4eb7815ddb3bffc128d6be34814fbec58b17c6dfab6c812a5eb3fd15d60fe3fa55128466cb80bae5579b71025e6a9c34b7f818eb11cdb029eff94fa465e419b9aef2e8679f12ce02b780aba2c17313bb375033beda50e90be5fd949002fbb85634d42598b62d21e346d13c99e4b6364354136f828f972a7b2fd8c4464defb82629ffdddea6355afcf76a5b6a1f797b2a46b51cee7f95f812d926d00436f98711c6f6a1e374a5f8a6f73f39b70150ae7315988b1a2c88174e9bad58c2e0f82476bb1c9ebd03079970441810e5c7c1b8d3a5b8eb21750f29eb92f348d5ccd2dd13fc68b063c754564f490e33ba1bc71b111eb14500d1dfcd883be26a8952aed912f4a999831b80a94d1f994c69558334e165513fbe129a9348922d6205d0bdb3fed3d0fec41a8f7dac2a46f2f9bdd837d6fa791bd25ea3bedf454b7fcf8907000a78d4e911bc798c6d261817400572cea0a26e9e2f5b16cd7129e92738be52b4832c2a072e2c7db3c2b889fa089b1284a3ae2ec654da817e15e0b26856be1caa1c7c0b3446158365a0992bc01fc67c19d5444ad8c6be044902030000807082c403010000000000000000000000000000000000000000000000000000000000000000ffffffff4903e2ce050004c8cb7d5b598c2ba5c7f0b4587e7d60e62e08985aee803a179dda187d2f0c5284df7da7482f4f76657277696e7465722f5361706c696e672f706f6f6c696e2e636f6d2fffffffff02d0eb9a3b000000001976a914de2757e612dee78a681ace9b8e623570dce776f588ac80b2e60e0000000017a91414c884180589f6d0fdc7c400181afb1bb10c9fe487000000000000000000030000807082c403012263eb14eca399ec05b64be85325e2c5ab93e5d70d7dc49b4e8b8dcb52ff0f7b680200006a47304402207ade36a404000f4d5cebed5c28e28c57a539b7985b66aa4c014e0c59a43fde3a02201bd61f0a998301241d83fc9370654e0acda7cc6602258b954af6bf06605dffaf0121026e88bf2ad6aa4fdc21b2636583184a589fcddb3c1ed40b4e3613cf61601fad54feffffff01bc131600000000001976a9142f592f81575c361e91397dccc60772b9335f432588accfce0500f4ce050000 2 | -------------------------------------------------------------------------------- /testdata/corpus/block3: -------------------------------------------------------------------------------- 1 | 04000000c848f5a041f4406004243ecf645ef91f303f6766f5615e4cd2c7ab08000000002c46ffced042e210ec98759ee1bf5792b62c3a3c56f24667422350dcf3c84e070000000000000000000000000000000000000000000000000000000000000000d4cb7d5b7beb091cf18a4fa59e6d1400000000000000000000000000000000000000000038d0a613fd400500572cbab31dea47dc52529819b5b80cce7fa6ff4f0c1e8f09d50bee7b5012813826948828c3d5e6f14505a4f7ab54884cb7b8f1a49675b99f820d219567d72695fc14f74a26554ad3c346123852669bd29e50eb056cda5e176bd065ccbbf2663f3a12a11f7773a0847c09059fddb09a37908e1a8240f29dd6eaec78f92b1f67e2a1cd27c2b1d6f9c5c7d0c8545dedb6f949c93507838d4525606b73d176ed8ddfa2a20d93ba4d4303512f8fa4c96efcc084329bad15f9997edef03fc90edcd1e7f943c95b5c7123d66a4d0ec9012ab2578e1d9bbcb47bdf56e9eb72e3c82dcfb14ed9785c860522791bb3bde200f76db5e907555c27ef91489cc4410b4129e4aa08b800c00952b451cf3640c326b690e719d92cd2a87799d1dd63b36984c30a6582bdb1fc6d16b323c2ef33b23deaf2325763f8b1fe7b21dd7c801ffc7df2c4d68511aff3b2059e681bb1afaa19090d03592df27c452350d040718758902afcaee5fa48a00e9fffbd3ec4222973c2686fb3e7beee6c5ebe0ecc130b8981d00adfcd464832b625d7cd82adb99e02f71c4b3d9ec6ce5812cb5075cd70cffd967e73b9f08c04c97c728e02efa88b18f318423ce212fca69c05b743289eb18912bdbcde1bc5fb507286baccb1b8937438092c5418a86fe56bdf441637ac4a49e172f491f743a694bc0516e02ebb1e36ec3b4a460e5f45bf6d2c055924c396c9546f9839b696785fdc09f99ddc7e701a3f0f61b7cb90d6de292400b8423f45bdb6d11d921711cc0a5316d200c220f70e80cda935f6b911e34d1aed9769d40ab973a6aaf1bbbc33ecf57f60d93a490d5835583ca4b70de9218510784caaf2d28c1b3dfe1af1e47ea3c96ddd46e27343ea5b80d175c3ef69b5387d2bfae8d026c765d2f5ec7ec7dacdca63dfbae85974e5a57e636edd68754664b56b9421a009ed09d7007b72db75980945b9f8e0da56d03cdcd8cdb9ce8a6d21336a8ab946fdeb59472585584a85e59e31152f0a9e03da924877854e93d5f6fbcff21a4165bbc4b60c8a195bd46761499fe1a28eb464c0957203d337cd0781d1e385308017999260c8da0933ee3b751d3ce92015b3e67393dc98d883d70113d24f72c4b73a885f1cea47062e88aba3983f192c69dc10db68b95fb3e021363ba87ad938bf97077263a7aeca4f769dfd54370194582d31d7c632c60833fa9346814b26ce1d7c451d6fbd6c309de603780185f499b963a7310abc42450445a2f2840f25350d38f2140b3904fd636ff5016334e2c234d72eff0d8096588c71ffdcde3569b3914f02acbe1938957135acc6d10f2d5bf56885c177e6ca778f7ce431225fd535a15949fdfd2e8ab0c47fd8251e294c02cf98d91340afb5812950c46d9b8dadba902fc70ea73c2c26b5a4d0d59410da46018486d9ec5a0a9b89422190afb7e72b7150f97b359d70841170ae23bacf915f10a019c601f48017472dd2b7c7b89bf0128dfd4a01e450adf17071ded02a3ae13eb7f8fec54510955a305256b2be2d97994140f36222d5ba55ec0ea35749fc894289af4454e944c1dbb1d7d40f1e72276651c4e3542bfd9f145500d9295eab83c6fc92362875620cd0159b7b53ff530d66b25adde2187438045c5fc4b8bdaff93dfb65b85c71753846bfa91f6ac60c9db561423b6109feef932486ece2cdc3c539112318c79ecc7e08ddda73ce663c3d4e9b9a7c743955330f1ca702b8a5673f82328b5f637dfa3743bcbe07583bfecf731cab93abc8d4a5dbfcdbf3536db25f1fff0d71175b744d561eecd1a89fe0484d466d2c5a68d61abba2dd9e8786211482b2b50218eec505f94c67971242a67a93872681ac81e1f541ecbf95a8bd7f5d8c1e60d95a76138bab40da3455b5a36549920d7f4d0102030000807082c403010000000000000000000000000000000000000000000000000000000000000000ffffffff1a03e3ce05152f5669614254432f48656c6c6f20776f726c64212fffffffff0288f19a3b000000001976a914fb8a6a4c11cb216ce21f9f371dfc9271a469bd6d88ac80b2e60e0000000017a91414c884180589f6d0fdc7c400181afb1bb10c9fe487000000000000000000030000807082c403016f4029430669c709d041c579c8e9469446f1159064613e6cd47514691b93b8ff000000006a47304402204ff60fca23a36026d374850bc018e7c714de78d8560d4188fa247b1a4ee4dcee02206538b475265173915662465b4a8554a111003701371d553134047bf185c98ba20121029ba2dc5033c7c17f06274b85c0f8e51ad518f34235713ffee4705867162819fafeffffff0208672302000000001976a9140ed9a32475fa7214939a6705fd790a74976bab0388ac3e926c01000000001976a914d752bedc09529fb3719004407dd6b5f29000e3e688acd7ce0500f6ce050000 2 | -------------------------------------------------------------------------------- /testdata/zip143_raw_tx: -------------------------------------------------------------------------------- 1 | # These are here because the second one is so big it makes the test file annoying to read. 2 | # Test vector 1 3 | 030000807082c40300028f739811893e0000095200ac6551ac636565b1a45a0805750200025151481cdd86b3cc431800 4 | # Test vector 2 5 | 030000807082c403024201cfb1cd8dbf69b8250c18ef41294ca97993db546c1fe01f7e9c8e36d6a5e29d4e30a703ac6a0098421c69378af1e40f64e125946f62c2fa7b2fecbcb64b6968912a6381ce3dc166d56a1d62f5a8d7056363635353e8c7203d026af786387ae60100080063656a63ac520023752997f4ff0400075151005353656597b0e4e4c705fc05020000000000000000000000000000000076495c222f7fba1e31defa3d5a57efc2e1e9b01a035587d5fb1a38e01d94903d3c3e0ad3360c1d3710acd20b183e31d49f25c9a138f49b1a537edcf04be34a9851a7af9db6990ed83dd64af3597c04323ea51b0052ad8084a8b9da948d320dadd64f5431e61ddf658d24ae67c22c8d1309131fc00fe7f235734276d38d47f1e191e00c7a1d48af046827591e9733a97fa6b679f3dc601d008285edcbdae69ce8fc1be4aac00ff2711ebd931de518856878f73476f21a482ec9378365c8f7393c94e2885315eb4671098b79535e790fe53e29fef2b3766697ac32b4f473f468a008e72389fc03880d780cb07fcfaabe3f1a84b27db59a4a153d882d2b2103596555ed9494c6ac893c49723833ec8926c1039586a7afcf4a0d9c731e985d99589c03b838e8aaf745533ed9e8ae3a1cd074a51a20da8aba18d1dbebbc862ded42435e02476930d069896cff30eb414f727b89e001afa2fb8dc3436d75a4a6f26572504b0b2232ecb9f0c02411e52596bc5e90457e745939ffedbd12863ce71a02af117d417adb3d15cc54dcb1fce467500c6b8fb86b12b56da9c382857deecc40a98d5f2903395ee4762dd21afdbb5d47fa9a6dd984d567db2857b927b7fae2db587105415d0242789d38f50b8dbcc129cab3d17d19f3355bcf73cecb8cb8a5da01307152f13902a270572670dc82d39026c6cb4cd4b0f7f5aa2a4f5a5341ec5dd715406f2fdd2a02733f5f641c8c21862a1bafce2609d9eecfa158cfb5cd79f88008e315dc7d8388036c1782fd2795d18a763624c25fa959cc97489ce75745824b77868c53239cfbdf73caec65604037314faaceb56218c6bd30f8374ac13386793f21a9fb80ad03bc0cda4a44946c00e1b1a1df0e5b87b5bece477a709649e950060591394812951e1fe3895b8cc3d14d2cf6556df6ed4b4ddd3d9a69f53357d7767f4f5ccbdbc596631277f8fecd08cb056b95e3025b9792fff7f244fc716269b926d62e9596fa825c6bf21aff9e68625a192440ea06828123d97884806f15fa08da52754a1095e3ff1abd5ce4fddfccfc3a6128aef784a64610a89d1a7099216d0814d3a2d452431c32d411ac1cce82ad0229407bbc48985675e3f874a4533f1d63a84dfa3e0f460fe2f57e34fbc75423c3737f5b2a0615f5722db041a3ef66fa483afd3c2e19e59444a64add6df1d963f5dd5b5010d3d025f0287c4cf19c75f33d51ddddba5d657b43ee8da645443814cc7329f3e9b4e54c236c29af3923101756d9fa4bd0f7d2ddaacb6b0f86a2658e0a07a05ac5b950051cd24c47a88d13d659ba2a46ca1830816d09cd7646f76f716abec5de07fe9b523410806ea6f288f8736c23357c85f45791e1708029d9824d90704607f387a03e49bf9836574431345a7877efaa8a08e73081ef8d62cb780ab6883a50a0d470190dfba10a857f82842d3825b3d6da0573d316eb160dc0b716c48fbd467f75b780149ae8808f4e68f50c0536acddf6f1aeab016b6bc1ec144b4e553acfd670f77e755fc88e0677e31ba459b44e307768958fe3789d41c2b1ff434cb30e15914f01bc6bc2307b488d2556d7b7380ea4ffd712f6b02fe806b94569cd4059f396bf29b99d0a40e5e1711ca944f72d436a102fca4b97693da0b086fe9d2e7162470d02e0f05d4bec9512bfb3f38327296efaa74328b118c27402c70c3a90b49ad4bbc68e37c0aa7d9b3fe17799d73b841e751713a02943905aae0803fd69442eb7681ec2a05600054e92eed555028f21b6a155268a2dd6640a69301a52a38d4d9f9f957ae35af7167118141ce4c9be0a6a492fe79f1581a155fa3a2b9dafd82e650b386ad3a08cb6b83131ac300b0846354a7eef9c410e4b62c47c5426907dfc6685c5c99b7141ac626ab4761fd3f41e728e1a28f89db89ffdeca364dd2f0f0739f0534556483199c71f189341ac9b78a269164206a0ea1ce73bfb2a942e7370b247c046f8e75ef8e3f8bd821cf577491864e20e6d08fd2e32b555c92c661f19588b72a89599710a88061253ca285b6304b37da2b5294f5cb354a894322848ccbdc7c2545b7da568afac87ffa005c312241c2d57f4b45d6419f0d2e2c5af33ae243785b325cdab95404fc7aed70525cddb41872cfcc214b13232edc78609753dbff930eb0dc156612b9cb434bc4b693392deb87c530435312edcedc6a961133338d786c4a3e103f60110a16b1337129704bf4754ff6ba9fbe65951e610620f71cda8fc877625f2c5bb04cbe1228b1e886f4050afd8fe94e97d2e9e85c6bb748c0042d3249abb1342bb0eebf62058bf3de080d94611a3750915b5dc6c0b3899d41222bace760ee9c8818ded599e34c56d7372af1eb86852f2a732104bdb750739de6c2c6e0f9eb7cb17f1942bfc9f4fd6ebb6b4cdd4da2bca26fac4578e9f543405acc7d86ff59158bd0cba3aef6f4a8472d144d99f8b8d1dedaa9077d4f01d4bb27bbe31d88fbefac3dcd4797563a26b1d61fcd9a464ab21ed550fe6fa09695ba0b2f10e00000000000000000000000000000000ea6468cc6e20a66f826e3d14c5006f0563887f5e1289be1b2004caca8d3f34d6e84bf59c1e04619a7c23a996941d889e4622a9b9b1d59d5e319094318cd405ba27b7e2c084762d31453ec4549a4d97729d033460fcf89d6494f2ffd789e98082ea5ce9534b3acd60fe49e37e4f666931677319ed89f85588741b3128901a93bd78e4be0225a9e2692c77c969ed0176bdf9555948cbd5a332d045de6ba6bf4490adfe7444cd467a09075417fcc0062e49f008c51ad4227439c1b4476ccd8e97862dab7be1e8d399c05ef27c6e22ee273e15786e394c8f1be31682a30147963ac8da8d41d804258426a3f70289b8ad19d8de13be4eebe3bd4c8a6f55d6e0c373d456851879f5fbc282db9e134806bff71e11bc33ab75dd6ca067fb73a043b646a70339cab4928386786d2f24141ee120fdc34d6764eafc66880ee0204f53cc1167ed02b43a52dea3ca7cff8ef35cd8e6d7c111a68ef44bcd0c1513ad47ca61c659cc5d0a5b440f6b9f59aff66879bb6688fd2859362b182f207b3175961f6411a493bffd048e7d0d87d82fe6f990a2b0a25f5aa0111a6e68f37bf6f3ac2d26b84686e569038d99c1383597fad81193c4c1b16e6a90e2d507cdfe6fbdaa86163e9cf5de310003ca7e8da047b090db9f37952fbfee76af61668190bd52ed490e677b515d0143840307219c7c0ee7fc7bfc79f325644e4df4c0d7db08e9f0bd024943c705abff899403a605cfbc7ed746a7d3f7c37d9e8bdc433b7d79e08a12f738a8f0dbddfef2f26502f3e47d1b0fd11e6a13311fb799c79c641d9da43b33e7ad012e28255398789262275f1175be8462c01491c4d842406d0ec4282c9526174a09878fe8fdde33a29604e5e5e7b2a025d6650b97dbb52befb59b1d30a57433b0a351474444099daa371046613260cf3354cfcdada663ece824ffd7e44393886a86165ddddf2b4c41773554c86995269408b11e6737a4c447586f69173446d8e48bf84cbc000a807899973eb93c5e819aad669413f8387933ad1584aa35e43f4ecd1e2d0407c0b1b89920ffdfdb9bea51ac95b557af71b89f903f5d9848f14fcbeb1837570f544d6359eb23faf38a0822da36ce426c4a2fbeffeb0a8a2e297a9d19ba15024590e3329d9fa9261f9938a4032dd34606c9cf9f3dd33e576f05cd1dd6811c6298757d77d9e810abdb226afcaa4346a6560f8932b3181fd355d5d391976183f8d99388839632d6354f666d09d3e5629ea19737388613d38a34fd0f6e50ee5a0cc9677177f50028c141378187bd2819403fc534f80076e9380cb4964d3b6b45819d3b8e9caf54f051852d671bf8c1ffde2d1510756418cb4810936aa57e6965d6fb656a760b7f19adf96c173488552193b147ee58858033dac7cd0eb204c06490bbdedf5f7571acb2ebe76acef3f2a01ee987486dfe6c3f0a5e234c127258f97a28fb5d164a8176be946b8097d0e317287f33bf9c16f9a545409ce29b1f4273725fc0df02a04ebae178b3414fb0a82d50deb09fcf4e6ee9d180ff4f56ff3bc1d3601fc2dc90d814c3256f4967d3a8d64c83fea339c51f5a8e5801fbb97835581b602465dee04b5922c2761b54245bec0c9eef2db97d22b2b3556cc969fbb13d06509765a52b3fac54b93f421bf08e18d52ddd52cc1c8ca8adfaccab7e5cc2f4573fbbf8239bb0b8aedbf8dad16282da5c9125dba1c059d0df8abf621078f02d6c4bc86d40845ac1d59710c45f07d585eb48b32fc0167ba256e73ca3b9311c62d109497957d8dbe10aa3e866b40c0baa2bc492c19ad1e6372d9622bf163fbffeaeee796a3cd9b6fbbfa4d792f34d7fd6e763cd5859dd26833d21d9bc5452bd19515dff9f4995b35bc0c1f876e6ad11f2452dc9ae85aec01fc56f8cbfda75a7727b75ebbd6bbffb43b63a3b1b671e40feb0db002974a3c3b1a788567231bf6399ff89236981149d423802d2341a3bedb9ddcbac1fe7b6435e1479c72e7089d029e7fbbaf3cf37e9b9a6b776791e4c5e6fda57e8d5f14c8c35a2d270846b9dbe005cda16af4408f3ab06a916eeeb9c9594b70424a4c1d171295b6763b22f47f80b53ccbb904bd68fd65fbd3fbdea1035e98c21a7dbc91a9b5bc7690f05ec317c97f8764eb48e911d428ec8d861b708e8298acb62155145155ae95f0a1d1501034753146e22d05f586d7f6b4fe12dad9a17f5db70b1db96b8d9a83edadc966c8a5466b61fc998c31f1070d9a5c9a6d268d304fe6b8fd3b4010348611abdcbd49fe4f85b623c7828c71382e1034ea67bc8ae97404b0c50b2a04f559e49950afcb0ef462a2ae024b0f0224dfd73684b88c7fbe92d02b68f759c4752663cd7b97a14943649305521326bde085630864629291bae25ff8822a14c4b666a9259ad0dc42a8290ac7bc7f53a16f379f758e5de750f04fd7cad47701c8597f97888bea6fa0bf2999956fbfd0ee68ec36e4688809ae231eb8bc4369f5fe1573f57e099d9c09901bf39caac48dc11956a8ae905ead86954547c448ae43d315e669c4242da565938f417bf43ce7b2b30b1cd4018388e1a910f0fc41fb0877a5925e466819d375b0a912d4fe843b76ef6f223f0f7c894f38f7ab780dfd75f669c8c06cffa43eb47565a50e3b1fa45ad61ce9a1c4727b7aaa53562f523e73952 6 | -------------------------------------------------------------------------------- /testtools/genblocks/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | // 5 | // This tool reads a set of files, each containing a list of transactions 6 | // (one per line, can be empty), and writes to stdout a list of blocks, 7 | // one per input file, in hex format (same as zcash-cli getblock 12345 0), 8 | // each on a separate line. Each fake block contains a fake coinbase 9 | // transaction and all of the transactions in the corresponding file. 10 | 11 | // The default start height is 1000, so the program expects to find 12 | // files blocks/1000.txt, blocks/1001.txt, ... 13 | // 14 | // Typical way to run this program to create 6 blocks, all empty except 15 | // for the fifth, which contains one transaction: 16 | // $ mkdir blocks 17 | // $ touch blocks/{1000,1001,1002,1003,1004,1005}.txt 18 | // $ echo "0400008085202f8901950521a79e89ed418a4b506f42e9829739b1ca516d4c590bddb4465b4b347bb2000000006a4730440220142920f2a9240c5c64406668c9a16d223bd01db33a773beada7f9c9b930cf02b0220171cbee9232f9c5684eb918db70918e701b86813732871e1bec6fbfb38194f53012102975c020dd223263d2a9bfff2fa6004df4c07db9f01c531967546ef941e2fcfbffeffffff026daf9b00000000001976a91461af073e7679f06677c83aa48f205e4b98feb8d188ac61760356100000001976a91406f6b9a7e1525ee12fd77af9b94a54179785011b88ac4c880b007f880b000000000000000000000000" > blocks/1004.txt 19 | // $ go run testtools/genblocks/main.go >testdata/default-darkside-blocks 20 | // 21 | // Alternative way to create the empty files: 22 | // $ seq 1000 1005 | while read i; do touch blocks/$i.txt; done 23 | 24 | package main 25 | 26 | import ( 27 | "bufio" 28 | "crypto/sha256" 29 | "encoding/hex" 30 | "flag" 31 | "fmt" 32 | "os" 33 | "path" 34 | "strconv" 35 | "strings" 36 | 37 | "github.com/zcash/lightwalletd/parser" 38 | ) 39 | 40 | type options struct { 41 | startHeight int 42 | blocksDir string 43 | } 44 | 45 | func main() { 46 | opts := &options{} 47 | flag.IntVar(&opts.startHeight, "start-height", 1000, "generated blocks start at this height") 48 | flag.StringVar(&opts.blocksDir, "blocks-dir", "./blocks", "directory containing .txt for each block height , with one hex-encoded transaction per line") 49 | flag.Parse() 50 | 51 | prevhash := make([]byte, 32) 52 | curHeight := opts.startHeight 53 | 54 | // Keep opening .txt and incrementing until the file doesn't exist. 55 | for { 56 | testBlocks, err := os.Open(path.Join(opts.blocksDir, strconv.Itoa(curHeight)+".txt")) 57 | if err != nil { 58 | break 59 | } 60 | scan := bufio.NewScanner(testBlocks) 61 | 62 | fakeCoinbase := "0400008085202f890100000000000000000000000000000000000000000000000000" + 63 | "00000000000000ffffffff2a03d12c0c00043855975e464b8896790758f824ceac97836" + 64 | "22c17ed38f1669b8a45ce1da857dbbe7950e2ffffffff02a0ebce1d000000001976a914" + 65 | "7ed15946ec14ae0cd8fa8991eb6084452eb3f77c88ac405973070000000017a914e445cf" + 66 | "a944b6f2bdacefbda904a81d5fdd26d77f8700000000000000000000000000000000000000" 67 | 68 | // This coinbase transaction was pulled from block 797905, whose 69 | // little-endian encoding is 0xD12C0C00. Replace it with the block 70 | // number we want. 71 | fakeCoinbase = strings.Replace(fakeCoinbase, "d12c0c00", 72 | fmt.Sprintf("%02x", curHeight&0xFF)+ 73 | fmt.Sprintf("%02x", (curHeight>>8)&0xFF)+ 74 | fmt.Sprintf("%02x", (curHeight>>16)&0xFF)+ 75 | fmt.Sprintf("%02x", (curHeight>>24)&0xFF), 1) 76 | 77 | var numTransactions uint = 1 // coinbase 78 | allTransactionsHex := "" 79 | for scan.Scan() { // each line (hex-encoded transaction) 80 | allTransactionsHex += scan.Text() 81 | numTransactions++ 82 | } 83 | if err = scan.Err(); err != nil { 84 | panic("line too long!") 85 | } 86 | if numTransactions > 65535 { 87 | panic(fmt.Sprint("too many transactions ", numTransactions, 88 | " maximum 65535")) 89 | } 90 | 91 | hashOfTxnsAndHeight := sha256.Sum256([]byte(allTransactionsHex + "#" + string(rune(curHeight)))) 92 | 93 | // These fields do not need to be valid for the lightwalletd/wallet stack to work. 94 | // The lightwalletd/wallet stack rely on the miners to validate these. 95 | // Make the block header depend on height + all transactions (in an incorrect way) 96 | blockHeader := &parser.BlockHeader{ 97 | RawBlockHeader: &parser.RawBlockHeader{ 98 | Version: 4, 99 | HashPrevBlock: prevhash, 100 | HashMerkleRoot: hashOfTxnsAndHeight[:], 101 | HashFinalSaplingRoot: make([]byte, 32), 102 | Time: 1, 103 | NBitsBytes: make([]byte, 4), 104 | Nonce: make([]byte, 32), 105 | Solution: make([]byte, 1344), 106 | }, 107 | } 108 | 109 | headerBytes, err := blockHeader.MarshalBinary() 110 | if err != nil { 111 | panic(fmt.Sprint("Cannot marshal block header: ", err)) 112 | } 113 | fmt.Print(hex.EncodeToString(headerBytes)) 114 | 115 | // After the header, there's a compactsize representation of the number of transactions. 116 | if numTransactions < 253 { 117 | fmt.Printf("%02x", numTransactions) 118 | } else { 119 | fmt.Printf("%02x%02x%02x", 253, numTransactions%256, numTransactions/256) 120 | } 121 | fmt.Printf("%s%s\n", fakeCoinbase, allTransactionsHex) 122 | 123 | curHeight++ 124 | prevhash = blockHeader.GetEncodableHash() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /testtools/zap/main.go: -------------------------------------------------------------------------------- 1 | // This program increments a given byte of a given file, 2 | // to test data corruption detection -- BE CAREFUL! 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | func main() { 12 | if len(os.Args) != 3 { 13 | fmt.Println("usage:", os.Args[0], "file offset") 14 | os.Exit(1) 15 | } 16 | f, err := os.OpenFile(os.Args[1], os.O_RDWR, 0644) 17 | if err != nil { 18 | fmt.Println("open failed:", err) 19 | os.Exit(1) 20 | } 21 | offset, err := strconv.ParseInt(os.Args[2], 10, 64) 22 | if err != nil { 23 | fmt.Println("bad offset:", err) 24 | os.Exit(1) 25 | } 26 | b := make([]byte, 1) 27 | if n, err := f.ReadAt(b, offset); err != nil || n != 1 { 28 | fmt.Println("read failed:", n, err) 29 | os.Exit(1) 30 | } 31 | b[0] += 1 32 | if n, err := f.WriteAt(b, offset); err != nil || n != 1 { 33 | fmt.Println("read failed:", n, err) 34 | os.Exit(1) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /utils/pullblocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: ./pullblocks.sh 500000 500100 > blocks.txt 3 | test $# -ne 2 && { echo usage: $0 start end;exit 1;} 4 | 5 | let i=$1 6 | while test $i -le $2 7 | do 8 | zcash-cli getblock $i 0 9 | let i++ 10 | done 11 | -------------------------------------------------------------------------------- /utils/submitblocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Submits a list of blocks, one per line in the file, to darksidewalletd. 3 | # Usage: ./submitblocks.sh 4 | # e.g. ./submitblocks.sh 1000 blocks.txt 5 | # 6 | set -e 7 | test $# -ne 2 && { echo usage: $0 sapling-height blocks-file;exit 1;} 8 | 9 | # must do a Reset first 10 | grpcurl -plaintext -d '{"saplingActivation":'$1',"branchID":"2bb40e60","chainName":"main"}' localhost:9067 cash.z.wallet.sdk.rpc.DarksideStreamer/Reset 11 | 12 | # send the blocks and make them active 13 | sed 's/^/{"block":"/;s/$/"}/' $2 | 14 | grpcurl -plaintext -d @ localhost:9067 cash.z.wallet.sdk.rpc.DarksideStreamer/StageBlocksStream 15 | let latest=$1+$(cat $2|wc -l)-1 16 | grpcurl -plaintext -d '{"height":'$latest'}' localhost:9067 cash.z.wallet.sdk.rpc.DarksideStreamer/ApplyStaged 17 | -------------------------------------------------------------------------------- /walletrpc/compact_formats.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | syntax = "proto3"; 6 | package cash.z.wallet.sdk.rpc; 7 | option go_package = "lightwalletd/walletrpc"; 8 | option swift_prefix = ""; 9 | 10 | // Remember that proto3 fields are all optional. A field that is not present will be set to its zero value. 11 | // bytes fields of hashes are in canonical little-endian format. 12 | 13 | // ChainMetadata represents information about the state of the chain as of a given block. 14 | message ChainMetadata { 15 | uint32 saplingCommitmentTreeSize = 1; // the size of the Sapling note commitment tree as of the end of this block 16 | uint32 orchardCommitmentTreeSize = 2; // the size of the Orchard note commitment tree as of the end of this block 17 | } 18 | 19 | // CompactBlock is a packaging of ONLY the data from a block that's needed to: 20 | // 1. Detect a payment to your shielded Sapling address 21 | // 2. Detect a spend of your shielded Sapling notes 22 | // 3. Update your witnesses to generate new Sapling spend proofs. 23 | message CompactBlock { 24 | uint32 protoVersion = 1; // the version of this wire format, for storage 25 | uint64 height = 2; // the height of this block 26 | bytes hash = 3; // the ID (hash) of this block, same as in block explorers 27 | bytes prevHash = 4; // the ID (hash) of this block's predecessor 28 | uint32 time = 5; // Unix epoch time when the block was mined 29 | bytes header = 6; // (hash, prevHash, and time) OR (full header) 30 | repeated CompactTx vtx = 7; // zero or more compact transactions from this block 31 | ChainMetadata chainMetadata = 8; // information about the state of the chain as of this block 32 | } 33 | 34 | // CompactTx contains the minimum information for a wallet to know if this transaction 35 | // is relevant to it (either pays to it or spends from it) via shielded elements 36 | // only. This message will not encode a transparent-to-transparent transaction. 37 | message CompactTx { 38 | // Index and hash will allow the receiver to call out to chain 39 | // explorers or other data structures to retrieve more information 40 | // about this transaction. 41 | uint64 index = 1; // the index within the full block 42 | bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers 43 | 44 | // The transaction fee: present if server can provide. In the case of a 45 | // stateless server and a transaction with transparent inputs, this will be 46 | // unset because the calculation requires reference to prior transactions. 47 | // If there are no transparent inputs, the fee will be calculable as: 48 | // valueBalanceSapling + valueBalanceOrchard + sum(vPubNew) - sum(vPubOld) - sum(tOut) 49 | uint32 fee = 3; 50 | 51 | repeated CompactSaplingSpend spends = 4; 52 | repeated CompactSaplingOutput outputs = 5; 53 | repeated CompactOrchardAction actions = 6; 54 | } 55 | 56 | // CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash 57 | // protocol specification. 58 | message CompactSaplingSpend { 59 | bytes nf = 1; // nullifier (see the Zcash protocol specification) 60 | } 61 | 62 | // output encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the 63 | // `encCiphertext` field of a Sapling Output Description. These fields are described in 64 | // section 7.4 of the Zcash protocol spec: 65 | // https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus 66 | // Total size is 116 bytes. 67 | message CompactSaplingOutput { 68 | bytes cmu = 1; // note commitment u-coordinate 69 | bytes ephemeralKey = 2; // ephemeral public key 70 | bytes ciphertext = 3; // first 52 bytes of ciphertext 71 | } 72 | 73 | // https://github.com/zcash/zips/blob/main/zip-0225.rst#orchard-action-description-orchardaction 74 | // (but not all fields are needed) 75 | message CompactOrchardAction { 76 | bytes nullifier = 1; // [32] The nullifier of the input note 77 | bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note 78 | bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key 79 | bytes ciphertext = 4; // [52] The first 52 bytes of the encCiphertext field 80 | } 81 | -------------------------------------------------------------------------------- /walletrpc/darkside.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | syntax = "proto3"; 6 | package cash.z.wallet.sdk.rpc; 7 | option go_package = "lightwalletd/walletrpc"; 8 | option swift_prefix = ""; 9 | import "service.proto"; 10 | 11 | message DarksideMetaState { 12 | int32 saplingActivation = 1; 13 | string branchID = 2; 14 | string chainName = 3; 15 | uint32 startSaplingCommitmentTreeSize = 4; 16 | uint32 startOrchardCommitmentTreeSize = 5; 17 | } 18 | 19 | // A block is a hex-encoded string. 20 | message DarksideBlock { 21 | string block = 1; 22 | } 23 | 24 | // DarksideBlocksURL is typically something like: 25 | // https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt 26 | message DarksideBlocksURL { 27 | string url = 1; 28 | } 29 | 30 | // DarksideTransactionsURL refers to an HTTP source that contains a list 31 | // of hex-encoded transactions, one per line, that are to be associated 32 | // with the given height (fake-mined into the block at that height) 33 | message DarksideTransactionsURL { 34 | int32 height = 1; 35 | string url = 2; 36 | } 37 | 38 | message DarksideHeight { 39 | int32 height = 1; 40 | } 41 | 42 | message DarksideEmptyBlocks { 43 | int32 height = 1; 44 | int32 nonce = 2; 45 | int32 count = 3; 46 | } 47 | 48 | message DarksideSubtreeRoots { 49 | ShieldedProtocol shieldedProtocol = 1; 50 | uint32 startIndex = 2; 51 | repeated SubtreeRoot subtreeRoots = 3; 52 | } 53 | 54 | // Darksidewalletd maintains two staging areas, blocks and transactions. The 55 | // Stage*() gRPCs add items to the staging area; ApplyStaged() "applies" everything 56 | // in the staging area to the working (operational) state that the mock zcashd 57 | // serves; transactions are placed into their corresponding blocks (by height). 58 | service DarksideStreamer { 59 | // Reset reverts all darksidewalletd state (active block range, latest height, 60 | // staged blocks and transactions) and lightwalletd state (cache) to empty, 61 | // the same as the initial state. This occurs synchronously and instantaneously; 62 | // no reorg happens in lightwalletd. This is good to do before each independent 63 | // test so that no state leaks from one test to another. 64 | // Also sets (some of) the values returned by GetLightdInfo(). The Sapling 65 | // activation height specified here must be where the block range starts. 66 | rpc Reset(DarksideMetaState) returns (Empty) {} 67 | 68 | // StageBlocksStream accepts a list of blocks and saves them into the blocks 69 | // staging area until ApplyStaged() is called; there is no immediate effect on 70 | // the mock zcashd. Blocks are hex-encoded. Order is important, see ApplyStaged. 71 | rpc StageBlocksStream(stream DarksideBlock) returns (Empty) {} 72 | 73 | // StageBlocks is the same as StageBlocksStream() except the blocks are fetched 74 | // from the given URL. Blocks are one per line, hex-encoded (not JSON). 75 | rpc StageBlocks(DarksideBlocksURL) returns (Empty) {} 76 | 77 | // StageBlocksCreate is like the previous two, except it creates 'count' 78 | // empty blocks at consecutive heights starting at height 'height'. The 79 | // 'nonce' is part of the header, so it contributes to the block hash; this 80 | // lets you create identical blocks (same transactions and height), but with 81 | // different hashes. 82 | rpc StageBlocksCreate(DarksideEmptyBlocks) returns (Empty) {} 83 | 84 | // StageTransactionsStream stores the given transaction-height pairs in the 85 | // staging area until ApplyStaged() is called. Note that these transactions 86 | // are not returned by the production GetTransaction() gRPC until they 87 | // appear in a "mined" block (contained in the active blockchain presented 88 | // by the mock zcashd). 89 | rpc StageTransactionsStream(stream RawTransaction) returns (Empty) {} 90 | 91 | // StageTransactions is the same except the transactions are fetched from 92 | // the given url. They are all staged into the block at the given height. 93 | // Staging transactions to different heights requires multiple calls. 94 | rpc StageTransactions(DarksideTransactionsURL) returns (Empty) {} 95 | 96 | // ApplyStaged iterates the list of blocks that were staged by the 97 | // StageBlocks*() gRPCs, in the order they were staged, and "merges" each 98 | // into the active, working blocks list that the mock zcashd is presenting 99 | // to lightwalletd. Even as each block is applied, the active list can't 100 | // have gaps; if the active block range is 1000-1006, and the staged block 101 | // range is 1003-1004, the resulting range is 1000-1004, with 1000-1002 102 | // unchanged, blocks 1003-1004 from the new range, and 1005-1006 dropped. 103 | // 104 | // After merging all blocks, ApplyStaged() appends staged transactions (in 105 | // the order received) into each one's corresponding (by height) block 106 | // The staging area is then cleared. 107 | // 108 | // The argument specifies the latest block height that mock zcashd reports 109 | // (i.e. what's returned by GetLatestBlock). Note that ApplyStaged() can 110 | // also be used to simply advance the latest block height presented by mock 111 | // zcashd. That is, there doesn't need to be anything in the staging area. 112 | rpc ApplyStaged(DarksideHeight) returns (Empty) {} 113 | 114 | // Calls to the production gRPC SendTransaction() store the transaction in 115 | // a separate area (not the staging area); this method returns all transactions 116 | // in this separate area, which is then cleared. The height returned 117 | // with each transaction is -1 (invalid) since these transactions haven't 118 | // been mined yet. The intention is that the transactions returned here can 119 | // then, for example, be given to StageTransactions() to get them "mined" 120 | // into a specified block on the next ApplyStaged(). 121 | rpc GetIncomingTransactions(Empty) returns (stream RawTransaction) {} 122 | 123 | // Clear the incoming transaction pool. 124 | rpc ClearIncomingTransactions(Empty) returns (Empty) {} 125 | 126 | // Add a GetAddressUtxosReply entry to be returned by GetAddressUtxos(). 127 | // There is no staging or applying for these, very simple. 128 | rpc AddAddressUtxo(GetAddressUtxosReply) returns (Empty) {} 129 | 130 | // Clear the list of GetAddressUtxos entries (can't fail) 131 | rpc ClearAddressUtxo(Empty) returns (Empty) {} 132 | 133 | // Adds a GetTreeState to the tree state cache 134 | rpc AddTreeState(TreeState) returns (Empty) {} 135 | 136 | // Removes a GetTreeState for the given height from cache if present (can't fail) 137 | rpc RemoveTreeState(BlockID) returns (Empty) {} 138 | 139 | // Clear the list of GetTreeStates entries (can't fail) 140 | rpc ClearAllTreeStates(Empty) returns (Empty) {} 141 | 142 | // Sets the subtree roots cache (for GetSubtreeRoots), 143 | // replacing any existing entries 144 | rpc SetSubtreeRoots(DarksideSubtreeRoots) returns (Empty) {} 145 | 146 | // Stop causes the server to shut down cleanly. 147 | rpc Stop(Empty) returns (Empty) {} 148 | } 149 | -------------------------------------------------------------------------------- /walletrpc/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package walletrpc 5 | 6 | //go:generate protoc -I . --go_out=. --go_opt=paths=source_relative ./compact_formats.proto 7 | //go:generate protoc -I . --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./service.proto 8 | //go:generate protoc -I . --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./darkside.proto 9 | -------------------------------------------------------------------------------- /walletrpc/service.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | 5 | syntax = "proto3"; 6 | package cash.z.wallet.sdk.rpc; 7 | option go_package = "lightwalletd/walletrpc"; 8 | option swift_prefix = ""; 9 | import "compact_formats.proto"; 10 | 11 | // A BlockID message contains identifiers to select a block: a height or a 12 | // hash. Specification by hash is not implemented, but may be in the future. 13 | message BlockID { 14 | uint64 height = 1; 15 | bytes hash = 2; 16 | } 17 | 18 | // BlockRange specifies a series of blocks from start to end inclusive. 19 | // Both BlockIDs must be heights; specification by hash is not yet supported. 20 | message BlockRange { 21 | BlockID start = 1; 22 | BlockID end = 2; 23 | } 24 | 25 | // A TxFilter contains the information needed to identify a particular 26 | // transaction: either a block and an index, or a direct transaction hash. 27 | // Currently, only specification by hash is supported. 28 | message TxFilter { 29 | BlockID block = 1; // block identifier, height or hash 30 | uint64 index = 2; // index within the block 31 | bytes hash = 3; // transaction ID (hash, txid) 32 | } 33 | 34 | // RawTransaction contains the complete transaction data. It also includes the 35 | // height for the block in which the transaction was included in the main 36 | // chain, if any (as detailed below). 37 | message RawTransaction { 38 | // The serialized representation of the Zcash transaction. 39 | bytes data = 1; 40 | // The height at which the transaction is mined, or a sentinel value. 41 | // 42 | // Due to an error in the original protobuf definition, it is necessary to 43 | // reinterpret the result of the `getrawtransaction` RPC call. Zcashd will 44 | // return the int64 value `-1` for the height of transactions that appear 45 | // in the block index, but which are not mined in the main chain. Here, the 46 | // height field of `RawTransaction` was erroneously created as a `uint64`, 47 | // and as such we must map the response from the zcashd RPC API to be 48 | // representable within this space. Additionally, the `height` field will 49 | // be absent for transactions in the mempool, resulting in the default 50 | // value of `0` being set. Therefore, the meanings of the `height` field of 51 | // the `RawTransaction` type are as follows: 52 | // 53 | // * height 0: the transaction is in the mempool 54 | // * height 0xffffffffffffffff: the transaction has been mined on a fork that 55 | // is not currently the main chain 56 | // * any other height: the transaction has been mined in the main chain at the 57 | // given height 58 | uint64 height = 2; 59 | } 60 | 61 | // A SendResponse encodes an error code and a string. It is currently used 62 | // only by SendTransaction(). If error code is zero, the operation was 63 | // successful; if non-zero, it and the message specify the failure. 64 | message SendResponse { 65 | int32 errorCode = 1; 66 | string errorMessage = 2; 67 | } 68 | 69 | // Chainspec is a placeholder to allow specification of a particular chain fork. 70 | message ChainSpec {} 71 | 72 | // Empty is for gRPCs that take no arguments, currently only GetLightdInfo. 73 | message Empty {} 74 | 75 | // LightdInfo returns various information about this lightwalletd instance 76 | // and the state of the blockchain. 77 | message LightdInfo { 78 | string version = 1; 79 | string vendor = 2; 80 | bool taddrSupport = 3; // true 81 | string chainName = 4; // either "main" or "test" 82 | uint64 saplingActivationHeight = 5; // depends on mainnet or testnet 83 | string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp 84 | uint64 blockHeight = 7; // latest block on the best chain 85 | string gitCommit = 8; 86 | string branch = 9; 87 | string buildDate = 10; 88 | string buildUser = 11; 89 | uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing 90 | string zcashdBuild = 13; // example: "v4.1.1-877212414" 91 | string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/" 92 | string donationAddress = 15; // Zcash donation UA address 93 | } 94 | 95 | // TransparentAddressBlockFilter restricts the results to the given address 96 | // or block range. 97 | message TransparentAddressBlockFilter { 98 | string address = 1; // t-address 99 | BlockRange range = 2; // start, end heights 100 | } 101 | 102 | // Duration is currently used only for testing, so that the Ping rpc 103 | // can simulate a delay, to create many simultaneous connections. Units 104 | // are microseconds. 105 | message Duration { 106 | int64 intervalUs = 1; 107 | } 108 | 109 | // PingResponse is used to indicate concurrency, how many Ping rpcs 110 | // are executing upon entry and upon exit (after the delay). 111 | // This rpc is used for testing only. 112 | message PingResponse { 113 | int64 entry = 1; 114 | int64 exit = 2; 115 | } 116 | 117 | message Address { 118 | string address = 1; 119 | } 120 | message AddressList { 121 | repeated string addresses = 1; 122 | } 123 | message Balance { 124 | int64 valueZat = 1; 125 | } 126 | 127 | message Exclude { 128 | repeated bytes txid = 1; 129 | } 130 | 131 | // The TreeState is derived from the Zcash z_gettreestate rpc. 132 | message TreeState { 133 | string network = 1; // "main" or "test" 134 | uint64 height = 2; // block height 135 | string hash = 3; // block id 136 | uint32 time = 4; // Unix epoch time when the block was mined 137 | string saplingTree = 5; // sapling commitment tree state 138 | string orchardTree = 6; // orchard commitment tree state 139 | } 140 | 141 | enum ShieldedProtocol { 142 | sapling = 0; 143 | orchard = 1; 144 | } 145 | 146 | message GetSubtreeRootsArg { 147 | uint32 startIndex = 1; // Index identifying where to start returning subtree roots 148 | ShieldedProtocol shieldedProtocol = 2; // Shielded protocol to return subtree roots for 149 | uint32 maxEntries = 3; // Maximum number of entries to return, or 0 for all entries. 150 | } 151 | message SubtreeRoot { 152 | bytes rootHash = 2; // The 32-byte Merkle root of the subtree. 153 | bytes completingBlockHash = 3; // The hash of the block that completed this subtree. 154 | uint64 completingBlockHeight = 4; // The height of the block that completed this subtree in the main chain. 155 | } 156 | 157 | // Results are sorted by height, which makes it easy to issue another 158 | // request that picks up from where the previous left off. 159 | message GetAddressUtxosArg { 160 | repeated string addresses = 1; 161 | uint64 startHeight = 2; 162 | uint32 maxEntries = 3; // zero means unlimited 163 | } 164 | message GetAddressUtxosReply { 165 | string address = 6; 166 | bytes txid = 1; 167 | int32 index = 2; 168 | bytes script = 3; 169 | int64 valueZat = 4; 170 | uint64 height = 5; 171 | } 172 | message GetAddressUtxosReplyList { 173 | repeated GetAddressUtxosReply addressUtxos = 1; 174 | } 175 | 176 | service CompactTxStreamer { 177 | // Return the height of the tip of the best chain 178 | rpc GetLatestBlock(ChainSpec) returns (BlockID) {} 179 | // Return the compact block corresponding to the given block identifier 180 | rpc GetBlock(BlockID) returns (CompactBlock) {} 181 | // Same as GetBlock except actions contain only nullifiers 182 | rpc GetBlockNullifiers(BlockID) returns (CompactBlock) {} 183 | // Return a list of consecutive compact blocks 184 | rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {} 185 | // Same as GetBlockRange except actions contain only nullifiers 186 | rpc GetBlockRangeNullifiers(BlockRange) returns (stream CompactBlock) {} 187 | 188 | // Return the requested full (not compact) transaction (as from zcashd) 189 | rpc GetTransaction(TxFilter) returns (RawTransaction) {} 190 | // Submit the given transaction to the Zcash network 191 | rpc SendTransaction(RawTransaction) returns (SendResponse) {} 192 | 193 | // Return the transactions corresponding to the given t-address within the given block range 194 | // NB - this method is misnamed, it returns transactions, not transaction IDs. 195 | // NOTE: this method is deprecated, please use GetTaddressTransactions instead. 196 | rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {} 197 | 198 | // Return the transactions corresponding to the given t-address within the given block range 199 | rpc GetTaddressTransactions(TransparentAddressBlockFilter) returns (stream RawTransaction) {} 200 | 201 | rpc GetTaddressBalance(AddressList) returns (Balance) {} 202 | rpc GetTaddressBalanceStream(stream Address) returns (Balance) {} 203 | 204 | // Return the compact transactions currently in the mempool; the results 205 | // can be a few seconds out of date. If the Exclude list is empty, return 206 | // all transactions; otherwise return all *except* those in the Exclude list 207 | // (if any); this allows the client to avoid receiving transactions that it 208 | // already has (from an earlier call to this rpc). The transaction IDs in the 209 | // Exclude list can be shortened to any number of bytes to make the request 210 | // more bandwidth-efficient; if two or more transactions in the mempool 211 | // match a shortened txid, they are all sent (none is excluded). Transactions 212 | // in the exclude list that don't exist in the mempool are ignored. 213 | rpc GetMempoolTx(Exclude) returns (stream CompactTx) {} 214 | 215 | // Return a stream of current Mempool transactions. This will keep the output stream open while 216 | // there are mempool transactions. It will close the returned stream when a new block is mined. 217 | rpc GetMempoolStream(Empty) returns (stream RawTransaction) {} 218 | 219 | // GetTreeState returns the note commitment tree state corresponding to the given block. 220 | // See section 3.7 of the Zcash protocol specification. It returns several other useful 221 | // values also (even though they can be obtained using GetBlock). 222 | // The block can be specified by either height or hash. 223 | rpc GetTreeState(BlockID) returns (TreeState) {} 224 | rpc GetLatestTreeState(Empty) returns (TreeState) {} 225 | 226 | // Returns a stream of information about roots of subtrees of the note commitment tree 227 | // for the specified shielded protocol (Sapling or Orchard). 228 | rpc GetSubtreeRoots(GetSubtreeRootsArg) returns (stream SubtreeRoot) {} 229 | 230 | rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {} 231 | rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {} 232 | 233 | // Return information about this lightwalletd instance and the blockchain 234 | rpc GetLightdInfo(Empty) returns (LightdInfo) {} 235 | // Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production) 236 | rpc Ping(Duration) returns (PingResponse) {} 237 | } 238 | -------------------------------------------------------------------------------- /walletrpc/walletrpc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 The Zcash developers 2 | // Distributed under the MIT software license, see the accompanying 3 | // file COPYING or https://www.opensource.org/licenses/mit-license.php . 4 | package walletrpc 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestString_read(t *testing.T) { 11 | } 12 | --------------------------------------------------------------------------------