diff --git a/.cargo/config.toml b/.cargo/config.toml index ea6b21a77bb..8220a38ffa2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,89 +1,10 @@ # Zebra cargo configuration -# Flags that apply to all Zebra crates and configurations +# TODO: Remove when ZSA is stable [target.'cfg(all())'] rustflags = [ - # Enable tx_v6 everywhere by default "--cfg", 'feature="tx_v6"', - - # TODO: Remove when ZSA is stable "--cfg", "zcash_unstable=\"nu7\"", - - # Zebra standard lints for Rust 1.65+ - - # High-risk code - "-Dunsafe_code", - "-Dnon_ascii_idents", - - # Potential bugs - # - # If we deny these lints, we could be excluded from Crater builds: - # https://www.reddit.com/r/rust/comments/f5xpib/psa_denywarnings_is_actively_harmful/ - - # Compatibility - "-Wrust_2021_compatibility", - "-Wnonstandard_style", - "-Wfuture_incompatible", - - # Async code - "-Wclippy::await_holding_lock", - "-Wclippy::await_holding_refcell_ref", - - # Pointers - "-Wclippy::cast_ptr_alignment", - "-Wclippy::fn_to_numeric_cast_any", - - # Integers - "-Wclippy::checked_conversions", - "-Wclippy::implicit_saturating_sub", - "-Wclippy::invalid_upcast_comparisons", - "-Wclippy::range_minus_one", - "-Wclippy::range_plus_one", - "-Wclippy::unnecessary_cast", - - # Incomplete code - "-Wclippy::dbg_macro", - "-Wclippy::todo", - - # Manual debugging output. - # Use tracing::trace!() or tracing::debug!() instead. - "-Wclippy::print_stdout", - "-Wclippy::print_stderr", - "-Wclippy::dbg_macro", - - # Code styles we want to accept - "-Aclippy::try_err", - - # Panics - "-Wclippy::fallible_impl_from", - "-Wclippy::unwrap_in_result", - - # Documentation - "-Wmissing_docs", - - # TODOs: - # Fix this lint eventually. - "-Aclippy::result_large_err", - - # `cargo fix` might help do these fixes, - # or add a config.toml to sub-directories which should allow these lints, - # or try allowing the lint in the specific module (lib.rs doesn't seem to work in some cases) - # - # lint configs that don't work: - # - allowing these lints in lib.rs (command-line warn overrides allow in lib.rs?) - # - adding a [target.'cfg(not(test))'] rustflags config (it runs on test code anyway) - - # fix code that triggers these lints, - # or disable the lint for that code (or for all test code) - # - #"-Wclippy::cast_lossless", # 30 non-test warnings, a few test warnings - #"-Wclippy::cast_possible_truncation", # 40 non-test warnings, 20 test warnings - #"-Wclippy::cast_possible_wrap", # 13 test warnings (fixed outside tests) - #"-Wclippy::cast_precision_loss", # 25 non-test warnings, 10 test warnings - #"-Wclippy::cast_sign_loss", # 6 non-test warnings, 15 test warnings - - # fix hidden lifetime parameters - #"-Wrust_2018_idioms", ] [build] @@ -94,12 +15,13 @@ rustdocflags = [ # TODO: Remove when ZSA is stable "--cfg", "zcash_unstable=\"nu7\"", - # The -A and -W settings must be the same as the `RUSTDOCFLAGS` in: - # https://github.com/ZcashFoundation/zebra/blob/main/.github/workflows/docs-deploy-firebase.yml#L68 - # Links in public docs can point to private items. "-Arustdoc::private_intra_doc_links", ] [env] -RUST_BACKTRACE="1" +RUST_BACKTRACE = "1" + +[profile.profiling] +inherits = "release" +debug = true diff --git a/.config/nextest.toml b/.config/nextest.toml index 2ba7e0bc3ff..224f08be215 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -30,6 +30,16 @@ filter = "not test(=trusted_chain_sync_handles_forks_correctly) and not test(=de # TODO: We need a better test architecture to run all non-stateful [profile.all-tests] failure-output = "immediate" + +# Keep going after a failure so one broken test does not hide the state of +# everything still queued, and log only retried and failed tests: "pass" buries +# real failures under ~1000 lines of output. +fail-fast = false +status-level = "retry" + +# Hard cap for every test in this profile, so a test that hangs instead of +# failing cannot run until the workflow timeout and take the whole job down. +slow-timeout = { period = "5m", terminate-after = 4 } default-filter = "not test(check_no_git_dependencies) and not test(=fully_synced_rpc_z_getsubtreesbyindex_snapshot_test) and not test(=lwd_rpc_test) and not test(=lwd_rpc_send_tx) and not test(=lwd_grpc_wallet) and not test(=lwd_integration) and not test(=lwd_sync_full) and not test(=lwd_sync_update) and not test(=lightwalletd_test_suite) and not test(=rpc_get_block_template) and not test(=rpc_submit_block) and not test(=get_peer_info) and not test(~generate_checkpoints_) and not test(=sync_update_mainnet) and not test(=activate_mempool_mainnet)" # --- Individual Test Profiles --- @@ -130,15 +140,3 @@ default-filter = 'package(zebrad) and test(=lightwalletd_test_suite)' [profile.rpc-z-getsubtreesbyindex-snapshot] slow-timeout = { period = "30m", terminate-after = 2 } default-filter = 'package(zebrad) and test(=fully_synced_rpc_z_getsubtreesbyindex_snapshot_test)' - -# --- QEDIT temporary retries --- - -# TODO: Remove retries after investigating periodic random failures. -# Add more tests here only after confirming similar intermittent failures. -[[profile.default.overrides]] -filter = ''' -test(=sync_large_checkpoints_mempool_mainnet) -or test(=get_peer_info) -or test(=activate_mempool_mainnet) -''' -retries = 3 diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index db993fa0537..7bf8edcc1ad 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -39,7 +39,7 @@ This step can be skipped if there is a large pending dependency upgrade. (For ex Here's how we make sure we got everything: - [ ] Run `cargo update` on the latest `main` branch, and keep the output -- [ ] Until we bump the MSRV to 1.88 or higher, `home` must be downgraded manually: `cargo update home@0.5.12 --precise 0.5.11` +- [ ] Until we bump the workspace MSRV to 1.88 or higher, `home` must be downgraded manually: `cargo update home@0.5.12 --precise 0.5.11` - [ ] If needed, [add duplicate dependency exceptions to deny.toml](https://github.com/ZcashFoundation/zebra/blob/main/book/src/dev/continuous-integration.md#fixing-duplicate-dependencies-in-check-denytoml-bans) - [ ] If needed, remove resolved duplicate dependencies from `deny.toml` - [ ] Open a separate PR with the changes diff --git a/.github/PULL_REQUEST_TEMPLATE/release-checklist.md b/.github/PULL_REQUEST_TEMPLATE/release-checklist.md index a55045dbc80..3358781c6bd 100644 --- a/.github/PULL_REQUEST_TEMPLATE/release-checklist.md +++ b/.github/PULL_REQUEST_TEMPLATE/release-checklist.md @@ -25,7 +25,7 @@ This step can be skipped if there is a large pending dependency upgrade. (For ex Here's how we make sure we got everything: - [ ] Run `cargo update` on the latest `main` branch, and keep the output -- [ ] Until we bump the MSRV to 1.88 or higher, `home` must be downgraded manually: `cargo update home@0.5.12 --precise 0.5.11` +- [ ] Until we bump the workspace MSRV to 1.88 or higher, `home` must be downgraded manually: `cargo update home@0.5.12 --precise 0.5.11` - [ ] If needed, [add duplicate dependency exceptions to deny.toml](https://github.com/ZcashFoundation/zebra/blob/main/book/src/dev/continuous-integration.md#fixing-duplicate-dependencies-in-check-denytoml-bans) - [ ] If needed, remove resolved duplicate dependencies from `deny.toml` - [ ] Open a separate PR with the changes @@ -131,11 +131,14 @@ Check that the release will work: ```sh cargo release version --verbose --execute --allow-branch '*' -p patch # [ major | minor ] -cargo release replace --verbose --execute --allow-branch '*' -p +# zebrad only +cargo release replace --verbose --execute --allow-branch '*' -p zebrad ``` - [ ] Commit and push the above version changes to the release branch. +- [ ] **Major (network upgrade) releases only:** Check that `INITIAL_MIN_NETWORK_PROTOCOL_VERSION` in [`zebra-network/src/constants.rs`](https://github.com/ZcashFoundation/zebra/blob/main/zebra-network/src/constants.rs) uses the latest activated network upgrade. + ## Update End of Support The end of support height is calculated from the current blockchain height: diff --git a/.github/actions/setup-zebra-build/action.yml b/.github/actions/setup-zebra-build/action.yml index 31bd7772e7f..b7ed4718729 100644 --- a/.github/actions/setup-zebra-build/action.yml +++ b/.github/actions/setup-zebra-build/action.yml @@ -1,20 +1,27 @@ name: 'Setup Zebra Build Environment' -description: 'Install protoc as system library' +description: 'Install protoc and RocksDB as system libraries' runs: using: 'composite' steps: - - name: Install protoc on Ubuntu + - name: Install protoc and librocksdb-dev on Ubuntu if: runner.os == 'Linux' shell: bash run: | sudo apt-get -qq update - sudo apt-get -qq install -y --no-install-recommends protobuf-compiler + sudo apt-get -qq install -y --no-install-recommends protobuf-compiler librocksdb-dev + echo "ROCKSDB_LIB_DIR=/usr/lib/" >> $GITHUB_ENV - - name: Install protoc on macOS + - name: Install protoc and RocksDB on macOS if: runner.os == 'macOS' shell: bash run: | - brew install protobuf + brew install protobuf rocksdb + # Set ROCKSDB_LIB_DIR for both Intel and Apple Silicon Macs + if [ -d "/opt/homebrew/lib" ]; then + echo "ROCKSDB_LIB_DIR=/opt/homebrew/lib" >> $GITHUB_ENV + else + echo "ROCKSDB_LIB_DIR=/usr/local/lib" >> $GITHUB_ENV + fi - name: Install protoc on Windows if: runner.os == 'Windows' diff --git a/.github/mergify.yml b/.github/mergify.yml index 934981439ca..31405d0e5ae 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -16,8 +16,10 @@ defaults: # and it does not create a draft PR if not needed batch_max_wait_time: "10 minutes" queue_conditions: - # Mergify automatically applies status check, approval, and conversation rules, - # which are the same as the GitHub main branch protection rules + # Mergify can auto-mirror classic branch protection, but not GitHub + # Rulesets. The conditions below restate the gates we actually rely on + # so PRs cannot be queued past failing required checks or unresolved + # change-requests. # https://docs.mergify.com/conditions/#about-branch-protection - base=main # is not in draft @@ -26,6 +28,14 @@ defaults: - label!=do-not-merge # has at least one approving reviewer - "#approved-reviews-by >= 1" + # no reviewer has requested changes + - "#changes-requested-reviews-by = 0" + # The three required aggregator check runs must pass. After the + # `changes` + paths-filter refactor (#10637), each aggregator is + # produced by exactly one workflow, so the names below are stable. + - check-success=lint + - check-success=unit-tests + - check-success=test-crates # Allows to define the rules that reign over our merge queues @@ -65,3 +75,5 @@ priority_rules: - 'label~=^P-(Optional|Low)' allow_checks_interruption: true priority: low +merge_protections_settings: + reporting_method: check-runs diff --git a/.github/path-filters.yml b/.github/path-filters.yml new file mode 100644 index 00000000000..66fbe7f406b --- /dev/null +++ b/.github/path-filters.yml @@ -0,0 +1,76 @@ +# Single source of truth for "what changed files trigger what workflow." +# +# Consumed by dorny/paths-filter in each required-check workflow's `changes` job. +# Each key maps 1:1 to a workflow basename; values are picomatch globs. +# +# Editing this file re-triggers every workflow on the next PR (the file is +# listed in every filter), which is the desired safety property when the +# triggering rules themselves change. + +lint: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - ".cargo/config.toml" + - "**/clippy.toml" + - "deny.toml" + - "supply-chain/**" + - ".github/workflows/lint.yml" + - ".github/path-filters.yml" + +unit_tests: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - ".config/nextest.toml" + - ".github/workflows/tests-unit.yml" + - ".github/path-filters.yml" + +test_crates: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - ".cargo/config.toml" + - "**/clippy.toml" + - ".github/workflows/test-crates.yml" + - ".github/path-filters.yml" + +test_docker: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - "docker/Dockerfile" + - "docker/entrypoint.sh" + - "docker/**/*.toml" + - "zebrad/tests/common/configs/**" + - ".github/workflows/test-docker.yml" + - ".github/path-filters.yml" + +book: + - "book/**" + - "katex-header.html" + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - ".cargo/config.toml" + - "**/clippy.toml" + - ".github/workflows/book.yml" + - ".github/path-filters.yml" + +coverage: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - ".config/nextest.toml" + - "codecov.yml" + - ".github/workflows/coverage.yml" + - ".github/path-filters.yml" + +docs_check: + - "**/*.md" + - "book/**" + - ".lychee.toml" + - ".codespellrc" + - ".trunk/configs/.markdownlint.yaml" + - ".github/workflows/docs-check.yml" + - ".github/path-filters.yml" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1b1cd14c6ec..d0c765b4056 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -42,7 +42,7 @@ ### PR Checklist -- [ ] The PR name is suitable for the release notes. +- [ ] The PR title follows [conventional commits](https://www.conventionalcommits.org/) format: `type(scope): description` - [ ] The PR follows the [contribution guidelines](https://github.com/ZcashFoundation/zebra/blob/main/CONTRIBUTING.md). - [ ] This change was discussed in an issue or with the team beforehand. - [ ] The solution is tested. diff --git a/.github/scripts/validate-checkpoints.sh b/.github/scripts/validate-checkpoints.sh new file mode 100755 index 00000000000..17a5b6bd4bb --- /dev/null +++ b/.github/scripts/validate-checkpoints.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# validate-checkpoints.sh +# +# Validates checkpoint file format and structure in a single pass. +# +# Usage: .github/scripts/validate-checkpoints.sh +# +# Exit codes: +# 0 - valid +# 1 - validation error (details printed to stderr) + +set -euo pipefail + +FILE="${1:?Usage: validate-checkpoints.sh }" + +if [ ! -f "$FILE" ]; then + echo "ERROR: File not found: $FILE" >&2 + exit 1 +fi + +# Single-pass validation using awk +awk ' + !/^[0-9]+ [0-9a-f]{64}$/ { + printf "ERROR: line %d: invalid format: %s\n", NR, $0 > "/dev/stderr" + errs++ + next + } + NR == 1 && $1 != "0" { + printf "ERROR: file must start at height 0, found %s\n", $1 > "/dev/stderr" + errs++ + } + NR > 1 && $1 <= prev_h { + printf "ERROR: line %d: height %d is not greater than previous %d\n", NR, $1, prev_h > "/dev/stderr" + errs++ + } + NR > 1 && ($1 - prev_h) > 400 { + printf "ERROR: line %d: gap of %d blocks between %d and %d\n", NR, $1 - prev_h, prev_h, $1 > "/dev/stderr" + errs++ + } + $1 in seen_h { + printf "ERROR: line %d: duplicate height %d\n", NR, $1 > "/dev/stderr" + errs++ + } + $2 in seen_hash { + printf "ERROR: line %d: duplicate hash %s\n", NR, $2 > "/dev/stderr" + errs++ + } + { + seen_h[$1] = 1 + seen_hash[$2] = 1 + prev_h = $1 + last_h = $1 + total++ + } + END { + printf "Validated %d entries, last height: %d\n", total, last_h + if (errs > 0) { + printf "FAILED: %d validation errors\n", errs > "/dev/stderr" + exit 1 + } + print "OK: All entries valid" + } +' "$FILE" diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 184792c0d4f..06e205a2f4d 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -161,6 +161,8 @@ _The diagram above illustrates the parallel execution patterns in our CI/CD syst - **Delete GCP resources** (`zfnd-delete-gcp-resources.yml`): Cleanup utilities - Helper scripts in `.github/workflows/scripts/` used by the above +Required-check workflows follow a `changes` (paths-filter) + gated workers + aggregator pattern. File-to-workflow mapping lives in [`.github/path-filters.yml`](../path-filters.yml). The aggregator job ID matches the workflow basename and the GitHub ruleset context (`lint`, `unit-tests`, `test-crates`, ...). See [`book/src/dev/continuous-integration.md`](../../book/src/dev/continuous-integration.md). + ## Test Execution Strategy ### Test Orchestration with Nextest diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000000..25513904d8e --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,232 @@ +name: Benchmarks + +on: + workflow_dispatch: + pull_request: + types: [labeled, synchronize] + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +# Bench list shared by both jobs. New benches need a `[[bench]]` stanza +# with `harness = false` in the crate's `Cargo.toml`. +env: + BENCHES: | + criterion -p zebra-consensus --bench groth16 + criterion -p zebra-consensus --bench halo2 + criterion -p zebra-consensus --bench sapling + criterion -p zebra-chain --bench transaction + criterion -p zebra-chain --bench block --features bench + criterion -p zebra-chain --bench redpallas + +jobs: + # Runs all benchmarks and publishes results to gh-pages for historical + # tracking. Triggered manually via workflow_dispatch. + benchmark: + name: Run Benchmarks + if: github.event_name == 'workflow_dispatch' + permissions: + contents: write + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + persist-credentials: false + + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 + with: + toolchain: stable + cache-on-failure: true + + - name: Install cargo-criterion + run: cargo install cargo-criterion --locked + + - name: Run benchmarks + run: | + : > bench_output.json + while IFS= read -r bench; do + [ -z "$bench" ] && continue + echo "::group::cargo $bench" + # shellcheck disable=SC2086 + cargo $bench --message-format=json >> bench_output.json + echo "::endgroup::" + done <<< "$BENCHES" + + - name: Convert criterion JSON to github-action-benchmark format + run: | + jq -s '[.[] | select(.reason == "benchmark-complete") | { + name: .id, + unit: .typical.unit, + value: .typical.estimate, + range: "\(.typical.lower_bound as $lo | .typical.upper_bound as $hi | (($hi - $lo) / 2 | . * 100 | round / 100)) \(.typical.unit)" + }]' bench_output.json > bench_results.json + + - name: Generate summary + run: | + { + echo "## Benchmark Results" + echo "" + echo "| Benchmark | Time | ±CI |" + echo "|-----------|------|-----|" + jq -r 'select(.reason == "benchmark-complete") | + "| \(.id) | \(.typical.estimate | . / 1e3 | if . >= 1000 then "\(. / 1000 | . * 100 | round / 100) ms" elif . >= 1 then "\(. * 100 | round / 100) µs" else "\(. * 1000 | . * 100 | round / 100) ns" end) | ±\(.typical.lower_bound as $lo | .typical.upper_bound as $hi | (($hi - $lo) / 2 / .typical.estimate * 10000 | round / 100))% |"' \ + bench_output.json + echo "" + echo "**Total benchmarks:** $(grep -c '"benchmark-complete"' bench_output.json)" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Store benchmark results + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 + with: + tool: customSmallerIsBetter + output-file-path: bench_results.json + gh-pages-branch: gh-pages + benchmark-data-dir-path: dev/bench + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: true + # 150% = 1.5x slower than the previous run. Stays above ~10-20% + # runner noise while catching real regressions. + alert-threshold: "150%" + comment-on-alert: true + fail-on-alert: false + + # Compares benchmarks between the PR branch and the base branch. + # Runs when the `C-benchmark` label is present: both when it is first added + # and on subsequent pushes to the PR. + compare: + name: Compare Benchmarks + if: >- + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'C-benchmark') + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 + with: + toolchain: stable + cache-on-failure: true + + - name: Install critcmp + run: cargo install critcmp --locked + + - name: Benchmark base branch + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + git checkout "$BASE_SHA" + failures=() + while IFS= read -r bench; do + [ -z "$bench" ] && continue + # Strip the `criterion ` prefix so we can run via `cargo bench`. + cmd="${bench#criterion }" + echo "::group::base: cargo bench $cmd" + # shellcheck disable=SC2086 + if ! cargo bench $cmd -- --save-baseline base; then + failures+=("$cmd") + fi + echo "::endgroup::" + done <<< "$BENCHES" + if [ "${#failures[@]}" -gt 0 ]; then + { + echo "### Base-branch benchmark failures" + echo "" + echo "The following benches failed on the base branch (likely because they are new in this PR):" + echo "" + for f in "${failures[@]}"; do + echo "- \`cargo bench $f\`" + done + echo "" + echo "critcmp will show no comparison for these." + } >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Benchmark PR branch + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git checkout "$HEAD_SHA" + while IFS= read -r bench; do + [ -z "$bench" ] && continue + cmd="${bench#criterion }" + echo "::group::pr: cargo bench $cmd" + # shellcheck disable=SC2086 + cargo bench $cmd -- --save-baseline pr + echo "::endgroup::" + done <<< "$BENCHES" + + - name: Compare results + id: compare + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + # Branch names are attacker-controlled. Strip anything that isn't a + # conventional ref character before rendering them inside markdown. + sanitize() { printf '%s' "$1" | tr -cd 'A-Za-z0-9._/-'; } + base_ref=$(sanitize "$BASE_REF") + head_ref=$(sanitize "$HEAD_REF") + + { + echo "## Benchmark Comparison: base vs PR" + echo "" + echo '```' + critcmp base pr + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + { + echo 'COMMENT<> "$GITHUB_OUTPUT" + + - name: Post PR comment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 + env: + COMMENT: ${{ steps.compare.outputs.COMMENT }} + with: + script: | + const body = process.env.COMMENT; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => + c.user.type === 'Bot' && c.body.startsWith('## Benchmark Comparison') + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index f8bd76f05f4..d17c43d614c 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -3,15 +3,6 @@ name: Book on: push: branches: [main] - paths: - - book/** - - katex-header.html - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - .cargo/config.toml - - "**/clippy.toml" - - .github/workflows/book.yml # Ensures that only one workflow task will run at a time. Previous builds, if # already in process, will get cancelled. Only the latest commit will be allowed @@ -25,9 +16,28 @@ permissions: pull-requests: read jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + book: ${{ steps.filter.outputs.book || 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' + with: + filters: .github/path-filters.yml + build: name: Build Docs + needs: changes + if: needs.changes.outputs.book == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest @@ -44,7 +54,7 @@ jobs: mdbook-version: ~0.5 use-linkcheck: true use-mermaid: true - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: nightly cache-on-failure: true @@ -70,11 +80,31 @@ jobs: # https://github.com/ZcashFoundation/zebra/blob/main/.cargo/config.toml#L87 RUSTDOCFLAGS: --html-in-header katex-header.html -D warnings -A rustdoc::private_intra_doc_links --cfg docsrs --show-type-layout --generate-link-to-definition --enable-index-page -Zunstable-options + - name: Include benchmark dashboard + # Snapshots `gh-pages/dev/bench` (published by `benchmarks.yml`) into + # the docs artifact so it is reachable at `/dev/bench`. + run: | + if ! git fetch origin gh-pages --depth=1 2>/dev/null; then + echo "gh-pages branch does not exist yet, skipping benchmark dashboard" + exit 0 + fi + + if ! git show origin/gh-pages:dev/bench/index.html >/dev/null 2>&1; then + echo "gh-pages has no benchmark data yet, skipping benchmark dashboard" + exit 0 + fi + + # index.html and data.js are published atomically by + # github-action-benchmark, so data.js isn't guarded separately. + mkdir -p target/docs/dev/bench + git show origin/gh-pages:dev/bench/index.html > target/docs/dev/bench/index.html + git show origin/gh-pages:dev/bench/data.js > target/docs/dev/bench/data.js + - name: Setup Pages - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b #v5.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d #v6.0.0 - name: Upload site - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b #v4.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 #v5.0.0 with: path: ./target/docs @@ -83,11 +113,15 @@ jobs: # Only deploy if a push to main # TODO: Uncomment this before merging # if: github.ref_name == 'main' && github.event_name == 'push' + needs: [changes, build] + if: needs.changes.outputs.book == 'true' runs-on: ubuntu-latest - needs: [build] timeout-minutes: 30 - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment. + # `contents: read` is intentionally omitted: this job publishes rendered + # source to a public site, so missing checkout permission keeps it inert + # on private forks as a fail-closed safeguard. permissions: pages: write id-token: write @@ -99,13 +133,13 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e #v4.0.5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 #v5.0.0 - book-success: - name: book success + book: runs-on: ubuntu-latest if: always() needs: + - changes - build - deploy timeout-minutes: 30 @@ -114,3 +148,4 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} + allowed-skips: build, deploy diff --git a/.github/workflows/checkpoint-update.yml b/.github/workflows/checkpoint-update.yml new file mode 100644 index 00000000000..61489e26ed3 --- /dev/null +++ b/.github/workflows/checkpoint-update.yml @@ -0,0 +1,234 @@ +# Automated checkpoint and end-of-support height updates. +# +# Triggered when the weekly integration tests complete successfully. +# Downloads checkpoint artifacts produced by generate-checkpoints-* jobs, +# appends new entries to the checkpoint files, updates the end-of-support +# height, validates everything, and opens a PR. +# +# The PR requires human review before merge; checkpoints are consensus-critical. +name: Checkpoint Update + +on: + # zizmor: ignore[dangerous-triggers] -- triggers only on internal CI workflow completion, + # processes deterministic checkpoint data from artifacts, no untrusted user input + workflow_run: + workflows: ["Integration Tests on GCP"] + types: [completed] + branches: [main] + + # Manual trigger for testing; resolves the latest successful integration test run automatically + workflow_dispatch: + +permissions: {} + +jobs: + update-checkpoints: + name: Update checkpoint files + runs-on: ubuntu-latest + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + permissions: + actions: read + contents: write + pull-requests: write + env: + MAINNET_CHECKPOINTS: zebra-chain/src/parameters/checkpoint/main-checkpoints.txt + TESTNET_CHECKPOINTS: zebra-chain/src/parameters/checkpoint/test-checkpoints.txt + EOS_FILE: zebrad/src/components/sync/end_of_support.rs + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + persist-credentials: true + ref: main + + # Resolve the integration test run ID. + # For workflow_run: use the triggering run directly. + # For workflow_dispatch: find the latest successful run via the API. + - name: Resolve integration test run ID + id: resolve-run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + WF_RUN_ID: ${{ github.event.workflow_run.id }} + WF_RUN_URL: ${{ github.event.workflow_run.html_url }} + REPO: ${{ github.repository }} + run: | + if [ "$EVENT_NAME" = "workflow_run" ]; then + RUN_ID="$WF_RUN_ID" + RUN_URL="$WF_RUN_URL" + else + RUN_ID=$(gh run list \ + --workflow "Integration Tests on GCP" \ + --branch main \ + --status success \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId') + RUN_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}" + fi + + if [ -z "$RUN_ID" ]; then + echo "No successful integration test run found" + exit 1 + fi + + echo "run_id=${RUN_ID}" >> "$GITHUB_OUTPUT" + echo "run_url=${RUN_URL}" >> "$GITHUB_OUTPUT" + echo "Using integration test run: ${RUN_URL}" + + # Download checkpoint artifacts from the integration test run. + - name: Download mainnet checkpoint artifact + id: mainnet-artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 + with: + name: generate-checkpoints-mainnet-checkpoints + run-id: ${{ steps.resolve-run.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Download testnet checkpoint artifact + id: testnet-artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 + with: + name: generate-checkpoints-testnet-checkpoints + run-id: ${{ steps.resolve-run.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + # The generate-checkpoints-* jobs are skipped when no tip disk exists + # (e.g., full sync still in progress) or when the workflow runs in + # regenerate/sync-only mode. When skipped, no artifact is produced. + - name: Check if any artifacts were downloaded + id: check-artifacts + run: | + HAS_MAINNET="false" + HAS_TESTNET="false" + + # Artifact files use the same names as the repo files (new entries only) + if [ -f "main-checkpoints.txt" ]; then + LINES=$(wc -l < main-checkpoints.txt | tr -d ' ') + echo "Mainnet artifact: ${LINES} checkpoint lines" + HAS_MAINNET="true" + fi + + if [ -f "test-checkpoints.txt" ]; then + LINES=$(wc -l < test-checkpoints.txt | tr -d ' ') + echo "Testnet artifact: ${LINES} checkpoint lines" + HAS_TESTNET="true" + fi + + if [ "$HAS_MAINNET" = "false" ] && [ "$HAS_TESTNET" = "false" ]; then + echo "No checkpoint artifacts found, skipping update" + echo "has_updates=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "has_mainnet=${HAS_MAINNET}" >> "$GITHUB_OUTPUT" + echo "has_testnet=${HAS_TESTNET}" >> "$GITHUB_OUTPUT" + echo "has_updates=true" >> "$GITHUB_OUTPUT" + + # Append new mainnet checkpoints (entries with heights higher than current last) + - name: Append new mainnet checkpoints + if: steps.check-artifacts.outputs.has_mainnet == 'true' + run: | + CURRENT_LAST=$(tail -1 "${MAINNET_CHECKPOINTS}" | awk '{print $1}') + echo "Current last mainnet checkpoint: ${CURRENT_LAST}" + + # Extract only new entries (height > current last) + NEW_COUNT=$(awk -v last="$CURRENT_LAST" '$1 > last' main-checkpoints.txt | wc -l | tr -d ' ') + echo "New mainnet checkpoints to append: ${NEW_COUNT}" + + if [ "$NEW_COUNT" -gt 0 ]; then + awk -v last="$CURRENT_LAST" '$1 > last' main-checkpoints.txt >> "${MAINNET_CHECKPOINTS}" + NEW_LAST=$(tail -1 "${MAINNET_CHECKPOINTS}" | awk '{print $1}') + echo "Updated last mainnet checkpoint: ${NEW_LAST}" + fi + + # Append new testnet checkpoints + - name: Append new testnet checkpoints + if: steps.check-artifacts.outputs.has_testnet == 'true' + run: | + CURRENT_LAST=$(tail -1 "${TESTNET_CHECKPOINTS}" | awk '{print $1}') + echo "Current last testnet checkpoint: ${CURRENT_LAST}" + + NEW_COUNT=$(awk -v last="$CURRENT_LAST" '$1 > last' test-checkpoints.txt | wc -l | tr -d ' ') + echo "New testnet checkpoints to append: ${NEW_COUNT}" + + if [ "$NEW_COUNT" -gt 0 ]; then + awk -v last="$CURRENT_LAST" '$1 > last' test-checkpoints.txt >> "${TESTNET_CHECKPOINTS}" + NEW_LAST=$(tail -1 "${TESTNET_CHECKPOINTS}" | awk '{print $1}') + echo "Updated last testnet checkpoint: ${NEW_LAST}" + fi + + # Update the end-of-support estimated release height using the latest + # mainnet checkpoint height. This is a lower bound (~3-7 days behind + # the real tip), but the EOS window is 105 days, making it negligible. + - name: Update end-of-support height + if: steps.check-artifacts.outputs.has_mainnet == 'true' + run: | + LAST_HEIGHT=$(tail -1 "${MAINNET_CHECKPOINTS}" | awk '{print $1}') + # Format with underscores for Rust readability (e.g., 3_282_406) + FORMATTED=$(echo "$LAST_HEIGHT" | awk '{n=$0; r=""; for(i=length(n);i>0;i--) { r=substr(n,i,1) r; if((length(n)-i)%3==2 && i>1) r="_" r }; print r}') + echo "Setting ESTIMATED_RELEASE_HEIGHT to ${FORMATTED} (height ${LAST_HEIGHT})" + + sed -i "s/ESTIMATED_RELEASE_HEIGHT: u32 = [0-9_]*/ESTIMATED_RELEASE_HEIGHT: u32 = ${FORMATTED}/" \ + "${EOS_FILE}" + + grep "ESTIMATED_RELEASE_HEIGHT" "${EOS_FILE}" | head -1 + + # Validate the updated checkpoint files + - name: Validate checkpoint files + if: steps.check-artifacts.outputs.has_updates == 'true' + run: | + .github/scripts/validate-checkpoints.sh "${MAINNET_CHECKPOINTS}" + .github/scripts/validate-checkpoints.sh "${TESTNET_CHECKPOINTS}" + + # Check if there are actual changes to commit + - name: Check for changes + id: changes + if: steps.check-artifacts.outputs.has_updates == 'true' + run: | + if git diff --quiet; then + echo "No changes to commit" + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "Changes detected:" + git diff --stat + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + + # Open or update a PR with the checkpoint changes + - name: Create checkpoint update PR + if: steps.changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 #v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: chore/update-checkpoints + title: "chore(chain): update checkpoints and end-of-support height" + body: | + Automated checkpoint update from the weekly integration test run. + + **Source:** [Integration test run #${{ steps.resolve-run.outputs.run_id }}](${{ steps.resolve-run.outputs.run_url }}) + + ### Changes + + - Updated mainnet and/or testnet checkpoint files with new entries + - Updated `ESTIMATED_RELEASE_HEIGHT` in `end_of_support.rs` to match the latest mainnet checkpoint + + ### Validation + + The checkpoint validation script verified: + - All entries match `HEIGHT HASH` format + - Heights are monotonically increasing + - No gaps exceed 400 blocks + - No duplicate heights or hashes + + ### Review + + Checkpoints are consensus-critical; incorrect hashes could cause nodes + to follow a wrong fork. Verify the source integration test run linked + above completed successfully against the real Zcash network. + labels: A-release,C-enhancement + commit-message: "chore(chain): update checkpoints and end-of-support height" + delete-branch: true diff --git a/.github/workflows/ci-basic.yml b/.github/workflows/ci-basic.yml index 42cdd2f8a86..0557db0aeab 100644 --- a/.github/workflows/ci-basic.yml +++ b/.github/workflows/ci-basic.yml @@ -5,11 +5,19 @@ on: [push] permissions: contents: read +# Only keep the newest run per branch: older runs test superseded commits. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest strategy: + # Run both matrix legs to completion: a failure in one should not cancel + # the other, since they exercise different feature configurations. + fail-fast: false matrix: nu7: - true @@ -53,6 +61,13 @@ jobs: run: | sed -i 's|.*"--cfg", .feature="tx_v6".*|# &|' .cargo/config.toml sed -i 's|.*"--cfg", "zcash_unstable=\\"nu7\\"".*|# &|' .cargo/config.toml + # Fail loudly if the patterns stop matching: a silent no-op would make + # this leg build with the ZSA flags still on, so both matrix legs + # would test the same configuration without anyone noticing. + if git diff --quiet .cargo/config.toml; then + echo "::error::nu7/tx_v6 flags not found in .cargo/config.toml" + exit 1 + fi - name: Install cargo-nextest uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b # v2.68.10 @@ -60,13 +75,27 @@ jobs: tool: cargo-nextest - name: Run tests + env: + # Selects the CI test set from .config/nextest.toml. Without it, + # nextest falls back to [profile.default] and also runs the stateful + # and network-dependent tests, which belong to the integration + # workflows. + NEXTEST_PROFILE: all-tests run: | - timeout --preserve-status 1h \ - cargo nextest run --workspace --locked + # Backstop only: nextest enforces its own per-test timeouts. This + # budget also covers compilation, which takes several minutes. + # -k sends SIGKILL if the process ignores the initial SIGTERM. + # + # The acceptance tests are excluded until Ironwood support is + # merged: Mainnet and Testnet have activated NU6.3 (heights + # 3_428_143 / 4_134_000), and this v5.2.0-based branch cannot + # verify blocks past those heights. + timeout -k 60s --preserve-status 90m \ + cargo nextest run --workspace --locked -E 'not binary(=acceptance)' - name: Run doc tests run: | - timeout --preserve-status 30m \ + timeout -k 60s --preserve-status 30m \ cargo test --doc --workspace --locked - name: Run doc check @@ -78,13 +107,20 @@ jobs: - name: Run clippy run: cargo clippy --workspace --all-targets --features "default-release-binaries proptest-impl lightwalletd-grpc-tests zebra-checkpoints" + # Runs even when an earlier step failed, so the checkout is left in the + # state the next job on this machine expects. - name: Restore cargo config + if: ${{ !cancelled() }} run: git checkout -- .cargo/config.toml - name: Verify working directory is clean run: git diff --exit-code + # Runs even when an earlier step failed: disk and memory numbers are most + # useful when diagnosing a failed run, and a step with no `if` defaults to + # running only on success. - name: Show system resource summary + if: always() run: | df -h free -h diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d8dbca01881..ba56cde1f00 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,13 +3,6 @@ name: Coverage on: push: branches: [main] - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - .config/nextest.toml - - codecov.yml - - .github/workflows/coverage.yml # Ensures that only one workflow task will run at a time. Previous builds, if # already in process, will get cancelled. Only the latest commit will be allowed @@ -30,7 +23,28 @@ env: COLORBT_SHOW_HIDDEN: 1 jobs: - coverage: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + coverage: ${{ steps.filter.outputs.coverage || 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' + with: + filters: .github/path-filters.yml + + run-coverage: + needs: changes + if: needs.changes.outputs.coverage == 'true' + # `contents: read` is intentionally omitted: this job uploads source to a + # public Codecov project, so missing checkout permission keeps it inert + # on private forks as a fail-closed safeguard. permissions: id-token: write statuses: write @@ -41,12 +55,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: stable components: llvm-tools-preview cache-on-failure: true - - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 + - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 with: tool: cargo-llvm-cov,nextest - uses: ./.github/actions/setup-zebra-build @@ -64,18 +78,19 @@ jobs: PROPTEST_MAX_SHRINK_ITERS: 0 - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de #v5.5.2 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 #v6.0.1 with: files: lcov.info - coverage-success: - name: coverage success + coverage: runs-on: ubuntu-latest if: always() needs: - - coverage + - changes + - run-coverage steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} + allowed-skips: run-coverage diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index 9b7667be7ce..7c040fb41ab 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -3,22 +3,8 @@ name: Docs Check on: push: branches: [main] - paths: - - "**.md" - - "book/**" - - ".lychee.toml" - - ".codespellrc" - - ".trunk/configs/.markdownlint.yaml" - - ".github/workflows/docs-check.yml" pull_request: - branches: [main] - paths: - - "**.md" - - "book/**" - - ".lychee.toml" - - ".codespellrc" - - ".trunk/configs/.markdownlint.yaml" - - ".github/workflows/docs-check.yml" + merge_group: # Ensures that only one workflow task will run at a time. Previous builds, if # already in process, will get cancelled. Only the latest commit will be allowed @@ -32,47 +18,66 @@ permissions: pull-requests: read jobs: - link-check: - name: link-check - permissions: - contents: read - pull-requests: read + changes: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 5 + outputs: + docs_check: ${{ steps.filter.outputs.docs_check || 'true' }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' with: persist-credentials: false - - - name: Restore lychee cache - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb #v5.0.1 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' with: - path: .lycheecache - key: cache-lychee-${{ github.sha }} - restore-keys: cache-lychee- + filters: .github/path-filters.yml - - name: Link Checker - uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 #v2.7.0 - with: - args: >- - --verbose - --no-progress - --cache - --max-cache-age 1d - --config .lychee.toml - "**/*.md" - fail: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # TODO: this always ends up failing due to random issues. Possibly move to a weekly job + # link-check: + # name: link-check + # permissions: + # contents: read + # pull-requests: read + # runs-on: ubuntu-latest + # timeout-minutes: 15 + # steps: + # - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + # with: + # persist-credentials: false + + # - name: Restore lychee cache + # uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 + # with: + # path: .lycheecache + # key: cache-lychee-${{ github.sha }} + # restore-keys: cache-lychee- + + # - name: Link Checker + # uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 #v2.8.0 + # with: + # args: >- + # --verbose + # --no-progress + # --cache + # --max-cache-age 1d + # --config .lychee.toml + # "**/*.md" + # fail: true + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} spell-check: name: spell-check + needs: changes + if: needs.changes.outputs.docs_check == 'true' permissions: contents: read runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false @@ -84,17 +89,19 @@ jobs: markdown-lint: name: markdown-lint + needs: changes + if: needs.changes.outputs.docs_check == 'true' permissions: contents: read runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e #v6.4.0 with: node-version: "22" @@ -109,12 +116,12 @@ jobs: --ignore .git \ --config .trunk/configs/.markdownlint.yaml - docs-check-success: - name: docs check success + docs-check: runs-on: ubuntu-latest if: always() needs: - - link-check + - changes + # - link-check - spell-check - markdown-lint timeout-minutes: 5 @@ -123,3 +130,4 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} + allowed-skips: spell-check, markdown-lint diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 19a7511c40c..cc32f60cb08 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,23 +5,9 @@ on: branches: - main - zsa-support - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - clippy.toml - - .cargo/config.toml - - .github/workflows/lint.yml - push: branches: [main] - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - clippy.toml - - .cargo/config.toml - - .github/workflows/lint.yml + merge_group: # Ensures that only one workflow task will run at a time. Previous builds, if # already in process, will get cancelled. Only the latest commit will be allowed @@ -38,9 +24,28 @@ env: CLICOLOR: 1 jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + lint: ${{ steps.filter.outputs.lint || 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' + with: + filters: .github/path-filters.yml + clippy: name: clippy ${{ matrix.rust-version }} / ${{ matrix.type }} + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest @@ -63,7 +68,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: components: clippy toolchain: ${{ matrix.rust-version }} @@ -73,7 +78,10 @@ jobs: run: cargo clippy ${{ matrix.args }} --features "${{ matrix.features }}" crate-checks: + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read statuses: write runs-on: ubuntu-latest timeout-minutes: 30 @@ -81,10 +89,10 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: cache-on-failure: true - - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 + - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 with: tool: cargo-hack - uses: ./.github/actions/setup-zebra-build @@ -92,7 +100,10 @@ jobs: msrv: name: MSRV + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest @@ -105,16 +116,19 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: - toolchain: 1.89 # MSRV + toolchain: 1.91 # MSRV cache-on-failure: true - uses: ./.github/actions/setup-zebra-build - run: cargo build --bin "${{ matrix.binary }}" --workspace docs: name: docs + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read statuses: write runs-on: ubuntu-latest timeout-minutes: 30 @@ -122,7 +136,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: nightly cache-on-failure: true @@ -136,7 +150,10 @@ jobs: fmt: name: fmt + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read statuses: write runs-on: ubuntu-latest timeout-minutes: 30 @@ -144,7 +161,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: nightly components: rustfmt @@ -153,7 +170,10 @@ jobs: unused-deps: name: unused-deps + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest @@ -162,12 +182,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: # TODO: QED-it temp nightly pin: matches Zebra v4.2.0 CI test period; remove before upstream merge. toolchain: nightly-2026-03-06 cache-on-failure: true - - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 + - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 with: # TODO: QED-it temp cargo-udeps 0.1.60 pin: stable for Zebra v4.2.0 CI; remove before upstream merge. tool: cargo-udeps@0.1.60 @@ -175,6 +195,8 @@ jobs: - run: cargo udeps --workspace --all-targets --all-features --locked no-test-deps: + needs: changes + if: needs.changes.outputs.lint == 'true' runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -182,7 +204,7 @@ jobs: with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: stable cache-on-failure: true @@ -192,7 +214,10 @@ jobs: # Checks that selected rates can compile with power set of features features: name: features + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest @@ -201,12 +226,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: nightly cache-on-failure: true - name: cargo install cargo-hack - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 + uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 with: tool: cargo-hack - uses: ./.github/actions/setup-zebra-build @@ -216,7 +241,10 @@ jobs: check-cargo-lock: name: check-cargo-lock + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest @@ -224,7 +252,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: stable cache-on-failure: true @@ -233,7 +261,10 @@ jobs: deny: name: Check deny ${{ matrix.checks }} ${{ matrix.features }} + needs: changes + if: needs.changes.outputs.lint == 'true' permissions: + contents: read statuses: write runs-on: ubuntu-latest strategy: @@ -241,9 +272,13 @@ jobs: checks: - bans - sources + - advisories + - licenses # We don't need to check `--no-default-features` here, because (except in very rare cases): # - disabling features isn't going to add duplicate dependencies # - disabling features isn't going to add more crate sources + # For advisories and licenses, feature flags don't change the dependency tree materially, + # so we only need the default and --all-features variants. features: ["", --features default-release-binaries, --all-features] # Always run the --all-features job, to get accurate "skip tree root was not found" warnings fail-fast: false @@ -255,11 +290,11 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: cache-on-failure: true - name: Check ${{ matrix.checks }} with features ${{ matrix.features }} - uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 #v2.0.15 + uses: EmbarkStudios/cargo-deny-action@6c8f9facfa5047ec02d8485b6bf52b587b7777d1 #v2.0.18 with: # --all-features spuriously activates openssl, but we want to ban that dependency in # all of zebrad's production features for security reasons. But the --all-features job is @@ -268,13 +303,37 @@ jobs: command: check ${{ matrix.checks }} ${{ matrix.features == '--all-features' && '--allow banned' || '--allow unmatched-skip-root' }} arguments: --workspace ${{ matrix.features }} - lint-success: - name: lint success + vet: + name: cargo-vet check + needs: changes + if: needs.changes.outputs.lint == 'true' + permissions: + contents: read + statuses: write + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + persist-credentials: false + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 + with: + cache-on-failure: true + - name: Install cargo-vet + uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 + with: + tool: cargo-vet + - name: Run cargo-vet check + run: cargo vet check + + lint: runs-on: ubuntu-latest if: always() needs: + - changes - clippy - crate-checks + - msrv - docs - fmt - unused-deps @@ -282,9 +341,11 @@ jobs: - no-test-deps - features - deny + - vet timeout-minutes: 30 steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} + allowed-skips: clippy, crate-checks, msrv, docs, fmt, unused-deps, check-cargo-lock, no-test-deps, features, deny, vet diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 00000000000..0006237e250 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,106 @@ +# Validates that PR titles follow the conventional commits specification. +# https://www.conventionalcommits.org/en/v1.0.0/ +# +# Since PRs are squash-merged, the PR title becomes the commit message on main. +# This is the foundation for automated changelog generation and version bumping. +# +# Uses pull_request_target to post sticky comments on invalid titles (requires +# pull-requests: write). No code checkout or secret access — only reads the +# PR title via the API and posts/deletes a comment. +name: PR Title Check + +on: + # zizmor: ignore[dangerous-triggers] -- no checkout, no secrets, only API reads + PR comment write + pull_request_target: + types: [opened, edited, synchronize, reopened] + +permissions: {} + +jobs: + validate: + name: Validate PR title + runs-on: ubuntu-latest + # No checkout; runs on pull_request_target. Keep token scope minimal. + permissions: + pull-requests: write + steps: + - name: Check PR title + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 #v6.1.1 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + refactor + build + chore + docs + test + ci + style + revert + release + scopes: | + zebra-chain + zebra-consensus + zebra-network + zebra-state + zebra-rpc + zebra-script + zebra-node-services + zebra-test + zebra-utils + zebrad + tower-batch-control + tower-fallback + chain + consensus + network + state + rpc + script + deps + ci + release + requireScope: false + subjectPattern: ^.+$ + subjectPatternError: | + PR title must follow conventional commits: + + (): + + Examples: + fix(zebra-rpc): correct connection timeout + feat(zebra-chain): add signature verification + docs: update installation guide + build(deps): bump serde to 1.0.220 + + See: https://www.conventionalcommits.org/ + Docs: https://github.com/ZcashFoundation/zebra/blob/main/CONTRIBUTING.md#pull-requests + + - name: Comment on invalid PR title + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 #v3.0.4 + # null comparison is intentional: the action only sets error_message on failure. + # When validation passes the output key is absent (null), not empty string. + # See: https://github.com/amannn/action-semantic-pull-request#outputs + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/) format. Your title needs a small adjustment. + + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + See the [contribution guide](https://github.com/ZcashFoundation/zebra/blob/main/CONTRIBUTING.md#pull-requests) for details. + + - name: Remove comment on valid PR title + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 #v3.0.4 + if: ${{ steps.lint_pr_title.outputs.error_message == null }} + with: + header: pr-title-lint-error + delete: true diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 650d9183bef..d1b642c1495 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -84,6 +84,7 @@ jobs: if: failure() || cancelled() runs-on: ubuntu-latest permissions: + contents: read issues: write steps: - uses: jayqi/failed-build-issue-action@1a893bbf43ef1c2a8705e2b115cd4f0fe3c5649b #v1.2.0 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 4cacb79e9bf..83f4015d5d7 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes - - uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0 + - uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0 with: config-name: release-drafter.yml commitish: main diff --git a/.github/workflows/status-checks.patch.yml b/.github/workflows/status-checks.patch.yml deleted file mode 100644 index d237c6414d5..00000000000 --- a/.github/workflows/status-checks.patch.yml +++ /dev/null @@ -1,43 +0,0 @@ -# This workflow ensures required status checks pass when main workflows are skipped. -# !CRITICAL: Uses paths-ignore to ensure mutual exclusion with main workflows. -# Only runs when NO Rust/config files are modified. -# See: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks - -name: Status Check Patch - -on: - pull_request: - branches: [main] - paths-ignore: - # This MUST be an exact inverse of paths in lint.yml, tests-unit.yml, and test-crates.yml - # to ensure mutual exclusion - only one set of workflows runs - - '**/*.rs' - - '**/Cargo.toml' - - '**/Cargo.lock' - - .cargo/config.toml - - '**/clippy.toml' - - .github/workflows/lint.yml - - .github/workflows/tests-unit.yml - - .github/workflows/test-crates.yml - -permissions: - contents: read - -jobs: - lint-success: - name: lint success - runs-on: ubuntu-latest - steps: - - run: echo "No lint needed - no Rust files modified" - - test-success: - name: test success - runs-on: ubuntu-latest - steps: - - run: echo "No tests needed - no Rust files modified" - - test-crate-build-success: - name: test crate build success - runs-on: ubuntu-latest - steps: - - run: echo "No crate build needed - no Rust files modified" \ No newline at end of file diff --git a/.github/workflows/test-crates.yml b/.github/workflows/test-crates.yml index 4d481f50710..c6e829991b1 100644 --- a/.github/workflows/test-crates.yml +++ b/.github/workflows/test-crates.yml @@ -5,24 +5,9 @@ on: branches: - main - zsa-support - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - .cargo/config.toml - - "**/clippy.toml" - - .github/workflows/test-crates.yml - push: branches: [main] - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - .cargo/config.toml - - "**/clippy.toml" - - .github/workflows/test-crates.yml - + merge_group: workflow_dispatch: # Ensures that only one workflow task will run at a time. Previous builds, if @@ -44,9 +29,28 @@ env: COLORBT_SHOW_HIDDEN: 1 jobs: - matrix: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + test_crates: ${{ steps.filter.outputs.test_crates || 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' + with: + filters: .github/path-filters.yml + + crates-matrix: name: Generate crates matrix + needs: changes + if: needs.changes.outputs.test_crates == 'true' permissions: + contents: read statuses: write runs-on: ubuntu-latest timeout-minutes: 30 @@ -57,7 +61,7 @@ jobs: with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: # TODO: QED-it temp Rust 1.94 pin: stable for Zebra v4.2.0 CI; remove before upstream merge. toolchain: "1.94" @@ -74,7 +78,7 @@ jobs: # # A JSON object is created and assigned to a $MATRIX variable, which is used to create an # output named `matrix`, which is then used as the input in following steps, - # using ` ${{ fromJson(needs.matrix.outputs.matrix) }}` + # using ` ${{ fromJson(needs.crates-matrix.outputs.matrix) }}` - id: set-matrix name: Dynamically build crates JSON run: | @@ -91,11 +95,13 @@ jobs: build: name: Build ${{ matrix.crate }} crate + needs: [changes, crates-matrix] + if: needs.changes.outputs.test_crates == 'true' permissions: + contents: read id-token: write statuses: write timeout-minutes: 90 - needs: [matrix] # Some of these builds take more than 14GB disk space runs-on: ubuntu-latest strategy: @@ -103,14 +109,14 @@ jobs: # but still finish in a similar time to the longest tests max-parallel: 4 fail-fast: true - matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + matrix: ${{ fromJson(needs.crates-matrix.outputs.matrix) }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: # TODO: QED-it temp Rust 1.94 pin: stable for Zebra v4.2.0 CI; remove before upstream merge. toolchain: "1.94" @@ -147,18 +153,20 @@ jobs: build-msrv: name: Build ${{ matrix.crate }} crate with MSRV + needs: [changes, crates-matrix] + if: needs.changes.outputs.test_crates == 'true' permissions: + contents: read id-token: write statuses: write timeout-minutes: 90 - needs: [matrix] runs-on: ubuntu-latest strategy: # avoid rate-limit errors by only launching a few of these jobs at a time, # but still finish in a similar time to the longest tests max-parallel: 4 fail-fast: true - matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + matrix: ${{ fromJson(needs.crates-matrix.outputs.matrix) }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 @@ -172,7 +180,7 @@ jobs: echo "version=$MSRV" >> "$GITHUB_OUTPUT" echo "Extracted MSRV: $MSRV" - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: ${{ steps.msrv.outputs.version }} components: clippy @@ -188,12 +196,13 @@ jobs: cargo clippy --package ${{ matrix.crate }} --all-features --all-targets -- -D warnings cargo build --package ${{ matrix.crate }} --all-features --all-targets - test-crate-build-success: - name: test crate build success + + test-crates: runs-on: ubuntu-latest if: always() needs: - - matrix + - changes + - crates-matrix - build - build-msrv timeout-minutes: 30 @@ -202,3 +211,4 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} + allowed-skips: crates-matrix, build, build-msrv diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 1292e20b4f1..81ee1ce52a7 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -5,27 +5,9 @@ on: branches: - main - zsa-support - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - docker/Dockerfile - - docker/entrypoint.sh - - docker/**/*.toml - - zebrad/tests/common/configs/** - - .github/workflows/test-docker.yml - push: branches: [main] - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - docker/Dockerfile - - docker/entrypoint.sh - - docker/**/*.toml - - zebrad/tests/common/configs/** - - .github/workflows/test-docker.yml + merge_group: # Ensures that only one workflow task will run at a time. Previous builds, if # already in process, will get cancelled. Only the latest commit will be allowed @@ -46,9 +28,28 @@ env: COLORBT_SHOW_HIDDEN: 1 jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + test_docker: ${{ steps.filter.outputs.test_docker || 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' + with: + filters: .github/path-filters.yml + build-docker-image: name: Build Docker Image + needs: changes + if: needs.changes.outputs.test_docker == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ${{ vars.DOCKER_BUILD_RUNNER_AMD64 || 'ubuntu-latest' }} @@ -58,14 +59,14 @@ jobs: with: persist-credentials: false - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0 - name: Build & push id: docker_build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 #v6.19.2 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f #v7.1.0 with: target: tests context: . @@ -77,14 +78,15 @@ jobs: outputs: type=docker,dest=${{ runner.temp }}/zebrad-test.tar - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: name: zebrad-test path: ${{ runner.temp }}/zebrad-test.tar test-configurations: name: Test ${{ matrix.name }} - needs: build-docker-image + needs: [changes, build-docker-image] + if: needs.changes.outputs.test_docker == 'true' permissions: contents: read actions: read @@ -118,7 +120,7 @@ jobs: - id: custom-conf name: Custom config env_vars: -e CONFIG_FILE_PATH=/home/zebra/zebrad/tests/common/configs/custom-conf.toml - grep_patterns: -e "extra_coinbase_data:\\sSome\\(\\\"do you even shield\\?\\\"\\)" + grep_patterns: -e "extra_coinbase_data:\\sSome\\(\\\"Do you even shield\\?\\\"\\)" # RPC configuration tests - id: rpc-conf @@ -155,7 +157,7 @@ jobs: persist-credentials: false - name: Download artifact - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 #v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 with: name: zebrad-test path: ${{ runner.temp }} @@ -217,11 +219,11 @@ jobs: exit 0; fi - test-docker-config-success: - name: Test Docker Config Success + test-docker: runs-on: ubuntu-latest if: always() needs: + - changes - build-docker-image - test-configurations timeout-minutes: 30 @@ -230,3 +232,4 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} + allowed-skips: build-docker-image, test-configurations diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index d208faee918..b0d0243df3a 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -5,21 +5,9 @@ on: branches: - main - zsa-support - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - .config/nextest.toml - - .github/workflows/tests-unit.yml - push: branches: [main] - paths: - - "**/*.rs" - - "**/Cargo.toml" - - "**/Cargo.lock" - - .config/nextest.toml - - .github/workflows/tests-unit.yml + merge_group: # Ensures that only one workflow task will run at a time. Previous builds, if # already in process, will get cancelled. Only the latest commit will be allowed @@ -40,9 +28,28 @@ env: COLORBT_SHOW_HIDDEN: 1 jobs: - unit-tests: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + unit_tests: ${{ steps.filter.outputs.unit_tests || 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'pull_request' + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + if: github.event_name == 'pull_request' + with: + filters: .github/path-filters.yml + + run-unit-tests: name: ${{ matrix.rust-version }} on ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.unit_tests == 'true' permissions: + contents: read id-token: write statuses: write runs-on: ${{ matrix.os }} @@ -65,7 +72,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: ${{ matrix.rust-version }} # Mirror the cfgs in .cargo/config.toml; the action's RUSTFLAGS export @@ -73,7 +80,7 @@ jobs: rustflags: '-D warnings --cfg zcash_unstable="nu7" --cfg feature="tx_v6"' cache-key: unit-tests-${{ matrix.os }}-${{ matrix.rust-version }}-${{ matrix.features }} cache-on-failure: true - - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 + - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 with: tool: cargo-nextest - uses: ./.github/actions/setup-zebra-build @@ -103,33 +110,35 @@ jobs: check-no-git-dependencies: name: Check no git dependencies + needs: changes + if: needs.changes.outputs.unit_tests == 'true' && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'A-release') permissions: + contents: read id-token: write statuses: write runs-on: ubuntu-latest timeout-minutes: 30 - if: contains(github.event.pull_request.labels.*.name, 'A-release') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c #v1.15.2 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 #v1.16.1 with: toolchain: stable cache-on-failure: true - - uses: taiki-e/install-action@305bebabd4457bed9b82541755f034994382465b #v2.68.10 + - uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 #v2.79.3 with: tool: cargo-nextest - uses: ./.github/actions/setup-zebra-build - name: Check no git dependencies run: cargo nextest run --profile check-no-git-dependencies --locked --run-ignored=only - test-success: - name: test success + unit-tests: runs-on: ubuntu-latest if: always() needs: - - unit-tests + - changes + - run-unit-tests - check-no-git-dependencies timeout-minutes: 30 steps: @@ -137,4 +146,4 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} - allowed-skips: check-no-git-dependencies + allowed-skips: run-unit-tests, check-no-git-dependencies diff --git a/.github/workflows/trigger-integration-tests.yml b/.github/workflows/trigger-integration-tests.yml new file mode 100644 index 00000000000..7cd4f5a004d --- /dev/null +++ b/.github/workflows/trigger-integration-tests.yml @@ -0,0 +1,105 @@ +name: Trigger Integration Tests + +on: + pull_request: + branches: [main] + paths: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - .cargo/config.toml + - .github/workflows/trigger-integration-tests.yml + + push: + branches: [main] + paths: + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - .cargo/config.toml + - .github/workflows/trigger-integration-tests.yml + + workflow_dispatch: + inputs: + pr_number: + description: "PR number to run integration tests against (use for fork PRs)" + required: true + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || inputs.pr_number || github.run_id }} + cancel-in-progress: true + +permissions: {} + +jobs: + trigger-integration: + name: Trigger integration tests + # Fork PRs do not receive repository secrets on pull_request events. + # Skip the job on fork PRs; maintainers can still run it manually via + # workflow_dispatch with the PR number as input. + if: ${{ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) || (github.event_name == 'push' && github.ref_name == 'main') || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + permissions: + # Needed to post a sticky comment on the PR when triggered manually. + pull-requests: write + steps: + - name: Resolve target PR details (manual dispatch only) + id: pr + if: github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ inputs.pr_number }} + run: | + DATA=$(gh pr view "$PR" --repo "$GITHUB_REPOSITORY" --json headRefOid,body) + SHA=$(echo "$DATA" | jq -r .headRefOid) + BODY=$(echo "$DATA" | jq -r .body) + TEST_SHA=$(echo "$BODY" | sed '/[^ ]ZIT-Revision/!d' | sed -E 's/.*ZIT-Revision: ([^\\]*)\\.*/\1/') + # PR body is contributor-controlled; only forward sha-like values. + if [[ -n "$TEST_SHA" ]] && ! [[ "$TEST_SHA" =~ ^[a-zA-Z0-9._/-]+$ ]]; then + echo "::warning::Ignoring invalid ZIT-Revision value" + TEST_SHA="" + fi + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "test_sha=$TEST_SHA" >> "$GITHUB_OUTPUT" + + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.Z3_APP_ID }} + private-key: ${{ secrets.Z3_APP_PRIVATE_KEY }} + owner: zcash + repositories: integration-tests + + - name: Get requested test branch, if any + if: github.event_name == 'pull_request' + id: test-branch + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + TEST_SHA=$(gh pr -R ZcashFoundation/zebra list --search "${HEAD_SHA}" --json body | jq '.[0].body' | sed '/[^ ]ZIT-Revision/!d' | sed -E 's/.*ZIT-Revision: ([^\\]*)\\.*/\1/') + echo "test_sha=${TEST_SHA}" >> $GITHUB_OUTPUT + + - name: Trigger integration tests + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + SHA: ${{ steps.pr.outputs.sha || github.event.pull_request.head.sha || github.sha }} + TEST_SHA: ${{ steps.pr.outputs.test_sha || steps.test-branch.outputs.test_sha }} + run: > + gh api repos/zcash/integration-tests/dispatches + --field event_type="zebra-interop-request" + --field client_payload[sha]="${SHA}" + --field client_payload[test_sha]="${TEST_SHA}" + + - name: Post sticky comment on PR (manual dispatch only) + if: github.event_name == 'workflow_dispatch' + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 #v3.0.4 + with: + number: ${{ inputs.pr_number }} + header: integration-tests-dispatch + message: | + 🚀 Integration tests dispatched manually against this PR's HEAD (`${{ steps.pr.outputs.sha }}`). + + Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/zfnd-build-docker-image.yml b/.github/workflows/zfnd-build-docker-image.yml index bd322247223..ca075764ab0 100644 --- a/.github/workflows/zfnd-build-docker-image.yml +++ b/.github/workflows/zfnd-build-docker-image.yml @@ -51,7 +51,7 @@ on: description: "Environment to use for the build (commonly dev)" required: false type: string - default: dev + default: '' secrets: DOCKERHUB_USERNAME: @@ -73,6 +73,31 @@ env: CARGO_INCREMENTAL: ${{ vars.CARGO_INCREMENTAL }} jobs: + # Resolve the workflow environment once so build/merge jobs and downstream + # consumers route to the same dev/stage/prod GitHub environment. + determine-environment: + name: Determine environment + runs-on: ubuntu-latest + timeout-minutes: 1 + outputs: + environment: ${{ steps.set.outputs.environment }} + steps: + - id: set + env: + INPUT_ENVIRONMENT: ${{ inputs.environment }} + EVENT_NAME: ${{ github.event_name }} + run: | + if [ -n "$INPUT_ENVIRONMENT" ]; then + ENV="$INPUT_ENVIRONMENT" + else + case "$EVENT_NAME" in + release) ENV="prod" ;; + pull_request) ENV="dev" ;; + *) ENV="stage" ;; + esac + fi + echo "environment=$ENV" >> "$GITHUB_OUTPUT" + prepare-matrix: name: Prepare build matrix runs-on: ubuntu-latest @@ -100,10 +125,10 @@ jobs: build: name: Build images (${{ matrix.platform }}) - needs: prepare-matrix + needs: [determine-environment, prepare-matrix] timeout-minutes: 210 runs-on: ${{ matrix.runner }} - environment: ${{ github.event_name == 'release' && 'prod' || inputs.environment || 'dev' }} + environment: ${{ needs.determine-environment.outputs.environment }} strategy: fail-fast: false matrix: ${{ fromJSON(needs.prepare-matrix.outputs.matrix) }} @@ -122,14 +147,14 @@ jobs: persist-credentials: false - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 # Automatic tag management and OCI Image Format Specification for labels - name: Docker meta id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 #v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #v6.0.0 with: # list of Docker images to use as base name for tags # Docker Hub publishing requires ALL conditions to be true: @@ -174,14 +199,14 @@ jobs: access_token_lifetime: 10800s - name: Login to Google Artifact Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.0 with: registry: us-docker.pkg.dev username: oauth2accesstoken password: ${{ steps.auth.outputs.access_token }} - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -189,7 +214,7 @@ jobs: # Setup Docker Buildx to use Docker Build Cloud - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0 # zizmor: ignore[cache-poisoning] -- shared Docker Build Cloud cache across envs, isolation fix pending with: version: lab:latest driver: ${{ vars.DOCKER_BUILDER || 'docker' }} @@ -201,7 +226,7 @@ jobs: - name: Build & push (with attestations) id: docker_build if: github.repository_owner == 'ZcashFoundation' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 #v6.19.2 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f #v7.1.0 with: platforms: ${{ matrix.platform }} target: ${{ inputs.dockerfile_target }} @@ -227,7 +252,7 @@ jobs: - name: Build & push (without attestations) id: docker_build_no_attestations if: github.repository_owner != 'ZcashFoundation' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 #v6.19.2 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f #v7.1.0 with: platforms: ${{ matrix.platform }} target: ${{ inputs.dockerfile_target }} @@ -253,7 +278,7 @@ jobs: # Upload digest as artifact for merge job - name: Upload digest - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} path: /tmp/digests/* @@ -266,8 +291,8 @@ jobs: merge: name: Create multi-arch manifest runs-on: ubuntu-latest - needs: [prepare-matrix, build] - environment: ${{ github.event_name == 'release' && 'prod' || inputs.environment || 'dev' }} + needs: [determine-environment, prepare-matrix, build] + environment: ${{ needs.determine-environment.outputs.environment }} permissions: contents: read id-token: write @@ -280,13 +305,13 @@ jobs: persist-credentials: false - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 # Download all platform digests - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 #v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 with: path: /tmp/digests pattern: digests-* @@ -295,7 +320,7 @@ jobs: # Docker meta for tag generation - name: Docker meta id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 #v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #v6.0.0 with: images: | us-docker.pkg.dev/${{ vars.GCP_PROJECT }}/zebra/${{ inputs.image_name }} @@ -319,7 +344,7 @@ jobs: access_token_lifetime: 3600s - name: Login to Google Artifact Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.0 with: registry: us-docker.pkg.dev username: oauth2accesstoken @@ -330,7 +355,7 @@ jobs: # 2. Running Docker Scout on release PRs (requires DockerHub authentication) - name: Login to DockerHub if: (inputs.publish_to_dockerhub && github.event_name == 'release' && !github.event.release.prerelease) || (inputs.dockerfile_target == 'runtime' && contains(github.event.pull_request.title, 'Release v')) - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -362,7 +387,7 @@ jobs: # - `dev` for a pull request event - name: Docker Scout id: docker-scout - uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de #v1.18.2 + uses: docker/scout-action@cd72f264beff1cd72735de31148b9d3244a0234a #v1.21.0 # We only run Docker Scout on the `runtime` target, as the other targets are not meant to be released # and are commonly used for testing, and thus are ephemeral. # TODO: Remove the `contains` check once we have a better way to determine if just new vulnerabilities are present. diff --git a/.github/workflows/zfnd-ci-integration-tests-gcp.yml b/.github/workflows/zfnd-ci-integration-tests-gcp.yml index 98f5f00ec36..a084861c631 100644 --- a/.github/workflows/zfnd-ci-integration-tests-gcp.yml +++ b/.github/workflows/zfnd-ci-integration-tests-gcp.yml @@ -53,7 +53,7 @@ on: pull_request: branches: [main] - types: [labeled] + types: [labeled, synchronize, reopened] push: # Run only on main branch updates that modify Rust code or dependencies. @@ -126,7 +126,7 @@ jobs: build: name: Build CI Docker # Skip PRs from external repositories, let them pass, and then GitHub's Merge Queue will check them - if: ${{ (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && (github.event_name != 'pull_request' || github.event.label.name == 'run-stateful-tests') }} + if: ${{ (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-stateful-tests')) }} permissions: contents: read id-token: write @@ -155,7 +155,7 @@ jobs: get-available-disks: name: Check if cached state disks exist for ${{ inputs.network || vars.ZCASH_NETWORK }} # Skip PRs from external repositories, let them pass, and then GitHub's Merge Queue will check them - if: ${{ (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && (github.event_name != 'pull_request' || github.event.label.name == 'run-stateful-tests') }} + if: ${{ (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-stateful-tests')) }} permissions: contents: read id-token: write @@ -169,7 +169,7 @@ jobs: # Some outputs are ignored, because we don't run those jobs on testnet. get-available-disks-testnet: name: Check if cached state disks exist for testnet - if: ${{ (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && (github.event_name != 'pull_request' || github.event.label.name == 'run-stateful-tests') }} + if: ${{ (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-stateful-tests')) }} permissions: contents: read id-token: write @@ -649,6 +649,9 @@ jobs: # (PR statuses are already reported in the PR jobs list, and checked by GitHub's Merge Queue.) if: (failure() && github.event.pull_request == null) || (cancelled() && github.event.pull_request == null) runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: jayqi/failed-build-issue-action@1a893bbf43ef1c2a8705e2b115cd4f0fe3c5649b #v1.2.0 with: @@ -666,7 +669,7 @@ jobs: ${{ always() && (!startsWith(github.event_name, 'pull') || !github.event.pull_request.head.repo.fork) && - (github.event_name != 'pull_request' || github.event.label.name == 'run-stateful-tests') + (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-stateful-tests')) }} needs: - build diff --git a/.github/workflows/zfnd-delete-gcp-resources.yml b/.github/workflows/zfnd-delete-gcp-resources.yml index a3a9397aaad..29cf6f014fc 100644 --- a/.github/workflows/zfnd-delete-gcp-resources.yml +++ b/.github/workflows/zfnd-delete-gcp-resources.yml @@ -4,10 +4,8 @@ # 2. Deletes instance templates older than a set number of days. # 3. Deletes older disks not currently in use, with certain ones prefixed by commit hashes or "zebrad-". # 4. Deletes cache images from GCP, retaining a specified number of the latest images for certain types like zebrad checkpoint cache, zebrad tip cache, and lightwalletd + zebrad tip cache. -# 5. Deletes unused artifacts from Google Artifact Registry older than a defined number of hours while retaining the latest few. # -# It uses the gcloud CLI for most of its operations and also leverages specific GitHub Actions like the gcr-cleaner for deleting old images from the Google Artifact Registry. -# The workflow is scheduled to run daily at 0700 UTC. +# It uses the gcloud CLI for its operations and is scheduled to run daily at 0700 UTC. name: Delete GCP resources on: @@ -27,9 +25,6 @@ env: # But keep the latest $KEEP_LATEST_IMAGE_COUNT images of each type. # We keep this small to reduce storage costs. KEEP_LATEST_IMAGE_COUNT: 2 - # Delete all artifacts in registry created before $DELETE_IMAGE_HOURS hours ago. - # We keep this long enough for PRs that are still on the same commit can re-run with the same image. - DELETE_IMAGE_HOURS: 504h # 21 days permissions: contents: read @@ -103,52 +98,6 @@ jobs: run: | ./.github/workflows/scripts/gcp-delete-old-cache-images.sh - # We're using a generic approach here, which allows multiple registries to be included, - # even those not related to GCP. Enough reason to create a separate job. - # - # The same artifacts are used for both mainnet and testnet. - clean-registries: - name: Delete unused artifacts in registry - if: github.repository_owner == 'ZcashFoundation' - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - strategy: - matrix: - environment: [dev, prod] - environment: ${{ matrix.environment }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - with: - persist-credentials: false - - # Setup gcloud CLI - - name: Authenticate to Google Cloud - id: auth - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 #v3.0.0 - with: - workload_identity_provider: '${{ vars.GCP_WIF }}' - service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - token_format: access_token - - - name: Login to Google Artifact Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 #v3.7.0 - with: - registry: us-docker.pkg.dev - username: oauth2accesstoken - password: ${{ steps.auth.outputs.access_token }} - - # Deletes all images older than $DELETE_IMAGE_HOURS days. - - uses: docker://us-docker.pkg.dev/gcr-cleaner/gcr-cleaner/gcr-cleaner-cli@sha256:333a4d0617b19a86b55f9aaae10e49294b4469590166f3b4a1c4f7bbd20eb6c2 - # Refer to the official documentation to understand available arguments: - # https://github.com/GoogleCloudPlatform/gcr-cleaner - with: - args: >- - -repo=us-docker.pkg.dev/${{ vars.GCP_PROJECT }}/zebra/zebrad-test - -grace=${{ env.DELETE_IMAGE_HOURS }} - -keep=${{ env.KEEP_LATEST_IMAGE_COUNT }} - delete-resources-success: name: Delete GCP resources success runs-on: ubuntu-latest @@ -160,7 +109,6 @@ jobs: }} needs: - delete-resources - - clean-registries timeout-minutes: 1 steps: - name: Decide whether the needed jobs succeeded or failed diff --git a/.github/workflows/zfnd-deploy-integration-tests-gcp.yml b/.github/workflows/zfnd-deploy-integration-tests-gcp.yml index 7cf31a81738..032fa583618 100644 --- a/.github/workflows/zfnd-deploy-integration-tests-gcp.yml +++ b/.github/workflows/zfnd-deploy-integration-tests-gcp.yml @@ -108,6 +108,26 @@ permissions: contents: read jobs: + # Resolve the workflow environment once so every downstream job and the + # SENTRY_ENVIRONMENT / GCP labels stay in lockstep. + determine-environment: + name: Determine environment + runs-on: ubuntu-latest + timeout-minutes: 1 + outputs: + environment: ${{ steps.set.outputs.environment }} + steps: + - id: set + env: + EVENT_NAME: ${{ github.event_name }} + run: | + case "$EVENT_NAME" in + release) ENV="prod" ;; + pull_request) ENV="dev" ;; + *) ENV="stage" ;; + esac + echo "environment=$ENV" >> "$GITHUB_OUTPUT" + # Find a cached state disk for ${{ inputs.test_id }}, matching all of: # - disk cached state prefix -> zebrad-cache or lwd-cache # - state version (from the source code) - v{N} @@ -123,12 +143,14 @@ jobs: # get-disk-name: name: Get disk name + needs: [determine-environment] permissions: contents: read id-token: write uses: ./.github/workflows/zfnd-find-cached-disks.yml if: ${{ (inputs.needs_zebra_state || inputs.needs_lwd_state) || (inputs.saves_to_disk || inputs.force_save_to_disk) }} with: + environment: ${{ needs.determine-environment.outputs.environment }} network: ${{ inputs.network || vars.ZCASH_NETWORK }} disk_prefix: ${{ inputs.needs_lwd_state && 'lwd-cache' || inputs.needs_zebra_state && 'zebrad-cache' }} disk_suffix: ${{ (inputs.needs_zebra_state || inputs.needs_lwd_state) && inputs.disk_suffix || '' }} @@ -141,7 +163,8 @@ jobs: test-result: name: Run ${{ inputs.test_id }} test runs-on: zfnd-runners - needs: [ get-disk-name ] + needs: [ determine-environment, get-disk-name ] + environment: ${{ needs.determine-environment.outputs.environment }} if: ${{ !cancelled() && !failure() && (needs.get-disk-name.result == 'success' || needs.get-disk-name.result == 'skipped') }} continue-on-error: ${{ inputs.allow_failure }} timeout-minutes: ${{ inputs.is_long_test && 7200 || 180 }} @@ -161,7 +184,7 @@ jobs: fetch-depth: '2' - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 # GCP instance names are limited to 63 chars. With test_id (max ~31) + sha (7) + hyphens (2), @@ -175,7 +198,7 @@ jobs: # Install our SSH secret - name: Install private SSH key - uses: shimataro/ssh-key-action@6b84f2e793b32fa0b03a379cadadec75cc539391 #v2.8.0 + uses: shimataro/ssh-key-action@87a8f067114a8ce263df83e9ed5c849953548bc3 #v2.8.1 with: key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} name: google_compute_engine @@ -219,6 +242,8 @@ jobs: # $CONTAINER_ENV) or these known paths to access the state data. - name: Create ${{ inputs.test_id }} GCP compute instance id: create-instance + env: + WORKFLOW_ENVIRONMENT: ${{ needs.determine-environment.outputs.environment }} run: | NAME="${{ inputs.test_id }}-${{ env.GITHUB_SHA_SHORT }}" @@ -251,7 +276,7 @@ jobs: fi # Environment variables for the container - CONTAINER_ENV="${{ inputs.test_variables }},RUST_LOG=${{ env.RUST_LOG }},RUST_BACKTRACE=${{ env.RUST_BACKTRACE }},RUST_LIB_BACKTRACE=${{ env.RUST_LIB_BACKTRACE }},COLORBT_SHOW_HIDDEN=${{ env.COLORBT_SHOW_HIDDEN }},CARGO_INCREMENTAL=${{ env.CARGO_INCREMENTAL }}" + CONTAINER_ENV="${{ inputs.test_variables }},RUST_LOG=${{ env.RUST_LOG }},RUST_BACKTRACE=${{ env.RUST_BACKTRACE }},RUST_LIB_BACKTRACE=${{ env.RUST_LIB_BACKTRACE }},COLORBT_SHOW_HIDDEN=${{ env.COLORBT_SHOW_HIDDEN }},CARGO_INCREMENTAL=${{ env.CARGO_INCREMENTAL }},SENTRY_DSN=${{ vars.SENTRY_DSN }},SENTRY_ENVIRONMENT=${WORKFLOW_ENVIRONMENT},GITHUB_ACTIONS=${GITHUB_ACTIONS},GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME},GITHUB_REF_POINT_SLUG_URL=${GITHUB_REF_POINT_SLUG_URL},GITHUB_SHA=${GITHUB_SHA},GITHUB_RUN_ID=${GITHUB_RUN_ID},GITHUB_RUN_ATTEMPT=${GITHUB_RUN_ATTEMPT},GITHUB_WORKFLOW=${GITHUB_WORKFLOW},GITHUB_JOB=${GITHUB_JOB},CI_PR_NUMBER=${{ github.event.pull_request.number || '' }},CI_TEST_ID=${{ inputs.test_id }}" # Trim whitespace from GAR_BASE as for some reason it's getting a trailing space GAR_BASE_TRIMMED=$(echo "${{ vars.GAR_BASE }}" | xargs) @@ -274,7 +299,7 @@ jobs: --service-account=${{ vars.GCP_DEPLOYMENTS_SA }} \ --metadata=google-logging-enabled=true,google-logging-use-fluentbit=true,google-monitoring-enabled=true \ --metadata-from-file=startup-script=.github/workflows/scripts/gcp-vm-startup-script.sh \ - --labels=app=${{ inputs.app_name }},environment=test,network=${NETWORK},github_ref=${{ env.GITHUB_REF_SLUG_URL }},test=${{ inputs.test_id }} \ + --labels=app=${{ inputs.app_name }},environment=${WORKFLOW_ENVIRONMENT},network=${NETWORK},github_ref=${{ env.GITHUB_REF_SLUG_URL }},test=${{ inputs.test_id }} \ --tags ${{ inputs.app_name }} \ --zone ${{ vars.GCP_ZONE }} @@ -360,11 +385,102 @@ jobs: fi " + # Capture HEIGHT HASH lines from the container logs into a file on the + # instance so the upload-checkpoint-artifact job can retrieve them with + # `gcloud compute scp` instead of rediscovering the container. + - name: Capture checkpoints from container logs + if: ${{ startsWith(inputs.test_id, 'generate-checkpoints-') }} + env: + CONTAINER_ID: ${{ steps.find-container.outputs.CONTAINER_ID }} + INSTANCE_NAME: ${{ inputs.test_id }}-${{ env.GITHUB_REF_SLUG_URL }}-${{ env.GITHUB_SHA_SHORT }} + GCP_ZONE: ${{ vars.GCP_ZONE }} + run: | + gcloud compute ssh "${INSTANCE_NAME}" \ + --zone "${GCP_ZONE}" \ + --ssh-flag="-o ServerAliveInterval=5" \ + --ssh-flag="-o ConnectionAttempts=20" \ + --ssh-flag="-o ConnectTimeout=5" \ + --command=" + sudo docker logs ${CONTAINER_ID} 2>&1 | grep -oE '[0-9]+ [0-9a-f]{64}' > /tmp/checkpoints.txt; + echo \"Captured \$(wc -l < /tmp/checkpoints.txt) checkpoint lines\"; + " + + # Upload the checkpoint file captured in the test-result job as a workflow + # artifact for checkpoint-update.yml to consume. Only runs for checkpoint + # generation tests. + upload-checkpoint-artifact: + name: Upload ${{ inputs.test_id }} checkpoint artifact + runs-on: ubuntu-latest + needs: [ determine-environment, test-result ] + environment: ${{ needs.determine-environment.outputs.environment }} + if: ${{ needs.test-result.result == 'success' && startsWith(inputs.test_id, 'generate-checkpoints-') }} + env: + TEST_ID: ${{ inputs.test_id }} + GCP_ZONE: ${{ vars.GCP_ZONE }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 + with: + persist-credentials: false + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 + with: + short-length: 7 + slug-maxlength: 23 + - uses: shimataro/ssh-key-action@87a8f067114a8ce263df83e9ed5c849953548bc3 #v2.8.1 + with: + key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} + name: google_compute_engine + known_hosts: unnecessary + - name: Generate public SSH key + run: | + sudo apt-get update && sudo apt-get -qq install -y --no-install-recommends openssh-client + ssh-keygen -y -f ~/.ssh/google_compute_engine > ~/.ssh/google_compute_engine.pub + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 #v3.0.0 + with: + workload_identity_provider: '${{ vars.GCP_WIF }}' + service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db #v3.0.1 + - name: Pull checkpoint file from instance + run: | + INSTANCE_NAME="${TEST_ID}-${GITHUB_REF_SLUG_URL}-${GITHUB_SHA_SHORT}" + # Match the repo file naming used by checkpoint-update.yml. + if echo "${TEST_ID}" | grep -qi "testnet"; then + OUT="test-checkpoints.txt" + else + OUT="main-checkpoints.txt" + fi + + gcloud compute scp \ + --zone "${GCP_ZONE}" \ + "${INSTANCE_NAME}:/tmp/checkpoints.txt" \ + "${OUT}" + + LINES=$(wc -l < "${OUT}") + echo "Retrieved ${LINES} checkpoint lines into ${OUT}" + if [ "$LINES" -eq 0 ]; then + echo "ERROR: checkpoint file empty on instance" + exit 1 + fi + + - name: Upload checkpoint artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 + with: + name: ${{ inputs.test_id }}-checkpoints + path: "*-checkpoints.txt" + retention-days: 30 + # create a state image from the instance's state disk, if requested by the caller create-state-image: name: Create ${{ inputs.test_id }} cached state image runs-on: ubuntu-latest - needs: [ test-result ] + needs: [ determine-environment, test-result ] + environment: ${{ needs.determine-environment.outputs.environment }} # We run exactly one of without-cached-state or with-cached-state, and we always skip the other one. # Normally, if a job is skipped, all the jobs that depend on it are also skipped. # So we need to override the default success() check to make this job run. @@ -383,7 +499,7 @@ jobs: fetch-depth: '2' - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 slug-maxlength: 23 @@ -407,7 +523,7 @@ jobs: # Install our SSH secret - name: Install private SSH key - uses: shimataro/ssh-key-action@6b84f2e793b32fa0b03a379cadadec75cc539391 #v2.8.0 + uses: shimataro/ssh-key-action@87a8f067114a8ce263df83e9ed5c849953548bc3 #v2.8.1 with: key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} name: google_compute_engine @@ -699,9 +815,12 @@ jobs: delete-instance: name: Delete ${{ inputs.test_id }} instance runs-on: ubuntu-latest - needs: [ create-state-image ] + needs: [ determine-environment, create-state-image, upload-checkpoint-artifact ] + environment: ${{ needs.determine-environment.outputs.environment }} # If a disk generation step timeouts (+6 hours) the previous job (creating the image) will be skipped. # Even if the instance continues running, no image will be created, so it's better to delete it. + # upload-checkpoint-artifact is included to prevent the instance from being deleted + # before checkpoint data is extracted from the container logs. if: always() continue-on-error: true permissions: @@ -714,7 +833,7 @@ jobs: fetch-depth: '2' - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 slug-maxlength: 23 @@ -751,6 +870,7 @@ jobs: - get-disk-name - test-result - create-state-image + - upload-checkpoint-artifact - delete-instance timeout-minutes: 1 steps: @@ -758,4 +878,6 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} - allowed-skips: ${{ (inputs.saves_to_disk || inputs.force_save_to_disk) && '' || 'create-state-image' }} + allowed-skips: >- + ${{ (inputs.saves_to_disk || inputs.force_save_to_disk) && '' || 'create-state-image' }}, + ${{ startsWith(inputs.test_id, 'generate-checkpoints-') && '' || 'upload-checkpoint-artifact' }} diff --git a/.github/workflows/zfnd-deploy-nodes-gcp.yml b/.github/workflows/zfnd-deploy-nodes-gcp.yml index 7b5e4ed50cf..980df0a19aa 100644 --- a/.github/workflows/zfnd-deploy-nodes-gcp.yml +++ b/.github/workflows/zfnd-deploy-nodes-gcp.yml @@ -1,16 +1,20 @@ -# Google Cloud node deployments and tests that run when Rust code or dependencies are modified, -# but only on PRs from the ZcashFoundation/zebra repository. -# (External PRs are tested/deployed by GitHub's Merge Queue.) +# Deploy Zebra nodes to Google Cloud Platform. # -# 1. `versioning`: Extracts the major version from the release semver. Useful for segregating instances based on major versions. -# 2. `build`: Builds a Docker image named `zebrad` with the necessary tags derived from Git. -# 3. `test-docker-configurations`: Validates all Zebra Docker configurations by running a matrix of configuration tests. -# 6. `deploy-nodes`: Deploys Managed Instance Groups (MiGs) with 2-3 instances (1 per zone) for Mainnet and Testnet. -# - Stateful disks preserve state across updates (rolling updates with health checks) -# - Instance count matches available zones (up to 3), with 1 instance per zone -# - Main and release instances get static IPs (manual deployments get ephemeral IPs) -# - If triggered by main branch pushes, it always replaces the MIG. For releases, MIGs are replaced only if deploying the same major version; otherwise, a new major version is deployed. -# 7. `deploy-instance`: Deploys a single node in a specified GCP zone for testing specific commits. Instances from this job aren't auto-replaced or deleted. +# One zonal MIG per (environment, branch, network, zone). MIG names: +# - release: zebrad-${network}-${zone-letter} +# - push to main: zebrad-main-${network}-${zone-letter} +# - workflow_dispatch: zebrad-${branch}-${network}-${zone-letter} +# +# Push and release fan out to 6 cells (2 networks × 3 zones); +# workflow_dispatch deploys one cell (user picks network + zone). +# +# Design rationale: docs/decisions/devops/0006-gcp-deployment-naming.md +# Operations: book/src/dev/gcp-deployment-operations.md +# +# See ADR docs/decisions/devops/0006-gcp-deployment-naming.md for the design +# rationale and book/src/dev/gcp-deployment-operations.md for operational +# procedures (PR-deploy cleanup, disk-corruption recovery, DB-format-version-break +# release). name: Deploy Nodes to GCP # Ensures that only one workflow task will run at a time. Previous deployments, if @@ -23,7 +27,7 @@ name: Deploy Nodes to GCP # For pull requests, we only run the tests from this workflow, and don't do any deployments. # So an in-progress pull request gets cancelled, just like other tests. concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.environment }}-${{ inputs.network }}-${{ inputs.zone }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: @@ -39,6 +43,15 @@ on: options: - Mainnet - Testnet + zone: + description: "GCP zone for the workflow_dispatch deploy (single zone)" + required: true + type: choice + default: us-east1-b + options: + - us-east1-b + - us-east1-c + - us-east1-d environment: description: "Environment to deploy to" required: true @@ -119,52 +132,107 @@ permissions: contents: read jobs: - # If a release was made we want to extract the first part of the semver from the - # tag_name - # - # Generate the following output to pass to subsequent jobs - # - If our semver is `v1.3.0` the resulting output from this job would be `v1` - # - # Note: We just use the first part of the version to replace old instances, and change - # it when a major version is released, to keep a segregation between new and old - # versions. - versioning: - name: Versioning + # Build the (network, zone) matrix and resolve the target environment from + # the trigger. Downstream jobs read `set-matrix` outputs so the event → + # environment mapping is computed once. + set-matrix: runs-on: ubuntu-latest outputs: - major_version: ${{ steps.set.outputs.major_version }} - if: ${{ github.event_name == 'release' }} + networks: ${{ steps.set-matrix.outputs.networks }} + zones: ${{ steps.set-matrix.outputs.zones }} + environment: ${{ steps.set-matrix.outputs.environment }} steps: - - name: Getting Zebrad Version - id: get - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 - with: - result-encoding: string - script: | - return context.payload.release.tag_name.substring(0,2) - - name: Setting API Version - id: set - run: echo "major_version=${{ steps.get.outputs.result }}" >> "$GITHUB_OUTPUT" - - # Finds a cached state disk for zebra - # - # Passes the disk name to subsequent jobs using `cached_disk_name` output + - id: set-matrix + run: | + case "${{ github.event_name }}" in + release) ENV="prod" ;; + workflow_dispatch) ENV="${{ inputs.environment }}" ;; + push) ENV="stage" ;; + *) ENV="dev" ;; + esac + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + NETWORKS='["${{ inputs.network }}"]' + ZONES='["${{ inputs.zone }}"]' + else + NETWORKS='["Mainnet","Testnet"]' + ZONES='["us-east1-b","us-east1-c","us-east1-d"]' + fi + + { + echo "networks=${NETWORKS}" + echo "zones=${ZONES}" + echo "environment=${ENV}" + } >> "$GITHUB_OUTPUT" + + # Per-network cache-disk lookup. Cache images are network-specific + # (`zebrad-cache-…-mainnet-tip` vs `…-testnet-tip`), so the lookup must + # run once per network the matrix deploys to. Running a single workflow- + # level lookup with `inputs.network || vars.ZCASH_NETWORK` would return + # the wrong image for the other matrix row. # - # For push events, this job always runs. - # For workflow_dispatch events, it runs only if inputs.need_cached_disk is true. - # For release events, this job is skipped (releases use fixed disk names, not cached images). - # PRs from forked repositories are skipped. - get-disk-name: - name: Get disk name + # Skipped for releases (they do not use cached images) and for + # workflow_dispatch with `need_cached_disk=false`. + # One-shot upsert of the HTTP health checks used by the zonal MIGs. + # These are global, network-scoped resources (one per network), so running + # them once per push instead of once per zonal-MIG cell saves 2/3 of the + # upsert calls and avoids concurrent create/update races. + ensure-health-checks: + name: Ensure health checks exist + needs: [set-matrix] + runs-on: ubuntu-latest + environment: ${{ needs.set-matrix.outputs.environment }} + permissions: + contents: read + id-token: write + if: ${{ github.event_name != 'pull_request' }} + steps: + - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 #v3.0.0 + with: + workload_identity_provider: "${{ vars.GCP_WIF }}" + service_account: "${{ vars.GCP_DEPLOYMENTS_SA }}" + - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db #v3.0.1 + - run: | + for NET in $(echo '${{ needs.set-matrix.outputs.networks }}' | jq -r '.[] | ascii_downcase'); do + gcloud compute health-checks describe "zebra-${NET}-health" --global &>/dev/null \ + || gcloud compute health-checks create http "zebra-${NET}-health" \ + --port=8080 --request-path=/healthy \ + --check-interval=60s --timeout=10s \ + --unhealthy-threshold=3 --healthy-threshold=2 \ + --global + done + + get-disk-name-mainnet: + name: Get Mainnet cached disk + needs: [set-matrix] permissions: contents: read id-token: write uses: ./.github/workflows/zfnd-find-cached-disks.yml - # Skip for releases (they use fixed disk names like 'zebrad-cache-mainnet-tip') - # For workflow_dispatch: only run if need_cached_disk is true - if: ${{ github.event_name != 'release' && !(github.event.pull_request.head.repo.fork) && (github.event_name != 'workflow_dispatch' || inputs.need_cached_disk) }} + if: ${{ github.event_name != 'release' + && !(github.event.pull_request.head.repo.fork) + && (github.event_name != 'workflow_dispatch' || inputs.need_cached_disk) + && contains(fromJSON(needs.set-matrix.outputs.networks), 'Mainnet') }} with: - network: ${{ inputs.network || vars.ZCASH_NETWORK }} + network: Mainnet + environment: ${{ needs.set-matrix.outputs.environment }} + disk_prefix: zebrad-cache + disk_suffix: ${{ inputs.cached_disk_type || 'tip' }} + + get-disk-name-testnet: + name: Get Testnet cached disk + needs: [set-matrix] + permissions: + contents: read + id-token: write + uses: ./.github/workflows/zfnd-find-cached-disks.yml + if: ${{ github.event_name != 'release' + && !(github.event.pull_request.head.repo.fork) + && (github.event_name != 'workflow_dispatch' || inputs.need_cached_disk) + && contains(fromJSON(needs.set-matrix.outputs.networks), 'Testnet') }} + with: + network: Testnet + environment: ${{ needs.set-matrix.outputs.environment }} disk_prefix: zebrad-cache disk_suffix: ${{ inputs.cached_disk_type || 'tip' }} @@ -174,6 +242,7 @@ jobs: # The image will be commonly named `zebrad:` build: name: Build CD Docker + needs: [set-matrix] permissions: contents: read id-token: write @@ -192,62 +261,38 @@ jobs: no_cache: ${{ inputs.no_cache || false }} rust_log: info features: ${{ format('{0} {1}', vars.RUST_PROD_FEATURES, vars.RUST_TEST_FEATURES) }} - environment: ${{ github.event_name == 'release' && 'prod' || (github.event_name == 'workflow_dispatch' && inputs.environment) || 'dev' }} + environment: ${{ needs.set-matrix.outputs.environment }} # This step needs access to Docker Hub secrets to run successfully secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - # Deploy Managed Instance Groups (MiGs) for Mainnet and Testnet, - # with one node in the configured GCP region. - # - # Separate Mainnet and Testnet MiGs are deployed whenever there are: - # - pushes to the main branch, or - # - version releases of Zebra. + # Create or update one zonal MIG per matrix cell. Each cell is a + # (network, zone) tuple. MIG identity is the tuple plus environment + + # branch. No two MIGs ever share a disk; rolling updates are per-zone. # - # Once this workflow is triggered: - # - by pushes to main: the MiG is always replaced, - # - by releases: the MiG is only replaced if the same major version is being deployed, - # otherwise a new major version is deployed in a new MiG. - # - # Runs: - # - on every push to the `main` branch - # - on every release, when it's published - # - on workflow_dispatch for manual deployments - - # Determine which networks to deploy based on the trigger - set-matrix: - runs-on: ubuntu-latest - outputs: - networks: ${{ steps.set-networks.outputs.matrix }} - steps: - - id: set-networks - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - # Manually triggered deployment: output a valid JSON array with the single chosen network. - echo "matrix=[\"${{ inputs.network }}\"]" >> $GITHUB_OUTPUT - else - echo 'matrix=["Mainnet","Testnet"]' >> $GITHUB_OUTPUT - fi - + # `fail-fast: false` keeps each (network, zone) independent: one cell's + # failure must not cancel the five sister cells. deploy-nodes: strategy: + fail-fast: false matrix: network: ${{ fromJSON(needs.set-matrix.outputs.networks) }} - name: Deploy ${{ matrix.network }} nodes - needs: - [ - set-matrix, - build, - versioning, - get-disk-name, - ] + zone: ${{ fromJSON(needs.set-matrix.outputs.zones) }} + name: Deploy ${{ matrix.network }} ${{ matrix.zone }} + needs: [set-matrix, build, get-disk-name-mainnet, get-disk-name-testnet, ensure-health-checks] runs-on: ubuntu-latest timeout-minutes: 60 env: - CACHED_DISK_NAME: ${{ needs.get-disk-name.outputs.cached_disk_name }} - # Use prod environment for releases, allow manual selection for workflow_dispatch, default to dev for others - environment: ${{ github.event_name == 'release' && 'prod' || (github.event_name == 'workflow_dispatch' && inputs.environment) || 'dev' }} + # Pick the cache image for this matrix row's network. One image seeds + # all three zones for a given network. Released deploys skip the + # cache lookup entirely and get an empty value here. + CACHED_DISK_NAME: >- + ${{ matrix.network == 'Mainnet' + && needs.get-disk-name-mainnet.outputs.cached_disk_name + || needs.get-disk-name-testnet.outputs.cached_disk_name }} + # Use the workflow environment consistently across GitHub, GCP labels, and runtime metadata. + environment: ${{ needs.set-matrix.outputs.environment }} permissions: contents: read id-token: write @@ -264,22 +309,20 @@ jobs: persist-credentials: false - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 - # Makes the Zcash network name lowercase. - # - # Labels in GCP are required to be in lowercase, but the blockchain network - # uses sentence case, so we need to downcase the network. - # - # Passes lowercase network to subsequent steps using $NETWORK env variable. - - name: Downcase network name for labels + - name: Extract matrix values run: | - NETWORK_CAPS="${{ matrix.network }}" - echo "NETWORK=${NETWORK_CAPS,,}" >> "$GITHUB_ENV" + ZONE="${{ matrix.zone }}" + NET_CAPS="${{ matrix.network }}" + { + echo "NETWORK=${NET_CAPS,,}" + echo "ZONE=${ZONE}" + echo "ZONE_LETTER=${ZONE##*-}" + } >> "$GITHUB_ENV" - # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 #v3.0.0 @@ -292,232 +335,220 @@ jobs: with: install_components: 'beta' - - name: Create instance template for ${{ matrix.network }} + - name: Compute MIG and disk naming + env: + ENV: ${{ needs.set-matrix.outputs.environment }} + REF_NAME: ${{ github.ref_name }} + REF_SLUG: ${{ env.GITHUB_REF_SLUG_URL }} + SHA_SHORT: ${{ env.GITHUB_SHA_SHORT }} run: | - # Set common naming variables (exported for use in subsequent steps) - GIT_PREFIX="${{ needs.versioning.outputs.major_version || env.GITHUB_REF_SLUG_URL }}" - MIG_NAME="zebrad-${GIT_PREFIX}-${NETWORK}" - TEMPLATE_NAME="zebrad-${GIT_PREFIX}-${{ env.GITHUB_SHA_SHORT }}-${NETWORK}" - echo "MIG_NAME=${MIG_NAME}" >> $GITHUB_ENV - echo "TEMPLATE_NAME=${TEMPLATE_NAME}" >> $GITHUB_ENV - - # Constant DISK_NAME per deployment type enables disk preservation across updates - if [ ${{ github.event_name == 'release' }} ]; then - DISK_NAME="zebrad-cache-${NETWORK}" + if [ "${ENV}" = "prod" ]; then + PREFIX="" + elif [ "${REF_NAME}" = "main" ]; then + PREFIX="main-" else - # Use git prefix (branch/ref name) so each deployment gets its own disk - DISK_NAME="zebrad-cache-${GIT_PREFIX}-${NETWORK}" + PREFIX="${REF_SLUG}-" fi - echo "DISK_NAME=${DISK_NAME}" >> $GITHUB_ENV - - # Fixed disk name is safe since we use 1 instance per zone (no conflicts) - DISK_PARAMS="name=${DISK_NAME},device-name=${DISK_NAME},size=400GB,type=pd-balanced" + { + echo "MIG_NAME=zebrad-${PREFIX}${NETWORK}-${ZONE_LETTER}" + echo "DISK_NAME=zebrad-cache-${PREFIX}${NETWORK}-${ZONE_LETTER}" + echo "TEMPLATE_NAME=zebrad-${PREFIX}${SHA_SHORT}-${NETWORK}-${ZONE_LETTER}" + } >> "$GITHUB_ENV" + + # Reject early if the zonal disk is held by an instance from another MIG. + - name: Pre-flight check for stateful disk squatter + run: | + users=$(gcloud compute disks describe "${DISK_NAME}" --zone="${ZONE}" \ + --format="value(users.basename())" 2>/dev/null || true) + for user in ${users}; do + owner=$(gcloud compute instances describe "${user}" --zone="${ZONE}" \ + --format="value(metadata.items.filter(key:created-by).extract(value))" 2>/dev/null \ + | grep -oE 'instanceGroupManagers/[a-z0-9-]+' | cut -d/ -f2 || true) + if [ -n "${owner}" ] && [ "${owner}" != "${MIG_NAME}" ]; then + echo "::error::${DISK_NAME} in ${ZONE} is held by ${user} (MIG ${owner}). See gcp-deployment-operations.md." + exit 1 + fi + done - # Use cached image if available to speed up initial sync - if [ -n "${{ env.CACHED_DISK_NAME }}" ]; then - DISK_PARAMS+=",image=${{ env.CACHED_DISK_NAME }}" + # Create zonal disk from cache image on first deploy; attach existing on + # subsequent deploys or after a manual bootstrap. + - name: Ensure zonal disk exists + env: + ENV: ${{ needs.set-matrix.outputs.environment }} + run: | + if gcloud compute disks describe "${DISK_NAME}" --zone="${ZONE}" &>/dev/null; then + exit 0 fi - - # Set log file based on input or default - if [ ${{ github.event_name == 'workflow_dispatch' && inputs.log_file != '' }} ]; then + if [ -z "${CACHED_DISK_NAME}" ]; then + echo "::error::No ${DISK_NAME} and no cache image. Seed via integration-tests or manual snapshot." + exit 1 + fi + gcloud compute disks create "${DISK_NAME}" --zone="${ZONE}" \ + --image="${CACHED_DISK_NAME}" \ + --size=400 --type=pd-balanced \ + --labels="app=zebrad,environment=${ENV},network=${NETWORK},zone=${ZONE_LETTER},created_by=${{ github.event_name }},github_ref=${{ env.GITHUB_REF_SLUG_URL }},github_sha=${{ env.GITHUB_SHA_SHORT }}" + + # Single source of truth for the zone-suffix to reserved-IP mapping. + # Gated to stable deploys so feature-branch dispatches to dev can't + # advertise a prod IP that isn't actually attached to the MIG. + - name: Resolve reserved external IP + if: ${{ needs.set-matrix.outputs.environment == 'prod' || github.ref_name == 'main' }} + env: + GCP_REGION: ${{ vars.GCP_REGION }} + run: | + case "${ZONE_LETTER}" in + b) SUFFIX="" ;; + c) SUFFIX="-secondary" ;; + d) SUFFIX="-tertiary" ;; + esac + IP_NAME="zebra-${NETWORK}${SUFFIX}" + IP_ADDRESS=$(gcloud compute addresses describe "${IP_NAME}" \ + --region="${GCP_REGION}" --format='value(address)' 2>/dev/null || true) + echo "IP_NAME=${IP_NAME}" >> "$GITHUB_ENV" + echo "IP_ADDRESS=${IP_ADDRESS}" >> "$GITHUB_ENV" + + - name: Create instance template + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.log_file }}" ]; then LOG_FILE="${{ inputs.log_file }}" else LOG_FILE="${{ vars.CD_LOG_FILE }}" fi - - # Set RPC port based on network if [ "${{ matrix.network }}" = "Mainnet" ]; then - RPC_PORT="8232" + P2P=8233; RPC=8232 else - RPC_PORT="18232" + P2P=18233; RPC=18232 fi - - # Check if template already exists (templates are immutable, same commit = same config) if gcloud compute instance-templates describe "${TEMPLATE_NAME}" &>/dev/null; then - echo "Template ${TEMPLATE_NAME} already exists, reusing existing template" - else - gcloud compute instance-templates create-with-container "${TEMPLATE_NAME}" \ - --machine-type ${{ vars.GCP_SMALL_MACHINE }} \ - --provisioning-model=SPOT \ - --boot-disk-size=10GB \ - --boot-disk-type=pd-standard \ - --image-project=cos-cloud \ - --image-family=cos-stable \ - --subnet=${{ vars.GCP_SUBNETWORK }} \ - --no-address \ - --create-disk="${DISK_PARAMS}" \ - --container-mount-disk=mount-path='/home/zebra/.cache/zebra',name=${DISK_NAME},mode=rw \ - --container-stdin \ - --container-tty \ - --container-image ${{ vars.GAR_BASE }}/zebrad@${{ needs.build.outputs.image_digest }} \ - --container-env "ZEBRA_NETWORK__NETWORK=${{ matrix.network }},ZEBRA_NETWORK__LISTEN_ADDR=0.0.0.0,LOG_FILE=${LOG_FILE},SENTRY_DSN=${{ vars.SENTRY_DSN }},ZEBRA_HEALTH__LISTEN_ADDR=0.0.0.0:8080,ZEBRA_HEALTH__MIN_CONNECTED_PEERS=1,ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:${RPC_PORT}" \ - --service-account ${{ vars.GCP_DEPLOYMENTS_SA }} \ - --scopes cloud-platform \ - --metadata google-logging-enabled=true,google-logging-use-fluentbit=true,google-monitoring-enabled=true \ - --labels=app=zebrad,environment=${{ github.event_name == 'release' && 'prod' || (github.event_name == 'workflow_dispatch' && inputs.environment) || 'dev' }},network=${NETWORK},github_ref=${{ env.GITHUB_REF_SLUG_URL }} \ - --tags zebrad + exit 0 fi - # HTTP health check on /healthy endpoint (sync-aware: 200 during sync, 503 on failure) - - name: Create or update health check - run: | - gcloud compute health-checks create http zebra-${NETWORK}-health \ - --port=8080 \ - --request-path=/healthy \ - --check-interval=60s \ - --timeout=10s \ - --unhealthy-threshold=3 \ - --healthy-threshold=2 \ - --global 2>/dev/null || \ - gcloud compute health-checks update http zebra-${NETWORK}-health \ - --request-path=/healthy \ - --check-interval=60s \ - --timeout=10s \ - --unhealthy-threshold=3 \ - --healthy-threshold=2 \ - --global - - # Check if our destination instance group exists already - - name: Check if ${{ matrix.network }} instance group exists + EXTERNAL_ADDR_ENV="" + if [ -n "${IP_ADDRESS:-}" ]; then + EXTERNAL_ADDR_ENV=",ZEBRA_NETWORK__EXTERNAL_ADDR=${IP_ADDRESS}:${P2P}" + fi + + gcloud compute instance-templates create-with-container "${TEMPLATE_NAME}" \ + --machine-type=${{ vars.GCP_SMALL_MACHINE }} \ + --provisioning-model=SPOT \ + --boot-disk-size=10GB --boot-disk-type=pd-standard \ + --image-project=cos-cloud --image-family=cos-stable \ + --subnet=${{ vars.GCP_SUBNETWORK }} --no-address \ + --disk="name=${DISK_NAME},device-name=${DISK_NAME},mode=rw,auto-delete=no,boot=no" \ + --container-mount-disk="mount-path=/home/zebra/.cache/zebra,name=${DISK_NAME},mode=rw" \ + --container-stdin --container-tty \ + --container-image="${{ vars.GAR_BASE }}/zebrad@${{ needs.build.outputs.image_digest }}" \ + --container-env="ZEBRA_NETWORK__NETWORK=${{ matrix.network }},ZEBRA_NETWORK__LISTEN_ADDR=0.0.0.0:${P2P},LOG_FILE=${LOG_FILE},SENTRY_DSN=${{ vars.SENTRY_DSN }},SENTRY_ENVIRONMENT=${{ needs.set-matrix.outputs.environment }},GITHUB_ACTIONS=${GITHUB_ACTIONS},GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME},GITHUB_REF_POINT_SLUG_URL=${GITHUB_REF_POINT_SLUG_URL},GITHUB_SHA=${GITHUB_SHA},GITHUB_RUN_ID=${GITHUB_RUN_ID},GITHUB_RUN_ATTEMPT=${GITHUB_RUN_ATTEMPT},GITHUB_WORKFLOW=${GITHUB_WORKFLOW},GITHUB_JOB=${GITHUB_JOB},CI_PR_NUMBER=${{ github.event.pull_request.number || '' }},ZEBRA_HEALTH__LISTEN_ADDR=0.0.0.0:8080,ZEBRA_HEALTH__MIN_CONNECTED_PEERS=1,ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:${RPC}${EXTERNAL_ADDR_ENV}" \ + --service-account=${{ vars.GCP_DEPLOYMENTS_SA }} --scopes=cloud-platform \ + --metadata=google-logging-enabled=true,google-logging-use-fluentbit=true,google-monitoring-enabled=true \ + --labels="app=zebrad,environment=${{ needs.set-matrix.outputs.environment }},network=${NETWORK},zone=${ZONE_LETTER},created_by=${{ github.event_name }},github_ref=${{ env.GITHUB_REF_SLUG_URL }},github_sha=${{ env.GITHUB_SHA_SHORT }}" \ + --tags=zebrad + + - name: Check if zonal MIG exists id: does-group-exist continue-on-error: true run: | - gcloud compute instance-groups list | grep "${MIG_NAME}" | grep "${{ vars.GCP_REGION }}" + gcloud compute instance-groups managed describe "${MIG_NAME}" --zone="${ZONE}" >/dev/null 2>&1 - # Deploy new managed instance group with 1 instance per zone (2-3 total) - - name: Create managed instance group for ${{ matrix.network }} + # Fresh MIG: size=1 (one instance per zonal MIG). The template's + # `--disk=name=…` attaches the pre-created zonal disk. + - name: Create zonal MIG if: steps.does-group-exist.outcome == 'failure' run: | - # Query available zones (up to 3) and set instance count to match - ZONES=$(gcloud compute zones list \ - --filter="region:${{ vars.GCP_REGION }}" \ - --format="value(name)" \ - --limit=3 | paste -sd,) - - ZONE_COUNT=$(echo "${ZONES}" | tr ',' '\n' | wc -l) - - echo "Using ${ZONE_COUNT} zones: ${ZONES}" - - gcloud compute instance-groups managed create \ - "${MIG_NAME}" \ - --template "${TEMPLATE_NAME}" \ - --region "${{ vars.GCP_REGION }}" \ - --size "${ZONE_COUNT}" \ - --health-check="zebra-${NETWORK}-health" \ - --initial-delay=3600 \ - --instance-redistribution-type=NONE \ - --target-distribution-shape=EVEN \ - --zones="${ZONES}" - - # Stateful policy preserves disks across updates (auto-delete on MIG deletion) - - name: Configure stateful disk policy + gcloud compute instance-groups managed create "${MIG_NAME}" \ + --template="${TEMPLATE_NAME}" \ + --zone="${ZONE}" \ + --size=1 \ + --health-check="zebra-${NETWORK}-health" \ + --initial-delay=3600 + + - name: Apply stateful disk policy (fresh MIG) if: steps.does-group-exist.outcome == 'failure' run: | gcloud compute instance-groups managed update "${MIG_NAME}" \ - --stateful-disk "device-name=${DISK_NAME},auto-delete=on-permanent-instance-deletion" \ - --region "${{ vars.GCP_REGION }}" - - # Assign static IPs to instances (only for main branch and releases, not manual deployments) - - name: Assign static IPs to instances - if: ${{ steps.does-group-exist.outcome == 'failure' && github.event_name != 'workflow_dispatch' }} + --stateful-disk="device-name=${DISK_NAME},auto-delete=on-permanent-instance-deletion" \ + --zone="${ZONE}" + + # Bind the reserved IP resolved earlier to the fresh MIG instance. + # `instance-configs create --stateful-external-ip` accepts STAGING / + # RUNNING-UNKNOWN instances; the short poll handles the async gap + # between MIG-create returning and list-instances reporting. + - name: Assign static IP (fresh MIG, stable deploy) + if: ${{ steps.does-group-exist.outcome == 'failure' && (needs.set-matrix.outputs.environment == 'prod' || github.ref_name == 'main') }} run: | - # Wait for MIG to be stable (all instances created) - gcloud compute instance-groups managed wait-until "${MIG_NAME}" \ - --stable \ - --region "${{ vars.GCP_REGION }}" \ - --timeout=1200 - - # Get static IPs and instances - IP_NAMES=("zebra-${NETWORK}" "zebra-${NETWORK}-secondary" "zebra-${NETWORK}-tertiary") - mapfile -t IP_ADDRESSES < <( - for ip_name in "${IP_NAMES[@]}"; do - gcloud compute addresses describe "$ip_name" \ - --region ${{ vars.GCP_REGION }} \ - --format='value(address)' 2>/dev/null || echo "" - done - ) - - mapfile -t INSTANCES < <( - gcloud compute instance-groups managed list-instances "${MIG_NAME}" \ - --region "${{ vars.GCP_REGION }}" \ - --format="value(instance.basename())" | sort - ) - - # Assign IPs via stateful instance config (creates config + assigns IP in one command) - for i in "${!INSTANCES[@]}"; do - [ -z "${IP_ADDRESSES[$i]}" ] && continue - - echo "Assigning ${IP_ADDRESSES[$i]} to ${INSTANCES[$i]}" - gcloud compute instance-groups managed instance-configs create "${MIG_NAME}" \ - --instance="${INSTANCES[$i]}" \ - --stateful-external-ip="address=${IP_ADDRESSES[$i]},interface-name=nic0,auto-delete=never" \ - --region "${{ vars.GCP_REGION }}" + if [ -z "${IP_ADDRESS:-}" ]; then + echo "::warning::${IP_NAME:-zebra-${NETWORK}} not reserved; skipping" + exit 0 + fi + for _ in $(seq 1 30); do + INSTANCE=$(gcloud compute instance-groups managed list-instances "${MIG_NAME}" \ + --zone="${ZONE}" --format='value(instance.basename())' | head -1) + [ -n "${INSTANCE}" ] && break + sleep 2 done - - # Detect how many zones the MIG spans (needed for max-unavailable constraint) - - name: Get zone count for MIG + [ -z "${INSTANCE}" ] && { echo "::error::instance did not appear within 60s"; exit 1; } + gcloud compute instance-groups managed instance-configs create "${MIG_NAME}" \ + --instance="${INSTANCE}" --zone="${ZONE}" \ + --stateful-external-ip="address=${IP_ADDRESS},interface-name=nic0,auto-delete=never" + + # Rolling update waits only for the new template to start rolling out; + # full health convergence is the verify-nodes job's concern. + - name: Rolling update on existing MIG if: steps.does-group-exist.outcome == 'success' - id: zone-count run: | - ZONE_COUNT=$(gcloud compute instance-groups managed describe "${MIG_NAME}" \ - --region "${{ vars.GCP_REGION }}" \ - --format="value(distributionPolicy.zones.len())") - echo "count=${ZONE_COUNT}" >> $GITHUB_OUTPUT - echo "MIG spans ${ZONE_COUNT} zones" - - # Rolling update (RECREATE method requires max-surge=0, max-unavailable >= zone count) - - name: Update managed instance group for ${{ matrix.network }} - if: steps.does-group-exist.outcome == 'success' - run: | - gcloud compute instance-groups managed rolling-action start-update \ - "${MIG_NAME}" \ - --version template="${TEMPLATE_NAME}" \ - --replacement-method=recreate \ - --max-surge=0 \ - --max-unavailable=${{ steps.zone-count.outputs.count }} \ - --region "${{ vars.GCP_REGION }}" - - # Re-assign static IPs after rolling update (instances are recreated without external IPs) - - name: Re-assign static IPs after rolling update - if: ${{ steps.does-group-exist.outcome == 'success' && github.event_name != 'workflow_dispatch' }} + gcloud compute instance-groups managed rolling-action start-update "${MIG_NAME}" \ + --version=template="${TEMPLATE_NAME}" \ + --replacement-method=recreate \ + --max-surge=0 --max-unavailable=1 \ + --zone="${ZONE}" + + # Waits for each zonal MIG to reach HEALTHY (app-level: peer mesh + chain + # tip). Runs async from deploy-nodes. Skipped for workflow_dispatch. + verify-nodes: + name: Verify ${{ matrix.network }} ${{ matrix.zone }} + strategy: + fail-fast: false + matrix: + network: ${{ fromJSON(needs.set-matrix.outputs.networks) }} + zone: ${{ fromJSON(needs.set-matrix.outputs.zones) }} + needs: [set-matrix, deploy-nodes] + runs-on: ubuntu-latest + timeout-minutes: 120 + environment: ${{ needs.set-matrix.outputs.environment }} + permissions: + contents: read + id-token: write + if: >- + ${{ + !cancelled() && !failure() && + needs.deploy-nodes.result == 'success' && + github.event_name != 'workflow_dispatch' && + github.repository_owner == 'ZcashFoundation' + }} + steps: + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 #v3.0.0 + with: + workload_identity_provider: "${{ vars.GCP_WIF }}" + service_account: "${{ vars.GCP_DEPLOYMENTS_SA }}" + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db #v3.0.1 + + - name: Wait for MIG stable run: | - # Wait for rolling update to complete + ZONE="${{ matrix.zone }}" + NET_CAPS="${{ matrix.network }}" + NETWORK="${NET_CAPS,,}" + ZONE_LETTER="${ZONE##*-}" + case "${{ github.event_name }}" in + release) PREFIX="" ;; + push) PREFIX="main-" ;; + *) echo "::error::unsupported event"; exit 1 ;; + esac + MIG_NAME="zebrad-${PREFIX}${NETWORK}-${ZONE_LETTER}" gcloud compute instance-groups managed wait-until "${MIG_NAME}" \ - --stable \ - --region "${{ vars.GCP_REGION }}" \ - --timeout=1200 - - # Get static IPs and instances - IP_NAMES=("zebra-${NETWORK}" "zebra-${NETWORK}-secondary" "zebra-${NETWORK}-tertiary") - mapfile -t IP_ADDRESSES < <( - for ip_name in "${IP_NAMES[@]}"; do - gcloud compute addresses describe "$ip_name" \ - --region ${{ vars.GCP_REGION }} \ - --format='value(address)' 2>/dev/null || echo "" - done - ) - - mapfile -t INSTANCES < <( - gcloud compute instance-groups managed list-instances "${MIG_NAME}" \ - --region "${{ vars.GCP_REGION }}" \ - --format="value(instance.basename())" | sort - ) - - # Assign IPs via stateful instance config (creates config + assigns IP in one command) - for i in "${!INSTANCES[@]}"; do - [ -z "${IP_ADDRESSES[$i]}" ] && continue - - echo "Assigning ${IP_ADDRESSES[$i]} to ${INSTANCES[$i]}" - gcloud compute instance-groups managed instance-configs create "${MIG_NAME}" \ - --instance="${INSTANCES[$i]}" \ - --stateful-external-ip="address=${IP_ADDRESSES[$i]},interface-name=nic0,auto-delete=never" \ - --region "${{ vars.GCP_REGION }}" \ - --update-instance 2>/dev/null || \ - gcloud compute instance-groups managed instance-configs update "${MIG_NAME}" \ - --instance="${INSTANCES[$i]}" \ - --stateful-external-ip="address=${IP_ADDRESSES[$i]},interface-name=nic0,auto-delete=never" \ - --region "${{ vars.GCP_REGION }}" - done + --stable --zone="${ZONE}" --timeout=5400 deploy-nodes-success: name: Deploy nodes success @@ -528,29 +559,26 @@ jobs: always() && needs.deploy-nodes.result != 'skipped' }} - needs: - - versioning - - get-disk-name - - build - - set-matrix - - deploy-nodes + needs: [set-matrix, get-disk-name-mainnet, get-disk-name-testnet, ensure-health-checks, build, deploy-nodes] timeout-minutes: 1 steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 + - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe #v1.2.2 with: jobs: ${{ toJSON(needs) }} - allowed-skips: versioning, get-disk-name, build + allowed-skips: get-disk-name-mainnet, get-disk-name-testnet, build failure-issue: - name: Open or update issues for release failures + name: Open or update issues for deploy failures # When a new job is added to this workflow, add it to this list. - needs: [versioning, build, deploy-nodes] + needs: [build, deploy-nodes] # Only open tickets for failed or cancelled jobs that are not coming from PRs. # (PR statuses are already reported in the PR jobs list, and checked by GitHub's Merge Queue.) if: (failure() && github.event.pull_request == null) || (cancelled() && github.event.pull_request == null) runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: jayqi/failed-build-issue-action@1a893bbf43ef1c2a8705e2b115cd4f0fe3c5649b #v1.2.0 with: @@ -560,3 +588,23 @@ jobs: # If there is already an open issue with this label, any failures become comments on that issue. always-create-new-issue: false github-token: ${{ secrets.GITHUB_TOKEN }} + + verify-failure-issue: + name: Open or update issues for verify failures + needs: [verify-nodes] + # Deploy succeeded but the node did not reach HEALTHY within the verify + # window. Separate from `failure-issue` so on-call can distinguish an + # infrastructure problem (deploy-nodes) from a node-level warmup/sync + # problem (verify-nodes). + if: (failure() && github.event.pull_request == null) || (cancelled() && github.event.pull_request == null) + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: jayqi/failed-build-issue-action@1a893bbf43ef1c2a8705e2b115cd4f0fe3c5649b #v1.2.0 + with: + title-template: "{{refname}} verify failed: {{eventName}} in {{workflow}}" + label-name: S-ci-fail-verify-auto-issue + always-create-new-issue: false + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/zfnd-find-cached-disks.yml b/.github/workflows/zfnd-find-cached-disks.yml index d7cf9f44e09..c1afe90f791 100644 --- a/.github/workflows/zfnd-find-cached-disks.yml +++ b/.github/workflows/zfnd-find-cached-disks.yml @@ -24,6 +24,11 @@ on: description: The test ID requiring the cached state disks required: false type: string + environment: + description: The environment used for GitHub-scoped variables and labels + required: false + type: string + default: '' outputs: state_version: description: The version of the cached state disks @@ -48,7 +53,7 @@ jobs: get-cached-disks: name: Get ${{ inputs.test_id || inputs.network }} cached disk runs-on: ubuntu-latest - environment: ${{ github.event_name == 'release' && 'prod' || 'dev' }} + environment: ${{ inputs.environment || (github.event_name == 'release' && 'prod' || github.event_name == 'pull_request' && 'dev' || 'stage') }} outputs: state_version: ${{ steps.get-available-disks.outputs.state_version || steps.set-release-defaults.outputs.state_version }} cached_disk_name: ${{ steps.get-available-disks.outputs.cached_disk_name || steps.set-release-defaults.outputs.cached_disk_name }} @@ -64,7 +69,7 @@ jobs: persist-credentials: false fetch-depth: 0 - name: Inject slug/short variables - uses: rlespinasse/github-slug-action@9e7def61550737ba68c62d34a32dd31792e3f429 #v5.5.0 + uses: rlespinasse/github-slug-action@e6f261660910b273384c5c42b17a0217881b217a #v5.6.0 with: short-length: 7 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index a93c61cb07f..6a92d9d4738 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -22,4 +22,4 @@ jobs: persist-credentials: false - name: Run zizmor 🌈 - uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 diff --git a/.gitignore b/.gitignore index c23350beb25..a17d13ce430 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,8 @@ zebra-rpc/qa/cache/ # Lychee link checker cache .lycheecache /.agent-shell/ + +# fuzzing artifacts +zebra-chain/fuzz/artifacts +zebra-chain/fuzz/corpus + diff --git a/.lychee.toml b/.lychee.toml index 13056698289..16233bb9d07 100644 --- a/.lychee.toml +++ b/.lychee.toml @@ -25,6 +25,8 @@ exclude = [ "^https://github.com/.*/pull/0000", # Mergify dashboard (requires auth) "^https://dashboard.mergify.com/", + # IACR eprint server returns 403 to automated link checkers + "^https://eprint\\.iacr\\.org/", # Dead upstream links in historical audit document (zebra-dependencies-for-audit.md) "^https://github.com/iqlusioninc/abscissa/tree/develop", @@ -44,7 +46,7 @@ exclude_path = [ ] # Accept these HTTP status codes as valid -accept = ["200", "204", "206", "301", "302", "307", "308"] +accept = ["200", "204", "206", "301", "302", "307", "308", "429"] # Timeout for requests (seconds) timeout = 30 @@ -57,10 +59,10 @@ exclude_loopback = true cache = true # Maximum age of cached results -max_cache_age = "1d" +max_cache_age = "7d" # Maximum concurrent requests -max_concurrency = 16 +max_concurrency = 8 # User agent string user_agent = "lychee/0.14 (Zebra link checker; https://github.com/ZcashFoundation/zebra)" @@ -70,8 +72,4 @@ include_mail = false # Retry configuration max_retries = 3 -retry_wait_time = 2 - -# GitHub token environment variable (for higher rate limits) -# Set GITHUB_TOKEN in your environment or CI -github_token = "" +retry_wait_time = 10 diff --git a/AGENTS.md b/AGENTS.md index ff32f55c617..7dc674a63c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,7 +114,7 @@ cargo nextest run --profile sync-large-checkpoints-empty Zebra is a Zcash full node implementation in Rust. It is a validator node — it excludes features not strictly needed for block validation and chain sync. - **Rust edition**: 2021 -- **MSRV**: 1.85 +- **MSRV**: 1.85.1 (libraries), 1.91 (zebrad binary) - **Database format version**: defined in `zebra-state/src/constants.rs` ## Crate Architecture diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d9a3414cf..3dfe8f22386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,357 @@ and this project adheres to [Semantic Versioning](https://semver.org). ## [Unreleased] +## [Zebra 5.2.0](https://github.com/ZcashFoundation/zebra/releases/tag/v5.2.0) - 2026-06-18 + +This release increases Zebra's local rollback window as a defence-in-depth measure +against sustained consensus splits. + +### Changed + +- Increased Zebra's local rollback window (`MAX_BLOCK_REORG_HEIGHT`) from 99 to + 1000 blocks as a defence-in-depth measure against sustained consensus splits + ([#10650](https://github.com/ZcashFoundation/zebra/pull/10650)) + +## [Zebra 5.1.1](https://github.com/ZcashFoundation/zebra/releases/tag/v5.1.1) - 2026-06-11 + +This release reduces Zebra's end-of-support window ahead of the NU7 network upgrade +expected at the end of July 2026, so that nodes running outdated versions stop before +the upgrade activates. + +### Changed + +- Reduced the end-of-support period from 105 to 44 days, and updated the estimated + release height, ahead of the NU7 network upgrade + ([#10710](https://github.com/ZcashFoundation/zebra/pull/10710)) + +## [Zebra 5.1.0](https://github.com/ZcashFoundation/zebra/releases/tag/v5.1.0) - 2026-06-10 + +This release fixes a genesis-to-tip sync stall that could cause new nodes to hang +during initial block download, bumps the minimum network protocol version to NU6.2, +extends the `getpeerinfo` RPC, and includes several performance and correctness fixes. + +### Breaking Changes + +- The minimum network protocol version is now NU6.2 (170150). Peers running protocol + versions below NU6.2 will be disconnected. Update to Zebra 5.0.0 or later to remain + compatible ([#10692](https://github.com/ZcashFoundation/zebra/pull/10692)). + +### Added + +- Extended `getpeerinfo` RPC with `subver`, `version`, `services`, `lastrecv`, + `banscore`, and `connection_state` fields ([#10443](https://github.com/ZcashFoundation/zebra/pull/10443)) + +### Fixed + +- Fixed genesis-to-tip sync stall that could cause new nodes to hang during initial + block download ([#10679](https://github.com/ZcashFoundation/zebra/pull/10679)) +- Fixed mempool index being unnecessarily rebuilt per transaction in `getrawmempool` + ([#10599](https://github.com/ZcashFoundation/zebra/pull/10599)) +- Fixed `dequeue_children` by-height index handling in the state service + ([#10604](https://github.com/ZcashFoundation/zebra/pull/10604)) + +### Contributors + +Thank you to everyone who contributed to this release: +@andres-pcg, @conradoplg, @gustavovalverde, @judah-caruso, @oxarbitrage, @syszery and @upbqdn + +## [Zebra 5.0.0](https://github.com/ZcashFoundation/zebra/releases/tag/v5.0.0) - 2026-06-02 + +This release activates the NU6.2 network upgrade. NU6.2 re-enables Orchard +actions (temporarily disabled by the 4.5.3 soft fork) using the fixed Orchard +Action circuit, which fixes a **critical** bug in the Orchard pool. NU6.2 +activates at block height 3,364,600 on Mainnet and 4,052,000 on Testnet. We +recommend node operators update before the activation height. + +If the activation height has passed and your node followed a fork, you will need +to sync from scratch. If you have a backed-up state before the activation +height, you can sync from that. + +### Added + +- Activate the NU6.2 network upgrade (consensus branch id `0x5437f330`) at height 3,364,600 + on Mainnet and 4,052,000 on Testnet. NU6.2 re-enables Orchard actions with the fixed + Orchard Action circuit and routes Orchard proofs to a per-circuit verifying key + (`InsecurePreNu6_2` / `FixedPostNu6_2`). +- Advertise network protocol version 170150 for NU6.2 on Mainnet, Testnet, and Regtest. + +### Changed + +- Set the default Testnet temporary Orchard-disabling soft-fork height to 4,048,500; the + disable window runs until NU6.2 re-enables Orchard actions at height 4,052,000. + +### Security + +- Add a consensus rule that rejects Orchard bundles whose proof has a non-canonical size, + effective from the NU6.2 network upgrade (GHSA-jfw5-j458-pfv6). + +## [Zebra 4.5.3](https://github.com/ZcashFoundation/zebra/releases/tag/v4.5.3) - 2026-06-01 + +This hotfix release adds a soft fork that temporarily disables Orchard actions in +transactions, to mitigate a security issue. We recommend node operators update to +4.5.3 as soon as possible. + +### Security + +- Add a soft fork that temporarily rejects transactions containing Orchard actions. + It activates at a fixed height on Mainnet and at a configurable height on Testnet + (`temporary_orchard_disabling_soft_fork_height` in the network config), and the + mempool is revalidated at the activation height to drop any Orchard transactions + accepted beforehand + ([GHSA-jfw5-j458-pfv6](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jfw5-j458-pfv6)). + +## [Zebra 4.5.1](https://github.com/ZcashFoundation/zebra/releases/tag/v4.5.0) - 2026-05-29 + +This hotfix release fixes a critical security issues that was not correctly +addressed in 4.5.0. We recommend node operators update to 4.5.1 as soon as +possible. + +### Security + +- The last P2SH input sigop count fix was incorrect. Properly fixed it to match zcashd + ([GHSA-2prc-cj5x-4443](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-2prc-cj5x-4443)). + Thanks to @sangsoo-osec for reporting the issue. + +## [Zebra 4.5.0](https://github.com/ZcashFoundation/zebra/releases/tag/v4.5.0) - 2026-05-28 + +This release fixes several critical security issues. We recommend node operators update to +4.5.0 as soon as possible. + +The release also adds support for mining to a shielded address. + +### Security + +- Remove rejected block hashes from `SentHashes` so honest re-deliveries are + not short-circuited as duplicates + ([GHSA-4m69-67m6-prqp](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-4m69-67m6-prqp)). + Thanks to @ipwning and @x15-eth for reporting the issue. +- Cap the upfront `Vec::with_capacity` reservation in + `zcash_deserialize_external_count` so a peer-supplied `CompactSize` + cannot force a large allocation before any element bytes are read. The + `Vec` grows naturally via `push()` as real data arrives. Complements + the per-type `max_allocation()` caps from PR #10494 + ([GHSA-xr93-pcq3-pxf8](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-xr93-pcq3-pxf8), + [#10563](https://github.com/ZcashFoundation/zebra/pull/10563)). CWE-770. +- Cap `block::Hash::max_allocation` at `MAX_BLOCK_LOCATOR_LENGTH = 101` + (matching Bitcoin Core's `MAX_LOCATOR_SZ` in `net_processing.cpp`) and + `CountedHeader::max_allocation` at the existing + `MAX_HEADERS_PER_MESSAGE = 160` constant (already enforced on the + sending side and at the codec level for `read_headers`). The previous + values were derived from `MAX_PROTOCOL_MESSAGE_LEN` and returned 65,535 + and ~1,409 respectively, allowing a post-handshake peer to force ~2 MiB + of upfront `Vec` preallocation per `getblocks`/`getheaders` message + before any payload bytes were read. Same fix shape as + GHSA-xr93-pcq3-pxf8 for `AddrV1`/`AddrV2` (PR #10494) + ([#10570](https://github.com/ZcashFoundation/zebra/pull/10570)). CWE-770. +- Reject non-ASCII `longpollid` values in the `getblocktemplate` RPC + ([GHSA-qv2r-v3mx-f4pf](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-qv2r-v3mx-f4pf)). + Thanks to @sangsoo-osec for reporting the issue. +- Return error for malformed Sapling receiver in `z_listunifiedreceivers` RPC + ([GHSA-c8w6-x74f-vmg3](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-c8w6-x74f-vmg3)). + Thanks to @robustfengbin for reporting the issue. +- Prevent sync restart from poisoning the peer inventory registry, and score + peers that send consensus-invalid blocks + ([GHSA-gvjc-3w7c-92jx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gvjc-3w7c-92jx), + [GHSA-rj6c-83wx-jxf2](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-rj6c-83wx-jxf2), + [GHSA-hwxr-r2v4-9f2p](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-hwxr-r2v4-9f2p)). + Thanks to @ipwning and @sangsoo-osec for reporting the issues. +- Avoid panic in address-balance computation on same-address self-spend chains + by applying transparent debits before credits per transaction + ([GHSA-w834-cf6p-9m9w](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-w834-cf6p-9m9w)). + Thanks to @sangsoo-osec for reporting the issue. +- Reject repeated shielded transactions cleanly before the defence-in-depth + `tx_loc_by_hash` assertion, avoiding a panic + ([GHSA-hhm7-qrv5-h4r6](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-hhm7-qrv5-h4r6)). + Thanks to @Haxatron for reporting the issue. +- Cap pre-handshake message body length in `Codec` to 1 KB; the cap is raised + to `MAX_PROTOCOL_MESSAGE_LEN` after handshake completion + ([GHSA-h72h-ppcx-998p](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-h72h-ppcx-998p)). + Thanks to @ouicate for reporting the issue. +- Fix sigop undercount in P2SH inputs by routing through the + `legacy_sigop_count_script` FFI to match `zcashd`'s + `CScript::GetSigOpCount(true)` exactly, preventing a consensus split on + attacker-chosen redeem scripts + ([GHSA-gf9r-m956-97qx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gf9r-m956-97qx)). + Thanks to @samsulselfut for reporting the issue. +- Cap the inbound mempool download queue per advertising peer so a single + peer cannot monopolize verification capacity + ([GHSA-4fc2-h7jh-287c](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-4fc2-h7jh-287c)). + Thanks to @dingledropper for reporting the issue. +- Canonicalize IPv4-mapped addresses on the misbehavior path so a peer cannot + evade scoring by alternating between IPv4 and IPv4-mapped-IPv6 forms of the + same address + ([GHSA-63wg-wjjj-7cp8](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-63wg-wjjj-7cp8)). + Thanks to @Haxatron for reporting the issue. +- Drain the mempool downloader's `cancel_handles` entry when the outer + verification timeout fires, so the queued `Gossip::Tx(UnminedTx)` is not + retained until the process runs out of memory. Without the fix, a single peer + that gets each pushed transaction to hit `RATE_LIMIT_DELAY` could leak up to + ~2 MB per transaction monotonically + ([GHSA-65jj-fmw8-468q](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-65jj-fmw8-468q)). + Thanks to @AnticsDecoded for reporting the issue. +- Pop Sapling/Orchard subtrees when popping non-finalized tip + ([GHSA-2gf8-q9rr-jq3h](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-2gf8-q9rr-jq3h)). + Thanks to @dingledropper for reporting the issue. + +### Added + +- Startup warning on Linux when `net.ipv4.tcp_slow_start_after_idle` is enabled (which resets TCP congestion windows between block requests and significantly reduces single-peer block-propagation throughput on long-haul links), with a "Linux TCP tuning for block propagation" troubleshooting section ([#10513](https://github.com/ZcashFoundation/zebra/pull/10513)) +- Support for ZIP-213 (Shielded Coinbase) ([#10048](https://github.com/ZcashFoundation/zebra/pull/10048)) + +### Changed + +- Renamed `testnet_parameters` in the network config; use `[network.params]` + instead ([#10051](https://github.com/ZcashFoundation/zebra/pull/10051)). The + old format is still accepted. + +### Fixed + +- Avoid panicking in the address-book ban path when `network.max_connections_per_ip > 1`. Guard the optional `most_recent_by_ip` cache instead of unwrapping it, so a ban-threshold misbehavior update no longer crashes the address-book updater and poisons the shared mutex ([#10589](https://github.com/ZcashFoundation/zebra/pull/10589)) +- Propagate transaction-level value-balance errors from `Block::chain_value_pool_change()` instead of silently dropping them. The previous `flat_map(Result)` aggregation relied on `Result: IntoIterator` and yielded zero items on `Err`, so a failing transaction was omitted from the block sum rather than surfacing as a `ValueBalanceError` ([#10590](https://github.com/ZcashFoundation/zebra/pull/10590)) +- Solve Rust 1.97 beta clippy lints ([#10644](https://github.com/ZcashFoundation/zebra/pull/10644)) + +### Contributors + +Thank you to everyone who contributed to this release, we couldn't make Zebra without you: +@ValarDragon, @andres-pcg, @conradoplg, @dingledropper, @evan-forbes, @gustavovalverde, @oxarbitrage, @syszery, @upbqdn, @zmanian. + +## [Zebra 4.4.1](https://github.com/ZcashFoundation/zebra/releases/tag/v4.4.1) - 2026-05-04 + +This release fixes one critical security issue. We recommend node operators update to +4.4.1. + +### Security + +- Reject V5 transparent inputs signed with `SIGHASH_SINGLE` (or + `SIGHASH_SINGLE|ANYONECANPAY`) when the input has no transparent output at the + same index + ([GHSA-pvmv-cwg8-v6c8](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-pvmv-cwg8-v6c8)). + Follow-up to + [GHSA-cwfq-rfcr-8hmp](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-cwfq-rfcr-8hmp). + +Thanks to @sangsoo-osec, @zmanian, and @fivelittleducks for reporting the issue. + +## [Zebra 4.4.0](https://github.com/ZcashFoundation/zebra/releases/tag/v4.4.0) - 2026-05-01 + +This release includes several security and bug fixes. We recommend node +operators update to 4.4.0. + +### Security + +- Fix sigops counting ([GHSA-jv4h-j224-23cc](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jv4h-j224-23cc)). +- Consensus-divergence defense-in-depth follow-up to GHSA-8m29-fpq5-89jj: + the V5 sighash callback now substitutes a per-call CSPRNG-derived sighash + when rejecting undefined ZIP 244 hash-type bytes, so any peer-supplied + signature fails to verify and the block is rejected in agreement with + `zcashd` ([GHSA-gq4h-3grw-2rhv](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gq4h-3grw-2rhv), [#10524](https://github.com/ZcashFoundation/zebra/pull/10524)). +- Allocation amplification in inbound network deserializers: validate + coinbase Sapling spend count, coinbase data size, and Equihash solution + size before allocating, and enforce the 160-entry cap in `read_headers` + ([GHSA-438q-jx8f-cccv](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-438q-jx8f-cccv), + [#10525](https://github.com/ZcashFoundation/zebra/pull/10525), + [#10526](https://github.com/ZcashFoundation/zebra/pull/10526), + [#10527](https://github.com/ZcashFoundation/zebra/pull/10527), + [#10528](https://github.com/ZcashFoundation/zebra/pull/10528)). +- Validate transparent input/output alignment before script verification, so + malformed verifier requests return an error instead of panicking or + verifying against a misaligned previous output ([#10510](https://github.com/ZcashFoundation/zebra/pull/10510)). +- RPC hardening ([#10523](https://github.com/ZcashFoundation/zebra/pull/10523)): + - Cookie file is now written with explicit `0600` permissions on Unix; symlinks at the cookie path are rejected ([GHSA-jg86-rwhm-fhg4](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jg86-rwhm-fhg4)). + - HTTP request bodies are bounded before allocation, with the limit derived from `MAX_BLOCK_BYTES` to accommodate `submitblock` ([GHSA-8r29-5wjm-jgvx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8r29-5wjm-jgvx)). + - gRPC indexer streams use `try_send` to drop slow subscribers instead of backpressuring the server; the buffer was reduced from 4000 to 64. Well-behaved clients are unaffected ([GHSA-826r-gfq8-x79q](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-826r-gfq8-x79q)). + - `getrawtransaction` reuses the caller-provided block hash and best-chain flag from the initial query, fixing a TOCTOU race against a third state lookup ([GHSA-w23c-6rpp-ff87](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-w23c-6rpp-ff87)). +- Disconnect peers that persistently return empty or bad `FindBlocks` / + `FindHeaders` responses, freeing fanout slots for useful peers. Stall + classification happens inside `PeerSet::route_p2c`, so coverage spans both + request types without a separate per-IP guard. +- Prevent sync restart from poisoning the peer inventory registry ([GHSA-gvjc-3w7c-92jx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gvjc-3w7c-92jx)). +- Score and penalize peers that send consensus-invalid blocks ([GHSA-rj6c-83wx-jxf2](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-rj6c-83wx-jxf2), [GHSA-hwxr-r2v4-9f2p](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-hwxr-r2v4-9f2p)). +- Cap pre-handshake message body length in Codec to 1 KB ([GHSA-h72h-ppcx-998p](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-h72h-ppcx-998p)). + +### Added + +- `nTx` (per-block transaction count) field in the verbose `getblock` RPC response ([#10498](https://github.com/ZcashFoundation/zebra/pull/10498)). +- Public benchmark dashboard at [zebra.zfnd.org/dev/bench](https://zebra.zfnd.org/dev/bench) covering Groth16, Halo2, Sapling, RedPallas, block, and transaction benchmarks ([#10444](https://github.com/ZcashFoundation/zebra/pull/10444)). + +### Changed + +- Refreshed the Sentry/OpenTelemetry observability stack ([#10490](https://github.com/ZcashFoundation/zebra/pull/10490)): Sentry SDK upgraded to `0.47` (transport switched from `reqwest` to `ureq`); Sentry events now carry `SENTRY_ENVIRONMENT`, `git.ref`, `git.sha`, and CI context (`CI_PR_NUMBER`, `CI_TEST_ID`, `GITHUB_*`) when present; `opentelemetry` is now part of the `default-release-binaries` feature set, with export still gated on `OTEL_EXPORTER_OTLP_ENDPOINT` (or the tracing config); and `zebrad::sentry` is now crate-private. +- Upgraded the librustzcash crate cohort (`equihash` 0.3, `orchard` 0.13, `sapling-crypto` 0.7, `zcash_address` 0.11, `zcash_encoding` 0.4, `zcash_keys` 0.13, `zcash_primitives` 0.27, `zcash_proofs` 0.27, `zcash_protocol` 0.8, `zcash_transparent` 0.7) to the 2026-04 release wave, which migrates off the yanked `core2` crate to `corez 0.1.1` and clears RUSTSEC-2026-0105 ([#10522](https://github.com/ZcashFoundation/zebra/pull/10522)). +- Bumped workspace MSRV from 1.85.0 to 1.85.1, required by the new librustzcash releases. Also bumped `zebrad` MSRV from 1.89 to 1.91, required by `cargo-platform 0.3.3` (transitively via `vergen-git2`). +- Upgraded `ed25519-zebra` from 4.0 to 4.2 +- Upgraded `console-subscriber` from 0.4 to 0.5 +- Upgraded `toml` from 0.9 to 1.0 +- Upgraded `opentelemetry` from 0.28 to 0.31, `opentelemetry_sdk` from 0.28 to 0.31, `opentelemetry-otlp` from 0.28 to 0.31, `tracing-opentelemetry` from 0.29 to 0.32 + +### Fixed + +- `getrawtransaction` now reports correct `confirmations` ([#10507](https://github.com/ZcashFoundation/zebra/pull/10507)). + +## [Zebra 4.3.1](https://github.com/ZcashFoundation/zebra/releases/tag/v4.3.1) - 2026-04-17 + +This release fixes **four important security issues**: + +- [CVE-2026-40880: Cached Mempool Verification Bypasses Consensus Rules for Ahead-of-Tip Blocks](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-xvj8-ph7x-65gf) +- [CVE-2026-XXXXX: Consensus Divergence in Transparent Sighash Hash-Type Handling](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8m29-fpq5-89jj) +- [CVE-2026-XXXXX: rk Identity Point Panic in Transaction Verification](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-452v-w3gx-72wg) +- [CVE-2026-40881: addr/addrv2 Deserialization Resource Exhaustion](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-xr93-pcq3-pxf8) + +We recommend node operators to update to 4.3.1 as soon as possible. All previous +Zebra versions are vulnerable to these issues. + +### Added + +- Dockerized mining setup ([#10301](https://github.com/ZcashFoundation/zebra/pull/10301)) + +### Fixed + +- Fixed [a panic that could be triggered in the RPC interface on HTTP + errors](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-29x4-r6jv-ff4w), + such as resetting the connection halfway through a request. We do not consider + this a critical issue since the RPC port is security-sensitive and should not + be opened publicly, but we plan to update our documentation to make this + clear. + +### Changed + +- The Dockerfile and docker-compose.yml were changed to expose the P2P port by + default. This is important for the network since it allows other peers to + connect to the node. Note that if you deploy Zebra behind a firewall or NAT + you might require additional configuration + ([#10464](https://github.com/ZcashFoundation/zebra/pull/10464)). + +## [Zebra 4.3.0](https://github.com/ZcashFoundation/zebra/releases/tag/v4.3.0) - 2026-03-12 + +This release fixes **two important security issues**: + +- [CVE-2026-34202: Remote Denial of Service via Crafted V5 Transactions](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-qp6f-w4r3-h8wg) +- [CVE-2026-34377: Consensus Failure via Crafted V5 Authorization Data](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-3vmh-33xr-9cqh) + +We recommend node operators to update to 4.3.0 as soon as possible. All previous +Zebra versions are vulnerable to these issues. + +This release adds support for [ZIP-235](https://zips.z.cash/zip-0235) and +extends the documentation on performance profiling. It also fixes issues with +block propagation on Regtest, the computation of miner rewards for pre-Canopy +blocks in the `getblocksubsidy` RPC, and a performance regression on Testnet +where Zebra would hog a single CPU thread. + +### Added + +- Network Sustainability Mechanism: ZIP-235 ([#10357](https://github.com/ZcashFoundation/zebra/pull/10357)) +- Add `profiling` Cargo profile and use it in profiling docs ([#10411](https://github.com/ZcashFoundation/zebra/pull/10411)) + +### Fixed + +- Fix block propagation on Regtest ([#10403](https://github.com/ZcashFoundation/zebra/pull/10403)) +- Subtract Founders' Reward from block subsidy ([#10338](https://github.com/ZcashFoundation/zebra/pull/10338)) +- Cache parsed checkpoints ([#10409](https://github.com/ZcashFoundation/zebra/pull/10409)) + +### Contributors + +Thank you to everyone who contributed to this release, we couldn't make Zebra without you: +@gustavovalverde, @judah-caruso, @nuttycom, @oxarbitrage and @upbqdn. + ## [Zebra 4.2.0](https://github.com/ZcashFoundation/zebra/releases/tag/v4.2.0) - 2026-03-12 This release expands Zebra’s RPC functionality, improves mempool policy, @@ -24,6 +375,10 @@ documentation with OpenRPC. - `AnyChainBlock` state request for querying blocks in side chains ([#10325](https://github.com/ZcashFoundation/zebra/pull/10325)) - Remaining non-standard mempool transaction filters ([#10314](https://github.com/ZcashFoundation/zebra/pull/10314)) +### Fixed + +- Subtract Founders' Reward from block subsidy ([#10338](https://github.com/ZcashFoundation/zebra/pull/10338)) + ### Removed - Python QA RPC test framework (`zebra-rpc/qa/`) in favour of the new [integration-tests](https://github.com/zcash/integration-tests) project, which captures all previous work in [zcash/integration-tests#1](https://github.com/zcash/integration-tests/pull/1) ([#10363](https://github.com/ZcashFoundation/zebra/pull/10363)) diff --git a/Cargo.lock b/Cargo.lock index 28bf2828a34..a9ad5a984d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,7 +12,7 @@ dependencies = [ "arc-swap", "backtrace", "canonical-path", - "clap 4.5.60", + "clap 4.6.1", "color-eyre", "fs-err", "once_cell", @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -146,15 +146,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -187,9 +187,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -214,9 +214,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -224,28 +224,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -281,46 +259,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "axum" -version = "0.7.9" +name = "aws-lc-rs" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", ] [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "axum-core 0.5.6", + "axum-core", "bytes", "futures-util", "http", "http-body", "http-body-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -332,26 +305,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-core" version = "0.5.6" @@ -445,7 +398,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.12.1", @@ -465,14 +418,14 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "shlex", "syn 2.0.117", ] @@ -516,9 +469,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] @@ -529,7 +482,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b64e60c28b6d25ad92e8b367801ff9aa12b41d05fc8798055d296bace4a60cc" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "serde", ] @@ -684,9 +637,9 @@ checksum = "e6e9e01327e6c86e92ec72b1c798d4a94810f147209bbe3ffab6a86954937a6f" [[package]] name = "cargo-platform" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", "serde_core", @@ -723,9 +676,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -856,9 +809,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -866,9 +819,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -878,9 +831,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -890,9 +843,18 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] [[package]] name = "color-eyre" @@ -924,15 +886,25 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -941,15 +913,15 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "config" -version = "0.15.19" +version = "0.15.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" dependencies = [ "async-trait", "convert_case 0.6.0", @@ -960,7 +932,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "winnow", "yaml-rust2", ] @@ -978,24 +950,36 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "console-api" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +checksum = "e8599749b6667e2f0c910c1d0dff6901163ff698a52d5a39720f61b5be4b20d3" dependencies = [ "futures-core", - "prost 0.13.5", - "prost-types 0.13.5", - "tonic 0.12.3", + "prost", + "prost-types", + "tonic", + "tonic-prost", "tracing-core", ] [[package]] name = "console-subscriber" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +checksum = "fb4915b7d8dd960457a1b6c380114c2944f728e7c65294ab247ae6b6f1f37592" dependencies = [ "console-api", "crossbeam-channel", @@ -1004,14 +988,14 @@ dependencies = [ "hdrhistogram", "humantime", "hyper-util", - "prost 0.13.5", - "prost-types 0.13.5", + "prost", + "prost-types", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic 0.12.3", + "tonic", "tracing", "tracing-core", "tracing-subscriber", @@ -1045,11 +1029,12 @@ dependencies = [ [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -1093,6 +1078,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1100,13 +1095,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "core2" -version = "0.3.3" +name = "corez" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" -dependencies = [ - "memchr", -] +checksum = "4df6f98652d30167eaeea34d77b730e07c8caba6df17bd4551842b9b8da01deb" [[package]] name = "cpufeatures" @@ -1135,7 +1127,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.60", + "clap 4.6.1", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1451,7 +1443,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", ] @@ -1510,6 +1502,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1529,13 +1527,13 @@ dependencies = [ [[package]] name = "ed25519-zebra" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0017d969298eec91e3db7a2985a8cab4df6341d86e6f3a6f5878b13fb7846bc9" +checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" dependencies = [ "curve25519-dalek", "ed25519", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "pkcs8", "rand_core 0.6.4", "serde", @@ -1562,7 +1560,7 @@ dependencies = [ "flate2", "lazy_static", "percent-encoding", - "reqwest", + "reqwest 0.12.28", "rustc_version", "serde", "serde_json", @@ -1599,12 +1597,12 @@ dependencies = [ [[package]] name = "equihash" -version = "0.2.2" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.3.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "blake2b_simd", "cc", - "core2", + "corez", "document-features", ] @@ -1648,16 +1646,16 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.1" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "blake2b_simd", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -1689,7 +1687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand 0.8.6", "rustc-hex", "static_assertions", ] @@ -1735,6 +1733,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1767,6 +1771,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -1946,7 +1956,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", @@ -1983,7 +1993,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2003,9 +2013,8 @@ dependencies = [ [[package]] name = "halo2_gadgets" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45824ce0dd12e91ec0c68ebae2a7ed8ae19b70946624c849add59f1d1a62a143" +version = "0.5.0" +source = "git+https://github.com/QED-it/halo2?rev=ef3d0ba2e9a513658f8d3d00294ddebd92cc649e#ef3d0ba2e9a513658f8d3d00294ddebd92cc649e" dependencies = [ "arrayvec", "bitvec", @@ -2015,7 +2024,7 @@ dependencies = [ "halo2_proofs", "lazy_static", "pasta_curves", - "rand 0.8.5", + "rand 0.8.6", "sinsemilla", "subtle", "uint 0.9.5", @@ -2030,7 +2039,7 @@ checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" [[package]] name = "halo2_poseidon" version = "0.1.0" -source = "git+https://github.com/zcash/halo2?rev=2308caf68c48c02468b66cfc452dad54e355e32f#2308caf68c48c02468b66cfc452dad54e355e32f" +source = "git+https://github.com/QED-it/halo2?rev=ef3d0ba2e9a513658f8d3d00294ddebd92cc649e#ef3d0ba2e9a513658f8d3d00294ddebd92cc649e" dependencies = [ "bitvec", "ff", @@ -2040,8 +2049,8 @@ dependencies = [ [[package]] name = "halo2_proofs" -version = "0.3.1" -source = "git+https://github.com/zcash/halo2?rev=2308caf68c48c02468b66cfc452dad54e355e32f#2308caf68c48c02468b66cfc452dad54e355e32f" +version = "0.3.2" +source = "git+https://github.com/QED-it/halo2?rev=ef3d0ba2e9a513658f8d3d00294ddebd92cc649e#ef3d0ba2e9a513658f8d3d00294ddebd92cc649e" dependencies = [ "blake2b_simd", "ff", @@ -2072,9 +2081,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2082,6 +2089,17 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "hashlink" @@ -2268,9 +2286,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -2283,7 +2301,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2291,15 +2308,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -2336,7 +2352,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2", "tokio", "tower-service", "tracing", @@ -2368,22 +2384,21 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", @@ -2392,61 +2407,99 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" -version = "2.1.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ + "displaydoc", "icu_collections", - "icu_locale_core", + "icu_locid_transform", "icu_properties_data", "icu_provider", - "zerotrie", + "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" -version = "2.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", - "icu_locale_core", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", - "zerotrie", "zerovec", ] +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -2461,9 +2514,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", @@ -2472,9 +2525,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", @@ -2528,12 +2581,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -2544,7 +2597,7 @@ version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ - "console", + "console 0.15.11", "number_prefix", "portable-atomic", "unicode-width 0.2.2", @@ -2578,11 +2631,11 @@ dependencies = [ [[package]] name = "insta" -version = "1.46.3" +version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ - "console", + "console 0.16.3", "once_cell", "pest", "pest_derive", @@ -2600,9 +2653,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -2663,9 +2716,58 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] [[package]] name = "jobserver" @@ -2679,10 +2781,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2724,8 +2828,8 @@ dependencies = [ "http-body-util", "jsonrpsee-types", "parking_lot", - "rand 0.8.5", - "rustc-hash 2.1.1", + "rand 0.8.6", + "rustc-hash 2.1.2", "serde", "serde_json", "thiserror 1.0.69", @@ -2801,13 +2905,28 @@ dependencies = [ [[package]] name = "known-folders" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770919970f7d2f74fea948900d35e2ef64f44129e8ae4015f59de1f0aca7c2a5" +checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2831,9 +2950,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" @@ -2865,9 +2984,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "libc", ] @@ -2889,9 +3008,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -2920,9 +3039,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "litrs" @@ -2970,12 +3089,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -3006,12 +3119,12 @@ checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] @@ -3024,7 +3137,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "ipnet", "metrics", "metrics-util", @@ -3045,7 +3158,7 @@ dependencies = [ "hashbrown 0.15.5", "metrics", "quanta", - "rand 0.9.2", + "rand 0.9.4", "rand_xoshiro", "sketches-ddsketch", ] @@ -3074,9 +3187,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -3110,7 +3223,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -3153,9 +3266,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-format" @@ -3225,7 +3338,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -3246,7 +3359,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", ] @@ -3257,7 +3370,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -3290,7 +3403,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3308,7 +3421,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -3321,7 +3434,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] @@ -3332,7 +3445,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3344,7 +3457,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "objc2", "objc2-cloud-kit", @@ -3380,9 +3493,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3404,9 +3517,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openrpsee" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f88ef83e8c454c2da7822db74ab2bbe3f03efac05bfb5dd0523afbdeb99799" +checksum = "faeb689cfe5fad5e7285f87b00c903366b307d97f41de53e894ec608968ca3a1" dependencies = [ "documented", "jsonrpsee", @@ -3417,11 +3530,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "opentelemetry" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", @@ -3433,68 +3552,62 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", "http", "opentelemetry", - "reqwest", - "tracing", + "reqwest 0.12.28", ] [[package]] name = "opentelemetry-otlp" -version = "0.28.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ - "async-trait", - "futures-core", "http", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost 0.13.5", - "reqwest", + "prost", + "reqwest 0.12.28", "thiserror 2.0.18", "tracing", ] [[package]] name = "opentelemetry-proto" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost 0.13.5", - "tonic 0.12.3", + "prost", + "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ - "async-trait", "futures-channel", "futures-executor", "futures-util", - "glob", "opentelemetry", "percent-encoding", - "rand 0.8.5", - "serde_json", + "rand 0.9.4", "thiserror 2.0.18", "tokio", "tokio-stream", - "tracing", ] [[package]] @@ -3516,13 +3629,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" -version = "0.12.0" -source = "git+https://github.com/QED-it/orchard?rev=77d3274cb1f4620e9a1b86477c490fa123dff6bd#77d3274cb1f4620e9a1b86477c490fa123dff6bd" +version = "0.14.0" +source = "git+https://github.com/QED-it/orchard?rev=cf801a5d6701bc128e21678ba0034564af4a2aca#cf801a5d6701bc128e21678ba0034564af4a2aca" dependencies = [ "aes", "bitvec", "blake2b_simd", - "core2", + "corez", "ff", "fpe", "getset", @@ -3537,7 +3650,7 @@ dependencies = [ "nonempty", "pasta_curves", "proptest", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "reddsa", "secp256k1", @@ -3663,7 +3776,7 @@ dependencies = [ "ff", "group", "lazy_static", - "rand 0.8.5", + "rand 0.8.6", "static_assertions", "subtle", ] @@ -3740,7 +3853,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", ] [[package]] @@ -3812,12 +3925,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkcs8" version = "0.10.2" @@ -3830,9 +3937,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plotters" @@ -3879,15 +3986,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -3996,10 +4094,10 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.11.0", + "bitflags 2.11.1", "lazy_static", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", @@ -4019,16 +4117,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive 0.13.5", -] - [[package]] name = "prost" version = "0.14.3" @@ -4036,7 +4124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.14.3", + "prost-derive", ] [[package]] @@ -4051,8 +4139,8 @@ dependencies = [ "multimap", "petgraph", "prettyplease", - "prost 0.14.3", - "prost-types 0.14.3", + "prost", + "prost-types", "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", @@ -4060,19 +4148,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "prost-derive" version = "0.14.3" @@ -4086,31 +4161,22 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost 0.13.5", -] - [[package]] name = "prost-types" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost 0.14.3", + "prost", ] [[package]] name = "pulldown-cmark" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "memchr", "unicase", ] @@ -4188,9 +4254,9 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", - "socket2 0.6.3", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -4203,12 +4269,13 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -4227,7 +4294,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -4274,9 +4341,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -4285,9 +4352,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -4377,20 +4444,29 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + [[package]] name = "raw-cpuid" version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -4443,7 +4519,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -4546,6 +4622,46 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rgb" version = "0.8.53" @@ -4608,11 +4724,11 @@ dependencies = [ [[package]] name = "ron" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "once_cell", "serde", "serde_derive", @@ -4650,9 +4766,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc-hex" @@ -4675,7 +4791,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -4684,10 +4800,11 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -4697,22 +4814,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4753,8 +4910,8 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.6.0" -source = "git+https://github.com/QED-it/sapling-crypto?rev=59535fb5d34b5c5cf1b20ef18269f5c65228378c#59535fb5d34b5c5cf1b20ef18269f5c65228378c" +version = "0.7.0" +source = "git+https://github.com/QED-it/sapling-crypto?rev=e785faaa5eaf67bd2cb328fdd82a345df44e5754#e785faaa5eaf67bd2cb328fdd82a345df44e5754" dependencies = [ "aes", "bellman", @@ -4762,7 +4919,7 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "bls12_381", - "core2", + "corez", "document-features", "ff", "fpe", @@ -4773,7 +4930,7 @@ dependencies = [ "jubjub", "lazy_static", "memuse", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "redjubjub", "subtle", @@ -4783,6 +4940,15 @@ dependencies = [ "zip32", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -4832,7 +4998,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys", "serde", ] @@ -4856,11 +5022,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -4868,26 +5057,27 @@ dependencies = [ [[package]] name = "sentry" -version = "0.40.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ccd7644577f876e92e7873d505ad35c30fc4fcf1654d4885c74605c95d0a07" +checksum = "eb25f439f97d26fea01d717fa626167ceffcd981addaa670001e70505b72acbb" dependencies = [ + "cfg_aliases", "httpdate", - "reqwest", + "reqwest 0.13.3", "rustls", "sentry-backtrace", "sentry-contexts", "sentry-core", + "sentry-log", "sentry-tracing", - "tokio", "ureq", ] [[package]] name = "sentry-backtrace" -version = "0.40.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd11d45dfae763b628d5dc45872ade9dc7b1e6894e3d3787ee8d95bf426afe12" +checksum = "46a8c2c1bd5c1f735e84f28b48e7d72efcaafc362b7541bc8253e60e8fcdffc6" dependencies = [ "backtrace", "regex", @@ -4896,9 +5086,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.40.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd374f0936ce0fdb0afcfbd896aa2db5f828b4b4c6883d4b9c4e680577a9cd" +checksum = "9b88a90baa654d7f0e1f4b667f6b434293d9f72c71bef16b197c76af5b7d5803" dependencies = [ "hostname", "libc", @@ -4910,22 +5100,35 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.40.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e9d6c4dbf4db62c8aa6f9808ce16447ac265221c8620f78e63c159256c5f85" +checksum = "0ac170a5bba8bec6e3339c90432569d89641fa7a3d3e4f44987d24f0762e6adf" dependencies = [ - "rand 0.9.2", + "rand 0.9.4", "sentry-types", "serde", "serde_json", + "url", +] + +[[package]] +name = "sentry-log" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235865e639f1d72414fa5d35374e105c292e2ee3a2f90919d961bbb486626ee6" +dependencies = [ + "bitflags 2.11.1", + "log", + "sentry-core", ] [[package]] name = "sentry-tracing" -version = "0.40.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532c5f7a9b633e6f2bf6623223cede95b112a7f104141a8cf050003417fc4d54" +checksum = "27701acc51e68db5281802b709010395bfcbcb128b1d0a4e5873680d3b47ff0c" dependencies = [ + "bitflags 2.11.1", "sentry-backtrace", "sentry-core", "tracing-core", @@ -4934,13 +5137,13 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.40.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad0332036824f2c4e5f241495107a4769a64f4b46a8e95db1108a124e6d2d00" +checksum = "56780cb5597d676bf22e6c11d1f062eb4def46390ea3bfb047bcbcf7dfd19bdb" dependencies = [ "debugid", "hex", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde_json", "thiserror 2.0.18", @@ -5017,7 +5220,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -5027,9 +5230,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -5056,7 +5259,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -5146,9 +5349,25 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" @@ -5190,16 +5409,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.3" @@ -5207,7 +5416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5222,7 +5431,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "sha1", ] @@ -5348,6 +5557,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -5410,9 +5625,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -5485,7 +5700,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe075d7053dae61ac5413a34ea7d4913b6e6207844fd726bdd858b37ff72bf5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "libc", "log", @@ -5546,9 +5761,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -5566,9 +5781,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5581,9 +5796,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -5591,7 +5806,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -5599,9 +5814,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -5666,14 +5881,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", "winnow", @@ -5681,78 +5896,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.0.0+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost 0.13.5", - "socket2 0.5.10", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" @@ -5761,7 +5937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", - "axum 0.8.8", + "axum", "base64 0.22.1", "bytes", "h2", @@ -5773,7 +5949,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.3", + "socket2", "sync_wrapper", "tokio", "tokio-stream", @@ -5802,8 +5978,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", - "prost 0.14.3", - "tonic 0.14.5", + "prost", + "tonic", ] [[package]] @@ -5815,7 +5991,7 @@ dependencies = [ "prettyplease", "proc-macro2", "prost-build", - "prost-types 0.14.3", + "prost-types", "quote", "syn 2.0.117", "tempfile", @@ -5828,11 +6004,11 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf0685a51e6d02b502ba0764002e766b7f3042aed13d9234925b6ffbfa3fca7" dependencies = [ - "prost 0.14.3", - "prost-types 0.14.3", + "prost", + "prost-types", "tokio", "tokio-stream", - "tonic 0.14.5", + "tonic", "tonic-prost", ] @@ -5845,11 +6021,8 @@ dependencies = [ "futures-core", "futures-util", "hdrhistogram", - "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand 0.8.5", - "slab", "tokio", "tokio-util", "tower-layer", @@ -5865,7 +6038,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5885,7 +6058,7 @@ dependencies = [ "futures", "futures-core", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "rayon", "tokio", "tokio-test", @@ -5917,7 +6090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -5973,11 +6146,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", + "symlink", "thiserror 2.0.18", "time", "tracing-subscriber", @@ -6070,14 +6244,12 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.29.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", - "once_cell", "opentelemetry", - "opentelemetry_sdk", "smallvec", "tracing", "tracing-core", @@ -6088,9 +6260,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -6139,9 +6311,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -6202,9 +6374,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -6242,9 +6414,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64 0.22.1", "log", @@ -6252,15 +6424,15 @@ dependencies = [ "rustls", "rustls-pki-types", "ureq-proto", - "utf-8", + "utf8-zero", "webpki-roots", ] [[package]] name = "ureq-proto" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ "base64 0.22.1", "http", @@ -6270,22 +6442,27 @@ dependencies = [ [[package]] name = "url" -version = "2.5.8" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] -name = "utf-8" -version = "0.7.6" +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-zero" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" [[package]] name = "utf8_iter" @@ -6301,9 +6478,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "js-sys", "serde_core", @@ -6484,11 +6661,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -6497,14 +6674,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -6515,23 +6692,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6539,9 +6712,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -6552,9 +6725,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -6576,7 +6749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -6587,17 +6760,17 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -6613,11 +6786,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -6888,9 +7070,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -6904,6 +7086,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -6923,7 +7111,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -6953,8 +7141,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", + "bitflags 2.11.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -6973,7 +7161,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -6983,11 +7171,17 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" -version = "0.6.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wyz" @@ -7029,10 +7223,11 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -7040,9 +7235,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -7052,12 +7247,12 @@ dependencies = [ [[package]] name = "zcash_address" -version = "0.10.1" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.12.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "bech32", "bs58", - "core2", + "corez", "f4jumble", "zcash_encoding", "zcash_protocol", @@ -7065,10 +7260,10 @@ dependencies = [ [[package]] name = "zcash_encoding" -version = "0.3.0" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.4.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ - "core2", + "corez", "hex", "nonempty", ] @@ -7076,7 +7271,7 @@ dependencies = [ [[package]] name = "zcash_history" version = "0.4.0" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "blake2b_simd", "byteorder", @@ -7085,14 +7280,14 @@ dependencies = [ [[package]] name = "zcash_keys" -version = "0.12.0" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.14.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "bech32", "blake2b_simd", "bls12_381", "bs58", - "core2", + "corez", "document-features", "group", "memuse", @@ -7124,49 +7319,38 @@ dependencies = [ [[package]] name = "zcash_primitives" -version = "0.26.4" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.28.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ - "bip32", "blake2b_simd", "block-buffer 0.11.0-rc.3", - "bs58", - "core2", + "corez", "crypto-common 0.2.0-rc.1", "document-features", "equihash", "ff", - "fpe", - "getset", - "group", "hex", "incrementalmerkletree", "jubjub", "memuse", "nonempty", "orchard", - "rand 0.8.5", "rand_core 0.6.4", "redjubjub", - "ripemd 0.1.3", "sapling-crypto", "secp256k1", "sha2 0.10.9", - "subtle", - "tracing", - "zcash_address", "zcash_encoding", "zcash_note_encryption", "zcash_protocol", "zcash_script", "zcash_transparent", - "zip32", ] [[package]] name = "zcash_proofs" -version = "0.26.1" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.28.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "bellman", "blake2b_simd", @@ -7187,10 +7371,10 @@ dependencies = [ [[package]] name = "zcash_protocol" -version = "0.7.2" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.9.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ - "core2", + "corez", "document-features", "hex", "memuse", @@ -7199,12 +7383,12 @@ dependencies = [ [[package]] name = "zcash_script" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ef9d04e0434a80b62ad06c5a610557be358ef60a98afa5dbc8ecaf19ad72e7" +checksum = "2f872800287d118be71bdf6fe8c869c6a6ff6fb0a5762f68fb2af54c97edf0f2" dependencies = [ "bip32", - "bitflags 2.11.0", + "bitflags 2.11.1", "bounded-vec", "hex", "ripemd 0.1.3", @@ -7224,13 +7408,12 @@ dependencies = [ [[package]] name = "zcash_transparent" -version = "0.6.3" -source = "git+https://github.com/QED-it/librustzcash?rev=0ea737548f7aea6124056df54d55f6c5a35ef914#0ea737548f7aea6124056df54d55f6c5a35ef914" +version = "0.8.0" +source = "git+https://github.com/QED-it/librustzcash?rev=c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0#c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" dependencies = [ "bip32", - "blake2b_simd", "bs58", - "core2", + "corez", "document-features", "getset", "hex", @@ -7249,10 +7432,10 @@ dependencies = [ [[package]] name = "zebra-chain" -version = "6.0.0" +version = "10.1.0" dependencies = [ "bech32", - "bitflags 2.11.0", + "bitflags 2.11.1", "bitflags-serde-legacy", "bitvec", "blake2b_simd", @@ -7281,7 +7464,7 @@ dependencies = [ "primitive-types", "proptest", "proptest-derive", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_core 0.6.4", "rayon", @@ -7319,13 +7502,14 @@ dependencies = [ [[package]] name = "zebra-consensus" -version = "5.0.0" +version = "9.0.1" dependencies = [ "bellman", "blake2b_simd", "bls12_381", "chrono", "color-eyre", + "criterion", "derive-getters", "futures", "futures-util", @@ -7334,6 +7518,7 @@ dependencies = [ "howudoin", "jubjub", "lazy_static", + "libzcash_script", "metrics", "mset", "num-integer", @@ -7341,7 +7526,7 @@ dependencies = [ "orchard", "proptest", "proptest-derive", - "rand 0.8.5", + "rand 0.8.6", "rayon", "sapling-crypto", "serde", @@ -7358,6 +7543,8 @@ dependencies = [ "zcash_primitives", "zcash_proofs", "zcash_protocol", + "zcash_script", + "zcash_transparent", "zebra-chain", "zebra-node-services", "zebra-script", @@ -7367,9 +7554,9 @@ dependencies = [ [[package]] name = "zebra-network" -version = "5.0.0" +version = "9.0.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -7378,7 +7565,7 @@ dependencies = [ "hex", "howudoin", "humantime-serde", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "lazy_static", "metrics", @@ -7387,7 +7574,7 @@ dependencies = [ "pin-project", "proptest", "proptest-derive", - "rand 0.8.5", + "rand 0.8.6", "rayon", "regex", "schemars 1.2.1", @@ -7398,7 +7585,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "tower 0.4.13", "tracing", "tracing-error", @@ -7409,11 +7596,11 @@ dependencies = [ [[package]] name = "zebra-node-services" -version = "4.0.0" +version = "8.0.0" dependencies = [ "color-eyre", "jsonrpsee-types", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -7423,39 +7610,47 @@ dependencies = [ [[package]] name = "zebra-rpc" -version = "6.0.0" +version = "10.0.1" dependencies = [ + "anyhow", "base64 0.22.1", + "bytes", "chrono", "color-eyre", "derive-getters", "derive-new", "futures", "hex", + "http-body", "http-body-util", "hyper", - "indexmap 2.13.0", + "indexmap 2.14.0", "insta", "jsonrpsee", "jsonrpsee-proc-macros", "jsonrpsee-types", + "lazy_static", "metrics", "nix", "openrpsee", + "orchard", "phf", "proptest", - "prost 0.14.3", - "rand 0.8.5", + "prost", + "rand 0.8.6", "sapling-crypto", "schemars 1.2.1", "semver", "serde", "serde_json", "serde_with", + "strum", + "strum_macros", + "tempfile", "thiserror 2.0.18", "tokio", "tokio-stream", - "tonic 0.14.5", + "tonic", "tonic-prost", "tonic-prost-build", "tonic-reflection", @@ -7465,6 +7660,7 @@ dependencies = [ "zcash_address", "zcash_keys", "zcash_primitives", + "zcash_proofs", "zcash_protocol", "zcash_script", "zcash_transparent", @@ -7479,11 +7675,15 @@ dependencies = [ [[package]] name = "zebra-script" -version = "5.0.0" +version = "9.0.0" dependencies = [ "hex", "lazy_static", "libzcash_script", + "rand 0.8.6", + "ripemd 0.1.3", + "secp256k1", + "sha2 0.10.9", "thiserror 2.0.18", "zcash_primitives", "zcash_script", @@ -7493,7 +7693,7 @@ dependencies = [ [[package]] name = "zebra-state" -version = "5.0.0" +version = "9.0.1" dependencies = [ "bincode", "chrono", @@ -7510,7 +7710,7 @@ dependencies = [ "howudoin", "human_bytes", "humantime-serde", - "indexmap 2.13.0", + "indexmap 2.14.0", "insta", "itertools 0.14.0", "jubjub", @@ -7520,7 +7720,7 @@ dependencies = [ "once_cell", "proptest", "proptest-derive", - "rand 0.8.5", + "rand 0.8.6", "rayon", "regex", "rlimit", @@ -7548,14 +7748,14 @@ dependencies = [ "futures", "hex", "humantime", - "indexmap 2.13.0", + "indexmap 2.14.0", "insta", "itertools 0.14.0", "lazy_static", "once_cell", "owo-colors", "proptest", - "rand 0.8.5", + "rand 0.8.6", "regex", "spandoc", "tempfile", @@ -7569,13 +7769,13 @@ dependencies = [ [[package]] name = "zebra-utils" -version = "5.0.0" +version = "8.0.1" dependencies = [ "color-eyre", "hex", "itertools 0.14.0", "regex", - "reqwest", + "reqwest 0.12.28", "serde_json", "structopt", "thiserror 2.0.18", @@ -7591,13 +7791,13 @@ dependencies = [ [[package]] name = "zebrad" -version = "4.2.0" +version = "5.2.0" dependencies = [ "abscissa_core", - "atty", + "anyhow", "bytes", "chrono", - "clap 4.5.60", + "clap 4.6.1", "color-eyre", "config", "console-subscriber", @@ -7611,7 +7811,7 @@ dependencies = [ "humantime-serde", "hyper", "hyper-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "indicatif", "inferno", "insta", @@ -7628,21 +7828,23 @@ dependencies = [ "pin-project", "proptest", "proptest-derive", - "prost 0.14.3", - "rand 0.8.5", + "prost", + "rand 0.8.6", "rayon", "regex", "semver", "sentry", "serde", "serde_json", + "strum", + "strum_macros", "tempfile", "thiserror 2.0.18", "thread-priority", "tokio", "tokio-stream", - "toml 0.9.12+spec-1.1.0", - "tonic 0.14.5", + "toml 1.1.2+spec-1.1.0", + "tonic", "tonic-prost", "tonic-prost-build", "tower 0.4.13", @@ -7671,18 +7873,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -7691,18 +7893,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -7730,22 +7932,11 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - [[package]] name = "zerovec" -version = "0.11.5" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", @@ -7754,9 +7945,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ffc1c5c2e49..57de23eebaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,19 +32,18 @@ edition = "2021" incrementalmerkletree = { version = "0.8.2", features = ["legacy-api"] } # TODO: Remove the `temporary-zebra` feature once upstream `orchard` exposes the APIs # Zebra needs without this transition feature. -orchard = { version = "0.12", features = ["zsa-issuance", "temporary-zebra"] } -sapling-crypto = "0.6" -zcash_address = "0.10.1" -zcash_encoding = "0.3.0" +orchard = { version = "0.14", features = ["zsa-issuance", "temporary-zebra"] } +sapling-crypto = "0.7" +zcash_address = "0.12.0" +zcash_encoding = "0.4.0" zcash_history = "0.4.0" -zcash_keys = "0.12.0" -zcash_primitives = { version = "0.26.4", features = ["zsa-issuance", "zip-233"] } -zcash_proofs = "0.26.1" -zcash_transparent = "0.6.3" -zcash_protocol = "0.7.2" +zcash_keys = "0.14.0" +zcash_primitives = { version = "0.28.0", features = ["zsa-issuance", "zip-233"] } +zcash_proofs = "0.28.0" +zcash_transparent = "0.8.0" +zcash_protocol = "0.9.0" zip32 = "0.2" abscissa_core = "0.7" -atty = "0.2.14" base64 = "0.22.1" bech32 = "0.11.0" bellman = "0.14" @@ -61,15 +60,15 @@ bytes = "1.10" chrono = { version = "0.4.40", default-features = false } clap = "4.5" color-eyre = { version = "0.6.3", default-features = false } -console-subscriber = "0.4" +console-subscriber = "0.5" criterion = "0.5" crossbeam-channel = "0.5.14" derive-getters = "0.5" derive-new = "0.5" dirs = "6.0" -ed25519-zebra = "4.0.3" +ed25519-zebra = "4.2" elasticsearch = { version = "8.17.0-alpha.1", default-features = false } -equihash = "0.2.2" +equihash = "0.3" ff = "0.13" futures = "0.3.31" futures-core = "0.3.31" @@ -79,6 +78,7 @@ halo2 = "0.3" hex = "0.4.3" hex-literal = "0.4" howudoin = "0.1" +http-body = "1.0.1" http-body-util = "0.1.3" human_bytes = { version = "0.4", default-features = false } humantime = "2.2" @@ -127,7 +127,7 @@ rlimit = "0.10" rocksdb = { version = "0.22", default-features = false } secp256k1 = "0.29" semver = "1.0.26" -sentry = { version = "0.40", default-features = false } +sentry = { version = "0.47", default-features = false } serde = "1.0.219" serde-big-array = "0.5" serde_json = "1.0.140" @@ -138,6 +138,9 @@ schemars = "1" spandoc = "0.2" static_assertions = "1.1" structopt = "0.3" +anyhow = "1.0" +strum = "0.27" +strum_macros = "0.27" syn = "2.0.100" tempfile = "3.20" thiserror = "2.0" @@ -147,7 +150,7 @@ tokio = "1.44" tokio-stream = "0.1.17" tokio-test = "0.4" tokio-util = "0.7.14" -toml = "0.9.5" +toml = "1.0" tonic = "0.14" tonic-prost = "0.14" tonic-prost-build = "0.14" @@ -162,15 +165,15 @@ tracing-futures = "0.2.5" tracing-journald = "0.3" tracing-subscriber = "0.3.19" tracing-test = "0.2.4" -tracing-opentelemetry = "0.29" -opentelemetry = "0.28" -opentelemetry_sdk = "0.28" -opentelemetry-otlp = "0.28" +tracing-opentelemetry = "0.32" +opentelemetry = "0.31" +opentelemetry_sdk = "0.31" +opentelemetry-otlp = "0.31" uint = "0.10" vergen-git2 = { version = "9", default-features = false } x25519-dalek = "2.0.1" zcash_note_encryption = "0.4.1" -zcash_script = "0.4.3" +zcash_script = "0.4.5" config = { version = "0.15.14", features = ["toml"] } which = "8.0.0" @@ -288,6 +291,19 @@ overflow-checks = false incremental = false codegen-units = 16 +# The advisory +# https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gq4h-3grw-2rhv +# only reproduces reliably when the bundled libzcash_script C++ code is compiled +# in release mode since it relies on a buffer to not be zeroed out (arrays in +# C++ are usually zeroed out when allocated in debug mode). To reliably test +# against that issue, and to also better simulate real-world usage of +# libzcash_script, we enable optimizations for it in dev builds. +[profile.dev.package.libzcash_script] +opt-level = 3 +debug-assertions = false +overflow-checks = false +incremental = false +codegen-units = 16 [profile.release] panic = "abort" @@ -314,23 +330,71 @@ debug = false # The linter should ignore these expected config flags/values unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(tokio_unstable)', # Used by tokio-console - 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7"))' # Used in Zebra and librustzcash + 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7", "zip235"))' # Used in Zebra and librustzcash ] } +# High-risk code +unsafe_code = "deny" +non_ascii_idents = "deny" + +# Compatibility +rust_2021_compatibility = { level = "warn", priority = -1 } +nonstandard_style = { level = "warn", priority = -1 } +future_incompatible = { level = "warn", priority = -1 } + +# Documentation +missing_docs = "warn" + +[workspace.lints.clippy] +# Async code +await_holding_lock = "warn" +await_holding_refcell_ref = "warn" + +# Pointers +cast_ptr_alignment = "warn" +fn_to_numeric_cast_any = "warn" + +# Integers +checked_conversions = "warn" +implicit_saturating_sub = "warn" +invalid_upcast_comparisons = "warn" +range_minus_one = "warn" +range_plus_one = "warn" +unnecessary_cast = "warn" + +# Incomplete code +dbg_macro = "warn" +todo = "warn" + +# Manual debugging output — use tracing::trace!() or tracing::debug!() instead +print_stdout = "warn" +print_stderr = "warn" + +# Code styles we want to accept +try_err = "allow" + +# Panics +fallible_impl_from = "warn" +unwrap_in_result = "warn" + +# TODOs: fix this lint eventually +result_large_err = "allow" + [patch.crates-io] -halo2_proofs = { version = "0.3.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } -halo2_poseidon = { version = "0.1.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } +halo2_gadgets = { version = "0.5.0", git = "https://github.com/QED-it/halo2", rev = "ef3d0ba2e9a513658f8d3d00294ddebd92cc649e" } +halo2_proofs = { version = "0.3.2", git = "https://github.com/QED-it/halo2", rev = "ef3d0ba2e9a513658f8d3d00294ddebd92cc649e" } +halo2_poseidon = { version = "0.1.0", git = "https://github.com/QED-it/halo2", rev = "ef3d0ba2e9a513658f8d3d00294ddebd92cc649e" } sinsemilla = { git = "https://github.com/zcash/sinsemilla", rev = "aabb707e862bc3d7b803c77d14e5a771bcee3e8c" } zcash_note_encryption = { version = "0.4.1", git = "https://github.com/zcash/zcash_note_encryption", rev = "9f7e93d42cef839d02b9d75918117941d453f8cb" } -sapling-crypto = { package = "sapling-crypto", version = "0.6", git = "https://github.com/QED-it/sapling-crypto", rev = "59535fb5d34b5c5cf1b20ef18269f5c65228378c" } -orchard = { version = "0.12.0", git = "https://github.com/QED-it/orchard", rev = "77d3274cb1f4620e9a1b86477c490fa123dff6bd" } -zcash_primitives = { version = "0.26.1", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_protocol = { version = "0.7.2", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_address = { version = "0.10.1", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_encoding = { version = "0.3.0", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_history = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_keys = { version = "0.12.0", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_transparent = { version = "0.6.3", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -zcash_proofs = { version = "0.26.1", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } -equihash = { version = "0.2.2", git = "https://github.com/QED-it/librustzcash", rev = "0ea737548f7aea6124056df54d55f6c5a35ef914" } +sapling-crypto = { version = "0.7", git = "https://github.com/QED-it/sapling-crypto", rev = "e785faaa5eaf67bd2cb328fdd82a345df44e5754" } +orchard = { version = "0.14.0", git = "https://github.com/QED-it/orchard", rev = "cf801a5d6701bc128e21678ba0034564af4a2aca" } +zcash_primitives = { version = "0.28.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_protocol = { version = "0.9.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_address = { version = "0.12.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_encoding = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_history = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_keys = { version = "0.14.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_transparent = { version = "0.8.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +zcash_proofs = { version = "0.28.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } +equihash = { version = "0.3.0", git = "https://github.com/QED-it/librustzcash", rev = "c5c232db9c54eeb8bc1d0f4dc1b8a0e40f981bb0" } zcash_spec = { git = "https://github.com/QED-it/zcash_spec", rev = "d5e84264d2ad0646b587a837f4e2424ca64d3a05" } diff --git a/README.md b/README.md index 1c5cc684f19..8db04dfe6a4 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,17 @@ image](https://hub.docker.com/r/zfnd/zebra/tags) or you can install it manually. This command will run our latest release, and sync it to the tip: ```sh -docker run zfnd/zebra:latest +docker run -d \ + --name zebra \ + -p 8233:8233 \ + -v zebrad-cache:/home/zebra/.cache/zebra \ + zfnd/zebra:latest ``` -For more information, read our [Docker documentation](https://zebra.zfnd.org/user/docker.html). +The `-p 8233:8233` flag exposes the P2P port so other Zcash nodes can connect to +yours, and `-v` persists the chain state across restarts (use port `18233` for +Testnet). For more information, read our [Docker +documentation](https://zebra.zfnd.org/user/docker.html). ### Manual Install @@ -90,7 +97,7 @@ cargo install --locked zebrad Alternatively, you can install it from GitHub: ```sh -cargo install --git https://github.com/ZcashFoundation/zebra --tag v2.5.0 zebrad +cargo install --git https://github.com/ZcashFoundation/zebra --tag v5.2.0 zebrad ``` You can start Zebra by running diff --git a/SECURITY.md b/SECURITY.md index c00c8546b52..60b8f1ebfd6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,35 @@ Specifically, we will: The Zcash Foundation is committed to working with researchers who submit security vulnerability notifications to us to resolve those issues on an appropriate timeline and perform a coordinated release, giving credit to the reporter if they would like. -Our best contact for security issues is . +Please submit issues to , using the following PGP Key: + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaXswoxYJKwYBBAHaRw8BAQdA/CQqZ79S7A9OWZeYhY3AbMuTx2d41/pcehNc +Z1ZF7r6IeAQgFgoAIBYhBOezJEDaeE6/uKooRf1tVVkb+SvKBQJpezIJAh0AAAoJ +EP1tVVkb+SvKmqQBAMzp/pOZ/ifM0Tjuqzy4nTo8HT5xZwTfL84A40VURuElAP91 +/9wi+5ZKW09pdjHjag6tz0FhheinX1BEDbgww2u3CbQkWmNhc2ggRm91bmRhdGlv +biA8c2VjdXJpdHlAemZuZC5vcmc+iJYEExYKAD4WIQTnsyRA2nhOv7iqKEX9bVVZ +G/krygUCaXswowIbAwUJACeNAAULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRD9 +bVVZG/kryrS4AQCiyknTREsLCICWdbaJUARuZifhDxXIKH0oest8y8HQQwD9HuRd +936Cg5FbXHpBuF71fGU213OSgulG4+hr7rXdfgy4OARpezCjEgorBgEEAZdVAQUB +AQdAw5WBljp9hoqi8lu2KU5QzNuv/1lpeGWoESdWg/GZKUIDAQgHiH4EGBYKACYW +IQTnsyRA2nhOv7iqKEX9bVVZG/krygUCaXswowIbDAUJACeNAAAKCRD9bVVZG/kr +yjsWAQCiRiecQ9P3DPyQ/E/N0Dl3z4jE2fM2NjhROnX4jB/lDgD8Cru6rg2sdxTc +RHrjNOriwH3PxwALJorvERC1gl47jQ2YMwRpqByGFgkrBgEEAdpHDwEBB0DV0fxs +U5skejT0UERNZbec7GGe7Vs7s1h0moC4vkuY87QyWmNhc2ggRm91bmRhdGlvbiBT +ZWN1cml0eSBUZWFtIDxzZWN1cml0eUB6Zm5kLm9yZz6IlgQTFgoAPhYhBHVQw2w9 +9hamnx7+AGBG3e+Uz5m1BQJpqByGAhsDBQkDwmcABQsJCAcDBRUKCQgLBRYCAwEA +Ah4BAheAAAoJEGBG3e+Uz5m18MoBAOulghTZ717buHwBKBZupdXMdYPZcNSxlFC1 ++ROt3iYAAP0RfQPw/UYLQlsnc5JEov2pExVpdXJH4waJjh+r26ZQCbg4BGmoHIYS +CisGAQQBl1UBBQEBB0BXtdSydYIV586tkyNwAefvnQM0pJapklUbVD9f9AmQHAMB +CAeIfgQYFgoAJhYhBHVQw2w99hamnx7+AGBG3e+Uz5m1BQJpqByGAhsMBQkDwmcA +AAoJEGBG3e+Uz5m14rEA/0x/2XNwKd4buCm1tOGpTMaLQRoWhos6L/0wV9LExEKG +AQC1Wmyb9ul/2QNi//8sKNDfaYbn3h6OU45BTAWggp+ACQ== +=RIK0 +-----END PGP PUBLIC KEY BLOCK----- +``` ## Sending Disclosures @@ -45,7 +73,3 @@ Zcash is a technology that provides strong privacy. Notes are encrypted to their The standard describes reporters of vulnerabilities including full details of an issue, in order to reproduce it. This is necessary for instance in the case of an external researcher both demonstrating and proving that there really is a security issue, and that security issue really has the impact that they say it has - allowing the development team to accurately prioritize and resolve the issue. In the case of a counterfeiting bug, we might decide not to include those details with our reports to partners ahead of coordinated release, so long as we are sure that they are vulnerable. - -### Alpha Release Disclosures - -The Zcash Foundation will generate encryption keys for security disclosures for our first stable release. Until then, disclosures should be sent to unencrypted. diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index b0a1add85e5..09419bf1043 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -33,6 +33,7 @@ - [Zebra versioning and releases](dev/release-process.md) - [Continuous Integration](dev/continuous-integration.md) - [Continuous Delivery](dev/continuous-delivery.md) + - [GCP Deployment Operations](dev/gcp-deployment-operations.md) - [Generating Zebra Checkpoints](dev/zebra-checkpoints.md) - [Doing Mass Renames](dev/mass-renames.md) - [Updating the ECC dependencies](dev/ecc-updates.md) diff --git a/book/src/dev/continuous-delivery.md b/book/src/dev/continuous-delivery.md index d41cc97f521..6f2b0f4403d 100644 --- a/book/src/dev/continuous-delivery.md +++ b/book/src/dev/continuous-delivery.md @@ -1,28 +1,41 @@ # Zebra Continuous Delivery -Zebra has an extension of its continuous integration since it automatically deploys all -code changes to a testing and/or pre-production environment after each PR gets merged -into the `main` branch, and on each Zebra `release`. +The continuous-delivery pipeline deploys every commit merged to `main` to the `stage` environment and every published release to `prod`, on Google Cloud Platform. PR-triggered work uses the `dev` environment. -## Triggers +## Topology: one zonal MIG per (environment, branch, network, zone) + +The pipeline targets two GCP environments. Each network in each environment deploys to three zonal Managed Instance Groups (MIGs) in `us-east1` zones `b`, `c`, and `d`. Each zonal MIG holds one Zebra instance with one stateful cache disk and one static IP. + +| Trigger | Environment label | GCP project | MIGs per network | MIG name | Stateful disk | +| -------------------- | ----------------- | ----------------- | ---------------- | --------------------------------------------- | --------------------------------------------- | +| `release` | `prod` | `zfnd-prod-zebra` | 3 (one per zone) | `zebrad-${network}-${zone-letter}` | `zebrad-cache-${network}-${zone-letter}` | +| `push` to `main` | `stage` | `zfnd-dev-zebra` | 3 (one per zone) | `zebrad-main-${network}-${zone-letter}` | `zebrad-cache-main-${network}-${zone-letter}` | +| `workflow_dispatch` | `dev` or `prod` | selected by env | 1 (user-chosen zone) | `zebrad-${branch}-${network}-${zone-letter}` | `zebrad-cache-${branch}-${network}-${zone-letter}` | + +ADR [0006](../../../docs/decisions/devops/0006-gcp-deployment-naming.md) records the rationale; the [runbook](gcp-deployment-operations.md) covers day-to-day procedures. -The Continuous delivery pipeline is triggered when: +## Update mechanics -- A PR is merged to `main` (technically, a `push` event) -- A new release is published in GitHub +Each push and each release fans out to six `deploy-nodes` jobs (2 networks × 3 zones). A workflow_dispatch is a single job (user picks the zone). Every job runs the same flow for its zonal MIG: -## Deployments +1. Build a new instance template with the commit's container image. +2. Ensure the zonal stateful disk exists. On first deploy, create it from the latest matching cache image. On subsequent deploys, attach the existing disk. +3. If the zonal MIG exists, run `rolling-action start-update --max-unavailable=1`. True per-zone rolling: this zone's MIG replaces its instance while the other two zones keep serving. The stateful disk persists across the replace. +4. If the zonal MIG does not exist, create it with `--size=1` and apply the stateful policy. +5. Assign the static IP (push and release only; workflow_dispatch uses ephemeral). Zone-to-IP mapping is deterministic: zone `b` → primary, zone `c` → secondary, zone `d` → tertiary. + +Cache images come from `zfnd-ci-integration-tests-gcp.yml`'s `create-state-image` job. Image names encode branch, commit, state-DB version, network, and timestamp. One image per network seeds all three zones. Lookup priority in `gcp-get-cached-disks.sh`: current branch, then `main`, then any branch; most recent first. + +Deploy success has two channels: `deploy-nodes` reports infrastructure, `verify-nodes` reports application health. See the [runbook](gcp-deployment-operations.md#deploy-success-has-two-channels) for details. + +## Triggers -On each trigger Zebra is deployed using the branch or version references as part of -the deployment naming convention. Deployments are made using [Managed Instance Groups (MIGs)](https://cloud.google.com/compute/docs/instance-groups#managed_instance_groups) -from Google Cloud Platform with, 2 nodes in the us-central1 region. +The workflow runs on: -**Note**: These _MIGs_ are always replaced when PRs are merged to the `main` branch and -when a release is published. If a new major version is released, a new _MIG_ is also -created, keeping the previous major version running until it's no longer needed. +- a `push` to `main` that touches Rust code, dependencies, Docker files, or the workflow itself +- a published `release` +- a `workflow_dispatch` from any branch (dispatcher picks `network`, `zone`, and `environment`) -A single instance can also be deployed, on an on-demand basis, if required, when a -long-lived instance, with specific changes, is needed to be tested in the Mainnet with -the same infrastructure used for CI & CD. +Pull requests run only the Docker-configuration tests; they do not deploy. -Further validations of the actual process can be done on our continuous delivery [workflow file](https://github.com/ZcashFoundation/zebra/blob/main/.github/workflows/zfnd-deploy-nodes-gcp.yml). +For implementation details, see the [deploy workflow](https://github.com/ZcashFoundation/zebra/blob/main/.github/workflows/zfnd-deploy-nodes-gcp.yml). diff --git a/book/src/dev/continuous-integration.md b/book/src/dev/continuous-integration.md index 2677407993e..645dfed570b 100644 --- a/book/src/dev/continuous-integration.md +++ b/book/src/dev/continuous-integration.md @@ -48,10 +48,9 @@ Some PRs don't use Mergify: Merging with failing CI is usually disabled by our branch protection rules. See the `Admin: Manually Merging PRs` section below for manual merge instructions. -We use workflow conditions to skip some checks on PRs, Mergify, or the `main` branch. -For example, some workflow changes skip Rust code checks. When a workflow can skip a check, we need to create [a patch workflow](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks) -with an empty job with the same name. This is a [known Actions issue](https://github.com/orgs/community/discussions/13690#discussioncomment-6653382). -This lets the branch protection rules pass when the job is skipped. In Zebra, we name these workflows with the extension `.patch.yml`. +Each required status check is produced by exactly one workflow. A `changes` job uses [`dorny/paths-filter`](https://github.com/dorny/paths-filter) against [`.github/path-filters.yml`](https://github.com/ZcashFoundation/zebra/blob/main/.github/path-filters.yml) to gate worker jobs via `if:`; an aggregator job named after the workflow basename (`lint`, `unit-tests`, `test-crates`, ...) runs with `if: always()` and `re-actors/alls-green`, and is the sole producer of the required-check context. The aggregator job ID, the workflow file basename, and the ruleset context name are kept identical so `grep -r ':' .github/workflows/` finds the producer in one hop. + +On `pull_request` events the filter narrows what runs; on `push` to `main` and `merge_group` events the filter step is skipped and every gated worker runs (the `|| 'true'` default on the `changes` job outputs makes this explicit). ### Branch Protection Rules @@ -66,9 +65,13 @@ But the following jobs don't need branch protection rules: - Setup jobs that will fail another later job which always runs, for example: Google Cloud setup jobs. We have branch protection rules for build jobs, but we could remove them if we want. -When a new job is added in a PR, use the `#devops` Slack channel to ask a GitHub admin to add a branch protection rule after it merges. -Adding a new Zebra crate automatically adds a new job to build that crate by itself in [test-crates.yml](https://github.com/ZcashFoundation/zebra/blob/main/.github/workflows/test-crates.yml), -so new crate PRs also need to add a branch protection rule. +To add a new gated job to an existing required check, add it to the producing workflow, then add its job ID to the aggregator under both `needs:` and `allowed-skips:`. To add a brand-new required check: + +1. Add an entry to `.github/path-filters.yml` named after the workflow basename (use underscores in the filter key to avoid expression-syntax ambiguity). +2. Build the workflow with a `changes` job that reads the filter, gated workers, and an aggregator job whose ID matches the workflow basename. +3. Ask `#devops` to add the aggregator job ID to the GitHub ruleset. + +Adding a new Zebra crate automatically extends the `build` matrix in [test-crates.yml](https://github.com/ZcashFoundation/zebra/blob/main/.github/workflows/test-crates.yml); no manual step is required. #### Admin: Changing Branch Protection Rules @@ -80,10 +83,7 @@ To change branch protection rules: Any developer: -0. Run a PR containing the new rule, so its name is available to autocomplete. -1. If the job doesn't run on all PRs, add a patch job with the name of the job. - If the job calls a reusable workflow, the name is `Caller job / Reusable step`. - (The name of the job inside the reusable workflow is ignored.) +1. Run a PR containing the new aggregator, so its job ID is available to autocomplete in the ruleset UI. Admin: diff --git a/book/src/dev/ecc-updates.md b/book/src/dev/ecc-updates.md index 13069dc3b99..f6721321c05 100644 --- a/book/src/dev/ecc-updates.md +++ b/book/src/dev/ecc-updates.md @@ -1,6 +1,6 @@ # Updating the ECC dependencies -Zebra relies on numerous Electric Coin Company ([ECC](https://electriccoin.co/)) dependencies, and updating them can be a complex task. This guide will help you navigate the process. +Zebra relies on numerous dependencies maintained by ZODL, and updating them can be a complex task. This guide will help you navigate the process. The main dependency that influences that is [zcash](https://github.com/zcash/zcash) itself. This is because [zebra_script](https://github.com/ZcashFoundation/zcash_script) links to specific files from it (zcash_script.cpp and all on which it depends). Due to the architecture of zcash, this requires linking to a lot of seemingly unrelated dependencies like orchard, halo2, etc (which are all Rust crates). diff --git a/book/src/dev/gcp-deployment-operations.md b/book/src/dev/gcp-deployment-operations.md new file mode 100644 index 00000000000..8206d1bb811 --- /dev/null +++ b/book/src/dev/gcp-deployment-operations.md @@ -0,0 +1,255 @@ +# GCP Deployment Operations + +Operational procedures for the GCP Continuous Delivery pipeline. Architectural rationale: [ADR 0006](../../../docs/decisions/devops/0006-gcp-deployment-naming.md). High-level model: [Continuous Delivery](continuous-delivery.md). + +Two GCP projects (`zfnd-prod-zebra`, `zfnd-dev-zebra`) in `us-east1`, zones `b`, `c`, `d`. Every MIG, instance, and disk carries `environment`, `network`, `zone`, `created_by`, `github_ref`, and `github_sha` labels. The `environment` label is standardized as `dev`, `stage`, or `prod`. Use `created_by` as the kind discriminator: `release`, `push`, or `workflow_dispatch`. + +The recipes below assume `gh` and `gcloud` are authenticated, and use these shell defaults when scoped to dev: + +```bash +P=zfnd-dev-zebra +``` + +For production, set `P=zfnd-prod-zebra` instead. + +Zonal MIGs use `--zone` (not `--region`). One MIG per zone per network. MIG names end with the zone letter (`-b`, `-c`, `-d`). + +## Deploy success has two channels + +The workflow reports two independent signals: + +- `deploy-nodes` (and `failure-issue` with label `S-ci-fail-release-auto-issue`) reports **infrastructure**: template, zonal MIG, stateful disk, and static IP landed. Usually green within 3-5 minutes per matrix cell; a failure means the deploy itself broke. +- `verify-nodes` (and `verify-failure-issue` with label `S-ci-fail-verify-auto-issue`) reports **application**: the zonal MIG reached HEALTHY. Up to 90 minutes; a failure means the node took longer than that to establish peers and catch up to chain tip. The MIG itself is fine; on-call action is usually "wait, or investigate why sync is slow". + +## Quick reference + +| Goal | Section | +| ---------------------------------------------------- | ------------------------------------------------------------- | +| Smoke-test a PR branch / find / label / reap | [PR deploys](#pr-deploys) | +| Diagnose a deploy that does not converge | [Diagnose a stuck MIG](#diagnose-a-stuck-mig) | +| Recover from a corrupted cache disk | [Recover a corrupted cache disk](#recover-a-corrupted-cache-disk) | +| Cut a release with a backwards-incompatible DB format | [DB-format-version-break release](#db-format-version-break-release) | +| Migrate a regional MIG to zonal | [Regional-to-zonal migration](#regional-to-zonal-migration) | +| Look up cache images, static IPs, daily cleanup | [Reference](#reference) | + +## PR deploys + +A PR deploy creates one zonal MIG `zebrad-${branch}-${network}-${zone-letter}` in `zfnd-dev-zebra`, bootstrapped from the latest matching cache image (preferring images from the same branch, falling back to `main`, then any branch). Lives until you reap it. + +### Run one + +```bash +gh workflow run zfnd-deploy-nodes-gcp.yml -R ZcashFoundation/zebra \ + --ref my-branch \ + -f network=Mainnet \ + -f zone=us-east1-b \ + -f environment=dev \ + -f need_cached_disk=true \ + -f cached_disk_type=tip +``` + +### Find yours + +```bash +gcloud compute instance-groups managed list --project $P \ + --filter="labels.created_by=workflow_dispatch AND labels.github_ref=my-branch-slug" +``` + +### Spare from cleanup + +Apply one of two labels: + +- `keep_until=YYYY-MM-DD` — self-expiring; eligible for reaping after the date passes. +- `delete_protection=true` — indefinite; requires manual removal before reaping. + +Apply to the instance and disk now if you need protection immediately (labels propagate to new instances on the next template swap): + +```bash +MIG=zebrad-my-branch-mainnet-b +ZONE=us-east1-b +INSTANCE=$(gcloud compute instance-groups managed list-instances "$MIG" \ + --zone "$ZONE" --project $P --format='value(instance.basename())' | head -1) +gcloud compute instances add-labels "$INSTANCE" --zone "$ZONE" --project $P \ + --labels="keep_until=2026-05-01" +gcloud compute disks add-labels "zebrad-cache-my-branch-mainnet-b" \ + --zone "$ZONE" --project $P \ + --labels="keep_until=2026-05-01" +``` + +### Reap one + +The stateful policy `auto-delete=on-permanent-instance-deletion` removes the disk with the MIG. + +```bash +gcloud compute instance-groups managed delete "$MIG" --zone "$ZONE" --project $P --quiet +``` + +### Sweep expired + +```bash +TODAY=$(date +%Y-%m-%d) +for line in $(gcloud compute instance-groups managed list --project $P \ + --filter="labels.created_by=workflow_dispatch" \ + --format='csv[no-heading](name,zone.basename(),labels.keep_until,labels.delete_protection)' \ + | awk -F, -v today="$TODAY" '$4 == "true" || $3 == "" { next } $3 < today { print $1 "|" $2 }'); do + MIG="${line%|*}" + ZONE="${line#*|}" + echo "Reaping $MIG in $ZONE" + gcloud compute instance-groups managed delete "$MIG" --zone "$ZONE" --project $P --quiet +done +``` + +Review the candidate list before piping into delete. + +## Diagnose a stuck MIG + +```bash +MIG=zebrad-main-mainnet-b +ZONE=us-east1-b + +gcloud compute instance-groups managed describe "$MIG" --zone "$ZONE" --project $P \ + --format="value(currentActions,status.isStable,status.versionTarget.isReached)" +gcloud compute instance-groups managed list-instances "$MIG" --zone "$ZONE" --project $P \ + --format="table(NAME,STATUS,HEALTH_STATE,LAST_ERROR)" +gcloud compute instance-groups managed list-errors "$MIG" --zone "$ZONE" --project $P --limit=10 +``` + +When `list-errors` reports `RESOURCE_IN_USE_BY_ANOTHER_RESOURCE` on a stateful disk, find the squatter: + +```bash +DISK=zebrad-cache-main-mainnet-b +gcloud compute disks describe "$DISK" --zone "$ZONE" --project $P \ + --format="value(users.basename())" +``` + +The squatter is another MIG's instance. Reap that MIG (recipe above). + +## Recover a corrupted cache disk + +When Zebra crash-loops on a zonal disk, recover that one zone from the last good cache image. Sister zones keep serving. + +```bash +NET=mainnet; ZONE=us-east1-b; ZONE_LETTER=b +MIG=zebrad-main-${NET}-${ZONE_LETTER} +DISK=zebrad-cache-main-${NET}-${ZONE_LETTER} +TS=$(date +%Y%m%d-%H%M) + +# 1. Drain: preserve the disk via auto-delete=never, then scale to 0 +INSTANCE=$(gcloud compute instance-groups managed list-instances "$MIG" \ + --zone "$ZONE" --project $P --format='value(name)' | head -1) +gcloud compute instance-groups managed instance-configs update "$MIG" \ + --zone "$ZONE" --project $P --instance "$INSTANCE" \ + --stateful-disk "device-name=${DISK},auto-delete=never" +gcloud compute instance-groups managed resize "$MIG" --size 0 --zone "$ZONE" --project $P + +# 2. Snapshot the corrupted disk for forensics, then delete it +gcloud compute snapshots create "${DISK}-corrupted-${TS}" \ + --source-disk "$DISK" --source-disk-zone "$ZONE" --project $P +gcloud compute disks delete "$DISK" --zone "$ZONE" --project $P --quiet + +# 3. Redeploy via workflow_dispatch for this single zone +gh workflow run zfnd-deploy-nodes-gcp.yml -R ZcashFoundation/zebra \ + -f network=Mainnet -f zone="$ZONE" -f environment=dev \ + -f need_cached_disk=true -f cached_disk_type=tip +``` + +For production: set `P=zfnd-prod-zebra`, `MIG=zebrad-${NET}-${ZONE_LETTER}`, `DISK=zebrad-cache-${NET}-${ZONE_LETTER}`. Production has no automatic cache image; restore from a recent operator-taken snapshot. + +## DB-format-version-break release + +When `zebra-state/src/constants.rs::DATABASE_FORMAT_VERSION` changes in a backwards-incompatible way, the in-place rolling swap fails because the new Zebra can't read the old RocksDB. Do it one zone at a time with snapshot-based handoff: + +```bash +P=zfnd-prod-zebra; NET=mainnet; TS=$(date +%Y%m%d-%H%M) + +for Z in b c d; do + MIG=zebrad-${NET}-${Z}; DISK=zebrad-cache-${NET}-${Z}; ZONE=us-east1-${Z} + + # Snapshot before touching + gcloud compute snapshots create "${DISK}-pre-major-${TS}" \ + --source-disk "$DISK" --source-disk-zone "$ZONE" --project $P + + # Drain, preserving disk + INSTANCE=$(gcloud compute instance-groups managed list-instances "$MIG" \ + --zone "$ZONE" --project $P --format='value(name)' | head -1) + gcloud compute instance-groups managed instance-configs update "$MIG" \ + --zone "$ZONE" --project $P --instance "$INSTANCE" \ + --stateful-disk "device-name=${DISK},auto-delete=never" + gcloud compute instance-groups managed resize "$MIG" --size 0 --zone "$ZONE" --project $P +done + +# Publish the release; the workflow's release path recreates each zonal MIG +# with the new template, which attaches the preserved disks. + +# Rollback (only if the release fails): restore disks from snapshots, deploy previous tag +for Z in b c d; do + DISK=zebrad-cache-${NET}-${Z}; ZONE=us-east1-${Z} + gcloud compute disks delete "$DISK" --zone "$ZONE" --project $P --quiet + gcloud compute disks create "$DISK" --zone "$ZONE" --project $P \ + --source-snapshot "${DISK}-pre-major-${TS}" +done +``` + +## Regional-to-zonal migration + +One-shot procedure to convert an environment from the old regional architecture (one regional MIG per network holding three instances) to zonal (three zonal MIGs per network, one instance each). Run once per environment. + +```bash +P=zfnd-prod-zebra # or zfnd-dev-zebra +NET=mainnet +OLD_MIG=zebrad-${NET} # for prod; use zebrad-main-${NET} for dev +OLD_DISK=zebrad-cache-${NET} # for prod; use zebrad-cache-main-${NET} for dev +TS=$(date +%Y%m%d-%H%M) + +# 1. Snapshot each zonal disk (three per network) +for Z in b c d; do + gcloud compute snapshots create "${OLD_DISK}-${Z}-pre-zonal-${TS}" \ + --source-disk "$OLD_DISK" --source-disk-zone "us-east1-${Z}" --project $P +done + +# 2. Delete the old regional MIG (disks auto-deleted; we have snapshots) +gcloud compute instance-groups managed delete "$OLD_MIG" \ + --region us-east1 --project $P --quiet + +# 3. Create zonal disks from the snapshots with the new name scheme +# Prod: zebrad-cache-${NET}-${Z} +# Staging: zebrad-cache-main-${NET}-${Z} +for Z in b c d; do + NEW_DISK=zebrad-cache-${NET}-${Z} # or zebrad-cache-main-${NET}-${Z} + gcloud compute disks create "$NEW_DISK" \ + --zone "us-east1-${Z}" --project $P \ + --source-snapshot "${OLD_DISK}-${Z}-pre-zonal-${TS}" \ + --size 400 --type pd-balanced +done + +# 4. Trigger workflow_dispatch for each (network, zone). The workflow sees +# the zonal disks exist and attaches them; no fresh bootstrap from cache. +for Z in b c d; do + gh workflow run zfnd-deploy-nodes-gcp.yml -R ZcashFoundation/zebra \ + -f network=Mainnet -f zone="us-east1-${Z}" \ + -f environment=$([ "$P" = "zfnd-prod-zebra" ] && echo prod || echo dev) \ + -f need_cached_disk=false +done +``` + +Repeat for Testnet. After all six (2 networks × 3 zones) zonal MIGs are HEALTHY, the migration for this environment is complete; retire the pre-zonal snapshots after a hold period. + +## Reference + +**Static IPs** are externally provisioned and map deterministically to zones: + +- `us-east1-b` → `zebra-${network}` (primary) +- `us-east1-c` → `zebra-${network}-secondary` +- `us-east1-d` → `zebra-${network}-tertiary` + +The workflow assigns them via per-instance configs for push and release deploys; workflow_dispatch deploys use ephemeral IPs. Reserve new ones manually with `gcloud compute addresses create` before adding capacity. + +**Cache images** are produced by `zfnd-ci-integration-tests-gcp.yml`'s `create-state-image` job. Naming pattern: `{prefix}-{branch}-{sha}-v{state-version}-{network}-{tip|checkpoint}[-u]-{HHMMSS}`. One image per network seeds all three zones. Lookup priority: current branch, then `main`, then any branch; most recent first. List recent: + +```bash +gcloud compute images list --project zfnd-dev-zebra \ + --filter="name~^zebrad-cache-.*-v27-mainnet-tip" \ + --sort-by=~creationTimestamp --limit=5 +``` + +**Daily cleanup** (`zfnd-delete-gcp-resources.yml`) sweeps old instances, templates, disks, and cache images by age and name. It does not honor `keep_until` or `delete_protection` labels on PR deploys; use the [sweep recipe](#sweep-expired) for label-aware control. The twelve stable cache disks (`zebrad-cache-{mainnet,testnet}-{b,c,d}` in production, `zebrad-cache-main-{mainnet,testnet}-{b,c,d}` in stage) are never eligible because GCP refuses to delete attached disks; if any stable disk ever shows up unattached, that is itself an incident. diff --git a/book/src/dev/profiling-and-benchmarking.md b/book/src/dev/profiling-and-benchmarking.md index 9d60ac773d3..678fe66cc0a 100644 --- a/book/src/dev/profiling-and-benchmarking.md +++ b/book/src/dev/profiling-and-benchmarking.md @@ -8,13 +8,13 @@ To profile Zebra, you can use the [samply](https://github.com/mstange/samply) profiler. Once you have it installed, you can run: ```bash -sudo samply record zebrad +cargo build --profile profiling +sudo samply record ./target/profiling/zebrad ``` -where `zebrad` is the binary you want to inspect. You can then press `Ctrl+c`, -and the profiler will instruct you to navigate your web browser to - where you can snoop around the call stack to see where -Zebra loafs around the most. +You can then press `Ctrl+c`, and the profiler will instruct you to navigate your +web browser to where you can snoop around the call stack +to see where Zebra loafs around the most. ## Benchmarking diff --git a/book/src/user/custom-testnets.md b/book/src/user/custom-testnets.md index 7367482019b..9501133a3e3 100644 --- a/book/src/user/custom-testnets.md +++ b/book/src/user/custom-testnets.md @@ -140,8 +140,8 @@ Aside from the configurable parameters, custom Testnets in Zebra validate the sa Zebra's Testnet validates almost all of the same consensus rules as Mainnet, the differences are: -- Constants defined in the `zcash_primitives::consensus::Parameters` trait, which includes but may not be limited to: - - Zcash address prefixes (see [`NetworkConstants`](https://docs.rs/zcash_primitives/latest/zcash_primitives/consensus/trait.NetworkConstants.html)), and coin type, which is `133` on `Mainnet` or `1` elsewhere. +- Constants defined in the `zcash_protocol::consensus::Parameters` trait, which includes but may not be limited to: + - Zcash address prefixes (see [`NetworkConstants`](https://docs.rs/zcash_protocol/latest/zcash_protocol/consensus/trait.NetworkConstants.html)), and coin type, which is `133` on `Mainnet` or `1` elsewhere. - Network upgrade activation heights. - Constants defined in Zebra: - `PoWLimit` defined in the Zcash protocol specification, or target difficulty limit, which is `2^243 - 1` on Mainnet and `2^251 - 1` on the default Testnet. diff --git a/book/src/user/docker.md b/book/src/user/docker.md index 312af5be446..faf0d25de23 100644 --- a/book/src/user/docker.md +++ b/book/src/user/docker.md @@ -8,24 +8,17 @@ To get Zebra quickly up and running, you can use an off-the-rack image from [Docker Hub](https://hub.docker.com/r/zfnd/zebra/tags): ```shell -docker run --name zebra zfnd/zebra -``` - -If you want to preserve Zebra's state, you can create a Docker volume: - -```shell -docker volume create zebrad-cache -``` - -And mount it before you start the container: - -```shell -docker run \ - --mount source=zebrad-cache,target=/home/zebra/.cache/zebra \ +docker run -d \ --name zebra \ + -p 8233:8233 \ + -v zebrad-cache:/home/zebra/.cache/zebra \ zfnd/zebra ``` +The `-p 8233:8233` flag publishes Zebra's P2P port so other Zcash nodes can +connect to yours (use `-p 18233:18233` for Testnet), and `-v` mounts a named +volume so the chain state survives container restarts. + You can also use `docker compose`, which we recommend. First get the repo: ```shell @@ -39,6 +32,8 @@ Then run: docker compose -f docker/docker-compose.yml up ``` +The default compose file already exposes the Mainnet P2P port. + ## Custom Images If you want to use your own images with, for example, some opt-in compilation @@ -62,16 +57,18 @@ See [Building Zebra](https://github.com/ZcashFoundation/zebra#manual-build) for Zebra supports various features that can be enabled during build time using the `FEATURES` build argument: -For example, if we'd like to enable metrics on the image, we'd build it using the following `build-arg`: +For example, if you'd like to add an extra feature on top of the default release feature set, you'd build it using the following `build-arg`: > [!IMPORTANT] -> To fully use and display the metrics, you'll need to run a Prometheus and Grafana server, and configure it to scrape and visualize the metrics endpoint. This is explained in more detailed in the [Metrics](https://zebra.zfnd.org/user/metrics.html#zebra-metrics) section of the User Guide. +> Some optional features need extra runtime services or configuration. Check the +> matching User Guide page for the feature you enable before using the image in +> production. ```shell # Build with specific features docker build -f ./docker/Dockerfile --target runtime \ - --build-arg FEATURES="default-release-binaries prometheus" \ - --tag zebra:metrics . + --build-arg FEATURES="default-release-binaries elasticsearch" \ + --tag zebra:custom-features . ``` All available Cargo features are listed at @@ -153,6 +150,34 @@ ports: For Kubernetes, configure liveness and readiness probes against `/healthy` and `/ready` respectively. See the [Health Endpoints](./health.md) page for details. +### P2P Networking + +Zebra uses TCP port 8233 on Mainnet and 18233 on Testnet for peer-to-peer connections. When running in Docker, publish this port with `-p` (as shown in the [Quick Start](#quick-start)) so other nodes can connect to yours. Without it, Zebra still syncs via outbound connections but does not accept inbound peers. + +If Zebra is behind a NAT, firewall, or load balancer, set `external_addr` so it advertises your public address to peers instead of the internal bind address: + +```toml +[network] +external_addr = "203.0.113.42:8233" +``` + +Or via environment variable: + +```shell +-e ZEBRA_NETWORK__EXTERNAL_ADDR=203.0.113.42:8233 +``` + +For reference, the ports Zebra can use are: + +| Port | Protocol | Purpose | Default | +|-------|----------|--------------------|----------| +| 8233 | TCP | P2P (Mainnet) | Enabled | +| 18233 | TCP | P2P (Testnet) | Enabled | +| 8232 | TCP | RPC (Mainnet) | Disabled | +| 18232 | TCP | RPC (Testnet) | Disabled | +| 9999 | TCP | Prometheus metrics | Disabled | +| 8080 | TCP | Health endpoints | Disabled | + ## Examples To make the initial setup of Zebra with other services easier, we provide some @@ -174,28 +199,22 @@ directly in `docker/docker-compose.lwd.yml` (or an accompanying `.env` file). ### Running Zebra with Prometheus and Grafana -The following commands will run Zebra with Prometheus and Grafana: +The following commands will run Zebra with the observability stack (Prometheus, +Grafana, Jaeger, and AlertManager): ```shell -docker compose -f docker/docker-compose.grafana.yml build --no-cache -docker compose -f docker/docker-compose.grafana.yml up +docker compose -f docker/docker-compose.observability.yml build --no-cache +docker compose -f docker/docker-compose.observability.yml up ``` -In this example, we build a local Zebra image with the `prometheus` Cargo -compilation feature. Note that we enable this feature by specifying its name in -the build arguments. Having this Cargo feature specified at build time makes -`cargo` compile Zebra with the metrics support for Prometheus enabled. Note that -we also specify this feature as an environment variable at run time. Having this -feature specified at run time makes Docker's entrypoint script configure Zebra -to open a scraping endpoint on `localhost:9999` for Prometheus. - -Once all services are up, the Grafana web UI should be available at -`localhost:3000`, the Prometheus web UI should be at `localhost:9090`, and -Zebra's scraping page should be at `localhost:9999`. The default login and -password for Grafana are both `admin`. To make Grafana use Prometheus, you need -to add Prometheus as a data source with the URL `http://localhost:9090` in -Grafana's UI. You can then import various Grafana dashboards from the `grafana` -directory in the Zebra repo. +This builds a local Zebra image with the default release feature set, which now includes OpenTelemetry support, and starts all observability services. Once running: + +- Grafana: `http://localhost:3000` (default login: admin/admin) +- Prometheus: `http://localhost:9094` +- Jaeger: `http://localhost:16686` +- Zebra metrics: `http://localhost:9999` + +See `docker/observability/README.md` for dashboard setup and configuration. ### Running CI Tests Locally diff --git a/book/src/user/mining-docker.md b/book/src/user/mining-docker.md index b2d29f0431f..4c132ac3025 100644 --- a/book/src/user/mining-docker.md +++ b/book/src/user/mining-docker.md @@ -7,11 +7,19 @@ configuration instructions](https://zebra.zfnd.org/user/mining.html). Using docker, you can start mining by running: ```bash -docker run --name -zebra_local -e MINER_ADDRESS="t3dvVE3SQEi7kqNzwrfNePxZ1d4hUyztBA1" -e ZEBRA_RPC_PORT=8232 -p 8232:8232 zfnd/zebra:latest +docker run -d --name zebra_local \ + -e MINER_ADDRESS="t3dvVE3SQEi7kqNzwrfNePxZ1d4hUyztBA1" \ + -e ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:8232 \ + -p 8233:8233 \ + -p 8232:8232 \ + -v zebrad-cache:/home/zebra/.cache/zebra \ + zfnd/zebra:latest ``` -This command starts a container on Mainnet and binds port 8232 on your Docker -host. If you want to start generating blocks, you need to let Zebra sync first. +This command starts a container on Mainnet and binds the P2P port (8233) and +the RPC port (8232) on your Docker host. The P2P port lets other Zcash nodes +connect to your node. If you want to start generating blocks, you need to let +Zebra sync first. Note that you must pass the address for your mining rewards via the `MINER_ADDRESS` environment variable when you are starting the container, as we @@ -19,9 +27,6 @@ did with the ZF funding stream address above. The address we used starts with the prefix `t1`, meaning it is a Mainnet P2PKH address. Please remember to set your own address for the rewards. -The port we mapped between the container and the host with the `-p` flag in the -example above is Zebra's default Mainnet RPC port. - Instead of listing the environment variables on the command line, you can use Docker's `--env-file` flag to specify a file containing the variables. You can find more info here @@ -37,11 +42,18 @@ variable to `Testnet` and use a Testnet address for the rewards. For example, running ```bash -docker run --name zebra_local -e ZEBRA_NETWORK__NETWORK="Testnet" -e MINER_ADDRESS="t27eWDgjFYJGVXmzrXeVjnb5J3uXDM9xH9v" -e ZEBRA_RPC_PORT=18232 -p 18232:18232 zfnd/zebra:latest +docker run -d --name zebra_local \ + -e ZEBRA_NETWORK__NETWORK="Testnet" \ + -e MINER_ADDRESS="t27eWDgjFYJGVXmzrXeVjnb5J3uXDM9xH9v" \ + -e ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:18232 \ + -p 18233:18233 \ + -p 18232:18232 \ + -v zebrad-cache:/home/zebra/.cache/zebra \ + zfnd/zebra:latest ``` -will start a container on Testnet and bind port 18232 on your Docker host, which -is the standard Testnet RPC port. Notice that we also used a different rewards +will start a container on Testnet and bind the P2P port (18233) and the RPC port +(18232) on your Docker host. Notice that we also used a different rewards address. It starts with the prefix `t2`, indicating that it is a Testnet address. A Mainnet address would prevent Zebra from starting on Testnet, and conversely, a Testnet address would prevent Zebra from starting on Mainnet. diff --git a/book/src/user/requirements.md b/book/src/user/requirements.md index 7ab4d99af06..6090202a8a0 100644 --- a/book/src/user/requirements.md +++ b/book/src/user/requirements.md @@ -39,9 +39,16 @@ Zebra uses the following inbound and outbound TCP ports: If you configure Zebra with a specific [`listen_addr`](https://docs.rs/zebra-network/latest/zebra_network/config/struct.Config.html#structfield.listen_addr), it will advertise this address to other nodes for inbound connections. Outbound -connections are required to sync, inbound connections are optional. Zebra also -needs access to the Zcash DNS seeders, via the OS DNS resolver (usually port -53). +connections are required to sync, inbound connections are optional but +recommended. Zebra also needs access to the Zcash DNS seeders, via the OS DNS +resolver (usually port 53). + +If Zebra runs in Docker, publish the P2P port with `-p 8233:8233` (Mainnet) or +`-p 18233:18233` (Testnet) so other peers can connect to it. See the +[P2P section of the Docker guide](./docker.md#p2p-networking) for details. If +the node sits behind a firewall or NAT, open the P2P port and consider setting +[`external_addr`](https://docs.rs/zebra-network/latest/zebra_network/config/struct.Config.html#structfield.external_addr) +to your public IP so peers can discover it. Zebra makes outbound connections to peers on any port. But `zcashd` prefers peers on the default ports, so that it can't be used for DDoS attacks on other diff --git a/book/src/user/run.md b/book/src/user/run.md index 8d8979a5c74..c1f788c00de 100644 --- a/book/src/user/run.md +++ b/book/src/user/run.md @@ -49,6 +49,7 @@ Zebra release builds include several features by default: - `progress-bar` for terminal progress bars (see above) - `prometheus` for [Prometheus metrics](https://zebra.zfnd.org/user/metrics.html) - `sentry` for [Sentry monitoring](https://zebra.zfnd.org/user/tracing.html#sentry-production-monitoring) +- `opentelemetry` for [OpenTelemetry trace export](https://zebra.zfnd.org/user/tracing.html#opentelemetry-export) Additional [Cargo features](https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options) that require explicit enabling: diff --git a/book/src/user/tracing.md b/book/src/user/tracing.md index 333fd6b44c4..420a008c407 100644 --- a/book/src/user/tracing.md +++ b/book/src/user/tracing.md @@ -32,9 +32,28 @@ Zebra can generate [flamegraphs] of tracing spans. Activate flamegraphs using the `flamegraph` compile-time feature, and the [`flamegraph`][flamegraph] runtime config option. +## OpenTelemetry Export + +Official Zebra release builds include OpenTelemetry support. Export is disabled +until you configure an OpenTelemetry endpoint using the tracing config or the +`OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. + ## Sentry Production Monitoring -Compile Zebra with `--features sentry` to monitor it using [Sentry][sentry] in production. +Official Zebra release builds include Sentry support. Sentry is only activated +when the `SENTRY_DSN` environment variable is set. + +You can optionally set `SENTRY_ENVIRONMENT` to control the environment name +attached to Sentry events. Zebra also tags events with the git SHA when +available, preferring the runtime `GITHUB_SHA` (full commit SHA on GitHub +Actions) and falling back to the build-baked `SHORT_SHA` or `VERGEN_GIT_SHA`. +When it runs under GitHub Actions it reads standard `GITHUB_*` metadata plus +the optional `CI_TEST_ID` runtime variable for CI context. If `github-slug-action` exports `GITHUB_REF_POINT_SLUG_URL`, Zebra +uses that slugged branch or tag name for the `git.ref` tag, and CI workflows +can pass `CI_PR_NUMBER` and `CI_TEST_ID` for additional correlation. These +values are read at runtime, so container images do not need CI-specific build +arguments, and the `ZEBRA_*` environment namespace remains reserved for Zebra +configuration. [tracing_section]: https://docs.rs/zebrad/latest/zebrad/components/tracing/struct.InnerConfig.html [filter]: https://docs.rs/zebrad/latest/zebrad/components/tracing/struct.InnerConfig.html#structfield.filter diff --git a/book/src/user/troubleshooting.md b/book/src/user/troubleshooting.md index bfe5f56eee9..4f616b39a39 100644 --- a/book/src/user/troubleshooting.md +++ b/book/src/user/troubleshooting.md @@ -88,3 +88,49 @@ filter = 'info,zebra_network=debug' If you keep on seeing multiple info logs per second, please [open a bug.](https://github.com/ZcashFoundation/zebra/issues/new/choose) + +### Linux TCP tuning for block propagation + +On Linux, the kernel resets each TCP connection's congestion window after a short idle period +(`net.ipv4.tcp_slow_start_after_idle=1`, the default on most distros). Zcash's +pull-based, single-request-per-block propagation means most peer connections are +idle between blocks, so every full-block transfer starts from a cold congestion +window. On long-haul links this can cap single-peer throughput far below the +available bandwidth — even between nodes with 1–2 Gbps connections, observed +throughput during block propagation can be as low as ~6 Mbps. + +> **Warning: these settings are system-wide.** They affect _every_ TCP +> connection on the host and every program using it, not just Zebra. Review +> the implications before applying, particularly on multi-tenant or +> production hosts running unrelated workloads. + +The recommended sysctl config is: + +```text +# Disable slow-start-after-idle so TCP doesn't reset its congestion window +# between block requests. This is the highest-impact setting for full-block +# propagation on long-haul links. +net.ipv4.tcp_slow_start_after_idle=0 + +# Use CUBIC congestion control (already the default on most Linux distros; +# set explicitly so the configuration is self-documenting). +net.ipv4.tcp_congestion_control=cubic + +# Use fq_codel as the default queueing discipline. +net.core.default_qdisc=fq_codel +``` + +To apply persistently, write the block above to +`/etc/sysctl.d/99-zebra-network.conf` and reload with +`sudo sysctl --system`. To apply for the current boot only, use +`sudo sysctl -w =` for each line. + +Zebra logs a warning at startup on Linux if `tcp_slow_start_after_idle` is +enabled. + +#### Running Zebra in Docker + +These sysctls must be applied on the **host**, not inside the container. +Containers share the host's network stack settings for these knobs, so +setting them inside the container has no effect. Apply them on the +Docker host (and remember the system-wide caveat above). diff --git a/deny.toml b/deny.toml index 84a17aa4651..18ff47d0d3a 100644 --- a/deny.toml +++ b/deny.toml @@ -4,6 +4,53 @@ # * allow - No warning or error will be produced, though in some cases a note # will be +# This section is considered when running `cargo deny check advisories`. +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +version = 2 +yanked = "deny" +# Unmaintained crate advisories — all transitive via abscissa_core/structopt or direct. +# Remove each ignore entry as the dependency chain is upgraded. +ignore = [ + "RUSTSEC-2021-0139", # ansi_term — transitive via abscissa_core -> structopt + "RUSTSEC-2024-0375", # atty (unmaintained) — transitive via abscissa_core -> structopt + "RUSTSEC-2021-0145", # atty (unsound) — transitive via abscissa_core -> structopt + "RUSTSEC-2024-0370", # proc-macro-error — transitive via abscissa_core -> structopt + "RUSTSEC-2026-0173", # proc-macro-error2 (unmaintained fork of proc-macro-error) — transitive via abscissa_core -> structopt + "RUSTSEC-2025-0119", # number_prefix — transitive via indicatif + "RUSTSEC-2025-0141", # bincode — direct dependency +] + +# This section is considered when running `cargo deny check licenses`. +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +confidence-threshold = 0.8 +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "ISC", + "Zlib", + "Unicode-3.0", + "CC0-1.0", + "MPL-2.0", + "Unlicense", + "0BSD", + "MIT-0", + "CDLA-Permissive-2.0", + "CDDL-1.0", +] +exceptions = [] + +# rustls-webpki uses a custom ISC-style license +[[licenses.clarify]] +name = "rustls-webpki" +expression = "ISC" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + # This section is considered when running `cargo deny check bans`. # More documentation about the 'bans' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html @@ -50,6 +97,9 @@ skip = [ # dependencies starting at the specified crate, up to a certain depth, which is # by default infinite skip-tree = [ + # wait for bindgen to update itertools + { name = "itertools", version = "0.13" }, + # wait for ordered-map to release a dependency fix { name = "ordered-map", version = "=0.4.2" }, @@ -59,9 +109,6 @@ skip-tree = [ # wait for abscissa_core to upgrade {name = "tracing-log", version = "=0.1.4" }, - # Multiple base64 versions through different dependencies - { name = "base64", version = "=0.21.7" }, - # wait for abscissa_core to update toml { name = "toml", version = "=0.5.11" }, @@ -85,18 +132,38 @@ skip-tree = [ { name = "sha2", version = "=0.10.9" }, { name = "ripemd", version = "=0.1.3" }, + # pre-release RustCrypto crates pulled in by librustzcash 2026-04 release wave; + # remove once the broader ecosystem catches up to the same pre-release line + { name = "block-buffer", version = "=0.11.0-rc.3" }, + { name = "crypto-common", version = "=0.2.0-rc.1" }, + { name = "digest", version = "=0.11.0-pre.9" }, + + # console 0.15.x is pulled by indicatif (via howudoin); 0.16.x by insta (dev-dep). + # remove once indicatif/howudoin update to console 0.16. + { name = "console", version = "=0.15.11" }, + + # hashbrown 0.16.x pulled by ed25519-zebra; 0.17.x by indexmap 2.14 (transitive of librustzcash). + # remove once ed25519-zebra updates hashbrown. + { name = "hashbrown", version = "=0.16.1" }, + + # toml 0.9.x pulled by zebrad direct; 1.1.x by config 0.15 (also via zebrad). + # toml 1.x is the spec-1.1 line; remove once zebrad migrates its direct dep. + { name = "toml", version = "=0.9.12+spec-1.1.0" }, + + # winnow 0.7.x pulled by toml 0.9.x; 1.x by toml 1.x / config 0.15. + # resolved together with the toml entry above. + { name = "winnow", version = "=0.7.15" }, + # wait for abscissa_core to update synstructure { name = "synstructure", version = "=0.12.6" }, - # wait for librustzcash to update tonic - { name = "tonic", version = "0.12" }, - { name = "prost-types", version = "0.13" }, - { name = "windows-sys", version = "=0.52.0" }, { name = "indexmap", version = "1.9" }, { name = "hashbrown", version = "0.12" }, + # hashbrown 0.14.x pulled by indexmap 1.9 (transitive of librustzcash) + { name = "hashbrown", version = "0.14" }, # hashbrown 0.15.x pulled by various deps; 0.16.x pulled by indexmap 2.x { name = "hashbrown", version = "0.15" }, @@ -104,25 +171,20 @@ skip-tree = [ { name = "itertools", version = "0.13" }, { name = "getrandom", version = "0.2" }, + { name = "getrandom", version = "0.3" }, { name = "rand", version = "0.8" }, { name = "rand_core", version = "0.6" }, { name = "rand_chacha", version = "0.3" }, - # TODO: Remove once https://github.com/zcash/librustzcash/pull/1893 merges. - { name = "equihash", version = "0.2" }, - # wait for derive_builder to update { name = "darling", version = "0.20.11" }, # wait until `config` updates `convert_case` { name = "convert_case", version = "0.6.0" }, - # wait until `config` updates `toml` - { name = "toml_datetime", version = "0.7.5"}, - - # - { name = "getrandom", version = "0.3.4" }, + # wait for hashlink/petgraph/metrics-util to update hashbrown and foldhash + { name = "hashbrown", version = "0.15" }, ] # This section is considered when running `cargo deny check sources`. @@ -132,24 +194,23 @@ skip-tree = [ # Lint level for what to happen when a crate from a crate registry that is not # in the allow list is encountered unknown-registry = "deny" + # Lint level for what to happen when a crate from a git repository that is not # in the allow list is encountered unknown-git = "deny" + # List of URLs for allowed crate registries. Defaults to the crates.io index # if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] + # List of URLs for allowed Git repositories +# TODO: Remove the URLs once the patch table in Cargo.toml is removed. allow-git = [ - "https://github.com/zcash/librustzcash.git", - "https://github.com/zcash/incrementalmerkletree", - "https://github.com/zcash/orchard", - "https://github.com/zcash/sapling-crypto", - - "https://github.com/zcash/halo2", "https://github.com/zcash/sinsemilla", "https://github.com/zcash/zcash_note_encryption", # TODO: remove these QED-it fork entries once the required changes are merged upstream. + "https://github.com/QED-it/halo2", "https://github.com/QED-it/librustzcash", "https://github.com/QED-it/orchard", "https://github.com/QED-it/sapling-crypto", diff --git a/docker/.env b/docker/.env index 629d7d6b9c7..0c6a78349ba 100644 --- a/docker/.env +++ b/docker/.env @@ -6,22 +6,28 @@ # # ZEBRA_CONF_PATH="/path/to/your/custom/zebrad.toml" -# Sets the network Zebra runs will run on. +# Sets the network Zebra will run on. # -# NETWORK=Mainnet +# ZEBRA_NETWORK__NETWORK=Mainnet -# Zebra's RPC server is disabled by default. To enable it, set its port number. +# If behind a NAT, firewall, or load balancer, set this to your public IP:port +# so other Zcash nodes can connect to yours. Port 8233 for Mainnet, 18233 for +# Testnet. # -# ZEBRA_RPC_PORT=8232 # Default RPC port number on Mainnet. -# ZEBRA_RPC_PORT=18232 # Default RPC port number on Testnet. +# ZEBRA_NETWORK__EXTERNAL_ADDR=203.0.113.42:8233 + +# Zebra's RPC server is disabled by default. To enable it, set the listen address. +# +# ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:8232 # Mainnet +# ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:18232 # Testnet # To disable cookie authentication, set the value below to false. # -# ENABLE_COOKIE_AUTH=true +# ZEBRA_RPC__ENABLE_COOKIE_AUTH=true # Sets a custom directory for the cookie authentication file. # -# ZEBRA_COOKIE_DIR="/home/zebra/.config/cookie" +# ZEBRA_RPC__COOKIE_DIR="/home/zebra/.config/cookie" # Sets a custom directory for the state and network caches. # diff --git a/docker/Dockerfile b/docker/Dockerfile index c58f145bd49..52774ae31d2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,7 +13,7 @@ # We first set default values for build arguments used across the stages. # Each stage must define the build arguments (ARGs) it uses. -ARG RUST_VERSION=1.89.0 +ARG RUST_VERSION=1.91.0 ARG FEATURES="default-release-binaries" @@ -83,9 +83,9 @@ WORKDIR ${HOME} ARG CARGO_HOME ARG CARGO_TARGET_DIR -# Download and install the pre-built cargo-nextest binary. +# Download and install cargo-nextest, curl and librocksdb-dev. RUN apt-get -qq update && \ - apt-get -qq install -y --no-install-recommends curl && \ + apt-get -qq install -y --no-install-recommends librocksdb-dev curl && \ mkdir -p "${CARGO_HOME}/bin" && \ case ${BUILDPLATFORM:-linux/$(uname -m)} in \ "linux/amd64"|"linux/x86_64") ARCH="linux" ;; \ @@ -97,6 +97,9 @@ RUN apt-get -qq update && \ apt-get -qq autoremove -y && \ rm -rf /var/lib/apt/lists/* /tmp/* +# Link RocksDB dynamically for faster test builds. +ENV ROCKSDB_LIB_DIR="/usr/lib/" + # Copy source code first to ensure consistent timestamps COPY --link --chown=${UID}:${GID} ./ ${HOME} diff --git a/docker/default-zebra-config.toml b/docker/default-zebra-config.toml index 1e962718b80..a27359de781 100644 --- a/docker/default-zebra-config.toml +++ b/docker/default-zebra-config.toml @@ -14,6 +14,11 @@ [network] network = "Mainnet" listen_addr = "[::]:8233" + +# Set to your public IP:port if behind a NAT or load balancer. + +# external_addr = "203.0.113.42:8233" + cache_dir = "/home/zebra/.cache/zebra" [rpc] diff --git a/docker/docker-compose.lwd.yml b/docker/docker-compose.lwd.yml index 97a729f9b40..9d2a22ff973 100644 --- a/docker/docker-compose.lwd.yml +++ b/docker/docker-compose.lwd.yml @@ -16,7 +16,8 @@ services: - ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:8232 - ZEBRA_RPC__ENABLE_COOKIE_AUTH=false ports: - - "8232:8232" + - "8233:8233" # P2P (inbound peer connections) + - "8232:8232" # RPC (used by lightwalletd) lightwalletd: image: electriccoinco/lightwalletd diff --git a/docker/docker-compose.observability.yml b/docker/docker-compose.observability.yml index 0607a763dc0..cebba2855ab 100644 --- a/docker/docker-compose.observability.yml +++ b/docker/docker-compose.observability.yml @@ -16,8 +16,8 @@ services: dockerfile: docker/Dockerfile target: runtime args: - # Enable OpenTelemetry feature for tracing - FEATURES: "default-release-binaries opentelemetry" + # OpenTelemetry tracing is included transitively in default-release-binaries. + FEATURES: "default-release-binaries" volumes: - zebrad-cache:/home/zebra/.cache/zebra tty: true @@ -32,8 +32,9 @@ services: - ZEBRA_TRACING__OPENTELEMETRY_SERVICE_NAME=zebra - ZEBRA_TRACING__OPENTELEMETRY_SAMPLE_PERCENT=100 ports: - - "9999:9999" - - "8232:8232" + - "8233:8233" # P2P (inbound peer connections) + - "9999:9999" # Prometheus metrics + - "8232:8232" # RPC networks: - observability depends_on: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d22063a0ee3..c45fb61906f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -21,14 +21,14 @@ services: - source: zebra-config target: /home/zebra/.config/zebrad.toml - # Uncomment the `ports` mapping below to map ports between the container and - # host. - # - # ports: + # P2P port for inbound peer connections (recommended). + ports: + - "8233:8233" # P2P on Mainnet (use 18233:18233 for Testnet) + + # Uncomment any of these to expose additional services: + # - "18233:18233" # P2P on Testnet # - "8232:8232" # RPC endpoint on Mainnet # - "18232:18232" # RPC endpoint on Testnet - # - "8233:8233" # peer connections on Mainnet - # - "18233:18233" # peer connections on Testnet # - "9999:9999" # Metrics # - "3000:3000" # Tracing # - "8080:8080" # Health endpoints (/healthy, /ready) diff --git a/docker/mining/.env.example b/docker/mining/.env.example new file mode 100644 index 00000000000..50d76390722 --- /dev/null +++ b/docker/mining/.env.example @@ -0,0 +1,38 @@ +# Zebra + S-NOMP Mining Pool Configuration +# ======================================== + +# === REQUIRED === +# Mining address, all block rewards go to this address. +MINER_ADDRESS= + +# === OPTIONAL COINBASE DATA === +# Data included in coinbase transaction (max 94 bytes). Hex or UTF-8 encoded. +# MINER_DATA= +# Shielded memo in coinbase output (max 512 bytes). Only for shielded addresses. +# MINER_MEMO= + +# === NETWORK SELECTION === +# Options: Mainnet, Testnet +NETWORK=Testnet + +# === PORTS === +# Uncomment and modify to override defaults +# RPC_PORT=18232 # 18232 Testnet, 8232 Mainnet (internal, not exposed) +# PEER_PORT=18233 # 18233 Testnet, 8233 Mainnet (P2P network) +# STRATUM_PORT=3333 # Miners connect to this port + +# === NHEQMINER (optional, use --profile miner) === +# Worker name suffix (address.WORKER_NAME) +# WORKER_NAME=docker +# CPU threads (default: 1) +# CPU_THREADS=1 + +# === USAGE === +# 1. Copy to .env: cp .env.example .env +# 2. Set MINER_ADDRESS +# 3. Start: docker compose up -d +# 4. Check sync: docker compose logs -f zebra +# 5. Once synced, either: +# a) Start built-in miner: docker compose --profile miner up -d nheqminer +# b) Connect external miner: nheqminer -l :3333 -u
.worker1 -t +# 6. Web UI: http://:8080 diff --git a/docker/mining/README.md b/docker/mining/README.md new file mode 100644 index 00000000000..ce257b13e46 --- /dev/null +++ b/docker/mining/README.md @@ -0,0 +1,252 @@ +# Zebra + S-NOMP Mining Pool + +Docker Compose setup for running a Zcash mining pool with Zebra and S-NOMP. + +## Architecture + +``` +┌─────────────┐ ┌───────────┐ ┌─────────────┐ +│ Miners │─────▶│ S-NOMP │────▶│ Zebra │ +│ (nheqminer, │:3333 │ (Stratum) │:RPC │ (Full Node) │ +│ Antminer) │ │ │ │ │ +└─────────────┘ └──────┬────┘ └─────────────┘ + │ + ▼ + ┌─────────┐ + │ Redis │ + │ (Stats) │ + └─────────┘ +``` + +- **Zebra**: Zcash full node - validates blocks, provides `getblocktemplate` +- **S-NOMP**: Stratum mining pool - distributes work to miners, submits blocks +- **Redis**: Stores share counts and pool statistics + +All block rewards go to the address configured in `MINER_ADDRESS`. + +## Quick Start + +```bash +# 1. Configure +cp .env.example .env +# Edit .env - set MINER_ADDRESS (required) + +# 2. Start +docker compose up -d + +# 3. Wait for Zebra to sync (hours) +docker compose logs -f zebra + +# 4. Once synced, start mining (choose one): + +# Option A: Built-in nheqminer container +docker compose --profile miner up -d nheqminer + +# Option B: External miner +nheqminer -l :3333 -u
.worker1 -t +``` + +## Configuration + +Edit `.env` to configure: + +| Variable | Default | Description | +|-----------------|------------|----------------------------------------------| +| `MINER_ADDRESS` | (required) | Transparent address for block rewards | +| `NETWORK` | `Testnet` | `Mainnet` or `Testnet` | +| `RPC_PORT` | `18232` | Zebra RPC port (18232 Testnet, 8232 Mainnet) | +| `PEER_PORT` | `18233` | P2P port (18233 Testnet, 8233 Mainnet) | +| `STRATUM_PORT` | `3333` | Port miners connect to | +| `WORKER_NAME` | `docker` | Worker name suffix (for nheqminer container) | +| `CPU_THREADS` | `1` | CPU threads for nheqminer | + +## Checking Sync Status + +Zebra must fully sync before mining can begin. + +```bash +# Quick status +docker exec zebra curl -s -H "Content-Type: application/json" \ + localhost:18232 -d '{"jsonrpc":"2.0","id":1,"method":"getblockchaininfo","params":[]}' \ + | grep -oE '"(blocks|estimatedheight)":[0-9]+' + +# Watch sync progress +docker logs -f zebra 2>&1 | grep -E "state_tip|verified" + +# Detailed info +docker exec zebra curl -s -H "Content-Type: application/json" \ + localhost:18232 -d '{"jsonrpc":"2.0","id":1,"method":"getblockchaininfo","params":[]}' +``` + +## Connecting Miners + +### Built-in nheqminer Container + +The setup includes an optional nheqminer container for CPU mining. It uses a Docker Compose profile, so it won't start by default. + +```bash +# Start the miner (after Zebra is synced) +docker compose --profile miner up -d nheqminer + +# View miner logs +docker compose logs -f nheqminer + +# Stop the miner +docker compose --profile miner stop nheqminer + +# Adjust CPU threads in .env +CPU_THREADS=4 +``` + +### External nheqminer (CPU/GPU) + +```bash +# CPU mining +./nheqminer -l :3333 -u .worker1 -t + +# Example +./nheqminer -l 192.168.1.100:3333 -u t27eWDgjFYJGVXmzrXeVjnb5J3uXDM9xH9v.rig1 -t 4 +``` + +### Antminer Z15 + +1. Open miner web interface +2. Go to **Miner Configuration** +3. Set: + - **Pool URL**: `stratum+tcp://:3333` + - **Worker**: `.z15` + - **Password**: `x` + +## Web Interface + +S-NOMP provides a web UI at `http://:8080` showing: + +- Pool hashrate +- Connected workers +- Found blocks +- Per-worker statistics + +API endpoint: `http://:8080/api/stats` + +## Ports + +| Port | Service | Purpose | +|------------|---------|-------------------------------| +| 3333 | S-NOMP | Stratum (miners connect here) | +| 8080 | S-NOMP | Web UI and API | +| 18233/8233 | Zebra | P2P network (Testnet/Mainnet) | + +Internal only (not exposed): + +- 18232/8232: Zebra RPC +- 6379: Redis + +## Operations + +```bash +# Start all services (without miner) +docker compose up -d + +# Start all services including miner +docker compose --profile miner up -d + +# Stop all services +docker compose down + +# View logs +docker compose logs -f # All services +docker compose logs -f zebra # Zebra only +docker compose logs -f s-nomp # S-NOMP only +docker compose logs -f nheqminer # Miner only + +# Restart a service +docker compose restart s-nomp + +# Rebuild after updates +docker compose build --no-cache s-nomp +docker compose build --no-cache nheqminer +docker compose up -d s-nomp + +# Check service status +docker compose ps +docker compose --profile miner ps # Include miner + +# Shell into container +docker compose exec zebra bash +docker compose exec s-nomp bash +docker compose exec nheqminer bash +``` + +## Data Persistence + +Zebra chain data is stored in a Docker volume (`zebra-data`). This persists across container restarts. + +```bash +# View volume +docker volume ls | grep mining + +# Remove all data (will require full resync!) +docker compose down -v +``` + +## Troubleshooting + +### S-NOMP: "mempool is not active" + +Zebra is still syncing. Wait for sync to complete. + +```bash +docker compose logs zebra | tail -20 +``` + +### S-NOMP keeps restarting + +Check logs for errors: + +```bash +docker compose logs s-nomp | tail -50 +``` + +### Miners can't connect + +1. Check S-NOMP is running: `docker compose ps` +2. Check port is open: `nc -zv 3333` +3. Check firewall allows port 3333 + +### Zebra not syncing + +1. Check peer connections: + + ```bash + docker exec zebra curl -s -H "Content-Type: application/json" \ + localhost:18232 -d '{"jsonrpc":"2.0","id":1,"method":"getinfo","params":[]}' \ + | grep connections + ``` + +2. Ensure port 18233 (Testnet) or 8233 (Mainnet) is accessible + +### Reset everything + +```bash +docker compose down -v +docker compose up -d +``` + +## Switching Networks + +To switch between Testnet and Mainnet: + +1. Stop services: `docker compose down` +2. Edit `.env`: + + ```bash + NETWORK=Mainnet + MINER_ADDRESS=t1YourMainnetAddress + ``` + +3. Start: `docker compose up -d` + +## Security Notes + +- RPC port is internal only (not exposed to host) +- Cookie authentication is disabled for S-NOMP compatibility diff --git a/docker/mining/docker-compose.yml b/docker/mining/docker-compose.yml new file mode 100644 index 00000000000..68ee8eb7a18 --- /dev/null +++ b/docker/mining/docker-compose.yml @@ -0,0 +1,92 @@ +services: + zebra: + build: + context: ../.. + dockerfile: docker/Dockerfile + container_name: zebra + environment: + - ZEBRA_NETWORK__NETWORK=${NETWORK:-Testnet} + - ZEBRA_RPC__LISTEN_ADDR=0.0.0.0:${RPC_PORT:-18232} + - ZEBRA_RPC__ENABLE_COOKIE_AUTH=false + - ZEBRA_SYNC__PARALLEL_CPU_THREADS=12 + - ZEBRA_MINING__MINER_ADDRESS=${MINER_ADDRESS:?MINER_ADDRESS is required} + - ZEBRA_MINING__MINER_DATA=${MINER_DATA:-} + - ZEBRA_MINING__MINER_MEMO=${MINER_MEMO:-} + volumes: + - zebra-data:/home/zebra/.cache/zebra + ports: + - "${PEER_PORT:-18233}:${PEER_PORT:-18233}" + networks: + - mining + restart: unless-stopped + tty: true + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8080/healthy || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + + redis: + image: redis:7-alpine + container_name: redis + networks: + - mining + restart: unless-stopped + + s-nomp: + build: + context: https://github.com/ZcashFoundation/s-nomp.git + dockerfile: Dockerfile.arch + container_name: s-nomp + environment: + - ZEBRA_HOST=zebra + - ZEBRA_RPC_PORT=${RPC_PORT:-18232} + - REDIS_HOST=redis + - STRATUM_PORT=${STRATUM_PORT:-3333} + - NETWORK=${NETWORK:-Testnet} + - POOL_ADDRESS=${MINER_ADDRESS} + ports: + - "${STRATUM_PORT:-3333}:${STRATUM_PORT:-3333}" + - "8080:8080" + depends_on: + zebra: + condition: service_healthy + redis: + condition: service_started + networks: + - mining + restart: unless-stopped + volumes: + - ./s-nomp-entrypoint.sh:/app/s-nomp-entrypoint.sh:ro + entrypoint: /app/s-nomp-entrypoint.sh + + nheqminer: + build: + context: https://github.com/ZcashFoundation/nheqminer.git + dockerfile: Dockerfile + container_name: nheqminer + environment: + - POOL_HOST=s-nomp + - POOL_PORT=${STRATUM_PORT:-3333} + - WORKER_ADDRESS=${MINER_ADDRESS} + - WORKER_NAME=${WORKER_NAME:-docker} + - CPU_THREADS=${CPU_THREADS:-1} + command: > + -l ${POOL_HOST:-s-nomp}:${STRATUM_PORT:-3333} + -u ${MINER_ADDRESS}.${WORKER_NAME:-docker} + -t ${CPU_THREADS:-1} + depends_on: + - s-nomp + networks: + - mining + restart: unless-stopped + profiles: + - miner + +networks: + mining: + driver: bridge + +volumes: + zebra-data: diff --git a/docker/mining/s-nomp-entrypoint.sh b/docker/mining/s-nomp-entrypoint.sh new file mode 100755 index 00000000000..ebe8c9beec5 --- /dev/null +++ b/docker/mining/s-nomp-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +/app/docker-entrypoint.sh true + +sed -i 's/}$/,"switching":{}}/g' /app/config.json +sed -i 's/"diff": 0.05/"tls": false, "diff": 0.05/g' /app/pool_configs/zcash.json + +if [ "$NETWORK" = "Mainnet" ]; then + sed -i 's|tmRGc4CD1UyUdbSJmTUzcB6oDqk4qUaHnnh|t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs|g' /app/pool_configs/zcash.json + sed -i 's|blockRefreshInterval": 500|blockRefreshInterval": 2000|g' /app/config.json +fi + +exec node init.js diff --git a/docs/decisions/devops/0002-docker-use-gosu.md b/docs/decisions/devops/0002-docker-use-gosu.md index 1c54ae8cf0b..4488209ed91 100644 --- a/docs/decisions/devops/0002-docker-use-gosu.md +++ b/docs/decisions/devops/0002-docker-use-gosu.md @@ -1,5 +1,5 @@ --- -status: superseded by [ADR-0004](0004-docker-use-setpriv.md) +status: superseded by [ADR-0005](0005-docker-use-setpriv.md) date: 2025-02-28 story: Volumes permissions and privilege management in container entrypoint --- diff --git a/docs/decisions/devops/004-improve-docker-conf-tests.md b/docs/decisions/devops/0004-improve-docker-conf-tests.md similarity index 100% rename from docs/decisions/devops/004-improve-docker-conf-tests.md rename to docs/decisions/devops/0004-improve-docker-conf-tests.md diff --git a/docs/decisions/devops/0004-docker-use-setpriv.md b/docs/decisions/devops/0005-docker-use-setpriv.md similarity index 100% rename from docs/decisions/devops/0004-docker-use-setpriv.md rename to docs/decisions/devops/0005-docker-use-setpriv.md diff --git a/docs/decisions/devops/0006-gcp-deployment-naming.md b/docs/decisions/devops/0006-gcp-deployment-naming.md new file mode 100644 index 00000000000..6354c1083e5 --- /dev/null +++ b/docs/decisions/devops/0006-gcp-deployment-naming.md @@ -0,0 +1,114 @@ +--- +status: accepted +date: 2026-04-14 +builds-on: [Continuous Delivery](../../../book/src/dev/continuous-delivery.md) +story: Stateful disk collisions during release rollouts, silent main-branch CD failures, and the regional-stateful-MIG quirks that made rolling updates impossible on Google Cloud Platform. +--- + +# GCP Deployment Topology: zonal MIG per (environment, branch, network, zone) + +## Context and Problem Statement + +Zebra runs as a stateful node: chain state on disk represents many hours or days of synchronization. The continuous-delivery pipeline must update the running container without re-syncing from genesis on every release. On Google Cloud Platform, this constraint is satisfied by attaching a persistent disk (PD) to a Managed Instance Group (MIG) under a stateful policy, so the disk survives instance recreation during rolling updates. + +A read-write PD can attach to only one instance at a time. The first design used a single **regional** MIG per `(environment, network)` holding three instances, one per zone, each attached to a same-named zonal disk. Two architectural problems arose from that shape: + +1. **Two MIGs cannot share a disk.** Encoding the major Zebra version in the MIG name (`zebrad-v${MAJOR}-${network}`) meant each major release spun up a competing MIG. The platform cannot atomically transfer the stable disk from one MIG to another; the new MIG retried instance creation until one of the two won a zone. +2. **Regional stateful MIGs cannot do per-zone rolling.** GCP requires `--max-surge=0` (no extra capacity with a single-writer disk) and `--max-unavailable` to be either 0 or at least the zone count. A three-zone regional MIG can only update all-at-once or not at all. Empirically, the workflow's `--max-unavailable=1` was silently invalid. + +Operating a multi-instance regional MIG also coupled deploy-landed to node-healthy (a single `wait-until --stable` covered both, so testnet's slow peer warmup timed out deploys that had actually landed), conflated network failures under matrix fail-fast, and leaked mainnet cache images into the testnet deploy path because the cache lookup ran once at the workflow level. + +## Priorities & Constraints + +- Chain state must persist across releases, including major-version upgrades. A multi-day resync is unacceptable. +- A failed deploy must surface a clear, per-zone, per-network error within minutes. +- A developer must be able to deploy a PR branch to a single zone in dev without colliding with other deploys. +- Updates must be true rolling: one zone at a time, others keep serving. +- Cleanup must be auditable and reversible: label-based protection, manual reap commands, no accidental destruction of live stateful disks. +- Cache images produced by integration tests must remain the cold-start mechanism for fresh deploys; cache images are network-scoped (same image seeds all zones). +- The architecture must compose with the existing `release:published` trigger contract. +- The naming model must reuse existing label vocabulary. + +## Considered Options + +- **Option 1: Regional MIG per `(environment, network)`, versioned MIG names** (original). Each major release spawns a new MIG; disks shared across versions; rolling updates are all-at-once. +- **Option 2: Regional MIG per `(environment, network)`, stable MIG name, branch-derived disk** (first refactor, PR #10482). Fixes the versioned-MIG collision but keeps regional-stateful constraints (all-at-once updates, no per-zone rolling, `wait-until --stable` covering both deploy and health). +- **Option 3: Zonal MIG per `(environment, branch, network, zone)`, each MIG holds 1 instance with 1 stateful disk.** Three zonal MIGs per network per environment for push/release; one zonal MIG for workflow_dispatch. Rolling updates are per-zone with `--max-unavailable=1`. Deploy-landed and node-healthy are separate signals. + +### Pros and Cons of the Options + +#### Option 1: Regional versioned MIG (status quo, replaced by #10482) + +- Bad, because every major release recreates a single-writer disk collision. +- Bad, because no automatic mechanism retires the previous major-version MIG. +- Bad, because a fall-through in trigger-to-disk routing leaked staging and PR deploys into the prod-disk path. +- Good, because each major version had clearly separable infrastructure. + +#### Option 2: Regional stable MIG, branch-derived disk + +- Good, because the MIG is stable per network, so cross-major-version upgrades are `set-instance-template` plus `rolling-action`. +- Bad, because regional stateful MIGs cannot do per-zone rolling (all-at-once updates, significant downtime window). +- Bad, because `--max-unavailable=1` is platform-rejected; only `0` or `zone_count` are valid. Either no update or all zones down. +- Bad, because `wait-until --stable` couples deploy success to instance health, and testnet peer warmup regularly exceeds any reasonable timeout. +- Bad, because matrix fail-fast default cancels one network when the other fails. +- Good, because this was a large improvement over Option 1 for the versioning problem. + +#### Option 3: Zonal MIG per (environment, branch, network, zone) + +- Good, because the MIG is the instance: 1:1:1:1 between MIG, instance, stateful disk, static IP. No "regional disks with the same name across zones" ambiguity. +- Good, because `--max-unavailable=1` is natively valid (single-instance MIG). Updates are true per-zone rolling: one zonal MIG replaces its instance while the sister zonal MIGs keep serving. +- Good, because per-zone failures are isolated: one zone's warmup slowness doesn't block the others, and the matrix's `fail-fast: false` gives six independent (network, zone) status cells on the GitHub UI. +- Good, because deploy-landed (`--version-target-reached`) and node-healthy (`--stable` in a separate `verify-nodes` job) are already distinct signals with distinct failure labels. +- Good, because `find-cached-disks` runs per-network (one lookup per network, image seeds all three zones). +- Neutral, because the resource count grows from 2 MIGs to 6 per environment. Fewer resources per MIG (1 instance, 1 disk, 1 IP each) keeps each MIG's blast radius tiny. +- Bad, because migrating from regional-stateful to zonal is a one-shot operator-driven migration per environment. + +## Decision Outcome + +Chosen option: **Option 3 (zonal MIG per `(environment, branch, network, zone)`)**. The migration from the previous regional-stateful architecture is documented as a one-shot procedure in the runbook. + +### Naming + +One MIG per matrix cell. Names encode every identity axis: + +| Trigger | Environment | MIG | Disk | +| -------------------- | -------------------- | ---------------------------------------- | --------------------------------------------- | +| `release` | `zfnd-prod-zebra` | `zebrad-${network}-${zone-letter}` | `zebrad-cache-${network}-${zone-letter}` | +| `push` to `main` | `zfnd-dev-zebra` | `zebrad-main-${network}-${zone-letter}` | `zebrad-cache-main-${network}-${zone-letter}` | +| `workflow_dispatch` | `zfnd-dev-zebra` | `zebrad-${branch}-${network}-${zone-letter}` | `zebrad-cache-${branch}-${network}-${zone-letter}` | + +`zone-letter` is `b`, `c`, or `d` (the last segment of `us-east1-b`, etc.). Push and release fan out to six zonal MIGs (2 networks × 3 zones). A `workflow_dispatch` deploys a single zonal MIG with user-selected network and zone (default `us-east1-b`). + +### Update mechanics + +- Every trigger runs per-cell: `rolling-action start-update` on an existing zonal MIG with `--max-unavailable=1 --max-surge=0 --replacement-method=recreate`. True per-zone rolling. +- Fresh MIG creation pre-creates the zonal disk from the network-scoped cache image, then the template attaches it via `--disk=name=…` (not `--create-disk`). This decouples "disk populated from image" from "MIG attaches disk" and handles both fresh deploys and manual pre-seeding identically. +- The MIG's health check (`/healthy`, `--initial-delay=3600`) governs autohealing tolerance. +- Deploy-landed waits `--version-target-reached --timeout=600` (template rollout done). Verify-nodes waits `--stable --timeout=5400` (peer mesh + warmup complete). Distinct failure labels: `S-ci-fail-release-auto-issue` for infrastructure, `S-ci-fail-verify-auto-issue` for slow warmup. + +### Static IPs + +Deterministic zone-to-IP mapping for push and release: + +- `us-east1-b` → `zebra-${network}` (primary) +- `us-east1-c` → `zebra-${network}-secondary` +- `us-east1-d` → `zebra-${network}-tertiary` + +Workflow_dispatch deploys use ephemeral IPs (PR smoke tests don't need stable external addresses). + +### Labels + +Every MIG, instance, and disk carries `app`, `environment`, `network`, `zone`, `created_by`, `github_ref`, `github_sha`. The `environment` label is standardized as `dev`, `stage`, or `prod`. The `created_by` label (`release`, `push`, or `workflow_dispatch`) discriminates the deploy kind for cleanup and inspection. PR-deploy cleanup uses `keep_until=YYYY-MM-DD` and `delete_protection=true` opt-out labels. + +### Accepted trade-offs + +- Breaking migration from regional to zonal: snapshot-based one-shot procedure in the runbook. +- More MIGs per environment (6 vs 2), each simpler. +- Backwards-incompatible RocksDB format changes still need a snapshot-based handoff at release time; runbook covers it. +- The `cos-stable` + `gce-container-declaration` deploy pattern is deprecated by Google; a future ADR will address it. + +## More Information + +- [GCP Deployment Operations runbook](../../../book/src/dev/gcp-deployment-operations.md): PR-deploy cleanup, disk-corruption recovery, DB-format-version-break release procedure, regional-to-zonal migration procedure. +- [Continuous Delivery overview](../../../book/src/dev/continuous-delivery.md): the high-level model. +- GCP documentation on [instance groups](https://cloud.google.com/compute/docs/instance-groups), [stateful MIGs](https://cloud.google.com/compute/docs/instance-groups/stateful-migs), and [persistent disk attachment limits](https://cloud.google.com/compute/docs/disks#pd_modes). diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index b151f020ab5..760a44c963a 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -101,18 +101,6 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.3.0 -> 0.4.0" -[[audits.equihash]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.2.0 -> 0.2.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - -[[audits.f4jumble]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.1.0 -> 0.1.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.foldhash]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -203,12 +191,6 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.6.0 -> 0.7.0" -[[audits.incrementalmerkletree]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.7.0 -> 0.7.0@git:ffe4234788fd22662b937ba7c6ea01535fcc1293" -importable = false - [[audits.indexmap]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -319,12 +301,6 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.9.0 -> 0.10.0" -[[audits.orchard]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.10.0 -> 0.9.1@git:55fb089a335bbbc1cda186c706bc037073df8eb7" -importable = false - [[audits.owo-colors]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -405,21 +381,20 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.38.34 -> 0.38.37" -[[audits.sapling-crypto]] -who = "Alfredo Garcia " +[[audits.rustls-webpki]] +who = "Alfredo Garcia " criteria = "safe-to-deploy" -delta = "0.1.3 -> 0.2.0" +delta = "0.103.10 -> 0.103.12" [[audits.sapling-crypto]] who = "Alfredo Garcia " criteria = "safe-to-deploy" -delta = "0.2.0 -> 0.3.0" +delta = "0.1.3 -> 0.2.0" [[audits.sapling-crypto]] who = "Alfredo Garcia " criteria = "safe-to-deploy" -delta = "0.3.0 -> 0.2.0@git:b1ad3694ee13a2fc5d291ad04721a6252da0993c" -importable = false +delta = "0.2.0 -> 0.3.0" [[audits.serde]] who = "Alfredo Garcia " @@ -712,12 +687,6 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.4.0 -> 0.5.0" -[[audits.zcash_address]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.5.0 -> 0.5.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.zcash_address]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -739,40 +708,16 @@ criteria = "safe-to-deploy" delta = "0.14.0 -> 0.13.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" importable = false -[[audits.zcash_encoding]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.2.1 -> 0.2.1@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - -[[audits.zcash_history]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.4.0 -> 0.4.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.zcash_keys]] who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.3.0 -> 0.4.0" -[[audits.zcash_keys]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.4.0 -> 0.3.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.zcash_primitives]] who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.16.0 -> 0.17.0" -[[audits.zcash_primitives]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.17.0 -> 0.17.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.zcash_primitives]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -783,12 +728,6 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.16.0 -> 0.17.0" -[[audits.zcash_proofs]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.17.0 -> 0.17.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.zcash_proofs]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -809,12 +748,6 @@ who = "Alfredo Garcia " criteria = "safe-to-deploy" delta = "0.3.0 -> 0.4.0" -[[audits.zcash_protocol]] -who = "Alfredo Garcia " -criteria = "safe-to-deploy" -delta = "0.4.0 -> 0.3.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792" -importable = false - [[audits.zebra-chain]] who = "Alfredo Garcia " criteria = "safe-to-deploy" @@ -893,19 +826,19 @@ importable = false [[trusted.clap]] criteria = "safe-to-deploy" -user-id = 6743 # Ed Page (epage) +user-id = 6743 start = "2021-12-08" end = "2025-07-09" [[trusted.clap_builder]] criteria = "safe-to-deploy" -user-id = 6743 # Ed Page (epage) +user-id = 6743 start = "2023-03-28" end = "2025-07-09" [[trusted.clap_derive]] criteria = "safe-to-deploy" -user-id = 6743 # Ed Page (epage) +user-id = 6743 start = "2021-12-08" end = "2025-07-09" @@ -923,6 +856,330 @@ end = "2025-07-09" [[trusted.tokio]] criteria = "safe-to-deploy" -user-id = 6741 # Alice Ryhl (Darksonn) +user-id = 6741 start = "2020-12-25" end = "2025-07-29" + +[[trusted.zebra-chain]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-chain]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-chain]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-chain]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-chain]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-chain]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-consensus]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-consensus]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-consensus]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-consensus]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-consensus]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-consensus]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-network]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-network]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-network]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-network]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-network]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-network]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-node-services]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-node-services]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-node-services]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-node-services]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-node-services]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-node-services]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-rpc]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-rpc]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-rpc]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-rpc]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-rpc]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-rpc]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-script]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-script]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-script]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-script]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-script]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-script]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-state]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-state]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-state]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-state]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-state]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-state]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-utils]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-utils]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-utils]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-utils]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-utils]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebra-utils]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebrad]] +criteria = "safe-to-deploy" +user-id = 159631 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebrad]] +criteria = "safe-to-deploy" +user-id = 199775 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebrad]] +criteria = "safe-to-deploy" +user-id = 215589 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebrad]] +criteria = "safe-to-deploy" +user-id = 228785 # Alfredo Garcia (oxarbitrage) +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebrad]] +criteria = "safe-to-deploy" +user-id = 235397 +start = "2023-09-02" +end = "2027-06-11" + +[[trusted.zebrad]] +criteria = "safe-to-deploy" +user-id = 405015 +start = "2023-09-02" +end = "2027-06-11" diff --git a/supply-chain/config.toml b/supply-chain/config.toml index cdbb79de93f..2221b0cb04d 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -2,11 +2,23 @@ # cargo-vet config file [cargo-vet] -version = "0.9" +version = "0.10" + +[imports.bytecode-alliance] +url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml" + +[imports.embark-studios] +url = "https://raw.githubusercontent.com/EmbarkStudios/rust-ecosystem/main/audits.toml" + +[imports.fermyon] +url = "https://raw.githubusercontent.com/fermyon/spin/main/supply-chain/audits.toml" [imports.google] url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml" +[imports.isrg] +url = "https://raw.githubusercontent.com/divviup/libprio-rs/main/supply-chain/audits.toml" + [imports.mozilla] url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" @@ -16,77 +28,18 @@ url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/ [imports.zcashd] url = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[policy."equihash:0.2.0"] - -[policy."equihash:0.2.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792"] -audit-as-crates-io = true - -[policy."f4jumble:0.1.0"] - -[policy."f4jumble:0.1.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792"] -audit-as-crates-io = true - -[policy."incrementalmerkletree:0.6.0"] - -[policy."incrementalmerkletree:0.7.0@git:ffe4234788fd22662b937ba7c6ea01535fcc1293"] -audit-as-crates-io = true - -[policy.orchard] -audit-as-crates-io = true - -[policy.sapling-crypto] -audit-as-crates-io = true - -[policy.shardtree] -audit-as-crates-io = true - [policy.tower-batch-control] audit-as-crates-io = true [policy.tower-fallback] audit-as-crates-io = true -[policy."zcash_address:0.4.0"] - -[policy."zcash_address:0.5.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792"] -audit-as-crates-io = true - -[policy.zcash_client_backend] -audit-as-crates-io = true - -[policy.zcash_encoding] -audit-as-crates-io = true - -[policy.zcash_history] -audit-as-crates-io = true - -[policy."zcash_keys:0.3.0"] - -[policy."zcash_keys:0.3.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792"] -audit-as-crates-io = true - -[policy."zcash_primitives:0.16.0"] - -[policy."zcash_primitives:0.17.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792"] -audit-as-crates-io = true - -[policy.zcash_proofs] -audit-as-crates-io = true - -[policy."zcash_protocol:0.2.0"] - -[policy."zcash_protocol:0.3.0@git:1410f1449100a417bfbc4f6c7167aa9808e38792"] -audit-as-crates-io = true - [policy.zebra-chain] audit-as-crates-io = true [policy.zebra-consensus] audit-as-crates-io = true -[policy.zebra-grpc] -audit-as-crates-io = true - [policy.zebra-network] audit-as-crates-io = true @@ -96,9 +49,6 @@ audit-as-crates-io = true [policy.zebra-rpc] audit-as-crates-io = true -[policy.zebra-scan] -audit-as-crates-io = true - [policy.zebra-script] audit-as-crates-io = true @@ -114,9 +64,6 @@ audit-as-crates-io = true [policy.zebrad] audit-as-crates-io = true -[policy.zip321] -audit-as-crates-io = true - [[exemptions.abscissa_core]] version = "0.7.0" criteria = "safe-to-deploy" @@ -138,107 +85,79 @@ version = "0.8.4" criteria = "safe-to-deploy" [[exemptions.ahash]] -version = "0.8.11" +version = "0.8.12" criteria = "safe-to-deploy" [[exemptions.aho-corasick]] version = "1.1.3" criteria = "safe-to-deploy" -[[exemptions.android-tzdata]] -version = "0.1.1" -criteria = "safe-to-deploy" - -[[exemptions.anes]] -version = "0.1.6" -criteria = "safe-to-run" - -[[exemptions.ansi_term]] -version = "0.12.1" -criteria = "safe-to-deploy" - [[exemptions.anstream]] -version = "0.6.14" +version = "1.0.0" criteria = "safe-to-deploy" [[exemptions.anstyle]] -version = "1.0.7" +version = "1.0.14" criteria = "safe-to-deploy" [[exemptions.anstyle-parse]] -version = "0.2.4" +version = "1.0.0" criteria = "safe-to-deploy" [[exemptions.anstyle-query]] -version = "1.1.0" +version = "1.1.5" criteria = "safe-to-deploy" [[exemptions.anstyle-wincon]] -version = "3.0.3" +version = "3.0.11" criteria = "safe-to-deploy" [[exemptions.anyhow]] -version = "1.0.86" +version = "1.0.102" criteria = "safe-to-deploy" [[exemptions.arc-swap]] -version = "1.7.1" -criteria = "safe-to-deploy" - -[[exemptions.arrayref]] -version = "0.3.7" -criteria = "safe-to-deploy" - -[[exemptions.arrayvec]] -version = "0.7.4" +version = "1.9.1" criteria = "safe-to-deploy" [[exemptions.async-compression]] -version = "0.4.11" +version = "0.4.42" criteria = "safe-to-deploy" [[exemptions.async-trait]] -version = "0.1.80" +version = "0.1.88" criteria = "safe-to-deploy" -[[exemptions.atomic-waker]] -version = "1.1.2" +[[exemptions.aws-lc-rs]] +version = "1.16.3" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" -[[exemptions.atty]] -version = "0.2.14" +[[exemptions.aws-lc-sys]] +version = "0.40.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.axum]] -version = "0.6.20" +version = "0.8.9" criteria = "safe-to-deploy" [[exemptions.axum-core]] -version = "0.3.4" +version = "0.5.6" criteria = "safe-to-deploy" [[exemptions.backtrace]] -version = "0.3.71" -criteria = "safe-to-deploy" - -[[exemptions.base64]] -version = "0.11.0" -criteria = "safe-to-deploy" - -[[exemptions.base64]] -version = "0.21.7" -criteria = "safe-to-deploy" - -[[exemptions.base64]] -version = "0.22.1" +version = "0.3.76" criteria = "safe-to-deploy" [[exemptions.base64ct]] -version = "1.0.1" +version = "1.8.3" criteria = "safe-to-deploy" [[exemptions.bech32]] -version = "0.9.1" +version = "0.11.1" criteria = "safe-to-deploy" [[exemptions.bellman]] @@ -249,6 +168,10 @@ criteria = "safe-to-deploy" version = "1.3.3" criteria = "safe-to-deploy" +[[exemptions.bip32]] +version = "0.6.0-pre.1" +criteria = "safe-to-deploy" + [[exemptions.bitflags-serde-legacy]] version = "0.1.1" criteria = "safe-to-deploy" @@ -258,51 +181,43 @@ version = "1.0.1" criteria = "safe-to-deploy" [[exemptions.blake2b_simd]] -version = "1.0.2" +version = "1.0.4" criteria = "safe-to-deploy" [[exemptions.blake2s_simd]] -version = "1.0.2" +version = "1.0.4" criteria = "safe-to-deploy" [[exemptions.block-buffer]] -version = "0.10.4" +version = "0.11.0-rc.3" criteria = "safe-to-deploy" [[exemptions.bls12_381]] version = "0.8.0" criteria = "safe-to-deploy" -[[exemptions.bridgetree]] -version = "0.4.0" -criteria = "safe-to-deploy" - [[exemptions.bs58]] version = "0.5.1" criteria = "safe-to-deploy" -[[exemptions.bstr]] -version = "1.9.1" -criteria = "safe-to-deploy" - -[[exemptions.bumpalo]] -version = "3.16.0" +[[exemptions.byte-slice-cast]] +version = "1.2.3" criteria = "safe-to-deploy" -[[exemptions.byte-slice-cast]] -version = "1.2.2" +[[exemptions.bytemuck]] +version = "1.25.0" criteria = "safe-to-deploy" [[exemptions.bytes]] -version = "1.6.0" +version = "1.11.1" criteria = "safe-to-deploy" [[exemptions.bzip2-sys]] -version = "0.1.11+1.0.8" +version = "0.1.13+1.0.8" criteria = "safe-to-deploy" [[exemptions.camino]] -version = "1.1.7" +version = "1.2.2" criteria = "safe-to-deploy" [[exemptions.canonical-path]] @@ -310,11 +225,11 @@ version = "2.0.2" criteria = "safe-to-deploy" [[exemptions.cargo-platform]] -version = "0.1.8" +version = "0.3.3" criteria = "safe-to-deploy" [[exemptions.cargo_metadata]] -version = "0.18.1" +version = "0.23.1" criteria = "safe-to-deploy" [[exemptions.cbc]] @@ -322,11 +237,7 @@ version = "0.1.2" criteria = "safe-to-deploy" [[exemptions.cc]] -version = "1.0.100" -criteria = "safe-to-deploy" - -[[exemptions.cfg-if]] -version = "0.1.10" +version = "1.2.61" criteria = "safe-to-deploy" [[exemptions.chacha20]] @@ -338,23 +249,7 @@ version = "0.10.1" criteria = "safe-to-deploy" [[exemptions.chrono]] -version = "0.4.38" -criteria = "safe-to-deploy" - -[[exemptions.ciborium]] -version = "0.2.2" -criteria = "safe-to-run" - -[[exemptions.ciborium-io]] -version = "0.2.2" -criteria = "safe-to-run" - -[[exemptions.ciborium-ll]] -version = "0.2.2" -criteria = "safe-to-run" - -[[exemptions.cipher]] -version = "0.4.4" +version = "0.4.44" criteria = "safe-to-deploy" [[exemptions.clang-sys]] @@ -362,53 +257,124 @@ version = "1.8.1" criteria = "safe-to-deploy" [[exemptions.clap]] -version = "2.34.0" +version = "4.6.1" +criteria = "safe-to-deploy" + +[[exemptions.clap_builder]] +version = "4.6.0" +criteria = "safe-to-deploy" + +[[exemptions.clap_derive]] +version = "4.6.1" criteria = "safe-to-deploy" [[exemptions.clap_lex]] -version = "0.7.1" +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.cmake]] +version = "0.1.58" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.color-eyre]] -version = "0.6.3" +version = "0.6.5" criteria = "safe-to-deploy" [[exemptions.color-spantrace]] -version = "0.2.1" +version = "0.3.0" criteria = "safe-to-deploy" [[exemptions.colorchoice]] -version = "1.0.1" +version = "1.0.5" +criteria = "safe-to-deploy" + +[[exemptions.combine]] +version = "4.6.7" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.compression-codecs]] +version = "0.4.38" +criteria = "safe-to-deploy" + +[[exemptions.compression-core]] +version = "0.4.32" +criteria = "safe-to-deploy" + +[[exemptions.config]] +version = "0.15.22" +criteria = "safe-to-deploy" + +[[exemptions.console]] +version = "0.15.11" criteria = "safe-to-deploy" [[exemptions.console]] -version = "0.15.8" +version = "0.16.3" criteria = "safe-to-deploy" [[exemptions.console-api]] -version = "0.6.0" +version = "0.8.1" criteria = "safe-to-deploy" [[exemptions.console-subscriber]] -version = "0.2.0" +version = "0.4.1" criteria = "safe-to-deploy" [[exemptions.const-oid]] version = "0.9.6" criteria = "safe-to-deploy" +[[exemptions.const-random]] +version = "0.1.18" +criteria = "safe-to-deploy" + +[[exemptions.const-random-macro]] +version = "0.1.16" +criteria = "safe-to-deploy" + +[[exemptions.const_format]] +version = "0.2.36" +criteria = "safe-to-deploy" + +[[exemptions.const_format_proc_macros]] +version = "0.2.34" +criteria = "safe-to-deploy" + [[exemptions.constant_time_eq]] -version = "0.3.0" +version = "0.4.2" criteria = "safe-to-deploy" -[[exemptions.core-foundation-sys]] -version = "0.8.6" +[[exemptions.convert_case]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.convert_case]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.core-foundation]] +version = "0.10.1" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.corez]] +version = "0.1.1" criteria = "safe-to-deploy" +notes = "Replacement for the yanked core2 crate; pulled in transitively by librustzcash 2026-04 release wave." [[exemptions.cpufeatures]] version = "0.2.12" criteria = "safe-to-deploy" +[[exemptions.crc32fast]] +version = "1.5.0" +criteria = "safe-to-deploy" + [[exemptions.criterion]] version = "0.5.1" criteria = "safe-to-run" @@ -422,7 +388,7 @@ version = "0.5.13" criteria = "safe-to-deploy" [[exemptions.crossbeam-deque]] -version = "0.8.5" +version = "0.8.6" criteria = "safe-to-deploy" [[exemptions.crossbeam-epoch]] @@ -433,12 +399,12 @@ criteria = "safe-to-deploy" version = "0.8.20" criteria = "safe-to-deploy" -[[exemptions.crunchy]] -version = "0.2.2" +[[exemptions.crypto-common]] +version = "0.1.7" criteria = "safe-to-deploy" [[exemptions.crypto-common]] -version = "0.1.6" +version = "0.2.0-rc.1" criteria = "safe-to-deploy" [[exemptions.curve25519-dalek]] @@ -454,7 +420,7 @@ version = "0.13.4" criteria = "safe-to-deploy" [[exemptions.darling]] -version = "0.20.9" +version = "0.20.11" criteria = "safe-to-deploy" [[exemptions.darling_core]] @@ -462,25 +428,37 @@ version = "0.13.4" criteria = "safe-to-deploy" [[exemptions.darling_core]] -version = "0.20.9" +version = "0.20.11" criteria = "safe-to-deploy" [[exemptions.darling_macro]] -version = "0.13.4" +version = "0.20.11" criteria = "safe-to-deploy" -[[exemptions.darling_macro]] -version = "0.20.9" +[[exemptions.derive-getters]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.derive_builder]] +version = "0.20.2" +criteria = "safe-to-deploy" + +[[exemptions.derive_builder_core]] +version = "0.20.2" criteria = "safe-to-deploy" -[[exemptions.der]] -version = "0.7.9" +[[exemptions.derive_builder_macro]] +version = "0.20.2" criteria = "safe-to-deploy" [[exemptions.digest]] version = "0.10.7" criteria = "safe-to-deploy" +[[exemptions.digest]] +version = "0.11.0-pre.9" +criteria = "safe-to-deploy" + [[exemptions.dirs]] version = "5.0.1" criteria = "safe-to-deploy" @@ -489,8 +467,24 @@ criteria = "safe-to-deploy" version = "0.4.1" criteria = "safe-to-deploy" +[[exemptions.dispatch2]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.dlv-list]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.documented]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.documented-macros]] +version = "0.9.1" +criteria = "safe-to-deploy" + [[exemptions.dyn-clone]] -version = "1.0.17" +version = "1.0.19" criteria = "safe-to-deploy" [[exemptions.ed25519]] @@ -498,15 +492,11 @@ version = "2.2.3" criteria = "safe-to-deploy" [[exemptions.ed25519-zebra]] -version = "4.0.3" -criteria = "safe-to-deploy" - -[[exemptions.either]] -version = "1.12.0" +version = "4.2.0" criteria = "safe-to-deploy" [[exemptions.elasticsearch]] -version = "8.5.0-alpha.1" +version = "8.17.0-alpha.1" criteria = "safe-to-deploy" [[exemptions.encode_unicode]] @@ -518,11 +508,11 @@ version = "0.7.1" criteria = "safe-to-deploy" [[exemptions.equihash]] -version = "0.2.0" +version = "0.3.0" criteria = "safe-to-deploy" -[[exemptions.errno]] -version = "0.3.9" +[[exemptions.erased-serde]] +version = "0.4.10" criteria = "safe-to-deploy" [[exemptions.eyre]] @@ -530,15 +520,19 @@ version = "0.6.12" criteria = "safe-to-deploy" [[exemptions.f4jumble]] -version = "0.1.0" +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.fastrand]] +version = "2.4.1" criteria = "safe-to-deploy" [[exemptions.ff]] -version = "0.13.0" +version = "0.13.1" criteria = "safe-to-deploy" -[[exemptions.fiat-crypto]] -version = "0.2.9" +[[exemptions.find-msvc-tools]] +version = "0.1.9" criteria = "safe-to-deploy" [[exemptions.fixed-hash]] @@ -546,7 +540,11 @@ version = "0.8.0" criteria = "safe-to-deploy" [[exemptions.fixedbitset]] -version = "0.4.2" +version = "0.5.7" +criteria = "safe-to-deploy" + +[[exemptions.flate2]] +version = "1.1.9" criteria = "safe-to-deploy" [[exemptions.flume]] @@ -561,40 +559,50 @@ criteria = "safe-to-deploy" version = "2.11.0" criteria = "safe-to-deploy" +[[exemptions.fs_extra]] +version = "1.3.0" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + [[exemptions.funty]] version = "2.0.0" criteria = "safe-to-deploy" +[[exemptions.futures]] +version = "0.3.32" +criteria = "safe-to-deploy" + [[exemptions.futures-channel]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-core]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-executor]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-io]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-macro]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-sink]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-task]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.futures-util]] -version = "0.3.30" +version = "0.3.32" criteria = "safe-to-deploy" [[exemptions.generic-array]] @@ -609,16 +617,16 @@ criteria = "safe-to-deploy" version = "0.2.15" criteria = "safe-to-deploy" -[[exemptions.gimli]] -version = "0.28.1" +[[exemptions.getrandom]] +version = "0.2.17" criteria = "safe-to-deploy" -[[exemptions.git2]] -version = "0.18.3" +[[exemptions.getset]] +version = "0.1.6" criteria = "safe-to-deploy" -[[exemptions.globset]] -version = "0.4.14" +[[exemptions.git2]] +version = "0.20.4" criteria = "safe-to-deploy" [[exemptions.group]] @@ -626,43 +634,39 @@ version = "0.13.0" criteria = "safe-to-deploy" [[exemptions.h2]] -version = "0.3.26" +version = "0.4.13" criteria = "safe-to-deploy" [[exemptions.half]] -version = "2.4.1" +version = "2.7.1" criteria = "safe-to-run" [[exemptions.halo2_gadgets]] -version = "0.3.0" +version = "0.5.0" criteria = "safe-to-deploy" [[exemptions.halo2_legacy_pdqsort]] version = "0.1.0" criteria = "safe-to-deploy" +[[exemptions.halo2_poseidon]] +version = "0.1.0" +criteria = "safe-to-deploy" + [[exemptions.halo2_proofs]] -version = "0.3.0" +version = "0.3.2" criteria = "safe-to-deploy" [[exemptions.hashbrown]] version = "0.14.5" criteria = "safe-to-deploy" -[[exemptions.hdrhistogram]] -version = "7.5.4" -criteria = "safe-to-deploy" - -[[exemptions.heck]] -version = "0.3.3" -criteria = "safe-to-deploy" - -[[exemptions.heck]] -version = "0.5.0" +[[exemptions.hashlink]] +version = "0.7.0" criteria = "safe-to-deploy" -[[exemptions.hermit-abi]] -version = "0.1.19" +[[exemptions.hdrhistogram]] +version = "7.5.4" criteria = "safe-to-deploy" [[exemptions.hermit-abi]] @@ -674,15 +678,11 @@ version = "0.4.1" criteria = "safe-to-deploy" [[exemptions.hmac]] -version = "0.12.1" -criteria = "safe-to-deploy" - -[[exemptions.home]] -version = "0.5.9" +version = "0.13.0-pre.4" criteria = "safe-to-deploy" [[exemptions.hostname]] -version = "0.4.0" +version = "0.4.2" criteria = "safe-to-deploy" [[exemptions.howudoin]] @@ -690,75 +690,67 @@ version = "0.1.2" criteria = "safe-to-deploy" [[exemptions.http]] -version = "0.2.12" +version = "1.4.0" criteria = "safe-to-deploy" -[[exemptions.http]] -version = "1.1.0" +[[exemptions.http-body-util]] +version = "0.1.3" criteria = "safe-to-deploy" -[[exemptions.http-body]] -version = "0.4.6" +[[exemptions.httparse]] +version = "1.10.1" criteria = "safe-to-deploy" -[[exemptions.http-body]] -version = "1.0.0" -criteria = "safe-to-deploy" - -[[exemptions.http-body-util]] -version = "0.1.2" -criteria = "safe-to-deploy" - -[[exemptions.httparse]] -version = "1.9.4" -criteria = "safe-to-deploy" - -[[exemptions.human_bytes]] -version = "0.4.3" +[[exemptions.human_bytes]] +version = "0.4.3" criteria = "safe-to-deploy" [[exemptions.humantime]] -version = "2.1.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.humantime-serde]] version = "1.1.1" criteria = "safe-to-deploy" -[[exemptions.hyper]] -version = "0.14.29" +[[exemptions.hybrid-array]] +version = "0.2.3" criteria = "safe-to-deploy" [[exemptions.hyper]] -version = "1.3.1" +version = "1.9.0" criteria = "safe-to-deploy" [[exemptions.hyper-rustls]] -version = "0.24.2" +version = "0.27.9" criteria = "safe-to-deploy" [[exemptions.hyper-timeout]] -version = "0.4.1" +version = "0.5.2" criteria = "safe-to-deploy" [[exemptions.hyper-util]] -version = "0.1.5" +version = "0.1.20" criteria = "safe-to-deploy" [[exemptions.iana-time-zone]] -version = "0.1.60" +version = "0.1.65" criteria = "safe-to-deploy" -[[exemptions.iana-time-zone-haiku]] -version = "0.1.2" +[[exemptions.icu_locid_transform_data]] +version = "1.5.1" criteria = "safe-to-deploy" -[[exemptions.ident_case]] -version = "1.0.1" +[[exemptions.icu_normalizer_data]] +version = "1.5.1" criteria = "safe-to-deploy" -[[exemptions.idna]] -version = "0.5.0" +[[exemptions.icu_properties_data]] +version = "1.5.1" +criteria = "safe-to-deploy" + +[[exemptions.id-arena]] +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.impl-codec]] @@ -766,15 +758,15 @@ version = "0.6.0" criteria = "safe-to-deploy" [[exemptions.impl-trait-for-tuples]] -version = "0.2.2" +version = "0.2.3" criteria = "safe-to-deploy" [[exemptions.incrementalmerkletree]] -version = "0.5.1" +version = "0.8.2" criteria = "safe-to-deploy" [[exemptions.indenter]] -version = "0.3.3" +version = "0.3.4" criteria = "safe-to-deploy" [[exemptions.indexmap]] @@ -782,35 +774,35 @@ version = "1.9.3" criteria = "safe-to-deploy" [[exemptions.indexmap]] -version = "2.2.6" +version = "2.14.0" criteria = "safe-to-deploy" [[exemptions.indicatif]] -version = "0.17.8" +version = "0.17.11" criteria = "safe-to-deploy" [[exemptions.inferno]] -version = "0.11.19" +version = "0.12.6" criteria = "safe-to-deploy" [[exemptions.insta]] -version = "1.39.0" +version = "1.47.2" criteria = "safe-to-deploy" -[[exemptions.instant]] -version = "0.1.13" +[[exemptions.ipnet]] +version = "2.12.0" criteria = "safe-to-deploy" -[[exemptions.ipnet]] -version = "2.9.0" +[[exemptions.iri-string]] +version = "0.7.12" criteria = "safe-to-deploy" [[exemptions.is-terminal]] -version = "0.4.12" -criteria = "safe-to-deploy" +version = "0.4.16" +criteria = "safe-to-run" [[exemptions.is_terminal_polyfill]] -version = "1.70.0" +version = "1.70.1" criteria = "safe-to-deploy" [[exemptions.itertools]] @@ -821,60 +813,100 @@ criteria = "safe-to-deploy" version = "0.13.0" criteria = "safe-to-deploy" +[[exemptions.itoa]] +version = "1.0.18" +criteria = "safe-to-deploy" + +[[exemptions.jni]] +version = "0.22.4" +criteria = "safe-to-deploy" + +[[exemptions.jni-macros]] +version = "0.22.4" +criteria = "safe-to-deploy" + +[[exemptions.jni-sys]] +version = "0.4.1" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.jni-sys-macros]] +version = "0.4.1" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + [[exemptions.jobserver]] -version = "0.1.31" +version = "0.1.34" criteria = "safe-to-deploy" [[exemptions.js-sys]] -version = "0.3.69" +version = "0.3.97" criteria = "safe-to-deploy" -[[exemptions.jsonrpc]] -version = "0.18.0" +[[exemptions.json5]] +version = "0.4.1" criteria = "safe-to-deploy" -[[exemptions.jsonrpc-core]] -version = "18.0.0" +[[exemptions.jsonrpsee]] +version = "0.24.7" criteria = "safe-to-deploy" -[[exemptions.jsonrpc-derive]] -version = "18.0.0" +[[exemptions.jsonrpsee-core]] +version = "0.24.7" criteria = "safe-to-deploy" -[[exemptions.jsonrpc-http-server]] -version = "18.0.0" +[[exemptions.jsonrpsee-proc-macros]] +version = "0.24.7" criteria = "safe-to-deploy" -[[exemptions.jsonrpc-server-utils]] -version = "18.0.0" +[[exemptions.jsonrpsee-server]] +version = "0.24.7" +criteria = "safe-to-deploy" + +[[exemptions.jsonrpsee-types]] +version = "0.24.7" criteria = "safe-to-deploy" [[exemptions.jubjub]] version = "0.10.0" criteria = "safe-to-deploy" +[[exemptions.known-folders]] +version = "1.4.2" +criteria = "safe-to-deploy" + +[[exemptions.konst]] +version = "0.2.20" +criteria = "safe-to-deploy" + +[[exemptions.konst_macro_rules]] +version = "0.2.19" +criteria = "safe-to-deploy" + [[exemptions.lazycell]] version = "1.3.0" criteria = "safe-to-deploy" [[exemptions.libc]] -version = "0.2.155" +version = "0.2.186" criteria = "safe-to-deploy" [[exemptions.libgit2-sys]] -version = "0.16.2+1.7.2" +version = "0.18.3+1.9.2" criteria = "safe-to-deploy" [[exemptions.libloading]] -version = "0.8.4" +version = "0.8.9" criteria = "safe-to-deploy" [[exemptions.libm]] -version = "0.2.8" +version = "0.2.16" criteria = "safe-to-deploy" [[exemptions.libredox]] -version = "0.1.3" +version = "0.1.16" criteria = "safe-to-deploy" [[exemptions.librocksdb-sys]] @@ -882,31 +914,39 @@ version = "0.16.0+8.10.0" criteria = "safe-to-deploy" [[exemptions.libz-sys]] -version = "1.1.18" +version = "1.1.28" +criteria = "safe-to-deploy" + +[[exemptions.libzcash_script]] +version = "0.1.0" criteria = "safe-to-deploy" [[exemptions.linux-raw-sys]] -version = "0.4.14" +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.litrs]] +version = "1.0.0" criteria = "safe-to-deploy" [[exemptions.lock_api]] -version = "0.4.12" +version = "0.4.14" criteria = "safe-to-deploy" -[[exemptions.lz4-sys]] -version = "1.9.5" +[[exemptions.lru-slab]] +version = "0.1.2" criteria = "safe-to-deploy" -[[exemptions.matchers]] -version = "0.1.0" +[[exemptions.lz4-sys]] +version = "1.11.1+lz4-1.10.0" criteria = "safe-to-deploy" [[exemptions.matchit]] -version = "0.7.3" +version = "0.8.4" criteria = "safe-to-deploy" [[exemptions.memchr]] -version = "2.7.4" +version = "2.8.0" criteria = "safe-to-deploy" [[exemptions.memuse]] @@ -914,15 +954,15 @@ version = "0.2.1" criteria = "safe-to-deploy" [[exemptions.metrics]] -version = "0.22.3" +version = "0.24.6" criteria = "safe-to-deploy" [[exemptions.metrics-exporter-prometheus]] -version = "0.14.0" +version = "0.16.2" criteria = "safe-to-deploy" [[exemptions.metrics-util]] -version = "0.16.3" +version = "0.19.1" criteria = "safe-to-deploy" [[exemptions.mime]] @@ -934,7 +974,7 @@ version = "0.2.1" criteria = "safe-to-deploy" [[exemptions.mio]] -version = "0.8.11" +version = "1.2.0" criteria = "safe-to-deploy" [[exemptions.mset]] @@ -949,84 +989,122 @@ criteria = "safe-to-deploy" version = "0.7.0" criteria = "safe-to-deploy" -[[exemptions.net2]] -version = "0.2.39" +[[exemptions.num-bigint]] +version = "0.4.6" criteria = "safe-to-deploy" -[[exemptions.nonempty]] -version = "0.7.0" +[[exemptions.num-format]] +version = "0.4.4" criteria = "safe-to-deploy" -[[exemptions.nu-ansi-term]] -version = "0.46.0" +[[exemptions.num_cpus]] +version = "1.16.0" criteria = "safe-to-deploy" -[[exemptions.num-bigint]] -version = "0.4.5" +[[exemptions.num_threads]] +version = "0.1.7" criteria = "safe-to-deploy" -[[exemptions.num-format]] -version = "0.4.4" +[[exemptions.objc2]] +version = "0.6.4" criteria = "safe-to-deploy" -[[exemptions.num-integer]] -version = "0.1.46" +[[exemptions.objc2-cloud-kit]] +version = "0.3.2" criteria = "safe-to-deploy" -[[exemptions.num-traits]] -version = "0.2.19" +[[exemptions.objc2-core-data]] +version = "0.3.2" criteria = "safe-to-deploy" -[[exemptions.num_cpus]] -version = "1.16.0" +[[exemptions.objc2-core-graphics]] +version = "0.3.2" criteria = "safe-to-deploy" -[[exemptions.num_threads]] -version = "0.1.7" +[[exemptions.objc2-core-image]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.objc2-core-location]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.objc2-core-text]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.objc2-ui-kit]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.objc2-user-notifications]] +version = "0.3.2" criteria = "safe-to-deploy" [[exemptions.object]] -version = "0.32.2" +version = "0.36.0" criteria = "safe-to-deploy" [[exemptions.once_cell]] version = "1.19.0" criteria = "safe-to-deploy" -[[exemptions.oorandom]] -version = "11.1.3" -criteria = "safe-to-run" +[[exemptions.once_cell_polyfill]] +version = "1.70.2" +criteria = "safe-to-deploy" -[[exemptions.opaque-debug]] -version = "0.3.1" +[[exemptions.openrpsee]] +version = "0.1.1" criteria = "safe-to-deploy" -[[exemptions.option-ext]] -version = "0.2.0" +[[exemptions.openssl-probe]] +version = "0.2.1" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.opentelemetry]] +version = "0.31.0" +criteria = "safe-to-deploy" + +[[exemptions.opentelemetry-http]] +version = "0.31.0" +criteria = "safe-to-deploy" + +[[exemptions.opentelemetry-otlp]] +version = "0.31.1" +criteria = "safe-to-deploy" + +[[exemptions.opentelemetry-proto]] +version = "0.31.0" +criteria = "safe-to-deploy" + +[[exemptions.opentelemetry_sdk]] +version = "0.31.0" +criteria = "safe-to-deploy" + +[[exemptions.optfield]] +version = "0.4.0" criteria = "safe-to-deploy" [[exemptions.orchard]] -version = "0.8.0" +version = "0.14.0" criteria = "safe-to-deploy" [[exemptions.ordered-map]] version = "0.4.2" criteria = "safe-to-deploy" -[[exemptions.os_info]] -version = "3.8.2" -criteria = "safe-to-deploy" - -[[exemptions.overload]] -version = "0.1.1" +[[exemptions.ordered-multimap]] +version = "0.7.3" criteria = "safe-to-deploy" -[[exemptions.owo-colors]] -version = "3.5.0" +[[exemptions.os_info]] +version = "3.14.0" criteria = "safe-to-deploy" [[exemptions.owo-colors]] -version = "4.0.0" +version = "4.3.0" criteria = "safe-to-deploy" [[exemptions.pairing]] @@ -1034,67 +1112,75 @@ version = "0.23.0" criteria = "safe-to-deploy" [[exemptions.parity-scale-codec]] -version = "3.6.12" +version = "3.7.5" criteria = "safe-to-deploy" [[exemptions.parity-scale-codec-derive]] -version = "3.6.12" -criteria = "safe-to-deploy" - -[[exemptions.parking_lot]] -version = "0.11.2" +version = "3.7.5" criteria = "safe-to-deploy" [[exemptions.parking_lot]] -version = "0.12.3" +version = "0.12.5" criteria = "safe-to-deploy" [[exemptions.parking_lot_core]] -version = "0.8.6" -criteria = "safe-to-deploy" - -[[exemptions.parking_lot_core]] -version = "0.9.10" +version = "0.9.12" criteria = "safe-to-deploy" [[exemptions.pasta_curves]] version = "0.5.1" criteria = "safe-to-deploy" -[[exemptions.percent-encoding]] -version = "2.3.1" +[[exemptions.pathdiff]] +version = "0.2.3" criteria = "safe-to-deploy" [[exemptions.pest]] -version = "2.7.10" +version = "2.8.6" criteria = "safe-to-deploy" [[exemptions.pest_derive]] -version = "2.7.10" +version = "2.8.6" criteria = "safe-to-deploy" [[exemptions.pest_generator]] -version = "2.7.10" +version = "2.8.6" criteria = "safe-to-deploy" [[exemptions.pest_meta]] -version = "2.7.10" +version = "2.8.6" criteria = "safe-to-deploy" [[exemptions.petgraph]] -version = "0.6.5" +version = "0.8.3" +criteria = "safe-to-deploy" + +[[exemptions.phf]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_generator]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_macros]] +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.phf_shared]] +version = "0.8.0" criteria = "safe-to-deploy" [[exemptions.pin-project]] -version = "1.1.5" +version = "1.1.11" criteria = "safe-to-deploy" [[exemptions.pin-project-internal]] -version = "1.1.5" +version = "1.1.11" criteria = "safe-to-deploy" -[[exemptions.pin-utils]] -version = "0.1.0" +[[exemptions.pin-project-lite]] +version = "0.2.17" criteria = "safe-to-deploy" [[exemptions.pkcs8]] @@ -1102,19 +1188,19 @@ version = "0.10.2" criteria = "safe-to-deploy" [[exemptions.pkg-config]] -version = "0.3.30" +version = "0.3.33" criteria = "safe-to-deploy" [[exemptions.plotters]] -version = "0.3.6" +version = "0.3.7" criteria = "safe-to-run" [[exemptions.plotters-backend]] -version = "0.3.6" +version = "0.3.7" criteria = "safe-to-run" [[exemptions.plotters-svg]] -version = "0.3.6" +version = "0.3.7" criteria = "safe-to-run" [[exemptions.poly1305]] @@ -1122,15 +1208,15 @@ version = "0.8.0" criteria = "safe-to-deploy" [[exemptions.portable-atomic]] -version = "1.6.0" +version = "1.13.1" criteria = "safe-to-deploy" [[exemptions.ppv-lite86]] -version = "0.2.17" +version = "0.2.21" criteria = "safe-to-deploy" [[exemptions.prettyplease]] -version = "0.2.20" +version = "0.2.37" criteria = "safe-to-deploy" [[exemptions.primitive-types]] @@ -1138,43 +1224,43 @@ version = "0.12.2" criteria = "safe-to-deploy" [[exemptions.proc-macro-crate]] -version = "0.1.5" -criteria = "safe-to-deploy" - -[[exemptions.proc-macro-crate]] -version = "3.1.0" -criteria = "safe-to-deploy" - -[[exemptions.proc-macro-error]] -version = "1.0.4" +version = "3.5.0" criteria = "safe-to-deploy" [[exemptions.proptest]] -version = "1.5.0" +version = "1.11.0" criteria = "safe-to-deploy" [[exemptions.proptest-derive]] -version = "0.4.0" +version = "0.5.1" criteria = "safe-to-deploy" [[exemptions.prost]] -version = "0.12.6" +version = "0.14.3" criteria = "safe-to-deploy" [[exemptions.prost-build]] -version = "0.12.6" +version = "0.14.3" criteria = "safe-to-deploy" [[exemptions.prost-derive]] -version = "0.12.6" +version = "0.14.3" criteria = "safe-to-deploy" [[exemptions.prost-types]] -version = "0.12.6" +version = "0.14.3" +criteria = "safe-to-deploy" + +[[exemptions.pulldown-cmark]] +version = "0.13.3" +criteria = "safe-to-deploy" + +[[exemptions.pulldown-cmark-to-cmark]] +version = "22.0.0" criteria = "safe-to-deploy" [[exemptions.quanta]] -version = "0.12.3" +version = "0.12.6" criteria = "safe-to-deploy" [[exemptions.quick-error]] @@ -1182,7 +1268,7 @@ version = "1.2.3" criteria = "safe-to-deploy" [[exemptions.quick-xml]] -version = "0.26.0" +version = "0.39.2" criteria = "safe-to-deploy" [[exemptions.quickcheck]] @@ -1193,6 +1279,22 @@ criteria = "safe-to-deploy" version = "0.9.1" criteria = "safe-to-deploy" +[[exemptions.quinn]] +version = "0.11.9" +criteria = "safe-to-deploy" + +[[exemptions.quinn-proto]] +version = "0.11.14" +criteria = "safe-to-deploy" + +[[exemptions.r-efi]] +version = "5.2.0" +criteria = "safe-to-deploy" + +[[exemptions.r-efi]] +version = "6.0.0" +criteria = "safe-to-deploy" + [[exemptions.radium]] version = "0.7.0" criteria = "safe-to-deploy" @@ -1201,64 +1303,56 @@ criteria = "safe-to-deploy" version = "0.7.3" criteria = "safe-to-deploy" -[[exemptions.rand]] -version = "0.8.5" -criteria = "safe-to-deploy" - [[exemptions.rand_chacha]] version = "0.2.2" criteria = "safe-to-deploy" -[[exemptions.rand_chacha]] -version = "0.3.1" -criteria = "safe-to-deploy" - [[exemptions.rand_core]] version = "0.5.1" criteria = "safe-to-deploy" -[[exemptions.rand_core]] -version = "0.6.4" -criteria = "safe-to-deploy" - [[exemptions.rand_hc]] version = "0.2.0" criteria = "safe-to-deploy" -[[exemptions.raw-cpuid]] -version = "11.0.2" +[[exemptions.rand_xoshiro]] +version = "0.7.0" criteria = "safe-to-deploy" -[[exemptions.rayon]] -version = "1.10.0" +[[exemptions.rapidhash]] +version = "4.4.1" criteria = "safe-to-deploy" -[[exemptions.rayon-core]] -version = "1.12.1" +[[exemptions.raw-cpuid]] +version = "11.6.0" criteria = "safe-to-deploy" -[[exemptions.reddsa]] -version = "0.5.1" +[[exemptions.rayon]] +version = "1.12.0" criteria = "safe-to-deploy" -[[exemptions.redox_syscall]] -version = "0.2.16" +[[exemptions.reddsa]] +version = "0.5.1" criteria = "safe-to-deploy" [[exemptions.redox_syscall]] -version = "0.5.2" +version = "0.5.18" criteria = "safe-to-deploy" [[exemptions.redox_users]] version = "0.4.5" criteria = "safe-to-deploy" -[[exemptions.regex]] -version = "1.10.5" +[[exemptions.ref-cast]] +version = "1.0.25" criteria = "safe-to-deploy" -[[exemptions.regex-automata]] -version = "0.1.10" +[[exemptions.ref-cast-impl]] +version = "1.0.25" +criteria = "safe-to-deploy" + +[[exemptions.regex]] +version = "1.10.5" criteria = "safe-to-deploy" [[exemptions.regex-automata]] @@ -1266,29 +1360,33 @@ version = "0.4.7" criteria = "safe-to-deploy" [[exemptions.regex-syntax]] -version = "0.6.29" +version = "0.8.10" criteria = "safe-to-deploy" -[[exemptions.regex-syntax]] -version = "0.8.4" +[[exemptions.reqwest]] +version = "0.12.28" criteria = "safe-to-deploy" [[exemptions.reqwest]] -version = "0.11.27" +version = "0.13.3" criteria = "safe-to-deploy" [[exemptions.rgb]] -version = "0.8.37" +version = "0.8.53" criteria = "safe-to-deploy" [[exemptions.ring]] -version = "0.17.8" +version = "0.17.14" criteria = "safe-to-deploy" [[exemptions.ripemd]] version = "0.1.3" criteria = "safe-to-deploy" +[[exemptions.ripemd]] +version = "0.2.0-pre.4" +criteria = "safe-to-deploy" + [[exemptions.rlimit]] version = "0.10.1" criteria = "safe-to-deploy" @@ -1298,111 +1396,187 @@ version = "0.22.0" criteria = "safe-to-deploy" [[exemptions.ron]] -version = "0.7.1" +version = "0.12.1" +criteria = "safe-to-deploy" + +[[exemptions.route-recognizer]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.rust-ini]] +version = "0.21.3" criteria = "safe-to-deploy" [[exemptions.rustc-demangle]] -version = "0.1.24" +version = "0.1.27" criteria = "safe-to-deploy" -[[exemptions.rustc-hex]] -version = "2.1.0" +[[exemptions.rustc-hash]] +version = "2.1.2" criteria = "safe-to-deploy" -[[exemptions.rustc_version]] -version = "0.2.3" +[[exemptions.rustc-hex]] +version = "2.1.0" criteria = "safe-to-deploy" [[exemptions.rustix]] -version = "0.38.34" +version = "1.1.4" criteria = "safe-to-deploy" [[exemptions.rustls]] -version = "0.21.12" +version = "0.23.40" criteria = "safe-to-deploy" -[[exemptions.rustls-pemfile]] -version = "1.0.4" +[[exemptions.rustls-native-certs]] +version = "0.8.3" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.rustls-pki-types]] +version = "1.14.1" +criteria = "safe-to-deploy" + +[[exemptions.rustls-platform-verifier]] +version = "0.7.0" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.rustls-platform-verifier-android]] +version = "0.1.1" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.rustls-webpki]] -version = "0.101.7" +version = "0.103.13" criteria = "safe-to-deploy" [[exemptions.rusty-fork]] -version = "0.3.0" +version = "0.3.1" criteria = "safe-to-deploy" [[exemptions.ryu]] -version = "1.0.18" +version = "1.0.23" +criteria = "safe-to-deploy" + +[[exemptions.same-file]] +version = "1.0.6" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.sapling-crypto]] -version = "0.1.3" +version = "0.7.0" criteria = "safe-to-deploy" -[[exemptions.scopeguard]] -version = "1.2.0" +[[exemptions.schannel]] +version = "0.1.29" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" -[[exemptions.sct]] -version = "0.7.1" +[[exemptions.schemars]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.schemars_derive]] +version = "1.2.1" +criteria = "safe-to-deploy" + +[[exemptions.scopeguard]] +version = "1.2.0" criteria = "safe-to-deploy" [[exemptions.secp256k1]] -version = "0.26.0" +version = "0.29.1" criteria = "safe-to-deploy" [[exemptions.secp256k1-sys]] -version = "0.8.1" +version = "0.10.1" criteria = "safe-to-deploy" [[exemptions.secrecy]] version = "0.8.0" criteria = "safe-to-deploy" -[[exemptions.semver]] -version = "0.9.0" +[[exemptions.security-framework]] +version = "3.7.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" -[[exemptions.semver]] -version = "1.0.23" +[[exemptions.security-framework-sys]] +version = "2.17.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" -[[exemptions.semver-parser]] -version = "0.7.0" +[[exemptions.semver]] +version = "1.0.28" criteria = "safe-to-deploy" [[exemptions.sentry]] -version = "0.32.2" +version = "0.47.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.sentry-backtrace]] -version = "0.32.3" +version = "0.47.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.sentry-contexts]] -version = "0.32.3" +version = "0.47.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.sentry-core]] -version = "0.32.3" +version = "0.47.0" +criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" + +[[exemptions.sentry-log]] +version = "0.47.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.sentry-tracing]] -version = "0.32.3" +version = "0.47.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.sentry-types]] -version = "0.32.3" +version = "0.47.0" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.serde-big-array]] version = "0.5.1" criteria = "safe-to-deploy" +[[exemptions.serde-untagged]] +version = "0.1.9" +criteria = "safe-to-deploy" + +[[exemptions.serde_derive_internals]] +version = "0.29.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_json]] +version = "1.0.149" +criteria = "safe-to-deploy" + [[exemptions.serde_spanned]] -version = "0.6.6" +version = "1.1.1" criteria = "safe-to-deploy" [[exemptions.serde_urlencoded]] @@ -1410,59 +1584,55 @@ version = "0.7.1" criteria = "safe-to-deploy" [[exemptions.serde_with]] -version = "1.14.0" -criteria = "safe-to-deploy" - -[[exemptions.serde_with]] -version = "3.8.1" +version = "3.17.0" criteria = "safe-to-deploy" [[exemptions.serde_with_macros]] -version = "1.5.2" +version = "3.17.0" criteria = "safe-to-deploy" -[[exemptions.serde_with_macros]] -version = "3.8.1" +[[exemptions.sha2]] +version = "0.11.0-pre.4" criteria = "safe-to-deploy" -[[exemptions.sha2]] -version = "0.10.8" +[[exemptions.signal-hook-registry]] +version = "1.4.8" criteria = "safe-to-deploy" -[[exemptions.sharded-slab]] -version = "0.1.7" +[[exemptions.simd-adler32]] +version = "0.3.9" criteria = "safe-to-deploy" -[[exemptions.shardtree]] -version = "0.3.1" +[[exemptions.simd_cesu8]] +version = "1.1.1" criteria = "safe-to-deploy" -[[exemptions.shlex]] -version = "1.3.0" +[[exemptions.simdutf8]] +version = "0.1.5" criteria = "safe-to-deploy" -[[exemptions.signal-hook-registry]] -version = "1.4.2" +[[exemptions.sinsemilla]] +version = "0.1.0" criteria = "safe-to-deploy" -[[exemptions.similar]] -version = "2.5.0" +[[exemptions.siphasher]] +version = "1.0.2" criteria = "safe-to-deploy" [[exemptions.sketches-ddsketch]] -version = "0.2.2" +version = "0.3.1" criteria = "safe-to-deploy" [[exemptions.slab]] -version = "0.4.9" +version = "0.4.12" criteria = "safe-to-deploy" -[[exemptions.smallvec]] -version = "1.13.2" +[[exemptions.socket2]] +version = "0.6.3" criteria = "safe-to-deploy" -[[exemptions.socket2]] -version = "0.5.7" +[[exemptions.soketto]] +version = "0.8.1" criteria = "safe-to-deploy" [[exemptions.spandoc]] @@ -1481,92 +1651,80 @@ criteria = "safe-to-deploy" version = "0.7.3" criteria = "safe-to-deploy" -[[exemptions.str_stack]] -version = "0.1.0" -criteria = "safe-to-deploy" - -[[exemptions.strsim]] -version = "0.8.0" +[[exemptions.stable_deref_trait]] +version = "1.2.1" criteria = "safe-to-deploy" -[[exemptions.structopt]] -version = "0.3.26" +[[exemptions.str_stack]] +version = "0.1.0" criteria = "safe-to-deploy" -[[exemptions.structopt-derive]] -version = "0.4.18" +[[exemptions.symlink]] +version = "0.1.0" criteria = "safe-to-deploy" -[[exemptions.subtle]] -version = "2.4.1" +[[exemptions.syn]] +version = "2.0.117" criteria = "safe-to-deploy" [[exemptions.sync_wrapper]] version = "0.1.2" criteria = "safe-to-deploy" -[[exemptions.system-configuration]] -version = "0.5.1" -criteria = "safe-to-deploy" - -[[exemptions.system-configuration-sys]] -version = "0.5.0" -criteria = "safe-to-deploy" - -[[exemptions.tap]] -version = "1.0.1" -criteria = "safe-to-deploy" - [[exemptions.tempfile]] -version = "3.10.1" +version = "3.27.0" criteria = "safe-to-deploy" [[exemptions.termcolor]] version = "1.4.1" criteria = "safe-to-deploy" -[[exemptions.textwrap]] -version = "0.11.0" -criteria = "safe-to-deploy" - [[exemptions.thiserror]] -version = "1.0.61" +version = "2.0.17" criteria = "safe-to-deploy" [[exemptions.thiserror-impl]] -version = "1.0.61" +version = "2.0.17" criteria = "safe-to-deploy" [[exemptions.thread-priority]] -version = "1.1.0" -criteria = "safe-to-deploy" - -[[exemptions.thread_local]] -version = "1.1.8" +version = "1.2.0" criteria = "safe-to-deploy" [[exemptions.time]] version = "0.3.36" criteria = "safe-to-deploy" +[[exemptions.tiny-keccak]] +version = "2.0.2" +criteria = "safe-to-deploy" + +[[exemptions.tinyvec]] +version = "1.11.0" +criteria = "safe-to-deploy" + +[[exemptions.tokio]] +version = "1.52.1" +criteria = "safe-to-deploy" + [[exemptions.tokio-macros]] -version = "2.3.0" +version = "2.7.0" criteria = "safe-to-deploy" [[exemptions.tokio-rustls]] -version = "0.24.1" +version = "0.26.4" +criteria = "safe-to-deploy" + +[[exemptions.tokio-stream]] +version = "0.1.18" criteria = "safe-to-deploy" [[exemptions.tokio-test]] -version = "0.4.4" +version = "0.4.5" criteria = "safe-to-run" [[exemptions.tokio-util]] -version = "0.6.10" -criteria = "safe-to-deploy" - -[[exemptions.tokio-util]] -version = "0.7.11" +version = "0.7.18" criteria = "safe-to-deploy" [[exemptions.toml]] @@ -1574,43 +1732,63 @@ version = "0.5.11" criteria = "safe-to-deploy" [[exemptions.toml]] -version = "0.8.14" +version = "1.1.2+spec-1.1.0" criteria = "safe-to-deploy" [[exemptions.toml_datetime]] -version = "0.6.6" +version = "1.1.1+spec-1.1.0" criteria = "safe-to-deploy" [[exemptions.toml_edit]] -version = "0.21.1" +version = "0.25.11+spec-1.1.0" criteria = "safe-to-deploy" -[[exemptions.toml_edit]] -version = "0.22.14" +[[exemptions.toml_parser]] +version = "1.1.2+spec-1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.toml_writer]] +version = "1.1.1+spec-1.1.0" criteria = "safe-to-deploy" [[exemptions.tonic]] -version = "0.10.2" +version = "0.14.5" criteria = "safe-to-deploy" [[exemptions.tonic-build]] -version = "0.10.2" +version = "0.14.5" +criteria = "safe-to-deploy" + +[[exemptions.tonic-prost]] +version = "0.14.5" +criteria = "safe-to-deploy" + +[[exemptions.tonic-prost-build]] +version = "0.14.5" criteria = "safe-to-deploy" [[exemptions.tonic-reflection]] -version = "0.11.0" +version = "0.14.5" criteria = "safe-to-deploy" [[exemptions.tower]] version = "0.4.13" criteria = "safe-to-deploy" +[[exemptions.tower]] +version = "0.5.3" +criteria = "safe-to-deploy" + [[exemptions.tower-batch-control]] -version = "0.2.41-beta.14" +version = "1.0.1" criteria = "safe-to-deploy" [[exemptions.tower-fallback]] -version = "0.2.41-beta.14" +version = "0.2.41" +criteria = "safe-to-deploy" + +[[exemptions.tower-http]] +version = "0.6.8" criteria = "safe-to-deploy" [[exemptions.tower-layer]] @@ -1626,19 +1804,23 @@ version = "0.4.0" criteria = "safe-to-run" [[exemptions.tracing]] -version = "0.1.40" +version = "0.1.44" criteria = "safe-to-deploy" [[exemptions.tracing-appender]] -version = "0.2.3" +version = "0.2.5" criteria = "safe-to-deploy" [[exemptions.tracing-attributes]] -version = "0.1.27" +version = "0.1.31" +criteria = "safe-to-deploy" + +[[exemptions.tracing-core]] +version = "0.1.36" criteria = "safe-to-deploy" [[exemptions.tracing-error]] -version = "0.2.0" +version = "0.2.1" criteria = "safe-to-deploy" [[exemptions.tracing-flame]] @@ -1650,67 +1832,51 @@ version = "0.2.5" criteria = "safe-to-deploy" [[exemptions.tracing-journald]] -version = "0.3.0" +version = "0.3.2" criteria = "safe-to-deploy" [[exemptions.tracing-log]] version = "0.1.4" criteria = "safe-to-deploy" -[[exemptions.tracing-log]] -version = "0.2.0" +[[exemptions.tracing-opentelemetry]] +version = "0.32.1" criteria = "safe-to-deploy" [[exemptions.tracing-subscriber]] -version = "0.3.18" +version = "0.3.23" criteria = "safe-to-deploy" [[exemptions.tracing-test]] -version = "0.2.5" +version = "0.2.6" criteria = "safe-to-run" [[exemptions.tracing-test-macro]] -version = "0.2.5" +version = "0.2.6" criteria = "safe-to-run" -[[exemptions.try-lock]] -version = "0.2.5" +[[exemptions.typeid]] +version = "1.0.3" criteria = "safe-to-deploy" [[exemptions.typenum]] -version = "1.17.0" +version = "1.20.0" criteria = "safe-to-deploy" [[exemptions.ucd-trie]] -version = "0.1.6" +version = "0.1.7" criteria = "safe-to-deploy" [[exemptions.uint]] version = "0.9.5" criteria = "safe-to-deploy" -[[exemptions.uname]] -version = "0.1.1" -criteria = "safe-to-deploy" - -[[exemptions.unarray]] -version = "0.1.4" -criteria = "safe-to-deploy" - [[exemptions.unicase]] -version = "2.7.0" -criteria = "safe-to-deploy" - -[[exemptions.unicode-bidi]] -version = "0.3.15" -criteria = "safe-to-deploy" - -[[exemptions.unicode-width]] -version = "0.1.13" +version = "2.9.0" criteria = "safe-to-deploy" -[[exemptions.universal-hash]] -version = "0.5.1" +[[exemptions.unicode-ident]] +version = "1.0.24" criteria = "safe-to-deploy" [[exemptions.untrusted]] @@ -1718,35 +1884,31 @@ version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.ureq]] -version = "2.9.1" +version = "3.3.0" criteria = "safe-to-deploy" -[[exemptions.url]] -version = "2.5.2" +[[exemptions.ureq-proto]] +version = "0.6.0" criteria = "safe-to-deploy" -[[exemptions.utf8parse]] -version = "0.2.2" +[[exemptions.utf8-zero]] +version = "0.8.1" criteria = "safe-to-deploy" [[exemptions.uuid]] -version = "1.9.1" -criteria = "safe-to-deploy" - -[[exemptions.valuable]] -version = "0.1.0" +version = "1.23.1" criteria = "safe-to-deploy" -[[exemptions.vcpkg]] -version = "0.2.15" +[[exemptions.vergen]] +version = "9.1.0" criteria = "safe-to-deploy" -[[exemptions.vec_map]] -version = "0.8.2" +[[exemptions.vergen-git2]] +version = "9.1.0" criteria = "safe-to-deploy" -[[exemptions.vergen]] -version = "8.3.1" +[[exemptions.vergen-lib]] +version = "9.1.0" criteria = "safe-to-deploy" [[exemptions.wait-timeout]] @@ -1755,11 +1917,9 @@ criteria = "safe-to-deploy" [[exemptions.walkdir]] version = "2.5.0" -criteria = "safe-to-run" - -[[exemptions.want]] -version = "0.3.1" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.wasi]] version = "0.9.0+wasi-snapshot-preview1" @@ -1770,35 +1930,45 @@ version = "0.11.0+wasi-snapshot-preview1" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen]] -version = "0.2.92" -criteria = "safe-to-deploy" - -[[exemptions.wasm-bindgen-backend]] -version = "0.2.92" +version = "0.2.120" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-futures]] -version = "0.4.42" +version = "0.4.70" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-macro]] -version = "0.2.92" +version = "0.2.120" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-macro-support]] +version = "0.2.120" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-shared]] -version = "0.2.92" +version = "0.2.120" criteria = "safe-to-deploy" [[exemptions.web-sys]] -version = "0.3.69" +version = "0.3.97" +criteria = "safe-to-deploy" + +[[exemptions.web-time]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.webpki-root-certs]] +version = "1.0.7" criteria = "safe-to-deploy" +suggest = false +notes = "Sentry 0.47 upgrade" [[exemptions.webpki-roots]] -version = "0.25.4" +version = "1.0.7" criteria = "safe-to-deploy" [[exemptions.which]] -version = "4.4.2" +version = "8.0.2" criteria = "safe-to-deploy" [[exemptions.winapi]] @@ -1810,107 +1980,119 @@ version = "0.4.0" criteria = "safe-to-deploy" [[exemptions.winapi-util]] -version = "0.1.8" +version = "0.1.9" criteria = "safe-to-deploy" [[exemptions.winapi-x86_64-pc-windows-gnu]] version = "0.4.0" criteria = "safe-to-deploy" -[[exemptions.windows]] -version = "0.52.0" +[[exemptions.windows-core]] +version = "0.62.2" criteria = "safe-to-deploy" -[[exemptions.windows-core]] +[[exemptions.windows-implement]] +version = "0.60.2" +criteria = "safe-to-deploy" + +[[exemptions.windows-interface]] +version = "0.59.3" +criteria = "safe-to-deploy" + +[[exemptions.windows-result]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.windows-strings]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.windows-sys]] version = "0.52.0" criteria = "safe-to-deploy" [[exemptions.windows-sys]] -version = "0.48.0" +version = "0.60.2" criteria = "safe-to-deploy" [[exemptions.windows-sys]] -version = "0.52.0" +version = "0.61.2" criteria = "safe-to-deploy" [[exemptions.windows-targets]] -version = "0.48.5" +version = "0.52.5" criteria = "safe-to-deploy" [[exemptions.windows-targets]] -version = "0.52.5" +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_aarch64_gnullvm]] -version = "0.48.5" +version = "0.52.5" criteria = "safe-to-deploy" [[exemptions.windows_aarch64_gnullvm]] -version = "0.52.5" +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_aarch64_msvc]] -version = "0.48.5" +version = "0.52.5" criteria = "safe-to-deploy" [[exemptions.windows_aarch64_msvc]] -version = "0.52.5" +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_i686_gnu]] -version = "0.48.5" +version = "0.52.5" criteria = "safe-to-deploy" [[exemptions.windows_i686_gnu]] -version = "0.52.5" +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_i686_gnullvm]] version = "0.52.5" criteria = "safe-to-deploy" -[[exemptions.windows_i686_msvc]] -version = "0.48.5" +[[exemptions.windows_i686_gnullvm]] +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_i686_msvc]] version = "0.52.5" criteria = "safe-to-deploy" -[[exemptions.windows_x86_64_gnu]] -version = "0.48.5" +[[exemptions.windows_i686_msvc]] +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_x86_64_gnu]] version = "0.52.5" criteria = "safe-to-deploy" -[[exemptions.windows_x86_64_gnullvm]] -version = "0.48.5" +[[exemptions.windows_x86_64_gnu]] +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_x86_64_gnullvm]] version = "0.52.5" criteria = "safe-to-deploy" -[[exemptions.windows_x86_64_msvc]] -version = "0.48.5" +[[exemptions.windows_x86_64_gnullvm]] +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.windows_x86_64_msvc]] version = "0.52.5" criteria = "safe-to-deploy" -[[exemptions.winnow]] -version = "0.5.40" +[[exemptions.windows_x86_64_msvc]] +version = "0.53.0" criteria = "safe-to-deploy" [[exemptions.winnow]] -version = "0.6.13" -criteria = "safe-to-deploy" - -[[exemptions.winreg]] -version = "0.50.0" +version = "1.0.2" criteria = "safe-to-deploy" [[exemptions.wyz]] @@ -1922,19 +2104,19 @@ version = "2.0.1" criteria = "safe-to-deploy" [[exemptions.xdg]] -version = "2.5.2" +version = "3.0.0" criteria = "safe-to-deploy" -[[exemptions.zcash_address]] -version = "0.3.2" +[[exemptions.yaml-rust2]] +version = "0.10.4" criteria = "safe-to-deploy" -[[exemptions.zcash_client_backend]] -version = "0.12.1" +[[exemptions.zcash_address]] +version = "0.12.0" criteria = "safe-to-deploy" [[exemptions.zcash_encoding]] -version = "0.2.0" +version = "0.4.0" criteria = "safe-to-deploy" [[exemptions.zcash_history]] @@ -1942,89 +2124,61 @@ version = "0.4.0" criteria = "safe-to-deploy" [[exemptions.zcash_keys]] -version = "0.2.0" -criteria = "safe-to-deploy" - -[[exemptions.zcash_note_encryption]] -version = "0.4.0" +version = "0.14.0" criteria = "safe-to-deploy" [[exemptions.zcash_primitives]] -version = "0.15.1" +version = "0.28.0" criteria = "safe-to-deploy" [[exemptions.zcash_proofs]] -version = "0.15.0" +version = "0.28.0" criteria = "safe-to-deploy" [[exemptions.zcash_protocol]] -version = "0.1.1" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.zcash_script]] -version = "0.2.0" +version = "0.4.5" criteria = "safe-to-deploy" [[exemptions.zcash_spec]] -version = "0.1.0" -criteria = "safe-to-deploy" - -[[exemptions.zebra-chain]] -version = "1.0.0-beta.38" -criteria = "safe-to-deploy" - -[[exemptions.zebra-consensus]] -version = "1.0.0-beta.38" -criteria = "safe-to-deploy" - -[[exemptions.zebra-grpc]] -version = "0.1.0-alpha.5" -criteria = "safe-to-deploy" - -[[exemptions.zebra-network]] -version = "1.0.0-beta.38" -criteria = "safe-to-deploy" - -[[exemptions.zebra-node-services]] -version = "1.0.0-beta.38" -criteria = "safe-to-deploy" - -[[exemptions.zebra-rpc]] -version = "1.0.0-beta.38" -criteria = "safe-to-deploy" - -[[exemptions.zebra-scan]] -version = "0.1.0-alpha.7" +version = "0.2.1" criteria = "safe-to-deploy" -[[exemptions.zebra-script]] -version = "1.0.0-beta.38" +[[exemptions.zcash_transparent]] +version = "0.8.0" criteria = "safe-to-deploy" -[[exemptions.zebra-state]] -version = "1.0.0-beta.38" +[[exemptions.zebra-test]] +version = "3.0.0" criteria = "safe-to-deploy" -[[exemptions.zebra-test]] -version = "1.0.0-beta.38" +[[exemptions.zerocopy]] +version = "0.8.48" criteria = "safe-to-deploy" -[[exemptions.zebra-utils]] -version = "1.0.0-beta.38" +[[exemptions.zerocopy-derive]] +version = "0.8.48" criteria = "safe-to-deploy" -[[exemptions.zebrad]] -version = "1.8.0" +[[exemptions.zerofrom]] +version = "0.1.7" criteria = "safe-to-deploy" -[[exemptions.zeroize]] -version = "1.8.1" +[[exemptions.zerofrom-derive]] +version = "0.1.7" criteria = "safe-to-deploy" [[exemptions.zeroize_derive]] -version = "1.4.2" +version = "1.4.3" criteria = "safe-to-deploy" [[exemptions.zip32]] -version = "0.1.1" +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.zmij]] +version = "1.0.21" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 510c79906eb..76962cc5f60 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -1,57 +1,12 @@ # cargo-vet imports lock -[[unpublished.tower-batch-control]] -version = "0.2.41-beta.16" -audited_as = "0.2.41-beta.15" - -[[unpublished.tower-fallback]] -version = "0.2.41-beta.16" -audited_as = "0.2.41-beta.15" - -[[unpublished.zebra-chain]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-consensus]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-grpc]] -version = "0.1.0-alpha.7" -audited_as = "0.1.0-alpha.6" - -[[unpublished.zebra-network]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-node-services]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-rpc]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-scan]] -version = "0.1.0-alpha.9" -audited_as = "0.1.0-alpha.7" - -[[unpublished.zebra-script]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-state]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebra-utils]] -version = "1.0.0-beta.40" -audited_as = "1.0.0-beta.39" - -[[unpublished.zebrad]] -version = "2.0.0-rc.0" -audited_as = "1.9.0" +[[publisher.bumpalo]] +version = "3.20.2" +when = "2026-02-19" +user-id = 696 +user-login = "fitzgen" +user-name = "Nick Fitzgerald" [[publisher.cexpr]] version = "0.6.0" @@ -60,48 +15,13 @@ user-id = 3788 user-login = "emilio" user-name = "Emilio Cobos Álvarez" -[[publisher.clap]] -version = "4.5.20" -when = "2024-10-08" -user-id = 6743 -user-login = "epage" -user-name = "Ed Page" - -[[publisher.clap_builder]] -version = "4.5.20" -when = "2024-10-08" -user-id = 6743 -user-login = "epage" -user-name = "Ed Page" - -[[publisher.clap_derive]] -version = "4.5.18" -when = "2024-09-20" -user-id = 6743 -user-login = "epage" -user-name = "Ed Page" - -[[publisher.core-foundation]] -version = "0.9.3" -when = "2022-02-07" -user-id = 5946 -user-login = "jrmuizel" -user-name = "Jeff Muizelaar" - [[publisher.encoding_rs]] -version = "0.8.34" -when = "2024-04-10" +version = "0.8.35" +when = "2024-10-24" user-id = 4484 user-login = "hsivonen" user-name = "Henri Sivonen" -[[publisher.serde_json]] -version = "1.0.132" -when = "2024-10-19" -user-id = 3618 -user-login = "dtolnay" -user-name = "David Tolnay" - [[publisher.syn]] version = "1.0.109" when = "2023-02-24" @@ -109,1322 +29,3910 @@ user-id = 3618 user-login = "dtolnay" user-name = "David Tolnay" -[[publisher.syn]] -version = "2.0.82" -when = "2024-10-20" -user-id = 3618 -user-login = "dtolnay" -user-name = "David Tolnay" +[[publisher.unicode-segmentation]] +version = "1.13.2" +when = "2026-03-26" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" -[[publisher.tokio]] -version = "1.41.0" -when = "2024-10-22" -user-id = 6741 -user-login = "Darksonn" -user-name = "Alice Ryhl" +[[publisher.unicode-width]] +version = "0.1.14" +when = "2024-09-19" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" -[[publisher.unicode-normalization]] -version = "0.1.23" -when = "2024-02-20" +[[publisher.unicode-width]] +version = "0.2.2" +when = "2025-10-06" user-id = 1139 user-login = "Manishearth" user-name = "Manish Goregaokar" -[[publisher.unicode-segmentation]] -version = "1.11.0" -when = "2024-02-07" +[[publisher.unicode-xid]] +version = "0.2.6" +when = "2024-09-19" user-id = 1139 user-login = "Manishearth" user-name = "Manish Goregaokar" -[[audits.google.audits.adler]] -who = "Lukasz Anforowicz " +[[publisher.utf8_iter]] +version = "1.0.4" +when = "2023-12-01" +user-id = 4484 +user-login = "hsivonen" +user-name = "Henri Sivonen" + +[[publisher.wasip2]] +version = "1.0.3+wasi-0.2.9" +when = "2026-04-17" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasip3]] +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +when = "2026-01-15" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-encoder]] +version = "0.244.0" +when = "2026-01-06" +trusted-publisher = "github:bytecodealliance/wasm-tools" + +[[publisher.wasm-metadata]] +version = "0.236.0" +when = "2025-07-28" +user-id = 73222 +user-login = "wasmtime-publish" + +[[publisher.wasmparser]] +version = "0.244.0" +when = "2026-01-06" +trusted-publisher = "github:bytecodealliance/wasm-tools" + +[[publisher.wit-bindgen]] +version = "0.51.0" +when = "2026-01-12" +trusted-publisher = "github:bytecodealliance/wit-bindgen" + +[[publisher.wit-bindgen]] +version = "0.57.1" +when = "2026-04-17" +trusted-publisher = "github:bytecodealliance/wit-bindgen" + +[[publisher.wit-bindgen-core]] +version = "0.51.0" +when = "2026-01-12" +trusted-publisher = "github:bytecodealliance/wit-bindgen" + +[[publisher.wit-bindgen-rust]] +version = "0.51.0" +when = "2026-01-12" +trusted-publisher = "github:bytecodealliance/wit-bindgen" + +[[publisher.wit-bindgen-rust-macro]] +version = "0.51.0" +when = "2026-01-12" +trusted-publisher = "github:bytecodealliance/wit-bindgen" + +[[publisher.wit-component]] +version = "0.244.0" +when = "2026-01-06" +trusted-publisher = "github:bytecodealliance/wasm-tools" + +[[publisher.wit-parser]] +version = "0.244.0" +when = "2026-01-06" +trusted-publisher = "github:bytecodealliance/wasm-tools" + +[[publisher.zebra-chain]] +version = "10.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-consensus]] +version = "9.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-network]] +version = "9.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-node-services]] +version = "8.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-rpc]] +version = "10.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-script]] +version = "9.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-state]] +version = "9.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebra-utils]] +version = "8.0.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[publisher.zebrad]] +version = "5.1.0" +when = "2026-06-11" +user-id = 228785 +user-login = "oxarbitrage" +user-name = "Alfredo Garcia" + +[[audits.bytecode-alliance.wildcard-audits.bumpalo]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +user-id = 696 # Nick Fitzgerald (fitzgen) +start = "2019-03-16" +end = "2026-08-21" + +[[audits.bytecode-alliance.wildcard-audits.wasip2]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2025-08-10" +end = "2026-08-21" +notes = """ +This is a Bytecode Alliance authored crate. +""" + +[[audits.bytecode-alliance.wildcard-audits.wasip3]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.0.2" -notes = ''' -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` -and there were no hits (except in comments and in the `README.md` file). +user-id = 1 # Alex Crichton (alexcrichton) +start = "2025-09-10" +end = "2026-08-21" +notes = """ +This is a Bytecode Alliance authored crate. +""" -Note that some additional, internal notes about an older version of this crate -can be found at go/image-crate-chromium-security-review. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.wildcard-audits.wasm-encoder]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +trusted-publisher = "github:bytecodealliance/wasm-tools" +start = "2025-08-14" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.async-stream]] -who = "Tyler Mandry " +[[audits.bytecode-alliance.wildcard-audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "0.3.4" -notes = "Reviewed on https://fxrev.dev/761470" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +user-id = 73222 # wasmtime-publish +start = "2023-01-01" +end = "2026-06-03" +notes = """ +The Bytecode Alliance uses the `wasmtime-publish` crates.io account to automate +publication of this crate from CI. This repository requires all PRs are reviewed +by a Bytecode Alliance maintainer and it owned by the Bytecode Alliance itself. +""" -[[audits.google.audits.async-stream]] -who = "David Koloski " +[[audits.bytecode-alliance.wildcard-audits.wasmparser]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "0.3.4 -> 0.3.5" -notes = "Reviewed on https://fxrev.dev/906795" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +trusted-publisher = "github:bytecodealliance/wasm-tools" +start = "2025-08-14" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.async-stream-impl]] -who = "Tyler Mandry " +[[audits.bytecode-alliance.wildcard-audits.wit-bindgen]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "0.3.4" -notes = "Reviewed on https://fxrev.dev/761470" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +trusted-publisher = "github:bytecodealliance/wit-bindgen" +start = "2025-08-13" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.async-stream-impl]] -who = "David Koloski " +[[audits.bytecode-alliance.wildcard-audits.wit-bindgen-core]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "0.3.4 -> 0.3.5" -notes = "Reviewed on https://fxrev.dev/906795" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +trusted-publisher = "github:bytecodealliance/wit-bindgen" +start = "2025-08-13" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.autocfg]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.wildcard-audits.wit-bindgen-rust]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.1.0" -notes = """ -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` -and there were no hits except for reasonable, client-controlled usage of -`std::fs` in `AutoCfg::with_dir`. +trusted-publisher = "github:bytecodealliance/wit-bindgen" +start = "2025-08-13" +end = "2027-01-12" +notes = "The Bytecode Alliance is the author of this crate" -This crate has been added to Chromium in -https://source.chromium.org/chromium/chromium/src/+/591a0f30c5eac93b6a3d981c2714ffa4db28dbcb -The CL description contains a link to a Google-internal document with audit details. -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.wildcard-audits.wit-bindgen-rust-macro]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +trusted-publisher = "github:bytecodealliance/wit-bindgen" +start = "2025-08-13" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.autocfg]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.wildcard-audits.wit-component]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.1.0 -> 1.2.0" -notes = ''' -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` -and nothing changed from the baseline audit of 1.1.0. Skimmed through the -1.1.0 => 1.2.0 delta and everything seemed okay. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +trusted-publisher = "github:bytecodealliance/wasm-tools" +start = "2025-08-14" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.base64]] -who = "Adam Langley " +[[audits.bytecode-alliance.wildcard-audits.wit-parser]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "0.13.1" -notes = "Skimmed the uses of `std` to ensure that nothing untoward is happening. Code uses `forbid(unsafe_code)` and, indeed, there are no uses of `unsafe`" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +trusted-publisher = "github:bytecodealliance/wasm-tools" +start = "2025-08-14" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.bitflags]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.addr2line]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.3.2" -notes = """ -Security review of earlier versions of the crate can be found at -(Google-internal, sorry): go/image-crate-chromium-security-review +delta = "0.21.0 -> 0.22.0" -The crate exposes a function marked as `unsafe`, but doesn't use any -`unsafe` blocks (except for tests of the single `unsafe` function). I -think this justifies marking this crate as `ub-risk-1`. +[[audits.bytecode-alliance.audits.addr2line]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.22.0 -> 0.24.1" +notes = "Lots of internal code refactorings and code movement. Nothing out of place however." -Additional review comments can be found at https://crrev.com/c/4723145/31 -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.addr2line]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.24.1 -> 0.25.0" +notes = "All minor changes, even a net reduction of `unsafe`." -[[audits.google.audits.bitflags]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.addr2line]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "2.4.2" -notes = """ -Audit notes: +delta = "0.25.0 -> 0.25.1" +notes = "Minor updates, looks like a minor bug fix, nothing awry." + +[[audits.bytecode-alliance.audits.adler2]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "2.0.0" +notes = "Fork of the original `adler` crate, zero unsfae code, works in `no_std`, does what it says on th tin." -* I've checked for any discussion in Google-internal cl/546819168 (where audit - of version 2.3.3 happened) -* `src/lib.rs` contains `#![cfg_attr(not(test), forbid(unsafe_code))]` -* There are 2 cases of `unsafe` in `src/external.rs` but they seem to be - correct in a straightforward way - they just propagate the marker trait's - impl (e.g. `impl bytemuck::Pod`) from the inner to the outer type -* Additional discussion and/or notes may be found in https://crrev.com/c/5238056 +[[audits.bytecode-alliance.audits.allocator-api2]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.2.18 -> 0.2.20" +notes = """ +The changes appear to be reasonable updates from Rust's stdlib imported into +`allocator-api2`'s copy of this code. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.bitflags]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.anes]] +who = "Pat Hickey " criteria = "safe-to-deploy" -delta = "2.4.2 -> 2.5.0" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.1.6" +notes = "Contains no unsafe code, no IO, no build.rs." -[[audits.google.audits.bitflags]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.ansi_term]] +who = "Chris Fallin " criteria = "safe-to-deploy" -delta = "2.5.0 -> 2.6.0" -notes = "The changes from the previous version are negligible and thus it retains the same properties." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.12.1" +notes = "Only unsafe code is to access the console on Windows." -[[audits.google.audits.bytemuck]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.arrayref]] +who = "Nick Fitzgerald " criteria = "safe-to-deploy" -version = "1.16.3" +version = "0.3.6" notes = """ -Review notes from the original audit (of 1.14.3) may be found in -https://crrev.com/c/5362675. Note that this audit has initially missed UB risk -that was fixed in 1.16.2 - see https://github.com/Lokathor/bytemuck/pull/258. -Because of this, the original audit has been edited to certify version `1.16.3` -instead (see also https://crrev.com/c/5771867). +Unsafe code, but its logic looks good to me. Necessary given what it is +doing. Well tested, has quickchecks. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.byteorder]] -who = "danakj " +[[audits.bytecode-alliance.audits.atomic-waker]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.5.0" -notes = "Unsafe review in https://crrev.com/c/5838022" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "1.1.2" +notes = "Contains `unsafe` code but it's well-documented and scoped to what it's intended to be doing. Otherwise a well-focused and straightforward crate." -[[audits.google.audits.cast]] -who = "George Burgess IV " -criteria = "safe-to-run" -version = "0.3.0" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.atty]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.2.14" +notes = """ +Contains only unsafe code for what this crate's purpose is and only accesses +the environment's terminal information when asked. Does its stated purpose and +no more. +""" -[[audits.google.audits.cfg-if]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.base64]] +who = "Pat Hickey " criteria = "safe-to-deploy" -version = "1.0.0" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +version = "0.21.0" +notes = "This crate has no dependencies, no build.rs, and contains no unsafe code." -[[audits.google.audits.crc32fast]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.bitflags]] +who = "Jamey Sharp " criteria = "safe-to-deploy" -version = "1.4.2" +delta = "2.1.0 -> 2.2.1" notes = """ -Security review of earlier versions of the crate can be found at -(Google-internal, sorry): go/image-crate-chromium-security-review - -Audit comments for 1.4.2 can be found at https://crrev.com/c/4723145. +This version adds unsafe impls of traits from the bytemuck crate when built +with that library enabled, but I believe the impls satisfy the documented +safety requirements for bytemuck. The other changes are minor. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.equivalent]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.0.1" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +delta = "2.3.2 -> 2.3.3" +notes = """ +Nothing outside the realm of what one would expect from a bitflags generator, +all as expected. +""" -[[audits.google.audits.fastrand]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.9.0" +delta = "2.4.1 -> 2.6.0" notes = """ -`does-not-implement-crypto` is certified because this crate explicitly says -that the RNG here is not cryptographically secure. +Changes in how macros are invoked and various bits and pieces of macro-fu. +Otherwise no major changes and nothing dealing with `unsafe`. """ -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" -[[audits.google.audits.flate2]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.0.30" -notes = ''' -WARNING: This certification is a result of a **partial** audit. The -`any_zlib` code has **not** been audited. Ability to track partial -audits is tracked in https://github.com/mozilla/cargo-vet/issues/380 -Chromium does use the `any_zlib` feature(s). Accidentally depending on -this feature in the future is prevented using the `ban_features` feature -of `gnrt` - see: -https://crrev.com/c/4723145/31/third_party/rust/chromium_crates_io/gnrt_config.toml - -Security review of earlier versions of the crate can be found at -(Google-internal, sorry): go/image-crate-chromium-security-review +delta = "2.7.0 -> 2.9.4" +notes = "Tweaks to the macro, nothing out of order." -I grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.10.0 -> 2.11.1" +notes = "Minor updates, nothing awry here." -All `unsafe` in `flate2` is gated behind `#[cfg(feature = "any_zlib")]`: +[[audits.bytecode-alliance.audits.block-buffer]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.10.2" -* The code under `src/ffi/...` will not be used because the `mod c` - declaration in `src/ffi/mod.rs` depends on the `any_zlib` config -* 7 uses of `unsafe` in `src/mem.rs` also all depend on the - `any_zlib` config: - - 2 in `fn set_dictionary` (under `impl Compress`) - - 2 in `fn set_level` (under `impl Compress`) - - 3 in `fn set_dictionary` (under `impl Decompress`) +[[audits.bytecode-alliance.audits.cfg-if]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "I am the author of this crate." -All hits of `'\bfs\b'` are in comments, or example code, or test code -(but not in product code). +[[audits.bytecode-alliance.audits.cipher]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +version = "0.4.4" +notes = "Most unsafe is hidden by `inout` dependency; only remaining unsafe is raw-splitting a slice and an unreachable hint. Older versions of this regularly reach ~150k daily downloads." -There were no hits of `-i cipher`, `-i crypto`, `'\bnet\b'`. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.clap]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "2.34.0" -[[audits.google.audits.futures]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.der]] +who = "Chris Fallin " criteria = "safe-to-deploy" -version = "0.3.28" -notes = """ -`futures` has no logic other than tests - it simply `pub use`s things from -other crates. -""" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +version = "0.7.10" +notes = "No unsafe code aside from transmutes for transparent newtypes." -[[audits.google.audits.glob]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.encode_unicode]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "0.3.1" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +delta = "0.3.6 -> 1.0.0" +notes = "Lots of updates, small edits to `unsafe` code, but all as expected." -[[audits.google.audits.httpdate]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " criteria = "safe-to-deploy" -version = "1.0.3" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +version = "0.3.0" +notes = "This crate uses libc and windows-sys APIs to get and set the raw OS error value." -[[audits.google.audits.itertools]] -who = "ChromeOS" -criteria = "safe-to-run" -version = "0.10.5" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = "Just a dependency version bump and a bug fix for redox" -[[audits.google.audits.itoa]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " criteria = "safe-to-deploy" -version = "1.0.10" -notes = ''' -I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. +delta = "0.3.9 -> 0.3.10" -There are a few places where `unsafe` is used. Unsafe review notes can be found -in https://crrev.com/c/5350697. +[[audits.bytecode-alliance.audits.getrandom]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.4.2" +notes = "Nothing awry in this update, standard updates for some platforms and other misc things." -Version 1.0.1 of this crate has been added to Chromium in -https://crrev.com/c/3321896. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.gimli]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.29.0 -> 0.31.0" +notes = "Various updates here and there, nothing too major, what you'd expect from a DWARF parsing crate." -[[audits.google.audits.itoa]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.gimli]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.10 -> 1.0.11" -notes = """ -Straightforward diff between 1.0.10 and 1.0.11 - only 3 commits: +delta = "0.31.0 -> 0.31.1" +notes = "No fundmanetally new `unsafe` code, some small refactoring of existing code. Lots of changes in tests, not as many changes in the rest of the crate. More dwarf!" -* Bumping up the version -* A touch up of comments -* And my own PR to make `unsafe` blocks more granular: - https://github.com/dtolnay/itoa/pull/42 -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.gimli]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.31.1 -> 0.32.0" +notes = "Ever more DWARF to parse, but also no new `unsafe` and everything looks like gimli." -[[audits.google.audits.lazy_static]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.gimli]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.4.0" -notes = ''' -I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. +delta = "0.32.0 -> 0.32.3" +notes = "Ever more dwarf, it never ends! (nothing out of the ordinary)" -There are two places where `unsafe` is used. Unsafe review notes can be found -in https://crrev.com/c/5347418. +[[audits.bytecode-alliance.audits.hashbrown]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.14.5 -> 0.15.2" -This crate has been added to Chromium in https://crrev.com/c/3321895. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.heck]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = "Contains `forbid_unsafe` and only uses `std::fmt` from the standard library. Otherwise only contains string manipulation." -[[audits.google.audits.lazy_static]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.heck]] +who = "Chris Fallin " criteria = "safe-to-deploy" -delta = "1.4.0 -> 1.5.0" -notes = "Unsafe review notes: https://crrev.com/c/5650836" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.4.0 -> 0.3.3" -[[audits.google.audits.miniz_oxide]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.heck]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "0.7.4" -notes = ''' -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` -and there were no hits, except for some mentions of "unsafe" in the `README.md` -and in a comment in `src/deflate/core.rs`. The comment discusses whether a -function should be treated as unsafe, but there is no actual `unsafe` code, so -the crate meets the `ub-risk-0` criteria. +delta = "0.4.1 -> 0.5.0" +notes = "Minor changes for a `no_std` upgrade but otherwise everything looks as expected." -Note that some additional, internal notes about an older version of this crate -can be found at go/image-crate-chromium-security-review. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.hermit-abi]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.1.19" -[[audits.google.audits.nom]] -who = "danakj@chromium.org" +[[audits.bytecode-alliance.audits.hermit-abi]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "7.1.3" -notes = """ -Reviewed in https://chromium-review.googlesource.com/c/chromium/src/+/5046153 -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.3.9 -> 0.5.2" +notes = "API updates and looks like libc, nothing new here." -[[audits.google.audits.number_prefix]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.http-body]] +who = "Pat Hickey " criteria = "safe-to-deploy" -version = "0.4.0" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +version = "1.0.0-rc.2" -[[audits.google.audits.pin-project-lite]] -who = "David Koloski " +[[audits.bytecode-alliance.audits.http-body]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "0.2.9" -notes = "Reviewed on https://fxrev.dev/824504" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +delta = "1.0.0-rc.2 -> 1.0.0" +notes = "Only minor changes made for a stable release." -[[audits.google.audits.pin-project-lite]] -who = "David Koloski " +[[audits.bytecode-alliance.audits.iana-time-zone-haiku]] +who = "Dan Gohman " criteria = "safe-to-deploy" -delta = "0.2.9 -> 0.2.13" -notes = "Audited at https://fxrev.dev/946396" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +version = "0.1.2" -[[audits.google.audits.proc-macro-error-attr]] -who = "George Burgess IV " +[[audits.bytecode-alliance.audits.icu_properties]] +who = "Nick Fitzgerald " criteria = "safe-to-deploy" -version = "1.0.4" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +delta = "1.5.0 -> 1.5.1" -[[audits.google.audits.proc-macro2]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.idna]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.0.78" +version = "0.3.0" notes = """ -Grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits -(except for a benign \"fs\" hit in a doc comment) - -Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. +This is a crate without unsafe code or usage of the standard library. The large +size of this crate comes from the large generated unicode tables file. This +crate is broadly used throughout the ecosystem and does not contain anything +suspicious. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.proc-macro2]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.inout]] +who = "Andrew Brown " criteria = "safe-to-deploy" -delta = "1.0.78 -> 1.0.79" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.1.3" +notes = "A part of RustCrypto/utils, this crate is designed to handle unsafe buffers and carefully documents the safety concerns throughout. Older versions of this tally up to ~130k daily downloads." -[[audits.google.audits.proc-macro2]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.itertools]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.79 -> 1.0.80" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.12.1 -> 0.14.0" +notes = """ +Lots of new iterators and shuffling some things around. Some new unsafe code but +it's well-documented and well-tested. Nothing suspicious. +""" -[[audits.google.audits.proc-macro2]] -who = "Dustin J. Mitchell " +[[audits.bytecode-alliance.audits.leb128fmt]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.80 -> 1.0.81" -notes = "Comment changes only" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.1.0" +notes = "Well-scoped crate do doing LEB encoding with no `unsafe` code and does what it says on the tin." -[[audits.google.audits.proc-macro2]] -who = "danakj " +[[audits.bytecode-alliance.audits.matchers]] +who = "Pat Hickey " criteria = "safe-to-deploy" -delta = "1.0.81 -> 1.0.82" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.1.0" -[[audits.google.audits.proc-macro2]] -who = "Dustin J. Mitchell " +[[audits.bytecode-alliance.audits.matchers]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.82 -> 1.0.83" -notes = "Substantive change is replacing String with Box, saving memory." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.1.0 -> 0.2.0" +notes = "Some unsafe code, but not more than before. Nothing awry." -[[audits.google.audits.proc-macro2]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.miniz_oxide]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.83 -> 1.0.84" -notes = "Only doc comment changes in `src/lib.rs`." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.7.1" +notes = """ +This crate is a Rust implementation of zlib compression/decompression and has +been used by default by the Rust standard library for quite some time. It's also +a default dependency of the popular `backtrace` crate for decompressing debug +information. This crate forbids unsafe code and does not otherwise access system +resources. It's originally a port of the `miniz.c` library as well, and given +its own longevity should be relatively hardened against some of the more common +compression-related issues. +""" -[[audits.google.audits.proc-macro2]] -who = "danakj@chromium.org" +[[audits.bytecode-alliance.audits.miniz_oxide]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.84 -> 1.0.85" -notes = "Test-only changes." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.7.1 -> 0.8.0" +notes = "Minor updates, using new Rust features like `const`, no major changes." -[[audits.google.audits.proc-macro2]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.miniz_oxide]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.85 -> 1.0.86" +delta = "0.8.0 -> 0.8.5" notes = """ -Comment-only changes in `build.rs`. -Reordering of `Cargo.toml` entries. -Just bumping up the version number in `lib.rs`. -Config-related changes in `test_size.rs`. +Lots of small updates here and there, for example around modernizing Rust +idioms. No new `unsafe` code and everything looks like what you'd expect a +compression library to be doing. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.quote]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.miniz_oxide]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.0.35" -notes = """ -Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits -(except for benign \"net\" hit in tests and \"fs\" hit in README.md) -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.8.5 -> 0.8.9" +notes = "No new unsafe code, just refactorings." -[[audits.google.audits.quote]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Pat Hickey " criteria = "safe-to-deploy" -delta = "1.0.35 -> 1.0.36" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.46.0" +notes = "one use of unsafe to call windows specific api to get console handle." -[[audits.google.audits.quote]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.36 -> 1.0.37" -notes = """ -The delta just 1) inlines/expands `impl ToTokens` that used to be handled via -`primitive!` macro and 2) adds `impl ToTokens` for `CStr` and `CString`. -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.46.0 -> 0.50.1" +notes = "Lots of stylistic/rust-related changes, plus new features, but nothing out of the ordrinary." -[[audits.google.audits.rustversion]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.0.14" -notes = """ -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` -and there were no hits except for: +delta = "0.50.1 -> 0.50.3" +notes = "CI changes, Rust changes, nothing out of the ordinary." -* Using trivially-safe `unsafe` in test code: +[[audits.bytecode-alliance.audits.num-conv]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" +notes = "Minor update, nothing major" - ``` - tests/test_const.rs:unsafe fn _unsafe() {} - tests/test_const.rs:const _UNSAFE: () = unsafe { _unsafe() }; - ``` +[[audits.bytecode-alliance.audits.num-traits]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +version = "0.2.19" +notes = "As advertised: a numeric library. The only `unsafe` is from some float-to-int conversions, which seems expected." -* Using `unsafe` in a string: +[[audits.bytecode-alliance.audits.object]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.36.0 -> 0.36.5" +notes = "No new unsafe code, lots of new relocations/objects support, everything looks nominal" - ``` - src/constfn.rs: \"unsafe\" => Qualifiers::Unsafe, - ``` +[[audits.bytecode-alliance.audits.object]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.36.5 -> 0.37.1" +notes = "New object file formats, new formatting, new other minor changes, no new `unsafe`." -* Using `std::fs` in `build/build.rs` to write `${OUT_DIR}/version.expr` - which is later read back via `include!` used in `src/lib.rs`. +[[audits.bytecode-alliance.audits.object]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.37.1 -> 0.37.3" +notes = "Lots of new support for new object features, no new unsafe or anything suspicious." -Version `1.0.6` of this crate has been added to Chromium in -https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +[[audits.bytecode-alliance.audits.pem-rfc7468]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.7.0" +notes = "Only `unsafe` around a `from_utf8_unchecked`, and no IO." + +[[audits.bytecode-alliance.audits.percent-encoding]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "2.2.0" +notes = """ +This crate is a single-file crate that does what it says on the tin. There are +a few `unsafe` blocks related to utf-8 validation which are locally verifiable +as correct and otherwise this crate is good to go. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.rustversion]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.proc-macro-error]] +who = "Chris Fallin " criteria = "safe-to-deploy" -delta = "1.0.14 -> 1.0.15" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "1.0.4" -[[audits.google.audits.rustversion]] -who = "danakj " +[[audits.bytecode-alliance.audits.rand]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.15 -> 1.0.16" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.9.2 -> 0.9.4" +notes = "Minor bugfix release" -[[audits.google.audits.rustversion]] -who = "Dustin J. Mitchell " +[[audits.bytecode-alliance.audits.rand_xorshift]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.16 -> 1.0.17" -notes = "Just updates windows compat" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.3.0 -> 0.4.0" +notes = "Minor updates for a new `rand` crate version, nothing awry." -[[audits.google.audits.same-file]] -who = "Android Legacy" -criteria = "safe-to-run" -version = "1.0.6" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.sha1]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +delta = "0.10.5 -> 0.10.6" +notes = "Only new code is some loongarch64 additions which include assembly code for that platform." -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.sharded-slab]] +who = "Pat Hickey " criteria = "safe-to-deploy" -version = "1.0.197" -notes = """ -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. +version = "0.1.4" +notes = "I always really enjoy reading eliza's code, she left perfect comments at every use of unsafe." -There were some hits for `net`, but they were related to serialization and -not actually opening any connections or anything like that. +[[audits.bytecode-alliance.audits.shlex]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = "Only minor `unsafe` code blocks which look valid and otherwise does what it says on the tin." -There were 2 hits of `unsafe` when grepping: -* In `fn as_str` in `impl Buf` -* In `fn serialize` in `impl Serialize for net::Ipv4Addr` +[[audits.bytecode-alliance.audits.smallvec]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "1.13.2 -> 1.14.0" +notes = "Minor new feature, nothing out of the ordinary." -Unsafe review comments can be found in https://crrev.com/c/5350573/2 (this -review also covered `serde_json_lenient`). +[[audits.bytecode-alliance.audits.strsim]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.10.0 -> 0.8.0" -Version 1.0.130 of the crate has been added to Chromium in -https://crrev.com/c/3265545. The CL description contains a link to a -(Google-internal, sorry) document with a mini security review. -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.bytecode-alliance.audits.structopt]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.3.26" -[[audits.google.audits.serde]] -who = "Dustin J. Mitchell " +[[audits.bytecode-alliance.audits.structopt-derive]] +who = "Chris Fallin " criteria = "safe-to-deploy" -delta = "1.0.197 -> 1.0.198" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.4.18" -[[audits.google.audits.serde]] -who = "danakj " +[[audits.bytecode-alliance.audits.textwrap]] +who = "Chris Fallin " criteria = "safe-to-deploy" -delta = "1.0.198 -> 1.0.201" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.11.0" +notes = "No unsafe code." -[[audits.google.audits.serde]] -who = "Dustin J. Mitchell " +[[audits.bytecode-alliance.audits.thread_local]] +who = "Pat Hickey " criteria = "safe-to-deploy" -delta = "1.0.201 -> 1.0.202" -notes = "Trivial changes" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "1.1.4" +notes = "uses unsafe to implement thread local storage of objects" -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.tracing-log]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.202 -> 1.0.203" -notes = "s/doc_cfg/docsrs/ + tuple_impls/tuple_impl_body-related changes" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.1.3" +notes = """ +This is a standard adapter between the `log` ecosystem and the `tracing` +ecosystem. There's one `unsafe` block in this crate and it's well-scoped. +""" -[[audits.google.audits.serde]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.tracing-log]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.203 -> 1.0.204" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.1.3 -> 0.2.0" +notes = "Nothing out of the ordinary, a typical major version update and nothing awry." -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.try-lock]] +who = "Pat Hickey " criteria = "safe-to-deploy" -delta = "1.0.204 -> 1.0.207" -notes = "The small change in `src/private/ser.rs` should have no impact on `ub-risk-2`." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.2.4" +notes = "Implements a concurrency primitive with atomics, and is not obviously incorrect" -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.unarray]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.207 -> 1.0.209" +version = "0.1.4" notes = """ -The delta carries fairly small changes in `src/private/de.rs` and -`src/private/ser.rs` (see https://crrev.com/c/5812194/2..5). AFAICT the -delta has no impact on the `unsafe`, `from_utf8_unchecked`-related parts -of the crate (in `src/de/format.rs` and `src/ser/impls.rs`). +Crate is sound, albeit leaky, and not actively malicious. Probably not the best +crate to use in practice but it's suitable for testing dependencies. """ -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.serde]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.vcpkg]] +who = "Pat Hickey " criteria = "safe-to-deploy" -delta = "1.0.209 -> 1.0.210" -notes = "Almost no new code - just feature rearrangement" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.2.15" +notes = "no build.rs, no macros, no unsafe. It reads the filesystem and makes copies of DLLs into OUT_DIR." -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.want]] +who = "Pat Hickey " criteria = "safe-to-deploy" -version = "1.0.197" -notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.3.0" -[[audits.google.audits.serde_derive]] -who = "danakj " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.197 -> 1.0.201" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.236.0 -> 0.237.0" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.serde_derive]] -who = "Dustin J. Mitchell " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.201 -> 1.0.202" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.237.0 -> 0.238.1" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.202 -> 1.0.203" -notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.238.1 -> 0.239.0" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.serde_derive]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.203 -> 1.0.204" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.239.0 -> 0.240.0" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.204 -> 1.0.207" -notes = 'Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.240.0 -> 0.241.2" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.207 -> 1.0.209" -notes = ''' -There are no code changes in this delta - see https://crrev.com/c/5812194/2..5 - -I've neverthless also grepped for `-i cipher`, `-i crypto`, `\bfs\b`, -`\bnet\b`, and `\bunsafe\b`. There were no hits. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.241.2 -> 0.242.0" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.serde_derive]] -who = "Adrian Taylor " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -delta = "1.0.209 -> 1.0.210" -notes = "Almost no new code - just feature rearrangement" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +delta = "0.242.0 -> 0.243.0" +notes = "The Bytecode Alliance is the author of this crate" -[[audits.google.audits.static_assertions]] -who = "Lukasz Anforowicz " +[[audits.bytecode-alliance.audits.wasm-metadata]] +who = "Alex Crichton " criteria = "safe-to-deploy" -version = "1.1.0" -notes = """ -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` -and there were no hits except for one `unsafe`. +delta = "0.243.0 -> 0.244.0" +notes = "The Bytecode Alliance is the author of this crate" -The lambda where `unsafe` is used is never invoked (e.g. the `unsafe` code -never runs) and is only introduced for some compile-time checks. Additional -unsafe review comments can be found in https://crrev.com/c/5353376. +[[audits.bytecode-alliance.audits.zeroize]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.8.1 -> 1.8.2" -This crate has been added to Chromium in https://crrev.com/c/3736562. The CL -description contains a link to a document with an additional security review. -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.embark-studios.audits.derive-new]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.5.9" +notes = "Proc macro. No unsafe usage or ambient capabilities" -[[audits.google.audits.strsim]] -who = "danakj@chromium.org" +[[audits.embark-studios.audits.ident_case]] +who = "Johan Andersson " criteria = "safe-to-deploy" -version = "0.10.0" -notes = """ -Reviewed in https://crrev.com/c/5171063 +version = "1.0.1" +notes = "No unsafe usage or ambient capabilities" -Previously reviewed during security review and the audit is grandparented in. -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.embark-studios.audits.idna]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.4.0" +notes = "No unsafe usage or ambient capabilities" -[[audits.google.audits.tinytemplate]] -who = "Ying Hsu " -criteria = "safe-to-run" -version = "1.2.1" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.embark-studios.audits.schemars]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.8.12" +notes = "No unsafe usage (forbidden) or ambient capabilities" -[[audits.google.audits.tinyvec]] -who = "Lukasz Anforowicz " +[[audits.embark-studios.audits.similar]] +who = "Johan Andersson " criteria = "safe-to-deploy" -version = "1.6.0" -notes = """ -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` -and there were no hits except for some \"unsafe\" appearing in comments: +version = "2.2.1" +notes = "No unsafe usage or ambient capabilities" -``` -src/arrayvec.rs: // Note: This shouldn't use A::CAPACITY, because unsafe code can't rely on -src/lib.rs://! All of this is done with no `unsafe` code within the crate. Technically the -src/lib.rs://! `Vec` type from the standard library uses `unsafe` internally, but *this -src/lib.rs://! crate* introduces no new `unsafe` code into your project. -src/array.rs:/// Just a reminder: this trait is 100% safe, which means that `unsafe` code -``` +[[audits.embark-studios.audits.tap]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.1" +notes = "No unsafe usage or ambient capabilities" -This crate has been added to Chromium in -https://source.chromium.org/chromium/chromium/src/+/24773c33e1b7a1b5069b9399fd034375995f290b -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.embark-studios.audits.thiserror]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.40" +notes = "Wrapper over implementation crate, found no unsafe or ambient capabilities used" -[[audits.google.audits.tinyvec]] -who = "Adrian Taylor " +[[audits.embark-studios.audits.thiserror-impl]] +who = "Johan Andersson " criteria = "safe-to-deploy" -delta = "1.6.0 -> 1.6.1" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "1.0.40" +notes = "Found no unsafe or ambient capabilities used" -[[audits.google.audits.tinyvec]] -who = "Adrian Taylor " +[[audits.embark-studios.audits.uname]] +who = "Johan Andersson " criteria = "safe-to-deploy" -delta = "1.6.1 -> 1.7.0" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.1.1" +notes = "Inspected it and is tiny crate wrapping libc function with some unsafe usage for string handling" -[[audits.google.audits.tinyvec]] -who = "Dustin J. Mitchell " +[[audits.embark-studios.audits.utf8parse]] +who = "Johan Andersson " criteria = "safe-to-deploy" -delta = "1.7.0 -> 1.8.0" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +version = "0.2.1" +notes = "Single unsafe usage that looks sound, no ambient capabilities" -[[audits.google.audits.tinyvec_macros]] -who = "George Burgess IV " +[[audits.embark-studios.audits.valuable]] +who = "Johan Andersson " criteria = "safe-to-deploy" version = "0.1.0" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +notes = "No unsafe usage or ambient capabilities, sane build script" -[[audits.google.audits.tokio-stream]] -who = "David Koloski " +[[audits.embark-studios.audits.vec_map]] +who = "Johan Andersson " criteria = "safe-to-deploy" -version = "0.1.11" -notes = "Reviewed on https://fxrev.dev/804724" -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +version = "0.8.2" +notes = "No unsafe usage or ambient capabilities" -[[audits.google.audits.tokio-stream]] -who = "David Koloski " -criteria = "safe-to-deploy" -delta = "0.1.11 -> 0.1.14" -notes = "Reviewed on https://fxrev.dev/907732." -aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +[audits.fermyon.audits] -[[audits.google.audits.unicode-ident]] +[[audits.google.audits.arrayvec]] who = "Lukasz Anforowicz " criteria = "safe-to-deploy" -version = "1.0.12" +version = "0.7.6" notes = ''' -I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'` and there were +no hits, except for some `net` usage in tests. -All two functions from the public API of this crate use `unsafe` to avoid bound -checks for an array access. Cross-module analysis shows that the offsets can -be statically proven to be within array bounds. More details can be found in -the unsafe review CL at https://crrev.com/c/5350386. - -This crate has been added to Chromium in https://crrev.com/c/3891618. +The crate has quite a few bits of `unsafe` Rust. The audit comments can be +found in https://chromium-review.googlesource.com/c/chromium/src/+/6187726/2 ''' aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.unicode-xid]] -who = "George Burgess IV " +[[audits.google.audits.autocfg]] +who = "Manish Goregaokar " criteria = "safe-to-deploy" -version = "0.2.4" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +version = "1.4.0" +notes = "Contains no unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.version_check]] -who = "George Burgess IV " +[[audits.google.audits.base64]] +who = "amarjotgill " criteria = "safe-to-deploy" -version = "0.9.4" +version = "0.22.1" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.3.2" +notes = """ +Security review of earlier versions of the crate can be found at +(Google-internal, sorry): go/image-crate-chromium-security-review + +The crate exposes a function marked as `unsafe`, but doesn't use any +`unsafe` blocks (except for tests of the single `unsafe` function). I +think this justifies marking this crate as `ub-risk-1`. + +Additional review comments can be found at https://crrev.com/c/4723145/31 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.byteorder]] +who = "danakj " +criteria = "safe-to-deploy" +version = "1.5.0" +notes = "Unsafe review in https://crrev.com/c/5838022" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.cast]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.3.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium-io]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.ciborium-ll]] +who = "Daniel Verkamp " +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.core-foundation-sys]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.8.7" +notes = "OSX system APIs" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.displaydoc]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.2.5" +notes = "No unsafe code" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.either]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.13.0" +notes = "Unsafe code pertaining to wrapping Pin APIs. Mostly passes invariants down." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.either]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.13.0 -> 1.14.0" +notes = """ +Inheriting ub-risk-1 from the baseline review of 1.13.0. While the delta has some diffs in unsafe code, they are either: +- migrating code to use helper macros +- migrating match patterns to take advantage of default bindings mode from RFC 2005 +Either way, the result is code that does exactly the same thing and does not change the risk of UB. + +See https://crrev.com/c/6323164 for more audit details. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.either]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.14.0 -> 1.15.0" +notes = 'The delta in `lib.rs` only tweaks doc comments and `#[cfg(feature = "std")]`.' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.equivalent]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.equivalent]] +who = "Jonathan Hao " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.0.2" +notes = "No changes to any .rs files or Rust code." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.foldhash]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.4" +notes = "No changes to safety-relevant code" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.foldhash]] +who = "Chris Palmer " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.5" +notes = "No new `unsafe`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.glob]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.3.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.glob]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.2" +notes = "Still no unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.heck]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "0.4.1" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits. + +`heck` (version `0.3.3`) has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.httpdate]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.3" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.itertools]] +who = "ChromeOS" +criteria = "safe-to-run" +version = "0.10.5" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.lazy_static]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = ''' +I grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits. + +There are two places where `unsafe` is used. Unsafe review notes can be found +in https://crrev.com/c/5347418. + +This crate has been added to Chromium in https://crrev.com/c/3321895. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.lazy_static]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +notes = "Unsafe review notes: https://crrev.com/c/5650836" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.litemap]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.7.4" +notes = "Contains no unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.litemap]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "0.7.4 -> 0.7.5" +notes = "Delta implements the entry API but doesn't add or change any unsafe code." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.log]] +who = "danakj " +criteria = "safe-to-deploy" +version = "0.4.22" +notes = """ +Unsafe review in https://docs.google.com/document/d/1IXQbD1GhTRqNHIGxq6yy7qHqxeO4CwN5noMFXnqyDIM/edit?usp=sharing + +Unsafety is generally very well-documented, with one exception, which we +describe in the review doc. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.log]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "0.4.22 -> 0.4.25" +notes = "No impact on `unsafe` usage in `lib.rs`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.log]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "0.4.25 -> 0.4.26" +notes = "Only trivial code and documentation changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.nom]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "7.1.3" +notes = """ +Reviewed in https://chromium-review.googlesource.com/c/chromium/src/+/5046153 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.num-integer]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.1.46" +notes = "Contains no unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.number_prefix]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.4.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro-error-attr]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.4" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" -[[audits.google.audits.void]] -who = "George Burgess IV " +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.78" +notes = """ +Grepped for "crypt", "cipher", "fs", "net" - there were no hits +(except for a benign "fs" hit in a doc comment) + +Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.78 -> 1.0.79" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.79 -> 1.0.80" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.80 -> 1.0.81" +notes = "Comment changes only" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.81 -> 1.0.82" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.82 -> 1.0.83" +notes = "Substantive change is replacing String with Box, saving memory." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.83 -> 1.0.84" +notes = "Only doc comment changes in `src/lib.rs`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +delta = "1.0.84 -> 1.0.85" +notes = "Test-only changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.85 -> 1.0.86" +notes = """ +Comment-only changes in `build.rs`. +Reordering of `Cargo.toml` entries. +Just bumping up the version number in `lib.rs`. +Config-related changes in `test_size.rs`. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.86 -> 1.0.87" +notes = "No new unsafe interactions." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Liza Burakova Qualifiers::Unsafe, + ``` + +* Using `std::fs` in `build/build.rs` to write `${OUT_DIR}/version.expr` + which is later read back via `include!` used in `src/lib.rs`. + +Version `1.0.6` of this crate has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.14 -> 1.0.15" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.15 -> 1.0.16" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.16 -> 1.0.17" +notes = "Just updates windows compat" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Liza Burakova " +criteria = "safe-to-deploy" +delta = "1.0.17 -> 1.0.18" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.18 -> 1.0.19" +notes = "No unsafe, just doc changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.0.19 -> 1.0.20" +notes = "Only minor updates to documentation and the mock today used for testing." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. + +There were some hits for `net`, but they were related to serialization and +not actually opening any connections or anything like that. + +There were 2 hits of `unsafe` when grepping: +* In `fn as_str` in `impl Buf` +* In `fn serialize` in `impl Serialize for net::Ipv4Addr` + +Unsafe review comments can be found in https://crrev.com/c/5350573/2 (this +review also covered `serde_json_lenient`). + +Version 1.0.130 of the crate has been added to Chromium in +https://crrev.com/c/3265545. The CL description contains a link to a +(Google-internal, sorry) document with a mini security review. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.197 -> 1.0.198" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.198 -> 1.0.201" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.201 -> 1.0.202" +notes = "Trivial changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.202 -> 1.0.203" +notes = "s/doc_cfg/docsrs/ + tuple_impls/tuple_impl_body-related changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.203 -> 1.0.204" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.204 -> 1.0.207" +notes = "The small change in `src/private/ser.rs` should have no impact on `ub-risk-2`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.207 -> 1.0.209" +notes = """ +The delta carries fairly small changes in `src/private/de.rs` and +`src/private/ser.rs` (see https://crrev.com/c/5812194/2..5). AFAICT the +delta has no impact on the `unsafe`, `from_utf8_unchecked`-related parts +of the crate (in `src/de/format.rs` and `src/ser/impls.rs`). +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.209 -> 1.0.210" +notes = "Almost no new code - just feature rearrangement" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Liza Burakova " +criteria = "safe-to-deploy" +delta = "1.0.210 -> 1.0.213" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.213 -> 1.0.214" +notes = "No unsafe, no crypto" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.214 -> 1.0.215" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.215 -> 1.0.216" +notes = "The delta makes minor changes in `build.rs` - switching to the `?` syntax sugar." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.216 -> 1.0.217" +notes = "Minimal changes, nothing unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.0.217 -> 1.0.218" +notes = "No changes outside comments and documentation." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.218 -> 1.0.219" +notes = "Just allowing `clippy::elidable_lifetime_names`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.197 -> 1.0.201" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.201 -> 1.0.202" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.202 -> 1.0.203" +notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.203 -> 1.0.204" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.204 -> 1.0.207" +notes = 'Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.207 -> 1.0.209" +notes = ''' +There are no code changes in this delta - see https://crrev.com/c/5812194/2..5 + +I've neverthless also grepped for `-i cipher`, `-i crypto`, `\bfs\b`, +`\bnet\b`, and `\bunsafe\b`. There were no hits. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.209 -> 1.0.210" +notes = "Almost no new code - just feature rearrangement" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Liza Burakova " +criteria = "safe-to-deploy" +delta = "1.0.210 -> 1.0.213" +notes = "Grepped for 'unsafe', 'crypt', 'cipher', 'fs', 'net' - there were no hits" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.213 -> 1.0.214" +notes = "No changes to unsafe, no crypto" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.214 -> 1.0.215" +notes = "Minor changes should not impact UB risk" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.215 -> 1.0.216" +notes = "The delta adds `#[automatically_derived]` in a few places. Still no `unsafe`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.216 -> 1.0.217" +notes = "No changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.0.217 -> 1.0.218" +notes = "No changes outside comments and documentation." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.218 -> 1.0.219" +notes = "Minor changes (clippy tweaks, using `mem::take` instead of `mem::replace`)." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.sha1]] +who = "David Koloski " +criteria = "safe-to-deploy" +version = "0.10.5" +notes = "Reviewed on https://fxrev.dev/712371." +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.smallvec]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.13.2" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.static_assertions]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'` +and there were no hits except for one `unsafe`. + +The lambda where `unsafe` is used is never invoked (e.g. the `unsafe` code +never runs) and is only introduced for some compile-time checks. Additional +unsafe review comments can be found in https://crrev.com/c/5353376. + +This crate has been added to Chromium in https://crrev.com/c/3736562. The CL +description contains a link to a document with an additional security review. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strsim]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.10.0" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strum]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.25.0" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strum_macros]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.25.3" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.synstructure]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.13.1" +notes = "Exposes unsafe codegen APIs but does not itself contain unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.tinytemplate]] +who = "Ying Hsu " +criteria = "safe-to-run" +version = "1.2.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.tinyvec_macros]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.version_check]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.9.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.void]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.write16]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "No unsafe code." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.yoke]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.7.5" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.yoke-derive]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.7.5" +notes = "Custom derive implementing the `Yokeable` trait. Generally generates simple code that asserts covariance." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.isrg.audits.base64]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.21.0 -> 0.21.1" + +[[audits.isrg.audits.base64]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.21.1 -> 0.21.2" + +[[audits.isrg.audits.base64]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.21.2 -> 0.21.3" + +[[audits.isrg.audits.block-buffer]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.9.0" + +[[audits.isrg.audits.cfg-if]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.0.1" + +[[audits.isrg.audits.cfg-if]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.0.3" + +[[audits.isrg.audits.cfg-if]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.0.3 -> 1.0.4" + +[[audits.isrg.audits.cpufeatures]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.12 -> 0.2.13" + +[[audits.isrg.audits.cpufeatures]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.14 -> 0.2.15" + +[[audits.isrg.audits.cpufeatures]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.15 -> 0.2.16" + +[[audits.isrg.audits.cpufeatures]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.16 -> 0.2.17" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.1.17" +notes = """ +This crate does not contain any unsafe code, and does not use any items from +the standard library or other crates, aside from operations backed by +`std::ops`. All paths with array indexing use integer literals for indexes, so +there are no panics due to indexes out of bounds (as rustc would catch an +out-of-bounds literal index). I did not check whether arithmetic overflows +could cause a panic, and I am relying on the Coq code having satisfied the +necessary preconditions to ensure panics due to overflows are unreachable. +""" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.1.17 -> 0.1.18" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.18 -> 0.1.19" +notes = """ +This release renames many items and adds a new module. The code in the new +module is entirely composed of arithmetic and array accesses. +""" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.19 -> 0.1.20" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.1.20 -> 0.2.0" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" + +[[audits.isrg.audits.fiat-crypto]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" +notes = "No changes to `unsafe` code, or any functional changes that I can detect at all." + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.2 -> 0.2.4" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.5 -> 0.2.6" + +[[audits.isrg.audits.fiat-crypto]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.2.7" + +[[audits.isrg.audits.fiat-crypto]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.7 -> 0.2.8" + +[[audits.isrg.audits.fiat-crypto]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.2.8 -> 0.2.9" +notes = "No changes to Rust code between 0.2.8 and 0.2.9" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.4" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.3.4 -> 0.4.0" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.4.1" + +[[audits.isrg.audits.once_cell]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.21.1 -> 1.21.3" +notes = "The unsafe code has moved from `compare_exchange` to a new `init` function, which makes it easier to reason about." + +[[audits.isrg.audits.once_cell]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.21.3 -> 1.21.4" +notes = "The addition is a safe while loop around prior behavior. I don't see any way for that to become malicious." + +[[audits.isrg.audits.opaque-debug]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.3.0" + +[[audits.isrg.audits.rand]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.8.5 -> 0.9.1" + +[[audits.isrg.audits.rand]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "0.9.1 -> 0.9.2" + +[[audits.isrg.audits.rand_chacha]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.9.0" + +[[audits.isrg.audits.rand_core]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.6.4 -> 0.9.3" + +[[audits.isrg.audits.rand_core]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "0.9.3 -> 0.9.5" + +[[audits.isrg.audits.rayon-core]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +version = "1.12.1" + +[[audits.isrg.audits.rayon-core]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.12.1 -> 1.13.0" + +[[audits.isrg.audits.serde]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.219 -> 1.0.224" + +[[audits.isrg.audits.serde]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.224 -> 1.0.225" + +[[audits.isrg.audits.serde]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "1.0.225 -> 1.0.226" + +[[audits.isrg.audits.serde_core]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +version = "1.0.224" + +[[audits.isrg.audits.serde_core]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.224 -> 1.0.225" + +[[audits.isrg.audits.serde_core]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "1.0.225 -> 1.0.226" + +[[audits.isrg.audits.serde_derive]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.219 -> 1.0.224" + +[[audits.isrg.audits.serde_derive]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.224 -> 1.0.225" + +[[audits.isrg.audits.serde_derive]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "1.0.225 -> 1.0.226" + +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.10.2" + +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.10.8 -> 0.10.9" + +[[audits.isrg.audits.subtle]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "2.5.0 -> 2.6.1" + +[[audits.isrg.audits.thiserror]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.0.40 -> 1.0.43" + +[[audits.isrg.audits.thiserror]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "2.0.17 -> 2.0.18" + +[[audits.isrg.audits.thiserror-impl]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.0.40 -> 1.0.43" + +[[audits.isrg.audits.thiserror-impl]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "2.0.17 -> 2.0.18" + +[[audits.isrg.audits.universal-hash]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.4.1" + +[[audits.isrg.audits.universal-hash]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.1" + +[[audits.mozilla.wildcard-audits.cexpr]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +user-id = 3788 # Emilio Cobos Álvarez (emilio) +start = "2021-06-21" +end = "2024-04-21" +notes = "No unsafe code, rather straight-forward parser." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.encoding_rs]] +who = "Henri Sivonen " +criteria = "safe-to-deploy" +user-id = 4484 # Henri Sivonen (hsivonen) +start = "2019-02-26" +end = "2025-10-23" +notes = "I, Henri Sivonen, wrote encoding_rs for Gecko and have reviewed contributions by others. There are two caveats to the certification: 1) The crate does things that are documented to be UB but that do not appear to actually be UB due to integer types differing from the general rule; https://github.com/hsivonen/encoding_rs/issues/79 . 2) It would be prudent to re-review the code that reinterprets buffers of integers as SIMD vectors; see https://github.com/hsivonen/encoding_rs/issues/87 ." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-segmentation]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-05-15" +end = "2027-04-23" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-width]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-12-05" +end = "2026-02-01" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-xid]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-07-25" +end = "2027-04-23" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.utf8_iter]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +user-id = 4484 # Henri Sivonen (hsivonen) +start = "2022-04-19" +end = "2024-06-16" +notes = "Maintained by Henri Sivonen who works at Mozilla." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.adler2]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "2.0.0 -> 2.0.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.allocator-api2]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +version = "0.2.18" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.allocator-api2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.2.20 -> 0.2.21" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +version = "0.1.2" +notes = "I wrote this crate, reviewed by jimb. It is mostly a Rust port of some C++ code we already ship." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.5" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.arraydeque]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +version = "0.5.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +version = "0.59.2" +notes = "I'm the primary author and maintainer of the crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +delta = "0.59.2 -> 0.63.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.63.0 -> 0.64.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.64.0 -> 0.66.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.66.1 -> 0.68.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Andreas Pehrson " +criteria = "safe-to-deploy" +delta = "0.68.1 -> 0.69.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.69.1 -> 0.69.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +delta = "0.69.2 -> 0.69.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bindgen]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +delta = "0.69.4 -> 0.72.0" +notes = "I'm the primary maintainer of this crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.5.2" +notes = "Another crate I own via contain-rs that is ancient and maintenance mode, no known issues." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.2 -> 0.5.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.5.3 -> 0.6.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Jim Blandy " +criteria = "safe-to-deploy" +delta = "0.6.0 -> 0.8.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-vec]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.6.3" +notes = "Another crate I own via contain-rs that is ancient and in maintenance mode but otherwise perfectly fine." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-vec]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.6.3 -> 0.7.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-vec]] +who = "Jim Blandy " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.8.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "1.3.2 -> 2.0.2" +notes = "Removal of some unsafe code/methods. No changes to externals, just some refactoring (mostly internal)." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +delta = "2.0.2 -> 2.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "2.2.1 -> 2.3.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "2.3.3 -> 2.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "2.4.0 -> 2.4.1" +notes = "Only allowing new clippy lints" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = [ + "Teodor Tanasoaia ", + "Erich Gubler ", +] +criteria = "safe-to-deploy" +delta = "2.6.0 -> 2.7.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Benjamin VanderSloot " +criteria = "safe-to-deploy" +delta = "2.9.4 -> 2.10.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.block-buffer]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.block2]] +who = "Andy Leiserson " +criteria = "safe-to-deploy" +version = "0.6.2" +notes = "Contains unsafe code to interoperate with the ObjC runtime." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.cfg_aliases]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.2.1" +notes = "Very minor changes." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-channel]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.5.13 -> 0.5.14" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-channel]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.5.14 -> 0.5.15" +notes = "Fixes a regression from an earlier version which could lead to a double free" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.crossbeam-utils]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.8.20 -> 0.8.21" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crunchy]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +version = "0.2.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.13.4 -> 0.14.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.14.2 -> 0.14.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.14.3 -> 0.20.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling]] +who = "Ben Dean-Kawamura " +criteria = "safe-to-deploy" +delta = "0.20.1 -> 0.20.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling_core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.13.4 -> 0.14.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling_core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.14.2 -> 0.14.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling_core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.14.3 -> 0.20.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.darling_core]] +who = "Ben Dean-Kawamura " +criteria = "safe-to-deploy" +delta = "0.20.1 -> 0.20.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.debugid]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +version = "0.8.0" +notes = "This crates was written by Sentry and I've fully audited it as Firefox crash reporting machinery relies on it." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.3.11" +notes = """ +This crate contains a decent bit of `unsafe` code, however all internal +unsafety is verified with copious assertions (many are compile-time), and +otherwise the unsafety is documented and left to the caller to verify. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.3.11 -> 0.4.0" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.5.8" +notes = "New unsafe code is properly guarded" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.document-features]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +version = "0.2.8" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.document-features]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.2.8 -> 0.2.9" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.document-features]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.2.9 -> 0.2.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.document-features]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.2.10 -> 0.2.11" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.errno]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fnv]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.7" +notes = "Simple hasher implementation with no unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.foldhash]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.1.5 -> 0.2.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.form_urlencoded]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +version = "1.2.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.form_urlencoded]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.2.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.form_urlencoded]] +who = "edgul " +criteria = "safe-to-deploy" +delta = "1.2.1 -> 1.2.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.getrandom]] +who = "Chris Martin " +criteria = "safe-to-deploy" +delta = "0.2.15 -> 0.3.1" +notes = """ +I've looked over all unsafe code, and it appears to be safe, fully initializing the rng buffers. +In addition, I've checked Linux, Windows, Mac, and Android more thoroughly against API +documentation. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.getrandom]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.3" +notes = """ +Biggest non-trivial change is a new UEFI back-end, which looks reasonable to +the best of my ability: There's some trickiness on initialization but doesn't +look unsafe, at worse it leaks, and it might not if the relevant pointers are +static/non-owning. Other changes also look reasonable too: some tweaks to +inlining and a syscall-based linux back-end, whose relevant unsafe code looks +reasonable. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.gimli]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.30.0" +notes = """ +Unsafe code blocks are sound. Minimal dependencies used. No use of +side-effectful std functions. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.gimli]] +who = "Chris Martin " +criteria = "safe-to-deploy" +delta = "0.30.0 -> 0.29.0" +notes = "No unsafe code, mostly algorithms and parsing. Very unlikely to cause security issues." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +version = "0.12.3" +notes = "This version is used in rust's libstd, so effectively we're already trusting it" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.15.2 -> 0.15.5" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.15.5 -> 0.16.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.16.0 -> 0.16.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.16.1 -> 0.17.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashlink]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.8.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashlink]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +delta = "0.8.1 -> 0.9.1" +notes = "New CursorMut struct and other relatively straight-forward changes." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashlink]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.9.1 -> 0.10.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hex]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "0.4.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.home]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "0.5.3" +notes = """ +Crate with straightforward code for determining the user's HOME directory. Only +unsafe code is used to invoke the Windows SHGetFolderPathW API to get the +profile directory when the USERPROFILE environment variable is unavailable. +""" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.home]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "0.5.3 -> 0.5.11" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_collections]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "1.2.0" +notes = "This crate is used by ICU4X for internal data structure. There is no fileaccess and network access. This uses unsafe block, but we confirm data is valid before." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_collections]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_collections]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_locid]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "1.2.0" +notes = "This has unsafe block to handle ascii string in utf-8 string. I've vetted the one instance of unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_locid]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_locid]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_locid_transform]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = "This crate doesn't contain network and file access. Although this has unsafe block, the reason is added in the comment block. I audited code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_locid_transform]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_normalizer]] +who = "Henri Sivonen " +criteria = "safe-to-deploy" +version = "1.5.0" +notes = "I, Henri Sivonen, am the principal author of this crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_properties]] +who = "Jonathan Kew " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = "This is used by ICU4X for character property lookup. The few (4) usages of unsafe have comments clarifying their safety." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_properties]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_provider]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "1.2.0" +notes = "Although this has unsafe block, this has a commnet why this is safety and I audited code. Also, this doesn't have file access and network access." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_provider]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_provider]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_provider_macros]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "1.2.0" +notes = "This crate is macros for ICU4X's data provider implementer. This has no unsafe code and uses no ambient capabilities." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_provider_macros]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.icu_provider_macros]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.idna]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.idna]] +who = "Henri Sivonen " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 1.0.2" +notes = "In the 0.5.0 to 1.0.2 delta, I, Henri Sivonen, rewrote the non-Punycode internals of the crate and made the changes to the Punycode code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.idna]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "1.0.2 -> 1.0.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.idna_adapter]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +version = "1.2.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.4.26 -> 0.4.29" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.15.0 -> 0.25.0" +notes = "Plenty of new bindings but also several important bug fixes (including buffer overflows). New unsafe sections are restricted to wrappers and are no more dangerous than calling the C functions." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.25.0 -> 0.25.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.25.1 -> 0.26.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.26.2 -> 0.27.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.27.1 -> 0.28.0" +notes = """ +Many new features and bugfixes. Obviously there's a lot of unsafe code calling +libc, but the usage looks correct. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.28.0 -> 0.29.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.29.0 -> 0.30.1" +notes = "Some new wrappers, support for minor platforms and lots of work around type safety that reduces the unsafe surafce." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-conv]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = """ +Very straightforward, simple crate. No dependencies, unsafe, extern, +side-effectful std functions, etc. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-conv]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.2.0" +notes = "Revision only removes code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.objc2-core-foundation]] +who = "Andy Leiserson " +criteria = "safe-to-deploy" +version = "0.3.2" +notes = """ +Contains substantial unsafe code, as is typical for FFI. + +The (non-published) `header-translator` crate that produces generated bindings +in this crate was also reviewed, in lieu of a full review of the generated +bindings. + +Users of this crate should be aware of the information in +https://github.com/madsmtm/objc2/blob/main/crates/objc2/src/topics/frameworks_soundness.md. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.objc2-encode]] +who = "Andy Leiserson " +criteria = "safe-to-deploy" +version = "4.1.0" +notes = "Support library for objc2 with no unsafe code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.objc2-foundation]] +who = "Andy Leiserson " +criteria = "safe-to-deploy" +version = "0.3.2" +notes = """ +Contains substantial unsafe code, as is typical for FFI. + +The (non-published) `header-translator` crate that produces generated bindings +in this crate was also reviewed, in lieu of a full review of the generated +bindings. + +Users of this crate should be aware of the information in +https://github.com/madsmtm/objc2/blob/main/crates/objc2/src/topics/frameworks_soundness.md. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.objc2-io-surface]] +who = "Andy Leiserson " +criteria = "safe-to-deploy" +version = "0.3.2" +notes = """ +Contains substantial unsafe code, as is typical for FFI. + +The (non-published) `header-translator` crate that produces generated bindings +in this crate was also reviewed, in lieu of a full review of the generated +bindings. + +Users of this crate should be aware of the information in +https://github.com/madsmtm/objc2/blob/main/crates/objc2/src/topics/frameworks_soundness.md. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.objc2-quartz-core]] +who = "Andy Leiserson " +criteria = "safe-to-deploy" +version = "0.3.2" +notes = """ +Contains substantial unsafe code, as is typical for FFI. + +The (non-published) `header-translator` crate that produces generated bindings +in this crate was also reviewed, in lieu of a full review of the generated +bindings. + +Users of this crate should be aware of the information in +https://github.com/madsmtm/objc2/blob/main/crates/objc2/src/topics/frameworks_soundness.md. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.once_cell]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.20.2 -> 1.20.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.once_cell]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.20.3 -> 1.21.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.oorandom]] +who = "Jan-Erik Rediger " +criteria = "safe-to-run" +version = "11.1.5" +notes = "Small random number generator, explicitly not cryptographically secure, no use of unsafe code, no dependencies" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.option-ext]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "0.2.0" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.2.0 -> 2.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.3.0 -> 2.3.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "edgul " +criteria = "safe-to-deploy" +delta = "2.3.1 -> 2.3.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.powerfmt]] +who = "Alex Franchuk " criteria = "safe-to-deploy" -version = "1.0.2" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +version = "0.2.0" +notes = """ +A tiny bit of unsafe code to implement functionality that isn't in stable rust +yet, but it's all valid. Otherwise it's a pretty simple crate. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.wildcard-audits.cexpr]] -who = "Emilio Cobos Álvarez " +[[audits.mozilla.audits.proc-macro-error-attr2]] +who = "Kagami Sascha Rosylight " criteria = "safe-to-deploy" -user-id = 3788 # Emilio Cobos Álvarez (emilio) -start = "2021-06-21" -end = "2024-04-21" -notes = "No unsafe code, rather straight-forward parser." +version = "2.0.0" +notes = "No unsafe block." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro-error2]] +who = "Kagami Sascha Rosylight " +criteria = "safe-to-deploy" +version = "2.0.1" +notes = "No unsafe block with a lovely `#![forbid(unsafe_code)]`." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro2]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.94 -> 1.0.106" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Inden " +criteria = "safe-to-deploy" +version = "0.5.4" +notes = "This is a small crate, providing safe wrappers around various low-level networking specific operating system features. Given that the Rust standard library does not provide safe wrappers for these low-level features, safe wrappers need to be build in the crate itself, i.e. `quinn-udp`, thus requiring `unsafe` code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "0.5.4 -> 0.5.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "0.5.6 -> 0.5.8" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "0.5.8 -> 0.5.9" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Leonard Inden " +criteria = "safe-to-deploy" +delta = "0.5.9 -> 0.5.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Leonard Inden " +criteria = "safe-to-deploy" +delta = "0.5.10 -> 0.5.11" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Leonard Inden " +criteria = "safe-to-deploy" +delta = "0.5.11 -> 0.5.12" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quinn-udp]] +who = "Max Leonard Inden " +criteria = "safe-to-deploy" +delta = "0.5.12 -> 0.5.13" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.40 -> 1.0.45" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.rand]] +who = "Henrik Skupin " +criteria = "safe-to-deploy" +delta = "0.8.5 -> 0.8.6" +notes = """ +Fixes RUSTSEC-2026-0097 by removing `log` dependency. Removes `simd_support` +feature. No new dependencies or unsafe code. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.regex]] +who = "Benjamin VanderSloot " +criteria = "safe-to-deploy" +delta = "1.11.1 -> 1.12.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.regex-automata]] +who = "edgul " +criteria = "safe-to-deploy" +delta = "0.4.7 -> 0.4.9" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.regex-automata]] +who = "Benjamin VanderSloot " +criteria = "safe-to-deploy" +delta = "0.4.9 -> 0.4.14" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.wildcard-audits.core-foundation]] +[[audits.mozilla.audits.rustc-hash]] who = "Bobby Holley " criteria = "safe-to-deploy" -user-id = 5946 # Jeff Muizelaar (jrmuizel) -start = "2019-03-29" -end = "2023-05-04" -renew = false -notes = "I've reviewed every source contribution that was neither authored nor reviewed by Mozilla." +version = "1.1.0" +notes = "Straightforward crate with no unsafe code, does what it says on the tin." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.wildcard-audits.encoding_rs]] -who = "Henri Sivonen " +[[audits.mozilla.audits.rustc_version]] +who = "Nika Layzell " criteria = "safe-to-deploy" -user-id = 4484 # Henri Sivonen (hsivonen) -start = "2019-02-26" -end = "2024-08-28" -notes = "I, Henri Sivonen, wrote encoding_rs for Gecko and have reviewed contributions by others. There are two caveats to the certification: 1) The crate does things that are documented to be UB but that do not appear to actually be UB due to integer types differing from the general rule; https://github.com/hsivonen/encoding_rs/issues/79 . 2) It would be prudent to re-review the code that reinterprets buffers of integers as SIMD vectors; see https://github.com/hsivonen/encoding_rs/issues/87 ." +version = "0.4.0" +notes = """ +Use of powerful capabilities is limited to invoking `rustc -vV` to get version +information for parsing version information. +""" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.0.226 -> 1.0.227" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.wildcard-audits.unicode-normalization]] -who = "Manish Goregaokar " +[[audits.mozilla.audits.serde]] +who = "Jan-Erik Rediger " criteria = "safe-to-deploy" -user-id = 1139 # Manish Goregaokar (Manishearth) -start = "2019-11-06" -end = "2024-05-03" -notes = "All code written or reviewed by Manish" +delta = "1.0.227 -> 1.0.228" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_core]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.0.226 -> 1.0.227" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.wildcard-audits.unicode-segmentation]] -who = "Manish Goregaokar " +[[audits.mozilla.audits.serde_core]] +who = "Jan-Erik Rediger " criteria = "safe-to-deploy" -user-id = 1139 # Manish Goregaokar (Manishearth) -start = "2019-05-15" -end = "2024-05-03" -notes = "All code written or reviewed by Manish" +delta = "1.0.227 -> 1.0.228" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_derive]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.0.226 -> 1.0.227" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_derive]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.227 -> 1.0.228" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Jeff Muizelaar " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.8" +notes = """ +The bulk of this is https://github.com/RustCrypto/hashes/pull/490 which adds aarch64 support along with another PR adding longson. +I didn't check the implementation thoroughly but there wasn't anything obviously nefarious. 0.10.8 has been out for more than a year +which suggests no one else has found anything either. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sharded-slab]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.7" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.shlex]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.similar]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "2.2.1 -> 2.7.0" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.smallvec]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.14.0 -> 1.15.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strsim]] +who = "Ben Dean-Kawamura " +criteria = "safe-to-deploy" +delta = "0.10.0 -> 0.11.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.0 -> 0.26.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.26.3 -> 0.27.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum_macros]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.3 -> 0.26.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum_macros]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.26.4 -> 0.27.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.subtle]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "2.5.0" +notes = "The goal is to provide some constant-time correctness for cryptographic implementations. The approach is reasonable, it is known to be insufficient but this is pointed out in the documentation." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.synstructure]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "0.12.6" +notes = """ +I am the primary author of the `synstructure` crate, and its current +maintainer. The one use of `unsafe` is unnecessary, but documented and +harmless. It will be removed in the next version. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.synstructure]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "0.13.1 -> 0.13.2" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.thiserror]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.43 -> 1.0.69" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.thiserror-impl]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.43 -> 1.0.69" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.time]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.3.36 -> 0.3.41" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.time]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.3.41 -> 0.3.47" +notes = "New unsafe code seems properly guarded" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.1.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.4" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.8" +notes = "No unsafe code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.2.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.2.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.2.10 -> 0.2.18" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.2.18 -> 0.2.22" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.2.22 -> 0.2.27" +notes = "Refactors some unsafe code, nothing new" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +version = "0.7.0" +notes = "One of original auther was Zibi Braniecki who worked at Mozilla and maintained by ICU4X developers (Google and Mozilla). I've vetted the one instance of unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.7.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "0.7.1 -> 0.7.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinystr]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "0.7.4 -> 0.7.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +version = "2.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.4.0 -> 2.4.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.allocator-api2]] -who = "Nicolas Silva " +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " criteria = "safe-to-deploy" -version = "0.2.18" +delta = "2.4.1 -> 2.5.0" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.android_system_properties]] -who = "Nicolas Silva " +[[audits.mozilla.audits.url]] +who = "Henri Sivonen " criteria = "safe-to-deploy" -version = "0.1.2" -notes = "I wrote this crate, reviewed by jimb. It is mostly a Rust port of some C++ code we already ship." +delta = "2.5.0 -> 2.5.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.android_system_properties]] -who = "Mike Hommey " +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " criteria = "safe-to-deploy" -delta = "0.1.2 -> 0.1.4" +delta = "2.5.1 -> 2.5.4" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.android_system_properties]] -who = "Mike Hommey " +[[audits.mozilla.audits.utf16_iter]] +who = "Henri Sivonen " criteria = "safe-to-deploy" -delta = "0.1.4 -> 0.1.5" +version = "1.0.5" +notes = "I, Henri Sivonen, wrote this crate." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Emilio Cobos Álvarez " +[[audits.mozilla.audits.utf8parse]] +who = "Nika Layzell " criteria = "safe-to-deploy" -version = "0.59.2" -notes = "I'm the primary author and maintainer of the crate." +delta = "0.2.1 -> 0.2.2" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.windows-link]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "A microsoft crate allowing unsafe calls to windows apis." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Emilio Cobos Álvarez " +[[audits.mozilla.audits.windows-link]] +who = "Erich Gubler " criteria = "safe-to-deploy" -delta = "0.59.2 -> 0.63.0" +delta = "0.1.1 -> 0.2.0" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Mike Hommey " +[[audits.mozilla.audits.writeable]] +who = "Makoto Kato " criteria = "safe-to-deploy" -delta = "0.63.0 -> 0.64.0" +version = "0.5.2" +notes = "writeable is a variation of fmt::Write with sink version. This uses `unsafe` block to handle potentially-invalid UTF-8 character. I've vetted the one instance of unsafe code." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Mike Hommey " +[[audits.mozilla.audits.writeable]] +who = "Makoto Kato " criteria = "safe-to-deploy" -delta = "0.64.0 -> 0.66.1" +delta = "0.5.2 -> 0.5.4" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Mike Hommey " +[[audits.mozilla.audits.writeable]] +who = "Makoto Kato " criteria = "safe-to-deploy" -delta = "0.66.1 -> 0.68.1" +delta = "0.5.4 -> 0.5.5" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Andreas Pehrson " +[[audits.mozilla.audits.zeroize]] +who = "Benjamin Beurdouche " criteria = "safe-to-deploy" -delta = "0.68.1 -> 0.69.1" +version = "1.8.1" +notes = """ +This code DOES contain unsafe code required to internally call volatiles +for deleting data. This is expected and documented behavior. +""" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Mike Hommey " +[[audits.mozilla.audits.zerovec]] +who = "Makoto Kato " criteria = "safe-to-deploy" -delta = "0.69.1 -> 0.69.2" +version = "0.9.4" +notes = "This crate is zero-copy data structure implmentation. Although this uses unsafe block in several code, it requires for zero-copy. And this has a comment in code why this uses unsafe and I audited code." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bindgen]] -who = "Emilio Cobos Álvarez " +[[audits.mozilla.audits.zerovec]] +who = "Makoto Kato " criteria = "safe-to-deploy" -delta = "0.69.2 -> 0.69.4" +delta = "0.9.4 -> 0.10.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bit-set]] -who = "Aria Beingessner " +[[audits.mozilla.audits.zerovec]] +who = "Makoto Kato " criteria = "safe-to-deploy" -version = "0.5.2" -notes = "Another crate I own via contain-rs that is ancient and maintenance mode, no known issues." +delta = "0.10.1 -> 0.10.2" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bit-set]] +[[audits.mozilla.audits.zerovec]] who = "Mike Hommey " criteria = "safe-to-deploy" -delta = "0.5.2 -> 0.5.3" +delta = "0.10.2 -> 0.10.4" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.bit-vec]] -who = "Aria Beingessner " +[[audits.mozilla.audits.zerovec-derive]] +who = "Makoto Kato " criteria = "safe-to-deploy" -version = "0.6.3" -notes = "Another crate I own via contain-rs that is ancient and in maintenance mode but otherwise perfectly fine." +version = "0.10.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.cfg_aliases]] -who = "Alex Franchuk " +[[audits.mozilla.audits.zerovec-derive]] +who = "Makoto Kato " criteria = "safe-to-deploy" -delta = "0.1.1 -> 0.2.1" -notes = "Very minor changes." +delta = "0.10.1 -> 0.10.2" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.core-foundation]] -who = "Teodor Tanasoaia " +[[audits.mozilla.audits.zerovec-derive]] +who = "Max Inden " criteria = "safe-to-deploy" -delta = "0.9.3 -> 0.9.4" -notes = "I've reviewed every source contribution that was neither authored nor reviewed by Mozilla." +delta = "0.10.2 -> 0.10.3" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.debugid]] -who = "Gabriele Svelto " +[[audits.zcash.audits.aho-corasick]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.8.0" -notes = "This crates was written by Sentry and I've fully audited it as Firefox crash reporting machinery relies on it." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "1.1.3 -> 1.1.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.deranged]] -who = "Alex Franchuk " +[[audits.zcash.audits.arrayref]] +who = "Daira-Emma Hopwood " criteria = "safe-to-deploy" -version = "0.3.11" +delta = "0.3.6 -> 0.3.8" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.arrayref]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.8 -> 0.3.9" +notes = "Changes to `unsafe` lines are to make some existing `unsafe fn`s `const`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.async-trait]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.88 -> 0.1.89" notes = """ -This crate contains a decent bit of `unsafe` code, however all internal -unsafety is verified with copious assertions (many are compile-time), and -otherwise the unsafety is documented and left to the caller to verify. +Changes to generated code are to make use of `syn::Block` quoting in several places +instead of directly quoting its statements. """ -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.document-features]] -who = "Erich Gubler " +[[audits.zcash.audits.autocfg]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.2.8" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "1.4.0 -> 1.5.0" +notes = "Filesystem change is to remove the generated LLVM IR output file after probing." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.fastrand]] -who = "Mike Hommey " +[[audits.zcash.audits.base64]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "1.9.0 -> 2.0.0" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.21.3 -> 0.21.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.fastrand]] -who = "Mike Hommey " +[[audits.zcash.audits.base64]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "2.0.1 -> 2.1.0" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.21.4 -> 0.21.5" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.fnv]] -who = "Bobby Holley " +[[audits.zcash.audits.base64]] +who = "Daira-Emma Hopwood " criteria = "safe-to-deploy" -version = "1.0.7" -notes = "Simple hasher implementation with no unsafe code." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.21.5 -> 0.21.7" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.form_urlencoded]] -who = "Valentin Gosu " +[[audits.zcash.audits.bindgen]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "1.2.0" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.69.4 -> 0.69.5" +notes = """ +Change to `unsafe` block is to switch from `clang_getSpellingLocation` to +`clang_getFileLocation`; I confirmed these have the same arguments. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.form_urlencoded]] -who = "Valentin Gosu " +[[audits.zcash.audits.bindgen]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "1.2.0 -> 1.2.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.72.0 -> 0.72.1" +notes = """ +Change to `unsafe` code is to narrow the scope of an `unsafe` block; no changes +to the `unsafe` function being called. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.hashbrown]] -who = "Mike Hommey " +[[audits.zcash.audits.block-buffer]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.12.3" -notes = "This version is used in rust's libstd, so effectively we're already trusting it" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.10.3 -> 0.10.4" +notes = "Adds panics to prevent a block size of zero from causing unsoundness." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.hex]] -who = "Simon Friedberger " +[[audits.zcash.audits.bounded-vec]] +who = "Kris Nuttycombe " criteria = "safe-to-deploy" -version = "0.4.3" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +version = "0.9.0" +notes = "Crate forbids unsafe code and uses no powerful imports. It consists primarily of safe constructors for newtype wrappers around `Vec`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.linked-hash-map]] -who = "Aria Beingessner " +[[audits.zcash.audits.console-api]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.5.4" -notes = "I own this crate (I am contain-rs) and 0.5.4 passes miri. This code is very old and used by lots of people, so I'm pretty confident in it, even though it's in maintenance-mode and missing some nice-to-have APIs." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.8.1 -> 0.9.0" +notes = "Only changes are to generated code as a result of dependency and protobuf updates." +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.linked-hash-map]] -who = "Alex Franchuk " +[[audits.zcash.audits.console-subscriber]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.5.4 -> 0.5.6" -notes = "New unsafe code has debug assertions and meets invariants. All other changes are formatting-related." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.4.1 -> 0.5.0" +notes = "Adds support for Vsock addresses. Usages of `tokio-vsock` for I/O look correct." +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.litrs]] -who = "Erich Gubler " +[[audits.zcash.audits.cpufeatures]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.4.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.2.13 -> 0.2.14" +notes = """ +New `unsafe` block is to call `sysctlbyname` to detect DIT on Apple ARM64, which +is done in the same way as existing target feature checks on that arch. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.log]] -who = "Mike Hommey " +[[audits.zcash.audits.crunchy]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.4.17" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.2.3 -> 0.2.4" +notes = """ +Build script change is to fix a bug where a path separator for an included file +was being selected by the target OS instead of the host OS. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.log]] -who = "Jan-Erik Rediger " +[[audits.zcash.audits.darling]] +who = "Schell Carl Scivally " criteria = "safe-to-deploy" -delta = "0.4.17 -> 0.4.18" -notes = "One dependency removed, others updated (which we don't rely on), some APIs (which we don't use) changed." -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" +delta = "0.20.10 -> 0.21.3" +notes = "Mostly added tests and documentation. The bulk of the changes were made to `darling_core`." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.log]] -who = "Kagami Sascha Rosylight " +[[audits.zcash.audits.darling_core]] +who = "Schell Carl Scivally " criteria = "safe-to-deploy" -delta = "0.4.18 -> 0.4.20" -notes = "Only cfg attribute and internal macro changes and module refactorings" -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" +delta = "0.20.10 -> 0.21.3" +notes = "No unsafe, just helpers for proc-macros." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.nix]] -who = "Gabriele Svelto " +[[audits.zcash.audits.darling_macro]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.15.0 -> 0.25.0" -notes = "Plenty of new bindings but also several important bug fixes (including buffer overflows). New unsafe sections are restricted to wrappers and are no more dangerous than calling the C functions." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.20.11 -> 0.21.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.nix]] -who = "Mike Hommey " +[[audits.zcash.audits.dirs]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.25.0 -> 0.25.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "5.0.1 -> 6.0.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.nix]] -who = "Mike Hommey " +[[audits.zcash.audits.dirs-sys]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.25.1 -> 0.26.2" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.4.1 -> 0.5.0" +notes = """ +One change to an `unsafe` block, adapting to an API change in `windows_sys` +(`Win32::Foundation::HANDLE` changed from `isize` to `*mut c_void`). I confirmed +that the Windows documentation permits an argument of `std::ptr::null_mut()`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.nix]] -who = "Gabriele Svelto " +[[audits.zcash.audits.document-features]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.26.2 -> 0.27.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.2.11 -> 0.2.12" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.nix]] -who = "Alex Franchuk " +[[audits.zcash.audits.documented]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.27.1 -> 0.28.0" +delta = "0.3.0 -> 0.9.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.documented]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.9.1 -> 0.9.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.documented-macros]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.9.1 -> 0.9.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.dunce]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "1.0.5" notes = """ -Many new features and bugfixes. Obviously there's a lot of unsafe code calling -libc, but the usage looks correct. +Does what it says on the tin. No `unsafe`, and the only IO is `std::fs::canonicalize`. +Path and string handling looks plausibly correct. """ -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.nix]] -who = "Alex Franchuk " +[[audits.zcash.audits.dyn-clone]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.19 -> 1.0.20" +notes = """ +Changes to `unsafe` code: +- Migrating to `core::ptr::addr_of_mut!()` with MSRV bump. +- Gating a function that uses `unsafe` behind `target_has_atomic = "ptr"`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.8" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.8 -> 0.3.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.10 -> 0.3.11" +notes = "The `__errno` location for vxworks and cygwin looks correct from a quick search." +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.11 -> 0.3.13" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.13 -> 0.3.14" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.glob]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.2 -> 0.3.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.http-body]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.0.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.inout]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.is-terminal]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.16 -> 0.4.17" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.is_terminal_polyfill]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.70.1 -> 1.70.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.jsonrpsee]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.28.0 -> 0.29.0" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.7 -> 0.24.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.num-conv]] -who = "Alex Franchuk " +[[audits.zcash.audits.jsonrpsee]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.1.0" -notes = """ -Very straightforward, simple crate. No dependencies, unsafe, extern, -side-effectful std functions, etc. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.9 -> 0.24.10" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.powerfmt]] -who = "Alex Franchuk " +[[audits.zcash.audits.jsonrpsee-core]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.2.0" -notes = """ -A tiny bit of unsafe code to implement functionality that isn't in stable rust -yet, but it's all valid. Otherwise it's a pretty simple crate. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.7 -> 0.24.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.rustc-hash]] -who = "Bobby Holley " +[[audits.zcash.audits.jsonrpsee-core]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "1.1.0" -notes = "Straightforward crate with no unsafe code, does what it says on the tin." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.9 -> 0.24.10" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.strsim]] -who = "Ben Dean-Kawamura " +[[audits.zcash.audits.jsonrpsee-proc-macros]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.10.0 -> 0.11.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.7 -> 0.24.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.synstructure]] -who = "Nika Layzell " +[[audits.zcash.audits.jsonrpsee-proc-macros]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.12.6" -notes = """ -I am the primary author of the `synstructure` crate, and its current -maintainer. The one use of `unsafe` is unnecessary, but documented and -harmless. It will be removed in the next version. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.9 -> 0.24.10" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.time-core]] -who = "Kershaw Chang " +[[audits.zcash.audits.jsonrpsee-server]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.1.0" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.7 -> 0.24.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.time-core]] -who = "Kershaw Chang " +[[audits.zcash.audits.jsonrpsee-server]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.0 -> 0.1.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.9 -> 0.24.10" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.time-core]] -who = "Alex Franchuk " +[[audits.zcash.audits.jsonrpsee-types]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.1 -> 0.1.2" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.7 -> 0.24.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.time-macros]] -who = "Kershaw Chang " +[[audits.zcash.audits.jsonrpsee-types]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.2.6" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.24.9 -> 0.24.10" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.time-macros]] -who = "Kershaw Chang " +[[audits.zcash.audits.maybe-rayon]] +who = "Sean Bowe " criteria = "safe-to-deploy" -delta = "0.2.6 -> 0.2.10" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +version = "0.1.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.time-macros]] -who = "Alex Franchuk " +[[audits.zcash.audits.memuse]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.2.10 -> 0.2.18" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.2.1 -> 0.2.2" +notes = "Adds no-std support; no other changes. Note that I am the author of the crate." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.mozilla.audits.tracing-core]] -who = "Alex Franchuk " +[[audits.zcash.audits.multimap]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.1.30" -notes = """ -Most unsafe code is in implementing non-std sync primitives. Unsafe impls are -logically correct and justified in comments, and unsafe code is sound and -justified in comments. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "0.10.0 -> 0.10.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.zerocopy]] -who = "Alex Franchuk " +[[audits.zcash.audits.nonempty]] +who = "Kris Nuttycombe " criteria = "safe-to-deploy" -version = "0.7.32" +version = "0.11.0" notes = """ -This crate is `no_std` so doesn't use any side-effectful std functions. It -contains quite a lot of `unsafe` code, however. I verified portions of this. It -also has a large, thorough test suite. The project claims to run tests with -Miri to have stronger soundness checks, and also claims to use formal -verification tools to prove correctness. +Additional use of `unsafe` to wrap `NonZeroUsize::new_unchecked`; in both cases +the argument to this method is ` + 1`; in general this +is safe with the exception that if an existing `Vec` has length or capacity +`usize::MAX` this could wrap into zero; it would be better to use the safe +operation and then `expect` to generate a panic, rather than risk undefined +behavior. + +Additions are: +- no_std support +- sorting +- `nonzero` module (just wrappers +- `serde` support +- `nonempty macro` (trivial, verified safe) """ -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.mozilla.audits.zerocopy-derive]] -who = "Alex Franchuk " +[[audits.zcash.audits.num_cpus]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.7.32" -notes = "Clean, safe macros for zerocopy." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +delta = "1.16.0 -> 1.17.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.autocfg]] +[[audits.zcash.audits.opaque-debug]] who = "Daira-Emma Hopwood " criteria = "safe-to-deploy" -delta = "1.2.0 -> 1.3.0" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "0.3.0 -> 0.3.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.bip32]] -who = "Jack Grigg " +[[audits.zcash.audits.phf]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.5.1" +delta = "0.8.0 -> 0.11.1" notes = """ -- Crate has no unsafe code, and sets `#![forbid(unsafe_code)]`. -- Crate has no powerful imports. Only filesystem acces is via `include_str!`, and is safe. +Mostly modernisation, migrating to `PhfBorrow`, and making more things `&'static`. +No unsafe code in the new `OrderedMap` and `OrderedSet` types. """ -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" - -[[audits.zcash.audits.bytes]] -who = "Jack Grigg " -criteria = "safe-to-deploy" -delta = "1.7.1 -> 1.7.2" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.fastrand]] -who = "Jack Grigg " +[[audits.zcash.audits.phf]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "2.0.0 -> 2.0.1" +delta = "0.11.1 -> 0.11.2" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.fastrand]] +[[audits.zcash.audits.phf]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "2.1.0 -> 2.1.1" +delta = "0.11.2 -> 0.11.3" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.futures]] +[[audits.zcash.audits.phf]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.3.28 -> 0.3.30" -notes = "Only sub-crate updates and corresponding changes to tests." +delta = "0.11.3 -> 0.12.1" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.h2]] -who = "Jack Grigg " +[[audits.zcash.audits.phf_generator]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.3.26 -> 0.4.5" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "0.8.0 -> 0.11.1" +notes = "Just dependency and edition bumps and code formatting." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.phf_generator]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.11.1 -> 0.11.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.hyper-timeout]] +[[audits.zcash.audits.phf_generator]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.4.1 -> 0.5.1" -notes = "New uses of pin_project! look fine." -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "0.11.2 -> 0.11.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.hyper-util]] +[[audits.zcash.audits.phf_generator]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.5 -> 0.1.6" +delta = "0.11.3 -> 0.12.1" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.inout]] -who = "Daira Hopwood " +[[audits.zcash.audits.phf_shared]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.1.3" -notes = "Reviewed in full." +delta = "0.8.0 -> 0.11.1" +notes = """ +Adds `uncased` dependency, and newly generates unsafe code to transmute `&'static str` +into `&'static UncasedStr`. I verified that `UncasedStr` is a `#[repr(transparent)]` +newtype around `str`. +""" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.known-folders]] +[[audits.zcash.audits.phf_shared]] who = "Jack Grigg " criteria = "safe-to-deploy" -version = "1.0.1" -notes = """ -Uses `unsafe` blocks to interact with `windows-sys` crate. -- `SHGetKnownFolderPath` safety requirements are met. -- `CoTaskMemFree` has no effect if passed `NULL`, so there is no issue if some - future refactor created a pathway where `ffi::Guard` could be dropped before - `SHGetKnownFolderPath` is called. -- Small nit: `ffi::Guard::as_pwstr` takes `&self` but returns `PWSTR` which is - the mutable type; it should instead return `PCWSTR` which is the const type - (and what `lstrlenW` takes) instead of implicitly const-casting the pointer, - as this would better reflect the intent to take an immutable reference. -- The slice constructed from the `PWSTR` correctly goes out of scope before - `guard` is dropped. -- A code comment says that `path_ptr` is valid for `len` bytes, but `PCWSTR` is - a `*const u16` and `lstrlenW` returns its length \"in characters\" (which the - Windows documentation confirms means the number of `WCHAR` values). This is - likely a typo; the code checks that `len * size_of::() <= isize::MAX`. -""" +delta = "0.11.1 -> 0.11.2" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.known-folders]] +[[audits.zcash.audits.phf_shared]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "1.0.1 -> 1.1.0" -notes = "Addresses the notes from my previous review :)" +delta = "0.11.2 -> 0.11.3" +notes = "Bumped MSRV and dependency versions to remove an `unsafe` block." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.log]] -who = "Daira-Emma Hopwood " +[[audits.zcash.audits.phf_shared]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.4.20 -> 0.4.21" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +delta = "0.11.3 -> 0.12.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.maybe-rayon]] -who = "Sean Bowe " +[[audits.zcash.audits.quinn-udp]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.1.1" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +delta = "0.5.13 -> 0.5.14" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" -[[audits.zcash.audits.pin-project-lite]] -who = "Daira-Emma Hopwood " +[[audits.zcash.audits.r-efi]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.2.13 -> 0.2.14" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +delta = "5.2.0 -> 5.3.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" [[audits.zcash.audits.rand_xorshift]] who = "Sean Bowe " @@ -1450,25 +3958,68 @@ https://research.nccgroup.com/wp-content/uploads/2020/07/NCC_Group_Zcash2018_Pub """ aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.rustc_version]] +[[audits.zcash.audits.redjubjub]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.8.0" +notes = "This release adds `no-std` compatibility." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.redox_users]] who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.4.0" +delta = "0.4.5 -> 0.5.0" notes = """ -Most of the crate is code to parse and validate the output of `rustc -vV`. The caller can -choose which `rustc` to use, or can use `rustc_version::{version, version_meta}` which will -try `$RUSTC` followed by `rustc`. - -If an adversary can arbitrarily set the `$RUSTC` environment variable then this crate will -execute arbitrary code. But when this crate is used within a build script, `$RUSTC` should -be set correctly by `cargo`. +Changes `Config` from using scheme prefixes (with a default of `file:`) to root +FS prefixes (with a default of `/`). The behaviour of `Config::scheme` changed +correspondingly but without being renamed. The effect on the rest of the crate +is that the passwd, shadow, and group files now default to UNIX-style paths +(`/etc/passwd`) instead of scheme syntax (`file:etc/passwd`). """ +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.redox_users]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.regex]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.10.6 -> 1.11.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc_version]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.4.1" +notes = "Changes to `Command` usage are to add support for `RUSTC_WRAPPER`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustversion]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.20 -> 1.0.21" +notes = "Build script change is to fix building with `-Zfmt-debug=none`." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.secp256k1]] +[[audits.zcash.audits.rustversion]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.26.0 -> 0.27.0" +delta = "1.0.21 -> 1.0.22" +notes = "Changes to generated code are to prepend a clippy annotation." +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.schemars]] +who = "Schell Carl Scivally " +criteria = "safe-to-deploy" +delta = "0.8.12 -> 0.9.0" +notes = """ +The changes are primarily API refactoring and simplification, dependency updates, new type implementations, and feature flag reorganization. +The crate changed from #![forbid(unsafe_code)] (line 9347) to #![deny(unsafe_code)] (line 9348), to accommodate the ref-cast crate integration which requires #[allow(unsafe_code)] on specific functions. +The only notable change is the ref-cast usage which is a sound pattern for creating transparent newtype wrappers. +""" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" [[audits.zcash.audits.signature]] @@ -1487,83 +4038,83 @@ criteria = "safe-to-deploy" delta = "2.1.0 -> 2.2.0" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.sync_wrapper]] +[[audits.zcash.audits.strum]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.2 -> 1.0.1" +delta = "0.27.1 -> 0.27.2" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.thiserror]] +[[audits.zcash.audits.strum_macros]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "1.0.61 -> 1.0.63" +delta = "0.27.1 -> 0.27.2" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.thiserror-impl]] +[[audits.zcash.audits.sync_wrapper]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "1.0.61 -> 1.0.63" +delta = "0.1.2 -> 1.0.1" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.tinyvec_macros]] -who = "Jack Grigg " +[[audits.zcash.audits.sync_wrapper]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.0 -> 0.1.1" -notes = "Adds `#![forbid(unsafe_code)]` and license files." -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +delta = "1.0.1 -> 1.0.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.tokio-stream]] -who = "Daira-Emma Hopwood " +[[audits.zcash.audits.thread_local]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.14 -> 0.1.15" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "1.1.4 -> 1.1.7" +notes = """ +New `unsafe` usage: +- An extra `deallocate_bucket`, to replace a `Mutex::lock` with a `compare_exchange`. +- Setting and getting a `#[thread_local] static mut Option` on nightly. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.tonic]] +[[audits.zcash.audits.thread_local]] who = "Daira-Emma Hopwood " criteria = "safe-to-deploy" -delta = "0.10.2 -> 0.11.0" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "1.1.7 -> 1.1.8" +notes = """ +Adds `unsafe` code that makes an assumption that `ptr::null_mut::>()` is a valid representation +of an `AtomicPtr>`, but this is likely a correct assumption. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.tonic]] +[[audits.zcash.audits.thread_local]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.12.0 -> 0.12.1" -notes = "Changes to generics bounds look fine" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" - -[[audits.zcash.audits.tonic-build]] -who = "Daira-Emma Hopwood " -criteria = "safe-to-deploy" -delta = "0.10.2 -> 0.11.0" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "1.1.8 -> 1.1.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.tonic-build]] -who = "Jack Grigg " +[[audits.zcash.audits.tinyvec_macros]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.11.0 -> 0.12.0" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "0.1.0 -> 0.1.1" +notes = "Adds `#![forbid(unsafe_code)]` and license files." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.tonic-build]] +[[audits.zcash.audits.try-lock]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.12.0 -> 0.12.1" -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "0.2.4 -> 0.2.5" +notes = "Bumps MSRV to remove unsafe code block." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.tracing-core]] -who = "Jack Grigg " +[[audits.zcash.audits.universal-hash]] +who = "Daira Hopwood " criteria = "safe-to-deploy" -delta = "0.1.30 -> 0.1.31" -notes = """ -The only new `unsafe` block is to intentionally leak a scoped subscriber onto -the heap when setting it as the global default dispatcher. I checked that the -global default can only be set once and is never dropped. -""" +delta = "0.4.1 -> 0.5.0" +notes = "I checked correctness of to_blocks which uses unsafe code in a safe function." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.tracing-core]] +[[audits.zcash.audits.valuable]] who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.1.31 -> 0.1.32" +delta = "0.1.0 -> 0.1.1" +notes = "Build script changes are for linting." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" [[audits.zcash.audits.visibility]] @@ -1619,56 +4170,112 @@ criteria = "safe-to-deploy" version = "0.2.0" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.wasm-bindgen-macro-support]] -who = "Daira-Emma Hopwood " +[[audits.zcash.audits.wait-timeout]] +who = "Jack Grigg " criteria = "safe-to-deploy" -version = "0.2.92" +delta = "0.2.0 -> 0.2.1" +notes = """ +- Changes to `unsafe` code blocks are just formatting. +- Changes to `extern fn`s are to declare them explicitly as `extern "C" fn`s. +""" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.zcash_address]] -who = "Kris Nuttycombe " +[[audits.zcash.audits.want]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.3.2 -> 0.4.0" -notes = "This release contains no unsafe code and consists soley of added convenience methods." -aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" +delta = "0.3.0 -> 0.3.1" +notes = """ +Migrates to `try-lock 0.2.4` to replace some unsafe APIs that were not marked +`unsafe` (but that were being used safely). +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.zcash_encoding]] -who = "Kris Nuttycombe " +[[audits.zcash.audits.wasi]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.11.0+wasi-snapshot-preview1 -> 0.11.1+wasi-snapshot-preview1" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.winapi-util]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.9 -> 0.1.11" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.windows-link]] +who = "Jack Grigg " criteria = "safe-to-deploy" delta = "0.2.0 -> 0.2.1" -notes = "This release adds minor convenience methods and involves no unsafe code." +notes = "No code changes at all." aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.zcash_keys]] -who = "Kris Nuttycombe " +[[audits.zcash.audits.windows-targets]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.2.0 -> 0.3.0" +delta = "0.53.0 -> 0.53.5" aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.zcash_primitives]] -who = "Kris Nuttycombe " +[[audits.zcash.audits.windows_aarch64_gnullvm]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.15.1 -> 0.16.0" -notes = "The primary change here is the switch from the `hdwallet` dependency to using `bip32`." +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.zcash_proofs]] -who = "Kris Nuttycombe " +[[audits.zcash.audits.windows_aarch64_msvc]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.15.0 -> 0.16.0" -notes = "This release involves only updates of previously-vetted dependencies." +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.zerocopy]] -who = "Daira-Emma Hopwood " +[[audits.zcash.audits.windows_i686_gnu]] +who = "Jack Grigg " criteria = "safe-to-deploy" -delta = "0.7.32 -> 0.7.34" +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" -[[audits.zcash.audits.zerocopy-derive]] -who = "Daira-Emma Hopwood " +[[audits.zcash.audits.windows_i686_gnullvm]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.windows_i686_msvc]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.windows_x86_64_gnu]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.windows_x86_64_gnullvm]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.windows_x86_64_msvc]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.53.0 -> 0.53.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.zcash_note_encryption]] +who = "Kris Nuttycombe " criteria = "safe-to-deploy" -delta = "0.7.32 -> 0.7.34" +version = "0.4.1" +notes = "Additive-only change that exposes the ability to decrypt by pk_d and esk. No functional changes." aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" [audits.zcashd.audits] diff --git a/testnet-single-node-deploy/ec2/README.md b/testnet-single-node-deploy/ec2/README.md index c6d1773d16c..8862e891b24 100644 --- a/testnet-single-node-deploy/ec2/README.md +++ b/testnet-single-node-deploy/ec2/README.md @@ -114,8 +114,8 @@ docker build -f testnet-single-node-deploy/dockerfile -t $REPO:latest . && docke Must be built from this branch: it carries the ZSA transaction format the node produces. -**2. Launch** — Console → Launch Templates → `zebra-testnet` → *Launch instance -from template*. First boot takes 2-3 min. It comes up without a connector until +**2. Launch** — Console → Launch Templates → `zebra-testnet` → _Launch instance +from template_. First boot takes 2-3 min. It comes up without a connector until the workflow tags it `Role=leader`. **3. Genesis + verify** diff --git a/zebra-chain/CHANGELOG.md b/zebra-chain/CHANGELOG.md index 863f113ad4b..42bf237a78a 100644 --- a/zebra-chain/CHANGELOG.md +++ b/zebra-chain/CHANGELOG.md @@ -7,6 +7,137 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.1.0] - 2026-06-18 + +### Added + +- `parameters::constants::MAX_BLOCK_REORG_HEIGHT`: the maximum chain reorganisation height (1000). + +## [10.0.0] - 2026-06-10 + +### Breaking Changes + +- `Block::chain_value_pool_change()`: the `deferred_pool_balance_change` parameter type + changed from `Option` to `DeferredPoolBalanceChange`. + +### Changed + +- Updated mainnet and testnet checkpoints. + +## [9.0.0] - 2026-06-02 + +### Added + +- `NetworkUpgrade::Nu6_2` (consensus branch id `0x5437f330`), with activation heights + 3,364,600 on Mainnet and 4,052,000 on Testnet. +- `OrchardShieldedData::proof_size_is_canonical()`. +- `Network::orchard_canonical_proof_size_rule_active()` and + `Network::is_orchard_temporarily_disabled()`. +- A configurable NU6.2 activation height for Testnets (`ConfiguredActivationHeights::nu6_2`). + +### Changed + +- The default Testnet's temporary Orchard-disabling soft-fork height now defaults to + 4,048,500; Regtest leaves it unset. + +## [8.0.0] - 2026-05-28 + +### Removed + +- `block::Height::coinbase_zcash_serialized_size()` +- `transaction`: + - `builder` module + - `Transaction::new_v4_coinbase()` and `new_v5_coinbase()` +- `transparent`: + - `Input::new_coinbase()` and `extra_coinbase_data()` + - `CoinbaseData` struct and its impls + - `EXTRA_ZEBRA_COINBASE_DATA`, `GENESIS_COINBASE_DATA`, `MAX_COINBASE_DATA_LEN`, + `MAX_COINBASE_HEIGHT_DATA_LEN` constants + +### Changed + +- `transparent::Input::Coinbase`: + - `data` field type changed from `CoinbaseData` to `Vec` + - `data` now stores only miner data (without height encoding) +- `block::Hash::max_allocation()` now returns `MAX_BLOCK_LOCATOR_LENGTH` (`101`, + matching Bitcoin Core's `MAX_LOCATOR_SZ`); previously derived from + `MAX_PROTOCOL_MESSAGE_LEN` (~65,535). +- `block::CountedHeader::max_allocation()` now returns `MAX_HEADERS_PER_MESSAGE` + (`160`); previously ~1,409. Mitigates upfront preallocation by a + post-handshake peer on `getblocks`/`getheaders` (CWE-770; same fix shape as + [GHSA-xr93-pcq3-pxf8](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-xr93-pcq3-pxf8)). +- `serialization::zcash_deserialize_external_count` now caps the initial + `Vec::with_capacity` reservation at `MAX_INITIAL_ALLOCATION = 1024` so a + peer-supplied `CompactSize` cannot force a large allocation before any + element bytes are read; the `Vec` grows naturally via `push()`. Complements + the per-type `max_allocation()` caps (CWE-770). + +### Added + +- `block::MAX_BLOCK_LOCATOR_LENGTH: u64 = 101`. +- `block::Height`: + - `impl From for i64` + - `impl From<&block::Height> for i64` + - `impl TryFrom for block::Height` +- `transparent`: + - `Input::miner_data()` + - `Input::coinbase_script()` + - `impl TryFrom for zcash_transparent::address::TransparentAddress` + - `derive(Copy)` on `transparent::Address` +- `transaction`: + - `impl TryFrom<&[u8]> for AuthDigest` + - `impl AsRef<[u8; 32]> for Hash` + - `impl From<&[u8; 32]> for Hash` +- `serialization`: + - `SerializationError::{Num, Opcode, Script}` variants + - `impl ZcashSerialize for u8` + +### Fixed + +- `Block::chain_value_pool_change()` now propagates per-transaction + `ValueBalanceError`s instead of silently dropping them via `flat_map(Result)` + ([#10585](https://github.com/ZcashFoundation/zebra/issues/10585)). + +## [7.0.0] - 2026-05-01 + +### Added + +- `serialization::MAX_HEADERS_PER_MESSAGE: usize`. +- `transaction::VerifiedUnminedTx`: + - `p2sh_sigop_count: u32`. + - `block_sigop_count(&self) -> u32`. + +### Changed + +- Migrated to `zcash_primitives 0.27` (and the rest of the librustzcash 2026-04 + release wave), which replaces the yanked `core2` dependency with `corez`. +- `transaction::VerifiedUnminedTx::new` now takes an additional + `p2sh_sigop_count: u32` parameter. + +## [6.0.2] - 2026-04-17 + +This release fixes an important security issue: + +- [CVE-2026-XXXXX: rk Identity Point Panic in Transaction Verification](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-452v-w3gx-72wg) + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +## [6.0.1] - 2026-03-26 + +This release fixes an important security issue: + +- [CVE-2026-34202: Remote Denial of Service via Crafted V5 Transactions](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-qp6f-w4r3-h8wg) + +The impact of the issue for crate users will depend on the particular usage; +if you use zebra-chain to parse untrusted transactions, a particularly crafted +transaction will raise a panic which will crash your application; you should +update. + +### Fixed + +- Fixed miner subsidy computation. + ## [6.0.0] - 2026-03-12 ### Breaking Changes diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 067885c97d1..0909fcf4c78 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-chain" -version = "6.0.0" +version = "10.1.0" authors.workspace = true description = "Core Zcash data structures" license.workspace = true @@ -86,7 +86,7 @@ zcash_script.workspace = true # ECC deps halo2 = { package = "halo2_proofs", version = "0.3" } -orchard = { workspace = true } +orchard = { workspace = true, features = ["test-dependencies"] } zcash_encoding.workspace = true zcash_history.workspace = true zcash_note_encryption = { workspace = true } @@ -94,7 +94,7 @@ zcash_primitives = { workspace = true, features = ["transparent-inputs"] } sapling-crypto.workspace = true zcash_protocol.workspace = true zcash_address.workspace = true -zcash_transparent.workspace = true +zcash_transparent = { workspace = true } sinsemilla = { version = "0.1" } # Time @@ -168,5 +168,9 @@ required-features = ["bench"] name = "redpallas" harness = false +[[bench]] +name = "transaction" +harness = false + [lints] workspace = true diff --git a/zebra-chain/benches/redpallas.rs b/zebra-chain/benches/redpallas.rs index 1546765805c..25885ba26a5 100644 --- a/zebra-chain/benches/redpallas.rs +++ b/zebra-chain/benches/redpallas.rs @@ -1,4 +1,9 @@ -//! Benchmarks for batch verifiication of RedPallas signatures. +//! Benchmarks for batch verification of RedPallas signatures. +//! +//! Group name `redpallas` matches the `verifier` label in +//! `zebra.consensus.batch.duration_seconds` emitted from +//! `zebra-consensus/src/primitives/redpallas.rs`, so a prod regression on +//! that histogram maps to this file by name. // Disabled due to warnings in criterion macros #![allow(missing_docs)] @@ -37,7 +42,6 @@ enum Item { fn sigs_with_distinct_keys() -> impl Iterator { std::iter::repeat_with(|| { let mut rng = thread_rng(); - // let msg = b""; match rng.gen::() % 2 { 0 => { let sk = SigningKey::::new(thread_rng()); @@ -60,7 +64,7 @@ fn sigs_with_distinct_keys() -> impl Iterator { /// /// Includes heterogeneous groups across [SigType], [SigningKey]s, and messages. fn bench_batch_verify(c: &mut Criterion) { - let mut group = c.benchmark_group("Batch Verification"); + let mut group = c.benchmark_group("redpallas"); for &n in [8usize, 16, 24, 32, 40, 48, 56, 64].iter() { group.throughput(Throughput::Elements(n as u64)); @@ -121,5 +125,9 @@ fn bench_batch_verify(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, bench_batch_verify); +criterion_group! { + name = benches; + config = Criterion::default().noise_threshold(0.1).sample_size(50); + targets = bench_batch_verify +} criterion_main!(benches); diff --git a/zebra-chain/benches/transaction.rs b/zebra-chain/benches/transaction.rs new file mode 100644 index 00000000000..1267546fd32 --- /dev/null +++ b/zebra-chain/benches/transaction.rs @@ -0,0 +1,122 @@ +//! Benchmarks for per-version transaction deserialization and serialization. +//! +//! Zcash has five transaction versions with increasingly complex structures: +//! - V1: transparent only (pre-Overwinter) +//! - V2: adds Sprout JoinSplits with BCTV14 proofs +//! - V3: Overwinter (adds expiry_height, version group ID) +//! - V4: Sapling (adds shielded spends/outputs with Groth16 proofs, non-sequential field order) +//! - V5: NU5 (adds Orchard actions with Halo2 proofs, different field order than V4) +//! +//! V4 deserialization is notably more complex than earlier versions because the +//! binding signature is at the end of the transaction, requiring non-sequential +//! parsing. V5 introduces yet another field ordering and Orchard support. +//! +//! # Test data +//! +//! Transactions are extracted from real mainnet blocks in `zebra-test` vectors. +//! Each version is represented by one or more transactions from blocks at the +//! appropriate network upgrade heights. The benchmark serializes each transaction +//! to bytes first, then benchmarks both deserialization and serialization. + +// Disabled due to warnings in criterion macros +#![allow(missing_docs)] + +use std::io::Cursor; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +use zebra_chain::{ + block::Block, + serialization::{ZcashDeserialize, ZcashSerialize}, + transaction::Transaction, +}; + +/// Extracts the first transaction matching a given version from a block. +fn first_tx_of_version(block: &Block, version: u32) -> Option> { + block + .transactions + .iter() + .find(|tx| tx.version() == version) + .map(|tx| tx.zcash_serialize_to_vec().expect("valid transaction")) +} + +fn bench_transaction_deserialize(c: &mut Criterion) { + let mut group = c.benchmark_group("Transaction Deserialization"); + + // Collect (label, serialized_tx_bytes) pairs for each version. + let mut tx_samples: Vec<(&str, Vec)> = Vec::new(); + + // V1 — transparent coinbase from genesis-era block. + let block = Block::zcash_deserialize(Cursor::new( + zebra_test::vectors::BLOCK_MAINNET_1_BYTES.as_slice(), + )) + .expect("valid block"); + if let Some(bytes) = first_tx_of_version(&block, 1) { + tx_samples.push(("V1 transparent", bytes)); + } + + // V2 — first block with a Sprout JoinSplit (BCTV14 proofs). + let block = Block::zcash_deserialize(Cursor::new( + zebra_test::vectors::BLOCK_MAINNET_396_BYTES.as_slice(), + )) + .expect("valid block"); + if let Some(bytes) = first_tx_of_version(&block, 2) { + tx_samples.push(("V2 sprout joinsplit", bytes)); + } + + // V3 — first Overwinter block. + let block = Block::zcash_deserialize(Cursor::new( + zebra_test::vectors::BLOCK_MAINNET_347500_BYTES.as_slice(), + )) + .expect("valid block"); + if let Some(bytes) = first_tx_of_version(&block, 3) { + tx_samples.push(("V3 overwinter", bytes)); + } + + // V4 — Sapling block with shielded data. + let block = Block::zcash_deserialize(Cursor::new( + zebra_test::vectors::BLOCK_MAINNET_419201_BYTES.as_slice(), + )) + .expect("valid block"); + if let Some(bytes) = first_tx_of_version(&block, 4) { + tx_samples.push(("V4 sapling", bytes)); + } + + // V5 — NU5 block with Orchard data. + let block = Block::zcash_deserialize(Cursor::new( + zebra_test::vectors::BLOCK_MAINNET_1687107_BYTES.as_slice(), + )) + .expect("valid block"); + if let Some(bytes) = first_tx_of_version(&block, 5) { + tx_samples.push(("V5 orchard", bytes)); + } + + for (label, tx_bytes) in &tx_samples { + group.bench_with_input( + BenchmarkId::new("deserialize", label), + tx_bytes, + |b, bytes| b.iter(|| Transaction::zcash_deserialize(Cursor::new(bytes)).unwrap()), + ); + } + + group.finish(); + + let mut group = c.benchmark_group("Transaction Serialization"); + + for (label, tx_bytes) in &tx_samples { + let tx = Transaction::zcash_deserialize(Cursor::new(tx_bytes)).unwrap(); + + group.bench_with_input(BenchmarkId::new("serialize", label), &tx, |b, tx| { + b.iter(|| tx.zcash_serialize_to_vec().unwrap()) + }); + } + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default().noise_threshold(0.1).sample_size(50); + targets = bench_transaction_deserialize +} +criterion_main!(benches); diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index ef9e7cd114b..06903eb34f6 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -462,9 +462,9 @@ where } } +/// Errors that can be returned when validating [`Amount`]s. #[allow(missing_docs)] #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -/// Errors that can be returned when validating [`Amount`]s. pub enum Error { /// input {value} is outside of valid range for zatoshi Amount, valid_range={range:?} Constraint { diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index a67d297613c..ced7713b748 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -11,7 +11,7 @@ use crate::{ orchard, parameters::{Network, NetworkUpgrade}, sapling, - serialization::{TrustedPreallocate, MAX_PROTOCOL_MESSAGE_LEN}, + serialization::TrustedPreallocate, sprout, transaction::Transaction, transparent, @@ -228,19 +228,21 @@ impl Block { pub fn chain_value_pool_change( &self, utxos: &HashMap, - deferred_pool_balance_change: Option, + deferred_pool_balance_change: DeferredPoolBalanceChange, ) -> Result, ValueBalanceError> { - Ok(*self + // `Result` implements `IntoIterator`, so a `flat_map(|t| t.value_balance(utxos))` + // would silently drop transactions whose value balance returns `Err`. Use `try_fold` + // to propagate the first error instead. + let tx_pool_sum = self .transactions .iter() - .flat_map(|t| t.value_balance(utxos)) - .sum::, _>>()? + .try_fold(ValueBalance::::zero(), |acc, tx| { + acc + tx.value_balance(utxos)? + })?; + + Ok(*tx_pool_sum .neg() - .set_deferred_amount( - deferred_pool_balance_change - .map(DeferredPoolBalanceChange::value) - .unwrap_or_default(), - )) + .set_deferred_amount(deferred_pool_balance_change.value())) } /// Compute the root of the authorizing data Merkle tree, @@ -258,14 +260,30 @@ impl<'a> From<&'a Block> for Hash { } } -/// A serialized Block hash takes 32 bytes -const BLOCK_HASH_SIZE: u64 = 32; +/// The maximum number of `block::Hash` entries Zebra will preallocate for in +/// a single peer-deserialized vector. +/// +/// In the P2P protocol, `Vec` appears as the `known_blocks` block +/// locator in `getblocks` and `getheaders` messages. The Bitcoin/Zcash +/// convention encodes locators with exponentially-spaced heights (1, 2, 3, …, +/// 10, 20, 40, …, genesis), giving `~log2(N) + 10` entries for chain length N. +/// For current Zcash chain heights (~3M blocks) a legitimate locator has ~32 +/// entries. +/// +/// We cap at 101 to match Bitcoin Core's `MAX_LOCATOR_SZ` constant +/// (`net_processing.cpp`), which zcashd inherits. This avoids any risk of +/// rejecting legitimate locators sent by compatible nodes that follow the +/// existing Bitcoin/Zcash protocol convention. +/// +/// Without this cap, `Hash::max_allocation` was previously derived from +/// `MAX_PROTOCOL_MESSAGE_LEN / 32 = 65,535`, which allowed a remote peer to +/// force ~2 MiB heap preallocation per crafted `getblocks`/`getheaders` message +/// before any payload was read. This is the same class as +/// GHSA-xr93-pcq3-pxf8 (`addr_limit`), fixed for AddrV1/V2 in PR #10494. +pub const MAX_BLOCK_LOCATOR_LENGTH: u64 = 101; -/// The maximum number of hashes in a valid Zcash protocol message. impl TrustedPreallocate for Hash { fn max_allocation() -> u64 { - // Every vector type requires a length field of at least one byte for de/serialization. - // Since a block::Hash takes 32 bytes, we can never receive more than (MAX_PROTOCOL_MESSAGE_LEN - 1) / 32 hashes in a single message - ((MAX_PROTOCOL_MESSAGE_LEN - 1) as u64) / BLOCK_HASH_SIZE + MAX_BLOCK_LOCATOR_LENGTH } } diff --git a/zebra-chain/src/block/arbitrary.rs b/zebra-chain/src/block/arbitrary.rs index 6c4811005c5..c05aed01c93 100644 --- a/zebra-chain/src/block/arbitrary.rs +++ b/zebra-chain/src/block/arbitrary.rs @@ -582,6 +582,31 @@ where + Copy + 'static, { + // Coinbase transactions must not contain Sapling spends (GHSA-rgwx-8r98-p34c). + // The arbitrary `Transaction` strategy generates these independently, so clear + // any generated Sapling shielded data on coinbase transactions before the chain + // builder commits them. The deserialization rejection path is still exercised + // by the `transaction_roundtrip` proptest and the GHSA-rgwx-8r98-p34c reproduction + // vector in `zebra-chain`. + if transaction.is_coinbase() { + match &mut transaction { + Transaction::V4 { + sapling_shielded_data, + .. + } => *sapling_shielded_data = None, + Transaction::V5 { + sapling_shielded_data, + .. + } => *sapling_shielded_data = None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + sapling_shielded_data, + .. + } => *sapling_shielded_data = None, + Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {} + } + } + let mut spend_restriction = transaction.coinbase_spend_restriction(&Network::Mainnet, height); let mut new_inputs = Vec::new(); let mut spent_outputs = HashMap::new(); diff --git a/zebra-chain/src/block/commitment.rs b/zebra-chain/src/block/commitment.rs index 2915342abce..75df67017f2 100644 --- a/zebra-chain/src/block/commitment.rs +++ b/zebra-chain/src/block/commitment.rs @@ -139,7 +139,7 @@ impl Commitment { } } (Heartwood | Canopy, _) => Ok(ChainHistoryRoot(ChainHistoryMmrRootHash(bytes))), - (Nu5 | Nu6 | Nu6_1 | Nu7, _) => Ok(ChainHistoryBlockTxAuthCommitment( + (Nu5 | Nu6 | Nu6_1 | Nu6_2 | Nu7, _) => Ok(ChainHistoryBlockTxAuthCommitment( ChainHistoryBlockTxAuthCommitmentHash(bytes), )), diff --git a/zebra-chain/src/block/header.rs b/zebra-chain/src/block/header.rs index 39b265e0304..aa507cdde07 100644 --- a/zebra-chain/src/block/header.rs +++ b/zebra-chain/src/block/header.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::{ fmt::HexDebug, parameters::Network, - serialization::{TrustedPreallocate, MAX_PROTOCOL_MESSAGE_LEN}, + serialization::{TrustedPreallocate, MAX_HEADERS_PER_MESSAGE}, work::{difficulty::CompactDifficulty, equihash::Solution}, }; @@ -152,17 +152,6 @@ pub struct CountedHeader { pub header: Arc
, } -/// The serialized size of a Zcash block header. -/// -/// Includes the equihash input, 32-byte nonce, 3-byte equihash length field, and equihash solution. -const BLOCK_HEADER_LENGTH: usize = - crate::work::equihash::Solution::INPUT_LENGTH + 32 + 3 + crate::work::equihash::SOLUTION_SIZE; - -/// The minimum size for a serialized CountedHeader. -/// -/// A CountedHeader has BLOCK_HEADER_LENGTH bytes + 1 or more bytes for the transaction count -pub(crate) const MIN_COUNTED_HEADER_LEN: usize = BLOCK_HEADER_LENGTH + 1; - /// The Zcash accepted block version. /// /// The consensus rules do not force the block version to be this value but just equal or greater than it. @@ -170,9 +159,14 @@ pub(crate) const MIN_COUNTED_HEADER_LEN: usize = BLOCK_HEADER_LENGTH + 1; pub const ZCASH_BLOCK_VERSION: u32 = 4; impl TrustedPreallocate for CountedHeader { + /// Cap `CountedHeader` preallocation at the existing protocol-level + /// constant `MAX_HEADERS_PER_MESSAGE = 160`. The previous return value was + /// derived from `MAX_PROTOCOL_MESSAGE_LEN`, allowing peer-controlled + /// preallocation amplification — same shape as GHSA-xr93-pcq3-pxf8 for + /// `AddrV1`/`AddrV2` (PR #10494). fn max_allocation() -> u64 { - // Every vector type requires a length field of at least one byte for de/serialization. - // Therefore, we can never receive more than (MAX_PROTOCOL_MESSAGE_LEN - 1) / MIN_COUNTED_HEADER_LEN counted headers in a single message - ((MAX_PROTOCOL_MESSAGE_LEN - 1) / MIN_COUNTED_HEADER_LEN) as u64 + // Cast safe: MAX_HEADERS_PER_MESSAGE is the constant 160, which fits + // trivially in u64 on every platform. + MAX_HEADERS_PER_MESSAGE as u64 } } diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index 3a38155e4c5..304c8cd17eb 100644 --- a/zebra-chain/src/block/height.rs +++ b/zebra-chain/src/block/height.rs @@ -148,6 +148,14 @@ impl TryFrom for Height { } } +impl TryFrom for Height { + type Error = core::num::TryFromIntError; + + fn try_from(height: i64) -> Result { + u32::try_from(height).map(Height) + } +} + impl From for u32 { fn from(height: Height) -> Self { height.0 @@ -160,6 +168,18 @@ impl From for u64 { } } +impl From for i64 { + fn from(height: Height) -> Self { + height.0.into() + } +} + +impl From<&Height> for i64 { + fn from(height: &Height) -> Self { + height.0.into() + } +} + /// Convenience trait for converting a type into a valid Zcash [`Height`]. pub trait TryIntoHeight { /// The error type returned by [`Height`] conversion failures. diff --git a/zebra-chain/src/block/tests/preallocate.rs b/zebra-chain/src/block/tests/preallocate.rs index 97ee92dae63..cbafb0238b7 100644 --- a/zebra-chain/src/block/tests/preallocate.rs +++ b/zebra-chain/src/block/tests/preallocate.rs @@ -5,41 +5,58 @@ use std::sync::Arc; use proptest::prelude::*; use crate::{ - block::{ - header::MIN_COUNTED_HEADER_LEN, CountedHeader, Hash, Header, BLOCK_HASH_SIZE, - MAX_PROTOCOL_MESSAGE_LEN, + block::{CountedHeader, Hash, Header, MAX_BLOCK_LOCATOR_LENGTH}, + serialization::{ + arbitrary::max_allocation_is_big_enough, TrustedPreallocate, ZcashSerialize, + MAX_HEADERS_PER_MESSAGE, MAX_PROTOCOL_MESSAGE_LEN, }, - serialization::{arbitrary::max_allocation_is_big_enough, TrustedPreallocate, ZcashSerialize}, }; +/// The serialized size of a Zcash block header. +/// +/// Equihash input + 32-byte nonce + 3-byte equihash solution-length field + +/// equihash solution. Used as a serialized-size lower bound by the +/// `counted_header_min_length` proptest. +const BLOCK_HEADER_LENGTH: usize = + crate::work::equihash::Solution::INPUT_LENGTH + 32 + 3 + crate::work::equihash::SOLUTION_SIZE; + +/// The minimum size for a serialized `CountedHeader`: header bytes plus at +/// least one byte for the transaction count CompactSize. +const MIN_COUNTED_HEADER_LEN: usize = BLOCK_HEADER_LENGTH + 1; + proptest! { - /// Verify that the serialized size of a block hash used to calculate the allocation limit is correct + /// Verify that the serialized size of a block hash is 32 bytes, matching the protocol spec. #[test] fn block_hash_size_is_correct(hash in Hash::arbitrary()) { let serialized = hash.zcash_serialize_to_vec().expect("Serialization to vec must succeed"); - prop_assert!(serialized.len() as u64 == BLOCK_HASH_SIZE); + prop_assert!(serialized.len() as u64 == 32); } /// Verify that... - /// 1. The smallest disallowed vector of `Hash`s is too large to send via the Zcash Wire Protocol - /// 2. The largest allowed vector is small enough to fit in a legal Zcash Wire Protocol message + /// 1. `Hash::max_allocation` is exactly `MAX_BLOCK_LOCATOR_LENGTH`. The cap is intentionally + /// far smaller than the message-size limit, to prevent peer-controlled preallocation + /// amplification (sibling of GHSA-xr93-pcq3-pxf8). + /// 2. The largest allowed vector still fits in a legal Zcash Wire Protocol message. #[test] fn block_hash_max_allocation(hash in Hash::arbitrary_with(())) { let ( smallest_disallowed_vec_len, - smallest_disallowed_serialized_len, + _smallest_disallowed_serialized_len, largest_allowed_vec_len, largest_allowed_serialized_len, ) = max_allocation_is_big_enough(hash); + // The cap is exactly the locator-protocol cap, not derived from message size. + prop_assert!(Hash::max_allocation() == MAX_BLOCK_LOCATOR_LENGTH); + // Check that our smallest_disallowed_vec is only one item larger than the limit prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == Hash::max_allocation()); - // Check that our smallest_disallowed_vec is too big to send as a protocol message - prop_assert!(smallest_disallowed_serialized_len > MAX_PROTOCOL_MESSAGE_LEN); // Check that our largest_allowed_vec contains the maximum number of hashes prop_assert!((largest_allowed_vec_len as u64) == Hash::max_allocation()); + // Check that our largest_allowed_vec is small enough to send as a protocol message + // (this is now slack: the locator cap is much smaller than the message-size limit). prop_assert!(largest_allowed_serialized_len <= MAX_PROTOCOL_MESSAGE_LEN); } @@ -59,8 +76,12 @@ proptest! { #![proptest_config(ProptestConfig::with_cases(128))] /// Verify that... - /// 1. The smallest disallowed vector of `CountedHeaders`s is too large to send via the Zcash Wire Protocol - /// 2. The largest allowed vector is small enough to fit in a legal Zcash Wire Protocol message + /// 1. `CountedHeader::max_allocation` is exactly `MAX_HEADERS_PER_MESSAGE`. The cap is a + /// protocol-level constant (160 headers per `headers` message — the Zcash convention + /// Zebra already enforces on the sending side and at the codec level for `read_headers`) + /// rather than a message-size-derived value, to prevent peer-controlled preallocation + /// amplification (sibling of GHSA-xr93-pcq3-pxf8). + /// 2. The largest allowed vector still fits in a legal Zcash Wire Protocol message. #[test] fn counted_header_max_allocation(header in any::>()) { let header = CountedHeader { @@ -69,19 +90,23 @@ proptest! { let ( smallest_disallowed_vec_len, - smallest_disallowed_serialized_len, + _smallest_disallowed_serialized_len, largest_allowed_vec_len, largest_allowed_serialized_len, ) = max_allocation_is_big_enough(header); + // The cap is exactly the headers-protocol cap, not derived from message size. + // Cast safe: MAX_HEADERS_PER_MESSAGE is the constant 160 (well under u64::MAX). + prop_assert!(CountedHeader::max_allocation() == MAX_HEADERS_PER_MESSAGE as u64); + // Check that our smallest_disallowed_vec is only one item larger than the limit prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == CountedHeader::max_allocation()); - // Check that our smallest_disallowed_vec is too big to send as a protocol message - prop_assert!(smallest_disallowed_serialized_len > MAX_PROTOCOL_MESSAGE_LEN); // Check that our largest_allowed_vec contains the maximum number of CountedHeaders prop_assert!((largest_allowed_vec_len as u64) == CountedHeader::max_allocation()); + // Check that our largest_allowed_vec is small enough to send as a protocol message + // (this is now slack: the headers cap is much smaller than the message-size limit). prop_assert!(largest_allowed_serialized_len <= MAX_PROTOCOL_MESSAGE_LEN); } } diff --git a/zebra-chain/src/block/tests/prop.rs b/zebra-chain/src/block/tests/prop.rs index 461cafc72a1..f7745cbf581 100644 --- a/zebra-chain/src/block/tests/prop.rs +++ b/zebra-chain/src/block/tests/prop.rs @@ -99,6 +99,10 @@ proptest! { fn block_roundtrip(block in any::(), network in any::()) { let _init_guard = zebra_test::init(); + let has_coinbase_sapling_spends = block.transactions.iter().any(|tx| { + tx.is_coinbase() && tx.sapling_spends_per_anchor().count() > 0 + }); + let bytes = block.zcash_serialize_to_vec()?; // Check the block commitment @@ -108,8 +112,12 @@ proptest! { prop_assert_eq![block.header.commitment_bytes.0, commitment_bytes]; } - // Check the block size limit - if bytes.len() <= MAX_BLOCK_BYTES as _ { + if has_coinbase_sapling_spends { + // GHSA-rgwx-8r98-p34c fix: the parser now rejects coinbase + // transactions with Sapling spends before allocating. + bytes.zcash_deserialize_into::() + .expect_err("block with coinbase Sapling spends must be rejected"); + } else if bytes.len() <= MAX_BLOCK_BYTES as _ { // Check deserialization let other_block = bytes.zcash_deserialize_into()?; diff --git a/zebra-chain/src/block/tests/vectors.rs b/zebra-chain/src/block/tests/vectors.rs index 02764c19ef7..4fcbfd7b368 100644 --- a/zebra-chain/src/block/tests/vectors.rs +++ b/zebra-chain/src/block/tests/vectors.rs @@ -1,11 +1,13 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, io::{Cursor, Write}, + sync::Arc, }; use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc}; use crate::{ + amount::{Amount, DeferredPoolBalanceChange, NonNegative, MAX_MONEY}, block::{ serialize::MAX_BLOCK_BYTES, Block, BlockTimeError, Commitment::*, Hash, Header, Height, }, @@ -14,7 +16,8 @@ use crate::{ serialization::{ sha256d, SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, }, - transaction::LockTime, + transaction::{LockTime, Transaction}, + transparent, }; use super::generate; // TODO: this should be rewritten as strategies @@ -62,6 +65,56 @@ fn blockheaderhash_from_blockheader() { assert_eq!(blockheader, other_header); } +/// Regression test for https://github.com/ZcashFoundation/zebra/issues/10585. +/// +/// `Block::chain_value_pool_change()` previously aggregated transaction value +/// balances with `flat_map(|tx| tx.value_balance(utxos))`. Because +/// `Result` implements `IntoIterator`, `Err(_)` yielded zero items and +/// silently dropped the failing transaction from the block sum. The block +/// helper now uses `try_fold` so transaction-level value-balance errors +/// propagate. +#[test] +fn chain_value_pool_change_propagates_transaction_value_balance_errors() { + let _init_guard = zebra_test::init(); + + let max_money: Amount = MAX_MONEY.try_into().expect("MAX_MONEY is a valid amount"); + // Two `MAX_MONEY` transparent outputs make the transaction-level output + // sum exceed `MAX_MONEY`, so `value_balance` returns `Err`. + let coinbase = Transaction::V1 { + inputs: vec![transparent::Input::Coinbase { + height: Height(1), + data: vec![], + sequence: 0xFFFF_FFFF, + }], + outputs: vec![ + transparent::Output::new(max_money, transparent::Script::new(&[])), + transparent::Output::new(max_money, transparent::Script::new(&[])), + ], + lock_time: LockTime::unlocked(), + }; + let utxos = HashMap::new(); + + assert!( + coinbase.value_balance(&utxos).is_err(), + "transaction-level value balance should reject an output sum above MAX_MONEY" + ); + + let header: Header = zebra_test::vectors::DUMMY_HEADER + .zcash_deserialize_into() + .expect("dummy header should deserialize"); + let block = Block { + header: Arc::new(header), + transactions: vec![Arc::new(coinbase)], + }; + + assert!( + block + .chain_value_pool_change(&utxos, DeferredPoolBalanceChange::zero()) + .is_err(), + "block-level aggregation should propagate transaction value-balance errors" + ); +} + #[test] fn blockheader_serialization() { let _init_guard = zebra_test::init(); diff --git a/zebra-chain/src/history_tree.rs b/zebra-chain/src/history_tree.rs index e8258880fe7..cfb6522a08d 100644 --- a/zebra-chain/src/history_tree.rs +++ b/zebra-chain/src/history_tree.rs @@ -105,6 +105,7 @@ impl NonEmptyHistoryTree { NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu7 => { let tree = Tree::::new_from_cache( network, @@ -174,6 +175,7 @@ impl NonEmptyHistoryTree { NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu7 => { let (tree, entry) = Tree::::new_from_block( network, diff --git a/zebra-chain/src/orchard/action.rs b/zebra-chain/src/orchard/action.rs index f9e9ad9c0ec..fb1b33dcb32 100644 --- a/zebra-chain/src/orchard/action.rs +++ b/zebra-chain/src/orchard/action.rs @@ -81,7 +81,15 @@ impl ZcashDeserialize for Action { // https://zips.z.cash/protocol/protocol.pdf#concretereddsa // This only reads the 32-byte buffer. The type is enforced // on signature verification; see [`reddsa::batch`] - rk: reader.read_32_bytes()?.into(), + rk: { + let rk_bytes = reader.read_32_bytes()?; + if rk_bytes == [0u8; 32] { + return Err(SerializationError::Parse( + "Orchard action rk must not be the identity point", + )); + } + rk_bytes.into() + }, // Type is `{0 .. 𝑞_ℙ − 1}`. Note that the second rule quoted above // is also enforced here and it is technically redundant with the first. // See [`pallas::Base::zcash_deserialize`]. diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index c1355d5a8c9..b4e7cc6353f 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -28,6 +28,19 @@ use orchard::{note::AssetBase, value::ValueSum}; use super::{OrchardVanilla, ShieldedDataFlavor}; +/// Returns the canonical size in bytes of an Orchard proof for `num_actions` actions. +/// +/// An Orchard proof is a Halo2 proof whose length is exactly linear in the number of +/// actions (circuit instances): 4992 bytes for 1 action and 7264 bytes for 2 actions, +/// i.e. a fixed base plus 2272 bytes per action. The exact constants are owned by the +/// `orchard` crate, which derives them from the action circuit's `halo2_proofs` +/// `CircuitCost` and cross-checks them in its circuit tests, so we delegate to +/// [`orchard::Proof::expected_proof_size`] rather than re-deriving them here. The +/// `expected_proof_size_known_values` guard test cross-checks the returned values. +pub(crate) fn expected_proof_size(num_actions: usize) -> usize { + orchard::Proof::expected_proof_size::(num_actions) +} + /// A bundle of [`Action`] descriptions and signature data. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[cfg_attr( @@ -90,6 +103,17 @@ impl ShieldedData { self.actions.actions() } + /// Returns whether the proof has the canonical length for its number of actions. + /// + /// An Orchard proof is stored as an unbounded byte sequence, so a proof that is + /// present but not canonically sized can be padded with arbitrary trailing data + /// without affecting its validity. Bundles are parsed leniently (so that historical + /// transactions remain deserializable), so this is enforced separately as a + /// height-gated consensus rule. See `GHSA-jfw5-j458-pfv6`. + pub fn proof_size_is_canonical(&self) -> bool { + self.proof.0.len() == expected_proof_size::(self.actions.len()) + } + /// Collect the [`Nullifier`]s for this transaction. pub fn nullifiers(&self) -> impl Iterator { self.actions().map(|action| &action.nullifier) diff --git a/zebra-chain/src/orchard/tests.rs b/zebra-chain/src/orchard/tests.rs index 7db493ad946..4524730afda 100644 --- a/zebra-chain/src/orchard/tests.rs +++ b/zebra-chain/src/orchard/tests.rs @@ -1,6 +1,7 @@ #![allow(clippy::unwrap_in_result)] mod preallocate; +mod proof_size; mod prop; mod tree; pub(crate) mod vectors; diff --git a/zebra-chain/src/orchard/tests/proof_size.rs b/zebra-chain/src/orchard/tests/proof_size.rs new file mode 100644 index 00000000000..7b8d5e573c6 --- /dev/null +++ b/zebra-chain/src/orchard/tests/proof_size.rs @@ -0,0 +1,29 @@ +//! Tests for canonical Orchard proof sizes. + +use crate::orchard::{shielded_data::expected_proof_size, OrchardVanilla}; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use crate::orchard::OrchardZSA; + +/// The canonical Orchard proof size for `n` actions is: +/// +/// - `2272·n + 2720` bytes for `OrchardVanilla`; +/// - `2272·n + 2848` bytes for `OrchardZSA`. +/// +/// These values match the corresponding Orchard circuits' `halo2_proofs` +/// `CircuitCost` results and are consensus-critical, so pin them here. +#[test] +fn expected_proof_size_known_values() { + assert_eq!(expected_proof_size::(0), 2720); + assert_eq!(expected_proof_size::(1), 4992); + assert_eq!(expected_proof_size::(2), 7264); + assert_eq!(expected_proof_size::(3), 9536); + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + assert_eq!(expected_proof_size::(0), 2848); + assert_eq!(expected_proof_size::(1), 5120); + assert_eq!(expected_proof_size::(2), 7392); + assert_eq!(expected_proof_size::(3), 9664); + } +} diff --git a/zebra-chain/src/parameters/arbitrary.rs b/zebra-chain/src/parameters/arbitrary.rs index 63e678b3dc1..9a71b98695e 100644 --- a/zebra-chain/src/parameters/arbitrary.rs +++ b/zebra-chain/src/parameters/arbitrary.rs @@ -19,6 +19,15 @@ impl NetworkUpgrade { .boxed() } + /// Generates network upgrades that are valid for V5+ transactions (Nu5 onward). + pub fn nu5_branch_id_strategy() -> BoxedStrategy { + prop_oneof![ + Just(NetworkUpgrade::Nu5), + // TODO: add future network upgrades (#1974) + ] + .boxed() + } + /// Generates network upgrades from a reduced set pub fn reduced_branch_id_strategy() -> BoxedStrategy { // We use this strategy to test legacy chain diff --git a/zebra-chain/src/parameters/checkpoint/list.rs b/zebra-chain/src/parameters/checkpoint/list.rs index 9560e8b033e..6c8a9fa8595 100644 --- a/zebra-chain/src/parameters/checkpoint/list.rs +++ b/zebra-chain/src/parameters/checkpoint/list.rs @@ -12,11 +12,7 @@ use std::{ sync::Arc, }; -use crate::{ - block::{self, Height}, - parameters::{Network, NetworkUpgrade}, - BoxError, -}; +use crate::{block, parameters::Network, BoxError}; #[cfg(test)] mod tests; @@ -45,6 +41,16 @@ const MAINNET_CHECKPOINTS: &str = include_str!("main-checkpoints.txt"); /// information. pub(crate) const TESTNET_CHECKPOINTS: &str = include_str!("test-checkpoints.txt"); +lazy_static::lazy_static! { + /// Parsed mainnet checkpoint list, cached to avoid re-parsing on every use. + static ref MAINNET_CHECKPOINT_LIST: Arc = + Arc::new(MAINNET_CHECKPOINTS.parse().expect("hard-coded mainnet checkpoint list parses")); + + /// Parsed testnet checkpoint list, cached to avoid re-parsing on every use. + pub(crate) static ref TESTNET_CHECKPOINT_LIST: Arc = + Arc::new(TESTNET_CHECKPOINTS.parse().expect("hard-coded testnet checkpoint list parses")); +} + impl Network { /// Returns the hash for the genesis block in `network`. pub fn genesis_hash(&self) -> block::Hash { @@ -59,41 +65,10 @@ impl Network { } /// Returns the hard-coded checkpoint list for `network`. pub fn checkpoint_list(&self) -> Arc { - let checkpoints_for_network = match self { - Network::Mainnet => MAINNET_CHECKPOINTS, - Network::Testnet(params) => return params.checkpoints(), - }; - - // Check that the list starts with the correct genesis block and parses checkpoint list. - let first_checkpoint_height = checkpoints_for_network - .lines() - .next() - .map(checkpoint_height_and_hash); - - let checkpoints = match first_checkpoint_height { - // parse calls CheckpointList::from_list - Some(Ok((block::Height(0), hash))) if hash == self.genesis_hash() => { - checkpoints_for_network - .parse() - .expect("hard-coded checkpoint list parses and validates") - } - Some(Ok((block::Height(0), _))) => { - panic!("the genesis checkpoint does not match the {self} genesis hash") - } - Some(Ok(_)) => panic!("checkpoints must start at the genesis block height 0"), - Some(Err(err)) => panic!("{err}"), - - None if NetworkUpgrade::Canopy.activation_height(self) == Some(Height(1)) => { - CheckpointList::from_list([(block::Height(0), self.genesis_hash())]) - .expect("hard-coded checkpoint list parses and validates") - } - None => panic!( - "Zebra requires checkpoints on networks which do not activate \ - the Canopy network upgrade at block height 1" - ), - }; - - Arc::new(checkpoints) + match self { + Network::Mainnet => MAINNET_CHECKPOINT_LIST.clone(), + Network::Testnet(params) => params.checkpoints(), + } } } diff --git a/zebra-chain/src/parameters/checkpoint/main-checkpoints.txt b/zebra-chain/src/parameters/checkpoint/main-checkpoints.txt index ee0553a6146..e35cfeb92b3 100644 --- a/zebra-chain/src/parameters/checkpoint/main-checkpoints.txt +++ b/zebra-chain/src/parameters/checkpoint/main-checkpoints.txt @@ -13786,3 +13786,264 @@ 3268006 0000000000f82d454400e585606d6b40ee342b73118e9f23ce2d7f21aba9f8be 3268406 000000000055fc8bf26da430851429460ce552551652ea22c698b875fc8c4d35 3268806 000000000027fd2d13479a3f56f6cefa8436fae0fa6c08d19e3dd873eb5516b9 +3269206 0000000000c2429594af151e23872786e566d6929aa58cbc703a420e892a5bb4 +3269606 000000000008a034b19066eec0b45efe97e39ddb820e44909fb77f4b30b70a2a +3270006 000000000125609f77f07ebd70540229efed11ba18deed6c25a200c329499f2e +3270406 000000000028b3560e41ec2e2fda92e51b255fae10769c14bcb1ef2c1f833b48 +3270806 0000000000bb5e9a49740c628149fd5410bd012b02791c75d7932bb6c6df374f +3271206 0000000000e46cfc91a3023f2a946c0944ce793f6f2a45ffc1c45764b594bd68 +3271606 0000000000664a6ee3e45058cceb90f83b6804a393157ecd22acce5e2f303dd6 +3272006 0000000000ee963a05a58f0b636f20cd4f3338844be9c4f38238a9ac035bb626 +3272406 000000000134b95ca1e4e6996dbed52c77a26181180f782d5cd7b448ed80a634 +3272806 0000000000668631a59f43fc3a9c6a708cee99a938f9f6606579614e991f5082 +3273206 0000000000707b15a4d6e6f514da12a02947d5688bc595488c3cd5fea66811f1 +3273606 0000000000861f11d5cd8fc34856d0537ee7bcc8514fa98ffe4218529ced46da +3274006 0000000000d559a368d6cd53635efcb9b7a6c9a1b9b67bc39215bec45e5b4168 +3274406 0000000000335b5108c988a22d05cf34b345f2a48c340df1cd4c7f03c4ea0113 +3274806 0000000000d09769c79854a693639b6948f4ce97e2e1977aa5b6f8064da30f89 +3275206 0000000000a7802eb9f80ee8038804a9d61e120942b18dfdb339b2663f85cbfd +3275606 0000000000da71205fa1ff6a7ea619d48f339c940cac5ed77869c654e28fdf0d +3276006 00000000008169f3345380295dcb910877037b1d7b201abe477aeeaa2361ff84 +3276406 00000000004376add62bf23c5892a8ad8b4a7125ce18d9247ae45ded0fbc3624 +3276806 0000000000aa26dcd31e77e2d3f8592151e885d26758547b6776ed2ddf320885 +3277206 00000000001b66a7f25428e69be2edcd0b97f9f01ca621d4cc68552555bea3e8 +3277606 00000000010141e3c6726fee343587644be015099b09443ec439404fdbacdd72 +3278006 0000000000ff1aa6741c71eae94a2581a30df20e167c780414a38b947e7d17e1 +3278406 00000000011959ce0fc93dacd89d6f8beba4964a33e5512dda57e9afdba2173b +3278806 00000000004676c53f9294aca8337efb78c4865d1eb4a6a14a2fa448ac3fc4ed +3279206 00000000003b94a66589a095420fed52c129d255d4104db370890ceac4cd1504 +3279606 00000000011915e752d1a159e88bd3de1fa32b3206ff90ad46bb7a991af0a3fa +3280006 0000000000a0893cdad958646fbc2a60c271f6c0f0167f02dd0d73daf5ebbb4e +3280406 000000000033ed4d1b772175ecf221fd68670cf1647d8f2e2a319c66a142746d +3280806 00000000004488635645221cfcf9e58e6de075682ed950c53e36fec3f5b6079a +3281206 00000000003a059a8653bcc0af513f542b29c6195531c31d4ffc7b32b61e927f +3281606 00000000012b0b57b4e23921b00a2c9da16e670c9af349f79f83f6dbfad528b3 +3282006 00000000007a375e47cbe39f87af2c7466840e0111d4bd79f72974c2d819c8af +3282406 0000000000b1db9382a573ea7ebf141fcd87f3013f532d9485bf43194b372d8c +3282806 0000000000b7a4772b15ba032b29c450e3e4d5361f14637690bdb5257e35945f +3283206 000000000091c99c103f52431e7810612dcbad9efe38106d5603d927861f6133 +3283606 000000000021f5bc8147c3b380df82068b547fba35b096431ae21f32160d9003 +3284006 00000000005f955331b482ee987aadfee5428ada8cb43831d53985cb2f52c8b7 +3284406 0000000000e1cf951c0825fca5f3e6b6e009c821dcb2179d87ea10989cf85f18 +3284806 000000000044583ff70be4f4327947dc0738087ee755f8e6c8498e7e8217508c +3285206 00000000001e4c1094144aea66535001380db0d6890632411bb86d7ffe0c7feb +3285606 0000000000bb0ec4f6384adc407c6c1e6db1763b17cc4f35110b4ef687f11d16 +3286006 0000000000652bad94e9d53813469a6be043f75819336d5924946c7cbdb393e9 +3286406 000000000067788e229f62cda81a5940ebe1344819b5ee321f3f281b35465f75 +3286806 0000000001472ba8d61b5b090ae55e35384d540f7883c5c94088f6f44bd7a230 +3287206 000000000050c4e07ba8b673843ca69bd4faa40479a12fd2805ff34d67b35f0b +3287606 0000000000391ef3b9541a1885d571ff881ab486df3355c3670f29adbc69b7a0 +3288006 000000000138e48f4c6e6a052eb04d9025a1fc358ee1b34254aa9a7462259e90 +3288406 0000000000893bbec6de492788c21403b0db9d48a2b7cc3c9719afa9b5e3ad26 +3288806 0000000000304c1cd6d4aece42dd71717eea2f5f73919d7c50b9f002ffa41ebc +3289206 0000000000850eb84fef16fdcdc80c0db7653028ff28a6a72ebce9b282b38637 +3289606 00000000009b2059b8ac4db9a6194913db0740da315dff8d5081f4f85704a118 +3290006 00000000003eb4d29619dc2df6a43a3feb17f97085136f4a281ee642400404ff +3290406 00000000008ae86d22266a3adde4ae97ba7f2804cad66878f04592012bb22010 +3290806 0000000000b46a7861b6b4e2240e22389a01eff237944525d158f9861c7bc149 +3291206 0000000000482cb4ba7c74d5ee11d8659ea9c54c086f713ae5c4c8dca9061ee3 +3291606 000000000146b19eaca94b346a6ba0b11ef0bdcaca968acf53d913d9cdecdd90 +3292006 0000000000bf7044c7057c1cd95f68774cd2f280ae641fd3ec57012c456fcec4 +3292406 000000000025c4d634cdc44e249f7a25a46fde5def0f8e602d803446ceef72f5 +3292806 000000000016b73a0ccdf643dc22d68ec9ff96e9103f2a19abbb4c808b765ea1 +3293206 000000000062d2b390dce5142923b00b4d603b543e228e0ab0eab4a2b3197474 +3293606 0000000000c18b14f68189c50d336738de10d9e2e654010f1213d00484964e74 +3294006 00000000000efb4c7fbfe801a113b2746b4ac4c95cd9d799f904d45049212ea2 +3294406 00000000004f33943903d87f6377246adfc5f0ba1e0eba65e3860fd2713dbb31 +3294806 0000000000c64be7e355cca8df13023ee075d83d7bfa462921a6719c1f7bebdd +3295206 0000000001118694e9cf1ed6bba9a80a9546eaf065143069313434b763b75a0f +3295606 0000000000e47199f51a73308b9c49101920eb76c7995907029dfe7cad101f1c +3296006 00000000008a97044313e991de26e8274e02d887b2526b98b9fbda474a31cb46 +3296406 0000000000d159b9ad1e61e455926a69fbe182c4becd34cfd4658461f06e25c4 +3296806 00000000002e26b2ec73301b877a07aff87382fd6172610565f93d1b07466a80 +3297206 00000000005c680cd5c17e6eb8af406033620d892bd9b5b351228d77f23bbc07 +3297606 0000000000cc7bf8ac9ae3bb7556b738d74fa9037a60c68683ef52ea832ded45 +3298006 00000000005f7a3bf04dc98552322d16a4e3a82781537594a107bca73de11f34 +3298406 0000000000b7577ccbc22b0df717523649c20b20aaabc24663df94f6c9847935 +3298806 0000000000f3b2f138fa378afe521fe5994d3a260d36bd43dc7a8d3af63deb5b +3299206 00000000010ecb889725f6664051ed46032df1b867dba3e23a947940d8262308 +3299606 00000000004934d163f24ad002e49b56b2f0597bb2a4fc3c4bd6ee22c3298dba +3300006 0000000000a0d8e3ccda728d1ad1699361c4b33e57f6e5a06b464b747c9d14c0 +3300406 00000000005750c67b48cb111ecd04a9f8ec84273e532f6b0a30a30b8797fb36 +3300806 0000000000f7aedc33c6bd6fe2a131a97eb8ca053e70cdd548a4dedc23a3beb2 +3301206 00000000004fdffae4cb581d29341a64e27f24fefb8147107e863c61a2d9f0e9 +3301606 0000000000c9af4d435ca933a8003761a658b89b43d23d35af7c6188a9aa2a75 +3302006 000000000015ac643fbdfbb36d8c33739de6a2b388ef5e8a639ad36c5013d65d +3302406 0000000000c89dd8607f29ce917a3ad90c88eacad2809f6eb9281476cf7d217d +3302806 000000000104bbacf7cdb566349cdb701046fc2a5f3fd8a56330bb5929376ae1 +3303206 0000000000e5c2c538e70a47fcb9272fb60a8a3c70faeb601b0ba98321d365a3 +3303606 000000000045da118abed7ed3f58951295e854f9111932fee11b4476ce6a1811 +3304006 000000000015bf80ea21ac365061954426f0d3711cf154f31f968fe36cea6b11 +3304406 0000000000167cbe10d473df8999ebf920fff223ed00fd72762babc5c8971c1b +3304806 0000000000bc9544456e9a965861abe3c9242e21f952576b8dd24261899c0379 +3305206 0000000000db8555125d7c6fd1fff051ecf01b25003d2324b1ddecf69f23533b +3305606 000000000100f98c5f8743ba1881057d50bf512b4407b64d37c5ac0c08f86a75 +3306006 00000000010761c7c41780c73dbb029ce11112c0d2b79b1a4a723112ffc47ec4 +3306406 0000000000e6d49137944d6dcb8e2f6b7e074559096958c4100cc11237b4608e +3306806 00000000001688f37c9cd46669bf2cf44635577fe0092ac0681c7c8729e5cdb0 +3307206 0000000000964bf65670b35705f76d20c18f6cf9cdef478965b9bbab37e4eb6c +3307606 0000000000579a0a61d1fdd20ed0958d07e3edf9ede804fe6fa3f65b4dc6686d +3308006 0000000000a3b44ee4f118328edf869eda081acc56318e9b57ed1947b9313114 +3308406 000000000133a8209459afa36d445a2571fabd2ecc12362c8511ac3b2ed7a75b +3308806 000000000064bec6d86c83505bf69227dcd46333ae31b65e882f509875064de4 +3309206 00000000001693bf006dea43d621a1486d54f89c91cbbcd3effd4c25a34d7faa +3309606 0000000000d1b4030647bcb4f9256aec23b957a3e3f6154ff07b9c88fb460716 +3310006 00000000004b781939497fe5035b46c288e5f5bf918a374fb68fd654d45586fe +3310406 00000000002100bcf89b8d62a25d5c6db29dace1a5d5c80d8a9cf907a9987a2a +3310806 0000000000d071d49c47dcff4f251ae663d2cd1455eea633c78221ad046a4228 +3311206 00000000002808fec2c61df6364f8a855063579a548c07614306c7b58a2950fb +3311606 000000000134c9136bff6acf3403fa1bac7941ef1c78eb5c5ae3ebca1408801e +3312006 00000000000137c2ccdacff7ab18960ceebf28119c4374cff3ebe942e2e03bd5 +3312406 000000000030d2bfdde13cc695fc964915d08a1d3bb1b5d6f59d86676c403b05 +3312806 000000000073004368735a0e8bdaa20b2aff86b5d04e8e7c64c054155320722c +3313206 00000000002ffff0c8b35d86bdea6bd5e85be7af5d8dc6bffc1c07d8a234e8fc +3313606 00000000000d32ba85f8626b81af0fc34b70dae09940971de8d47c42ae3ea47a +3314006 0000000000cb1a910672ad7b084c01c4cb62c9271a7cbd675166a57cd493696e +3314406 000000000072ac133155490a9e45b5dde3181f5b596f0c1f938ac3d9e9b77db6 +3314806 0000000000fc47ca03808e42a04b494492c492bd1723865533f6033bd297a325 +3315206 00000000005413b981026ece477f23fee609466f1d630d521aff61fbf79618b8 +3315606 0000000001055ee6d18eee8b6a49e138c14a06d88e8ae0906765e1b899ae5bfe +3316006 000000000090575fc8bd83753582139907599d4ab2043c2ace7960f8d1c9e3b3 +3316406 00000000010091339e1cb214e0d6cb1b424333f0c82b2b1f8c8698ca869a8d95 +3316806 0000000000b7fe2db288d9c0f4c72dbc98be98b29ae378f96f21743f8d7788a0 +3317206 0000000000a87eba0b78a1dd7e23c6b2f89cce6eb2e93c20b176ea3bd3840f8b +3317606 00000000002823133c043a708e31b945e9f921ef88a1678cbbe60a09a0e13a06 +3318006 000000000025044c6c02090848ad6994c9583b69d49b5420f6932900d5889e63 +3318406 0000000000b9a495412305c9840baa113501608934ebd845c2006e656b9de5dd +3318806 0000000000c912f5f68ecc6a022fb9da01ceb554820705f5ff51e721f26e32ac +3319206 0000000000c03289b9e637aaa51f10f25342fbe3eafe8b8e456f4bf301632c98 +3319606 0000000000b903557c9b0aa3c5cc987f845f809d81fafcad2e1b36bb071e7acf +3320006 00000000009429d8a36eea62cff22f67e99956c13e4b32f88f34a4d47b54d0dc +3320406 00000000007748928020230289a92c2d43ddcf5f5483550859a88351b971c30d +3320806 000000000055f09b33e28ffb4a53df1537b5668d90552f81ad15886ed71ed3a1 +3321206 00000000002f8fe03c9672f08434198c3940c8b6a8a5c4e5c052d096a03c634e +3321606 0000000000b755f21f8cdc30a0b861489df5a9a86ffbdc4185368a3aff2c9075 +3322006 00000000003ad68b55aa1b93d598ffe09ab38b4e2f85519db62a6ee3c6b6f668 +3322406 0000000000c2f78f26d23bdbbdf5672f0630cb5f08a83c65509c58b40644854d +3322806 0000000000368407bebd558edcdc0b41acba814851347941105a384d39738043 +3323206 000000000036ab7030f0a2aacbfb89a4d98bcfad973df50890d9f7269e65fff2 +3323606 0000000000bccf24d2bdaa17b161f47a9aeb0da014ddf9515e1f47b2e6436441 +3324006 00000000002dca77c804dbc488d5ad5b42cb73dd1ae0a0616ea2b584c7519ca5 +3324406 00000000001179e3ac2567e33aa677bec1a64463a00611c9b8d6fb86efe19be3 +3324806 0000000000d5919ca500a624f534486aa1ca95e163abaa99753b88782605afd7 +3325206 000000000001474d1d502bbfc6abe56beb2118069bbcb809386a9125317e5dbd +3325606 0000000000dab04596543c623766d3d424c59a3d916ebb4cbd9d4c3c0e2ce93a +3326006 00000000007a484680893e87f933973205f5609141574d2d4fffbe0067a1f69d +3326406 00000000000ed6fc3f32649dc99837c7fa143d9d1b963d7fe3aef0afe228604e +3326806 000000000016420245a907598883b294d0d258da360887ae75bf0db9dfb24105 +3327206 0000000000f9fd6790cfa866496e22457cf1e23bd744683ee7a57d7ba4f50301 +3327606 0000000001020d37cbad3345ec2cf0f58cefeaf159e40b3fe2d2e8b134431e04 +3328006 0000000000b6b0515f4a0362a88de10cd9e9046b53c90035489f9d7aae3b3dba +3328406 0000000000a64f32af1d52d68391fa525189d0e166a150c88282ccb471a6da55 +3328806 000000000003bb00f4677c0e64e76de2682c75f9d528c9278b55af63f8087b43 +3329206 00000000007fa9939ff26352a59155dd17d3ad741163386ab05cac4630f4f2dd +3329606 0000000000ab719f4d0c954851e7bdab9af2df1814a2a551471cebd72c5c4a83 +3330006 0000000000b179c688dba5bc841b774be7f08875e90d4052cfae0448962e5613 +3330406 00000000006279e44973f4c2ce38ee26162215ecf7f402365249ce88b81427c1 +3330806 000000000128ba0d801671784b97b6471f16195779e17b2c59d8b30642ac7560 +3331206 0000000000ca6a20864eb6db06e7081e27941c6c35c7376b27cdd9b53f9dd3b2 +3331606 0000000000b92d2974c3d5b86138a3b1c05e34cc6abf0e1c993cbeacfe01829b +3332006 00000000008862fd543704965dacac3aab536bdeef4f8ab9e85defaef38bf8a0 +3332406 0000000000d1a7207d70d815719c13ee11267d2e326e17a40aaed59baeb9c0e2 +3332806 0000000000ab631d78c668889f7b4aedeeebb8ac81d08ee88593c915568aee5a +3333206 000000000042f6ebb3b3ed518fa30dfb69e5a191875ccf53dd299e7f37dfa628 +3333606 000000000143aa99d5836e0d5acc03dbdfa48373fc14caded018b6d26b757f0c +3334006 00000000005bb4062e71b867f18bc1e739ffa39f7220cfbd2765093e973016d2 +3334406 00000000006b3da62e89635d5e61d0cd320feb137f3992ef35a27856352996b8 +3334806 0000000000715aed6b922cda95bf0791f654adba3413de204861260a1ca3e57a +3335206 0000000000568042067804d9adccc2636d89c26b7735c15048ebca9444504510 +3335606 0000000000d420f32d9f0ac9e2230bec25c84df6fa1d31cf58203262d880fe14 +3336006 000000000070c7af61cd334d68a06be13bfec7a7568203ab89f0bacdacbf52c2 +3336406 000000000087fd5414a2fb238b047579edd4f291b275179cba63b5ec3f4f22b0 +3336806 00000000011f204a138351441f947a4a48ae616b80b2721707acbe76fc6e9293 +3337206 0000000000531b76a786d2d60b375016e3a477b9cdb8e709d6bcff13780e9fb8 +3337606 0000000000b88fd28707eac4523ce3223fe995132fb93aaba8204bbb0ab5459a +3338006 00000000009aa57fc4da4901a438637533e8c24282f50cb3b094a71fad99da73 +3338406 0000000000317b7919ac1426d3c96d63edb897d82339f805e107cfc67100a8bf +3338806 000000000093a2653d7fd3a370081f98b22eef2ab35c3035291af944fc633c17 +3339206 00000000006fe36e3f9f5f81ae58f4878da1bb304c470767aa500d5d02d5eb6c +3339606 0000000001264ff3628eebc2701fa32aca9f5e28fa5044a7a074f71f945926a8 +3340006 000000000088180fcb8c5e72dadabb3bbf69d194566618b9c6a64673b76af0a3 +3340406 0000000000032272b0b9eea36a76e601517c401355de45cdb081353492a4812f +3340806 0000000000efd90977aaa263d16e2e85b24ec7f2b62af5661216732ac32eb3e6 +3341206 00000000004eba27d035469f53bd280b0f7d79e2069e35831a4f8a1d7ebffb68 +3341606 00000000007d2da4302b9d4faf70f9a228738b168ca527e13b196cd28a25efd9 +3342006 0000000000beac8120ceac12054941eb1f551cba0e5949d2727dae77a42224bd +3342406 00000000008ecf915cda360abc4ddfb582a1d1cd118c9b21935c51aa9c80a87e +3342806 00000000003d4526652e45b867d322a81f32b9ff6d80e4cec3fa2fb12007e1c0 +3343206 000000000042021286834a127f5ee8959b0756efac206c68a4cfdcc50c8d1c97 +3343606 0000000000cda675f15a50216e5716c6addcd8d87fcb5d86ed33185c1fd742d6 +3344006 0000000000e2613d48c69a1cbc42acb22dfba8989ad37cc1df43cfaf331eed58 +3344406 0000000000a657a09e847ebc60d210c86ce8164eb31182b6c0e13ec3abb9d486 +3344806 000000000005a1b28ddbc0e42ad30f699a7b3abf016b44db05dca4cc4b41ee4b +3345206 000000000049212b0f4cea322d4441e949eff5625309612009d701af0c402aa7 +3345606 000000000092d4f62122a3d34b48cead2c297a3f2be157abcd5c766cb8f29730 +3346006 00000000001475bde0d493b1442f61ecc94eee54c218cde289f14144070bda1d +3346406 000000000008af91df0e81603987adb15b58c96cfe8e60b12039da302589cc51 +3346806 000000000079126597967c7c8ce18709ee87a7f6868238c88d79b518a81c8ecf +3347206 00000000007029614a7b5c18e58a9e7ab39545cdf5bcdf2bcdb351a6f6ea2e28 +3347606 00000000006c56a0508a7fa6d89d4d942b750b0b5733cc554e7a914671b3d5cb +3348006 0000000000687f4b5d28f629c318b881e7fadf925397af9eabb0f75bad1e691c +3348406 00000000005ed734e269363564a9b7dc6f477414c1bf5a84b8891492b8c09790 +3348806 0000000000356719e2154600aa7db36f3159afd410866a732ca490de47ab285f +3349206 00000000009fc1431997fd1f95ece85ac9ebc39cd1f9d400639f46fcf6aedefc +3349606 00000000005cb3085cfbfaf97484b0749991611f83a8f8625dc165e4508dc1fe +3350006 0000000000953d8e4c125b89407f616c2cc4915661f13edaf5933ff9bdc0d184 +3350406 00000000009a0a6dd6a3a5715389753a1e74d98beee75147b00585186628b328 +3350806 00000000004330b7243e0c9812d75502af367ab16b85d9b27b6e6a8cfb97b3ba +3351206 000000000002c11c3f8e1bb570b93f3d22a80ffa02c14648bcc418ffd4fb4098 +3351606 0000000000137a79d7e7f6a5bf010a54f6da39e21c77b2a6e9599ea9cdde6661 +3352006 0000000000df38f19d744a3a31089922e88f692ca8fb912a1542e2cfe1aa104c +3352406 00000000005494a746ee0d2303caeba8513c78eaa95c026fa271a2eee2c84eab +3352806 00000000008370bd41a9806860d77a0c8df61514070314deb676301aa6a217a1 +3353206 0000000000d0a9ca5cef109d8070524d6d0bcf2cc7e3d66630f34ee2fee47271 +3353606 00000000008b5211479a54c24191ff237efa3d39bb28b1cc696195eeff5e5605 +3354006 0000000000f9a7ec9b4f97d10fc44eed67f91d59653112cf74f3a3e5de057369 +3354406 000000000114f1c2c6c01d7c7fca304373594a1442f839a3d0082936cd54b0d1 +3354806 0000000000ae016f87c7409c70df10572eee53aa83397bc3845edf8843894dff +3355206 000000000000dea275a9a0b7e0d8a9b90ede78f6627f6167cdf1eec46eb50df6 +3355606 00000000006541e9c13a64ee7496d2bf5271597338d4737462b2813e0e3a2ace +3356006 0000000000397a35138152688c52c38aaeedee50890ed6a0a7a977984d95bb3b +3356406 0000000000bd9ad89fb8dee77e1fe2875b00f4dcd8529076b33fab305575fbf8 +3356806 00000000003baa2b8cfd5a9e85f232e1ae09c2331c50a2901805ecfc202934b2 +3357206 00000000001348e0f50a43b40999a73a10915e766272faac75474038ad37420f +3357606 0000000000c405c61199914429eb0a2ae5d7fb2c64775982c9283f2f1f2324db +3358006 0000000000a4bc547a096ef2bac1f31842d514e2109c540b02384c9e62532ccf +3358406 0000000000164ce5b7ed479c916b6175d8d236ea8c94cde62c6a57cb26c5f3de +3358806 0000000000996c5afcb6dc5bd051d48c0a517bd467f2a3bc70d6443686e3553b +3359206 00000000003ec4b78ac0f3ee9d336631c50fbf9b98ed2f262a9cae71c69a94d0 +3359606 000000000010d02b6a2a23f3d8fe543020e817b76bd08ab45d977595244059dd +3360006 00000000004787bc4de20dc3bd46d81456b1fc4d41585db27e1c5f1b77d962c7 +3360406 000000000085ef6a5415009b5e5abf4a27526a8c5a59794add61baf5b052adbd +3360806 00000000004e6eda90fde852de36116b8d8e35d61fca30de85f7770b47459685 +3361206 000000000083f23d256492f3b398266eb84a71f2d41e3fce7e072b5e8a6f7c82 +3361606 00000000005381b85014eeb596e917cc639af77f51654789e703af575089ebb4 +3362006 00000000007e794a3f9cd3af899da664dc835d9f239e3239276c0793049903c8 +3362406 00000000001b05ec945facf2606d1fc7a43bbc37268005f70a7802a687ad598d +3362806 000000000046dc17f07b66a8b319302a627def46861be5daadadfc3a4c9cbe7c +3363206 0000000000910232000e9bc22dcd11f53665e0c5becfdefe956bc74e09572d51 +3363606 000000000049c7a7fe0666ea9ed29f8a44340d3521d931903c40d66b013fb031 +3364006 00000000009028a1837137e1e28d3105d227ca8ff1b324ce9adb2d158e069584 +3364406 0000000000a92aad728673e66686bd4f548a465132a35373ebb485267da5ff97 +3364806 0000000000e05da45fe1527347956949806a418478d22d8e31de278df274b061 +3365206 00000000008c2a5e8e4616600f2e2b70df6defc17564e7a56d002456050a4f16 +3365606 000000000033a54fe1988899ed9586bf529a7440fa879da8dda6a3858521ef01 +3366006 0000000000737f3410c8f497c4fb0f4d9f0349c4d27009434530694042461ea5 +3366406 00000000005434ec188184a8baef9ef7aacb056911632429ec5280804a8a4cbe +3366806 0000000000248c8ea2f64a23ceddc930eb9e61be61f30fdac0922c4b68e76ee8 +3367206 00000000004fe359fad86c3507696c67c64873ef30a209992e35eb47f24502fb +3367606 00000000000655e771daac0877edd22fc24cc8380dda4916ab78081cdde4ec94 +3368006 000000000007c78ba4f6f09cd1dedf036ce47ab9a8b0de494a8187381e07337c +3368406 000000000005555fcb2a5cf274f1da736bff4e29c922e87c9bcd315b44c781bb +3368806 000000000069b1766903232809501143d1eb4a10d077d486895e9aeaaa604c1c +3369206 000000000025078b215f90bd867375f43aa0771e9bb73fe5dd5a6109fbb2ca66 +3369606 0000000000362a07c4d35658ecf2342547fde852de394c654f5796b12612bf7d +3370006 000000000015a29401513a13ee612de417aa0cf818a60ca38048d0f097a308ce +3370406 00000000009b0a978954d36d59a8881d632b23f132c1e6656fed81a3ddbfeaa1 +3370806 000000000045377ac6ccc590f1d38e4864ec59d7e6e0c9478ffa5792631d1059 +3371206 00000000003ccc38b59cf70201a928d797d890d342681cb1bf11933f48d480d8 +3371606 000000000011d8f6c43abc2dea60d92db2de284cb84a3d37936a7c9acd84fa77 +3372006 000000000008f6835701ad10b28d5d43b9d7288ecf38f80cfdc489d7cfd22a85 +3372406 0000000000516811c71281cdc04d29519c555ff7146bc806e24cba98d5a41262 +3372806 00000000003505e133cc2ac2ba23202a3904f6e95d396fe744faad83c1d1d5e3 +3373206 000000000041ebe9d09fa7907759a20144aa0176bf6af4dd507aca270536187d diff --git a/zebra-chain/src/parameters/checkpoint/test-checkpoints.txt b/zebra-chain/src/parameters/checkpoint/test-checkpoints.txt index 2b770632b36..0f23ad62142 100644 --- a/zebra-chain/src/parameters/checkpoint/test-checkpoints.txt +++ b/zebra-chain/src/parameters/checkpoint/test-checkpoints.txt @@ -9734,3 +9734,411 @@ 3893200 0001ef1bd843a633754d150a61fd0ca05afc04db5df48d5e7751d0ba12b22b1f 3893600 0120511a385539aa8b9e4c5120e4ea6bd43626d2b924d955c1b8e257b713dad3 3894000 00521a22e43053c0a98771f2703d9be25b38c253a602e2f315b1f0d254c0edb2 +3894400 002f445bb48152d859e05ecd7c07f401926b86171775bc36a03ac5120c73e61f +3894800 0056e556bc300231d992da4116f2d3a5f94e20c1f81d949510ff9a8fa2b3aa0e +3895200 0033e77d8fd8239c9c3afc7ebed9b433414ef8a2d8c076a307f2b67630c064c0 +3895600 008488cafea5db7c5768c71af69dfd80c5d5fafa85a80eeb1a32e61cdce33ec4 +3896000 0022c5f10b1d3a8c0cbfb80d58ae1386d781572e4da739eb47a0e024390a4f8c +3896400 0006f1c1dafea02f54bfe4852c4e760c6483fbfe2a4ac5e2eff4ecf384f77c42 +3896800 0135866925abd7b72c3862e7b602066167a7f8192935352982763c5843be15b9 +3897200 002703f302f8fef1e0db105445c123f032b61cff110336db2a0f10c78468f9f8 +3897600 009fe2336b2c9e5b08e1e55fd36c5e716f2c6a0de20062e4c9761c7d86fafe50 +3898000 009c6f5e111de029adc37a9da14b0c1522108de67828e090aba2e85fad763203 +3898400 00fa57d2a8ab3827b1d19f570c6f38418e2c84b946ac09909a26ada1a9e97b50 +3898800 00acc98c2da2d8a5f37bc042ea7112a225869221aadf152068c74e2e8adf5e46 +3899200 011692b5ea42760b2a6e59768f3f16238cae5008276e2bf606c87a4adb04b1c6 +3899600 0176c4ee37483b5eed0ffae22e8d20046253fea988a18f63a592398635aa6e51 +3900000 006aa8bc0b1df017a938c6122354012a1a5614c75dc9fa679ee087448f120121 +3900400 0010f94f47473f407859da89e5443b03b4e2354fc174860d4d4e5a9961efc505 +3900800 0010621a62c0e43d13d4f981aebaca2dee951a8a7e3a9fccb3b0abb0ccea6655 +3901200 0090e349905a26278f67af2a7f1e7ea654ce868fa5dabe30fa7f5847a36ef1a2 +3901600 001a92c2a014a81a8996c23e4b106752a68b734b8b95fddeb09e7b5752c2e8f1 +3902000 000afabe2f7f291ec6d565d4bf9a49fa42263e19122918d34d9ca69766608aad +3902400 00037f6494add7283d427d49b2b3ecee778937234a94c12fd57ab83510de104e +3902800 0020f7a9a9c49dc14b142039af3aff40b7e1db2aa59c7b6eacfe3e59853e279e +3903200 0093d522ce28d532f4a416480309ac1461b93b60c0c4fbd0168372499a5a7407 +3903600 00748afd42171aeb6f81bcfa5636174255be50b114f9dfebd06600e67978d146 +3904000 005ca2d73b87e586f31eb8f9869a18d3e2e7eba520cd4020825a9b8d5d2a433c +3904400 00494c322941e8078463bdbda11fc78b64f1bbe61ae9897395894453a8d1f79f +3904800 0000012c69405f40485324c044a10fb902aa1299242002c8d209ce4134d3e800 +3905200 001f98a42364c17b289adc650f7b37eac6a58a904fb9f3e28ff87a4db2a456f6 +3905600 001a7bbb46f9a0d5e9fbfa5f925208f10fa38bf89d2dac4d540755c65e14e234 +3906000 0047a6ce7d78e549ff74fcf9e8b2d31f1fb3f424413598486b174af7fc86b76a +3906400 00036ffbb4892e3368675f257f91feec572a4b1b5b4adec25941279c3e9755eb +3906800 0010823bdf6bebfaa833b88cd23e2213ab758f09456a3e85d1bfba9932a5face +3907200 003dba85347767e2657f19babadbc7149a79fd68447e8d2e20d5f6471f6eef0a +3907600 001f8f61ac826851d6197a2a01acdca463c3bffa91eadf862470d03afbd7e65e +3908000 000ff4cde503f061656daa6b92c42825a8a28ed9da70c9507794eef448ea8218 +3908400 00b62766e529cc995ce56d00e4232fc2bda20321de08dec81c5df490d7488a87 +3908800 000027bcca91d04e287a5d681cba0f3698a1e95911a473197b89c588746ad41a +3909200 00be2da57c2eac70c7daa13a4434a769eb30e99a5f2f6012c9ba0f21ee9ff3ae +3909600 0014a48b2f02d249b02c0dccb23e4a3cd3c177e85e9e12a578b86394ac48ac55 +3910000 00ca2e4225a9d7b0bb5f61f5a87c9304c9a37b81b179890247b30aca399272b1 +3910400 009ca6b9e62f54a3b8f53bb6954755791fa33a20238e0340ab307c726475013f +3910800 0053cd93234263e32bf046c376d8dc6668c16bbf22a5608040e28be4ff96d149 +3911200 00c916c0b742f4e36bd9539e05e38fd0dd3e1d38216eaa193d6486733ef448b8 +3911600 003cd9e81546f70f7b4efb4347cd4749c82c4a3da407dbfcd32d0b7fdcd38438 +3912000 00d03f82fcd120c88c432c034d8b84ee2dc7d2cdbeff2703aabec79eb32e1455 +3912400 006685b7f77950a5057e2e5e32341c644f8553eeb5858ddb6346a51ec63109af +3912800 0012c370f11820fd8251f28a04c90426579fb6b794b7ae30d07656e2b6860c44 +3913200 00068674277748a1b902adc4f8686ccab2f6c080a192ec1ffc1d6068141b975b +3913600 000d9a1a1b7bf48a535128dd839195aa51695c83410fcd63dd3c6432a7ead70d +3914000 0024f357b64951950dcd0238eb81319a7b169093f984d4f86ddf29d038fd7c9d +3914400 005b9e7c4fc235c045368cad10eda0e5624bfa1a5ed8fb01ea258334bc80350f +3914800 0125ef3510d51f2540c372390198c1121a78bfcfa0a01c36dc1e825430592e1b +3915200 0059b4805aceb4e1937f48d09badec62545520c6963630415a705a0b325f23b9 +3915600 0096cb7f3c9ecf90022d6404a598d7e6b98b3182c7df6b1a37b0660d27714b04 +3916000 001cde4d05d2a8a6a60e93b29c4d32039f0f01d8b74448c0159a7d5ba81e3d8a +3916400 00930808d04c9770c3a73a5e9e6ef78b4591bf38f678543357f53fd7aed4dc50 +3916800 0082ee56ae98de71bac0aa5f2f789c6c430872364f74ed677347b3069d9d5a7c +3917200 00455d20023926cac998aeb754ac7f5751c11eeb6e3e07b74ab06d49330a5510 +3917600 008cbd4c1dfee8c6935273397ea61ff63b0f5b1cb1ca1fd88b7d05332dad9ddb +3918000 07fbb10f09fd125f3cb784faf73660eb657b14262a1c62fd0facd179848a6ba8 +3918400 00399a739d5814b8e129c185762d5bd9d729c48f9018ff8bf50d03d2e3ec7b61 +3918800 0093790175f7b9873f14a0abbbda901e7956354ad803b98df78bbd572de7838f +3919200 013aa2554fb23807cf98e1a45c5797463b5d3e4eb81d05028505b284e1cf1063 +3919600 0059dc487c38e389436a6d0f7ca7535f0fe5826e5371889b5d414193dcf16a41 +3920000 009b44660280d13d45dbc964f6a9033786070da3174fb98b7e2b752425380811 +3920400 00e07583af944ae48d220fc14ba20620b34d1286a375d81759ab77c130fa528a +3920800 005865b2d9b7c954a32dbaaf019d7e566fd6d5ba26fb9aef3a7e152a8373b807 +3921200 00d4ce21020d0d563a73b0f23e0789db4aa2aa7ac1bf18fd5c748d1bab971dcd +3921600 0095468018944f71def6b17a39a1763ad3e33d97dc2d00deeb474e6aae09ddff +3922000 0028d6b23237030eca312c46c179eada09d945186339ec24aa3d91e428a1725a +3922400 00405f8b96191d2d8a4be07bfb08651aaa8eae667f621e4007fbad4d85df20d4 +3922800 002de815bfeeecd412fd921179bdb1cc7e597ee3a12e9242cdfd96087887afd5 +3923200 003baf4f5dea61157f3b97ddd52dfc9f43ff7977daf8a0ec1a5d933cf3c40678 +3923600 0001799d260e735f42bc05f0fbad44b9821390b2f3b5295a44f740fc59b775c0 +3924000 0049962008716e925f02170bf97f52c67ce9927348edb7dfc84d0c8e08b8ba27 +3924400 0000060cbac2ec7f7b16ad29506c187efabd083d72a9723ba3d8876cb1377db5 +3924800 000010257292e63295e1dfa7e032c2fa66f0760baabfc587837df510191b1798 +3925200 00013fa0172803e49046583362166b6235035621564a4bbc378436fd31a904ad +3925600 0003aac864a1a68bdec28cea1ac970f05a444873e28cf0bd1717e6c67127b1b3 +3926000 00000f069b455a6edba65b01f6e3e9d3218622e5f1114a9d794788e201bf3a8e +3926400 005e5c5af51b788b9b7526bd39d67345a1756c14c9ca0423e7a1d76fecaabb36 +3926800 00f3492b2c766424d09384291ec355a0887d4846784dc4820ed98a8bfd69ed5a +3927200 0029b1d2ca10a1c58bb2384542716149c9ac57ea0468dea2dc50ffc8c25d3beb +3927600 006698314afe35bc091d5ccd16103c45c0a683969d241a297b729875466c4afe +3928000 0053fa8261794d1aa2162e7c8ec36a91877ddae25c50c72b319f6d5f491e7abb +3928400 0002f9f1106f7eeef324ad73f2600b6b821b8e1ed52fffed06ed48c5dea6e40c +3928800 00765e9b9d75fe17c66206fc545b008abd874104d75325cde91ffd5e960e5db8 +3929200 007390e6329bad1fdf4818211d9d5c9935d0a4eefafa6a77db6c65bfec711f2b +3929600 00c161c0d917ebe92ff71e615340b8993f1af0d093d74a74ec662c9a4386c29f +3930000 000f9483b5f68551b99818cf0df712912962f8c2de178e06b6a43ee78c89f8ed +3930400 00e37e4ddb878da23100d07bf6e8ac97238584708869a2b2d4303265a3e616a7 +3930800 00ca20e3937455a9218d08187a5533cbfff5a9f2626ff14d54dc349531b01421 +3931200 00332cd3bc14ba3bc0027ead19c0d511095c4d86e3814cb85d3a89985dc557b3 +3931600 0032c22c2dfd4cd7889c5f1040ccb29f91f0075b91aac6fa8c6562fd1a54a38b +3932000 0037b64298816aaee7d70ffc4811c0a5f46f903d7e1c67ee61711336e6901c7f +3932400 0075487aec7493251c730585eae6fe2f832c21d8c58d29ffdb88a9ae64c95f33 +3932800 00a6693641a80e1e055399d0242adcafcf84e26d821d631f616e100d7801cc23 +3933200 00ce9dceb7503984a8ea58b0ecbf6f925580883a3450f262b1e777f6eca0eac5 +3933600 009709318e9922f0d7696c6dbe1c17397e9a0fdcde0d03ef554caa7c5532f509 +3934000 0065cd6c3d996d8de8885842dff7d42d9dd768acd8b8fd26283c928e08edf26b +3934400 005f4a3d6e073fa8b8e9824cac96a3afa14b612fde90e2397c6f198b5ffb1756 +3934800 00329b32e33d7fd9f3f72579de514ecbc6e0f53415528b5d8b1ac1051dd17e85 +3935200 003a0a984cccd07920d1163df948e8cdf2f5c1c5b5cfc80d06615583166f419f +3935600 00345d188be862694015414225a73af1c76fc50d960f7c971616946c96234b4d +3936000 002465e25f3619f2144af9112c90f0bb2671edf9a1d16a67a374397bd9a0d0f6 +3936400 0046ba264a3a87d119580673688009eab8affcb7c9a589ff0e789a5fe03c25c2 +3936800 004edb4fc9b8a2eed230c4471088955204d0e62f0e7055039cff43756aa6b97d +3937200 002824d419d6aa7bec92118b6b1a07284bfa412ee23c6d5d3707a5cd7488c85d +3937600 002204817c7614cac0e9070983f1e41abaaa210ab520195b07f323bd21c1d8d2 +3938000 0035f5179af2808e9a27c73b951ecb13b2a611a0013907526cfcc41b62cf03de +3938400 00239af8dc6a047fcbc00d7e8be5728279469f877c0879dc61e60d759dec60fc +3938800 0019ef076af73b5cbcbc0a14e36d8a0d87014af1c687acc155e5ea99fdf3db2e +3939200 0012ccd1b93f05b8dba522c863505362d5d1929078c934ec9ea52523dd900a7b +3939600 00000328354fbf358b05eb733f6a282a0db18c195f97bf4b2355ed15a12aaf8a +3940000 000001320ad6accf6d3a8fb193c4646e48b1c797f3d70175a4c7b5499054b9dd +3940400 001a0f9bb95b70634f1cc9b2388e2914ead83fc04fa05ac2e15241f5152cb7c5 +3940800 00113927ce21fd18d9475753aabd41e28f02abdc3d4c45f0c9662189ba4eb853 +3941200 001c70e777cdaf88a720cf695ea191a6c412600de2dc5034020799fb92168626 +3941600 008b36caf3ef5d9c7d1c4c865f9d8022801453948686e65fd2ad1a9878e0ae5e +3942000 00d307d1e742d43064c8ac667c86e59a09890a0dd15212009852bc798cd3d1f1 +3942400 000aff54d29a8dff64c638d7dcff9d1d7cc343518465f6c540d830cee7ed3ccd +3942800 0020363933d4ae895bbc669fe7ce32d3b07be049833f7a6e9a7f40d28e379340 +3943200 0017527240682bca1078faf27809eab717fe0b3f38d65f0053a9f06121a3e8c8 +3943600 0010efd59260c9b7ceb1990ab9305eb2a4944028ff3bcc91efcf2f2ab9cbe9be +3944000 003796b26ca3bfe585e255d239b0f56f89e9e5a398fefe7c99867e5ff8ed02e7 +3944400 003d79abf9476a26472fc191f143f2ee5b1c67b93994854d62f8e4635e2131af +3944800 00243bbf8d1700bf7c491985daf5b0cd0e6a6f8e9fda30e6cf79e639e6c1666b +3945200 004a7d39a71f2895c5c6012a3d83b663c47f15aca924d0c746dc032c998bfa72 +3945600 000e1391856979ddc908a4917d63358dd22aed6cf352bad45fd68c9193203474 +3946000 001871dfe796d22d06b6f8fa9e8ba8f5a59b2d93fcbb45173530e8855dab01db +3946400 0017d868be768b5d71a7bc3a940c0931a8a88d48c5eba890970f646b8c3a4d91 +3946800 00125f4429e998188a4d67f868f2cca3e61da977026464fcd421a87a3a79150d +3947200 004c359b74f0fae9a54163dc04a84116a50bc0eea4767d6e66a10bb9a01ad7b1 +3947600 003bb904426b36b98987eaf2ba164cdb7f1be24949800c43ad6af469c045736c +3948000 006905dcfa33f5645e55355334f56c38536bbae2ec671878f62e81d122ab75f9 +3948400 00069bc0f522ad9ca3794403d609f677ed688e255e1b87b2bdcad76a46bc0bc1 +3948800 00acc4302b487c2220f2adc65e23df667063279b7933dbdbc7cc3870cc4969d5 +3949200 0059897881a2edd6fa27cbbf7239faf33bd9410fff259b839386fcd7887b4015 +3949600 00078d4999441c5500c7b6ad65104a720f3cb858013b80c3306f2a4d0198b527 +3950000 0058d592f4e494e309bc3437298c16ba7303790b6432bf7873047234e3801465 +3950400 0027a456d261ce08d0f9de0f40321523bef5ff06880bb0d3a314169b3b73ecfd +3950800 0052a73990f3dea41ade1b98222914a802ba7daec54a2ed6bcede74ba969bb43 +3951200 002abe1e8604c8071e5d1824423c8f9c0ccf7b95e60e3639b99918d58d34057f +3951600 004bfe7c47a471a748766978e82f27ce4a8351dcd853576f61fe9f3e5a745439 +3952000 005a04cf5095dfc9c4313637c47b4dd1e40fc23cdd44f105a8bf94a7778792e6 +3952400 008a835d2fdfdf3c61a23648d8e8944876f3e0055b39161053834de0324b0150 +3952800 00438d33bf09bba0218e1d2b2cb7781a66f0c2eb67fff9486dfb85dd74f3cdbb +3953200 0025616fc8b6472a8b455264a99f767f2601f862d53fe4c6f5e257618a32c7c2 +3953600 003421452de4f19fc8c29688424a11df098cf81523e03a4ee43eeaeb2a6727f5 +3954000 00172bc4c0dcc5c00a2bebf44d7439fe1f20867ddf35c2dabae5d14fe1ec48fa +3954400 001eddbe0491f0cac2b0b022489c9c361429aa6fb2a753a79bb1d0f3779f1064 +3954800 00278dbe4112700b553f0f1cf2c4c1d6cde9703b2f6d6d8f66996c99cdf4853d +3955200 0019370c17c95cba6bb95e4d0a90669f0c9d676b98245fae24b86c3766f28232 +3955600 0002c217fca4b6dce0e8d9b404dcbef3fb7c1c2bb6e50005c3966e8d2af8d92c +3956000 0022f6e795c48d58e7bc59e80c74c61c174f4fa9d09ada8373f8e130c4fea788 +3956400 001c3c2c2ad2da057278d03d1d6adc836ca56d0dae4fbecc1f5eead467d50b4d +3956800 0002c8d79f92379641c1e1688e6e22c8f4b0d0d5c62c4ea4c397bd165d68c573 +3957200 000184f893ffac04c533c392780ec63d28f60a4cafbc294d1ad0472950860d58 +3957600 00199662dd691b05c8b6e702ca247e5e278e044f8941ea62dad01fca88ec2b8e +3958000 00007d45242934e5713e22d9427e119b9855766017b1f505259ed5efe3794ced +3958400 0000b2b931d78489c16874ee6db3d67af15f03e36c02c73f54cc681a239fd7d8 +3958800 002231e83a58abaffe20a553d237186f91add29565787d82404ea5f627ee9d5c +3959200 00df10032bc1c3ae835a547e95ca20ece561bffc9186092524dd35cbdcbe5c88 +3959600 004e9d045f7de4a03ca106b17451d90cdf5cd3e1c811b4e3afe013d44d1a579e +3960000 005ab089d2026411526991464dc8be43a70d2cb69a5055b371225618f96db8ff +3960400 00000023a1fdef8a40b6ea050a674c176df2c29020099fd37624541414110aeb +3960800 00001495f1d6beb2e71ebe2caebaa1558892d03ff23e2c1a02845f19a359f185 +3961200 0000014a96049807b1eef5bf2be1fce974ca737297961f1387e4b4d548804bc5 +3961600 00000f4605e24f4a2afbddcecf2084006f75ae434f27da9e055d7bf6fefb24f7 +3962000 000004a44a5adf551c59a5df5099689f3ed9e0c3bb093ec535332ab27342ce9a +3962400 000009998a0232ea68a643eb47fa521a8a1bc58052516c938c73d3835bb4da3a +3962800 0000075e6678f9c3d9c1df0a0c2fba94db004cc7e67f9ae856ad2550673e7ed3 +3963200 00002d2f3fb9c7d4a298b56cc16186c776e413ec2003a18f5efc9b8fa531f194 +3963600 00003c13f25e08d226ac746f27a9fc47997e9ae19c6accc0e1136aff4f394f1a +3964000 0000102abc79feb7d80d2887e192b43ba771306e5c44a274ca60d3f8390f38f0 +3964400 00f04182960dae520ac4c6bf31e9b5377bdc4b40bfca2ef5eb347faa97ba7c5f +3964800 0044798cfbc55086d4d7e1c797ce9d778dcdd25b2fc89c9bde7ac2b91ca68436 +3965200 00000d63292685d5141c0e73e15ef0247016552207568281f3422009b13f2a13 +3965600 0000007bf05b719cdcbd2d943e1afa6a28d4436e21b37643954e86d10aa201f5 +3966000 003c21501cc4ace74c853ee97c98216a20ff3a53be5613b7e7d28aeb941a001e +3966400 00cff2216ebd2ea69d320b7667630fe8848e3482c7ef5a22b85b3da52af7d467 +3966800 002caa73f8fb7b2f57a94bb488520563352ddc86249070a0c18f9d866561a8bc +3967200 00d380ea03cc67323864ccfd3ef0053b83c01912f9f66c32ce00ab803d5fd469 +3967600 0028d29d1269426639bd856c8a2fa9aab708f03f236849575f0fc79c065e7863 +3968000 0091f51177cff918e05e19f2c3fc00f34c057393e63136a5c13ae07c92dc15d3 +3968400 00ce7cf59f4f4c30f15b3c0d117112d46b9e4ac0924a37aea313587a4cec39e3 +3968800 008efc4f1a6618097b70657fa4a98eb7dd954cf28705ec15f18aa53a8e353eb0 +3969200 00dab81a3343c761521494985c1d5029606231328fa0c7cc5469152c44a366ec +3969600 004db0f871ee3b3ecd7ae703ca198d128ce9479500f82c04ae3be7685dcfcc7b +3970000 004c678f7c194ac268de89a2bf26079e741f046997cc3e8cd16198b743143c92 +3970400 00400735865d342d79cad6f8fb4eecb6bc399aa36028660206d1522d9689d809 +3970800 009fa861a083303e6f8f353581a1e5f4447c1bbba9399a3832b4e40aab62f25b +3971200 0095c3b109ff8d39e3fcfdca9b287976308981c9004b4d8bf105de8eaff9bc06 +3971600 0078823120c9c0d584c90b1eba0bd96cfc6a2576d62a42a91fae82ba3b40b660 +3972000 00eca3d4e404cb9f0fa12284ddf07534ffc73e965fcbb2b426c67274746293e8 +3972400 008b12329198fde6539c9b21f3662ee3d53b6ea8650556cf29bb2190deb4746e +3972800 002ab18c21f0fbb7569e4c460207e530f1d597ef9319f8f9b5a3644f2132c21d +3973200 001bd7aac7b08deb020284c8e36ad59e1ef2a14c682c4abe88d846d22bf698d5 +3973600 00b7ecf26ff2ee86630e80b2e7ed0cae647861dd811c56f5c27d2abfbcf709ad +3974000 014a57389a0c6f3ea8cf0ab4bc36e6f8ddbe451ba975930c8c107d0525717a3d +3974400 00372db21164350280c27d6c1476be7dcebd6a8bc3e3b696da5595bca13696d5 +3974800 003887560f18ece574d73b69cf1a04b2cfca4ea0eaa355b572be4a1ff894564e +3975200 018087ad01b3e085c723b63635e76390437bbf337096b40e931bbe781ada89ae +3975600 006754688ba95acdbd08268016f30271ecc8f08de91a8d1cf0e09133af5967d5 +3976000 001ce8f4236ffc89ea4fc6dfd7d2606b74917633e158988a106427ad06fddcc0 +3976400 00fb1e095f4d6ff952128b0b0556aaf82996851cba3207fa9261f169f8f1889b +3976800 01ea45350fabd54b918f6f584c57fb3c5cac1a0fb2567adb84c957802f9aea1f +3977200 00e316074fbbd48f78bfa84b6081e0f5bf77f255bc5bb8d5bd826f3383740101 +3977600 00c7149c6a7240c35c9dcef02918f8809ca14fa5a15bfd6954fe8bb590c0de8c +3978000 00005d7a0d7cd0286a1591e1dc9f6807cdc7b1f163b3860f733373104b710d4f +3978400 00007f2a3d6044a85c886c4d1a4104cdf11f8f5f030ba75767d00cd3da5d2918 +3978800 0000038a72a92c27d7345a2f5fb4e3d6db36fea144cd199fff78821930431898 +3979200 00000b3a60f2dba0a065d80168ca3cfdb4902ab195d6cd0370b3dc19eb1a89c1 +3979600 00071305b3b473ff56954f2efc134ffee4880aa7d6a04746113f47bb1eec2b7e +3980000 01a9ba1af5d1919780348216802d811414066c6442a2675db806cb05102a7bc3 +3980400 010572cd42d00a1be1e4ffe8feaa34e32575461d815f1ed3441bf7b9c5ba52fe +3980800 00a085950c299d7fb3375692e6290dc552027dd583f9cb52d7564bb199c89c0e +3981200 00fb3762bea730e19f107ff8267c6157656081f94bca55f842646af6d0f73b33 +3981600 0003a4637aa54f35ae92f19a2c2ce61ded90782c5f516594f011977d6b010230 +3982000 00229b0793fcccf5ed96a8ca87f04f20b741afe51eb373f140da484947dbb523 +3982400 005796feddeb9032ad51d965ab6c92f7f7302c569ac29f1c313fdd79982f3eee +3982800 00ab2b4c89d06f8fcd545b85a8969da7f3a789eed7de420c65b0f91ddcb6c319 +3983200 006f87ec3218d208bf3f89179556d56d44632489b2c1d78dcca2953aa1c8edb3 +3983600 00b440e14f5e89410213314e979dbbf4b47f8a49e23932eadbdd9019f94ff995 +3984000 012c99003abec52daa41ffb7d878855c23534c8da90dcf65b83b912a6c87c902 +3984400 0119c9c6e49c0a2808d11328f30fa61ed2b5f851fd4870eff6f18c71e5df765b +3984800 00155c1950abfca5f3ff6e61484e45df7272becf5d7e32db7e985ad8ad514d15 +3985200 0129ba804c087ea007b994507ba3d7b69ea0fc1f59dbdcb636132f559ed0ff8a +3985600 00cd53bcf7e11cbcaf9b01b44eb940190d3aa6d5256af04ecbeac232bb880d6c +3986000 01021c1c7c050fed6f233bc2c742d7284342d253eb4cd345e527a8bee9c44dae +3986400 0045eabb89554f368224495fed308dadbb9166d798862291d6ad0649e3d70a43 +3986800 01887d0715017b72a99faac0adc7d373d1dec38f0239eecec9604ddebe4213e4 +3987200 006484990a507cf295fce0313627db64aec1fde713a79f70be3aea3e422d86e6 +3987600 01f786bfcfa09caaecae7c89ec627fe1e06340ac619518f46750499d56c68368 +3988000 0097e24c6e4c862d7318f519eda10397531fedd8f80a3862eb7e356e3a58b0d9 +3988400 014c9949eaddae4c96a1e4044eb8831483561ed05aee79c230ffee9557e523c2 +3988800 0031d62edfbc6243578e55341e5642539416222beb624a37df84ea2dd692f162 +3989200 00177468ccbd4962d6e10259e8e78448242b3220a21955916b054e918787d5f1 +3989600 00306803c1df9043f88a4d0a2fa66df139b498bd90acbf2630eedabd824e8207 +3990000 002966289beed4a27ee7133f96f431a67082bb270cc8b4fa83ea476078a3151b +3990400 005ba8e815c58b959e46973003876a4a3f4d67d8b212270135b1051de10cf519 +3990800 00700deec5757db5d78fddae4810cf7d231f0e2ba70492e0b9ecb80cec8270bb +3991200 0035ce3cdc8711ae32352042c60011e56d3fa0eb07e758b8b0beb7ef69a1831b +3991600 00c0ee608a1f5648182f235adbd7b1b264a3c2dcc90221b7ab9aa90e0f83d7ca +3992000 00661e082b211d4541acdcd540d985ecb7d7a6c184eba5c51a2ca19680b462be +3992400 001f3074ece39622b187eccd863ddd561a7ce93e09ea83a0ed1e6ac22af05061 +3992800 0050e0ac59ca790bb3bd0e9c0d94233bb464c73725260b6ac8c33d863c2e95dc +3993200 00545f3ded94b60dfb284255dc5e35819573c3f6842cb9aac702a6b8e4254f67 +3993600 00ef0411f385c24ab162723b0fd98fe51e9fce32b963b57bdaa8bb6a71009bcf +3994000 000399e30c3195628397ff15efcb5e1b43334cefd573501264e7ec165286e2f6 +3994400 0097e37dfcf8cdecabacf33b386202c10dd674122a16558d138e9baa32fea1a7 +3994800 00158040dc4d7eb9b5345a80b1b160c1d92bf312dd4d25ebaad53f77f1965a4b +3995200 00aeab06fd422d2ed17711b9ce21d62191e4c9fbcf0ad21d9a2333b5d464a1c2 +3995600 013dd1823e642d1f2bcb94d554e5e90cb1fb255a80e0502007d35f05bfac8231 +3996000 00619963fc124972d6aec89c3a93615b4e846f89d086e29be7e47064b74fce64 +3996400 003c2cf8854fe94f19d13452d5ca355b6d3095ff6ab871f83f0a5016c3faf283 +3996800 00275e5cc55ed19203eb0f485e4bc9a46233175a29b2f9331139a4bce8eea1e3 +3997200 0154b6f263f8aca36dfb5b61028602ffbd57ba102de73e1649554a321fc85a8e +3997600 01b34416bf3c34746cc610558e41b134f9c4b635fe26fdff69cc1a1388a18eef +3998000 008bc50937c8c014a5b525b49935f46ea5d658c1dc9c088e3109d7c8298e42b2 +3998400 0008f7d182b5ff9d281e8323cb54c93c56d82dc8b44553cff2721d2fddb35286 +3998800 00683a1279e039fcc5e5bfb815ed7fbe22e9b41e119bbb9cac1a6a2ebf8d1434 +3999200 00dc6ab5925ce354177614162decc2c91954b51124a2d94f1ec7873b22d34c86 +3999600 010222630e232ad5d04787efbfc69e8eed08e3dfc36f31e0c3901011b50eed0e +4000000 001a5d19868fbd7681619223732e012fb188980c8f07e6dcb64e9675dbd59200 +4000400 00a3cd91e7136eb541b826885694099948d6abb373dd9c34074ef94956529805 +4000800 0003178c8101b8a9bc59b70be5357ab98af1ff1d990aaf1891399e17f8439cba +4001200 01064cee20dda6ef31f25bbe907c15f8e9c3eb0f5d49228993d69b355648407e +4001600 010fc04536fa96314f2e95e5b1d473c724f9630efd0e76095a3cb41aa7d09f51 +4002000 00a91a128f318f38a31cc038a57de1b9c30e8a168e7f57aaa2cdf3083f3c6f0c +4002400 00966b952a5b181accf8100f480d17c2fc8dc887a4714ea9e24d8f1d14caa44e +4002800 0012667828ed266510233b9b9c06e1896c4c7ab53e5b7a109401552967b29c77 +4003200 00e243f69d07a9ac5c292d4baad0c482671e13a1a7c98acf2d1611fe01a1cb66 +4003600 0048c7318c0bdb034d1b6d4104e00f0eacd1dbb6042730db39e1a65a02334519 +4004000 00525225da19b229d15205b4f8ea8d116a111a4d77c0ac6c10a991875fb856b1 +4004400 000d92cfdc8be3da0b4c030601f5bc63556dfac12b6a75ab36a69b39a10488c2 +4004800 01875fc1273f698b4f49d1a7c209cc4be59eba3075e7e33ac01a60d958588d20 +4005200 003b78dfdbd91583a9ad6a339da4f6f2ee4d193f1194c14a4c06bb4dce993189 +4005600 00396a1a5efb1fbb8c11fa3afea7348d9c7b43f77eec252220a6d63ff0fca049 +4006000 00c11b19d4635df55e84bc5f3f5df4f63c8bf732de33e7d3515b581f55c7f1d4 +4006400 00e6e4264b3c80327fc903b1ac2721ea7d716047bb38e87132aa509b8fc5add8 +4006800 0049ebf1eeb595cb5890e59bfd119ce31b01e3a56145b194f30e69e7814f651b +4007200 006518aed2f54f3d1fedfe1af70fbc9fd9dddc908c18aa1de185fb2800c82c9a +4007600 00196384d74709a5d2fbd3c1e2892f32789b73eb1ca552eaad42327dc54f379c +4008000 0162d528f81d2b00c2d689650b2ec07bbe813f958e2b6303cfb9150585799f3d +4008400 000392f38f06cbfad6ce380c82fdc746af026f1b39dd141751d76ecfb8af4ef9 +4008800 0060108193ee43cd52ff963fdde2b88909dcd2ec13ae4207c729081a10c54ec0 +4009200 007a055ac215f072c8bee753c1bdbd14af3cdcfda232a37ef0805075c31d1354 +4009600 006d5865a63b1f2a08c4f8de311f8271e776222905c3b1426821a74ced204c84 +4010000 007b5b24b9f36660d90ccbbdeaef6478ff8727bc7908c7626a540a7bfbd24336 +4010400 000000f2f4cec692461a3418ae4752da17755064fa0d21ab2dc4709f18988f6d +4010800 000002d89941ee6a5619d60d92f302de2c69595dbb5f6bf4979d941748381e0d +4011200 0052cfaedee48e0605de0231550ee42c102946755a149ee52820dcf0d06b47ae +4011600 00005a707992e808565c3be50ac8db2868d99682e58b564584ee5f5e7663d7d4 +4012000 0000365f09bce07c676c231bcf234f3c8d4d1feb7755c93e8509dcbc14fdfedb +4012400 0095691d3efed388039cdc2e950a56dfaf5daf186d10accf5eb100cfe1843dd0 +4012800 0109e9c9d31c1ff869c075ecf8072d71165b593e23a98a7db821b7a9d53e59e7 +4013200 000e71b087ead3c4b703488b5df71b4cc859fc59353b10bc16448f12667ab1d6 +4013600 00679f5ef3de6516de5605c97964a9fa57fa2e9d0489d540bf4b42b8613ba9c6 +4014000 0082195d939d63c17892b6f0095b292c9b91f3b8dc8137c841e0cfd8b49d91eb +4014400 007e1cee3b62f57109e89a8692ae3adf6a0ab30752767a02962fbf6154bd4acf +4014800 0072134f1a8f354b3b66fa72b536ac0a61c040667577e6d9bd12a4a4b4ac8a4c +4015200 0060789f746a6112505b2a6acd61d0e00c6ed3ace981ff2b3006679d5b8ad8ca +4015600 005f89d77050ce2321d29e9d335e4ad6acaef3c0cf9e50a4a15f99b1ba8ec7ad +4016000 0080bb0a470c3cbe9942f9d63240c9cee26df740e12e1f168374d9e74b3aca44 +4016400 0062a44d73fc093e850b365905a40f520118c2e0ed7d97e8f61cf48e2d2b0840 +4016800 001f61e5c9dbd85bf4cb2c2a8bed83a7061203c980b6deddf1fdb4d6334a175f +4017200 001574b370bc69a4ac921395a9b77d442d11b1d0a21c0e5082c0b0ac3aba267b +4017600 009695b25fc1b880ce31229c42256955a707186a95182e8b76c57d3156e53c01 +4018000 006f072d1517e9df3d8896ed648f1f13c2640d5200c1515e42b29b41abbc0c3e +4018400 003145a81243d6919f7da08a4ed18115c8f0d2c0d2cb93d8af5ae2a08f255dfc +4018800 00e4e0ebd4b65e56de4fee801d1639f789ab20ccd8925a2bebfe577fae42b7be +4019200 004d7dad2c85c041f719b566bd75d25e2190fdacf3470f138a2c7517ba48a5b2 +4019600 00f567d827f442d21e3d0d150b64182577bdd50f3ad712679f4651a799a7c73b +4020000 00d4b38ecea4744d26d86e4fc50e62d0f79b81eb372b3c64abe5476e66d2503e +4020400 00bd714de8c82e2cc9a4a42a474125d766e84063ea0dd1c42774e003a209a976 +4020800 00789a414d18ce9a44800b5e9c7ec6dde5959975b500152bb4ca7ab0428abdf8 +4021200 0067ba6e3ae7f3dc28ea07dc7b6dff6b1a337946616a57c77c3a90e809749e90 +4021600 00d039c46bedc95c60e8e96357d06719692eccce397810b8391cb67e36f04ac4 +4022000 002357cf758c318c723bbaa31013aabbdebf263c2b20981c7097915cc98e36ba +4022400 009b4c422d8474a69c521e19e760ba8ac937401d9d83902c72a7589625a28e66 +4022800 00bb98f0ebe200d6cb24cc595d464133467543221e1126ffeff9fb1bc875cc59 +4023200 0068c1949ead36e43d0e08f67316358505a72f63c0757afd622d3cccf71bfbb8 +4023600 008ca73e37d5483dbe997d3782b1bde957ba39a89932f7846d7922dc2bc40f98 +4024000 00ab2a82750ef34363f19c152a7a0934ad8af8cd0d162b9bfe0b5cd80a409cd2 +4024400 01810913934f078d78f665e8c0253aceb67c275681ee121324ce5bbbec239992 +4024800 001926753dcaf6b397b15b4938d244c98ee89a2403a41a5c9633a78d8270eeb3 +4025200 005668d45937a8e9addb3dc7a5f9b10bd86bbe558368edaf78f7a82340617da3 +4025600 0063511725e539e1c4c3b85c5b0b22c5bbe4549cca5d5f8fa50ca43f9a98dbc4 +4026000 0026056f6355caf3c18ecf1f914b5de408ce1545ecc75e81a8c0c252fadbb793 +4026400 0073122d54fa21e6a6826a71a20be7fa343f11f5b6fe2f0e81e8bdd4edccb45f +4026800 00b7fb1e361c42707a782591a870a94e1c46c3e8b51c82ded06e1feb31b86835 +4027200 001a9647887a6b4ea2852e2f4062800f2e558df4be7695c70f819f3d3ad7f0e4 +4027600 0080cb5def4f7b4043fd61874b26e5710ffd1545df011087bbfe0aa5fe788fa2 +4028000 0035922a1bfb3fb688b12c74724f0554c9385b5496a772281fe826953d3c3ad7 +4028400 000651a9b0a3f10bce6a61b290cdcba7ccceeaa192c329fedab56e3693462817 +4028800 00b75f3033fbb9d4b56a8fc5a7793a0210afc67b7f3b094f26967f6caa210b4d +4029200 00232776a697d46432feb528b3a1bda5cf2a57fe16b8509ac04b41351daa3d48 +4029600 000004aeaf02da98c45f76a27194182253a04bc5a9b0eeb10f3eba286d723482 +4030000 002716f8f5892707d77de3795441ebd99b9742a8892151b937b1e5706f3d5980 +4030400 0132810789921b29b2b196dbe39f3d35d6f72b10ec252d14d11fb5ae8d6a3368 +4030800 002288ab046beda7de55b5229e20243542167cabdb0645fe593424179cd610e7 +4031200 004bc1243def2924d8adccace9e1bc45aaf61fad932ac9766c5cfcbb717cbfa6 +4031600 003f0878c11c88e1e56be7f682a5e2eb7f71a76063bd85ea2fc860523a172ee9 +4032000 0053058d71007c508db6844740eaa585b1937a1cb9a6cf7915091e246a8da42c +4032400 00431145fbe78dd906800e2f482ea46f86c09feb03e458964cd67baeb2c8074c +4032800 00ae4bd50d6aad28ed5b7e1d7e231d6110d4f0ab5040a10e23e8c1085232a1e1 +4033200 000c24e4c8fffb08635b5097077cb1251ab19a9a7bb658d2bfc4d237a39be7a6 +4033600 0070a6db09cbb39d1e0e8ff0c1f5c81e90e4c4f591d6c3b1216fef8d2d203cbc +4034000 00b6057148181e0d481a35330c28666a3ce9d78480c64b59d71b16e261c99b57 +4034400 00b74695c4878dd3457ec30521fa890f058120864cb3f79addc225f95dd641fc +4034800 00ce07c2bbf333def61dcb212d08a5813b4ba636a8453db42dfb15a8002353e7 +4035200 006424de0de254075a3cfca94822d8e41d6c0db52a72045a7780cd817fccb482 +4035600 010d1060376eb107651753f9da007eb2ef2210ba5539113081eee153609cc543 +4036000 0099cb0f606bcf59df65cd787def69cd7b181eaa6fdc9aa19220d42794838f3e +4036400 00352e1a3165d0c5a6b8af8d6c26b6cbae4ea314bc322cf5db4fc4fd9e18ff18 +4036800 0057a54292d48eb696f04788686a4317d7e4179487fd308f1ca7ae1c5fdc090c +4037200 0044e4363c12d2d297c0f4dbf83ffde725d462af0b1b2a991a8e94359141f97e +4037600 006532f27533a76e10aa2fa2b74d51db694d23b217db31417ba8d33eef41bdc5 +4038000 00007110084b82bd064f3fad65b9b61096fde4e899f3485bf3c4d65edf574603 +4038400 00027f290c69bcc3af8a99d17e005b4ebe54283dd6ee4edd96e533b3c04aa87f +4038800 00b7a5648bc14ee35532a75c247a93b54603c432676bf6b44e5c1ca8b3288c6e +4039200 01371de5f685697ebcdd7585ba8ed4d2f145677234ca12da641e764136f73fc0 +4039600 00bb466f3c957d43f03c50b1199fbede73616cb5d871818e3c34db62c90306bf +4040000 008f0ad8a367ebe6ee224a1f6c8a99c5ef8881126a439cad242d54bcc25ef937 +4040400 00afe75fa090f32367ae0d0795f350d4b48cc44a1adecea17842edab4e99e4cb +4040800 00652c071292d5f1704f427f05eddefb9019d6a846bc65e9d5d5fdb079e6d264 +4041200 000e256bde87c1d732c77c912b36ee0387ea82e458c6f7a84d947f781b8e0813 +4041600 0082f5837e59372eb7f3b12ab95501abdad8512cd6e30ea8d511f6372841d205 +4042000 00348f4a2191ac984c5ded8d20cb08bc549dac22975f6ba702a315f1e1a7964f +4042400 001cd701db3b39a38ad0ff9217da579aa36e89d3d8da41336423f409af9040bc +4042800 002aa42094035eb1232d3df9ec4fbc26e0bee6a67fb0ffeabd71d3d31cbd0707 +4043200 008a77c3cf280599ac813651ec1a686e1503c9971622b3e0ee72f0d3bfb4ea22 +4043600 00621b72bc93c13364dfbeb445035cead0c4f13941ea82c196d49d8763ebef5f +4044000 0023d5b235b460a50abd5c0ca61ba969067fc47e04f671e59e337829f377f40d +4044400 02c32948db30b09673571efdbb36440b78c8abf5e681290c2d81698a7cfe5f46 +4044800 0056225dbff17664beb51f1b5a79aca7600ca5cf9d349ecace4036d2024ea16c +4045200 0099cb6c5e58e2515e854de35b5c709b95aae6d5dd7db1c8d591641b8bc4d807 +4045600 00aa46ee57affb17fc215a1527407706fc6b639639a960563a19c821fed9485c +4046000 0052bc6b7acd30304cee04482c6b02ea1264191678a89c3a903d0ae14b370792 +4046400 00d0410813be9fd5f66d7bda874623e6a84ef1af7a72bd233e8dacec9fadb7e2 +4046800 00e893bca73300916a2a0d62003209ae4eb592a895f4731ca7fe76467c297362 +4047200 011af41a0bdbec83df50bc7b1c2b85cf4012219a265d348ed72486e923329a02 +4047600 015f7e2ffe36bafedec4546ce9512d451ace20a092fb3327bbb6c7d9973c064c +4048000 01cddb84a16ce52797ad0d5b2eb128116fa0e64931b31c0257639f564c282c8b +4048400 0188fe9c6724a31ea9f51d9793468717cdfcaaacabfa2c2a0858d17a65dc6337 +4048800 02b0a3755b095f79c2a4e21292d94abd2b610beed1aab16bc83fc0dbb7dacea2 +4049200 00248368e327f3c4bdaf1f0e14bf5ed397c3cb2fd51e2752ed6d6b07b3664601 +4049600 000eaa5a75a97269d501a7c52fcac6cd1e2a3c95a042f811360ba4230418fead +4050000 0037b0e287168fb2a1e56c000df040d94a35f7105ea1c7340d8b3cda36dd25a8 +4050400 006654d7a218fb74c206d836cc9363d578461d88140a03efd18874ce11cd86cb +4050800 0002a8b0275c232443d737f669eea0b2f9de7b4118cd6c33035065ba44bacdbd +4051200 0032dd026a25156f905b357fed8ad4f0d3eeb8624a462e542352c816c2bd73c5 +4051600 007460d83aed2d0406155b0fdd8558f35c4e7e5e4d62ed4fab6c77803cc90f22 +4052000 0010cb912b0188da5bc055ee67e3f77d30cd27611369d865974a5bf0b1ec2912 +4052400 000a6c82262c2bcdc6ebe2c3f14b906ee30d9d7b5af6896eaa8c5ed0891040bd +4052800 001bcf3918f011701153176a65da058424077e5e1c18ee2d46026d6343aac012 +4053200 005951003bfa182d1158dea97cd5c5d0e3273638f10fded8ca6b510bcb34cbf5 +4053600 0010efa14e591a34b0969924c33ccdda4122015dfcd758d58110545e4b337368 +4054000 001e79c5e838f6f204a6dfb3e6e48e7781d6e3a59ceabc1295f61364d9701540 +4054400 002fc12d43f55e13834a891702f68e6025b9ad6be9d84e809947b4d2f9ac5f6c +4054800 0023b48ed04632d3714e4ebd22337f1d5a2ce61f61a291ea4820b2b1c36b50bf +4055200 0053369dce2d648630457cec94e23b409a9edda2ad83050c0d98d84d02c7585c +4055600 003e0241cf4ae062092a31bbfe7a3403c0f46189fc75f622f8b63cfc1df3c4be +4056000 0015fde91dfe6fb0f96f38ad4443f60dde319539ce6281c101d397e254099369 +4056400 0054f15c45d26765956e1277e4414605be964ba3ca66cce0b0d31e3b81ea720d +4056800 004bc75bf6aff4d207117d42dd68ec92054b98252ba1d707be477f5051280d2d +4057200 007666e9cecffb8011781dc7fda76d85f51be7635042ec021805b323e2fef896 diff --git a/zebra-chain/src/parameters/constants.rs b/zebra-chain/src/parameters/constants.rs index 8f9686aa3ca..ccb5648ab07 100644 --- a/zebra-chain/src/parameters/constants.rs +++ b/zebra-chain/src/parameters/constants.rs @@ -1,6 +1,7 @@ //! Definitions of Zebra chain constants, including: //! - slow start interval, -//! - slow start shift +//! - slow start shift, +//! - maximum reorg height use crate::block::Height; @@ -16,6 +17,18 @@ pub const SLOW_START_INTERVAL: Height = Height(20_000); /// This calculation is exact, because `SLOW_START_INTERVAL` is divisible by 2. pub const SLOW_START_SHIFT: Height = Height(SLOW_START_INTERVAL.0 / 2); +/// The maximum chain reorganisation height. +/// +/// This threshold determines the maximum length of the best non-finalized +/// chain. Once the chain grows past this height, Zebra finalizes its oldest +/// blocks; deeper reorganisations are outside Zebra's rollback window. +/// +/// This is a local-only node policy; it is not part of consensus. The window is +/// sized as a defence-in-depth measure against sustained consensus splits. +// +// TODO: change to HeightDiff +pub const MAX_BLOCK_REORG_HEIGHT: u32 = 1000; + /// Magic numbers used to identify different Zcash networks. pub mod magics { use crate::parameters::network::magic::Magic; @@ -52,6 +65,8 @@ pub mod activation_heights { pub const NU6: Height = Height(2_976_000); /// The block height at which `NU6.1` activates on Testnet. pub const NU6_1: Height = Height(3_536_500); + /// The block height at which `NU6.2` activates on Testnet. + pub const NU6_2: Height = Height(4_052_000); } /// Network upgrade activation heights for Mainnet. @@ -76,5 +91,7 @@ pub mod activation_heights { pub const NU6: Height = Height(2_726_400); /// The block height at which `NU6.1` activates on Mainnet. pub const NU6_1: Height = Height(3_146_400); + /// The block height at which `NU6.2` activates on Mainnet. + pub const NU6_2: Height = Height(3_364_600); } } diff --git a/zebra-chain/src/parameters/network.rs b/zebra-chain/src/parameters/network.rs index 475094d4a03..b2b540f7369 100644 --- a/zebra-chain/src/parameters/network.rs +++ b/zebra-chain/src/parameters/network.rs @@ -19,6 +19,17 @@ pub mod testnet; #[cfg(test)] mod tests; +// Mainnet temporary Orchard-disabling soft-fork height, shipped publicly in Zebra v4.5.3. +// This is DISTINCT from the NU6.2 *activation* (re-enable) height (3_364_600, see +// `network_upgrade.rs`), which lands 1_174 blocks later. Do NOT change this value: it is +// already deployed, so changing it would fork from live v4.5.3 nodes in the disable window. +const MAINNET_TEMPORARY_ORCHARD_DISABLING_SOFT_FORK_HEIGHT: Height = Height(3_363_426); + +// Default Testnet temporary Orchard-disabling soft-fork height. As on Mainnet, this is DISTINCT +// from the NU6.2 *activation* (re-enable) height (4_052_000, see `network_upgrade.rs`), which +// lands 3_500 blocks later. +const TESTNET_TEMPORARY_ORCHARD_DISABLING_SOFT_FORK_HEIGHT: Height = Height(4_048_500); + /// An enum describing the kind of network, whether it's the production mainnet or a testnet. // Note: The order of these variants is important for correct bincode (de)serialization // of history trees in the db format. @@ -97,7 +108,7 @@ impl NetworkKind { /// Returns the 2 bytes prefix for Bech32m-encoded transparent TEX /// payment addresses for the network as defined in [ZIP-320](https://zips.z.cash/zip-0320.html). pub fn tex_address_prefix(self) -> [u8; 2] { - // TODO: Add this bytes to `zcash_primitives::constants`? + // TODO: Add this bytes to `zcash_protocol::constants`? match self { Self::Mainnet => [0x1c, 0xb8], Self::Testnet | Self::Regtest => [0x1d, 0x25], @@ -331,6 +342,63 @@ impl Network { }) .collect() } + + /// Returns the height at which the soft fork that temporarily disables Orchard + /// actions in transactions activates, if it is configured for this network. + pub fn temporary_orchard_disabling_soft_fork_height(&self) -> Option { + match self { + Network::Mainnet => Some(MAINNET_TEMPORARY_ORCHARD_DISABLING_SOFT_FORK_HEIGHT), + Network::Testnet(parameters) => { + parameters.temporary_orchard_disabling_soft_fork_height() + } + } + } + + /// Returns whether Orchard has been temporarily disabled in transactions. + pub fn temporary_orchard_disabling_soft_fork_active(&self, height: Height) -> bool { + self.temporary_orchard_disabling_soft_fork_height() + .is_some_and(|h| height >= h) + } + + /// Returns whether Orchard is temporarily disabled in transactions at `height`. + /// + /// The temporary-disable soft fork is bounded above by NU6.2, which re-enables + /// Orchard actions: once NU6.2 is active the temporary-disable rule no longer + /// applies. On networks where NU6.2 is unscheduled this matches + /// [`Self::temporary_orchard_disabling_soft_fork_active`]. + pub fn is_orchard_temporarily_disabled(&self, height: Height) -> bool { + self.temporary_orchard_disabling_soft_fork_active(height) + && NetworkUpgrade::Nu6_2 + .activation_height(self) + .is_none_or(|nu6_2| height < nu6_2) + } + + /// Returns whether `height` is the first height at which the soft fork that + /// temporarily disables Orchard actions applies. + /// + /// This is the boundary at which the mempool must revalidate its contents, to drop + /// any transactions containing Orchard actions that were accepted before the soft + /// fork activated. + pub fn is_temporary_orchard_disabling_soft_fork_activation_height( + &self, + height: Height, + ) -> bool { + self.temporary_orchard_disabling_soft_fork_height() == Some(height) + } + + /// Returns whether the consensus rule requiring a canonically-sized Orchard proof + /// is active at `height`. + /// + /// This rule activates with the network upgrade that re-enables Orchard actions + /// (NU6.2). On networks where NU6.2 is unscheduled the rule is always inactive. It is a + /// constricting rule, so it must stay height-gated, or it would reject historical + /// Orchard actions mined before the soft fork that temporarily disabled them, and + /// prevent syncing. + pub fn orchard_canonical_proof_size_rule_active(&self, height: Height) -> bool { + NetworkUpgrade::Nu6_2 + .activation_height(self) + .is_some_and(|h| height >= h) + } } // This is used for parsing a command-line argument for the `TipHeight` command in zebrad. diff --git a/zebra-chain/src/parameters/network/subsidy.rs b/zebra-chain/src/parameters/network/subsidy.rs index 44a0aa999ba..972fdb53c21 100644 --- a/zebra-chain/src/parameters/network/subsidy.rs +++ b/zebra-chain/src/parameters/network/subsidy.rs @@ -396,6 +396,10 @@ pub enum SubsidyError { #[error("invalid amount")] InvalidAmount(#[from] amount::Error), + + #[cfg(zcash_unstable = "zip235")] + #[error("invalid zip233 amount")] + InvalidZip233Amount, } /// The divisor used for halvings. @@ -482,12 +486,13 @@ pub fn miner_subsidy( network: &Network, expected_block_subsidy: Amount, ) -> Result, amount::Error> { - let total_funding_stream_amount: Result, _> = - funding_stream_values(height, network, expected_block_subsidy)? - .values() - .sum(); + let founders_reward = founders_reward(network, height); + + let funding_streams_sum = funding_stream_values(height, network, expected_block_subsidy)? + .values() + .sum::, _>>()?; - expected_block_subsidy - total_funding_stream_amount? + expected_block_subsidy - founders_reward - funding_streams_sum } /// Returns the founders reward address for a given height and network as described in [§7.9]. @@ -532,7 +537,12 @@ pub fn founders_reward_address(net: &Network, height: Height) -> Option pub fn founders_reward(net: &Network, height: Height) -> Amount { - if halving(height, net) < 1 { + // The founders reward is 20% of the block subsidy before the first halving, and 0 afterwards. + // + // On custom testnets, the first halving can occur later than Canopy, which causes an + // inconsistency in the definition of the founders reward, which should occur only before + // Canopy, so we check if Canopy is active as well. + if halving(height, net) < 1 && NetworkUpgrade::current(net, height) < NetworkUpgrade::Canopy { block_subsidy(height, net) .map(|subsidy| subsidy.div_exact(5)) .expect("block subsidy must be valid for founders rewards") diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index 088b6c83f2d..397d629cfad 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -6,7 +6,7 @@ use crate::{ amount::{Amount, NonNegative}, block::{self, Height, HeightDiff}, parameters::{ - checkpoint::list::{CheckpointList, TESTNET_CHECKPOINTS}, + checkpoint::list::{CheckpointList, TESTNET_CHECKPOINT_LIST}, constants::{magics, SLOW_START_INTERVAL, SLOW_START_SHIFT}, network::error::ParametersBuilderError, network_upgrade::TESTNET_ACTIVATION_HEIGHTS, @@ -189,6 +189,7 @@ impl From<&BTreeMap> for ConfiguredActivationHeights { NetworkUpgrade::Nu5 => &mut configured_activation_heights.nu5, NetworkUpgrade::Nu6 => &mut configured_activation_heights.nu6, NetworkUpgrade::Nu6_1 => &mut configured_activation_heights.nu6_1, + NetworkUpgrade::Nu6_2 => &mut configured_activation_heights.nu6_2, NetworkUpgrade::Nu7 => &mut configured_activation_heights.nu7, #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => &mut configured_activation_heights.zfuture, @@ -362,6 +363,9 @@ pub struct ConfiguredActivationHeights { /// Activation height for `NU6.1` network upgrade. #[serde(rename = "NU6.1")] pub nu6_1: Option, + /// Activation height for `NU6.2` network upgrade. + #[serde(rename = "NU6.2")] + pub nu6_2: Option, /// Activation height for `NU7` network upgrade. #[serde(rename = "NU7")] pub nu7: Option, @@ -385,6 +389,7 @@ impl ConfiguredActivationHeights { nu5, nu6, nu6_1, + nu6_2, nu7, #[cfg(zcash_unstable = "zfuture")] zfuture, @@ -406,6 +411,7 @@ impl ConfiguredActivationHeights { nu5, nu6, nu6_1, + nu6_2, nu7, #[cfg(zcash_unstable = "zfuture")] zfuture, @@ -477,6 +483,8 @@ pub struct ParametersBuilder { lockbox_disbursements: Vec<(String, Amount)>, /// Checkpointed block hashes and heights for this network. checkpoints: Arc, + /// Height at which the soft-fork to temporarily disable Orchard in transactions activates + temporary_orchard_disabling_soft_fork_height: Option, } impl Default for ParametersBuilder { @@ -512,10 +520,10 @@ impl Default for ParametersBuilder { .iter() .map(|(addr, amount)| (addr.to_string(), *amount)) .collect(), - checkpoints: TESTNET_CHECKPOINTS - .parse() - .map(Arc::new) - .expect("must be able to parse checkpoints"), + checkpoints: TESTNET_CHECKPOINT_LIST.clone(), + temporary_orchard_disabling_soft_fork_height: Some( + super::TESTNET_TEMPORARY_ORCHARD_DISABLING_SOFT_FORK_HEIGHT, + ), } } } @@ -596,6 +604,7 @@ impl ParametersBuilder { nu5, nu6, nu6_1, + nu6_2, nu7, #[cfg(zcash_unstable = "zfuture")] zfuture, @@ -623,6 +632,7 @@ impl ParametersBuilder { .chain(nu5.into_iter().map(|h| (h, Nu5))) .chain(nu6.into_iter().map(|h| (h, Nu6))) .chain(nu6_1.into_iter().map(|h| (h, Nu6_1))) + .chain(nu6_2.into_iter().map(|h| (h, Nu6_2))) .chain(nu7.into_iter().map(|h| (h, Nu7))); #[cfg(zcash_unstable = "zfuture")] @@ -777,14 +787,12 @@ impl ParametersBuilder { mut self, checkpoints: impl Into, ) -> Result { - self.checkpoints = Arc::new(match checkpoints.into() { - ConfiguredCheckpoints::Default(true) => TESTNET_CHECKPOINTS - .parse() - .map_err(|_| ParametersBuilderError::InvalidCheckpointsFormat)?, - ConfiguredCheckpoints::Default(false) => { + self.checkpoints = match checkpoints.into() { + ConfiguredCheckpoints::Default(true) => TESTNET_CHECKPOINT_LIST.clone(), + ConfiguredCheckpoints::Default(false) => Arc::new( CheckpointList::from_list([(block::Height(0), self.genesis_hash)]) - .map_err(|_| ParametersBuilderError::FailedToParseDefaultCheckpoint)? - } + .map_err(|_| ParametersBuilderError::FailedToParseDefaultCheckpoint)?, + ), ConfiguredCheckpoints::Path(path_buf) => { let Ok(raw_checkpoints_str) = std::fs::read_to_string(&path_buf) else { return Err(ParametersBuilderError::FailedToReadCheckpointFile { @@ -792,16 +800,20 @@ impl ParametersBuilder { }); }; - raw_checkpoints_str - .parse::() - .map_err(|err| ParametersBuilderError::FailedToParseCheckpointFile { - path_buf: path_buf.clone(), - err: err.to_string(), - })? + Arc::new( + raw_checkpoints_str + .parse::() + .map_err(|err| ParametersBuilderError::FailedToParseCheckpointFile { + path_buf: path_buf.clone(), + err: err.to_string(), + })?, + ) } - ConfiguredCheckpoints::HeightsAndHashes(items) => CheckpointList::from_list(items) - .map_err(|_| ParametersBuilderError::InvalidCustomCheckpoints)?, - }); + ConfiguredCheckpoints::HeightsAndHashes(items) => Arc::new( + CheckpointList::from_list(items) + .map_err(|_| ParametersBuilderError::InvalidCustomCheckpoints)?, + ), + }; Ok(self) } @@ -811,6 +823,19 @@ impl ParametersBuilder { self.with_checkpoints(ConfiguredCheckpoints::Default(false)) } + /// Sets the height for this network at which the soft fork that temporarily disables + /// Orchard transactions will activate. + pub fn with_temporary_orchard_disabling_soft_fork_height(mut self, height: Height) -> Self { + self.temporary_orchard_disabling_soft_fork_height = Some(height); + self + } + + /// Disables the soft fork that would temporarily disable Orchard transactions. + pub fn disable_temporary_orchard_disabling_soft_fork(mut self) -> Self { + self.temporary_orchard_disabling_soft_fork_height = None; + self + } + /// Converts the builder to a [`Parameters`] struct fn finish(self) -> Parameters { let Self { @@ -828,6 +853,7 @@ impl ParametersBuilder { post_blossom_halving_interval, lockbox_disbursements, checkpoints, + temporary_orchard_disabling_soft_fork_height, } = self; Parameters { network_name, @@ -844,6 +870,7 @@ impl ParametersBuilder { post_blossom_halving_interval, lockbox_disbursements, checkpoints, + temporary_orchard_disabling_soft_fork_height, } } @@ -890,6 +917,7 @@ impl ParametersBuilder { post_blossom_halving_interval, lockbox_disbursements, checkpoints: _, + temporary_orchard_disabling_soft_fork_height: _, } = Self::default(); self.activation_heights == activation_heights @@ -963,6 +991,8 @@ pub struct Parameters { lockbox_disbursements: Vec<(String, Amount)>, /// List of checkpointed block heights and hashes checkpoints: Arc, + /// Height at which the soft-fork to temporarily disable Orchard in transactions activates + temporary_orchard_disabling_soft_fork_height: Option, } impl Default for Parameters { @@ -1000,6 +1030,9 @@ impl Parameters { .with_disable_pow(true) .with_unshielded_coinbase_spends(true) .with_slow_start_interval(Height::MIN) + // Like the default Testnet activation heights stripped below, the default Testnet's + // temporary Orchard-disabling soft fork does not apply to Regtest. + .disable_temporary_orchard_disabling_soft_fork() // Removes default Testnet activation heights if not configured, // most network upgrades are disabled by default for Regtest in zcashd .with_activation_heights(activation_heights.for_regtest())? @@ -1047,6 +1080,7 @@ impl Parameters { post_blossom_halving_interval, lockbox_disbursements: _, checkpoints: _, + temporary_orchard_disabling_soft_fork_height: _, } = Self::new_regtest(Default::default()).expect("default regtest parameters are valid"); self.network_name == network_name @@ -1148,6 +1182,12 @@ impl Parameters { pub fn checkpoints(&self) -> Arc { self.checkpoints.clone() } + + /// Returns the height at which the soft-fork to temporarily disable Orchard in + /// transactions activates. + pub fn temporary_orchard_disabling_soft_fork_height(&self) -> Option { + self.temporary_orchard_disabling_soft_fork_height + } } impl Network { diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index f3ea5f82cf7..9241c5de25d 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -688,3 +688,68 @@ fn funding_streams_default_values() { 1 ); } + +/// Checks the temporary Orchard-disabling soft fork height accessors, including the +/// activation-height boundary used to trigger a mempool reset. +#[test] +fn temporary_orchard_disabling_soft_fork_heights() { + let _init_guard = zebra_test::init(); + + // Mainnet uses a fixed activation height. + let mainnet_height = Height(3_363_426); + assert_eq!( + Network::Mainnet.temporary_orchard_disabling_soft_fork_height(), + Some(mainnet_height), + ); + assert!(!Network::Mainnet + .temporary_orchard_disabling_soft_fork_active((mainnet_height - 1).unwrap()),); + assert!(Network::Mainnet.temporary_orchard_disabling_soft_fork_active(mainnet_height)); + // Only the exact activation height is the boundary that triggers a reset. + assert!(!Network::Mainnet + .is_temporary_orchard_disabling_soft_fork_activation_height((mainnet_height - 1).unwrap())); + assert!( + Network::Mainnet.is_temporary_orchard_disabling_soft_fork_activation_height(mainnet_height) + ); + assert!(!Network::Mainnet + .is_temporary_orchard_disabling_soft_fork_activation_height((mainnet_height + 1).unwrap())); + + // The default Testnet uses a fixed activation height, below its NU6.2 activation height. + let testnet_default_height = Height(4_048_500); + assert_eq!( + Network::new_default_testnet().temporary_orchard_disabling_soft_fork_height(), + Some(testnet_default_height), + ); + + // Regtest does not apply the temporary Orchard-disabling soft fork. + assert_eq!( + Network::new_regtest(Default::default()).temporary_orchard_disabling_soft_fork_height(), + None, + ); + + // A configured Testnet uses its configured height. + let testnet_height = Height(2_000_000); + let configured = testnet::Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height(testnet_height) + .to_network() + .expect("failed to build configured network"); + + assert_eq!( + configured.temporary_orchard_disabling_soft_fork_height(), + Some(testnet_height), + ); + assert!(configured.is_temporary_orchard_disabling_soft_fork_activation_height(testnet_height)); + assert!(!configured + .is_temporary_orchard_disabling_soft_fork_activation_height((testnet_height + 1).unwrap())); + + // A Testnet with the soft fork disabled has no activation height. + let disabled = testnet::Parameters::build() + .disable_temporary_orchard_disabling_soft_fork() + .to_network() + .expect("failed to build configured network"); + + assert_eq!( + disabled.temporary_orchard_disabling_soft_fork_height(), + None, + ); + assert!(!disabled.is_temporary_orchard_disabling_soft_fork_activation_height(testnet_height)); +} diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 4b24bde5bb8..5ad159a6cb1 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -58,6 +58,9 @@ pub enum NetworkUpgrade { /// The Zcash protocol after the NU6.1 upgrade. #[serde(rename = "NU6.1")] Nu6_1, + /// The Zcash protocol after the NU6.2 upgrade. + #[serde(rename = "NU6.2")] + Nu6_2, /// The Zcash protocol after the NU7 upgrade. #[serde(rename = "NU7")] Nu7, @@ -108,6 +111,7 @@ pub(super) const MAINNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] (NU5, Nu5), (NU6, Nu6), (NU6_1, Nu6_1), + (NU6_2, Nu6_2), ] }; /// Testnet network upgrade activation heights. @@ -133,6 +137,7 @@ pub(super) const TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] (NU5, Nu5), (NU6, Nu6), (NU6_1, Nu6_1), + (NU6_2, Nu6_2), ] }; @@ -226,6 +231,7 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] = (Nu5, ConsensusBranchId(0xc2d6d0b4)), (Nu6, ConsensusBranchId(0xc8e71055)), (Nu6_1, ConsensusBranchId(0x4dec4df0)), + (Nu6_2, ConsensusBranchId(0x5437f330)), // Registered unconditionally so chain history works in builds without // `zcash_unstable="nu7"`; Nu7-specific code paths remain cfg-gated elsewhere. (Nu7, ConsensusBranchId(0x77190ad8)), @@ -395,7 +401,7 @@ impl NetworkUpgrade { pub fn target_spacing(&self) -> Duration { let spacing_seconds = match self { Genesis | BeforeOverwinter | Overwinter | Sapling => PRE_BLOSSOM_POW_TARGET_SPACING, - Blossom | Heartwood | Canopy | Nu5 | Nu6 | Nu6_1 | Nu7 => { + Blossom | Heartwood | Canopy | Nu5 | Nu6 | Nu6_1 | Nu6_2 | Nu7 => { POST_BLOSSOM_POW_TARGET_SPACING.into() } @@ -520,6 +526,7 @@ impl From for NetworkUpgrade { zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5, zcash_protocol::consensus::NetworkUpgrade::Nu6 => Self::Nu6, zcash_protocol::consensus::NetworkUpgrade::Nu6_1 => Self::Nu6_1, + zcash_protocol::consensus::NetworkUpgrade::Nu6_2 => Self::Nu6_2, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zcash_protocol::consensus::NetworkUpgrade::Nu7 => Self::Nu7, #[cfg(zcash_unstable = "zfuture")] diff --git a/zebra-chain/src/parameters/tests.rs b/zebra-chain/src/parameters/tests.rs index ef6088410c5..7cd626abf8b 100644 --- a/zebra-chain/src/parameters/tests.rs +++ b/zebra-chain/src/parameters/tests.rs @@ -270,6 +270,7 @@ const NETWORK_UPGRADES_IN_ORDER: &[NetworkUpgrade] = &[ Nu5, Nu6, Nu6_1, + Nu6_2, #[cfg(any(test, feature = "zebra-test"))] Nu7, ]; @@ -287,6 +288,7 @@ fn full_activation_list_contains_all_upgrades() { let network = Network::Mainnet; let full_list = network.full_activation_list(); - // NU7 is only included in tests; on Mainnet, NU7 isn’t live yet, so we subtract 1 here. + // NU7 is unscheduled on Mainnet (no activation height committed), so it is absent from the + // full activation list even though it is always present in the iter. assert_eq!(full_list.len(), NetworkUpgrade::iter().count() - 1); } diff --git a/zebra-chain/src/primitives/zcash_history.rs b/zebra-chain/src/primitives/zcash_history.rs index cfb3073399a..d75a75754ce 100644 --- a/zebra-chain/src/primitives/zcash_history.rs +++ b/zebra-chain/src/primitives/zcash_history.rs @@ -278,6 +278,7 @@ impl Version for zcash_history::V1 { | NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu7 => {} #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => {} diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index 48282ea8ee2..15c066aa9d6 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -308,6 +308,82 @@ impl PrecomputedTxData { } } +/// Internal error type returned by [`sighash_inner`] when a sighash request +/// violates one of the documented preconditions of [`sighash`] or +/// [`sighash_v4_raw`]. +/// +/// Public callers (`SigHasher::sighash`, `SigHasher::sighash_v4_raw`) document +/// these conditions as panics, so they unwrap the result at the public +/// boundary. Keeping the internal code `Result`-shaped avoids spreading +/// `.expect()` calls across multiple locations whose justifications all +/// depend on the same caller invariants. +#[derive(Debug)] +enum SighashError { + /// Caller passed an `input_index` greater than or equal to the number of + /// transparent inputs the caller declared in `all_previous_outputs`. + InputIndexOutOfBounds { + input_index: usize, + input_count: usize, + }, + /// Caller asked for a transparent sighash on a transaction that + /// `zcash_primitives` parsed without a transparent bundle. This contradicts + /// the precondition that `Some((input_index, _))` is only passed for + /// transactions with at least one transparent input. + NoTransparentBundle, + /// `input_index` is within bounds for `all_previous_outputs` but out of + /// bounds for the transparent bundle's `vin` returned by + /// `zcash_primitives`. Reaching this branch indicates a serialize / + /// deserialize round-trip inconsistency between Zebra's `Transaction` and + /// the parsed `zcash_primitives::Transaction`, which would be a bug in + /// either crate. + BundleInputCountMismatch { + input_index: usize, + bundle_vin_len: usize, + all_prev_outputs_len: usize, + }, + /// The previous output's value could not be converted to `Zatoshis`. + /// Reaching this branch means the caller passed an output whose amount + /// was not validated by the consensus rules before sighash computation. + InvalidPreviousOutputAmount, +} + +impl std::fmt::Display for SighashError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InputIndexOutOfBounds { + input_index, + input_count, + } => write!( + f, + "input_index {input_index} is out of bounds (transaction has \ + {input_count} transparent inputs)" + ), + Self::NoTransparentBundle => f.write_str( + "transparent sighash requested for a transaction with no \ + transparent bundle (vin and vout both empty)", + ), + Self::BundleInputCountMismatch { + input_index, + bundle_vin_len, + all_prev_outputs_len, + } => write!( + f, + "input_index {input_index} valid for all_previous_outputs (len \ + {all_prev_outputs_len}) but out of bounds for the parsed \ + transparent bundle (vin len {bundle_vin_len}); this indicates \ + a serialize/deserialize round-trip inconsistency" + ), + Self::InvalidPreviousOutputAmount => f.write_str( + "previous output amount could not be converted to Zatoshis; \ + the amount should have been validated before sighash \ + computation", + ), + } + } +} + +impl std::error::Error for SighashError {} + /// Compute a signature hash using librustzcash. /// /// # Inputs @@ -318,42 +394,129 @@ impl PrecomputedTxData { /// - `input_index_script_code`: a tuple with the index of the transparent Input /// for which we are producing a sighash and the respective script code being /// validated, or None if it's a shielded input. +/// +/// # Panics +/// +/// - if `input_index_script_code` is `Some((input_index, _))` and `input_index` +/// is out of bounds for `precomputed_tx_data.all_previous_outputs`. The +/// public callers in `zebra-chain` document this as a precondition. +/// - if the previous output at `input_index` has a value that cannot be +/// converted to `Zatoshis`. Output values are validated before sighash +/// computation, so this branch is unreachable in practice. pub(crate) fn sighash( precomputed_tx_data: &PrecomputedTxData, hash_type: HashType, input_index_script_code: Option<(usize, Vec)>, ) -> SigHash { + sighash_inner( + precomputed_tx_data, + hash_type.try_into().expect("hash type should be canonical"), + input_index_script_code, + ) + .expect( + "sighash precondition violated: callers must pass an in-bounds \ + input_index when computing a transparent sighash, and the transaction \ + must contain the transparent input being signed", + ) +} + +/// Compute a pre-V5 (V4) signature hash using the raw `hash_type` byte. +/// +/// `zcashd` serializes the full raw byte into the V4 sighash preimage and only +/// masks with `SIGHASH_MASK` (0x1f) for selection logic. Callers handling V5+ +/// transactions must use [`sighash`] instead so ZIP-244 strictness is enforced. +/// +/// # Panics +/// +/// Same preconditions as [`sighash`]. +pub(crate) fn sighash_v4_raw( + precomputed_tx_data: &PrecomputedTxData, + raw_hash_type: u8, + input_index_script_code: Option<(usize, Vec)>, +) -> SigHash { + sighash_inner( + precomputed_tx_data, + zcash_transparent::sighash::SighashType::from_raw(raw_hash_type), + input_index_script_code, + ) + .expect( + "sighash precondition violated: callers must pass an in-bounds \ + input_index when computing a transparent sighash, and the transaction \ + must contain the transparent input being signed", + ) +} + +/// Internal sighash computation that surfaces precondition violations through +/// `Result` instead of spreading `.expect()` calls across multiple sites. +/// +/// All callers in `zebra-chain` unwrap the returned `Result` at the public +/// boundary, but funnelling the error variants through one type makes it +/// obvious which preconditions each call site relies on. +fn sighash_inner( + precomputed_tx_data: &PrecomputedTxData, + sighash_type: zcash_transparent::sighash::SighashType, + input_index_script_code: Option<(usize, Vec)>, +) -> Result { let lock_script: zcash_transparent::address::Script; let unlock_script: zcash_transparent::address::Script; let signable_input = match input_index_script_code { Some((input_index, script_code)) => { - let output = &precomputed_tx_data.all_previous_outputs[input_index]; + // The `all_previous_outputs` vector is supplied by the caller in + // 1:1 correspondence with `tx.inputs()`, and the transparent + // bundle was produced by round-tripping the same transaction + // bytes through `zcash_primitives::Transaction::read`. Both have + // length equal to `tx.inputs().len()`, so an out-of-bounds index + // is a caller error that should be reported once here. + let all_prev_outputs_len = precomputed_tx_data.all_previous_outputs.len(); + let output = precomputed_tx_data + .all_previous_outputs + .get(input_index) + .ok_or(SighashError::InputIndexOutOfBounds { + input_index, + input_count: all_prev_outputs_len, + })?; + // `zcash_primitives::Transaction::read` returns + // `transparent_bundle = None` only when both `vin` and `vout` are + // empty. The caller only reaches this branch with `Some(_)` when + // the transaction has at least one transparent input, so reaching + // a `None` here means the caller violated the precondition or + // librustzcash changed its behaviour. + let bundle = precomputed_tx_data + .tx_data + .transparent_bundle() + .ok_or(SighashError::NoTransparentBundle)?; lock_script = output.lock_script.clone().into(); unlock_script = zcash_transparent::address::Script(script::Code(script_code)); - zp_tx::sighash::SignableInput::Transparent( - zcash_transparent::sighash::SignableInput::from_parts( - hash_type.try_into().expect("hash type should be ALL"), - input_index, - &unlock_script, - &lock_script, - output - .value - .try_into() - .expect("amount was previously validated"), - ), + let value = output + .value + .try_into() + .map_err(|_| SighashError::InvalidPreviousOutputAmount)?; + let from_parts = zcash_transparent::sighash::SignableInput::from_parts( + bundle, + sighash_type, + input_index, + &unlock_script, + &lock_script, + value, ) + .map_err(|_| SighashError::BundleInputCountMismatch { + input_index, + bundle_vin_len: bundle.vin.len(), + all_prev_outputs_len, + })?; + zp_tx::sighash::SignableInput::Transparent(from_parts) } None => zp_tx::sighash::SignableInput::Shielded, }; - SigHash( + Ok(SigHash( *zp_tx::sighash::signature_hash( &precomputed_tx_data.tx_data, &signable_input, &precomputed_tx_data.txid_parts, ) .as_ref(), - ) + )) } /// Compute the authorizing data commitment of this transaction as specified in [ZIP-244]. diff --git a/zebra-chain/src/serialization.rs b/zebra-chain/src/serialization.rs index 3b12f8dd48c..a0b2e3826b4 100644 --- a/zebra-chain/src/serialization.rs +++ b/zebra-chain/src/serialization.rs @@ -40,5 +40,6 @@ pub use zcash_deserialize::{ }; pub use zcash_serialize::{ zcash_serialize_bytes, zcash_serialize_bytes_external_count, zcash_serialize_empty_list, - zcash_serialize_external_count, FakeWriter, ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN, + zcash_serialize_external_count, FakeWriter, ZcashSerialize, MAX_HEADERS_PER_MESSAGE, + MAX_PROTOCOL_MESSAGE_LEN, }; diff --git a/zebra-chain/src/serialization/error.rs b/zebra-chain/src/serialization/error.rs index ab0294fcf9f..32981b76827 100644 --- a/zebra-chain/src/serialization/error.rs +++ b/zebra-chain/src/serialization/error.rs @@ -51,6 +51,18 @@ pub enum SerializationError { /// rule](https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus). #[error("transaction balance is non-zero but doesn't have Sapling shielded spends or outputs")] BadTransactionBalance, + + /// Could not de/serialize a transparent script. + #[error("script error: {0}")] + Script(#[from] zcash_script::script::Error), + + /// Errors that occur when parsing opcodes in transparent scripts. + #[error("script opcode error: {0}")] + Opcode(#[from] zcash_script::opcode::Error), + + /// Errors that occur when parsing integers in transparent scripts. + #[error("script number error: {0}")] + Num(#[from] zcash_script::num::Error), } impl From for io::Error { @@ -73,6 +85,9 @@ impl From for io::Error { io::ErrorKind::InvalidData, "bad transaction balance: non-zero with no Sapling shielded spends or outputs", ), + SerializationError::Script(e) => io::Error::new(io::ErrorKind::InvalidData, e), + SerializationError::Opcode(e) => io::Error::new(io::ErrorKind::InvalidData, e), + SerializationError::Num(e) => io::Error::new(io::ErrorKind::InvalidData, e), } } } diff --git a/zebra-chain/src/serialization/tests/preallocate.rs b/zebra-chain/src/serialization/tests/preallocate.rs index f84e906c39c..2df08b77586 100644 --- a/zebra-chain/src/serialization/tests/preallocate.rs +++ b/zebra-chain/src/serialization/tests/preallocate.rs @@ -10,19 +10,6 @@ use crate::serialization::{ MAX_PROTOCOL_MESSAGE_LEN, }; -// Allow direct serialization of Vec for these tests. We don't usually -// allow this because some types have specific rules for about serialization -// of their inner Vec. This method could be easily misused if it applied -// more generally. -// -// Due to Rust's trait rules, these trait impls apply to all zebra-chain tests, -// not just the tests in this module. But other crates' tests can't access them. -impl ZcashSerialize for u8 { - fn zcash_serialize(&self, mut writer: W) -> Result<(), std::io::Error> { - writer.write_all(&[*self]) - } -} - impl TrustedPreallocate for u8 { fn max_allocation() -> u64 { // MAX_PROTOCOL_MESSAGE_LEN takes up 5 bytes when encoded as a CompactSize. diff --git a/zebra-chain/src/serialization/zcash_deserialize.rs b/zebra-chain/src/serialization/zcash_deserialize.rs index 74ccbad9244..ed1c74b873e 100644 --- a/zebra-chain/src/serialization/zcash_deserialize.rs +++ b/zebra-chain/src/serialization/zcash_deserialize.rs @@ -4,6 +4,12 @@ use std::{io, net::Ipv6Addr, sync::Arc}; use super::{AtLeastOne, CompactSizeMessage, SerializationError, MAX_PROTOCOL_MESSAGE_LEN}; +/// Initial-allocation cap for `zcash_deserialize_external_count`. +/// +/// 1024 is large enough that honest messages amortize their growth to a few +/// reallocations. +const MAX_INITIAL_ALLOCATION: usize = 1024; + /// Consensus-critical deserialization for Zcash. /// /// This trait provides a generic deserialization for consensus-critical @@ -93,7 +99,11 @@ pub fn zcash_deserialize_external_count return Err(SerializationError::Parse("Vector longer than u64::MAX")), } - let mut vec = Vec::with_capacity(external_count); + // Cap the upfront reservation. The Vec grows via push() as elements + // arrive, so a peer-supplied `external_count` can't force a large + // allocation before any data is read. Fixes the deserializer-level + // case of GHSA-xr93-pcq3-pxf8. + let mut vec = Vec::with_capacity(external_count.min(MAX_INITIAL_ALLOCATION)); for _ in 0..external_count { vec.push(T::zcash_deserialize(&mut reader)?); } diff --git a/zebra-chain/src/serialization/zcash_serialize.rs b/zebra-chain/src/serialization/zcash_serialize.rs index f8c3a9e03c1..21e8d0b679e 100644 --- a/zebra-chain/src/serialization/zcash_serialize.rs +++ b/zebra-chain/src/serialization/zcash_serialize.rs @@ -1,5 +1,6 @@ //! Converting Zcash consensus-critical data structures into bytes. +use byteorder::WriteBytesExt; use std::{io, net::Ipv6Addr}; use super::{AtLeastOne, CompactSizeMessage}; @@ -9,6 +10,11 @@ use super::{AtLeastOne, CompactSizeMessage}; /// This value is used to calculate safe preallocation limits for some types pub const MAX_PROTOCOL_MESSAGE_LEN: usize = 2 * 1024 * 1024; +/// The maximum number of block headers in a single `headers` protocol message. +/// +/// +pub const MAX_HEADERS_PER_MESSAGE: usize = 160; + /// Consensus-critical serialization for Zcash. /// /// This trait provides a generic serialization for consensus-critical @@ -45,6 +51,12 @@ pub trait ZcashSerialize: Sized { } } +impl ZcashSerialize for u8 { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_u8(*self) + } +} + /// A fake writer helper used to get object lengths without allocating RAM. pub struct FakeWriter(pub usize); diff --git a/zebra-chain/src/sprout/tree.rs b/zebra-chain/src/sprout/tree.rs index 0c30572c555..4b36fe0bdc8 100644 --- a/zebra-chain/src/sprout/tree.rs +++ b/zebra-chain/src/sprout/tree.rs @@ -15,7 +15,6 @@ use std::fmt; use byteorder::{BigEndian, ByteOrder}; use incrementalmerkletree::frontier::Frontier; use lazy_static::lazy_static; -use sha2::digest::generic_array::GenericArray; use thiserror::Error; use super::commitment::NoteCommitment; @@ -53,7 +52,7 @@ fn merkle_crh_sprout(left: [u8; 32], right: [u8; 32]) -> [u8; 32] { 0x5be0cd19, ]; - sha2::compress256(&mut state, &[GenericArray::clone_from_slice(&other_block)]); + sha2::compress256(&mut state, &[other_block.into()]); // Yes, SHA-256 does big endian here. // https://github.com/RustCrypto/hashes/blob/master/sha2/src/sha256.rs#L40 diff --git a/zebra-chain/src/tests/vectors.rs b/zebra-chain/src/tests/vectors.rs index f72d2e5a8f4..c0bf1e21b6e 100644 --- a/zebra-chain/src/tests/vectors.rs +++ b/zebra-chain/src/tests/vectors.rs @@ -74,6 +74,7 @@ impl Network { transaction, Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 2db214de532..663791c9394 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -14,8 +14,6 @@ mod sighash; mod txid; mod unmined; -pub mod builder; - #[cfg(any(test, feature = "proptest-impl"))] #[allow(clippy::unwrap_in_result)] pub mod arbitrary; @@ -1253,6 +1251,32 @@ impl Transaction { } } + /// Returns whether the Orchard proof has the canonical size in this + /// transaction, if there is any. + pub fn orchard_proof_size_is_canonical(&self) -> Option { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } => None, + + Transaction::V5 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|shielded_data| shielded_data.proof_size_is_canonical()), + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|shielded_data| shielded_data.proof_size_is_canonical()), + } + } + /// Return if the transaction has any Orchard shielded data, /// regardless of version. pub fn has_orchard_shielded_data(&self) -> bool { @@ -1964,6 +1988,30 @@ impl Transaction { .map(|output| &mut output.value) } + /// Access the [`orchard::ShieldedData`] in this transaction, + /// regardless of version. + pub fn v5_orchard_shielded_data( + &self, + ) -> Option<&orchard::ShieldedData> { + match self { + Transaction::V5 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(orchard_shielded_data), + + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { + orchard_shielded_data: None, + .. + } => None, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Transaction::V6 { .. } => None, + } + } + /// Modify the [`orchard::ShieldedData`] in this transaction, /// regardless of version. pub fn v5_orchard_shielded_data_mut( diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index 7aec9f9da77..7ca85f053b7 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -153,7 +153,7 @@ impl Transaction { ), > + 'static { ( - NetworkUpgrade::branch_id_strategy(), + NetworkUpgrade::nu5_branch_id_strategy(), any::(), any::(), transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS), @@ -793,7 +793,6 @@ impl Arbitrary for orchard::Shiel any::(), any::(), any::(), - any::(), vec( any::>(), 1..MAX_ARBITRARY_ITEMS, @@ -802,6 +801,32 @@ impl Arbitrary for orchard::Shiel #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] any::(), ) + .prop_flat_map(|props| { + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let (flags, value_balance, shared_anchor, actions, binding_sig) = props; + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let (flags, value_balance, shared_anchor, actions, binding_sig, burn) = props; + + // Since NU6.2, an Orchard proof must have the canonical length for its number of + // actions (`2272 * num_actions + 2720` bytes), otherwise it is rejected as + // non-canonical (GHSA-jfw5-j458-pfv6). The V5 txid is computed by round-tripping + // through `librustzcash`, which enforces this length, so a proof of any other + // size makes the round-trip (and thus `Transaction::hash`) fail. Generate a proof + // of exactly the expected length, which depends on the number of actions. + let proof_size = + orchard::shielded_data::expected_proof_size::(actions.len()); + ( + Just(flags), + Just(value_balance), + Just(shared_anchor), + vec(any::(), proof_size).prop_map(Halo2Proof), + Just(actions), + Just(binding_sig), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Just(burn), + ) + }) .prop_map(|props| { #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] let (flags, value_balance, shared_anchor, proof, actions, binding_sig) = props; @@ -879,7 +904,10 @@ impl Arbitrary for Transaction { NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => { Self::v4_strategy(ledger_state) } - NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 => prop_oneof![ + NetworkUpgrade::Nu5 + | NetworkUpgrade::Nu6 + | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 => prop_oneof![ Self::v4_strategy(ledger_state.clone()), Self::v5_strategy(ledger_state) ] @@ -935,6 +963,7 @@ impl Arbitrary for VerifiedUnminedTx { any::(), any::>(), any::(), + any::(), any::<(u16, u16)>().prop_map(|(unpaid_actions, conventional_actions)| { ( unpaid_actions % conventional_actions.saturating_add(1), @@ -951,6 +980,7 @@ impl Arbitrary for VerifiedUnminedTx { transaction, miner_fee, sigops, + p2sh_sigops, (conventional_actions, mut unpaid_actions), fee_weight_ratio, time, @@ -968,6 +998,7 @@ impl Arbitrary for VerifiedUnminedTx { transaction, miner_fee, legacy_sigop_count: sigops, + p2sh_sigop_count: p2sh_sigops, conventional_actions, unpaid_actions, fee_weight_ratio, diff --git a/zebra-chain/src/transaction/auth_digest.rs b/zebra-chain/src/transaction/auth_digest.rs index 782bdcd4f29..36ef22ff9a8 100644 --- a/zebra-chain/src/transaction/auth_digest.rs +++ b/zebra-chain/src/transaction/auth_digest.rs @@ -1,6 +1,6 @@ //! Authorizing digests for Zcash transactions. -use std::{fmt, sync::Arc}; +use std::{array::TryFromSliceError, fmt, sync::Arc}; use hex::{FromHex, ToHex}; @@ -77,6 +77,14 @@ impl From<[u8; 32]> for AuthDigest { } } +impl TryFrom<&[u8]> for AuthDigest { + type Error = TryFromSliceError; + + fn try_from(bytes: &[u8]) -> Result { + Ok(AuthDigest(bytes.try_into()?)) + } +} + impl From for [u8; 32] { fn from(auth_digest: AuthDigest) -> Self { auth_digest.0 diff --git a/zebra-chain/src/transaction/hash.rs b/zebra-chain/src/transaction/hash.rs index 06d6d24dfad..4fa228313ad 100644 --- a/zebra-chain/src/transaction/hash.rs +++ b/zebra-chain/src/transaction/hash.rs @@ -62,6 +62,12 @@ use super::{txid::TxIdBuilder, AuthDigest, Transaction}; #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub struct Hash(pub [u8; 32]); +impl AsRef<[u8; 32]> for Hash { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + impl From for Hash { fn from(transaction: Transaction) -> Self { // use the ref implementation, to avoid cloning the transaction @@ -90,6 +96,12 @@ impl From<[u8; 32]> for Hash { } } +impl From<&[u8; 32]> for Hash { + fn from(bytes: &[u8; 32]) -> Self { + Self::from(*bytes) + } +} + impl From for [u8; 32] { fn from(hash: Hash) -> Self { hash.0 diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 600cfd05459..0b8d4ffb2f3 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -15,16 +15,14 @@ use crate::{ primitives::{Halo2Proof, ZkSnarkProof}, serialization::{ zcash_deserialize_external_count, zcash_serialize_empty_list, - zcash_serialize_external_count, AtLeastOne, ReadZcashExt, SerializationError, - TrustedPreallocate, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, + zcash_serialize_external_count, AtLeastOne, CompactSizeMessage, ReadZcashExt, + SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashDeserializeInto, + ZcashSerialize, }, }; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use crate::{ - orchard::OrchardZSA, orchard_zsa::NoBurn, parameters::TX_V6_VERSION_GROUP_ID, - serialization::CompactSizeMessage, -}; +use crate::{orchard::OrchardZSA, orchard_zsa::NoBurn, parameters::TX_V6_VERSION_GROUP_ID}; use super::*; use crate::sapling; @@ -119,217 +117,344 @@ where // range, so we can implement its serialization and deserialization separately. // (Unlike V4, where it must be serialized as part of the transaction.) -impl ZcashSerialize for Option> { - fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - match self { - None => { - // Denoted as `nSpendsSapling` in the spec. - zcash_serialize_empty_list(&mut writer)?; - // Denoted as `nOutputsSapling` in the spec. - zcash_serialize_empty_list(&mut writer)?; - } - Some(sapling_shielded_data) => { - sapling_shielded_data.zcash_serialize(&mut writer)?; - } - } - Ok(()) +impl ZcashSerialize for sapling::ShieldedData { + fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { + // This `ZcashSerialize` implementation is valid for V5 only. + // V6 code must not call it and must use the inner serializer instead. + serialize_v5_sapling_shielded_data_inner( + self, + writer, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + false, + ) } } -impl ZcashSerialize for sapling::ShieldedData { - fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - // Collect arrays for Spends - // There's no unzip3, so we have to unzip twice. - let (spend_prefixes, spend_proofs_sigs): (Vec<_>, Vec<_>) = self - .spends() - .cloned() - .map(sapling::Spend::::into_v5_parts) - .map(|(prefix, proof, sig)| (prefix, (proof, sig))) - .unzip(); - let (spend_proofs, spend_sigs) = spend_proofs_sigs.into_iter().unzip(); +pub(crate) fn serialize_v5_sapling_shielded_data( + shielded_data: &Option>, + mut writer: W, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] use_versioned_signatures: bool, +) -> Result<(), io::Error> { + match shielded_data { + None => { + // Denoted as `nSpendsSapling` in the spec. + zcash_serialize_empty_list(&mut writer)?; + + // Denoted as `nOutputsSapling` in the spec. + zcash_serialize_empty_list(&mut writer)?; + } + Some(sapling_shielded_data) => { + serialize_v5_sapling_shielded_data_inner( + sapling_shielded_data, + &mut writer, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + use_versioned_signatures, + )?; + } + } - // Collect arrays for Outputs - let (output_prefixes, output_proofs): (Vec<_>, _) = self - .outputs() - .cloned() - .map(sapling::Output::into_v5_parts) - .unzip(); + Ok(()) +} - // Denoted as `nSpendsSapling` and `vSpendsSapling` in the spec. - spend_prefixes.zcash_serialize(&mut writer)?; - // Denoted as `nOutputsSapling` and `vOutputsSapling` in the spec. - output_prefixes.zcash_serialize(&mut writer)?; +fn serialize_v5_sapling_shielded_data_inner( + shielded_data: &sapling::ShieldedData, + mut writer: W, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] use_versioned_signatures: bool, +) -> Result<(), io::Error> { + // Collect arrays for Spends + // There's no unzip3, so we have to unzip twice. + let (spend_prefixes, spend_proofs_sigs): (Vec<_>, Vec<_>) = shielded_data + .spends() + .cloned() + .map(sapling::Spend::::into_v5_parts) + .map(|(prefix, proof, sig)| (prefix, (proof, sig))) + .unzip(); + let (spend_proofs, spend_sigs): (_, Vec<_>) = spend_proofs_sigs.into_iter().unzip(); + + // Collect arrays for Outputs + let (output_prefixes, output_proofs): (Vec<_>, _) = shielded_data + .outputs() + .cloned() + .map(sapling::Output::into_v5_parts) + .unzip(); + + // Denoted as `nSpendsSapling` and `vSpendsSapling` in the spec. + spend_prefixes.zcash_serialize(&mut writer)?; + // Denoted as `nOutputsSapling` and `vOutputsSapling` in the spec. + output_prefixes.zcash_serialize(&mut writer)?; + + // Denoted as `valueBalanceSapling` in the spec. + shielded_data.value_balance.zcash_serialize(&mut writer)?; + + // Denoted as `anchorSapling` in the spec. + // `TransferData` ensures this field is only present when there is at + // least one spend. + if let Some(shared_anchor) = shielded_data.shared_anchor() { + writer.write_all(&<[u8; 32]>::from(shared_anchor)[..])?; + } - // Denoted as `valueBalanceSapling` in the spec. - self.value_balance.zcash_serialize(&mut writer)?; + // Denoted as `vSpendProofsSapling` in the spec. + zcash_serialize_external_count(&spend_proofs, &mut writer)?; - // Denoted as `anchorSapling` in the spec. - // `TransferData` ensures this field is only present when there is at - // least one spend. - if let Some(shared_anchor) = self.shared_anchor() { - writer.write_all(&<[u8; 32]>::from(shared_anchor)[..])?; - } + // Denoted as `vSpendAuthSigsSapling` in the spec. + // + // V5 stores raw spend authorization signatures. + // V6 stores them as `VersionedSigV0` wrappers. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + if use_versioned_signatures { + let versioned_spend_sigs: Vec<_> = + spend_sigs.into_iter().map(VersionedSigV0::new).collect(); - // Denoted as `vSpendProofsSapling` in the spec. - zcash_serialize_external_count(&spend_proofs, &mut writer)?; - // Denoted as `vSpendAuthSigsSapling` in the spec. - zcash_serialize_external_count(&spend_sigs, &mut writer)?; + zcash_serialize_external_count(&versioned_spend_sigs, &mut writer)?; + } else { + zcash_serialize_external_count(&spend_sigs, &mut writer)?; + } + } - // Denoted as `vOutputProofsSapling` in the spec. - zcash_serialize_external_count(&output_proofs, &mut writer)?; + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + zcash_serialize_external_count(&spend_sigs, &mut writer)?; - // Denoted as `bindingSigSapling` in the spec. - writer.write_all(&<[u8; 64]>::from(self.binding_sig)[..])?; + // Denoted as `vOutputProofsSapling` in the spec. + zcash_serialize_external_count(&output_proofs, &mut writer)?; - Ok(()) + // Denoted as `bindingSigSapling` in the spec. + // + // V5 stores a raw 64-byte binding signature. + // V6 stores it as a `VersionedSigV0` wrapper. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + if use_versioned_signatures { + VersionedSigV0::new(shielded_data.binding_sig).zcash_serialize(&mut writer)?; + } else { + writer.write_all(&<[u8; 64]>::from(shielded_data.binding_sig)[..])?; + } } + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + writer.write_all(&<[u8; 64]>::from(shielded_data.binding_sig)[..])?; + + Ok(()) } // we can't split ShieldedData out of Option deserialization, // because the counts are read along with the arrays. impl ZcashDeserialize for Option> { #[allow(clippy::unwrap_in_result)] - fn zcash_deserialize(mut reader: R) -> Result { - // Denoted as `nSpendsSapling` and `vSpendsSapling` in the spec. - let spend_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; - - // Denoted as `nOutputsSapling` and `vOutputsSapling` in the spec. - let output_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; + fn zcash_deserialize(reader: R) -> Result { + deserialize_v5_sapling_shielded_data( + reader, + false, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + false, + ) + } +} - // nSpendsSapling and nOutputsSapling as variables - let spends_count = spend_prefixes.len(); - let outputs_count = output_prefixes.len(); +/// Deserialize V5/V6 Sapling shielded data with an optional early coinbase +/// rejection. +/// +/// When `is_coinbase` is true, a non-zero `nSpendsSapling` count is rejected +/// **before** allocating the spend vector, closing the late-validation gap +/// described in GHSA-rgwx-8r98-p34c. +/// +/// `use_versioned_signatures` selects raw V5 signatures or V6 +/// `VersionedSigV0` signatures. +#[allow(clippy::unwrap_in_result)] +pub(crate) fn deserialize_v5_sapling_shielded_data( + mut reader: R, + is_coinbase: bool, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] use_versioned_signatures: bool, +) -> Result>, SerializationError> { + // Denoted as `nSpendsSapling` in the spec — read count before allocating. + let spend_count: CompactSizeMessage = (&mut reader).zcash_deserialize_into()?; + let spend_count: usize = spend_count.into(); + + // # Consensus + // + // > A coinbase transaction MUST NOT have any Spend descriptions. + // + // + // + // Reject before allocating to prevent a peer from forcing thousands of + // spend-prefix allocations for a transaction that will always be invalid. + if is_coinbase && spend_count > 0 { + return Err(SerializationError::Parse( + "coinbase transaction must not have Sapling spends", + )); + } - // All the other fields depend on having spends or outputs - if spend_prefixes.is_empty() && output_prefixes.is_empty() { - return Ok(None); - } + // Denoted as `vSpendsSapling` in the spec. + let spend_prefixes: Vec = + zcash_deserialize_external_count(spend_count, &mut reader)?; - // Denoted as `valueBalanceSapling` in the spec. - let value_balance = (&mut reader).zcash_deserialize_into()?; + // Denoted as `nOutputsSapling` and `vOutputsSapling` in the spec. + let output_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; - // Denoted as `anchorSapling` in the spec. - // - // # Consensus - // - // > Elements of a Spend description MUST be valid encodings of the types given above. - // - // https://zips.z.cash/protocol/protocol.pdf#spenddesc - // - // Type is `B^{[ℓ_{Sapling}_{Merkle}]}`, i.e. 32 bytes - // - // > LEOS2IP_{256}(anchorSapling), if present, MUST be less than 𝑞_𝕁. - // - // https://zips.z.cash/protocol/protocol.pdf#spendencodingandconsensus - // - // Validated in [`crate::sapling::tree::Root::zcash_deserialize`]. - let shared_anchor = if spends_count > 0 { - Some((&mut reader).zcash_deserialize_into()?) - } else { - None - }; + // nSpendsSapling and nOutputsSapling as variables + let spends_count = spend_prefixes.len(); + let outputs_count = output_prefixes.len(); - // Denoted as `vSpendProofsSapling` in the spec. - // - // # Consensus - // - // > Elements of a Spend description MUST be valid encodings of the types given above. - // - // https://zips.z.cash/protocol/protocol.pdf#spenddesc - // - // Type is `ZKSpend.Proof`, described in - // https://zips.z.cash/protocol/protocol.pdf#grothencoding - // It is not enforced here; this just reads 192 bytes. - // The type is validated when validating the proof, see - // [`groth16::Item::try_from`]. In #3179 we plan to validate here instead. - let spend_proofs = zcash_deserialize_external_count(spends_count, &mut reader)?; - - // Denoted as `vSpendAuthSigsSapling` in the spec. - // - // # Consensus - // - // > Elements of a Spend description MUST be valid encodings of the types given above. - // - // https://zips.z.cash/protocol/protocol.pdf#spenddesc - // - // Type is SpendAuthSig^{Sapling}.Signature, i.e. - // B^Y^{[ceiling(ℓ_G/8) + ceiling(bitlength(𝑟_G)/8)]} i.e. 64 bytes - // https://zips.z.cash/protocol/protocol.pdf#concretereddsa - // See [`redjubjub::Signature::zcash_deserialize`]. - let spend_sigs = zcash_deserialize_external_count(spends_count, &mut reader)?; + // All the other fields depend on having spends or outputs + if spend_prefixes.is_empty() && output_prefixes.is_empty() { + return Ok(None); + } - // Denoted as `vOutputProofsSapling` in the spec. - // - // # Consensus - // - // > Elements of an Output description MUST be valid encodings of the types given above. - // - // https://zips.z.cash/protocol/protocol.pdf#outputdesc - // - // Type is `ZKOutput.Proof`, described in - // https://zips.z.cash/protocol/protocol.pdf#grothencoding - // It is not enforced here; this just reads 192 bytes. - // The type is validated when validating the proof, see - // [`groth16::Item::try_from`]. In #3179 we plan to validate here instead. - let output_proofs = zcash_deserialize_external_count(outputs_count, &mut reader)?; - - // Denoted as `bindingSigSapling` in the spec. - let binding_sig = reader.read_64_bytes()?.into(); - - // Create shielded spends from deserialized parts - let spends: Vec<_> = spend_prefixes - .into_iter() - .zip(spend_proofs) - .zip(spend_sigs) - .map(|((prefix, proof), sig)| { - sapling::Spend::::from_v5_parts(prefix, proof, sig) - }) - .collect(); + // Denoted as `valueBalanceSapling` in the spec. + let value_balance = (&mut reader).zcash_deserialize_into()?; + + // Denoted as `anchorSapling` in the spec. + // + // # Consensus + // + // > Elements of a Spend description MUST be valid encodings of the types given above. + // + // https://zips.z.cash/protocol/protocol.pdf#spenddesc + // + // Type is `B^{[ℓ_{Sapling}_{Merkle}]}`, i.e. 32 bytes + // + // > LEOS2IP_{256}(anchorSapling), if present, MUST be less than 𝑞_𝕁. + // + // https://zips.z.cash/protocol/protocol.pdf#spendencodingandconsensus + // + // Validated in [`crate::sapling::tree::Root::zcash_deserialize`]. + let shared_anchor = if spends_count > 0 { + Some((&mut reader).zcash_deserialize_into()?) + } else { + None + }; + + // Denoted as `vSpendProofsSapling` in the spec. + // + // # Consensus + // + // > Elements of a Spend description MUST be valid encodings of the types given above. + // + // https://zips.z.cash/protocol/protocol.pdf#spenddesc + // + // Type is `ZKSpend.Proof`, described in + // https://zips.z.cash/protocol/protocol.pdf#grothencoding + // It is not enforced here; this just reads 192 bytes. + // The type is validated when validating the proof, see + // [`groth16::Item::try_from`]. In #3179 we plan to validate here instead. + let spend_proofs = zcash_deserialize_external_count(spends_count, &mut reader)?; + + // Denoted as `vSpendAuthSigsSapling` in the spec. + // + // V5 stores raw spend authorization signatures. + // V6 stores them as `VersionedSigV0` wrappers, which are unwrapped after + // deserialization. + // + // # Consensus + // + // > Elements of a Spend description MUST be valid encodings of the types given above. + // + // https://zips.z.cash/protocol/protocol.pdf#spenddesc + // + // Type is SpendAuthSig^{Sapling}.Signature, i.e. + // B^Y^{[ceiling(ℓ_G/8) + ceiling(bitlength(𝑟_G)/8)]} i.e. 64 bytes + // https://zips.z.cash/protocol/protocol.pdf#concretereddsa + // See [`redjubjub::Signature::zcash_deserialize`]. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let spend_sigs: Vec> = if use_versioned_signatures { + let versioned_sigs: Vec>> = + zcash_deserialize_external_count(spends_count, &mut reader)?; - // Create shielded outputs from deserialized parts - let outputs = output_prefixes + versioned_sigs .into_iter() - .zip(output_proofs) - .map(|(prefix, proof)| sapling::Output::from_v5_parts(prefix, proof)) - .collect(); - - // Create transfers - // - // # Consensus - // - // > The anchor of each Spend description MUST refer to some earlier - // > block’s final Sapling treestate. The anchor is encoded separately - // > in each Spend description for v4 transactions, or encoded once and - // > shared between all Spend descriptions in a v5 transaction. - // - // - // - // This rule is also implemented in - // [`zebra_state::service::check::anchor`] and - // [`zebra_chain::sapling::spend`]. - // - // The "anchor encoding for v5 transactions" is implemented here. - let transfers = match shared_anchor { - Some(shared_anchor) => sapling::TransferData::SpendsAndMaybeOutputs { - shared_anchor, - spends: spends - .try_into() - .expect("checked spends when parsing shared anchor"), - maybe_outputs: outputs, - }, - None => sapling::TransferData::JustOutputs { - outputs: outputs - .try_into() - .expect("checked spends or outputs and returned early"), - }, - }; - - Ok(Some(sapling::ShieldedData { - value_balance, - transfers, - binding_sig, - })) - } + .map(VersionedSigV0::into_signature) + .collect() + } else { + zcash_deserialize_external_count(spends_count, &mut reader)? + }; + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let spend_sigs: Vec> = + zcash_deserialize_external_count(spends_count, &mut reader)?; + + // Denoted as `vOutputProofsSapling` in the spec. + // + // # Consensus + // + // > Elements of an Output description MUST be valid encodings of the types given above. + // + // https://zips.z.cash/protocol/protocol.pdf#outputdesc + // + // Type is `ZKOutput.Proof`, described in + // https://zips.z.cash/protocol/protocol.pdf#grothencoding + // It is not enforced here; this just reads 192 bytes. + // The type is validated when validating the proof, see + // [`groth16::Item::try_from`]. In #3179 we plan to validate here instead. + let output_proofs = zcash_deserialize_external_count(outputs_count, &mut reader)?; + + // Denoted as `bindingSigSapling` in the spec. + // + // V5 stores a raw 64-byte binding signature. + // V6 stores it as a `VersionedSigV0` wrapper, which is unwrapped after + // deserialization. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let binding_sig = if use_versioned_signatures { + VersionedSigV0::zcash_deserialize(&mut reader)?.into_signature() + } else { + reader.read_64_bytes()?.into() + }; + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let binding_sig = reader.read_64_bytes()?.into(); + + // Create shielded spends from deserialized parts + let spends: Vec<_> = spend_prefixes + .into_iter() + .zip(spend_proofs) + .zip(spend_sigs) + .map(|((prefix, proof), sig)| { + sapling::Spend::::from_v5_parts(prefix, proof, sig) + }) + .collect(); + + // Create shielded outputs from deserialized parts + let outputs = output_prefixes + .into_iter() + .zip(output_proofs) + .map(|(prefix, proof)| sapling::Output::from_v5_parts(prefix, proof)) + .collect(); + + // Create transfers + // + // # Consensus + // + // > The anchor of each Spend description MUST refer to some earlier + // > block’s final Sapling treestate. The anchor is encoded separately + // > in each Spend description for v4 transactions, or encoded once and + // > shared between all Spend descriptions in a v5 transaction. + // + // + // + // This rule is also implemented in + // [`zebra_state::service::check::anchor`] and + // [`zebra_chain::sapling::spend`]. + // + // The "anchor encoding for v5 transactions" is implemented here. + let transfers = match shared_anchor { + Some(shared_anchor) => sapling::TransferData::SpendsAndMaybeOutputs { + shared_anchor, + spends: spends + .try_into() + .expect("checked spends when parsing shared anchor"), + maybe_outputs: outputs, + }, + None => sapling::TransferData::JustOutputs { + outputs: outputs + .try_into() + .expect("checked spends or outputs and returned early"), + }, + }; + + Ok(Some(sapling::ShieldedData { + value_balance, + transfers, + binding_sig, + })) } impl ZcashSerialize for Option> @@ -833,7 +958,11 @@ impl ZcashSerialize for Transaction { // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and // `bindingSigSapling`. - sapling_shielded_data.zcash_serialize(&mut writer)?; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + serialize_v5_sapling_shielded_data(sapling_shielded_data, &mut writer, false)?; + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + serialize_v5_sapling_shielded_data(sapling_shielded_data, &mut writer)?; // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, @@ -894,7 +1023,7 @@ impl ZcashSerialize for Transaction { // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and // `bindingSigSapling`. - sapling_v6::zcash_serialize_v6(sapling_shielded_data, &mut writer)?; + serialize_v5_sapling_shielded_data(sapling_shielded_data, &mut writer, true)?; // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, @@ -1039,11 +1168,14 @@ impl ZcashDeserialize for Transaction { // then assemble them. // Denoted as `tx_in_count` and `tx_in` in the spec. - let inputs = Vec::zcash_deserialize(&mut limited_reader)?; + let inputs: Vec = Vec::zcash_deserialize(&mut limited_reader)?; // Denoted as `tx_out_count` and `tx_out` in the spec. let outputs = Vec::zcash_deserialize(&mut limited_reader)?; + let is_coinbase = inputs.len() == 1 + && matches!(inputs.first(), Some(transparent::Input::Coinbase { .. })); + // Denoted as `lock_time` in the spec. let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?; @@ -1053,8 +1185,25 @@ impl ZcashDeserialize for Transaction { // Denoted as `valueBalanceSapling` in the spec. let value_balance = (&mut limited_reader).zcash_deserialize_into()?; - // Denoted as `nSpendsSapling` and `vSpendsSapling` in the spec. - let shielded_spends = Vec::zcash_deserialize(&mut limited_reader)?; + // Denoted as `nSpendsSapling` — read count before allocating. + let spend_count: CompactSizeMessage = + (&mut limited_reader).zcash_deserialize_into()?; + let spend_count: usize = spend_count.into(); + + // # Consensus + // + // > A coinbase transaction MUST NOT have any Spend descriptions. + // + // + if is_coinbase && spend_count > 0 { + return Err(SerializationError::Parse( + "coinbase transaction must not have Sapling spends", + )); + } + + // Denoted as `vSpendsSapling` in the spec. + let shielded_spends: Vec> = + zcash_deserialize_external_count(spend_count, &mut limited_reader)?; // Denoted as `nOutputsSapling` and `vOutputsSapling` in the spec. let shielded_outputs = @@ -1123,6 +1272,18 @@ impl ZcashDeserialize for Transaction { let network_upgrade = NetworkUpgrade::try_from(limited_reader.read_u32::()?)?; + // # Consensus + // + // > [NU5 onward] The transaction version number MUST be 4 or 5. + // + // V5 transactions are only valid from NU5 onward, so reject + // transactions with pre-NU5 consensus branch IDs. + if network_upgrade < NetworkUpgrade::Nu5 { + return Err(SerializationError::Parse( + "v5 transaction must have NU5 or later consensus branch ID", + )); + } + // Denoted as `lock_time` in the spec. let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?; @@ -1130,23 +1291,32 @@ impl ZcashDeserialize for Transaction { let expiry_height = block::Height(limited_reader.read_u32::()?); // Denoted as `tx_in_count` and `tx_in` in the spec. - let inputs = Vec::zcash_deserialize(&mut limited_reader)?; + let inputs: Vec = Vec::zcash_deserialize(&mut limited_reader)?; // Denoted as `tx_out_count` and `tx_out` in the spec. let outputs = Vec::zcash_deserialize(&mut limited_reader)?; + let is_coinbase = inputs.len() == 1 + && matches!(inputs.first(), Some(transparent::Input::Coinbase { .. })); + // A bundle of fields denoted in the spec as `nSpendsSapling`, `vSpendsSapling`, // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and // `bindingSigSapling`. - let sapling_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let sapling_shielded_data = + deserialize_v5_sapling_shielded_data(&mut limited_reader, is_coinbase, false)?; + + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + let sapling_shielded_data = + deserialize_v5_sapling_shielded_data(&mut limited_reader, is_coinbase)?; // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; - Ok(Transaction::V5 { + let tx = Transaction::V5 { network_upgrade, lock_time, expiry_height, @@ -1154,7 +1324,11 @@ impl ZcashDeserialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, - }) + }; + + tx.to_librustzcash(network_upgrade)?; + + Ok(tx) } #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] (6, true) => { @@ -1167,7 +1341,13 @@ impl ZcashDeserialize for Transaction { // Convert it to a NetworkUpgrade let network_upgrade = NetworkUpgrade::try_from(limited_reader.read_u32::()?)?; - + // V6 transactions are only valid from NU7 onward, so reject + // transactions with pre-NU7 consensus branch IDs. + if network_upgrade < NetworkUpgrade::Nu7 { + return Err(SerializationError::Parse( + "v6 transaction must have NU7 or later consensus branch ID", + )); + } // Denoted as `lock_time` in the spec. let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?; @@ -1178,11 +1358,14 @@ impl ZcashDeserialize for Transaction { let zip233_amount = (&mut limited_reader).zcash_deserialize_into()?; // Denoted as `tx_in_count` and `tx_in` in the spec. - let inputs = Vec::zcash_deserialize(&mut limited_reader)?; + let inputs: Vec = Vec::zcash_deserialize(&mut limited_reader)?; // Denoted as `tx_out_count` and `tx_out` in the spec. let outputs = Vec::zcash_deserialize(&mut limited_reader)?; + let is_coinbase = inputs.len() == 1 + && matches!(inputs.first(), Some(transparent::Input::Coinbase { .. })); + // Denoted as `vSighashInfo` in the spec (ZIP-230). // There is one `TransparentSighashInfo` per transparent input (tx_in_count entries). // For now, only V0 is supported, which must decode to a Vector == [0x00]. @@ -1194,7 +1377,8 @@ impl ZcashDeserialize for Transaction { // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and // `bindingSigSapling`. - let sapling_shielded_data = sapling_v6::zcash_deserialize_v6(&mut limited_reader)?; + let sapling_shielded_data = + deserialize_v5_sapling_shielded_data(&mut limited_reader, is_coinbase, true)?; // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, @@ -1369,9 +1553,7 @@ impl FromHex for SerializedTransaction { mod sapling_v6 { use super::*; - use redjubjub::{Binding, Signature, SpendAuth}; - - type SaplingShieldedData = sapling::ShieldedData; + use redjubjub::{Binding, Signature}; impl ZcashSerialize for Signature { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { @@ -1385,151 +1567,4 @@ mod sapling_v6 { Ok(reader.read_64_bytes()?.into()) } } - - pub(super) fn zcash_serialize_v6( - shielded_data: &Option, - mut writer: W, - ) -> Result<(), io::Error> { - match shielded_data { - None => { - // Same as V5: empty spend and output lists - zcash_serialize_empty_list(&mut writer)?; - zcash_serialize_empty_list(&mut writer)?; - } - Some(sapling_shielded_data) => { - zcash_serialize_v6_inner(sapling_shielded_data, &mut writer)?; - } - } - Ok(()) - } - - fn zcash_serialize_v6_inner( - shielded_data: &SaplingShieldedData, - mut writer: W, - ) -> Result<(), io::Error> { - // V6 difference: wrap spend auth signatures with VersionedSigV0 - let (spend_prefixes, spend_proofs_sigs): (Vec<_>, Vec<_>) = shielded_data - .spends() - .cloned() - .map(sapling::Spend::::into_v5_parts) - .map(|(prefix, proof, sig)| (prefix, (proof, VersionedSigV0::new(sig)))) - .unzip(); - let (spend_proofs, spend_sigs) = spend_proofs_sigs.into_iter().unzip(); - - // Same as V5: collect output parts - let (output_prefixes, output_proofs): (Vec<_>, _) = shielded_data - .outputs() - .cloned() - .map(sapling::Output::into_v5_parts) - .unzip(); - - // Same as V5: serialize spend/output prefixes - spend_prefixes.zcash_serialize(&mut writer)?; - output_prefixes.zcash_serialize(&mut writer)?; - - // Same as V5: value balance - shielded_data.value_balance.zcash_serialize(&mut writer)?; - - // Same as V5: shared anchor (if spends present) - if let Some(shared_anchor) = shielded_data.shared_anchor() { - writer.write_all(&<[u8; 32]>::from(shared_anchor)[..])?; - } - - // Same as V5: spend proofs - zcash_serialize_external_count(&spend_proofs, &mut writer)?; - - // V6 difference: versioned spend auth signatures - zcash_serialize_external_count(&spend_sigs, &mut writer)?; - - // Same as V5: output proofs - zcash_serialize_external_count(&output_proofs, &mut writer)?; - - // V6 difference: versioned binding signature - VersionedSigV0::new(shielded_data.binding_sig).zcash_serialize(&mut writer)?; - - Ok(()) - } - - #[allow(clippy::unwrap_in_result)] - pub(super) fn zcash_deserialize_v6( - mut reader: R, - ) -> Result, SerializationError> { - // Same as V5: deserialize spend/output prefixes - let spend_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; - let output_prefixes: Vec<_> = (&mut reader).zcash_deserialize_into()?; - - let spends_count = spend_prefixes.len(); - let outputs_count = output_prefixes.len(); - - // Same as V5: return None if no spends or outputs - if spend_prefixes.is_empty() && output_prefixes.is_empty() { - return Ok(None); - } - - // Same as V5: value balance - let value_balance = (&mut reader).zcash_deserialize_into()?; - - // Same as V5: shared anchor (if spends present) - let shared_anchor = if spends_count > 0 { - Some((&mut reader).zcash_deserialize_into()?) - } else { - None - }; - - // Same as V5: spend proofs - let spend_proofs = zcash_deserialize_external_count(spends_count, &mut reader)?; - - // V6 difference: deserialize versioned spend auth signatures - let spend_sigs: Vec>> = - zcash_deserialize_external_count(spends_count, &mut reader)?; - - // Same as V5: output proofs - let output_proofs = zcash_deserialize_external_count(outputs_count, &mut reader)?; - - // V6 difference: deserialize versioned binding signature - let binding_sig = VersionedSigV0::zcash_deserialize(&mut reader)?.into_signature(); - - // V6 difference: unwrap versioned spend auth signatures - let spends: Vec<_> = spend_prefixes - .into_iter() - .zip(spend_proofs) - .zip(spend_sigs) - .map(|((prefix, proof), spend_sig)| { - sapling::Spend::::from_v5_parts( - prefix, - proof, - spend_sig.into_signature(), - ) - }) - .collect(); - - // Same as V5: create outputs from parts - let outputs = output_prefixes - .into_iter() - .zip(output_proofs) - .map(|(prefix, proof)| sapling::Output::from_v5_parts(prefix, proof)) - .collect(); - - // Same as V5: create transfers from spends/outputs - let transfers = match shared_anchor { - Some(shared_anchor) => sapling::TransferData::SpendsAndMaybeOutputs { - shared_anchor, - spends: spends - .try_into() - .expect("checked spends when parsing shared anchor"), - maybe_outputs: outputs, - }, - None => sapling::TransferData::JustOutputs { - outputs: outputs - .try_into() - .expect("checked spends or outputs and returned early"), - }, - }; - - Ok(Some(sapling::ShieldedData { - value_balance, - transfers, - binding_sig, - })) - } } diff --git a/zebra-chain/src/transaction/sighash.rs b/zebra-chain/src/transaction/sighash.rs index d1bae9c0ced..869bd92318f 100644 --- a/zebra-chain/src/transaction/sighash.rs +++ b/zebra-chain/src/transaction/sighash.rs @@ -10,7 +10,7 @@ use super::Transaction; use crate::parameters::NetworkUpgrade; use crate::{transparent, Error}; -use crate::primitives::zcash_primitives::{sighash, PrecomputedTxData}; +use crate::primitives::zcash_primitives::{sighash, sighash_v4_raw, PrecomputedTxData}; bitflags::bitflags! { /// The different SigHash types, as defined in @@ -124,6 +124,24 @@ impl SigHasher { ) } + /// Calculate the sighash for the current pre-V5 (V4) transaction using the + /// raw `hash_type` byte taken directly from the signature. + /// + /// This preserves non-canonical bits (e.g. `0x41`) in the preimage so that + /// the resulting digest matches `zcashd`'s pre-V5 sighash semantics. + /// Callers handling V5+ transactions must use [`SigHasher::sighash`]. + pub fn sighash_v4_raw( + &self, + raw_hash_type: u8, + input_index_script_code: Option<(usize, Vec)>, + ) -> SigHash { + sighash_v4_raw( + &self.precomputed_tx_data, + raw_hash_type, + input_index_script_code, + ) + } + /// Returns the Orchard bundle in the precomputed transaction data. pub fn orchard_bundle( &self, diff --git a/zebra-chain/src/transaction/tests/prop.rs b/zebra-chain/src/transaction/tests/prop.rs index 09a9dbeac7e..6891cff6f2f 100644 --- a/zebra-chain/src/transaction/tests/prop.rs +++ b/zebra-chain/src/transaction/tests/prop.rs @@ -22,16 +22,28 @@ proptest! { fn transaction_roundtrip(tx in any::()) { let _init_guard = zebra_test::init(); + let has_coinbase_sapling_spends = tx.is_coinbase() + && tx.sapling_spends_per_anchor().count() > 0; + let data = tx.zcash_serialize_to_vec().expect("tx should serialize"); - let tx2 = data.zcash_deserialize_into().expect("randomized tx should deserialize"); - prop_assert_eq![&tx, &tx2]; + if has_coinbase_sapling_spends { + // GHSA-rgwx-8r98-p34c fix: the parser now rejects coinbase + // transactions with Sapling spends before allocating. + data.zcash_deserialize_into::() + .expect_err("coinbase with Sapling spends must be rejected"); + } else { + let tx2 = data.zcash_deserialize_into() + .expect("randomized tx should deserialize"); + + prop_assert_eq![&tx, &tx2]; - let data2 = tx2 - .zcash_serialize_to_vec() - .expect("vec serialization is infallible"); + let data2 = tx2 + .zcash_serialize_to_vec() + .expect("vec serialization is infallible"); - prop_assert_eq![data, data2, "data must be equal if structs are equal"]; + prop_assert_eq![data, data2, "data must be equal if structs are equal"]; + } } #[test] diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index 1d67a5a5525..fa821be2467 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -843,6 +843,43 @@ fn zip244_sighash() -> Result<()> { Ok(()) } +/// Real Orchard proofs from mined transactions must have the canonical size, and padding +/// a proof with trailing bytes must make it non-canonical (GHSA-jfw5-j458-pfv6). This +/// also cross-checks `expected_proof_size` against real proofs produced by the chain. +#[test] +fn orchard_proof_size_is_canonical() { + let mut checked = 0; + + for net in Network::iter() { + for tx in v5_transactions(net.block_iter()) { + let Some(shielded_data) = tx.v5_orchard_shielded_data() else { + continue; + }; + + // A real, mined Orchard proof has the canonical length for its actions. + assert!( + shielded_data.proof_size_is_canonical(), + "a real Orchard proof should be canonically sized" + ); + + // Padding the proof with trailing data must break canonicity. + let mut padded = shielded_data.clone(); + padded.proof.0.push(0); + assert!( + !padded.proof_size_is_canonical(), + "a padded Orchard proof must not be considered canonical" + ); + + checked += 1; + } + } + + assert!( + checked > 0, + "expected at least one Orchard transaction in the test vectors" + ); +} + #[test] fn consensus_branch_id() { for net in Network::iter() { @@ -1032,3 +1069,188 @@ fn test_coinbase_script() -> Result<()> { Ok(()) } + +/// Regression test for the Orchard `rk` identity-point DoS vulnerability. +/// +/// A v5 transaction whose Orchard action has `rk = [0u8; 32]` (the Pallas +/// identity point) **deserializes successfully** — Zebra performs no +/// identity-point check in [`crate::orchard::Action::zcash_deserialize`]. +/// +/// When the same transaction is subsequently fed to the Orchard Halo2 batch +/// verifier via [`orchard::bundle::BatchValidator::add_bundle`], the call +/// chain reaches `orchard::circuit::to_halo2_instance()`, which calls +/// `.coordinates().unwrap()` on the identity point. `coordinates()` returns +/// `None` for the identity, so the `unwrap` **panics**, crashing the node. +/// +/// ## Root cause +/// +/// `zebra-chain/src/orchard/action.rs:83` reads `rk` as raw bytes with no +/// identity-point check: `reader.read_32_bytes()?.into()`. The upstream +/// `orchard` crate defers validation to signature verification, but +/// `to_halo2_instance()` unwraps the coordinate extraction unconditionally. +/// +/// An analogous identity check already exists for `ephemeral_key` +/// (`zebra-chain/src/orchard/keys.rs:225-238`), demonstrating the correct +/// pattern. +#[test] +fn orchard_rk_identity_point() { + use group::prime::PrimeCurveAffine; + use reddsa::Signature; + + use crate::{ + at_least_one, + block::Height, + orchard::{ + keys::EphemeralPublicKey, tree, Action, AuthorizedAction, EncryptedNote, Flags, + NoteCommitment, Nullifier, ShieldedData, ValueCommitment, WrappedNoteKey, + }, + primitives::Halo2Proof, + serialization::ZcashSerialize, + }; + use halo2::pasta::pallas; + + let _init_guard = zebra_test::init(); + + // Construct an Orchard action with rk = [0u8; 32] (identity point). + // Other fields use the Pallas generator or the identity as appropriate. + let action = Action { + // cv can be any valid Pallas point; identity is accepted here. + cv: ValueCommitment(pallas::Affine::identity()), + nullifier: Nullifier(pallas::Base::zero()), + // rk = identity point — this is the vulnerability trigger. + rk: [0u8; 32].into(), + // cm_x is the x-coordinate of the note commitment. + cm_x: NoteCommitment(pallas::Affine::identity()).extract_x(), + // ephemeral_key must be non-identity; use the generator. + ephemeral_key: EphemeralPublicKey(pallas::Affine::generator()), + enc_ciphertext: EncryptedNote([0u8; 580]), + out_ciphertext: WrappedNoteKey([0u8; 80]), + }; + + let shielded_data = ShieldedData { + flags: Flags::ENABLE_SPENDS | Flags::ENABLE_OUTPUTS, + value_balance: crate::amount::Amount::try_from(0).expect("zero is a valid amount"), + shared_anchor: tree::Root::default(), + // An empty proof is accepted at deserialization time. + proof: Halo2Proof(vec![]), + actions: at_least_one![AuthorizedAction { + action, + spend_auth_sig: Signature::from([0u8; 64]), + }], + binding_sig: Signature::from([0u8; 64]), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + burn: Default::default(), + }; + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + inputs: vec![], + outputs: vec![], + sapling_shielded_data: None, + orchard_shielded_data: Some(shielded_data), + }; + + // Step 1: serialize the transaction. + let tx_bytes = tx + .zcash_serialize_to_vec() + .expect("crafted transaction must serialize without error"); + + // Step 2: deserialize + Transaction::zcash_deserialize(&tx_bytes[..]).expect_err("rk = identity should fail"); +} + +/// Reproduction for GHSA-rgwx-8r98-p34c: +/// Coinbase Sapling spend vectors allocate before zero-spend consensus rule. +/// +/// A V5 coinbase transaction with Sapling spends can be serialized and +/// deserialized — the parser allocates Sapling spend vectors (bounded by +/// `TrustedPreallocate::max_allocation()`) before any coinbase-specific +/// check. The consensus rule rejecting coinbase Sapling spends only runs +/// later in `zebra-consensus`, not during deserialization. +#[test] +fn coinbase_v5_with_sapling_spends_deserializes_successfully() { + let _init_guard = zebra_test::init(); + + let network = Network::Mainnet; + + // Find a real V4 transaction with Sapling spends from the test block vectors. + let tx_with_spends = arbitrary::test_transactions(&network) + .find(|(_, tx)| tx.sapling_spends_per_anchor().count() > 0); + + let Some((height, original_tx)) = tx_with_spends else { + panic!("test block vectors must contain at least one transaction with Sapling spends"); + }; + + let original_spend_count = original_tx.sapling_spends_per_anchor().count(); + assert!( + original_spend_count > 0, + "source transaction must have Sapling spends" + ); + + // Convert the V4 transaction to a fake V5 — this preserves valid Sapling data. + let fake_v5 = arbitrary::transaction_to_fake_v5(&original_tx, &network, height); + + // Replace transparent inputs with a single coinbase input. + let Transaction::V5 { + lock_time, + expiry_height, + outputs, + sapling_shielded_data, + orchard_shielded_data, + .. + } = fake_v5 + else { + panic!("transaction_to_fake_v5 must return V5"); + }; + + // Confirm the fake V5 still has Sapling spends. + let sapling_shielded_data = + sapling_shielded_data.expect("converted V5 must retain Sapling shielded data with spends"); + + let coinbase_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time, + expiry_height, + inputs: vec![transparent::Input::Coinbase { + height, + data: vec![0x00; 4], + sequence: 0xFFFF_FFFF, + }], + outputs: if outputs.is_empty() { + vec![transparent::Output { + value: crate::amount::Amount::zero(), + lock_script: Script::new(&[0u8; 20]), + }] + } else { + outputs + }, + sapling_shielded_data: Some(sapling_shielded_data), + orchard_shielded_data, + }; + + // The constructed transaction must look like a coinbase with Sapling spends. + assert!(coinbase_tx.is_coinbase(), "transaction must be coinbase"); + assert!( + coinbase_tx.sapling_spends_per_anchor().count() > 0, + "coinbase transaction has Sapling spends" + ); + + // Serialize it. + let serialized = coinbase_tx + .zcash_serialize_to_vec() + .expect("coinbase V5 with Sapling spends must serialize"); + + // Deserialize it — the parser must now reject coinbase transactions with + // Sapling spends before allocating spend vectors (GHSA-rgwx-8r98-p34c fix). + let err = serialized + .zcash_deserialize_into::() + .expect_err("coinbase with Sapling spends must be rejected during deserialization"); + + assert!( + err.to_string() + .contains("coinbase transaction must not have Sapling spends"), + "unexpected error: {err}" + ); +} diff --git a/zebra-chain/src/transaction/unmined.rs b/zebra-chain/src/transaction/unmined.rs index 403f6647168..6e6519660bc 100644 --- a/zebra-chain/src/transaction/unmined.rs +++ b/zebra-chain/src/transaction/unmined.rs @@ -343,6 +343,13 @@ pub struct VerifiedUnminedTx { /// `MAX_STANDARD_TX_SIGOPS`. pub legacy_sigop_count: u32, + /// The number of P2SH redeem-script signature operations in this transaction. + /// + /// This mirrors zcashd's `GetP2SHSigOpCount()`. It must be added to `legacy_sigop_count` for + /// the block-level `MAX_BLOCK_SIGOPS` check and for `getblocktemplate` sigop budgeting, + /// matching zcashd's consensus behavior. + pub p2sh_sigop_count: u32, + /// The number of conventional actions for `transaction`, as defined by [ZIP-317]. /// /// The number of actions is limited by [`MAX_BLOCK_BYTES`], so it fits in a u32. @@ -403,12 +410,13 @@ impl fmt::Display for VerifiedUnminedTx { } impl VerifiedUnminedTx { - /// Create a new verified unmined transaction from an unmined transaction, - /// its miner fee, its legacy sigop count, and the spent outputs for its transparent inputs. + /// Create a new verified unmined transaction from an unmined transaction, its miner fee, its + /// legacy and P2SH sigop counts, and the spent outputs for its transparent inputs. pub fn new( transaction: UnminedTx, miner_fee: Amount, legacy_sigop_count: u32, + p2sh_sigop_count: u32, spent_outputs: Arc>, tx_sighash: SigHash, ) -> Result { @@ -422,6 +430,7 @@ impl VerifiedUnminedTx { transaction, miner_fee, legacy_sigop_count, + p2sh_sigop_count, fee_weight_ratio, conventional_actions, unpaid_actions, @@ -432,6 +441,16 @@ impl VerifiedUnminedTx { }) } + /// The total number of transparent signature operations for block-level accounting: legacy + + /// P2SH. + /// + /// This is the value that must be used for the consensus `MAX_BLOCK_SIGOPS` limit and for + /// `getblocktemplate` sigop budgeting. + pub fn block_sigop_count(&self) -> u32 { + self.legacy_sigop_count + .saturating_add(self.p2sh_sigop_count) + } + /// Returns `true` if the transaction pays at least the [ZIP-317] conventional fee. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting diff --git a/zebra-chain/src/transaction/unmined/zip317.rs b/zebra-chain/src/transaction/unmined/zip317.rs index e094090b803..7e514a5f75c 100644 --- a/zebra-chain/src/transaction/unmined/zip317.rs +++ b/zebra-chain/src/transaction/unmined/zip317.rs @@ -17,6 +17,10 @@ use crate::{ #[cfg(test)] mod tests; +/// ZIP-227 ZSA issuance contribution to the ZIP-317 fee calculation. +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +mod zsa; + /// The marginal fee for the ZIP-317 fee calculation, in zatoshis per logical action. // // TODO: allow Amount in constants @@ -159,46 +163,15 @@ pub fn conventional_actions(transaction: &Transaction) -> u32 { let tx_out_logical_actions = div_ceil(tx_out_total_size, P2PKH_STANDARD_OUTPUT_SIZE); #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - let n_issue_actions = { - use zcash_primitives::transaction::fees::zip317::CREATION_COST; - - let n_issue_notes = transaction - .orchard_zsa_issue_data() - .map_or(0, |issue_data| issue_data.inner().get_all_notes().len()); - - // TODO: Calculate the real `n_asset_creations`. - // - // This is simplified to zero for now because ZIP-317 defines `nAssetCreations` using - // the Global Issuance State, so it cannot be calculated from the transaction alone. - // Doing the exact calculation requires additional logic for querying the state task - // from the consensus task. - // - // See also the ZIP-317 fee calculation in librustzcash: - // zcash_primitives/src/transaction/fees/zip317.rs, FeeRule::fee_required - let n_asset_creations = 0; - - n_issue_notes + (CREATION_COST * n_asset_creations) - }; - + let n_zsa_logical_actions = zsa::logical_actions(transaction); #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] - let n_issue_actions = 0; + let n_zsa_logical_actions = 0; let logical_actions = max(tx_in_logical_actions, tx_out_logical_actions) + 2 * n_join_split + max(n_spends_sapling, n_outputs_sapling) + n_actions_orchard - + n_issue_actions; - - // TODO: Add ZSA issuance-related ZIP-317 terms to logical_actions formula, like: - // - // let logical_actions = - // ... - // + n_zsa_issue_notes - // + CREATION_COST * n_asset_creations; - // - // librustzcash already includes these; see: - // zcash_primitives/src/transaction/fees/zip317.rs, FeeRule::fee_required - + + n_zsa_logical_actions; let logical_actions: u32 = logical_actions .try_into() .expect("transaction items are limited by serialized size limit"); diff --git a/zebra-chain/src/transaction/unmined/zip317/zsa.rs b/zebra-chain/src/transaction/unmined/zip317/zsa.rs new file mode 100644 index 00000000000..2e012801ff1 --- /dev/null +++ b/zebra-chain/src/transaction/unmined/zip317/zsa.rs @@ -0,0 +1,65 @@ +//! The [ZIP-227] ZSA issuance contribution to the [ZIP-317] fee calculation. +//! +//! Note that the 2026-06-26 revision of ZIP-317 removed the ZSA fee contributions; see its +//! [change history](https://zips.z.cash/zip-0317#section-1). +//! +//! [ZIP-227]: https://zips.z.cash/zip-0227.html +//! [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation + +use std::collections::HashSet; + +use zcash_primitives::transaction::fees::zip317::CREATION_COST; + +use crate::transaction::Transaction; + +/// Returns the number of ZIP-317 `logical_actions` contributed by ZIP-227 issuance in +/// `transaction`: `nIssueNotes + CREATION_COST * nReferenceNotes`. +/// +/// `nReferenceNotes` is the number of distinct Assets whose Issue Action in this transaction +/// starts with a [reference note]. +/// It is used in place of `nAssetCreations`, which the current ZIP-227 defines in terms of the +/// Global Issuance State and which therefore cannot be computed from the transaction alone: +/// +/// 1. ZIP-317 requires the conventional fee to be computable from only the public data of the +/// transaction. +/// 2. ZIP-227 chains the issuance state transaction-by-transaction within a block, so a +/// state-dependent count would depend on the transaction's position in the block — which is +/// itself decided by the block template algorithm from the conventional fee. +/// 3. `UnminedTx` caches the conventional fee at construction, so a state-dependent value would +/// go stale across reorgs and over the transaction's mempool lifetime. +/// 4. Reading the Global Issuance State to determine the fact of asset creation means a call +/// from `Verifier::call` in `zebra-consensus` into `zebra-state`, which runs as a separate +/// task. That is theoretically possible — see `mempool_best_chain_next_median_time_past` as +/// an example — but it complicates the code, slows its execution, and is subject to TOCTOU +/// races. +/// +/// A reference note is required on the first issuance of an Asset and optional on subsequent +/// issuance, so `nReferenceNotes >= nAssetCreations` and the fee can never be underpaid. It is +/// higher only if an issuer voluntarily places a reference note first when re-issuing an Asset, +/// which has no practical benefit and only raises their own fee. Counting distinct Assets rather +/// than Issue Actions keeps that overcount as small as the transaction data allows. +/// +/// [reference note]: https://zips.z.cash/zip-0227.html#reference-notes +// +// TODO: `nReferenceNotes` replaces `nAssetCreations` from ZIP-227. Either amend ZIP-227 to define +// the contribution in these terms, or switch back to `nAssetCreations` here. +pub fn logical_actions(transaction: &Transaction) -> usize { + let Some(issue_data) = transaction.orchard_zsa_issue_data() else { + return 0; + }; + let issue_bundle = issue_data.inner(); + + let n_issue_notes = issue_bundle.get_all_notes().len(); + + let n_reference_notes = issue_bundle + .actions() + .iter() + .filter_map(|action| action.get_reference_note()) + // An Asset is created at most once, so count each Asset once however many Issue + // Actions in this transaction carry a reference note for it. + .map(|note| note.asset().to_bytes()) + .collect::>() + .len(); + + n_issue_notes + (CREATION_COST * n_reference_notes) +} diff --git a/zebra-chain/src/transparent.rs b/zebra-chain/src/transparent.rs index ad671de1b15..8eb9a05c374 100644 --- a/zebra-chain/src/transparent.rs +++ b/zebra-chain/src/transparent.rs @@ -9,6 +9,7 @@ mod utxo; use std::{collections::HashMap, fmt, iter, ops::AddAssign}; +use zcash_script::{opcode::Evaluable as _, pattern::push_num}; use zcash_transparent::{address::TransparentAddress, bundle::TxOut}; use crate::{ @@ -17,11 +18,11 @@ use crate::{ parameters::Network, serialization::ZcashSerialize, transaction, + transparent::serialize::GENESIS_COINBASE_SCRIPT_SIG, }; pub use address::Address; pub use script::Script; -pub use serialize::{GENESIS_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN}; pub use utxo::{ new_ordered_outputs, new_outputs, outputs_from_utxos, utxos_from_ordered_utxos, CoinbaseSpendRestriction, OrderedUtxo, Utxo, @@ -52,70 +53,11 @@ use proptest_derive::Arbitrary; // TODO: change type to HeightDiff pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100; -/// Extra coinbase data that identifies some coinbase transactions generated by Zebra. -/// -// -// # Note -// -// rust-analyzer will crash in some editors when moving over an actual Zebra emoji, -// so we encode it here. This is a known issue in emacs-lsp and other lsp implementations: -// - https://github.com/rust-lang/rust-analyzer/issues/9121 -// - https://github.com/emacs-lsp/lsp-mode/issues/2080 -// - https://github.com/rust-lang/rust-analyzer/issues/13709 -pub const EXTRA_ZEBRA_COINBASE_DATA: &str = "z\u{1F993}"; - /// The rate used to calculate the dust threshold, in zatoshis per 1000 bytes. /// /// History: pub const ONE_THIRD_DUST_THRESHOLD_RATE: u32 = 100; -/// Arbitrary data inserted by miners into a coinbase transaction. -// -// TODO: rename to ExtraCoinbaseData, because height is also part of the coinbase data? -#[derive(Clone, Eq, PartialEq)] -#[cfg_attr( - any(test, feature = "proptest-impl", feature = "elasticsearch"), - derive(Serialize) -)] -pub struct CoinbaseData( - /// Invariant: this vec, together with the coinbase height, must be less than - /// 100 bytes. We enforce this by only constructing CoinbaseData fields by - /// parsing blocks with 100-byte data fields, and checking newly created - /// CoinbaseData lengths in the transaction builder. - pub(super) Vec, -); - -#[cfg(any(test, feature = "proptest-impl"))] -impl CoinbaseData { - /// Create a new `CoinbaseData` containing `data`. - /// - /// Only for use in tests. - pub fn new(data: Vec) -> CoinbaseData { - CoinbaseData(data) - } -} - -impl AsRef<[u8]> for CoinbaseData { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl std::fmt::Debug for CoinbaseData { - #[allow(clippy::unwrap_in_result)] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let escaped = String::from_utf8( - self.0 - .iter() - .cloned() - .flat_map(std::ascii::escape_default) - .collect(), - ) - .expect("ascii::escape_default produces utf8"); - f.debug_tuple("CoinbaseData").field(&escaped).finish() - } -} - /// OutPoint /// /// A particular transaction output reference. @@ -177,8 +119,9 @@ pub enum Input { Coinbase { /// The height of this block. height: block::Height, - /// Free data inserted by miners after the block height. - data: CoinbaseData, + /// Optional, arbitrary data miners can insert into a coinbase tx. + /// Limited to ~ 94 bytes. + data: Vec, /// The sequence number for the output. sequence: u32, }, @@ -203,7 +146,7 @@ impl fmt::Display for Input { let mut fmter = f.debug_struct("transparent::Input::Coinbase"); fmter.field("height", height); - fmter.field("data_len", &data.0.len()); + fmter.field("data_len", &data.len()); fmter.finish() } @@ -212,68 +155,30 @@ impl fmt::Display for Input { } impl Input { - /// Returns a new coinbase input for `height` with optional `data` and `sequence`. - /// - /// # Consensus - /// - /// The combined serialized size of `height` and `data` can be at most 100 bytes. - /// - /// > A coinbase transaction script MUST have length in {2 .. 100} bytes. - /// - /// - /// - /// # Panics - /// - /// If the coinbase data is greater than [`MAX_COINBASE_DATA_LEN`]. - pub fn new_coinbase(height: block::Height, data: Vec, sequence: Option) -> Input { - // `zcashd` includes an extra byte after the coinbase height in the coinbase data. We do - // that only if the data is empty to stay compliant with the following consensus rule: - // - // > A coinbase transaction script MUST have length in {2 .. 100} bytes. - // - // ## Rationale - // - // Coinbase heights < 17 are serialized as a single byte, and if there is no coinbase data, - // the script of a coinbase tx with such a height would consist only of this single byte, - // violating the consensus rule. - let data = if data.is_empty() { vec![0] } else { data }; - let data_limit = MAX_COINBASE_DATA_LEN - height.coinbase_zcash_serialized_size(); - - assert!( - data.len() <= data_limit, - "miner data has {} bytes, which exceeds the limit of {data_limit} bytes", - data.len(), - ); - - Input::Coinbase { - height, - data: CoinbaseData(data), - // If the caller does not specify the sequence number, use a sequence number that - // activates the LockTime. - sequence: sequence.unwrap_or(0), - } - } - - /// Returns the extra coinbase data in this input, if it is an [`Input::Coinbase`]. - pub fn extra_coinbase_data(&self) -> Option<&CoinbaseData> { + /// Returns the miner data in this input, if it is an [`Input::Coinbase`]. + pub fn miner_data(&self) -> Option<&Vec> { match self { - Input::PrevOut { .. } => None, Input::Coinbase { data, .. } => Some(data), + _ => None, } } - /// Returns the full coinbase script (the encoded height along with the - /// extra data) if this is an [`Input::Coinbase`]. Also returns `None` if - /// the coinbase is for the genesis block but does not match the expected - /// genesis coinbase data. + /// Returns the full coinbase script (the encoded height along with the optional miner data) if + /// this is an [`Input::Coinbase`]. Also returns `None` if the coinbase is for the genesis block + /// but does not match the expected genesis coinbase data. pub fn coinbase_script(&self) -> Option> { match self { Input::PrevOut { .. } => None, Input::Coinbase { height, data, .. } => { - let mut height_and_data = Vec::new(); - serialize::write_coinbase_height(*height, data, &mut height_and_data).ok()?; - height_and_data.extend(&data.0); - Some(height_and_data) + if height.is_min() { + (data.as_slice() == GENESIS_COINBASE_SCRIPT_SIG) + .then_some(GENESIS_COINBASE_SCRIPT_SIG.to_vec()) + } else { + let mut script = push_num(height.into()).to_bytes(); + script.extend_from_slice(data); + + Some(script) + } } } } diff --git a/zebra-chain/src/transparent/address.rs b/zebra-chain/src/transparent/address.rs index ccadc67544a..f3763e11b51 100644 --- a/zebra-chain/src/transparent/address.rs +++ b/zebra-chain/src/transparent/address.rs @@ -11,6 +11,7 @@ use crate::{ #[cfg(test)] use proptest::prelude::*; use zcash_address::{ToAddress, ZcashAddress}; +use zcash_transparent::address::TransparentAddress; /// Transparent Zcash Addresses /// @@ -25,7 +26,7 @@ use zcash_address::{ToAddress, ZcashAddress}; /// // TODO Remove this type and move to `TransparentAddress` in `zcash-transparent`. #[derive( - Clone, Eq, PartialEq, Hash, serde_with::SerializeDisplay, serde_with::DeserializeFromStr, + Clone, Copy, Eq, PartialEq, Hash, serde_with::SerializeDisplay, serde_with::DeserializeFromStr, )] pub enum Address { /// P2SH (Pay to Script Hash) addresses @@ -75,6 +76,20 @@ impl From
for ZcashAddress { } } +impl TryFrom
for TransparentAddress { + type Error = &'static str; + + fn try_from(taddr: Address) -> Result { + match taddr { + Address::PayToScriptHash { script_hash, .. } => Ok(Self::ScriptHash(script_hash)), + Address::PayToPublicKeyHash { pub_key_hash, .. } => { + Ok(Self::PublicKeyHash(pub_key_hash)) + } + Address::Tex { .. } => Err("TransparentAddress can't be a Tex address"), + } + } +} + impl fmt::Debug for Address { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut debug_struct = f.debug_struct("TransparentAddress"); diff --git a/zebra-chain/src/transparent/arbitrary.rs b/zebra-chain/src/transparent/arbitrary.rs index b2db6a8b411..97c0f726eb6 100644 --- a/zebra-chain/src/transparent/arbitrary.rs +++ b/zebra-chain/src/transparent/arbitrary.rs @@ -8,7 +8,7 @@ use crate::{ LedgerState, }; -use super::{Address, CoinbaseData, Input, OutPoint, Script, GENESIS_COINBASE_DATA}; +use super::{serialize::GENESIS_COINBASE_SCRIPT_SIG, Address, Input, OutPoint, Script}; impl Input { /// Construct a strategy for creating valid-ish vecs of Inputs. @@ -28,13 +28,13 @@ impl Arbitrary for Input { fn arbitrary_with(height: Self::Parameters) -> Self::Strategy { if let Some(height) = height { - (vec(any::(), 0..95), any::()) + (vec(any::(), 1..95), any::()) .prop_map(move |(data, sequence)| Input::Coinbase { height, - data: if height == block::Height(0) { - CoinbaseData(GENESIS_COINBASE_DATA.to_vec()) + data: if height.is_min() { + GENESIS_COINBASE_SCRIPT_SIG.to_vec() } else { - CoinbaseData(data) + data }, sequence, }) diff --git a/zebra-chain/src/transparent/serialize.rs b/zebra-chain/src/transparent/serialize.rs index dfab5d7e68d..ca2f58c127e 100644 --- a/zebra-chain/src/transparent/serialize.rs +++ b/zebra-chain/src/transparent/serialize.rs @@ -3,61 +3,98 @@ use std::io; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use zcash_script::{opcode::Evaluable, pattern}; +use zcash_transparent::coinbase::{MAX_COINBASE_SCRIPT_LEN, MIN_COINBASE_SCRIPT_LEN}; use crate::{ - block::{self, Height}, + block::Height, serialization::{ - zcash_serialize_bytes, FakeWriter, ReadZcashExt, SerializationError, ZcashDeserialize, - ZcashDeserializeInto, ZcashSerialize, + zcash_deserialize_bytes_external_count, CompactSizeMessage, ReadZcashExt, + SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, }, transaction, }; -use super::{CoinbaseData, Input, OutPoint, Output, Script}; - -/// The maximum length of the coinbase data. -/// -/// Includes the encoded coinbase height, if any. -/// -/// # Consensus -/// -/// > A coinbase transaction script MUST have length in {2 .. 100} bytes. -/// -/// -pub const MAX_COINBASE_DATA_LEN: usize = 100; - -/// The maximum length of the encoded coinbase height. -/// -/// # Consensus -/// -/// > The length of heightBytes MUST be in the range {1 .. 5}. Then the encoding is the length -/// > of heightBytes encoded as one byte, followed by heightBytes itself. -/// -/// -pub const MAX_COINBASE_HEIGHT_DATA_LEN: usize = 6; - -/// The minimum length of the coinbase data. -/// -/// Includes the encoded coinbase height, if any. -/// -/// # Consensus -/// -/// > A coinbase transaction script MUST have length in {2 .. 100} bytes. -/// -/// -pub const MIN_COINBASE_DATA_LEN: usize = 2; +use super::{Input, OutPoint, Output, Script}; /// The coinbase data for a genesis block. /// /// Zcash uses the same coinbase data for the Mainnet, Testnet, and Regtest /// genesis blocks. -pub const GENESIS_COINBASE_DATA: [u8; 77] = [ +pub const GENESIS_COINBASE_SCRIPT_SIG: [u8; 77] = [ 4, 255, 255, 7, 31, 1, 4, 69, 90, 99, 97, 115, 104, 48, 98, 57, 99, 52, 101, 101, 102, 56, 98, 55, 99, 99, 52, 49, 55, 101, 101, 53, 48, 48, 49, 101, 51, 53, 48, 48, 57, 56, 52, 98, 54, 102, 101, 97, 51, 53, 54, 56, 51, 97, 55, 99, 97, 99, 49, 52, 49, 97, 48, 52, 51, 99, 52, 50, 48, 54, 52, 56, 51, 53, 100, 51, 52, ]; +/// Parses the BIP-34 block-height prefix of a non-genesis coinbase script and returns the height +/// along with the trailing miner data. +/// +/// # Consensus +/// +/// > A coinbase transaction for a block at block height greater than 0 MUST have a script that, as +/// > its first item, encodes the block height `height` as follows. For `height` in the range +/// > {1 .. 16}, the encoding is a single byte of value `0x50` + `height`. Otherwise, let +/// > `heightBytes` be the signed little-endian representation of `height`, using the minimum +/// > nonzero number of bytes such that the most significant byte is < `0x80`. The length of +/// > `heightBytes` MUST be in the range {1 .. 5}. Then the encoding is the length of `heightBytes` +/// > encoded as one byte, followed by `heightBytes` itself. This matches the encoding used by +/// > Bitcoin in the implementation of [BIP-34] (but the description here is to be considered +/// > normative). +/// +/// +/// +/// [BIP-34]: +/// +/// # Strategy +/// +/// Rather than parsing the height bytes ourselves, we read a candidate height directly off the +/// wire (using the prefix shape to locate the bytes), then re-encode it via +/// [`zcash_script::pattern::push_num`] — the same primitive used by +/// [`zcash_transparent::bundle::TxIn::coinbase`] to build coinbase inputs — and require byte-exact +/// equality. Any non-canonical input (wrong shape, non-minimal length, oversize, negative, +/// signed-bit games) fails this check. +fn parse_coinbase_height(script_sig: &[u8]) -> Result<(Height, Vec), SerializationError> { + let parse_err = SerializationError::Parse; + + // Read a candidate height directly off the wire. The first byte tells us where the height + // bytes are; we don't validate them yet — the oracle below catches any non-canonical input. + let (h, len): (i64, usize) = match *script_sig + .first() + .ok_or(parse_err("Empty coinbase script"))? + { + op_n @ 0x51..=0x60 => (i64::from(op_n - 0x50), 1), + n @ 1..=5 => { + let bytes = script_sig + .get(1..=usize::from(n)) + .ok_or(parse_err("Coinbase height push truncated"))?; + // Permissive read: zero-extend the wire bytes into an i64. The candidate is only + // trusted after the canonical-encode-and-compare check below. + let mut buf = [0u8; 8]; + buf[..bytes.len()].copy_from_slice(bytes); + (i64::from_le_bytes(buf), 1 + bytes.len()) + } + _ => return Err(parse_err("Invalid coinbase script prefix")), + }; + + // Oracle: re-encode the candidate the way zcash_transparent's coinbase builder does, and + // require byte-exact equality. + if script_sig + .get(..len) + .ok_or(parse_err("Coinbase script too short"))? + != pattern::push_num(h).to_bytes().as_slice() + { + return Err(parse_err("Non-canonical coinbase height encoding")); + } + + let h = u32::try_from(h).map_err(|_| parse_err("Negative coinbase height"))?; + let height = + Height::try_from(h).map_err(|_| parse_err("Coinbase height exceeds Height::MAX"))?; + + Ok((height, script_sig[len..].to_vec())) +} + impl ZcashSerialize for OutPoint { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { writer.write_all(&self.hash.0[..])?; @@ -87,162 +124,6 @@ impl ZcashDeserialize for OutPoint { // determine the coinbase height, because it is not present in older network // upgrades. -/// Split `data` into a block height and remaining miner-controlled coinbase data. -/// -/// The height may consume `0..=5` bytes at the stat of the coinbase data. -/// The genesis block does not include an encoded coinbase height. -/// -/// # Consensus -/// -/// > A coinbase transaction for a *block* at *block height* greater than 0 MUST have -/// > a script that, as its first item, encodes the *block height* `height` as follows. -/// > For `height` in the range {1..16}, the encoding is a single byte of value -/// > `0x50` + `height`. Otherwise, let `heightBytes` be the signed little-endian -/// > representation of `height`, using the minimum nonzero number of bytes such that -/// > the most significant byte is < `0x80`. -/// > The length of `heightBytes` MUST be in the range {1..5}. -/// > Then the encoding is the length of `heightBytes` encoded as one byte, -/// > followed by `heightBytes` itself. This matches the encoding used by Bitcoin in the -/// > implementation of [BIP-34] (but the description here is to be considered normative). -/// -/// -/// -pub(crate) fn parse_coinbase_height( - mut data: Vec, -) -> Result<(block::Height, CoinbaseData), SerializationError> { - match (data.first(), data.len()) { - // Blocks 1 through 16 inclusive encode block height with OP_N opcodes. - (Some(op_n @ 0x51..=0x60), len) if len >= 1 => Ok(( - Height((op_n - 0x50) as u32), - CoinbaseData(data.split_off(1)), - )), - // Blocks 17 through 128 exclusive encode block height with the `0x01` opcode. - // The Bitcoin encoding requires that the most significant byte is below 0x80. - (Some(0x01), len) if len >= 2 && data[1] < 0x80 => { - let h = data[1] as u32; - if (17..128).contains(&h) { - Ok((Height(h), CoinbaseData(data.split_off(2)))) - } else { - Err(SerializationError::Parse("Invalid block height")) - } - } - // Blocks 128 through 32768 exclusive encode block height with the `0x02` opcode. - // The Bitcoin encoding requires that the most significant byte is below 0x80. - (Some(0x02), len) if len >= 3 && data[2] < 0x80 => { - let h = data[1] as u32 + ((data[2] as u32) << 8); - if (128..32_768).contains(&h) { - Ok((Height(h), CoinbaseData(data.split_off(3)))) - } else { - Err(SerializationError::Parse("Invalid block height")) - } - } - // Blocks 32768 through 2**23 exclusive encode block height with the `0x03` opcode. - // The Bitcoin encoding requires that the most significant byte is below 0x80. - (Some(0x03), len) if len >= 4 && data[3] < 0x80 => { - let h = data[1] as u32 + ((data[2] as u32) << 8) + ((data[3] as u32) << 16); - if (32_768..8_388_608).contains(&h) { - Ok((Height(h), CoinbaseData(data.split_off(4)))) - } else { - Err(SerializationError::Parse("Invalid block height")) - } - } - // The genesis block does not encode the block height by mistake; special case it. - // The first five bytes are [4, 255, 255, 7, 31], the little-endian encoding of - // 520_617_983. - // - // In the far future, Zcash might reach this height, and the miner might use the - // same coinbase data as the genesis block. So we need an updated consensus rule - // to handle this edge case. - // - // TODO: update this check based on the consensus rule changes in - // https://github.com/zcash/zips/issues/540 - (Some(0x04), _) if data[..] == GENESIS_COINBASE_DATA[..] => { - Ok((Height(0), CoinbaseData(data))) - } - // As noted above, this is included for completeness. - // The Bitcoin encoding requires that the most significant byte is below 0x80. - (Some(0x04), len) if len >= 5 && data[4] < 0x80 => { - let h = data[1] as u32 - + ((data[2] as u32) << 8) - + ((data[3] as u32) << 16) - + ((data[4] as u32) << 24); - if (8_388_608..=Height::MAX.0).contains(&h) { - Ok((Height(h), CoinbaseData(data.split_off(5)))) - } else { - Err(SerializationError::Parse("Invalid block height")) - } - } - _ => Err(SerializationError::Parse( - "Could not parse BIP34 height in coinbase data", - )), - } -} - -/// Encode `height` into a block height, as a prefix of the coinbase data. -/// Does not write `coinbase_data`. -/// -/// The height may produce `0..=5` initial bytes of coinbase data. -/// -/// # Errors -/// -/// Returns an error if the coinbase height is zero, -/// and the `coinbase_data` does not match the Zcash mainnet and testnet genesis coinbase data. -/// (They are identical.) -/// -/// This check is required, because the genesis block does not include an encoded -/// coinbase height, -pub(crate) fn write_coinbase_height( - height: block::Height, - coinbase_data: &CoinbaseData, - mut w: W, -) -> Result<(), io::Error> { - // We can't write this as a match statement on stable until exclusive range - // guards are stabilized. - // The Bitcoin encoding requires that the most significant byte is below 0x80, - // so the ranges run up to 2^{n-1} rather than 2^n. - if let 0 = height.0 { - // The genesis block's coinbase data does not have a height prefix. - // So we return an error if the entire coinbase data doesn't match genesis. - // (If we don't do this check, then deserialization will fail.) - // - // TODO: update this check based on the consensus rule changes in - // https://github.com/zcash/zips/issues/540 - if coinbase_data.0 != GENESIS_COINBASE_DATA { - return Err(io::Error::other("invalid genesis coinbase data")); - } - } else if let h @ 1..=16 = height.0 { - w.write_u8(0x50 + (h as u8))?; - } else if let h @ 17..=127 = height.0 { - w.write_u8(0x01)?; - w.write_u8(h as u8)?; - } else if let h @ 128..=32_767 = height.0 { - w.write_u8(0x02)?; - w.write_u16::(h as u16)?; - } else if let h @ 32_768..=8_388_607 = height.0 { - w.write_u8(0x03)?; - w.write_u8(h as u8)?; - w.write_u8((h >> 8) as u8)?; - w.write_u8((h >> 16) as u8)?; - } else if let h @ 8_388_608..=block::Height::MAX_AS_U32 = height.0 { - w.write_u8(0x04)?; - w.write_u32::(h)?; - } else { - panic!("Invalid coinbase height"); - } - Ok(()) -} - -impl Height { - /// Get the size of `Height` when serialized into a coinbase input script. - pub fn coinbase_zcash_serialized_size(&self) -> usize { - let mut writer = FakeWriter(0); - let empty_data = CoinbaseData(Vec::new()); - - write_coinbase_height(*self, &empty_data, &mut writer).expect("writer should never fail"); - writer.0 - } -} - impl ZcashSerialize for Input { /// Serialize this transparent input. /// @@ -265,19 +146,17 @@ impl ZcashSerialize for Input { unlock_script.zcash_serialize(&mut writer)?; writer.write_u32::(*sequence)?; } - Input::Coinbase { - height, - data, - sequence, - } => { + Input::Coinbase { sequence, .. } => { + // Write the null prevout. writer.write_all(&[0; 32][..])?; writer.write_u32::(0xffff_ffff)?; - let mut height_and_data = Vec::new(); - write_coinbase_height(*height, data, &mut height_and_data)?; - height_and_data.extend(&data.0); - zcash_serialize_bytes(&height_and_data, &mut writer)?; + // Write the script sig containing the height and data. + self.coinbase_script() + .ok_or_else(|| io::Error::other("invalid coinbase script sig"))? + .zcash_serialize(&mut writer)?; + // Write the sequence. writer.write_u32::(*sequence)?; } } @@ -287,36 +166,53 @@ impl ZcashSerialize for Input { impl ZcashDeserialize for Input { fn zcash_deserialize(mut reader: R) -> Result { - // This inlines the OutPoint deserialization to peek at the hash value - // and detect whether we have a coinbase input. - let bytes = reader.read_32_bytes()?; - if bytes == [0; 32] { + // This inlines the OutPoint deserialization to peek at the hash value and detect whether we + // have a coinbase input. + let hash = reader.read_32_bytes()?; + + // Coinbase inputs have a null prevout hash. + if hash == [0; 32] { + // Coinbase txs have the prevout index set to `u32::MAX`. if reader.read_u32::()? != 0xffff_ffff { - return Err(SerializationError::Parse("wrong index in coinbase")); + return Err(SerializationError::Parse("Wrong index in coinbase")); } - let data: Vec = (&mut reader).zcash_deserialize_into()?; - - // Check the coinbase data length. - if data.len() > MAX_COINBASE_DATA_LEN { - return Err(SerializationError::Parse("coinbase data is too long")); - } else if data.len() < MIN_COINBASE_DATA_LEN { - return Err(SerializationError::Parse("coinbase data is too short")); + // Read the coinbase script length and validate it against the consensus + // bound *before* allocating any script bytes. The generic `Vec` + // deserializer would otherwise allocate up to MAX_PROTOCOL_MESSAGE_LEN + // bytes for an attacker-controlled CompactSize length and only reject + // afterwards, letting a peer force multi-MiB transient allocations per + // bogus block. + // + // # Consensus + // + // > A coinbase transaction script MUST have length in {2 .. 100} bytes. + // + // + let len: CompactSizeMessage = (&mut reader).zcash_deserialize_into()?; + let len: usize = len.into(); + if len < MIN_COINBASE_SCRIPT_LEN { + return Err(SerializationError::Parse("Coinbase script is too short")); + } else if len > MAX_COINBASE_SCRIPT_LEN { + return Err(SerializationError::Parse("Coinbase script is too long")); } + let script_sig = zcash_deserialize_bytes_external_count(len, &mut reader)?; - let (height, data) = parse_coinbase_height(data)?; - - let sequence = reader.read_u32::()?; + let (height, data) = if script_sig.as_slice() == GENESIS_COINBASE_SCRIPT_SIG { + (Height::MIN, GENESIS_COINBASE_SCRIPT_SIG.to_vec()) + } else { + parse_coinbase_height(&script_sig)? + }; Ok(Input::Coinbase { height, data, - sequence, + sequence: reader.read_u32::()?, }) } else { Ok(Input::PrevOut { outpoint: OutPoint { - hash: transaction::Hash(bytes), + hash: transaction::Hash(hash), index: reader.read_u32::()?, }, unlock_script: Script::zcash_deserialize(&mut reader)?, diff --git a/zebra-chain/src/transparent/tests/prop.rs b/zebra-chain/src/transparent/tests/prop.rs index 5d3f586d298..b03a67ad603 100644 --- a/zebra-chain/src/transparent/tests/prop.rs +++ b/zebra-chain/src/transparent/tests/prop.rs @@ -4,12 +4,7 @@ use zebra_test::prelude::*; use crate::{block, fmt::SummaryDebug, transaction::arbitrary::MAX_ARBITRARY_ITEMS, LedgerState}; -use super::super::{ - serialize::{parse_coinbase_height, write_coinbase_height}, - Input, -}; - -use proptest::collection::vec; +use super::super::Input; #[test] fn coinbase_has_height() -> Result<()> { @@ -48,53 +43,3 @@ fn input_coinbase_vecs_only_have_coinbase_input() -> Result<()> { Ok(()) } - -#[test] -fn coinbase_height_round_trip_from_random_input() -> Result<()> { - let _init_guard = zebra_test::init(); - - let strategy = - any::().prop_flat_map(|height| Input::arbitrary_with(Some(height))); - - proptest!(|(input in strategy)| { - let (height, data) = match input { - Input::Coinbase { height, data, .. } => (height, data), - _ => unreachable!("all inputs will have coinbase height and data"), - }; - let mut encoded = Vec::new(); - write_coinbase_height(height, &data, &mut encoded)?; - let decoded = parse_coinbase_height(encoded)?; - - prop_assert_eq!(height, decoded.0); - }); - - Ok(()) -} - -proptest! { - #[test] - fn coinbase_height_round_trip_from_random_bytes(mut height_bytes in vec(any::(), 1..5)) { - let mut encoded1 = vec![height_bytes.len() as u8]; - encoded1.append(&mut height_bytes); - - let decoded = parse_coinbase_height(encoded1.clone()).ok(); - - if decoded.is_some() { - let mut encoded2 = Vec::new(); - write_coinbase_height(decoded.as_ref().unwrap().0, &decoded.unwrap().1, &mut encoded2)?; - prop_assert_eq!(encoded2, encoded1); - } - } - - #[test] - fn coinbase_height_round_trip_from_random_byte(height_byte in vec(any::(), 1..2)) { - let encoded1 = height_byte; - let decoded = parse_coinbase_height(encoded1.clone()).ok(); - - if decoded.is_some() { - let mut encoded2 = Vec::new(); - write_coinbase_height(decoded.as_ref().unwrap().0, &decoded.unwrap().1, &mut encoded2)?; - prop_assert_eq!(encoded2, encoded1); - } - } -} diff --git a/zebra-chain/src/transparent/tests/vectors.rs b/zebra-chain/src/transparent/tests/vectors.rs index 37acc51c81d..a94b83ed1c4 100644 --- a/zebra-chain/src/transparent/tests/vectors.rs +++ b/zebra-chain/src/transparent/tests/vectors.rs @@ -1,47 +1,183 @@ use std::sync::Arc; -use super::super::serialize::parse_coinbase_height; -use crate::{block::Block, parameters::Network, serialization::ZcashDeserializeInto, transaction}; +use crate::{ + block::{Block, Height}, + parameters::Network, + serialization::{SerializationError, ZcashDeserialize, ZcashDeserializeInto}, + transaction, + transparent::Input, +}; use hex::FromHex; +use zcash_script::{opcode::Evaluable, pattern}; use zebra_test::prelude::*; +/// Builds a serialized transparent input wire-format with a null prevout (coinbase) and the given +/// `script_sig` bytes. Used to exercise [`Input::zcash_deserialize`] directly. +fn build_coinbase_wire(script_sig: &[u8]) -> Vec { + let mut buf = vec![0u8; 32]; + buf.extend_from_slice(&0xffff_ffffu32.to_le_bytes()); + let len = script_sig.len(); + if len < 0xfd { + buf.push(len as u8); + } else if len <= 0xffff { + buf.push(0xfd); + buf.extend_from_slice(&(len as u16).to_le_bytes()); + } else { + buf.push(0xfe); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + } + buf.extend_from_slice(script_sig); + buf.extend_from_slice(&0u32.to_le_bytes()); + buf +} + +/// Returns the canonical BIP-34 height-prefix bytes for the given height, computed via the +/// upstream encoder used by `zcash_transparent::bundle::TxIn::coinbase`. +fn canonical_height_prefix(height: Height) -> Vec { + pattern::push_num(i64::from(height.0)).to_bytes() +} + #[test] -fn parse_coinbase_height_mins() { +fn coinbase_deserialize_rejects_non_minimal_heights() { let _init_guard = zebra_test::init(); - // examples with height 1: + // Each of these is a non-canonical encoding of a valid height. They must all be rejected. + // The trailing byte makes the script ≥ 2 bytes (the consensus minimum); without it the length + // check would reject before the height check, masking the bug we are guarding against. + let cases: &[(&str, &[u8])] = &[ + // Height 1: minimal is OP_1 (0x51). Length-prefixed forms are non-canonical. + ("h1_via_01_01", &[0x01, 0x01, 0x00]), + ("h1_via_02", &[0x02, 0x01, 0x00, 0x00]), + ("h1_via_03", &[0x03, 0x01, 0x00, 0x00, 0x00]), + ("h1_via_04", &[0x04, 0x01, 0x00, 0x00, 0x00, 0x00]), + // Height 16: minimal is OP_16 (0x60). 1-byte push form is non-canonical. + ("h16_via_01_10", &[0x01, 0x10, 0x00]), + // Height 17: minimal is `[0x01, 0x11]`. Wider forms are non-canonical. + ("h17_via_02", &[0x02, 0x11, 0x00, 0x00]), + ("h17_via_03", &[0x03, 0x11, 0x00, 0x00, 0x00]), + ]; - let case1 = vec![0x51]; - assert!(parse_coinbase_height(case1.clone()).is_ok()); - assert_eq!(parse_coinbase_height(case1).unwrap().0 .0, 1); + for (name, sig) in cases { + let wire = build_coinbase_wire(sig); + let result = Input::zcash_deserialize(&wire[..]); + assert!( + result.is_err(), + "{name}: must reject non-canonical encoding {sig:02x?}, got {result:?}", + ); + } +} - let case2 = vec![0x01, 0x01]; - assert!(parse_coinbase_height(case2).is_err()); +#[test] +fn coinbase_deserialize_accepts_canonical_heights() { + let _init_guard = zebra_test::init(); - let case3 = vec![0x02, 0x01, 0x00]; - assert!(parse_coinbase_height(case3).is_err()); + // Boundary heights spanning every valid prefix length, plus Height::MAX. + let heights = [ + 1, + 16, + 17, + 127, + 128, + 32_767, + 32_768, + 8_388_607, + 8_388_608, + Height::MAX.0, + ]; - let case4 = vec![0x03, 0x01, 0x00, 0x00]; - assert!(parse_coinbase_height(case4).is_err()); + for h in heights { + let height = Height(h); + let mut script_sig = canonical_height_prefix(height); + // Pad to ≥ MIN_COINBASE_SCRIPT_LEN (= 2 bytes) so the length check passes. + if script_sig.len() < 2 { + script_sig.push(0x00); + } + let wire = build_coinbase_wire(&script_sig); + let parsed = Input::zcash_deserialize(&wire[..]) + .unwrap_or_else(|e| panic!("must accept canonical height {h}: {e:?}")); + match parsed { + Input::Coinbase { + height: parsed_h, .. + } => assert_eq!(parsed_h, height, "round-trip mismatch for height {h}"), + _ => panic!("expected Coinbase input for height {h}"), + } + } +} - let case5 = vec![0x04, 0x01, 0x00, 0x00, 0x00]; - assert!(parse_coinbase_height(case5).is_err()); +/// Build a coinbase `Input` byte sequence: 32 zero bytes for the null outpoint +/// hash, the coinbase index `0xffffffff` little-endian, and a coinbase-script +/// CompactSize length followed by `payload`. +/// +/// Used by the regression test below to confirm that `Input::zcash_deserialize` +/// rejects attacker-controlled coinbase script lengths *before* allocating or +/// reading the script bytes. +fn coinbase_input_bytes(compactsize_len: &[u8], payload: &[u8]) -> Vec { + let mut bytes = Vec::with_capacity(32 + 4 + compactsize_len.len() + payload.len()); + bytes.extend_from_slice(&[0u8; 32]); + bytes.extend_from_slice(&0xffff_ffffu32.to_le_bytes()); + bytes.extend_from_slice(compactsize_len); + bytes.extend_from_slice(payload); + bytes +} + +/// Coinbase scripts longer than the consensus maximum (100 bytes) must be +/// rejected at length-decode time, not after allocating the bytes. +/// +/// Regression test: encoding a CompactSize length of 101 with no payload would +/// have made the previous implementation try to `read_exact(101)` and surface +/// an `io::Error`; the fix returns the consensus error string before the read. +#[test] +fn coinbase_script_oversize_rejected_before_allocation() { + let _init_guard = zebra_test::init(); + + let bytes = coinbase_input_bytes(&[101u8], &[]); + let result = Input::zcash_deserialize(std::io::Cursor::new(&bytes)); + assert!( + matches!( + result, + Err(SerializationError::Parse("Coinbase script is too long")) + ), + "expected `Coinbase script is too long`, got {result:?}", + ); +} - // examples with height 17: +#[test] +fn coinbase_deserialize_rejects_out_of_range_heights() { + let _init_guard = zebra_test::init(); - let case1 = vec![0x01, 0x11]; - assert!(parse_coinbase_height(case1.clone()).is_ok()); - assert_eq!(parse_coinbase_height(case1).unwrap().0 .0, 17); + // Sign-bit set on the top byte: would decode as negative under signed-LE. + let sig = [0x01, 0x80, 0x00]; + Input::zcash_deserialize(&build_coinbase_wire(&sig)[..]) + .expect_err("must reject negative-signed height encoding"); - let case2 = vec![0x02, 0x11, 0x00]; - assert!(parse_coinbase_height(case2).is_err()); + // 6-byte push (n = 6 invalid; spec restricts heightBytes length to {1..=5}). + let sig = [0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + Input::zcash_deserialize(&build_coinbase_wire(&sig)[..]) + .expect_err("must reject 6-byte height push"); - let case3 = vec![0x03, 0x11, 0x00, 0x00]; - assert!(parse_coinbase_height(case3).is_err()); + // Height > Height::MAX. Height::MAX = 0x7FFF_FFFF, so 0x8000_0000 encoded with sign-bit clear + // would need 5 bytes: [0x05, 0x00, 0x00, 0x00, 0x80, 0x00]. + let sig = [0x05, 0x00, 0x00, 0x00, 0x80, 0x00]; + Input::zcash_deserialize(&build_coinbase_wire(&sig)[..]) + .expect_err("must reject height > Height::MAX"); +} - let case4 = vec![0x04, 0x11, 0x00, 0x00, 0x00]; - assert!(parse_coinbase_height(case4).is_err()); +proptest! { + #[test] + fn coinbase_canonical_round_trip(h in 1u32..=Height::MAX.0) { + let height = Height(h); + let mut script_sig = canonical_height_prefix(height); + if script_sig.len() < 2 { + script_sig.push(0x00); + } + let wire = build_coinbase_wire(&script_sig); + let parsed = Input::zcash_deserialize(&wire[..]).expect("canonical encoding must parse"); + match parsed { + Input::Coinbase { height: parsed_h, .. } => prop_assert_eq!(parsed_h, height), + _ => prop_assert!(false, "expected Coinbase input"), + } + } } #[test] diff --git a/zebra-chain/src/work/difficulty.rs b/zebra-chain/src/work/difficulty.rs index fa6da45a140..a7fce74deb8 100644 --- a/zebra-chain/src/work/difficulty.rs +++ b/zebra-chain/src/work/difficulty.rs @@ -291,28 +291,49 @@ impl CompactDifficulty { Ok(difficulty) } - /// Returns a floating-point number representing a difficulty as a multiple - /// of the minimum difficulty for the provided network. + /// Returns a floating-point number representing this block's difficulty + /// as a multiple of the minimum network difficulty. + /// + /// A result of 1.0 means the block was mined at minimum difficulty. + /// Values above 1.0 mean proportionally more work was done. + /// + /// # How it works + /// + /// `CompactDifficulty` encodes a target as `mantissa × 256^(exponent - 3)`. + /// Since difficulty is inversely proportional to the target threshold: + /// + /// ```text + /// result = network_min_target / self_target + /// = (network_mantissa / self_mantissa) × 256^(network_exponent - self_exponent) + /// ``` + /// + /// The mantissa ratio is computed first by stripping both exponent bytes (`<< 8`), + /// then the result is scaled up or down by 256 once per unit of exponent difference. // Copied from - // TODO: Explain here what this ported code is doing and why, request help to do so with the ECC team. + // Used for RPC functions only, not consensus-critical. pub fn relative_to_network(&self, network: &Network) -> f64 { let network_difficulty = network.target_difficulty_limit().to_compact(); - let [mut n_shift, ..] = self.0.to_be_bytes(); - let [n_shift_amount, ..] = network_difficulty.0.to_be_bytes(); - let mut d_diff = f64::from(network_difficulty.0 << 8) / f64::from(self.0 << 8); - - while n_shift < n_shift_amount { - d_diff *= 256.0; - n_shift += 1; + // get exponent byte from both values + let [self_exponent_byte, ..] = self.0.to_be_bytes(); + let [network_exponent_byte, ..] = network_difficulty.0.to_be_bytes(); + // take the ratio of network mantissa difficulty to self mantissa difficulty + let mantissa_ratio = f64::from(network_difficulty.0 << 8) / f64::from(self.0 << 8); + + let mut ratio = mantissa_ratio; + let mut exponent_cursor = self_exponent_byte; + // multiply by 256 for each exponent byte difference + while exponent_cursor < network_exponent_byte { + ratio *= 256.0; + exponent_cursor += 1; } - while n_shift > n_shift_amount { - d_diff /= 256.0; - n_shift -= 1; + while exponent_cursor > network_exponent_byte { + ratio /= 256.0; + exponent_cursor -= 1; } - d_diff + ratio } } diff --git a/zebra-chain/src/work/difficulty/tests/vectors.rs b/zebra-chain/src/work/difficulty/tests/vectors.rs index cfebdcf0595..d1242a5a2c3 100644 --- a/zebra-chain/src/work/difficulty/tests/vectors.rs +++ b/zebra-chain/src/work/difficulty/tests/vectors.rs @@ -403,7 +403,7 @@ fn testnet_minimum_difficulty() -> Result<(), Report> { block::Height(1_028_500), ]; - for (&height, _block) in zebra_test::vectors::TESTNET_BLOCKS.iter() { + for &height in zebra_test::vectors::TESTNET_BLOCKS.keys() { let height = block::Height(height); /// SPANDOC: Do minimum difficulty checks for testnet block {?height} diff --git a/zebra-chain/src/work/equihash.rs b/zebra-chain/src/work/equihash.rs index 34863f26626..0c9d491eb65 100644 --- a/zebra-chain/src/work/equihash.rs +++ b/zebra-chain/src/work/equihash.rs @@ -8,8 +8,8 @@ use serde_big_array::BigArray; use crate::{ block::Header, serialization::{ - zcash_serialize_bytes, SerializationError, ZcashDeserialize, ZcashDeserializeInto, - ZcashSerialize, + zcash_deserialize_bytes_external_count, zcash_serialize_bytes, CompactSizeMessage, + SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, }, }; @@ -196,7 +196,7 @@ impl Solution { /// /// # Panics /// - /// - If `header` contains an invalid difficulty threshold. + /// - If `header` contains an invalid difficulty threshold. #[cfg(feature = "internal-miner")] fn difficulty_is_valid(header: &Header) -> bool { // Simplified from zebra_consensus::block::check::difficulty_is_valid(). @@ -262,7 +262,19 @@ impl ZcashSerialize for Solution { impl ZcashDeserialize for Solution { fn zcash_deserialize(mut reader: R) -> Result { - let solution: Vec = (&mut reader).zcash_deserialize_into()?; + let len: CompactSizeMessage = (&mut reader).zcash_deserialize_into()?; + let len: usize = len.into(); + + // Validate the length against the consensus-required sizes before + // allocating, so an attacker-controlled CompactSize cannot force a + // multi-megabyte allocation. + if len > SOLUTION_SIZE { + return Err(SerializationError::Parse( + "incorrect equihash solution size", + )); + } + + let solution = zcash_deserialize_bytes_external_count(len, &mut reader)?; Self::from_bytes(&solution) } } diff --git a/zebra-chain/src/work/tests/vectors.rs b/zebra-chain/src/work/tests/vectors.rs index 06a752de018..7e24ac4d236 100644 --- a/zebra-chain/src/work/tests/vectors.rs +++ b/zebra-chain/src/work/tests/vectors.rs @@ -1,6 +1,9 @@ use crate::{ block::{Block, MAX_BLOCK_BYTES}, - serialization::{CompactSizeMessage, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, + serialization::{ + CompactSizeMessage, SerializationError, ZcashDeserialize, ZcashDeserializeInto, + ZcashSerialize, + }, work::equihash::{Solution, SOLUTION_SIZE}, }; @@ -79,3 +82,30 @@ fn equihash_solution_size_field() { } } } + +#[test] +fn equihash_solution_rejects_oversize_compactsize_before_allocating() { + let _init_guard = zebra_test::init(); + + let mut data = Vec::new(); + let oversize: CompactSizeMessage = (SOLUTION_SIZE + 1) + .try_into() + .expect("fits in MAX_PROTOCOL_MESSAGE_LEN"); + oversize + .zcash_serialize(&mut data) + .expect("CompactSize should serialize"); + + let err = Solution::zcash_deserialize(data.as_slice()) + .expect_err("oversize equihash CompactSize must fail to deserialize"); + + // This is fragile, but the only current way to check if the deserializer + // rejected the size before allocating. + // If this fails, double check if the message error has not changed. + assert!( + matches!( + err, + SerializationError::Parse("incorrect equihash solution size"), + ), + "expected size-rejection Parse error, got: {err:?}", + ); +} diff --git a/zebra-consensus/CHANGELOG.md b/zebra-consensus/CHANGELOG.md index 30c0424a9cd..cc35eae8363 100644 --- a/zebra-consensus/CHANGELOG.md +++ b/zebra-consensus/CHANGELOG.md @@ -7,6 +7,113 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.0.1] - 2026-06-18 + +### Changed + +- `zebra-state` dependency bumped to `9.0.1` + +## [9.0.0] - 2026-06-10 + +### Changed + +- Deferred pool balance change calculation moved out of the checkpoint verifier. + `CheckpointVerifiedBlock::new()` no longer takes a `deferred_pool_balance_change` + parameter; it is now computed by the finalized state on commit. +- `zebra-chain` dependency bumped to `10.0.0`. + +## [8.0.0] - 2026-06-02 + +### Added + +- `error::TransactionError::OrchardProofSize`. +- Per-Orchard-circuit verifying keys and batch verifiers: `VERIFYING_KEY_PRE_NU6_2`, + `VERIFYING_KEY_POST_NU6_2`, `VERIFIER_PRE_NU6_2`, `VERIFIER_POST_NU6_2`, and `verifier_for()` + to select the verifier for a network upgrade. + +### Removed + +- `VERIFYING_KEY` and `VERIFIER`; replaced by the per-circuit pairs above. + +### Changed + +- Enforce a canonical Orchard proof size from the NU6.2 network upgrade, and verify Orchard + proofs against the fixed circuit's verifying key from NU6.2 onward. + +## [7.0.0] - 2026-05-28 + +### Added + +- `error::TransactionError`: + - `Amount`, `Balance`, `CoinbaseConstruction`, `Io`, `Other`, `TryFromSlice` variants + - `impl From for TransactionError` + - `impl From for TransactionError` + - `impl From for TransactionError` + - `impl From for TransactionError` + - `impl From for TransactionError` + - `impl From for TransactionError` + - `impl From for TransactionError` + +### Changed + +- Misbehavior scoring expanded to attribute more error classes to the + announcing peer + ([GHSA-gvjc-3w7c-92jx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gvjc-3w7c-92jx), + [GHSA-rj6c-83wx-jxf2](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-rj6c-83wx-jxf2), + [GHSA-hwxr-r2v4-9f2p](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-hwxr-r2v4-9f2p)): + - `VerifyBlockError::misbehavior_score()` now scores `Subsidy`, + `Transaction`, and `Commit` (delegating where appropriate) in addition to + `Equihash`. + - `BlockError::misbehavior_score()` now scores `NoTransactions`, + `BadMerkleRoot`, `WrongTransactionConsensusBranchId`, and + `TooManyTransparentSignatureOperations` as `100`, and delegates on + `Transaction`. + - `TransactionError::mempool_misbehavior_score()` now also scores + `LockedUntilAfterBlockHeight` and `LockedUntilAfterBlockTime` as `100`. +- `zebra-chain` dependency bumped to `8.0.0`. +- `zebra-node-services` dependency bumped to `6.0.0`. +- `zebra-script` dependency bumped to `7.0.0`. +- `zebra-state` dependency bumped to `7.0.0`. + +## [6.0.0] - 2026-05-01 + +### Added + +- `groth16`: + - `Item::from_joinsplit(joinsplit, joinsplit_pub_key) -> Result`, + replacing the removed `Description`-based conversions. + +### Removed + +- `groth16` (trimmed after the Sapling migration to `bellman`-free verification): + - `DescriptionWrapper` + - `SAPLING` + - `Description` trait + - `impl Description for (&sprout::joinsplit::JoinSplit, &VerificationKeyBytes)` + - `impl TryFrom> for Item` (use `Item::from_joinsplit` instead). + +## [5.0.2] - 2026-04-17 + +This release fixes an important security issue: + +- [CVE-2026-40880: Cached Mempool Verification Bypasses Consensus Rules for Ahead-of-Tip Blocks](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-xvj8-ph7x-65gf) + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +## [5.0.1] - 2026-03-25 + +This release fixes an important security issue: + +- [CVE-2026-34377: Consensus Failure via Crafted V5 Authorization Data](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-3vmh-33xr-9cqh) + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +### Added + +- ZIP-235 support under the `zcash_unstable = "zip235"` flag + ## [5.0.0] - 2026-03-12 ### Added diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index d8d07f40ce8..1a50b504aa8 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-consensus" -version = "5.0.0" +version = "9.0.1" authors.workspace = true description = "Implementation of Zcash consensus checks" license.workspace = true @@ -8,6 +8,10 @@ repository.workspace = true edition.workspace = true rust-version.workspace = true +# Disable bench auto-discovery so `benches/common.rs` is only compiled when +# explicitly imported via `mod common;` from a bench. +autobenches = false + readme = "../README.md" homepage.workspace = true # crates.io is limited to 5 keywords and categories @@ -57,17 +61,19 @@ derive-getters = { workspace = true, features = ["auto_copy_getters"] } sapling-crypto = { workspace = true, features = ["multicore"]} orchard.workspace = true -zcash_primitives.workspace = true - zcash_proofs = { workspace = true, features = ["multicore", "bundled-prover"] } +zcash_transparent = { workspace = true } +libzcash_script = { workspace = true } +zcash_script = { workspace = true } +zcash_primitives = { workspace = true } tower-fallback = { path = "../tower-fallback/", version = "0.2.41" } tower-batch-control = { path = "../tower-batch-control/", version = "1.0.1" } -zebra-script = { path = "../zebra-script", version = "5.0.0" } -zebra-state = { path = "../zebra-state", version = "5.0.0" } -zebra-node-services = { path = "../zebra-node-services", version = "4.0.0" } -zebra-chain = { path = "../zebra-chain", version = "6.0.0" } +zebra-script = { path = "../zebra-script", version = "9.0.0" } +zebra-state = { path = "../zebra-state", version = "9.0.1" } +zebra-node-services = { path = "../zebra-node-services", version = "8.0.0" } +zebra-chain = { path = "../zebra-chain", version = "10.1.0" } zcash_protocol.workspace = true @@ -91,9 +97,23 @@ tokio = { workspace = true, features = ["full", "tracing", "test-util"] } tracing-error = { workspace = true } tracing-subscriber = { workspace = true } -zebra-state = { path = "../zebra-state", version = "5.0.0", features = ["proptest-impl"] } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = ["proptest-impl"] } +zebra-state = { path = "../zebra-state", version = "9.0.1", features = ["proptest-impl"] } +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test/", version = "3.0.0" } +criterion = { workspace = true, features = ["html_reports"] } + +[[bench]] +name = "groth16" +harness = false + +[[bench]] +name = "halo2" +harness = false + +[[bench]] +name = "sapling" +harness = false + [lints] workspace = true diff --git a/zebra-consensus/benches/common.rs b/zebra-consensus/benches/common.rs new file mode 100644 index 00000000000..a46e545d000 --- /dev/null +++ b/zebra-consensus/benches/common.rs @@ -0,0 +1,17 @@ +//! Shared helpers for zebra-consensus benchmarks. +//! +//! Each bench file under `benches/` is its own crate, so helpers shared across +//! them live here and are wired in via `mod common;` at the top of each bench. + +#![allow(dead_code)] + +/// Creates a batch of `n` items by cycling through `source`. +/// +/// The source test vectors in `zebra-test` contain only a handful of real +/// proofs/bundles. To benchmark realistic batch sizes, items are repeated. +/// This is valid for cost-per-proof measurements because proof verification +/// runs the same curve operations regardless of proof content, but it does +/// not capture the memory/cache patterns of fully unique inputs. +pub fn cycled(source: &[T], n: usize) -> Vec { + source.iter().cycle().take(n).cloned().collect() +} diff --git a/zebra-consensus/benches/groth16.rs b/zebra-consensus/benches/groth16.rs new file mode 100644 index 00000000000..a005e163556 --- /dev/null +++ b/zebra-consensus/benches/groth16.rs @@ -0,0 +1,128 @@ +//! Benchmarks for Groth16 proof verification (Sprout JoinSplits). +//! +//! Groth16 verification is a pairing check on BLS12-381. Sprout has no batch +//! verifier in Zebra; each proof is verified individually, so the only +//! throughput dimension is per-proof cost. Test vectors contain few +//! JoinSplits, so items are cycled to fill larger sizes. + +// Disabled due to warnings in criterion macros +#![allow(missing_docs)] + +mod common; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; + +use bellman::groth16::PreparedVerifyingKey; +use bls12_381::Bls12; + +use zebra_chain::{ + block::Block, + primitives::{ed25519, Groth16Proof}, + serialization::ZcashDeserializeInto, + sprout::JoinSplit, +}; + +use zebra_consensus::groth16::{Item, SPROUT}; + +/// A Sprout JoinSplit paired with its transaction's JoinSplit public key, +/// ready to be converted into a Groth16 verification [`Item`]. +#[derive(Clone)] +struct JoinSplitSource { + joinsplit: JoinSplit, + pub_key: ed25519::VerificationKeyBytes, +} + +/// Extracts JoinSplit/pub-key pairs from the mainnet test blocks. +/// +/// Only V4 transactions contain Groth16 JoinSplits (V2/V3 use BCTV14 proofs, +/// V5+ dropped Sprout support). The returned pairs are the raw inputs to +/// [`Item`] construction and to `primary_inputs()`. +fn extract_joinsplit_sources() -> Vec { + let mut sources = Vec::new(); + + for bytes in zebra_test::vectors::MAINNET_BLOCKS.values() { + let block: Block = bytes.zcash_deserialize_into().expect("valid block"); + + for tx in &block.transactions { + let joinsplits: Vec<&JoinSplit> = + tx.sprout_groth16_joinsplits().collect(); + + if joinsplits.is_empty() { + continue; + } + + let pub_key = tx + .sprout_joinsplit_pub_key() + .expect("pub key must exist since there are joinsplits"); + + for joinsplit in joinsplits { + sources.push(JoinSplitSource { + joinsplit: joinsplit.clone(), + pub_key, + }); + } + } + } + + assert!( + !sources.is_empty(), + "test blocks must contain Groth16 JoinSplits" + ); + sources +} + +/// Converts a [`JoinSplitSource`] into a verification [`Item`]. +fn item_from(source: &JoinSplitSource) -> Item { + Item::from_joinsplit(&source.joinsplit, &source.pub_key).expect("valid groth16 item") +} + +fn bench_groth16_verify(c: &mut Criterion) { + let pvk: &'static PreparedVerifyingKey = SPROUT.prepared_verifying_key(); + let sources = extract_joinsplit_sources(); + let items: Vec = sources.iter().map(item_from).collect(); + + let mut group = c.benchmark_group("Groth16 Verification"); + + group.throughput(Throughput::Elements(1)); + group.bench_function("single proof", |b| { + let item = items[0].clone(); + b.iter(|| { + item.clone().verify_single(pvk).expect("valid proof"); + }) + }); + + for n in [2, 4, 8, 16, 32, 64] { + let batch = common::cycled(&items, n); + + group.throughput(Throughput::Elements(n as u64)); + group.bench_with_input(BenchmarkId::new("unbatched", n), &batch, |b, batch| { + b.iter(|| { + for item in batch { + item.clone().verify_single(pvk).expect("valid proof"); + } + }) + }); + } + + group.finish(); +} + +fn bench_groth16_inputs(c: &mut Criterion) { + let sources = extract_joinsplit_sources(); + let source = sources.first().expect("at least one JoinSplit source"); + + let mut group = c.benchmark_group("Groth16 Input Preparation"); + + // Cost of Proof::read (192 bytes) + primary input computation, i.e. the + // full Item construction that runs before verification. + group.bench_function("item_creation", |b| b.iter(|| item_from(source))); + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default().noise_threshold(0.1).sample_size(50); + targets = bench_groth16_verify, bench_groth16_inputs +} +criterion_main!(benches); diff --git a/zebra-consensus/benches/halo2.rs b/zebra-consensus/benches/halo2.rs new file mode 100644 index 00000000000..1aa3b7f6eb0 --- /dev/null +++ b/zebra-consensus/benches/halo2.rs @@ -0,0 +1,133 @@ +//! Benchmarks for Halo2 proof verification (Orchard Actions). +//! +//! Measures the cost of verifying Halo2 zero-knowledge proofs used in Orchard +//! Action descriptions. Halo2 uses an inner product argument on the Pallas +//! curve; each bundle contains an aggregate proof covering all its actions. +//! +//! # Benchmark group +//! +//! `halo2`: matches the `verifier` label in +//! `zebra.consensus.batch.duration_seconds` emitted from +//! `zebra-consensus/src/primitives/halo2.rs`, so a prod regression on the +//! Halo2 histogram maps to this file by name. +//! +//! # Throughput dimension +//! +//! Halo2 verification cost scales with the number of actions in the aggregate +//! proof, not the number of bundles. The benchmark reports throughput in +//! actions (via [`Throughput::Elements`]) so the dashboard shows +//! time-per-action — the unit that actually matters for capacity planning. +//! +//! # Batching +//! +//! `orchard::bundle::BatchValidator` supports cross-bundle batching, but +//! [`Item`]'s internal fields are private, so this benchmark can only +//! exercise `verify_single()` (a one-item batch). + +// Disabled due to warnings in criterion macros +#![allow(missing_docs)] + +mod common; + +use std::sync::Arc; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; + +use zebra_chain::{ + block::Block, parameters::NetworkUpgrade, serialization::ZcashDeserializeInto, transparent, +}; + +use tower_batch_control::RequestWeight; +use zebra_consensus::halo2::{Item, VERIFYING_KEY_PRE_NU6_2}; + +/// Extracts valid Halo2 items (Orchard bundles + sighashes) from NU5+ mainnet +/// test blocks. +/// +/// Transactions with transparent inputs are skipped because computing their +/// sighash requires the previous outputs they spend, which are not available +/// in the test vectors. Orchard-only and Sapling-to-Orchard transactions +/// work with an empty previous outputs set. +fn extract_halo2_items_from_blocks() -> Vec { + let mut items = Vec::new(); + + for bytes in zebra_test::vectors::MAINNET_BLOCKS.values() { + let block: Block = bytes.zcash_deserialize_into().expect("valid block"); + + for tx in &block.transactions { + if !tx.has_orchard_shielded_data() { + continue; + } + + if !tx.inputs().is_empty() { + continue; + } + + let all_previous_outputs: Arc> = Arc::new(Vec::new()); + + let Ok(sighasher) = tx.sighasher(NetworkUpgrade::Nu5, all_previous_outputs) else { + continue; + }; + + let Some(bundle) = sighasher.orchard_bundle() else { + continue; + }; + + let sighash = sighasher.sighash(zebra_chain::transaction::HashType::ALL, None); + + // These mainnet test blocks are NU5-era Orchard history (mined before NU6.2), + // so they verify under the pre-NU6.2 verifying key (see the `verify_single` calls + // below, which pass `VERIFYING_KEY_PRE_NU6_2`). + items.push(Item::new(bundle, sighash)); + } + } + + assert!( + !items.is_empty(), + "NU5+ test blocks must contain Orchard transactions without transparent inputs" + ); + + items +} + +fn bench_halo2_verify(c: &mut Criterion) { + let source_items = extract_halo2_items_from_blocks(); + + let mut group = c.benchmark_group("halo2"); + + group.throughput(Throughput::Elements(source_items[0].request_weight() as u64)); + group.bench_function("single bundle", |b| { + let item = source_items[0].clone(); + b.iter(|| { + assert!(item.clone().verify_single(&VERIFYING_KEY_PRE_NU6_2)); + }) + }); + + for n in [2, 4, 8, 16, 32] { + let items = common::cycled(&source_items, n); + // Scale throughput by total actions, not bundles, so the dashboard + // series stays stable across test-vector changes. + let total_actions: u64 = items.iter().map(|i| i.request_weight() as u64).sum(); + + group.throughput(Throughput::Elements(total_actions)); + group.bench_with_input( + BenchmarkId::new("unbatched", n), + &items, + |b, items: &Vec| { + b.iter(|| { + for item in items { + assert!(item.clone().verify_single(&VERIFYING_KEY_PRE_NU6_2)); + } + }) + }, + ); + } + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default().noise_threshold(0.1).sample_size(50); + targets = bench_halo2_verify +} +criterion_main!(benches); diff --git a/zebra-consensus/benches/sapling.rs b/zebra-consensus/benches/sapling.rs new file mode 100644 index 00000000000..b5ccb36cbae --- /dev/null +++ b/zebra-consensus/benches/sapling.rs @@ -0,0 +1,165 @@ +//! Benchmarks for Sapling proof and signature verification. +//! +//! Measures the cost of verifying Sapling shielded transaction data: Groth16 +//! spend/output proofs plus binding/spend-auth signatures. Uses +//! `sapling_crypto::BatchValidator` directly, which supports true batch +//! verification across bundles. +//! +//! # Benchmark group +//! +//! `groth16_sapling`: matches the `verifier` label in +//! `zebra.consensus.batch.duration_seconds` emitted from +//! `zebra-consensus/src/primitives/sapling.rs`, so a prod regression on the +//! Sapling histogram maps to this file by name. +//! +//! # Batched vs unbatched +//! +//! The `batched` series is the production path; the `unbatched` series is +//! the fallback taken when a batch fails verification and items are +//! re-verified individually. + +// Disabled due to warnings in criterion macros +#![allow(missing_docs)] + +mod common; + +use std::sync::Arc; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use rand::thread_rng; + +use zebra_chain::{ + block::{self, Block}, + parameters::{Network, NetworkUpgrade}, + serialization::ZcashDeserializeInto, + transaction::{HashType, SigHash}, + transparent, +}; + +use sapling_crypto::{bundle::Authorized, BatchValidator, Bundle}; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::value::ZatBalance; + +/// A Sapling bundle paired with its transaction sighash, ready for verification. +#[derive(Clone)] +struct SaplingItem { + bundle: Bundle, + sighash: SigHash, +} + +/// Extracts valid Sapling bundles and sighashes from mainnet test blocks. +/// +/// Transactions with transparent inputs are skipped because their sighash +/// computation requires previous outputs not available in the test vectors. +fn extract_sapling_items_from_blocks() -> Vec { + let mut items = Vec::new(); + + let network = Network::Mainnet; + + for (height, bytes) in zebra_test::vectors::MAINNET_BLOCKS.iter() { + let block: Block = bytes.zcash_deserialize_into().expect("valid block"); + let height = block::Height(*height); + + for tx in &block.transactions { + if !tx.has_sapling_shielded_data() { + continue; + } + + if !tx.inputs().is_empty() { + continue; + } + + // Use the correct network upgrade for this block height so the + // sighash is computed with the right consensus branch ID. + let nu = NetworkUpgrade::current(&network, height); + + let all_previous_outputs: Arc> = Arc::new(Vec::new()); + + let Ok(sighasher) = tx.sighasher(nu, all_previous_outputs) else { + continue; + }; + + let Some(bundle) = sighasher.sapling_bundle() else { + continue; + }; + + let sighash = sighasher.sighash(HashType::ALL, None); + + items.push(SaplingItem { bundle, sighash }); + } + } + + assert!( + !items.is_empty(), + "Sapling-era test blocks must contain Sapling transactions without transparent inputs" + ); + + items +} + +fn bench_sapling_verify(c: &mut Criterion) { + let sapling = LocalTxProver::bundled(); + let (spend_vk, output_vk) = sapling.verifying_keys(); + let source_items = extract_sapling_items_from_blocks(); + + let mut group = c.benchmark_group("groth16_sapling"); + + group.throughput(Throughput::Elements(1)); + group.bench_function("single bundle", |b| { + let item = source_items[0].clone(); + b.iter(|| { + let mut batch = BatchValidator::default(); + assert!(batch.check_bundle(item.bundle.clone(), item.sighash.into())); + assert!(batch.validate(&spend_vk, &output_vk, thread_rng())); + }) + }); + + for n in [2, 4, 8, 16, 32, 64] { + let items = common::cycled(&source_items, n); + + group.throughput(Throughput::Elements(n as u64)); + group.bench_with_input( + BenchmarkId::new("unbatched", n), + &items, + |b, items: &Vec| { + b.iter(|| { + for item in items { + let mut batch = BatchValidator::default(); + assert!(batch.check_bundle(item.bundle.clone(), item.sighash.into())); + assert!(batch.validate(&spend_vk, &output_vk, thread_rng())); + } + }) + }, + ); + } + + // All bundles share one BatchValidator, amortizing the final + // multi-scalar multiplication. + for n in [2, 4, 8, 16, 32, 64] { + let items = common::cycled(&source_items, n); + + group.throughput(Throughput::Elements(n as u64)); + group.bench_with_input( + BenchmarkId::new("batched", n), + &items, + |b, items: &Vec| { + b.iter(|| { + let mut batch = BatchValidator::default(); + for item in items { + assert!(batch.check_bundle(item.bundle.clone(), item.sighash.into())); + } + assert!(batch.validate(&spend_vk, &output_vk, thread_rng())); + }) + }, + ); + } + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default().noise_threshold(0.1).sample_size(50); + targets = bench_sapling_verify +} +criterion_main!(benches); diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 0db32173c58..6efbdfcb096 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -108,23 +108,36 @@ impl VerifyBlockError { /// Returns a suggested misbehaviour score increment for a certain error. pub fn misbehavior_score(&self) -> u32 { - // TODO: Adjust these values based on zcashd (#9258). use VerifyBlockError::*; match self { Block { source } => source.misbehavior_score(), - Equihash { .. } => 100, + Equihash { .. } | Subsidy(_) => 100, + Transaction(err) => err.mempool_misbehavior_score(), + Commit(err) => err.misbehavior_score(), _other => 0, } } } -/// The maximum allowed number of legacy signature check operations in a block. +/// The maximum number of transparent signature operations allowed in a block. /// -/// This consensus rule is not documented, so Zebra follows the `zcashd` implementation. -/// We re-use some `zcashd` C++ script code via `zebra-script` and `zcash_script`. +/// # Consensus /// -/// See: -/// +/// For every block, the sum of legacy and P2SH transparent signature operations across all +/// transactions must not exceed [20_000]. +/// +/// ## Notes +/// +/// This rule is inherited from pre-SegWit Bitcoin, and is not explicitly stated in the Zcash +/// protocol spec. It is covered implicitly in [§7.6], which closes with "Other rules inherited from +/// Bitcoin". The inclusion of this rule is tracked in [`zcash/zips#568`]. +/// +/// Zebra mirrors `zcashd`'s `ConnectBlock`, which sums `GetLegacySigOpCount()` and +/// `GetP2SHSigOpCount()` per transaction before comparing against this constant. +/// +/// [20_000]: +/// [`zcash/zips#568`]: +/// [§7.6]: pub const MAX_BLOCK_SIGOPS: u32 = 20_000; impl SemanticBlockVerifier @@ -361,7 +374,6 @@ where new_outputs, transaction_hashes, transaction_sighashes: Some(transaction_sighashes), - deferred_pool_balance_change: Some(deferred_pool_balance_change), }; // Return early for proposal requests. diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 11811a1a984..4544fc7c515 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -261,32 +261,31 @@ pub fn subsidy_is_valid( if Some(height) == NetworkUpgrade::Nu6_1.activation_height(net) { let lockbox_disbursements = net.lockbox_disbursements(height); - // FIXME: Temporary workaround for configured non-standard test networks, - // including Regtest. After syncing with upstream Zebra v4.2.0, tests - // started failing here when NU6.1 activates with an empty configured - // lockbox disbursement list. The upstream `nu7_nsm_transactions` test - // also hits this when run with `tx_v6`/`nu7` enabled. Revisit later. - // if lockbox_disbursements.is_empty() { - // Err(BlockError::Other( - // "missing lockbox disbursements for NU6.1 activation block".to_string(), - // ))?; - // } - - if !lockbox_disbursements.is_empty() { - deferred_pool_balance_change = lockbox_disbursements.into_iter().try_fold( - deferred_pool_balance_change, - |balance, (addr, expected_amount)| { - if !has_amount(&addr, expected_amount) { - Err(SubsidyError::OneTimeLockboxDisbursementNotFound)?; - } - - balance - .checked_sub(expected_amount) - .ok_or(SubsidyError::Underflow) - }, - )?; + // The Mainnet and default Testnet disbursement lists are hardcoded and must be + // non-empty. Custom testnets and Regtest may configure no disbursements, in which + // case the NU6.1 activation block is not required to contain any disbursement + // outputs. + let must_have_disbursements = + matches!(net, Network::Mainnet) || net.is_default_testnet(); + if lockbox_disbursements.is_empty() && must_have_disbursements { + Err(BlockError::Other( + "missing lockbox disbursements for NU6.1 activation block".to_string(), + ))?; } - } + + deferred_pool_balance_change = lockbox_disbursements.into_iter().try_fold( + deferred_pool_balance_change, + |balance, (addr, expected_amount)| { + if !has_amount(&addr, expected_amount) { + Err(SubsidyError::OneTimeLockboxDisbursementNotFound)?; + } + + balance + .checked_sub(expected_amount) + .ok_or(SubsidyError::Underflow) + }, + )?; + }; // Check each funding stream output. funding_streams.into_iter().try_for_each( @@ -331,6 +330,26 @@ pub fn miner_fees_are_valid( let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount(); let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount(); + // Coinbase transaction can still have a NSM deposit + #[cfg(zcash_unstable = "zip235")] + let zip233_amount: Amount = coinbase_tx + .zip233_amount() + .constrain() + .map_err(|_| SubsidyError::InvalidZip233Amount)?; + + #[cfg(not(zcash_unstable = "zip235"))] + let zip233_amount = Amount::zero(); + + #[cfg(zcash_unstable = "zip235")] + if let Some(nsm_activation_height) = NetworkUpgrade::Nu7.activation_height(network) { + if height >= nsm_activation_height { + let minimum_zip233_amount = ((block_miner_fees * 6).unwrap() / 10).unwrap(); + if zip233_amount < minimum_zip233_amount { + Err(SubsidyError::InvalidZip233Amount)? + } + } + } + // # Consensus // // > - define the total output value of its coinbase transaction to be the total value in zatoshi of its transparent @@ -344,8 +363,9 @@ pub fn miner_fees_are_valid( // from the block subsidy value plus the transaction fees paid by transactions in this block. let total_output_value = (transparent_value_balance - sapling_value_balance - orchard_value_balance - + expected_deferred_pool_balance_change.value()) - .map_err(|_| SubsidyError::Overflow)?; + + expected_deferred_pool_balance_change.value() + + zip233_amount) + .map_err(|_| SubsidyError::Overflow)?; let total_input_value = (expected_block_subsidy + block_miner_fees).map_err(|_| SubsidyError::Overflow)?; diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index ab068e88dd2..b7ba48ce872 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -576,6 +576,192 @@ fn miner_fees_validation_failure() -> Result<(), Report> { Ok(()) } +#[cfg(all(feature = "tx_v6", zcash_unstable = "zip235"))] +#[test] +fn miner_fees_validation_fails_when_zip233_amount_is_zero() -> Result<(), Report> { + use zebra_chain::parameters::testnet::{ + self, ConfiguredActivationHeights, ConfiguredFundingStreams, + }; + + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let zip233_amount = Amount::zero(); + let expected_block_subsidy = 100_000_000.try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = DeferredPoolBalanceChange::new(Amount::zero()); + + let regtest = testnet::Parameters::build() + .with_slow_start_interval(Height::MIN) + .with_activation_heights(ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + }) + .unwrap() + .with_funding_streams(vec![ConfiguredFundingStreams { + height_range: Some(Height(1)..Height(10)), + recipients: None, + }]) + .to_network() + .unwrap(); + + let network_upgrade = NetworkUpgrade::Nu7; + let height = network_upgrade + .activation_height(®test) + .expect("failed to get the activation height for Nu7"); + + let coinbase_tx = Transaction::V6 { + network_upgrade, + lock_time: LockTime::unlocked(), + expiry_height: height, + zip233_amount, + inputs: vec![], + outputs: vec![transparent::Output::new( + transparent_value_balance, + zebra_chain::transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + assert_eq!( + check::miner_fees_are_valid( + &coinbase_tx, + height, + block_miner_fees, + expected_block_subsidy, + expected_deferred_amount, + ®test, + ), + Err(BlockError::Transaction(TransactionError::Subsidy( + SubsidyError::InvalidZip233Amount + ))) + ); + + Ok(()) +} + +#[cfg(all(feature = "tx_v6", zcash_unstable = "zip235"))] +#[test] +fn miner_fees_validation_succeeds_when_zip233_amount_is_correct() -> Result<(), Report> { + use zebra_chain::parameters::testnet::{ + self, ConfiguredActivationHeights, ConfiguredFundingStreams, + }; + + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let zip233_amount = 600.try_into().unwrap(); + let expected_block_subsidy = (100_000_600).try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = DeferredPoolBalanceChange::new(Amount::zero()); + + let regtest = testnet::Parameters::build() + .with_slow_start_interval(Height::MIN) + .with_activation_heights(ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + }) + .unwrap() + .with_funding_streams(vec![ConfiguredFundingStreams { + height_range: Some(Height(1)..Height(10)), + recipients: None, + }]) + .to_network() + .unwrap(); + + let network_upgrade = NetworkUpgrade::Nu7; + let height = network_upgrade + .activation_height(®test) + .expect("failed to get the activation height for Nu7"); + + let coinbase_tx = Transaction::V6 { + network_upgrade, + lock_time: LockTime::unlocked(), + expiry_height: height, + zip233_amount, + inputs: vec![], + outputs: vec![transparent::Output::new( + transparent_value_balance, + zebra_chain::transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + assert_eq!( + check::miner_fees_are_valid( + &coinbase_tx, + height, + block_miner_fees, + expected_block_subsidy, + expected_deferred_amount, + ®test, + ), + Ok(()) + ); + + Ok(()) +} + +#[cfg(all(feature = "tx_v6", zcash_unstable = "zip235"))] +#[test] +fn miner_fees_validation_fails_when_zip233_amount_is_incorrect() -> Result<(), Report> { + use zebra_chain::parameters::testnet::{ + self, ConfiguredActivationHeights, ConfiguredFundingStreams, + }; + + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let zip233_amount = 500.try_into().unwrap(); + let expected_block_subsidy = (100_000_500).try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = DeferredPoolBalanceChange::new(Amount::zero()); + + let regtest = testnet::Parameters::build() + .with_slow_start_interval(Height::MIN) + .with_activation_heights(ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + }) + .unwrap() + .with_funding_streams(vec![ConfiguredFundingStreams { + height_range: Some(Height(1)..Height(10)), + recipients: None, + }]) + .to_network() + .unwrap(); + + let network_upgrade = NetworkUpgrade::Nu7; + let height = network_upgrade + .activation_height(®test) + .expect("failed to get the activation height for Nu7"); + + let coinbase_tx = Transaction::V6 { + network_upgrade, + lock_time: LockTime::unlocked(), + expiry_height: height, + zip233_amount, + inputs: vec![], + outputs: vec![transparent::Output::new( + transparent_value_balance, + zebra_chain::transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + assert_eq!( + check::miner_fees_are_valid( + &coinbase_tx, + height, + block_miner_fees, + expected_block_subsidy, + expected_deferred_amount, + ®test, + ), + Err(BlockError::Transaction(TransactionError::Subsidy( + SubsidyError::InvalidZip233Amount + ))) + ); + + Ok(()) +} + #[test] fn time_is_valid_for_historical_blocks() -> Result<(), Report> { let _init_guard = zebra_test::init(); @@ -690,6 +876,43 @@ fn merkle_root_fake_v5_for_network(network: Network) -> Result<(), Report> { Ok(()) } +/// A block whose transaction list contains the same `transaction::Hash` +/// twice must be rejected with [`BlockError::DuplicateTransaction`]. +/// +/// This guards against Bitcoin's Merkle tree malleability (CVE-2012-2459), +/// where two identical transaction hashes can produce the same Merkle root +/// as the underlying unique list. +#[test] +fn merkle_root_validity_rejects_duplicate_transaction_hash() -> Result<(), Report> { + let _init_guard = zebra_test::init(); + + let network = Network::Mainnet; + + let mut block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES[..]) + .expect("block should deserialize"); + + // Duplicate the coinbase transaction so the block contains two + // transactions with the same hash. + let duplicate = block + .transactions + .first() + .expect("block has coinbase") + .clone(); + block.transactions.push(duplicate); + + let transaction_hashes: Vec<_> = block.transactions.iter().map(|tx| tx.hash()).collect(); + + // Recompute the Merkle root from the duplicated transaction list so + // that `merkle_root_validity` passes the `BadMerkleRoot` check and + // reaches the duplicate-hash check. + Arc::make_mut(&mut block.header).merkle_root = transaction_hashes.iter().cloned().collect(); + + let result = check::merkle_root_validity(&network, &block, &transaction_hashes); + assert_eq!(result, Err(BlockError::DuplicateTransaction)); + + Ok(()) +} + #[test] fn legacy_sigops_count_for_large_generated_blocks() { let _init_guard = zebra_test::init(); @@ -775,3 +998,35 @@ fn transaction_expiration_height_for_network(network: &Network) -> Result<(), Re Ok(()) } + +#[test] +fn block_error_misbehavior_scores() { + use crate::error::BlockError; + + assert_eq!(BlockError::NoTransactions.misbehavior_score(), 100); + assert_eq!( + BlockError::BadMerkleRoot { + actual: zebra_chain::block::merkle::Root([0; 32]), + expected: zebra_chain::block::merkle::Root([1; 32]), + } + .misbehavior_score(), + 100 + ); + assert_eq!( + BlockError::WrongTransactionConsensusBranchId.misbehavior_score(), + 100 + ); + assert_eq!( + BlockError::MissingHeight(zebra_chain::block::Hash([0; 32])).misbehavior_score(), + 100 + ); +} + +#[test] +fn verify_block_error_misbehavior_scores() { + let dup_err = zebra_state::CommitBlockError::Duplicate { + hash_or_height: None, + location: zebra_state::KnownBlock::BestChain, + }; + assert_eq!(VerifyBlockError::Commit(dup_err).misbehavior_score(), 0); +} diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index 6c3387fa57e..7ea7535a6bf 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -28,12 +28,11 @@ use tower::{Service, ServiceExt}; use tracing::instrument; use zebra_chain::{ - amount::{self, DeferredPoolBalanceChange}, + amount::{self}, block::{self, Block}, parameters::{ - checkpoint::list::CheckpointList, - subsidy::{block_subsidy, funding_stream_values, FundingStreamReceiver, SubsidyError}, - Network, GENESIS_PREVIOUS_BLOCK_HASH, + checkpoint::list::CheckpointList, subsidy::SubsidyError, Network, + GENESIS_PREVIOUS_BLOCK_HASH, }, work::equihash, }; @@ -610,18 +609,8 @@ where crate::block::check::equihash_solution_is_valid(&block.header)?; } - // See [ZIP-1015](https://zips.z.cash/zip-1015). - let expected_deferred_amount = - funding_stream_values(height, &self.network, block_subsidy(height, &self.network)?)? - .remove(&FundingStreamReceiver::Deferred); - - let deferred_pool_balance_change = expected_deferred_amount - .unwrap_or_default() - .checked_sub(self.network.lockbox_disbursement_total_amount(height)) - .map(DeferredPoolBalanceChange::new); - // don't do precalculation until the block passes basic difficulty checks - let block = CheckpointVerifiedBlock::new(block, Some(hash), deferred_pool_balance_change); + let block = CheckpointVerifiedBlock::new(block, Some(hash)); crate::block::check::merkle_root_validity( &self.network, diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 5e4072b6850..d2266342d47 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -5,9 +5,12 @@ //! implement, and ensures that we don't reject blocks or transactions //! for a non-enumerated reason. +use std::{array::TryFromSliceError, convert::Infallible}; + use chrono::{DateTime, Utc}; use thiserror::Error; +use zcash_protocol::value::BalanceError; use zebra_chain::{ amount, block, orchard, parameters::subsidy::SubsidyError, @@ -80,6 +83,9 @@ pub enum TransactionError { transaction_hash: zebra_chain::transaction::Hash, }, + #[error("could not construct coinbase tx: {0}")] + CoinbaseConstruction(String), + #[error( "expiry {expiry_height:?} must be less than the maximum {MAX_EXPIRY_HEIGHT:?} \ coinbase: {is_coinbase}, block: {block_height:?}, failing transaction: {transaction_hash:?}" @@ -227,6 +233,24 @@ pub enum TransactionError { #[error("wrong tx format: tx version is ≥ 5, but `nConsensusBranchId` is missing")] MissingConsensusBranchId, + + #[error("input/output error")] + Io(String), + + #[error("failed to convert a slice")] + TryFromSlice(String), + + #[error("invalid amount")] + Amount(String), + + #[error("invalid balance")] + Balance(String), + + #[error("Orchard proof has a non-canonical size")] + OrchardProofSize, + + #[error("unexpected error")] + Other(String), } impl From for TransactionError { @@ -235,6 +259,48 @@ impl From for TransactionError { } } +impl From for TransactionError { + fn from(err: zcash_transparent::builder::Error) -> Self { + TransactionError::CoinbaseConstruction(err.to_string()) + } +} + +impl From> for TransactionError { + fn from(err: zcash_primitives::transaction::builder::Error) -> Self { + TransactionError::CoinbaseConstruction(err.to_string()) + } +} + +impl From for TransactionError { + fn from(err: BalanceError) -> Self { + TransactionError::Balance(err.to_string()) + } +} + +impl From for TransactionError { + fn from(err: libzcash_script::Error) -> Self { + TransactionError::Script(zebra_script::Error::from(err)) + } +} + +impl From for TransactionError { + fn from(err: std::io::Error) -> Self { + TransactionError::Io(err.to_string()) + } +} + +impl From for TransactionError { + fn from(err: TryFromSliceError) -> Self { + TransactionError::TryFromSlice(err.to_string()) + } +} + +impl From for TransactionError { + fn from(err: amount::Error) -> Self { + TransactionError::Amount(err.to_string()) + } +} + // TODO: use a dedicated variant and From impl for each concrete type, and update callers (#5732) impl From for TransactionError { fn from(mut err: BoxError) -> Self { @@ -298,7 +364,9 @@ impl TransactionError { | DisabledAddToSproutPool | NotEnoughFlags | WrongConsensusBranchId - | MissingConsensusBranchId => 100, + | MissingConsensusBranchId + | LockedUntilAfterBlockHeight(_) + | LockedUntilAfterBlockTime(_) => 100, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] CoinbaseHasEnableZSA => 100, @@ -413,7 +481,12 @@ impl BlockError { | MaxHeight(_, _, _) | InvalidDifficulty(_, _) | TargetDifficultyLimit(_, _, _, _, _) - | DifficultyFilter(_, _, _, _) => 100, + | DifficultyFilter(_, _, _, _) + | NoTransactions + | BadMerkleRoot { .. } + | WrongTransactionConsensusBranchId + | TooManyTransparentSignatureOperations { .. } => 100, + Transaction(err) => err.mempool_misbehavior_score(), _other => 0, } } diff --git a/zebra-consensus/src/primitives/groth16.rs b/zebra-consensus/src/primitives/groth16.rs index a541d2cf033..2089bcbf2ee 100644 --- a/zebra-consensus/src/primitives/groth16.rs +++ b/zebra-consensus/src/primitives/groth16.rs @@ -1,4 +1,4 @@ -//! Async Groth16 batch verifier service +//! Async Groth16 verifier service for Sprout JoinSplit proofs use std::fmt; @@ -35,7 +35,7 @@ mod tests; #[cfg(test)] mod vectors; -pub use params::{SAPLING, SPROUT}; +pub use params::SPROUT; use crate::error::TransactionError; @@ -101,15 +101,6 @@ pub static JOINSPLIT_VERIFIER: Lazy< ) }); -/// A Groth16 Description (JoinSplit, Spend, or Output) with a Groth16 proof -/// and its inputs encoded as scalars. -pub trait Description { - /// The Groth16 proof of this description. - fn proof(&self) -> &Groth16Proof; - /// The primary inputs for this proof, encoded as [`jubjub::Fq`] scalars. - fn primary_inputs(&self) -> Vec; -} - /// Compute the [h_{Sig} hash function][1] which is used in JoinSplit descriptions. /// /// `random_seed`: the random seed from the JoinSplit description. @@ -139,7 +130,10 @@ pub(super) fn h_sig( h_sig } -impl Description for (&JoinSplit, &ed25519::VerificationKeyBytes) { +impl Item { + /// Converts a Sprout JoinSplit description and its associated public key into a + /// Groth16 verification [`Item`]. + /// /// Encodes the primary input for the JoinSplit proof statement as Bls12_381 base /// field elements, to match [`bellman::groth16::verify_proof()`]. /// @@ -151,12 +145,12 @@ impl Description for (&JoinSplit, &ed25519::VerificationKeyBytes) /// This is not yet officially documented; see the reference implementation: /// /// - // // The borrows are actually needed to avoid taking ownership #[allow(clippy::needless_borrow)] - fn primary_inputs(&self) -> Vec { - let (joinsplit, joinsplit_pub_key) = self; - + pub fn from_joinsplit( + joinsplit: &JoinSplit, + joinsplit_pub_key: &ed25519::VerificationKeyBytes, + ) -> Result { let rt: [u8; 32] = joinsplit.anchor.into(); let mac1: [u8; 32] = (&joinsplit.vmacs[0]).into(); let mac2: [u8; 32] = (&joinsplit.vmacs[1]).into(); @@ -188,27 +182,8 @@ impl Description for (&JoinSplit, &ed25519::VerificationKeyBytes) public_input.extend(vpub_new); let public_input = multipack::bytes_to_bits(&public_input); + let primary_inputs = multipack::compute_multipacking(&public_input); - multipack::compute_multipacking(&public_input) - } - - fn proof(&self) -> &Groth16Proof { - &self.0.zkproof - } -} - -/// A wrapper to allow a TryFrom blanket implementation of the [`Description`] -/// trait for the [`Item`] struct. -/// See for more details. -pub struct DescriptionWrapper(pub T); - -impl TryFrom> for Item -where - T: Description, -{ - type Error = TransactionError; - - fn try_from(input: DescriptionWrapper<&T>) -> Result { // # Consensus // // > Elements of a JoinSplit description MUST have the types given above @@ -217,11 +192,10 @@ where // // This validates the 𝜋_{ZKJoinSplit} element. In #3179 we plan to validate // during deserialization, see [`JoinSplit::zcash_deserialize`]. - Ok(Item::from(( - bellman::groth16::Proof::read(&input.0.proof().0[..]) - .map_err(|e| TransactionError::MalformedGroth16(e.to_string()))?, - input.0.primary_inputs(), - ))) + let proof = bellman::groth16::Proof::read(&joinsplit.zkproof.0[..]) + .map_err(|e| TransactionError::MalformedGroth16(e.to_string()))?; + + Ok(Item::from((proof, primary_inputs))) } } diff --git a/zebra-consensus/src/primitives/groth16/params.rs b/zebra-consensus/src/primitives/groth16/params.rs index fb09ab9fc65..0533aa74656 100644 --- a/zebra-consensus/src/primitives/groth16/params.rs +++ b/zebra-consensus/src/primitives/groth16/params.rs @@ -1,19 +1,10 @@ -//! Loading and checking correctness of Groth16 Sapling and Sprout parameters. +//! Loading and checking correctness of Groth16 Sprout parameters. use bellman::groth16::{prepare_verifying_key, PreparedVerifyingKey, VerifyingKey}; use bls12_381::Bls12; use derive_getters::Getters; -use zcash_proofs::prover::LocalTxProver; lazy_static::lazy_static! { - /// Sapling prover containing spend and output params for the Sapling circuit. - /// - /// Used to: - /// - /// - construct Sapling outputs in coinbase txs, and - /// - verify Sapling shielded data in the tx verifier. - pub static ref SAPLING: LocalTxProver = LocalTxProver::bundled(); - /// Spend parameters for the Sprout circuit. /// /// Used to verify Sprout shielded data in transactions. diff --git a/zebra-consensus/src/primitives/groth16/tests.rs b/zebra-consensus/src/primitives/groth16/tests.rs index f262fbc5e6f..bbcb2cb55d7 100644 --- a/zebra-consensus/src/primitives/groth16/tests.rs +++ b/zebra-consensus/src/primitives/groth16/tests.rs @@ -41,8 +41,7 @@ where .sprout_joinsplit_pub_key() .expect("pub key must exist since there are joinsplits"); let joinsplit_rsp = verifier.ready().await?.call( - DescriptionWrapper(&(joinsplit, &pub_key)) - .try_into() + Item::from_joinsplit(joinsplit, &pub_key) .map_err(tower_fallback::BoxedError::from)?, ); @@ -104,11 +103,10 @@ where tracing::trace!(?joinsplit); - let joinsplit_rsp = verifier.ready().await?.call( - DescriptionWrapper(&(joinsplit, pub_key)) - .try_into() - .map_err(tower_fallback::BoxedError::from)?, - ); + let joinsplit_rsp = verifier + .ready() + .await? + .call(Item::from_joinsplit(joinsplit, pub_key).map_err(tower_fallback::BoxedError::from)?); async_checks.push(joinsplit_rsp); @@ -225,8 +223,7 @@ where // which will make the verification fail. let modified_pub_key = [0x42; 32].into(); let joinsplit_rsp = verifier.ready().await?.call( - DescriptionWrapper(&(joinsplit, &modified_pub_key)) - .try_into() + Item::from_joinsplit(joinsplit, &modified_pub_key) .map_err(tower_fallback::BoxedError::from)?, ); diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index a11131d8bfc..59d9414b128 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -10,23 +10,32 @@ use std::{ use futures::{future::BoxFuture, FutureExt}; use once_cell::sync::Lazy; -use orchard::{bundle::BatchValidator, circuit::VerifyingKey, flavor::OrchardVanilla}; -use rand::thread_rng; -use zcash_primitives::transaction::OrchardBundle; -use zebra_chain::transaction::SigHash; +use orchard::{ + bundle::BatchValidator, + circuit::{OrchardCircuitVersion, VerifyingKey}, + flavor::OrchardVanilla, +}; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] use orchard::flavor::OrchardZSA; +use rand::thread_rng; +use zebra_chain::{parameters::NetworkUpgrade, transaction::SigHash}; + +use zcash_primitives::transaction::OrchardBundle; + use crate::BoxError; use thiserror::Error; use tokio::sync::watch; -use tower::{util::ServiceFn, Service}; +use tower::Service; use tower_batch_control::{Batch, BatchControl, RequestWeight}; use tower_fallback::Fallback; use super::spawn_fifo; +#[cfg(test)] +mod tests; + /// Adjusted batch size for halo2 batches. /// /// Unlike other batch verifiers, halo2 has aggregate proofs. @@ -43,29 +52,58 @@ type VerifyResult = bool; /// The type of the batch sender channel. type Sender = watch::Sender>; -/// Temporary substitute type for fake batch verification. -/// -/// TODO: implement batch verification -pub type BatchVerifyingKey = ItemVerifyingKey; - /// The type of a prepared verifying key. /// This is the key used to verify individual items. pub type ItemVerifyingKey = VerifyingKey; +// NU6.2 re-enables Orchard actions and ships the *fixed* variable-base +// scalar-multiplication Orchard circuit (the circuit bug that caused Orchard to be temporarily +// disabled; see GHSA-jfw5-j458-pfv6). The fix changes the Orchard Action circuit, and therefore +// its verifying key: a proof produced under one circuit version does not verify under the other +// key. So we keep BOTH keys, each in its own dedicated verifier, and route each bundle to the +// correct verifier by the block's network upgrade (see [`verifier_for`]): +// +// * Orchard bundles mined before NU6.2 (NU5..NU6.2) were produced by the historical, insecure +// circuit and only verify under the [`InsecurePreNu6_2`] key. These must keep verifying so +// that nodes can re-sync and reindex pre-soft-fork Orchard history. +// +// * Orchard bundles mined at NU6.2 onward are produced by the fixed circuit and only verify +// under the [`FixedPostNu6_2`] key. +// +// NOTE: this deliberately does NOT copy zcashd PR #176's WIP shortcut of validating everything +// against the fixed key; that is incorrect for re-syncing pre-soft-fork Orchard blocks, whose +// proofs only verify under the insecure key. lazy_static::lazy_static! { - /// The halo2 proof verifying key for OrchardVanilla. - pub static ref VERIFYING_KEY_VANILLA: ItemVerifyingKey = + /// The Orchard Action verifying key for the **pre-NU6.2** (insecure) circuit. + /// + /// Reconstructs the verifying key of the original (NU5..NU6.2) Orchard Action circuit. + /// Bundles mined before NU6.2 committed to this circuit and only verify under this key, so it + /// MUST be retained to re-verify pre-NU6.2 history on resync. It must never be used to verify + /// post-NU6.2 bundles. + pub static ref VERIFYING_KEY_PRE_NU6_2: ItemVerifyingKey = + ItemVerifyingKey::build_for_version::(OrchardCircuitVersion::InsecurePreNu6_2); + + /// The Orchard Action verifying key for the **NU6.2+** (fixed) circuit. + /// + /// Built from the fixed variable-base scalar-multiplication Orchard Action circuit shipped in + /// NU6.2. Bundles mined at or after the NU6.2 activation height commit to this circuit and + /// only verify under this key. See [`VERIFYING_KEY_PRE_NU6_2`] for the era split. + pub static ref VERIFYING_KEY_POST_NU6_2: ItemVerifyingKey = ItemVerifyingKey::build::(); } #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] lazy_static::lazy_static! { - /// The halo2 proof verifying key for OrchardZSA. + /// The Orchard Action verifying key for the ZSA circuit. pub static ref VERIFYING_KEY_ZSA: ItemVerifyingKey = ItemVerifyingKey::build::(); } /// A Halo2 verification item, used as the request type of the service. +/// +/// An [`Item`] is key-agnostic: it carries only the bundle and sighash. The verifying key (pre- +/// vs post-NU6.2) is supplied by whichever [`Verifier`] processes the item, so an item is always +/// validated against exactly one key and eras are never mixed within a batch. #[derive(Clone, Debug)] pub struct Item { bundle: OrchardBundle, @@ -75,9 +113,9 @@ pub struct Item { impl RequestWeight for Item { fn request_weight(&self) -> usize { match &self.bundle { - OrchardBundle::OrchardVanilla(b) => b.actions().len(), + OrchardBundle::OrchardVanilla(bundle) => bundle.actions().len(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - OrchardBundle::OrchardZSA(b) => b.actions().len(), + OrchardBundle::OrchardZSA(bundle) => bundle.actions().len(), } } } @@ -88,10 +126,11 @@ impl Item { Self { bundle, sighash } } - /// Perform non-batched verification of this [`Item`]. + /// Perform non-batched verification of this [`Item`] against `vk`. /// /// This is useful (in combination with `Item::clone`) for implementing - /// fallback logic when batch verification fails. + /// fallback logic when batch verification fails. The caller supplies the + /// verifying key for the item's era. pub fn verify_single(self, vk: &ItemVerifyingKey) -> bool { let mut batch = BatchValidator::default(); batch.queue(self); @@ -138,79 +177,162 @@ impl From for Halo2Error { } } -/// Global batch verification context for Halo2 proofs of Action statements. +/// The single-item fallback service for one Orchard circuit era. /// -/// This service transparently batches contemporaneous proof verifications, -/// handling batch failures by falling back to individual verification. +/// When a batch fails, [`Fallback`] re-runs each item individually through this service. It holds +/// the *same* verifying key as the batch it backs, so the fallback can never validate an item +/// against a different era's key than the batch did. The key is named once, when the verifier is +/// built (see [`batch_verifier`]). /// -/// Note that making a `Service` call requires mutable access to the service, so -/// you should call `.clone()` on the global handle to create a local, mutable -/// handle. -pub static VERIFIER_VANILLA: Lazy< - Fallback< - Batch, - ServiceFn BoxFuture<'static, Result<(), BoxError>>>, - >, -> = Lazy::new(|| { +/// This is a tiny named service rather than a `service_fn` closure because the closure would have +/// to capture `vk`, and a capturing closure has an unnameable, non-`Clone` type — but the global +/// verifier must be `Clone` to hand out per-call handles. A `&'static` field keeps this `Copy`. +#[derive(Clone, Copy)] +pub struct OrchardFallback { + /// The verifying key for this era, shared with the batch verifier it backs. + vk: &'static ItemVerifyingKey, +} + +impl Service for OrchardFallback { + type Response = (); + type Error = BoxError; + type Future = BoxFuture<'static, Result<(), BoxError>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, item: Item) -> Self::Future { + Verifier::verify_single_spawning(item, self.vk).boxed() + } +} + +/// The concrete type of a global Halo2 verification service. +/// +/// Each Orchard circuit era gets its own instance — see [`VERIFIER_PRE_NU6_2`] and +/// [`VERIFIER_POST_NU6_2`] — so that batches, fallbacks, and verifying keys are fully separated +/// per era. +type VerifierService = Fallback, OrchardFallback>; + +/// Builds a global Halo2 verifier that validates every item against `vk`. +/// +/// The returned service batches contemporaneous proof verifications and, if a batch fails, falls +/// back to verifying each item individually. The batch and its fallback share the single `vk` +/// passed here, so an item built by this verifier is always checked against exactly one era's key. +/// Callers select the correct era's key by which `VERIFYING_KEY_*` they pass (see the two statics +/// below); there is no runtime key resolution. +fn batch_verifier(vk: &'static ItemVerifyingKey) -> VerifierService { Fallback::new( Batch::new( - Verifier::new(&VERIFYING_KEY_VANILLA), + Verifier::new(vk), HALO2_MAX_BATCH_SIZE, None, super::MAX_BATCH_LATENCY, ), - // We want to fallback to individual verification if batch verification fails, - // so we need a Service to use. - // - // Because we have to specify the type of a static, we need to be able to - // write the type of the closure and its return value. But both closures and - // async blocks have unnameable types. So instead we cast the closure to a function - // (which is possible because it doesn't capture any state), and use a BoxFuture - // to erase the result type. - // (We can't use BoxCloneService to erase the service type, because it is !Sync.) - tower::service_fn( - (|item: Item| Verifier::verify_single_spawning(item, &VERIFYING_KEY_VANILLA).boxed()) - as fn(_) -> _, - ), + OrchardFallback { vk }, ) -}); +} -/// Like [`VERIFIER_VANILLA`], but for OrchardZSA proofs. +/// Global batch verification context for **pre-NU6.2** Halo2 Action proofs. +/// +/// Items routed here are verified against [`VERIFYING_KEY_PRE_NU6_2`] (the insecure circuit +/// retained for historical blocks). This service transparently batches contemporaneous proof +/// verifications, handling batch failures by falling back to individual verification. +/// +/// Note that making a `Service` call requires mutable access to the service, so you should call +/// `.clone()` on the global handle to create a local, mutable handle. +pub static VERIFIER_PRE_NU6_2: Lazy = + Lazy::new(|| batch_verifier(&VERIFYING_KEY_PRE_NU6_2)); + +/// Global batch verification context for **NU6.2+** Halo2 Action proofs. +/// +/// Items routed here are verified against [`VERIFYING_KEY_POST_NU6_2`] (the fixed circuit). This +/// service transparently batches contemporaneous proof verifications, handling batch failures by +/// falling back to individual verification. +/// +/// Note that making a `Service` call requires mutable access to the service, so you should call +/// `.clone()` on the global handle to create a local, mutable handle. +pub static VERIFIER_POST_NU6_2: Lazy = + Lazy::new(|| batch_verifier(&VERIFYING_KEY_POST_NU6_2)); + +/// Global batch verification context for ZSA Halo2 Action proofs. #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -pub static VERIFIER_ZSA: Lazy< - Fallback< - Batch, - ServiceFn BoxFuture<'static, Result<(), BoxError>>>, - >, -> = Lazy::new(|| { - Fallback::new( - Batch::new( - Verifier::new(&VERIFYING_KEY_ZSA), - HALO2_MAX_BATCH_SIZE, - None, - super::MAX_BATCH_LATENCY, - ), - tower::service_fn( - (|item: Item| Verifier::verify_single_spawning(item, &VERIFYING_KEY_ZSA).boxed()) - as fn(_) -> _, - ), - ) -}); +pub static VERIFIER_ZSA: Lazy = Lazy::new(|| batch_verifier(&VERIFYING_KEY_ZSA)); + +/// Returns the global Halo2 verifier for `OrchardVanilla` bundles in blocks at `network_upgrade`. +/// +/// Era and flavor are independent axes; this covers only the era. [`verifier_for_bundle`] composes +/// flavor on top, sending `OrchardZSA` bundles to [`VERIFIER_ZSA`] instead. NU7 carries both +/// flavors, because V5 transactions stay valid at NU7, so the upgrade alone cannot pick the key. +/// +/// The Orchard Action circuit — and therefore its verifying key — changed at NU6.2 (the fixed +/// variable-base scalar-multiplication circuit; see GHSA-jfw5-j458-pfv6), and a proof produced +/// under one circuit does not verify under the other key. So each bundle must be checked against +/// the key for the upgrade of the block it appears in: +/// +/// * upgrades before NU6.2 are routed to [`VERIFIER_PRE_NU6_2`] (the historical insecure key), +/// so pre-soft-fork Orchard history still verifies on re-sync; +/// * NU6.2 and every later upgrade are routed to [`VERIFIER_POST_NU6_2`] (the fixed key). +/// +/// The mapping is an explicit, exhaustive `match` on every [`NetworkUpgrade`] variant: there is +/// no version-comparison fallthrough and no default-to-insecure arm, so adding a future upgrade +/// is a compile error here until it is bound to a key on purpose. +pub fn verifier_for(network_upgrade: NetworkUpgrade) -> &'static VerifierService { + use NetworkUpgrade::*; + + match network_upgrade { + // Orchard did not exist before NU5, so these upgrades never carry Orchard bundles. They + // are bound to the pre-NU6.2 (insecure) verifier because that is the only key under which + // any Orchard history before NU6.2 verifies; routing them anywhere else cannot be correct. + Genesis | BeforeOverwinter | Overwinter | Sapling | Blossom | Heartwood | Canopy | Nu5 + | Nu6 | Nu6_1 => &VERIFIER_PRE_NU6_2, + + // NU6.2 ships the fixed circuit, and every upgrade after it inherits that fixed circuit, + // so all of them verify under the fixed key. + Nu6_2 | Nu7 => &VERIFIER_POST_NU6_2, + + // `ZFuture` only exists under the `zcash_unstable = "zfuture"` cfg. It is a post-NU6.2 + // upgrade, so it inherits the fixed circuit and is bound to the fixed key here on purpose + // (rather than via a wildcard) to keep this match exhaustive and fail-closed under every + // build configuration. + #[cfg(zcash_unstable = "zfuture")] + ZFuture => &VERIFIER_POST_NU6_2, + } +} + +/// Returns the global Halo2 verifier holding the key for `bundle`'s circuit, in a block at +/// `network_upgrade`. +/// +/// This composes the two axes: flavor picks the circuit family, and for vanilla bundles +/// [`verifier_for`] then picks the era. NU7 needs both, because a V5 transaction there carries a +/// vanilla bundle while a V6 one carries a ZSA bundle. +pub fn verifier_for_bundle( + bundle: &OrchardBundle, + network_upgrade: NetworkUpgrade, +) -> &'static VerifierService { + match bundle { + OrchardBundle::OrchardVanilla(_) => verifier_for(network_upgrade), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + OrchardBundle::OrchardZSA(_) => &VERIFIER_ZSA, + } +} /// Halo2 proof verifier implementation /// /// This is the core implementation for the batch verification logic of the /// Halo2 verifier. It handles batching incoming requests, driving batches to /// completion, and reporting results. +/// +/// Each verifier validates against a single, fixed [`ItemVerifyingKey`]; the two Orchard circuit +/// eras are served by two independent verifiers, so a batch never mixes pre- and post-NU6.2 +/// proofs. pub struct Verifier { + /// The verifying key that every batch and fallback from this verifier uses. + vk: &'static ItemVerifyingKey, + /// The synchronous Halo2 batch validator. batch: BatchValidator, - /// The halo2 proof verification key. - /// - /// Making this 'static makes managing lifetimes much easier. - vk: &'static ItemVerifyingKey, - /// A channel for broadcasting the result of a batch to the futures for each batch item. /// /// Each batch gets a newly created channel, so there is only ever one result sent per channel. @@ -219,45 +341,50 @@ pub struct Verifier { } impl Verifier { + /// Creates a verifier that validates every item against `vk`. fn new(vk: &'static ItemVerifyingKey) -> Self { - let batch = BatchValidator::default(); let (tx, _) = watch::channel(None); - Self { batch, vk, tx } + Self { + vk, + batch: BatchValidator::default(), + tx, + } } - /// Returns the batch verifier and channel sender from `self`, - /// replacing them with a new empty batch. - fn take(&mut self) -> (BatchValidator, &'static BatchVerifyingKey, Sender) { + /// Returns the batch verifier and channel sender, + /// replacing the batch and channel with new empty ones. + fn take(&mut self) -> (BatchValidator, Sender) { // Use a new verifier and channel for each batch. let batch = mem::take(&mut self.batch); - let (tx, _) = watch::channel(None); let tx = mem::replace(&mut self.tx, tx); - (batch, self.vk, tx) + (batch, tx) } - /// Synchronously process the batch, and send the result using the channel sender. - /// This function blocks until the batch is completed. - fn verify(batch: BatchValidator, vk: &'static BatchVerifyingKey, tx: Sender) { + /// Synchronously process the batch against `vk`, and send the result using + /// the channel sender. This function blocks until the batch is completed. + fn verify(batch: BatchValidator, vk: &'static ItemVerifyingKey, tx: Sender) { let result = batch.validate(vk, thread_rng()); let _ = tx.send(Some(result)); } - /// Flush the batch using a thread pool, and return the result via the channel. + /// Flush the batch using a thread pool, sending the result via the channel. /// This returns immediately, usually before the batch is completed. fn flush_blocking(&mut self) { - let (batch, vk, tx) = self.take(); + let vk = self.vk; + let (batch, tx) = self.take(); // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures. // // We don't care about execution order here, because this method is only called on drop. - tokio::task::block_in_place(|| rayon::spawn_fifo(|| Self::verify(batch, vk, tx))); + tokio::task::block_in_place(|| rayon::spawn_fifo(move || Self::verify(batch, vk, tx))); } - /// Flush the batch using a thread pool, and return the result via the channel. - /// This function returns a future that becomes ready when the batch is completed. - async fn flush_spawning(batch: BatchValidator, vk: &'static BatchVerifyingKey, tx: Sender) { + /// Flush the batch using a thread pool, validating against `vk` and + /// returning the result via the channel. This function returns a future that + /// becomes ready when the batch is completed. + async fn flush_spawning(batch: BatchValidator, vk: &'static ItemVerifyingKey, tx: Sender) { // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures. let start = std::time::Instant::now(); let result = spawn_fifo(move || batch.validate(vk, thread_rng())).await; @@ -277,14 +404,13 @@ impl Verifier { let _ = tx.send(result.ok()); } - /// Verify a single item using a thread pool, and return the result. + /// Verify a single item against `vk` using a thread pool, and return the result. async fn verify_single_spawning( item: Item, - pvk: &'static ItemVerifyingKey, + vk: &'static ItemVerifyingKey, ) -> Result<(), BoxError> { - // TODO: Restore code for verifying single proofs or return a result from batch.validate() // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures. - if spawn_fifo(move || item.verify_single(pvk)).await? { + if spawn_fifo(move || item.verify_single(vk)).await? { Ok(()) } else { Err("could not validate orchard proof".into()) @@ -295,11 +421,7 @@ impl Verifier { impl fmt::Debug for Verifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = "Verifier"; - f.debug_struct(name) - .field("batch", &"..") - .field("vk", &"..") - .field("tx", &self.tx) - .finish() + f.debug_struct(name).field("batch", &"..").finish() } } @@ -346,9 +468,10 @@ impl Service> for Verifier { BatchControl::Flush => { tracing::trace!("got halo2 flush command"); - let (batch, vk, tx) = self.take(); + let vk = self.vk; + let (batch, tx) = self.take(); - Box::pin(Self::flush_spawning(batch, vk, tx).map(Ok)) + Box::pin(Self::flush_spawning(batch, vk, tx).map(|()| Ok(()))) } } } diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs new file mode 100644 index 00000000000..c5f597118fe --- /dev/null +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -0,0 +1,188 @@ +//! Tests for the Halo2 Orchard Action verifier. +//! +//! The key correctness property of this module is the **era split**: the Orchard Action circuit +//! (and therefore its verifying key) changed at NU6.2 to fix a variable-base scalar-multiplication +//! soundness bug (GHSA-jfw5-j458-pfv6). A proof produced under one circuit does not verify under +//! the other key. These tests guard that: +//! +//! * a real pre-NU6.2 Orchard proof verifies under the pre-NU6.2 (insecure) key, so historical +//! blocks still re-sync; +//! * the same proof is **rejected** by the post-NU6.2 (fixed) key, so the verifier is not +//! "fail-open" — it does not accept whatever it is handed regardless of era; and +//! * [`verifier_for`] routes each network upgrade to the service holding the matching key, +//! with NU6.2 and every later upgrade going to the fixed-key verifier, and never to the ZSA +//! verifier — flavor is composed on top by [`verifier_for_bundle`], which must send vanilla +//! bundles to the era verifier even at NU7. + +use std::sync::Arc; + +use orchard::bundle::Authorized; +use zcash_primitives::transaction::OrchardBundle; +use zebra_chain::{ + block::Block, + parameters::NetworkUpgrade, + serialization::ZcashDeserializeInto, + transaction::{HashType, SigHash}, + transparent, +}; + +use super::{ + verifier_for, verifier_for_bundle, Item, VERIFIER_POST_NU6_2, VERIFIER_PRE_NU6_2, + VERIFYING_KEY_POST_NU6_2, VERIFYING_KEY_PRE_NU6_2, +}; + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +use super::VERIFIER_ZSA; + +/// Returns one real pre-NU6.2 Orchard bundle and its sighash, extracted from the mainnet test +/// blocks. +/// +/// These mainnet blocks are NU5-era Orchard history, mined long before NU6.2, so their proofs +/// were produced by the historical (insecure) circuit and only verify under +/// [`VERIFYING_KEY_PRE_NU6_2`]. Transactions with transparent inputs are skipped because their +/// sighash needs the previous outputs they spend, which are not in the test vectors. +fn pre_nu6_2_bundle_and_sighash() -> (OrchardBundle, SigHash) { + for bytes in zebra_test::vectors::MAINNET_BLOCKS.values() { + let block: Block = bytes + .zcash_deserialize_into() + .expect("hard-coded test vector must deserialize"); + + for tx in &block.transactions { + if !tx.has_orchard_shielded_data() || !tx.inputs().is_empty() { + continue; + } + + let all_previous_outputs: Arc> = Arc::new(Vec::new()); + let Ok(sighasher) = tx.sighasher(NetworkUpgrade::Nu5, all_previous_outputs) else { + continue; + }; + let Some(bundle) = sighasher.orchard_bundle() else { + continue; + }; + + let sighash = sighasher.sighash(HashType::ALL, None); + return (bundle, sighash); + } + } + + panic!("mainnet test blocks must contain a transparent-input-free Orchard transaction"); +} + +/// A real pre-NU6.2 Orchard proof verifies under the pre-NU6.2 key and is rejected by the +/// post-NU6.2 key. +/// +/// This is the core guard for the era split: it proves the two keys are genuinely different and +/// that selecting the wrong era's key causes a hard verification failure. If the verifier ever +/// "fails open" (e.g. validates everything against a single key, like the rejected zcashd WIP +/// shortcut), the wrong-key assertion below would fail. +#[test] +fn pre_nu6_2_proof_only_verifies_under_pre_nu6_2_key() { + let (bundle, sighash) = pre_nu6_2_bundle_and_sighash(); + + // Correct era key: the historical proof must verify, so pre-NU6.2 history still re-syncs. + assert!( + Item::new(bundle.clone(), sighash).verify_single(&VERIFYING_KEY_PRE_NU6_2), + "a real pre-NU6.2 Orchard proof must verify under the pre-NU6.2 (insecure) key" + ); + + // Wrong era key: the same proof must be rejected. This is the not-fail-open guarantee. + assert!( + !Item::new(bundle, sighash).verify_single(&VERIFYING_KEY_POST_NU6_2), + "a pre-NU6.2 Orchard proof must be REJECTED by the post-NU6.2 (fixed) key; \ + verifying it would mean the era selection is fail-open" + ); +} + +/// [`verifier_for`] routes each upgrade to the service that holds the correct era key. +/// +/// We compare service identity by pointer: `verifier_for` returns a borrow of one of the two +/// global `Lazy` services, so the pre-NU6.2 upgrades must alias [`VERIFIER_PRE_NU6_2`] and NU6.2+ +/// must alias [`VERIFIER_POST_NU6_2`]. Because the pre/post split is what binds an item to a key, +/// routing to the wrong service is exactly routing to the wrong key. +/// +/// This is an async test because forcing the global `Lazy` verifiers builds their `Batch` layer, +/// which spawns a worker task and therefore needs a Tokio runtime. +#[tokio::test(flavor = "multi_thread")] +async fn verifier_for_routes_each_upgrade_to_the_correct_key() { + // Deref each `Lazy` to the inner service it guards, matching what `verifier_for` returns, so + // the pointer comparisons below compare the same service type. + let pre: &'static super::VerifierService = &VERIFIER_PRE_NU6_2; + let post: &'static super::VerifierService = &VERIFIER_POST_NU6_2; + + // Everything before NU6.2 (including upgrades from before Orchard existed) routes to the + // insecure key, which is the only key any pre-NU6.2 Orchard history verifies under. + for nu in [ + NetworkUpgrade::Nu5, + NetworkUpgrade::Nu6, + NetworkUpgrade::Nu6_1, + ] { + assert!( + std::ptr::eq(verifier_for(nu), pre), + "{nu:?} must route to the pre-NU6.2 (insecure) verifier" + ); + } + + // NU6.2 and every later upgrade route to the fixed key. Nu7 is included because it carries + // vanilla bundles too, in V5 transactions. + for nu in [NetworkUpgrade::Nu6_2, NetworkUpgrade::Nu7] { + assert!( + std::ptr::eq(verifier_for(nu), post), + "{nu:?} must route to the post-NU6.2 (fixed) verifier" + ); + } + + // The ZSA circuit is a separate axis, so `verifier_for` must never return its verifier. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + let zsa: &'static super::VerifierService = &VERIFIER_ZSA; + + assert!( + !std::ptr::eq(zsa, pre) && !std::ptr::eq(zsa, post), + "the ZSA verifier must not alias either era verifier" + ); + } +} + +/// A vanilla Orchard bundle at NU7 is routed by era, not to the ZSA verifier. +/// +/// V5 transactions stay valid at NU7 and carry vanilla bundles, so routing on the upgrade alone — +/// as the v5.2.0 merge briefly did — sends them to the OrchardZSA key, which cannot verify a +/// vanilla proof. +/// +/// The choice is asserted rather than the outcome, because both the right and the wrong key reject +/// this pre-NU6.2 fixture. The ZSA direction is covered end to end by `check_orchard_zsa_workflow`. +#[tokio::test(flavor = "multi_thread")] +async fn vanilla_bundle_at_nu7_is_routed_by_era_not_flavor() { + let (bundle, _sighash) = pre_nu6_2_bundle_and_sighash(); + + assert!( + matches!(bundle, OrchardBundle::OrchardVanilla(_)), + "a V5 transaction's bundle must be the vanilla flavor" + ); + + assert!( + std::ptr::eq( + verifier_for_bundle(&bundle, NetworkUpgrade::Nu7), + &*VERIFIER_POST_NU6_2 + ), + "a vanilla bundle at NU7 must route to the post-NU6.2 (fixed) verifier" + ); + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + assert!( + !std::ptr::eq( + verifier_for_bundle(&bundle, NetworkUpgrade::Nu7), + &*VERIFIER_ZSA + ), + "a vanilla bundle must never route to the ZSA verifier" + ); + + // The era axis still applies below NU6.2, so historical blocks re-sync. + assert!( + std::ptr::eq( + verifier_for_bundle(&bundle, NetworkUpgrade::Nu5), + &*VERIFIER_PRE_NU6_2 + ), + "a vanilla bundle at NU5 must route to the pre-NU6.2 (insecure) verifier" + ); +} diff --git a/zebra-consensus/src/primitives/sapling.rs b/zebra-consensus/src/primitives/sapling.rs index 279eb3df8b1..4b47b293943 100644 --- a/zebra-consensus/src/primitives/sapling.rs +++ b/zebra-consensus/src/primitives/sapling.rs @@ -17,10 +17,17 @@ use tower_batch_control::{Batch, BatchControl, RequestWeight}; use tower_fallback::Fallback; use sapling_crypto::{bundle::Authorized, BatchValidator, Bundle}; +use zcash_proofs::prover::LocalTxProver; use zcash_protocol::value::ZatBalance; use zebra_chain::transaction::SigHash; -use crate::groth16::SAPLING; +/// Sapling prover containing spend and output params for the Sapling circuit. +/// +/// Used to: +/// +/// - construct Sapling outputs in coinbase txs, and +/// - verify Sapling shielded data in the tx verifier. +static SAPLING: Lazy = Lazy::new(LocalTxProver::bundled); #[derive(Clone)] pub struct Item { @@ -115,10 +122,10 @@ impl Service> for Verifier { rx.borrow() .ok_or("threadpool unexpectedly dropped channel sender")? .then(|| { - metrics::counter!("proofs.groth16.verified").increment(1); + metrics::counter!("proofs.sapling.verified").increment(1); }) .ok_or_else(|| { - metrics::counter!("proofs.groth16.invalid").increment(1); + metrics::counter!("proofs.sapling.invalid").increment(1); "batch verification of Sapling shielded data failed" }) }) diff --git a/zebra-consensus/src/script.rs b/zebra-consensus/src/script.rs index 1c58b32f215..5a2958912da 100644 --- a/zebra-consensus/src/script.rs +++ b/zebra-consensus/src/script.rs @@ -7,6 +7,9 @@ use zebra_script::CachedFfiTransaction; use crate::BoxError; +#[cfg(test)] +mod tests; + /// Asynchronous script verification. /// /// The verifier asynchronously requests the UTXO a transaction attempts @@ -52,26 +55,30 @@ impl tower::Service for Verifier { cached_ffi_transaction, input_index, } = req; - let input = &cached_ffi_transaction.inputs()[input_index]; - match input { - transparent::Input::PrevOut { outpoint, .. } => { - let outpoint = *outpoint; - // Avoid calling the state service if the utxo is already known - let span = tracing::trace_span!("script", ?outpoint); + let span = tracing::trace_span!("script"); + async move { + let input = &cached_ffi_transaction + .inputs() + .get(input_index) + .ok_or_else(|| { + format!("cached_ffi_transaction missing input at index {input_index}") + })?; + + match input { + transparent::Input::PrevOut { outpoint, .. } => { + let outpoint = *outpoint; - async move { + // Avoid calling the state service if the utxo is already known cached_ffi_transaction.is_valid(input_index)?; - tracing::trace!("script verification succeeded"); + tracing::trace!(?outpoint, "script verification succeeded"); Ok(()) } - .instrument(span) - .boxed() - } - transparent::Input::Coinbase { .. } => { - async { Err("unexpected coinbase input".into()) }.boxed() + transparent::Input::Coinbase { .. } => Err("unexpected coinbase input".into()), } } + .instrument(span) + .boxed() } } diff --git a/zebra-consensus/src/script/tests.rs b/zebra-consensus/src/script/tests.rs new file mode 100644 index 00000000000..b740b634b28 --- /dev/null +++ b/zebra-consensus/src/script/tests.rs @@ -0,0 +1,66 @@ +//! Tests for the script verifier service. + +use std::sync::Arc; + +use tower::ServiceExt; +use zebra_chain::{ + block::Block, parameters::NetworkUpgrade, serialization::ZcashDeserialize, transparent, +}; +use zebra_script::CachedFfiTransaction; + +use super::{Request, Verifier}; + +/// Calling the script `Verifier` with an `input_index` past the end of the transaction's +/// inputs must return an error instead of panicking. +#[tokio::test] +async fn verifier_returns_error_for_out_of_range_input_index() { + let _init_guard = zebra_test::init(); + + // Use a v4 mainnet block whose second transaction has at least one transparent input. + let block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_419199_BYTES[..]) + .expect("test vector is a valid block"); + let tx = block + .transactions + .iter() + .find(|tx| { + tx.inputs() + .iter() + .any(|i| matches!(i, transparent::Input::PrevOut { .. })) + }) + .expect("block has a non-coinbase tx") + .clone(); + + // Build matching previous outputs (one per input). Their content does not matter + // because the verifier should error on the out-of-range index before touching them. + let previous_outputs: Vec = tx + .inputs() + .iter() + .map(|_| transparent::Output { + value: 0.try_into().expect("zero is a valid amount"), + lock_script: transparent::Script::new(&[]), + }) + .collect(); + + let cached = Arc::new( + CachedFfiTransaction::new( + tx.clone(), + Arc::new(previous_outputs), + NetworkUpgrade::Sapling, + ) + .expect("constructor accepts the test fixture"), + ); + + let out_of_range = tx.inputs().len(); + let result = Verifier + .oneshot(Request { + cached_ffi_transaction: cached, + input_index: out_of_range, + }) + .await; + + let err = result.expect_err("out-of-range input_index must error, not panic"); + assert!( + err.to_string().contains("missing input"), + "unexpected error message: {err}", + ); +} diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index b7fd6135e29..b3f342fd3a5 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -43,7 +43,7 @@ use zebra_node_services::mempool; use zebra_script::{CachedFfiTransaction, Sigops}; use zebra_state as zs; -use crate::{error::TransactionError, groth16::DescriptionWrapper, primitives, script, BoxError}; +use crate::{error::TransactionError, primitives, script, BoxError}; pub mod check; #[cfg(test)] @@ -406,22 +406,48 @@ where async move { tracing::trace!(?tx_id, ?req, "got tx verify request"); - if let Some(result) = Self::find_verified_unmined_tx(&req, mempool.clone(), state.clone()).await { - let verified_tx = result?; - - return Ok(Response::Block { - tx_id, - miner_fee: Some(verified_tx.miner_fee), - sigops: verified_tx.legacy_sigop_count, - tx_sighash: verified_tx.tx_sighash, - }); - } - // Do quick checks first check::has_inputs_and_outputs(&tx)?; check::has_enough_orchard_flags(&tx)?; check::consensus_branch_id(&tx, req.height(), &network)?; + // Soft fork: temporarily require transactions to not contain Orchard actions. + // + // This soft fork was added while NU 6.1 was the active epoch on the Zcash + // chain, but we apply it uniformly even if NU 6.1 is not active in case it is + // ported to other chains with a different sequence of NUs. + // + // This will be treated as "Rules that apply generally before the next NU" + // when we add the NU that re-enables Orchard actions. + if network.is_orchard_temporarily_disabled(req.height()) && tx.has_orchard_shielded_data() { + return Err(TransactionError::Other("transaction has Orchard actions (temporarily disabled)".into())); + } + + // From the network upgrade that re-enables Orchard actions (NU6.2), require + // that any Orchard proof has the canonical length for its number of actions. + // A proof that is present but not canonically sized can be padded with + // arbitrary trailing data without affecting its validity, allowing excess + // bandwidth and storage costs to be imposed while paying only fees sized to a + // canonical proof (GHSA-jfw5-j458-pfv6). + // + // This is a constricting rule, so it is gated on that network upgrade: + // Orchard actions mined before it, under earlier rules that did not enforce + // the proof size, must remain valid so that nodes can sync and reindex the + // chain before the soft fork that temporarily disabled Orchard. Orchard + // bundles are deserialized leniently, so the size is checked here, where the + // block height is available, rather than during parsing. + // + // The gate activates at the NU6.2 activation height committed in + // MAINNET/TESTNET_ACTIVATION_HEIGHTS. See + // `Network::orchard_canonical_proof_size_rule_active`. + if network.orchard_canonical_proof_size_rule_active(req.height()) { + if let Some(proof_size_is_canonical) = tx.orchard_proof_size_is_canonical() { + if !proof_size_is_canonical { + return Err(TransactionError::OrchardProofSize); + } + } + } + // Validate the coinbase input consensus rules if req.is_mempool() && tx.is_coinbase() { return Err(TransactionError::CoinbaseInMempool); @@ -547,7 +573,7 @@ where ); Ok(()) } - ); + ); async_checks.push(check_anchors_and_revealed_nullifiers_query); } @@ -589,7 +615,13 @@ where Request::Block { .. } => Response::Block { tx_id, miner_fee, - sigops, + // In block validation, the consensus sigop total must include P2SH + // redeem-script sigops, matching zcashd's `ConnectBlock` which sums + // `GetLegacySigOpCount` and `GetP2SHSigOpCount` per transaction before + // comparing against `MAX_BLOCK_SIGOPS`. Coinbase inputs contribute zero P2SH + // sigops. See + // . + sigops: sigops.saturating_add(cached_ffi_transaction.p2sh_sigops()), tx_sighash }, Request::Mempool { transaction: tx, .. } => { @@ -603,6 +635,7 @@ where tx, miner_fee.expect("fee should have been checked earlier"), sigops, + cached_ffi_transaction.p2sh_sigops(), spent_outputs.into(), tx_sighash, )?; @@ -627,12 +660,12 @@ where Ok(rsp) } - .inspect(move |result| { - // Hide the transaction data to avoid filling the logs - tracing::trace!(?tx_id, result = ?result.as_ref().map(|_tx| ()), "got tx verify result"); - }) - .instrument(span) - .boxed() + .inspect(move |result| { + // Hide the transaction data to avoid filling the logs + tracing::trace!(?tx_id, result = ?result.as_ref().map(|_tx| ()), "got tx verify result"); + }) + .instrument(span) + .boxed() } } @@ -667,74 +700,6 @@ where } } - /// Attempts to find a transaction in the mempool by its transaction hash and checks - /// that all of its dependencies are available in the block or in the state. Waits - /// for UTXOs being spent by the given transaction to arrive in the state if they're - /// not found elsewhere. - /// - /// Returns [`Some(Ok(VerifiedUnminedTx))`](VerifiedUnminedTx) if successful, - /// None if the transaction id was not found in the mempool, - /// or `Some(Err(TransparentInputNotFound))` if the transaction was found, but some of its - /// dependencies were not found in the block or state after a timeout. - async fn find_verified_unmined_tx( - req: &Request, - mempool: Option>, - state: Timeout, - ) -> Option> { - let tx = req.transaction(); - - if req.is_mempool() || tx.is_coinbase() { - return None; - } - - let mempool = mempool?; - let known_outpoint_hashes = req.known_outpoint_hashes(); - let tx_id = req.tx_mined_id(); - - let mempool::Response::TransactionWithDeps { - transaction: verified_tx, - dependencies, - } = mempool - .oneshot(mempool::Request::TransactionWithDepsByMinedId(tx_id)) - .await - .ok()? - else { - panic!("unexpected response to TransactionWithDepsByMinedId request"); - }; - - // Note: This does not verify that the spends are in order, the spend order - // should be verified during contextual validation in zebra-state. - let missing_deps: HashSet<_> = dependencies - .into_iter() - .filter(|dependency_id| !known_outpoint_hashes.contains(dependency_id)) - .collect(); - - if missing_deps.is_empty() { - return Some(Ok(verified_tx)); - } - - let missing_outpoints = tx.inputs().iter().filter_map(|input| { - if let transparent::Input::PrevOut { outpoint, .. } = input { - missing_deps.contains(&outpoint.hash).then_some(outpoint) - } else { - None - } - }); - - for missing_outpoint in missing_outpoints { - let query = state - .clone() - .oneshot(zebra_state::Request::AwaitUtxo(*missing_outpoint)); - match query.await { - Ok(zebra_state::Response::Utxo(_)) => {} - Err(_) => return Some(Err(TransactionError::TransparentInputNotFound)), - _ => unreachable!("AwaitUtxo always responds with Utxo"), - }; - } - - Some(Ok(verified_tx)) - } - /// Wait for the UTXOs that are being spent by the given transaction. /// /// Looks up UTXOs that are being spent by the given transaction in the state or waits @@ -955,6 +920,7 @@ where | NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu7 => Ok(()), #[cfg(zcash_unstable = "zfuture")] @@ -1014,7 +980,7 @@ where cached_ffi_transaction, )? .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)) - .and(Self::verify_orchard_bundle(orchard_bundle, &sighash)); + .and(Self::verify_orchard_bundle(orchard_bundle, &sighash, nu)); Ok((async_check, sighash)) } @@ -1040,6 +1006,7 @@ where NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu7 => Ok(()), #[cfg(zcash_unstable = "zfuture")] @@ -1107,7 +1074,7 @@ where cached_ffi_transaction, )? .and(Self::verify_sapling_bundle(sapling_bundle, &sighash)) - .and(Self::verify_orchard_bundle(orchard_bundle, &sighash)); + .and(Self::verify_orchard_bundle(orchard_bundle, &sighash, nu)); Ok((async_check, sighash)) } @@ -1146,7 +1113,8 @@ where | NetworkUpgrade::Canopy | NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 - | NetworkUpgrade::Nu6_1 => Err(TransactionError::UnsupportedByNetworkUpgrade( + | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 => Err(TransactionError::UnsupportedByNetworkUpgrade( transaction.version(), network_upgrade, )), @@ -1209,7 +1177,7 @@ where // checks that (at a minimum) must pass for the // transaction to verify. checks.push(primitives::groth16::JOINSPLIT_VERIFIER.oneshot( - DescriptionWrapper(&(joinsplit, &joinsplit_data.pub_key)).try_into()?, + primitives::groth16::Item::from_joinsplit(joinsplit, &joinsplit_data.pub_key)?, )); } @@ -1319,12 +1287,24 @@ where } /// Verifies a transaction's Orchard shielded data. + /// + /// A proof only verifies under the key for the circuit that produced it, so the verifier is + /// selected on two independent axes: + /// + /// * flavor: `OrchardZSA` bundles use the ZSA circuit, whatever the upgrade. + /// * era, for `OrchardVanilla` bundles: the vanilla circuit changed at NU6.2 to fix + /// GHSA-jfw5-j458-pfv6, so `network_upgrade` — the upgrade at the transaction's block + /// height — selects the historical or the fixed key. + /// + /// [`primitives::halo2::verifier_for_bundle`] composes both. + /// + /// Both are needed: NU7 carries vanilla bundles (in V5 transactions, still valid at NU7) as + /// well as ZSA ones. Each verifier holds one key and batches separately, so circuits never mix. fn verify_orchard_bundle( bundle: Option>, sighash: &SigHash, + network_upgrade: NetworkUpgrade, ) -> AsyncChecks { - use zcash_primitives::transaction::OrchardBundle; - let mut async_checks = AsyncChecks::new(); if let Some(bundle) = bundle { @@ -1339,20 +1319,13 @@ where // aggregated Halo2 proof per transaction, even with multiple // Actions in one transaction. So we queue it for verification // only once instead of queuing it up for every Action description. - let item = primitives::halo2::Item::new(bundle.clone(), *sighash); - let check = match &bundle { - OrchardBundle::OrchardVanilla(_) => primitives::halo2::VERIFIER_VANILLA - .clone() - .oneshot(item) - .boxed(), - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - OrchardBundle::OrchardZSA(_) => primitives::halo2::VERIFIER_ZSA + // + // Route the bundle to the verifier holding the key for its circuit. + async_checks.push( + primitives::halo2::verifier_for_bundle(&bundle, network_upgrade) .clone() - .oneshot(item) - .boxed(), - }; - - async_checks.push(check); + .oneshot(primitives::halo2::Item::new(bundle, *sighash)), + ); } async_checks diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 0e52fb91d62..84a729ff14b 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -21,7 +21,10 @@ use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block, Height}, orchard::{Action, AuthorizedAction, Flags, OrchardVanilla}, - parameters::{testnet::ConfiguredActivationHeights, Network, NetworkUpgrade}, + parameters::{ + testnet::{ConfiguredActivationHeights, Parameters}, + Network, NetworkUpgrade, + }, primitives::{ed25519, x25519, Groth16Proof}, sapling, serialization::{AtLeastOne, DateTime32, ZcashDeserialize, ZcashDeserializeInto}, @@ -33,17 +36,17 @@ use zebra_chain::{ }, zip317, Hash, HashType, JoinSplitData, LockTime, Transaction, }, - transparent::{self, CoinbaseData, CoinbaseSpendRestriction}, + transparent::{self, CoinbaseSpendRestriction}, }; -#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use zebra_chain::transaction::arbitrary::insert_fake_v6_orchard_shielded_data; - use zebra_node_services::mempool; use zebra_state::ValidateContextError; use zebra_test::mock_service::MockService; -use crate::{error::TransactionError, transaction::POLL_MEMPOOL_DELAY}; +use crate::{ + error::TransactionError, + transaction::{SigHash, POLL_MEMPOOL_DELAY}, +}; use super::{check, Request, Verifier}; @@ -730,7 +733,7 @@ async fn mempool_request_with_unmined_output_spends_is_accepted() { } #[tokio::test(flavor = "multi_thread")] -async fn skips_verification_of_block_transactions_in_mempool() { +async fn dont_skip_verification_of_block_transactions_in_mempool() { let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); let mempool: MockService<_, _, _, _> = MockService::build().for_prop_tests(); let (mempool_setup_tx, mempool_setup_rx) = tokio::sync::oneshot::channel(); @@ -831,7 +834,7 @@ async fn skips_verification_of_block_transactions_in_mempool() { ); let crate::transaction::Response::Mempool { - transaction, + transaction: _, spent_mempool_outpoints, } = verifier_response.expect("already checked that response is ok") else { @@ -844,20 +847,6 @@ async fn skips_verification_of_block_transactions_in_mempool() { "spent_mempool_outpoints in tx verifier response should match input_outpoint" ); - let mut mempool_clone = mempool.clone(); - tokio::spawn(async move { - for _ in 0..2 { - mempool_clone - .expect_request(mempool::Request::TransactionWithDepsByMinedId(tx_hash)) - .await - .expect("verifier should call mock mempool service with correct request") - .respond(mempool::Response::TransactionWithDeps { - transaction: transaction.clone(), - dependencies: [input_outpoint.hash].into(), - }); - } - }); - let make_request = |known_outpoint_hashes| Request::Block { transaction_hash: tx_hash, transaction: Arc::new(tx), @@ -867,6 +856,23 @@ async fn skips_verification_of_block_transactions_in_mempool() { time: Utc::now(), }; + // Both block requests go through full verification (no mempool bypass), so each + // calls AwaitUtxo on the state service. + let utxo_clone = utxo.clone(); + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::AwaitUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::Utxo(utxo_clone)); + + state + .expect_request(zebra_state::Request::AwaitUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::Utxo(utxo)); + }); + // Briefly yield and sleep so the spawned task can first expect the requests. tokio::time::sleep(std::time::Duration::from_millis(10)).await; @@ -874,19 +880,11 @@ async fn skips_verification_of_block_transactions_in_mempool() { .clone() .oneshot(make_request.clone()(Arc::new([input_outpoint.hash].into()))) .await - .expect("should return Ok without calling state service") + .expect("should succeed after calling state service") else { panic!("unexpected response variant from transaction verifier for Block request") }; - tokio::spawn(async move { - state - .expect_request(zebra_state::Request::AwaitUtxo(input_outpoint)) - .await - .expect("verifier should call mock state service with correct request") - .respond(zebra_state::Response::Utxo(utxo)); - }); - let crate::transaction::Response::Block { .. } = verifier .clone() .oneshot(make_request.clone()(Arc::new(HashSet::new()))) @@ -897,13 +895,12 @@ async fn skips_verification_of_block_transactions_in_mempool() { }; tokio::time::sleep(POLL_MEMPOOL_DELAY * 2).await; - // polled before AwaitOutput request, after a mempool transaction with transparent outputs, - // is successfully verified, and twice more when checking if a transaction in a block is - // already the mempool. + // polled before AwaitOutput request and after a mempool transaction with transparent outputs + // is successfully verified. assert_eq!( mempool.poll_count(), - 4, - "the mempool service should have been polled 4 times" + 2, + "the mempool service should have been polled twice" ); } @@ -1252,30 +1249,40 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() { #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] #[test] fn v6_coinbase_transaction_with_enable_zsa_flag_fails_validation() { - let network = Network::new_regtest( - ConfiguredActivationHeights { - canopy: Some(1), - nu7: Some(1), - ..Default::default() - } - .into(), - ); + use zebra_chain::transaction::arbitrary::insert_fake_v6_orchard_shielded_data; + use zebra_test::vectors::{ + OrchardWorkflowBlock, OrchardWorkflowBlockResult, ORCHARD_ZSA_WORKFLOW_BLOCKS, + }; - let outputs = vec![(Amount::zero(), transparent::Script::new(Default::default()))]; + let block = ORCHARD_ZSA_WORKFLOW_BLOCKS + .iter() + .find_map( + |OrchardWorkflowBlock { + height: _, + bytes, + expected_result, + }| { + matches!(expected_result, OrchardWorkflowBlockResult::Valid).then(|| { + Block::zcash_deserialize(&bytes[..]).expect("block should deserialize") + }) + }, + ) + .expect("workflow must contain a valid block"); - let mut tx = Transaction::new_v6_coinbase( - &network, - Height(1), - outputs, - Vec::new(), - Some(Amount::zero()), - ); + let mut tx = block + .transactions + .first() + .expect("block must contain a coinbase transaction") + .as_ref() + .clone(); + + assert!(tx.is_coinbase()); let shielded_data = insert_fake_v6_orchard_shielded_data(&mut tx); assert!(!shielded_data.flags.contains(Flags::ENABLE_ZSA)); - shielded_data.flags = Flags::ENABLE_ZSA; + shielded_data.flags.insert(Flags::ENABLE_ZSA); assert_eq!( check::coinbase_tx_no_prevout_joinsplit_spend(&tx), @@ -2287,7 +2294,7 @@ async fn v5_transaction_with_exceeding_expiry_height() { expiry_height, sapling_shielded_data: None, orchard_shielded_data: None, - network_upgrade: NetworkUpgrade::Nu6_1, + network_upgrade: NetworkUpgrade::Nu6_2, }; let transaction_hash = transaction.hash(); @@ -2895,6 +2902,340 @@ async fn v5_with_duplicate_orchard_action() { } } +/// Checks the activation boundary of the temporary Orchard-disabling soft fork: +/// it is inactive below the configured height and active at and above it, can be +/// disabled entirely, and Mainnet uses its fixed activation height. +#[test] +fn orchard_disabling_soft_fork_activation_boundary() { + let _init_guard = zebra_test::init(); + + let soft_fork_height = Height(2_000_000); + + // A Testnet with the soft fork configured to activate at `soft_fork_height`. + let network = Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height(soft_fork_height) + .to_network() + .expect("failed to build configured network"); + + assert!( + !network.temporary_orchard_disabling_soft_fork_active(Height(1_999_999)), + "soft fork must be inactive below the configured height", + ); + assert!( + network.temporary_orchard_disabling_soft_fork_active(soft_fork_height), + "soft fork must be active at the configured height", + ); + assert!( + network.temporary_orchard_disabling_soft_fork_active(Height(2_000_001)), + "soft fork must be active above the configured height", + ); + + // A Testnet with the soft fork disabled is never active. + let disabled = Parameters::build() + .disable_temporary_orchard_disabling_soft_fork() + .to_network() + .expect("failed to build configured network"); + + assert!( + !disabled.temporary_orchard_disabling_soft_fork_active(Height(4_042_000)), + "a disabled soft fork must never be active", + ); + + // Mainnet uses a fixed activation height (3_363_426). + assert!( + !Network::Mainnet.temporary_orchard_disabling_soft_fork_active(Height(3_363_425)), + "Mainnet soft fork must be inactive below its fixed height", + ); + assert!( + Network::Mainnet.temporary_orchard_disabling_soft_fork_active(Height(3_363_426)), + "Mainnet soft fork must be active at its fixed height", + ); +} + +/// The temporary Orchard-disabling soft fork must reject transactions that +/// contain Orchard actions once it is active, in both block and mempool +/// verification contexts. +#[tokio::test] +async fn orchard_disabling_soft_fork_rejects_orchard_actions_in_blocks_and_mempool() { + let _init_guard = zebra_test::init(); + + // Find a V5 transaction whose only shielded data is Orchard, so it both + // contains Orchard actions and can pass `has_inputs_and_outputs` once the + // Orchard flags are set below. + let default_testnet = Network::new_default_testnet(); + let mut tx = v5_transactions(default_testnet.block_iter()) + .rev() + .find(|transaction| { + transaction.inputs().is_empty() + && transaction.outputs().is_empty() + && transaction.sapling_spends_per_anchor().next().is_none() + && transaction.sapling_outputs().next().is_none() + && transaction.joinsplit_count() == 0 + }) + .expect("V5 tx with only Orchard actions"); + + // Enable spends and outputs so the transaction passes `has_inputs_and_outputs` + // and `has_enough_orchard_flags`, reaching the soft-fork check. + *tx.orchard_flags_mut() + .expect("tx without transparent, Sprout, or Sapling data must have Orchard actions") = + Flags::ENABLE_SPENDS | Flags::ENABLE_OUTPUTS; + + // Verify at the transaction's own expiry height, where its NU5 consensus + // branch id is valid on the default Testnet activation schedule. + let height = tx.expiry_height().expect("V5 tx has an expiry height"); + + // Configure a Testnet identical to the default public Testnet except that the + // Orchard-disabling soft fork activates at `height`, so it is active for this + // transaction. + let network = Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height(height) + .to_network() + .expect("failed to build configured network"); + + assert!( + network.temporary_orchard_disabling_soft_fork_active(height), + "soft fork must be active at the transaction's height", + ); + + let expected = Err(TransactionError::Other( + "transaction has Orchard actions (temporarily disabled)".into(), + )); + + // The soft-fork check runs before any state-service query, so the state + // service must never be called. + let block_response = Verifier::new_for_tests( + &network, + service_fn(|_| async { unreachable!("state service should not be called") }), + ) + .oneshot(Request::Block { + transaction_hash: tx.hash(), + transaction: Arc::new(tx.clone()), + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height, + time: DateTime::::MAX_UTC, + }) + .await; + + assert_eq!( + block_response, expected, + "block verification must reject a transaction with Orchard actions after the soft fork", + ); + + let mempool_response = Verifier::new_for_tests( + &network, + service_fn(|_| async { unreachable!("state service should not be called") }), + ) + .oneshot(Request::Mempool { + transaction: tx.into(), + height, + }) + .await; + + assert_eq!( + mempool_response, expected, + "mempool verification must reject a transaction with Orchard actions after the soft fork", + ); +} + +/// Negative control mirroring the zcashd test: a transaction without Orchard +/// actions is unaffected by the soft fork and is still accepted while it is +/// active. +#[tokio::test] +async fn orchard_disabling_soft_fork_accepts_non_orchard_transactions() { + let _init_guard = zebra_test::init(); + + // A Testnet with the Orchard-disabling soft fork active from height 1. + let network = Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height(Height(1)) + .to_network() + .expect("failed to build configured network"); + + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + + let canopy_activation_height = NetworkUpgrade::Canopy + .activation_height(&network) + .expect("Canopy activation height is specified"); + + let transaction_block_height = + (canopy_activation_height + 10).expect("transaction block height is too large"); + let fake_source_fund_height = + (transaction_block_height - 1).expect("fake source fund block height is too small"); + + assert!( + network.temporary_orchard_disabling_soft_fork_active(transaction_block_height), + "soft fork must be active at the transaction's height", + ); + + // A transparent transfer has no Orchard actions, so the soft fork must not + // affect it. The input must exceed the output by enough to pay the ZIP-317 + // conventional fee, so the transaction is otherwise valid. + let (input, output, known_utxos) = mock_transparent_transfer( + fake_source_fund_height, + true, + 0, + Amount::try_from(10001).expect("valid amount"), + ); + + let transaction = Transaction::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::Height(block::Height(0)), + expiry_height: (transaction_block_height + 1).expect("expiry height is too large"), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let input_outpoint = match transaction.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + let verifier = Verifier::new_for_tests(&network, state.clone()); + + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::UnspentBestChainUtxo( + known_utxos + .get(&input_outpoint) + .map(|utxo| utxo.utxo.clone()), + )); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let response = verifier + .oneshot(Request::Mempool { + transaction: transaction.into(), + height: transaction_block_height, + }) + .await; + + assert!( + response.is_ok(), + "non-Orchard transaction must be accepted while the soft fork is active, got: {response:?}", + ); +} + +/// Mirrors the zcashd boundary test: the soft fork must accept an Orchard +/// transaction one block below its activation height but reject the same +/// transaction at the activation height. +#[tokio::test] +async fn orchard_disabling_soft_fork_accepts_orchard_actions_below_activation_height() { + let _init_guard = zebra_test::init(); + + // Use an unmodified Orchard-only V5 transaction from the test vectors so its + // proofs remain valid for the acceptance path. + let default_testnet = Network::new_default_testnet(); + let tx = v5_transactions(default_testnet.block_iter()) + .rev() + .find(|transaction| { + transaction.inputs().is_empty() + && transaction.outputs().is_empty() + && transaction.sapling_spends_per_anchor().next().is_none() + && transaction.sapling_outputs().next().is_none() + && transaction.joinsplit_count() == 0 + }) + .expect("V5 tx with only Orchard actions"); + + assert!( + tx.has_orchard_shielded_data(), + "test transaction must contain Orchard actions", + ); + + let height = tx.expiry_height().expect("V5 tx has an expiry height"); + + // The soft fork activates one block above the transaction's height, so it is + // inactive for this transaction and verification proceeds normally. + let accepting_network = Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height( + (height + 1).expect("height is too large"), + ) + .to_network() + .expect("failed to build configured network"); + + assert!( + !accepting_network.temporary_orchard_disabling_soft_fork_active(height), + "soft fork must be inactive below its activation height", + ); + + // The only state request for an Orchard-only transaction verified as part of + // a block is the nullifier and anchor check. + let mut state: MockService = + MockService::build().for_prop_tests(); + let accept_verifier = Verifier::new_for_tests(&accepting_network, state.clone()); + + tokio::spawn(async move { + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let accept_response = accept_verifier + .oneshot(Request::Block { + transaction_hash: tx.hash(), + transaction: Arc::new(tx.clone()), + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height, + time: DateTime::::MAX_UTC, + }) + .await; + + assert!( + accept_response.is_ok(), + "Orchard transaction must be accepted below the soft fork height, got: {accept_response:?}", + ); + + // At the activation height the same transaction is rejected. The soft-fork + // check runs before any state query, so the state service is never called. + let rejecting_network = Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height(height) + .to_network() + .expect("failed to build configured network"); + + let reject_response = Verifier::new_for_tests( + &rejecting_network, + service_fn(|_| async { unreachable!("state service should not be called") }), + ) + .oneshot(Request::Block { + transaction_hash: tx.hash(), + transaction: Arc::new(tx), + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height, + time: DateTime::::MAX_UTC, + }) + .await; + + assert_eq!( + reject_response, + Err(TransactionError::Other( + "transaction has Orchard actions (temporarily disabled)".into() + )), + "Orchard transaction must be rejected at the soft fork height", + ); +} + /// Checks that the tx verifier handles consensus branch ids in V5 txs correctly. #[tokio::test] async fn v5_consensus_branch_ids() { @@ -3151,7 +3492,7 @@ fn mock_coinbase_transparent_output( let input = transparent::Input::Coinbase { height: coinbase_height, - data: CoinbaseData::new(Vec::new()), + data: vec![], sequence: u32::MAX, }; @@ -3696,3 +4037,219 @@ async fn mempool_zip317_ok() { "expected successful verification, got: {verifier_response:?}" ); } + +/// Test for CVE-2026-34377 https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-3vmh-33xr-9cqh +/// +/// Ensure a block with a transaction with garbage Orchard proofs is rejected, even if the mempool has a valid version of the same transaction. +#[tokio::test(flavor = "multi_thread")] +async fn block_with_garbage_orchard_proofs_is_rejected() { + use zebra_chain::{primitives::Halo2Proof, transaction::VerifiedUnminedTx}; + + let _init_guard = zebra_test::init(); + + let mempool: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let (mempool_setup_tx, mempool_setup_rx) = tokio::sync::oneshot::channel(); + let verifier = Verifier::new(&Network::Mainnet, state.clone(), mempool_setup_rx); + let verifier = Buffer::new(verifier, 1); + + mempool_setup_tx + .send(mempool.clone()) + .ok() + .expect("send should succeed"); + + let height = NetworkUpgrade::Nu6 + .activation_height(&Network::Mainnet) + .expect("Nu6 activation height is specified"); + let fund_height = (height - 1).expect("too small"); + let (input, output, known_utxos) = mock_transparent_transfer( + fund_height, + true, + 0, + Amount::try_from(10001).expect("invalid value"), + ); + + let mut tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu6, + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: height, + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + insert_fake_v5_orchard_shielded_data(&mut tx); + + let tx_hash = tx.hash(); + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("not coinbase"), + }; + + // corrupt only auth data, txid stays the same (ZIP-244) + let mut garbage_tx = tx.clone(); + let od = garbage_tx.v5_orchard_shielded_data_mut().unwrap(); + od.proof = Halo2Proof(vec![0xDE, 0xAD, 0xBE, 0xEF]); + od.binding_sig = [0xFF; 64].into(); + for action in od.actions.iter_mut() { + action.spend_auth_sig = [0xFF; 64].into(); + } + assert_eq!(tx.hash(), garbage_tx.hash()); + + // simulate valid version in mempool + let spent_output = known_utxos + .get(&input_outpoint) + .unwrap() + .utxo + .output + .clone(); + let verified_tx = VerifiedUnminedTx::new( + tx.clone().into(), + Amount::try_from(10000).unwrap(), + 0, + 0, + Arc::new(vec![spent_output]), + SigHash([0; 32]), + ) + .unwrap(); + + let mut mc = mempool.clone(); + tokio::spawn(async move { + mc.expect_request(mempool::Request::TransactionWithDepsByMinedId(tx_hash)) + .await + .unwrap() + .respond(mempool::Response::TransactionWithDeps { + transaction: verified_tx, + dependencies: [input_outpoint.hash].into(), + }); + }); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + + // submit garbage version as block tx, must be rejected + let resp = verifier + .clone() + .oneshot(Request::Block { + transaction_hash: tx_hash, + transaction: Arc::new(garbage_tx), + known_outpoint_hashes: Arc::new([input_outpoint.hash].into()), + known_utxos: Arc::new(HashMap::new()), + height, + time: Utc::now(), + }) + .await; + + assert!(resp.is_err(), "garbage proof must be rejected"); +} + +/// Regression test for the mempool-cache expiry bypass vulnerability. +/// +/// A non-coinbase transaction with `nExpiryHeight = H+1` that was cached in the +/// mempool as valid at height `H+1` can be presented inside a block at height +/// `H+2`. The block transaction verifier must re-run the expiry check even +/// when it hits the mempool cache fast path in `find_verified_unmined_tx`; +/// skipping that check lets Zebra accept a block that honest nodes reject, +/// causing a consensus split. +/// +/// # Attack window +/// +/// The attack is possible because: +/// * The mempool is active while Zebra is "close to tip" (not only at exact tip). +/// * The download/verification pipeline accepts blocks up to +/// `tip + full_verify_concurrency_limit` ahead of the current tip. +/// * `find_verified_unmined_tx` returns the cached result before the normal +/// expiry validation. +/// +/// Concretely: while Zebra's best tip is still `H`, the mempool can already +/// hold a `VerifiedUnminedTx` for a transaction with `nExpiryHeight = H+1`. +/// If the verifier is simultaneously asked to semantically verify a candidate +/// block at `H+2` that contains the same transaction, the cache hit fires and +/// the block passes semantic verification with an expired transaction inside. +#[tokio::test(flavor = "multi_thread")] +async fn mempool_cached_result_bypasses_expiry_check_for_block_at_next_height() { + let _init_guard = zebra_test::init(); + + let network = Network::Mainnet; + + // Heights used in the scenario: + // H = canopy_height (local best tip while the attack occurs) + // H+1 = mempool_height (nExpiryHeight; tx is valid for mempool admission here) + // H+2 = expired_block_height (block height at which the tx has expired) + let canopy_height = NetworkUpgrade::Canopy + .activation_height(&network) + .expect("Canopy activation height is specified"); + let mempool_height = (canopy_height + 1).expect("mempool height should be valid"); + let expired_block_height = (canopy_height + 2).expect("expired block height should be valid"); + let fund_height = (canopy_height - 1).expect("fund height should be valid"); + + let (input, output, _known_utxos) = mock_transparent_transfer( + fund_height, + true, + 0, + Amount::try_from(10001).expect("valid value"), + ); + + // V4 transaction with nExpiryHeight = mempool_height (H+1). + // Valid in block H+1 (block_height == expiry_height) but expired in H+2 + // (block_height > expiry_height). LockTime::unlocked() avoids a + // BestChainNextMedianTimePast state query, keeping the test simpler. + let tx = Transaction::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::unlocked(), + expiry_height: mempool_height, + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let tx_hash = tx.hash(); + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + let mempool: MockService<_, _, _, _> = MockService::build().for_unit_tests(); + let state: MockService<_, _, _, _> = MockService::build().for_unit_tests(); + let (mempool_setup_tx, mempool_setup_rx) = tokio::sync::oneshot::channel(); + let verifier = Verifier::new(&network, state.clone(), mempool_setup_rx); + let verifier = Buffer::new(verifier, 1); + + mempool_setup_tx + .send(mempool.clone()) + .ok() + .expect("send should succeed"); + + // Submit the same transaction as a Block request at expired_block_height + // (H+2). The known_outpoint_hashes set satisfies the dependency check + // inside find_verified_unmined_tx so the cache hit fires immediately. + // + // The verifier must return Err(TransactionError::ExpiredTransaction) + // because H+2 > nExpiryHeight. + let result = timeout( + test_timeout(), + verifier.clone().oneshot(Request::Block { + transaction_hash: tx_hash, + transaction: Arc::new(tx.clone()), + known_outpoint_hashes: Arc::new([input_outpoint.hash].into()), + known_utxos: Arc::new(HashMap::new()), + height: expired_block_height, + time: Utc::now(), + }), + ) + .await + .expect("block request should not time out"); + + // Buffer boxes the service error, so downcast to check the specific variant. + let err = result.expect_err( + "expected block verification to fail for a transaction with \ + expired nExpiryHeight mined via the mempool cache path", + ); + let tx_err = err + .downcast::() + .expect("error should downcast to TransactionError"); + assert!( + matches!(*tx_err, TransactionError::ExpiredTransaction { .. }), + "expected ExpiredTransaction error for block at height {expired_block_height:?} \ + with nExpiryHeight {mempool_height:?} via mempool cache; \ + got: {tx_err:?}" + ); +} diff --git a/zebra-consensus/src/transaction/tests/prop.rs b/zebra-consensus/src/transaction/tests/prop.rs index 09633682b8c..7e78a101ffe 100644 --- a/zebra-consensus/src/transaction/tests/prop.rs +++ b/zebra-consensus/src/transaction/tests/prop.rs @@ -362,8 +362,15 @@ fn sanitize_transaction_version( BeforeOverwinter => 2, Overwinter => 3, Sapling | Blossom | Heartwood | Canopy => 4, - // FIXME: Use 6 for Nu7 - Nu5 | Nu6 | Nu6_1 | Nu7 => 5, + Nu5 | Nu6 | Nu6_1 | Nu6_2 => 5, + + // Without ZSA V6 support, NU7 transactions are limited to V5. + #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] + Nu7 => 5, + + // With ZSA V6 support, NU7 also permits V6 transactions. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Nu7 => 6, #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => u8::MAX, diff --git a/zebra-network/CHANGELOG.md b/zebra-network/CHANGELOG.md index cfd4ee1dc14..8e6e4f915d4 100644 --- a/zebra-network/CHANGELOG.md +++ b/zebra-network/CHANGELOG.md @@ -5,7 +5,110 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [9.0.0] - 2026-06-10 + +### Breaking Changes + +- `INITIAL_MIN_NETWORK_PROTOCOL_VERSION` bumped from `Nu6` (170120) to `Nu6_2` (170150) + on Mainnet, Testnet, and Regtest. Peers running protocol version 170120 are no longer + accepted ([#10692](https://github.com/ZcashFoundation/zebra/pull/10692)). +- Removed `Copy` derive from `types::MetaAddr` (now only `Clone`) to support `String` fields. +- Changed `types::MetaAddr::new_connected()` to take additional `user_agent` and + `negotiated_version` parameters. + +### Added + +- `MetaAddr::user_agent()` accessor returning `Option<&str>`. +- `MetaAddr::negotiated_version()` accessor returning `Option`. +- `MetaAddr::last_connection_state()` accessor returning `PeerAddrState`. +- `MetaAddr::services()` accessor returning `Option`. +- `Display` impl for `PeerAddrState`. +- Made `types::Version` type public with `Display` impl. + +### Fixed + +- Fixed genesis-to-tip sync stall where the peer crawler could stop receiving + ready peers after an extended crawl period + ([#5709](https://github.com/ZcashFoundation/zebra/issues/5709)). + +## [8.0.0] - 2026-06-02 + +### Changed + +- Bump `CURRENT_NETWORK_PROTOCOL_VERSION` to 170150. + +## [7.0.0] - 2026-05-28 + +This release fixes three network security issues: + +- Cap pre-handshake message body length in `Codec` to `MAX_HANDSHAKE_BODY_LEN` + (1 KB); the limit is raised to `MAX_PROTOCOL_MESSAGE_LEN` after the + handshake completes + ([GHSA-h72h-ppcx-998p](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-h72h-ppcx-998p)). +- Tag transaction-advertisement requests with the announcing peer so the + mempool can enforce a per-peer queue cap + ([GHSA-4fc2-h7jh-287c](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-4fc2-h7jh-287c)). +- Canonicalize IPv4-mapped addresses on the misbehavior path so a peer cannot + evade scoring by alternating between `IPv4` and `IPv4-mapped-IPv6` forms of + the same address + ([GHSA-63wg-wjjj-7cp8](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-63wg-wjjj-7cp8)). + +The impact of these issues for crate users will depend on the particular +usage; if you use it as a building block for a consensus node, you should +update. + +### Added + +- `MetaAddr::new_misbehavior(addr: PeerSocketAddr, score_increment: u32) -> MetaAddrChange`, + which canonicalizes IPv4-mapped addresses before scoring. +- `Codec::reconfigure_full_body_len(&mut self)`, raising the codec's body + limit from the pre-handshake cap (`MAX_HANDSHAKE_BODY_LEN = 1024`) to + `MAX_PROTOCOL_MESSAGE_LEN` after handshake completion. + +### Changed + +- `Request::AdvertiseTransactionIds` is now a 2-tuple variant: + `AdvertiseTransactionIds(HashSet, Option)`. + The new second field carries the announcing peer for per-peer queue caps. + Affects `Display`, `Request::command`, and all pattern matches. +- `Codec` default builder now starts with `max_len = MAX_HANDSHAKE_BODY_LEN`; + pre-handshake messages above 1 KB are rejected. +- Network config: `testnet_parameters` can now be supplied either via the + legacy `testnet_parameters` table or via an untagged `DNetwork` enum + (`network = "..."` plus inline params). Serialization emits the new form; + the legacy form remains deserializable + ([#10051](https://github.com/ZcashFoundation/zebra/pull/10051)). +- `zebra-chain` dependency bumped to `8.0.0`. + +### Fixed + +- `AddressBook` no longer panics on the ban path when + `max_connections_per_ip != 1`; the optional `most_recent_by_ip` cache is + now guarded instead of unwrapped + ([#10580](https://github.com/ZcashFoundation/zebra/issues/10580)). + +## [6.0.0] - 2026-05-01 + +This release adds defense in depth for inbound deserializers. The +`zebra-chain` 7.0 cohort enforces 160-entry cap in `read_headers` and +size-limits coinbase data and Equihash solutions before allocation +([GHSA-438q-jx8f-cccv](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-438q-jx8f-cccv)). + +### Changed + +- `Request::AdvertiseBlock` now carries a second tuple field + `Option` so the inbound service can attribute the announcing + peer when fanning out. + +## [5.0.1] - 2026-04-17 + +This release fixes an important security issue: + +- [CVE-2026-40881: addr/addrv2 Deserialization Resource Exhaustion](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-xr93-pcq3-pxf8) + +The impact of the issue for crate users will depend on the particular usage; if +your application allows deserializing arbitrary `addr` and/or `addrv2` messages, +you should update. ## [5.0.0] - 2026-03-12 diff --git a/zebra-network/Cargo.toml b/zebra-network/Cargo.toml index 8f3affb558d..c3667f6d95e 100644 --- a/zebra-network/Cargo.toml +++ b/zebra-network/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-network" -version = "5.0.0" +version = "9.0.0" authors = ["Zcash Foundation ", "Tower Maintainers "] description = "Networking code for Zebra" # # Legal @@ -85,7 +85,7 @@ howudoin = { workspace = true, optional = true } proptest = { workspace = true, optional = true } proptest-derive = { workspace = true, optional = true } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = ["async-error"] } +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = ["async-error"] } [dev-dependencies] proptest = { workspace = true } diff --git a/zebra-network/src/address_book.rs b/zebra-network/src/address_book.rs index 41a392d1a84..a7d27e0dc66 100644 --- a/zebra-network/src/address_book.rs +++ b/zebra-network/src/address_book.rs @@ -158,7 +158,7 @@ impl AddressBook { // Avoid initiating outbound handshakes when max_connections_per_ip is 1. let should_limit_outbound_conns_per_ip = max_connections_per_ip == 1; let mut new_book = AddressBook { - by_addr: OrderedMap::new(|meta_addr| Reverse(*meta_addr)), + by_addr: OrderedMap::new(|meta_addr: &MetaAddr| Reverse(meta_addr.clone())), local_listener: canonical_socket_addr(local_listener), network: network.clone(), addr_limit: constants::MAX_ADDRS_IN_ADDRESS_BOOK, @@ -215,16 +215,16 @@ impl AddressBook { .map(|meta_addr| (meta_addr.addr, meta_addr)); for (socket_addr, meta_addr) in addrs { - // overwrite any duplicate addresses - new_book.by_addr.insert(socket_addr, meta_addr); // Add the address to `most_recent_by_ip` if it has responded - if new_book.should_update_most_recent_by_ip(meta_addr) { + if new_book.should_update_most_recent_by_ip(&meta_addr) { new_book .most_recent_by_ip .as_mut() .expect("should be some when should_update_most_recent_by_ip is true") - .insert(socket_addr.ip(), meta_addr); + .insert(socket_addr.ip(), meta_addr.clone()); } + // overwrite any duplicate addresses + new_book.by_addr.insert(socket_addr, meta_addr); // exit as soon as we get enough addresses if new_book.by_addr.len() >= addr_limit { break; @@ -347,8 +347,8 @@ impl AddressBook { // Unfortunately, `OrderedMap` doesn't implement `get`. let meta_addr = self.by_addr.remove(&addr); - if let Some(meta_addr) = meta_addr { - self.by_addr.insert(addr, meta_addr); + if let Some(ref meta_addr) = meta_addr { + self.by_addr.insert(addr, meta_addr.clone()); } meta_addr @@ -366,7 +366,7 @@ impl AddressBook { /// - this is the only field checked by `has_connection_recently_responded()` /// /// See [`AddressBook::is_ready_for_connection_attempt_with_ip`] for more details. - fn should_update_most_recent_by_ip(&self, updated: MetaAddr) -> bool { + fn should_update_most_recent_by_ip(&self, updated: &MetaAddr) -> bool { let Some(most_recent_by_ip) = self.most_recent_by_ip.as_ref() else { return false; }; @@ -429,7 +429,7 @@ impl AddressBook { let instant_now = Instant::now(); let chrono_now = Utc::now(); - let updated = change.apply_to_meta_addr(previous, instant_now, chrono_now); + let updated = change.apply_to_meta_addr(previous.clone(), instant_now, chrono_now); trace!( ?change, @@ -440,7 +440,7 @@ impl AddressBook { "calculated updated address book entry", ); - if let Some(updated) = updated { + if let Some(ref updated) = updated { if updated.misbehavior() >= constants::MAX_PEER_MISBEHAVIOR_SCORE { // Ban and skip outbound connections with excessively misbehaving peers. let banned_ip = updated.addr.ip(); @@ -452,10 +452,13 @@ impl AddressBook { bans_by_ip.shift_remove_index(0); } - self.most_recent_by_ip - .as_mut() - .expect("should be some when should_remove_most_recent_by_ip is true") - .remove(&banned_ip); + // `most_recent_by_ip` is only populated when + // `max_connections_per_ip == 1`. The ban path runs for any + // configured value, so we must guard the optional cache rather + // than unwrap it. + if let Some(most_recent_by_ip) = self.most_recent_by_ip.as_mut() { + most_recent_by_ip.remove(&banned_ip); + } let banned_addrs: Vec<_> = self .by_addr @@ -496,17 +499,17 @@ impl AddressBook { return None; } - self.by_addr.insert(updated.addr, updated); - // Add the address to `most_recent_by_ip` if it sent the most recent // response Zebra has received from this IP. if self.should_update_most_recent_by_ip(updated) { self.most_recent_by_ip .as_mut() .expect("should be some when should_update_most_recent_by_ip is true") - .insert(updated.addr.ip(), updated); + .insert(updated.addr.ip(), updated.clone()); } + self.by_addr.insert(updated.addr, updated.clone()); + debug!( ?change, ?updated, diff --git a/zebra-network/src/address_book/tests/prop.rs b/zebra-network/src/address_book/tests/prop.rs index e65b54045cd..d8294fc929b 100644 --- a/zebra-network/src/address_book/tests/prop.rs +++ b/zebra-network/src/address_book/tests/prop.rs @@ -119,7 +119,7 @@ proptest! { for (_addr, changes) in addr_changes_lists.iter() { for change in changes { - address_book.update(*change); + address_book.update(change.clone()); prop_assert!( address_book.len() <= addr_limit, @@ -143,7 +143,7 @@ proptest! { for index in 0..MAX_ADDR_CHANGE { for (_addr, changes) in addr_changes_lists.iter() { if let Some(change) = changes.get(index) { - address_book.update(*change); + address_book.update(change.clone()); prop_assert!( address_book.len() <= addr_limit, diff --git a/zebra-network/src/address_book/tests/vectors.rs b/zebra-network/src/address_book/tests/vectors.rs index b4a5ffc4024..4ee6568799d 100644 --- a/zebra-network/src/address_book/tests/vectors.rs +++ b/zebra-network/src/address_book/tests/vectors.rs @@ -11,8 +11,8 @@ use zebra_chain::{ }; use crate::{ - constants::{DEFAULT_MAX_CONNS_PER_IP, MAX_ADDRS_IN_ADDRESS_BOOK}, - meta_addr::MetaAddr, + constants::{DEFAULT_MAX_CONNS_PER_IP, MAX_ADDRS_IN_ADDRESS_BOOK, MAX_PEER_MISBEHAVIOR_SCORE}, + meta_addr::{MetaAddr, MetaAddrChange}, protocol::external::types::PeerServices, AddressBook, }; @@ -36,6 +36,74 @@ fn address_book_empty() { assert_eq!(address_book.len(), 0); } +/// Helper: build a `MetaAddrChange::NewGossiped` for a given address and +/// last-seen time. Used to seed the address book before triggering a ban so +/// the test exercises the by-IP cleanup loop on real entries. +fn gossiped_change( + addr: crate::PeerSocketAddr, + services: PeerServices, + untrusted_last_seen: DateTime32, +) -> MetaAddrChange { + MetaAddr::new_gossiped_meta_addr(addr, services, untrusted_last_seen) + .new_gossiped_change() + .expect("gossiped MetaAddr should produce a NewGossiped change") +} + +/// Regression test for https://github.com/ZcashFoundation/zebra/issues/10580. +/// +/// Applying a ban-threshold misbehavior update with +/// `max_connections_per_ip > 1` previously panicked because the ban branch +/// unconditionally unwrapped `most_recent_by_ip`, which is only populated when +/// `max_connections_per_ip == 1`. +#[test] +fn misbehavior_ban_does_not_panic_with_max_connections_per_ip_above_one() { + let banned_addr: crate::PeerSocketAddr = "127.0.0.1:8233".parse().unwrap(); + let other_port_same_ip: crate::PeerSocketAddr = "127.0.0.1:8234".parse().unwrap(); + let unrelated_addr: crate::PeerSocketAddr = "127.0.0.2:8233".parse().unwrap(); + + let mut address_book = + AddressBook::new("0.0.0.0:0".parse().unwrap(), &Mainnet, 2, Span::current()); + + // Seed two entries on the soon-to-be-banned IP plus an unrelated entry, + // so the ban path's `by_addr` cleanup loop has visible work to do. + address_book.update(gossiped_change( + banned_addr, + PeerServices::NODE_NETWORK, + DateTime32::MIN, + )); + address_book.update(gossiped_change( + other_port_same_ip, + PeerServices::NODE_NETWORK, + DateTime32::MIN.saturating_add(Duration32::from_seconds(1)), + )); + address_book.update(gossiped_change( + unrelated_addr, + PeerServices::NODE_NETWORK, + DateTime32::MIN.saturating_add(Duration32::from_seconds(2)), + )); + + assert!(address_book.get(banned_addr).is_some()); + assert!(address_book.get(other_port_same_ip).is_some()); + + address_book.update(MetaAddrChange::UpdateMisbehavior { + addr: banned_addr, + score_increment: MAX_PEER_MISBEHAVIOR_SCORE, + }); + + assert!( + address_book.bans().contains_key(&banned_addr.ip()), + "ban-threshold misbehavior should ban the peer IP" + ); + assert!( + address_book.get(banned_addr).is_none(), + "primary banned address should be removed from the address book" + ); + assert!( + address_book.get(unrelated_addr).is_some(), + "unrelated IP entries should remain after banning a different IP" + ); +} + /// Make sure peers are attempted in priority order. #[test] fn address_book_peer_order() { @@ -51,7 +119,7 @@ fn address_book_peer_order() { ); // Regardless of the order of insertion, the most recent address should be chosen first - let addrs = vec![meta_addr1, meta_addr2]; + let addrs = vec![meta_addr1.clone(), meta_addr2.clone()]; let address_book = AddressBook::new_with_addrs( "0.0.0.0:0".parse().unwrap(), &Mainnet, @@ -64,11 +132,11 @@ fn address_book_peer_order() { address_book .reconnection_peers(Instant::now(), Utc::now()) .next(), - Some(meta_addr2), + Some(meta_addr2.clone()), ); // Reverse the order, check that we get the same result - let addrs = vec![meta_addr2, meta_addr1]; + let addrs = vec![meta_addr2.clone(), meta_addr1.clone()]; let address_book = AddressBook::new_with_addrs( "0.0.0.0:0".parse().unwrap(), &Mainnet, @@ -81,14 +149,14 @@ fn address_book_peer_order() { address_book .reconnection_peers(Instant::now(), Utc::now()) .next(), - Some(meta_addr2), + Some(meta_addr2.clone()), ); // Now check that the order depends on the time, not the address meta_addr1.addr = addr2; meta_addr2.addr = addr1; - let addrs = vec![meta_addr1, meta_addr2]; + let addrs = vec![meta_addr1.clone(), meta_addr2.clone()]; let address_book = AddressBook::new_with_addrs( "0.0.0.0:0".parse().unwrap(), &Mainnet, @@ -101,11 +169,11 @@ fn address_book_peer_order() { address_book .reconnection_peers(Instant::now(), Utc::now()) .next(), - Some(meta_addr2), + Some(meta_addr2.clone()), ); // Reverse the order, check that we get the same result - let addrs = vec![meta_addr2, meta_addr1]; + let addrs = vec![meta_addr2.clone(), meta_addr1]; let address_book = AddressBook::new_with_addrs( "0.0.0.0:0".parse().unwrap(), &Mainnet, diff --git a/zebra-network/src/address_book_updater.rs b/zebra-network/src/address_book_updater.rs index b4e68c0ba2d..3a91b0220a0 100644 --- a/zebra-network/src/address_book_updater.rs +++ b/zebra-network/src/address_book_updater.rs @@ -56,7 +56,7 @@ impl AddressBookUpdater { ) { // Create an mpsc channel for peerset address book updates, // based on the maximum number of inbound and outbound peers. - let (worker_tx, mut worker_rx) = mpsc::channel(max( + let (worker_tx, mut worker_rx) = mpsc::channel::(max( config.peerset_total_connection_limit(), MIN_CHANNEL_SIZE, )); @@ -98,6 +98,7 @@ impl AddressBookUpdater { // // Briefly hold the address book threaded mutex, to update the // state for a single address. + let event_ip = event.addr().ip(); let updated = worker_address_book .lock() .expect("mutex should be unpoisoned") @@ -111,7 +112,7 @@ impl AddressBookUpdater { .expect("mutex should be unpoisoned") .bans(); - if bans.contains_key(&event.addr().ip()) { + if bans.contains_key(&event_ip) { let _ = bans_sender.send(bans); } } diff --git a/zebra-network/src/config.rs b/zebra-network/src/config.rs index ab8909d4444..2b63a301d52 100644 --- a/zebra-network/src/config.rs +++ b/zebra-network/src/config.rs @@ -607,6 +607,31 @@ struct DTestnetParameters { /// If `true`, automatically repeats configured funding stream addresses to fill /// all required periods. extend_funding_stream_addresses_as_required: Option, + /// Height at which the soft fork that temporarily disables Orchard actions activates. + /// + /// If unset, the default activation height for the network is used; the soft fork + /// cannot be disabled via configuration. + temporary_orchard_disabling_soft_fork_height: Option, +} + +/// Network configuration used during deserialization. +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum DNetwork { + DefaultForKind(NetworkKind), + ConfiguredRegtest { + params: Box, + + #[serde(default, skip_serializing)] + regtest: Option, + }, + ConfiguredTestnet(Box), +} + +impl Default for DNetwork { + fn default() -> Self { + DNetwork::DefaultForKind(NetworkKind::Mainnet) + } } #[derive(Serialize, Deserialize)] @@ -614,8 +639,12 @@ struct DTestnetParameters { struct DConfig { listen_addr: String, external_addr: Option, - network: NetworkKind, + network: DNetwork, + + /// Legacy testnet parameters, kept for backwards compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] testnet_parameters: Option, + initial_mainnet_peers: IndexSet, initial_testnet_peers: IndexSet, cache_dir: CacheDir, @@ -675,6 +704,9 @@ impl From> for DTestnetParameters { params.checkpoints().into() }, extend_funding_stream_addresses_as_required: None, + temporary_orchard_disabling_soft_fork_height: params + .temporary_orchard_disabling_soft_fork_height() + .map(|height| height.0), } } } @@ -693,16 +725,32 @@ impl From for DConfig { max_connections_per_ip, }: Config, ) -> Self { - let testnet_parameters = network - .parameters() - .filter(|params| !params.is_default_testnet()) - .map(Into::into); + let dnetwork = match network.kind() { + NetworkKind::Testnet => match network + .parameters() + .filter(|params| !params.is_default_testnet()) + .map(Into::into) + { + Some(params) => DNetwork::ConfiguredTestnet(Box::new(params)), + None => DNetwork::DefaultForKind(NetworkKind::Testnet), + }, + + NetworkKind::Regtest => match network.parameters().map(Into::into) { + Some(params) => DNetwork::ConfiguredRegtest { + params: Box::new(params), + regtest: Some(true), + }, + None => DNetwork::DefaultForKind(NetworkKind::Regtest), + }, + + other_kind => DNetwork::DefaultForKind(other_kind), + }; DConfig { listen_addr: listen_addr.to_string(), external_addr: external_addr.map(|addr| addr.to_string()), - network: network.into(), - testnet_parameters, + network: dnetwork, + testnet_parameters: None, initial_mainnet_peers, initial_testnet_peers, cache_dir, @@ -721,7 +769,7 @@ impl<'de> Deserialize<'de> for Config { let DConfig { listen_addr, external_addr, - network: network_kind, + network: dnetwork, testnet_parameters, initial_mainnet_peers, initial_testnet_peers, @@ -731,176 +779,25 @@ impl<'de> Deserialize<'de> for Config { max_connections_per_ip, } = DConfig::deserialize(deserializer)?; - /// Accepts an [`IndexSet`] of initial peers, - /// - /// Returns true if any of them are the default Testnet or Mainnet initial peers. - fn contains_default_initial_peers(initial_peers: &IndexSet) -> bool { - let Config { - initial_mainnet_peers: mut default_initial_peers, - initial_testnet_peers: default_initial_testnet_peers, - .. - } = Config::default(); - default_initial_peers.extend(default_initial_testnet_peers); - - initial_peers - .intersection(&default_initial_peers) - .next() - .is_some() - } - - let network = match (network_kind, testnet_parameters) { - (NetworkKind::Mainnet, _) => Network::Mainnet, - (NetworkKind::Testnet, None) => Network::new_default_testnet(), - (NetworkKind::Regtest, testnet_parameters) => { - let params = testnet_parameters - .map( - |DTestnetParameters { - activation_heights, - pre_nu6_funding_streams, - post_nu6_funding_streams, - funding_streams, - lockbox_disbursements, - checkpoints, - extend_funding_stream_addresses_as_required, - .. - }| { - let mut funding_streams_vec = funding_streams.unwrap_or_default(); - if let Some(funding_streams) = post_nu6_funding_streams { - funding_streams_vec.insert(0, funding_streams); - } - if let Some(funding_streams) = pre_nu6_funding_streams { - funding_streams_vec.insert(0, funding_streams); - } - - RegtestParameters { - activation_heights: activation_heights.unwrap_or_default(), - funding_streams: Some(funding_streams_vec), - lockbox_disbursements, - checkpoints: Some(checkpoints), - extend_funding_stream_addresses_as_required, - } - }, - ) - .unwrap_or_default(); - - Network::new_regtest(params) + let network = match (dnetwork, testnet_parameters) { + (DNetwork::ConfiguredTestnet(params), _) => { + build_configured_testnet::(*params, &initial_testnet_peers)? } - ( - NetworkKind::Testnet, - Some(DTestnetParameters { - network_name, - network_magic, - slow_start_interval, - target_difficulty_limit, - disable_pow, - genesis_hash, - activation_heights, - pre_nu6_funding_streams, - post_nu6_funding_streams, - funding_streams, - pre_blossom_halving_interval, - lockbox_disbursements, - checkpoints, - extend_funding_stream_addresses_as_required, - }), - ) => { - let mut params_builder = testnet::Parameters::build(); - - if let Some(network_name) = network_name.clone() { - params_builder = params_builder - .with_network_name(network_name) - .map_err(de::Error::custom)? - } - - if let Some(network_magic) = network_magic { - params_builder = params_builder - .with_network_magic(Magic(network_magic)) - .map_err(de::Error::custom)?; - } - - if let Some(genesis_hash) = genesis_hash { - params_builder = params_builder - .with_genesis_hash(genesis_hash) - .map_err(de::Error::custom)?; - } - - if let Some(slow_start_interval) = slow_start_interval { - params_builder = params_builder.with_slow_start_interval( - slow_start_interval.try_into().map_err(de::Error::custom)?, - ); - } - - if let Some(target_difficulty_limit) = target_difficulty_limit.clone() { - params_builder = params_builder - .with_target_difficulty_limit( - target_difficulty_limit - .parse::() - .map_err(de::Error::custom)?, - ) - .map_err(de::Error::custom)?; - } - - if let Some(disable_pow) = disable_pow { - params_builder = params_builder.with_disable_pow(disable_pow); - } - - // Retain default Testnet activation heights unless there's an empty [testnet_parameters.activation_heights] section. - if let Some(activation_heights) = activation_heights { - params_builder = params_builder - .with_activation_heights(activation_heights) - .map_err(de::Error::custom)? - } - - if let Some(halving_interval) = pre_blossom_halving_interval { - params_builder = params_builder - .with_halving_interval(halving_interval.into()) - .map_err(de::Error::custom)? - } - - // Set configured funding streams after setting any parameters that affect the funding stream address period. - let mut funding_streams_vec = funding_streams.unwrap_or_default(); - - if let Some(funding_streams) = post_nu6_funding_streams { - funding_streams_vec.insert(0, funding_streams); - } - - if let Some(funding_streams) = pre_nu6_funding_streams { - funding_streams_vec.insert(0, funding_streams); - } - - if !funding_streams_vec.is_empty() { - params_builder = params_builder.with_funding_streams(funding_streams_vec); - } - - if let Some(lockbox_disbursements) = lockbox_disbursements { - params_builder = - params_builder.with_lockbox_disbursements(lockbox_disbursements); - } - - params_builder = params_builder - .with_checkpoints(checkpoints) - .map_err(de::Error::custom)?; - - if let Some(true) = extend_funding_stream_addresses_as_required { - params_builder = params_builder.extend_funding_streams(); - } - - // Return an error if the initial testnet peers includes any of the default initial Mainnet or Testnet - // peers and the configured network parameters are incompatible with the default public Testnet. - if !params_builder.is_compatible_with_default_parameters() - && contains_default_initial_peers(&initial_testnet_peers) - { - return Err(de::Error::custom( - "cannot use default initials peers with incompatible testnet", - )); - }; - - // Return the default Testnet if no network name was configured and all parameters match the default Testnet - if network_name.is_none() && params_builder == testnet::Parameters::build() { - Network::new_default_testnet() - } else { - params_builder.to_network().map_err(de::Error::custom)? - } + (DNetwork::ConfiguredRegtest { params, .. }, _) => { + Network::new_regtest(build_regtest_params(*params)) + } + (DNetwork::DefaultForKind(NetworkKind::Mainnet), _) => Network::Mainnet, + (DNetwork::DefaultForKind(NetworkKind::Testnet), Some(params)) => { + build_configured_testnet::(params, &initial_testnet_peers)? + } + (DNetwork::DefaultForKind(NetworkKind::Testnet), None) => { + Network::new_default_testnet() + } + (DNetwork::DefaultForKind(NetworkKind::Regtest), Some(params)) => { + Network::new_regtest(build_regtest_params(params)) + } + (DNetwork::DefaultForKind(NetworkKind::Regtest), None) => { + Network::new_regtest(Default::default()) } }; @@ -958,3 +855,180 @@ impl<'de> Deserialize<'de> for Config { }) } } + +/// Accepts an [`IndexSet`] of initial peers, +/// +/// Returns true if any of them are the default Testnet or Mainnet initial peers. +fn contains_default_initial_peers(initial_peers: &IndexSet) -> bool { + let Config { + initial_mainnet_peers: mut default_initial_peers, + initial_testnet_peers: default_initial_testnet_peers, + .. + } = Config::default(); + default_initial_peers.extend(default_initial_testnet_peers); + + initial_peers + .intersection(&default_initial_peers) + .next() + .is_some() +} + +fn build_configured_testnet<'de, D>( + params: DTestnetParameters, + initial_testnet_peers: &IndexSet, +) -> Result +where + D: Deserializer<'de>, +{ + let DTestnetParameters { + network_name, + network_magic, + slow_start_interval, + target_difficulty_limit, + disable_pow, + genesis_hash, + activation_heights, + pre_nu6_funding_streams, + post_nu6_funding_streams, + funding_streams, + pre_blossom_halving_interval, + lockbox_disbursements, + checkpoints, + extend_funding_stream_addresses_as_required, + temporary_orchard_disabling_soft_fork_height, + } = params; + + let mut params_builder = testnet::Parameters::build(); + + if let Some(network_name) = network_name.clone() { + params_builder = params_builder + .with_network_name(network_name) + .map_err(de::Error::custom)? + } + + if let Some(network_magic) = network_magic { + params_builder = params_builder + .with_network_magic(Magic(network_magic)) + .map_err(de::Error::custom)?; + } + + if let Some(genesis_hash) = genesis_hash { + params_builder = params_builder + .with_genesis_hash(genesis_hash) + .map_err(de::Error::custom)?; + } + + if let Some(slow_start_interval) = slow_start_interval { + params_builder = params_builder + .with_slow_start_interval(slow_start_interval.try_into().map_err(de::Error::custom)?); + } + + if let Some(target_difficulty_limit) = target_difficulty_limit.clone() { + params_builder = params_builder + .with_target_difficulty_limit( + target_difficulty_limit + .parse::() + .map_err(de::Error::custom)?, + ) + .map_err(de::Error::custom)?; + } + + if let Some(disable_pow) = disable_pow { + params_builder = params_builder.with_disable_pow(disable_pow); + } + + // Retain default Testnet activation heights unless there's an empty [testnet_parameters.activation_heights] section. + if let Some(activation_heights) = activation_heights { + params_builder = params_builder + .with_activation_heights(activation_heights) + .map_err(de::Error::custom)? + } + + if let Some(halving_interval) = pre_blossom_halving_interval { + params_builder = params_builder + .with_halving_interval(halving_interval.into()) + .map_err(de::Error::custom)? + } + + // Set configured funding streams after setting any parameters that affect the funding stream address period. + let mut funding_streams_vec = funding_streams.unwrap_or_default(); + + if let Some(funding_streams) = post_nu6_funding_streams { + funding_streams_vec.insert(0, funding_streams); + } + + if let Some(funding_streams) = pre_nu6_funding_streams { + funding_streams_vec.insert(0, funding_streams); + } + + if !funding_streams_vec.is_empty() { + params_builder = params_builder.with_funding_streams(funding_streams_vec); + } + + if let Some(lockbox_disbursements) = lockbox_disbursements { + params_builder = params_builder.with_lockbox_disbursements(lockbox_disbursements); + } + + params_builder = params_builder + .with_checkpoints(checkpoints) + .map_err(de::Error::custom)?; + + if let Some(true) = extend_funding_stream_addresses_as_required { + params_builder = params_builder.extend_funding_streams(); + } + + // Retain the default soft-fork activation height unless one is configured. + if let Some(height) = temporary_orchard_disabling_soft_fork_height { + params_builder = params_builder.with_temporary_orchard_disabling_soft_fork_height( + height.try_into().map_err(de::Error::custom)?, + ); + } + + // Return an error if the initial testnet peers includes any of the default initial Mainnet or Testnet + // peers and the configured network parameters are incompatible with the default public Testnet. + if !params_builder.is_compatible_with_default_parameters() + && contains_default_initial_peers(initial_testnet_peers) + { + return Err(de::Error::custom( + "cannot use default initials peers with incompatible testnet", + )); + }; + + // Return the default Testnet if no network name was configured and all parameters match the default Testnet + if network_name.is_none() && params_builder == testnet::Parameters::build() { + Ok(Network::new_default_testnet()) + } else { + Ok(params_builder.to_network().map_err(de::Error::custom)?) + } +} + +fn build_regtest_params(params: DTestnetParameters) -> RegtestParameters { + let DTestnetParameters { + activation_heights, + pre_nu6_funding_streams, + post_nu6_funding_streams, + funding_streams, + lockbox_disbursements, + checkpoints, + extend_funding_stream_addresses_as_required, + .. + } = params; + + let mut funding_streams_vec = funding_streams.unwrap_or_default(); + + if let Some(funding_streams) = post_nu6_funding_streams { + funding_streams_vec.insert(0, funding_streams); + } + + if let Some(funding_streams) = pre_nu6_funding_streams { + funding_streams_vec.insert(0, funding_streams); + } + + RegtestParameters { + activation_heights: activation_heights.unwrap_or_default(), + funding_streams: Some(funding_streams_vec), + lockbox_disbursements, + checkpoints: Some(checkpoints), + extend_funding_stream_addresses_as_required, + } +} diff --git a/zebra-network/src/config/tests/vectors.rs b/zebra-network/src/config/tests/vectors.rs index 8207857c218..ef3152b0cc4 100644 --- a/zebra-network/src/config/tests/vectors.rs +++ b/zebra-network/src/config/tests/vectors.rs @@ -1,7 +1,13 @@ //! Fixed test vectors for zebra-network configuration. use static_assertions::const_assert; -use zebra_chain::parameters::testnet::{self, ConfiguredFundingStreams}; +use zebra_chain::{ + block::Height, + parameters::{ + testnet::{self, ConfiguredFundingStreams}, + Network, + }, +}; use crate::{ constants::{INBOUND_PEER_LIMIT_MULTIPLIER, OUTBOUND_PEER_LIMIT_MULTIPLIER}, @@ -109,3 +115,35 @@ fn funding_streams_serialization_roundtrip() { assert_eq!(config, deserialized); } + +/// Checks that a configured Testnet's temporary Orchard-disabling soft fork height +/// survives a serialization round-trip. +#[test] +fn temporary_orchard_disabling_soft_fork_height_serialization_roundtrip() { + let _init_guard = zebra_test::init(); + + let soft_fork_height = Height(2_000_000); + + let config = Config { + network: testnet::Parameters::build() + .with_temporary_orchard_disabling_soft_fork_height(soft_fork_height) + .to_network() + .expect("failed to build configured network"), + initial_testnet_peers: [].into(), + ..Config::default() + }; + + let serialized = toml::to_string(&config).unwrap(); + let deserialized: Config = toml::from_str(&serialized).unwrap(); + + assert_eq!(config, deserialized); + + // The configured height must be preserved through the round-trip. + let Network::Testnet(params) = &deserialized.network else { + panic!("deserialized network must be a Testnet"); + }; + assert_eq!( + params.temporary_orchard_disabling_soft_fork_height(), + Some(soft_fork_height), + ); +} diff --git a/zebra-network/src/constants.rs b/zebra-network/src/constants.rs index 10ada14d155..990a22f16ec 100644 --- a/zebra-network/src/constants.rs +++ b/zebra-network/src/constants.rs @@ -341,9 +341,9 @@ pub const TIMESTAMP_TRUNCATION_SECONDS: u32 = 30 * 60; /// This version of Zebra draws the current network protocol version from /// [ZIP-255](https://zips.z.cash/zip-0255). // TODO: Update this constant to the correct value after NU7 activation (see NU deployment ZIPs), -pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_140); -// pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_150); // NU7 Testnet. -// pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_160); // NU7 Mainnet. +pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_150); // NU6.2 (Mainnet + Testnet). + // pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_160); // NU7 Testnet. + // pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_170); // NU7 Mainnet. /// The default RTT estimate for peer responses. /// @@ -406,14 +406,14 @@ lazy_static! { /// /// The minimum network protocol version typically changes after Mainnet and /// Testnet network upgrades. - // TODO: Change `Nu6` to `Nu7` after NU7 activation. + // TODO: Change `Nu6_2` to `Nu7` after NU7 activation. // TODO: Move the value here to a field on `testnet::Parameters` (#8367) pub static ref INITIAL_MIN_NETWORK_PROTOCOL_VERSION: HashMap = { let mut hash_map = HashMap::new(); - hash_map.insert(NetworkKind::Mainnet, Version::min_specified_for_upgrade(&Mainnet, Nu6)); - hash_map.insert(NetworkKind::Testnet, Version::min_specified_for_upgrade(&Network::new_default_testnet(), Nu6)); - hash_map.insert(NetworkKind::Regtest, Version::min_specified_for_upgrade(&Network::new_regtest(Default::default()), Nu6)); + hash_map.insert(NetworkKind::Mainnet, Version::min_specified_for_upgrade(&Mainnet, Nu6_2)); + hash_map.insert(NetworkKind::Testnet, Version::min_specified_for_upgrade(&Network::new_default_testnet(), Nu6_2)); + hash_map.insert(NetworkKind::Regtest, Version::min_specified_for_upgrade(&Network::new_regtest(Default::default()), Nu6_2)); hash_map }; diff --git a/zebra-network/src/lib.rs b/zebra-network/src/lib.rs index 32b87fcdb52..4da458462ad 100644 --- a/zebra-network/src/lib.rs +++ b/zebra-network/src/lib.rs @@ -200,7 +200,7 @@ pub mod types { pub use crate::{ meta_addr::MetaAddr, protocol::{ - external::{AddrInVersion, Nonce}, + external::{types::Version, AddrInVersion, Nonce}, types::PeerServices, }, }; diff --git a/zebra-network/src/meta_addr.rs b/zebra-network/src/meta_addr.rs index 0470eac22f2..deeffb8ed61 100644 --- a/zebra-network/src/meta_addr.rs +++ b/zebra-network/src/meta_addr.rs @@ -12,7 +12,10 @@ use zebra_chain::{parameters::Network, serialization::DateTime32}; use crate::{ constants, peer::{address_is_valid_for_outbound_connections, PeerPreference}, - protocol::{external::canonical_peer_addr, types::PeerServices}, + protocol::{ + external::{canonical_peer_addr, types::Version}, + types::PeerServices, + }, }; use MetaAddrChange::*; @@ -66,6 +69,17 @@ pub enum PeerAddrState { AttemptPending, } +impl std::fmt::Display for PeerAddrState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Responded => write!(f, "connected"), + NeverAttemptedGossiped => write!(f, "never_connected"), + Failed => write!(f, "failed"), + AttemptPending => write!(f, "connecting"), + } + } +} + impl PeerAddrState { /// Return true if this state is a "never attempted" state. pub fn is_never_attempted(&self) -> bool { @@ -146,7 +160,7 @@ impl PartialOrd for PeerAddrState { /// This struct can be created from `addr` or `addrv2` messages. /// /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#Network_address) -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub struct MetaAddr { /// The peer's canonical socket address. @@ -223,10 +237,16 @@ pub struct MetaAddr { /// Whether this peer address was added to the address book /// when the peer made an inbound connection. is_inbound: bool, + + /// The user agent string reported by the peer during handshake, if available. + user_agent: Option, + + /// The protocol version negotiated with the peer during handshake, if available. + negotiated_version: Option, } /// A change to an existing `MetaAddr`. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub enum MetaAddrChange { // TODO: @@ -280,6 +300,8 @@ pub enum MetaAddrChange { addr: PeerSocketAddr, services: PeerServices, is_inbound: bool, + user_agent: String, + negotiated_version: Version, }, /// Updates an existing `MetaAddr` when we send a ping to a peer. @@ -349,6 +371,8 @@ impl MetaAddr { last_connection_state: NeverAttemptedGossiped, misbehavior_score: 0, is_inbound: false, + user_agent: None, + negotiated_version: None, } } @@ -385,11 +409,15 @@ impl MetaAddr { addr: PeerSocketAddr, services: &PeerServices, is_inbound: bool, + user_agent: String, + negotiated_version: Version, ) -> MetaAddrChange { UpdateConnected { addr: canonical_peer_addr(*addr), services: *services, is_inbound, + user_agent, + negotiated_version, } } @@ -445,6 +473,19 @@ impl MetaAddr { } } + /// Returns a [`MetaAddrChange::UpdateMisbehavior`] for a peer that has misbehaved. + /// + /// Canonicalizes the address to match the form stored by a successful handshake + /// (`new_connected`). On Linux dual-stack sockets, inbound IPv4 connections + /// arrive as IPv4-mapped IPv6 addresses (`::ffff:A.B.C.D`); without + /// canonicalization, `apply_to_meta_addr` panics on the addr invariant. + pub fn new_misbehavior(addr: PeerSocketAddr, score_increment: u32) -> MetaAddrChange { + UpdateMisbehavior { + addr: canonical_peer_addr(*addr), + score_increment, + } + } + /// Create a new `MetaAddr` for a peer that has just shut down. pub fn new_shutdown(addr: PeerSocketAddr) -> MetaAddrChange { // TODO: if the peer shut down in the Responded state, preserve that @@ -695,6 +736,26 @@ impl MetaAddr { } } + /// Returns the services advertised by the peer, if available. + pub fn services(&self) -> Option { + self.services + } + + /// Returns the last known connection state for this peer. + pub fn last_connection_state(&self) -> PeerAddrState { + self.last_connection_state + } + + /// Returns the user agent string reported by this peer, if available. + pub fn user_agent(&self) -> Option<&str> { + self.user_agent.as_deref() + } + + /// Returns the negotiated protocol version for this peer, if available. + pub fn negotiated_version(&self) -> Option { + self.negotiated_version + } + /// Returns a score of misbehavior encountered in a peer at this address. pub fn misbehavior(&self) -> u32 { self.misbehavior_score @@ -741,6 +802,8 @@ impl MetaAddr { last_connection_state: NeverAttemptedGossiped, misbehavior_score: 0, is_inbound: false, + user_agent: None, + negotiated_version: None, }) } } @@ -940,6 +1003,8 @@ impl MetaAddrChange { /// Returns the corresponding `MetaAddr` for this change. pub fn into_new_meta_addr(self, instant_now: Instant, local_now: DateTime32) -> MetaAddr { + let user_agent = self.user_agent(); + let negotiated_version = self.negotiated_version(); MetaAddr { addr: self.addr(), services: self.untrusted_services(), @@ -952,6 +1017,8 @@ impl MetaAddrChange { last_connection_state: self.peer_addr_state(), misbehavior_score: self.misbehavior_score(), is_inbound: self.is_inbound(), + user_agent, + negotiated_version, } } @@ -974,6 +1041,27 @@ impl MetaAddrChange { } } + /// Returns the user agent from this change, if available. + pub fn user_agent(&self) -> Option { + if let MetaAddrChange::UpdateConnected { user_agent, .. } = self { + Some(user_agent.clone()) + } else { + None + } + } + + /// Returns the negotiated protocol version from this change, if available. + pub fn negotiated_version(&self) -> Option { + if let MetaAddrChange::UpdateConnected { + negotiated_version, .. + } = self + { + Some(*negotiated_version) + } else { + None + } + } + /// Returns the corresponding [`MetaAddr`] for a local listener change. /// /// This method exists so we don't have to provide an unused [`Instant`] to get a local @@ -997,6 +1085,8 @@ impl MetaAddrChange { last_connection_state: self.peer_addr_state(), misbehavior_score: self.misbehavior_score(), is_inbound: self.is_inbound(), + user_agent: None, + negotiated_version: None, } } @@ -1015,7 +1105,7 @@ impl MetaAddrChange { let Some(previous) = previous.into() else { // no previous: create a new entry - return Some(self.into_new_meta_addr(instant_now, local_now)); + return Some(self.clone().into_new_meta_addr(instant_now, local_now)); }; assert_eq!(previous.addr, self.addr(), "unexpected addr mismatch"); @@ -1153,6 +1243,8 @@ impl MetaAddrChange { last_connection_state: self.peer_addr_state(), misbehavior_score: previous.misbehavior_score + self.misbehavior_score(), is_inbound: previous.is_inbound || self.is_inbound(), + user_agent: None, + negotiated_version: None, }) } else { // Existing entry and change are both Attempt, Responded, Failed, @@ -1179,6 +1271,8 @@ impl MetaAddrChange { last_connection_state: self.peer_addr_state(), misbehavior_score: previous.misbehavior_score + self.misbehavior_score(), is_inbound: previous.is_inbound || self.is_inbound(), + user_agent: self.user_agent().or(previous.user_agent), + negotiated_version: self.negotiated_version().or(previous.negotiated_version), }) } } diff --git a/zebra-network/src/meta_addr/arbitrary.rs b/zebra-network/src/meta_addr/arbitrary.rs index 27afb799224..c62edeec1ed 100644 --- a/zebra-network/src/meta_addr/arbitrary.rs +++ b/zebra-network/src/meta_addr/arbitrary.rs @@ -69,9 +69,10 @@ impl MetaAddrChange { ) -> BoxedStrategy<(MetaAddr, Vec)> { any::() .prop_flat_map(move |addr| { + let peer_addr = addr.addr; ( Just(addr), - vec(MetaAddrChange::addr_strategy(addr.addr), 1..max_addr_change), + vec(MetaAddrChange::addr_strategy(peer_addr), 1..max_addr_change), ) }) .boxed() diff --git a/zebra-network/src/meta_addr/tests/prop.rs b/zebra-network/src/meta_addr/tests/prop.rs index d42a20d69de..ab9d7deb860 100644 --- a/zebra-network/src/meta_addr/tests/prop.rs +++ b/zebra-network/src/meta_addr/tests/prop.rs @@ -75,7 +75,7 @@ proptest! { let local_now: DateTime32 = chrono_now.try_into().expect("will succeed until 2038"); for change in changes { - if let Some(changed_addr) = change.apply_to_meta_addr(addr, instant_now, chrono_now) { + if let Some(changed_addr) = change.apply_to_meta_addr(addr.clone(), instant_now, chrono_now) { // untrusted last seen times: // check that we replace None with Some, but leave Some unchanged if addr.untrusted_last_seen.is_some() { @@ -123,12 +123,12 @@ proptest! { for change in changes { while addr.is_ready_for_connection_attempt(instant_now, chrono_now, &Mainnet) { // Simulate an attempt - addr = if let Some(addr) = MetaAddr::new_reconnect(addr.addr) - .apply_to_meta_addr(addr, instant_now, chrono_now) { + addr = if let Some(new_addr) = MetaAddr::new_reconnect(addr.addr) + .apply_to_meta_addr(addr.clone(), instant_now, chrono_now) { attempt_count += 1; // Assume that this test doesn't last longer than MIN_PEER_RECONNECTION_DELAY prop_assert!(attempt_count <= 1); - addr + new_addr } else { // Stop updating when an attempt comes too soon after a failure. // In production these are prevented by the dialer code. @@ -137,7 +137,7 @@ proptest! { } // If `change` is invalid for the current MetaAddr state, skip it. - if let Some(changed_addr) = change.apply_to_meta_addr(addr, instant_now, chrono_now) { + if let Some(changed_addr) = change.apply_to_meta_addr(addr.clone(), instant_now, chrono_now) { prop_assert_eq!(changed_addr.addr, addr.addr); addr = changed_addr; } @@ -229,7 +229,7 @@ proptest! { ); let expected_result = new_addr; - let book_result = address_book.update(change); + let book_result = address_book.update(change.clone()); let book_contents: Vec = address_book.peers().collect(); // Ignore the same addresses that the address book ignores @@ -255,7 +255,7 @@ proptest! { expected_result, ); - if let Some(book_result) = book_result { + if let Some(ref book_result) = book_result { prop_assert_eq!(book_result.addr, addr.addr); // TODO: pass times to MetaAddrChange::apply_to_meta_addr and AddressBook::update, // so the times are equal @@ -328,7 +328,7 @@ proptest! { // Only put valid addresses in the address book. // This means some tests will start with an empty address book. let addrs = if addr.last_known_info_is_valid_for_outbound(&Mainnet) { - Some(addr) + Some(addr.clone()) } else { None }; @@ -439,18 +439,18 @@ proptest! { for change_index in 0..MAX_ADDR_CHANGE { for (addr, changes) in addr_changes_lists.iter() { - let addr = addrs.entry(addr.addr).or_insert(*addr); + let addr = addrs.entry(addr.addr).or_insert(addr.clone()); let change = changes.get(change_index); while addr.is_ready_for_connection_attempt(instant_now, chrono_now, &Mainnet) { // Simulate an attempt - *addr = if let Some(addr) = MetaAddr::new_reconnect(addr.addr) - .apply_to_meta_addr(*addr, instant_now, chrono_now) { - *attempt_counts.entry(addr.addr).or_default() += 1; + *addr = if let Some(new_addr) = MetaAddr::new_reconnect(addr.addr) + .apply_to_meta_addr(addr.clone(), instant_now, chrono_now) { + *attempt_counts.entry(new_addr.addr).or_default() += 1; prop_assert!( - *attempt_counts.get(&addr.addr).unwrap() <= LIVE_PEER_INTERVALS + 1 + *attempt_counts.get(&new_addr.addr).unwrap() <= LIVE_PEER_INTERVALS + 1 ); - addr + new_addr } else { // Stop updating when an attempt comes too soon after a failure. // In production these are prevented by the dialer code. @@ -460,7 +460,7 @@ proptest! { // If `change` is invalid for the current MetaAddr state, skip it. // If we've run out of changes for this addr, do nothing. - if let Some(changed_addr) = change.and_then(|change| change.apply_to_meta_addr(*addr, instant_now, chrono_now)) + if let Some(changed_addr) = change.and_then(|change| change.apply_to_meta_addr(addr.clone(), instant_now, chrono_now)) { prop_assert_eq!(changed_addr.addr, addr.addr); *addr = changed_addr; diff --git a/zebra-network/src/meta_addr/tests/vectors.rs b/zebra-network/src/meta_addr/tests/vectors.rs index 56d29abb54e..4a7d1eda907 100644 --- a/zebra-network/src/meta_addr/tests/vectors.rs +++ b/zebra-network/src/meta_addr/tests/vectors.rs @@ -1,6 +1,9 @@ //! Fixed test cases for MetaAddr and MetaAddrChange. -use std::time::{Duration, Instant}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::{Duration, Instant}, +}; use chrono::Utc; @@ -11,7 +14,8 @@ use zebra_chain::{ use crate::{ constants::{CONCURRENT_ADDRESS_CHANGE_PERIOD, MAX_PEER_ACTIVE_FOR_GOSSIP}, - protocol::types::PeerServices, + meta_addr::MetaAddrChange, + protocol::{external::canonical_peer_addr, types::PeerServices}, PeerSocketAddr, }; @@ -40,6 +44,8 @@ fn sanitize_extremes() { last_connection_state: Default::default(), misbehavior_score: Default::default(), is_inbound: false, + user_agent: None, + negotiated_version: None, }; let max_time_entry = MetaAddr { @@ -54,6 +60,8 @@ fn sanitize_extremes() { last_connection_state: Default::default(), misbehavior_score: Default::default(), is_inbound: false, + user_agent: None, + negotiated_version: None, }; if let Some(min_sanitized) = min_time_entry.sanitize(&Mainnet) { @@ -255,7 +263,7 @@ fn long_delayed_change_is_not_applied() { .expect("constant is valid"); let change = MetaAddr::new_errored(address, PeerServices::NODE_NETWORK); - let outcome = change.apply_to_meta_addr(peer, instant_early, chrono_early); + let outcome = change.apply_to_meta_addr(peer.clone(), instant_early, chrono_early); assert_eq!( outcome, None, @@ -298,7 +306,7 @@ fn later_revert_change_is_applied() { .expect("constant is valid"); let change = MetaAddr::new_reconnect(address); - let outcome = change.apply_to_meta_addr(peer, instant_late, chrono_late); + let outcome = change.apply_to_meta_addr(peer.clone(), instant_late, chrono_late); assert!( outcome.is_some(), @@ -339,7 +347,7 @@ fn concurrent_state_revert_change_is_not_applied() { .expect("constant is valid"); let change = MetaAddr::new_reconnect(address); - let outcome = change.apply_to_meta_addr(peer, instant_early, chrono_early); + let outcome = change.apply_to_meta_addr(peer.clone(), instant_early, chrono_early); assert_eq!( outcome, None, @@ -357,7 +365,7 @@ fn concurrent_state_revert_change_is_not_applied() { .expect("constant is valid"); let change = MetaAddr::new_reconnect(address); - let outcome = change.apply_to_meta_addr(peer, instant_late, chrono_late); + let outcome = change.apply_to_meta_addr(peer.clone(), instant_late, chrono_late); assert_eq!( outcome, None, @@ -398,7 +406,7 @@ fn concurrent_state_progress_change_is_applied() { .expect("constant is valid"); let change = MetaAddr::new_errored(address, None); - let outcome = change.apply_to_meta_addr(peer, instant_early, chrono_early); + let outcome = change.apply_to_meta_addr(peer.clone(), instant_early, chrono_early); assert!( outcome.is_some(), @@ -416,7 +424,7 @@ fn concurrent_state_progress_change_is_applied() { .expect("constant is valid"); let change = MetaAddr::new_errored(address, None); - let outcome = change.apply_to_meta_addr(peer, instant_late, chrono_late); + let outcome = change.apply_to_meta_addr(peer.clone(), instant_late, chrono_late); assert!( outcome.is_some(), @@ -448,3 +456,113 @@ fn rtt_is_stored_correctly_in_meta_addr() { assert_eq!(peer.rtt, Some(rtt)); } + +/// Proof-of-concept for GHSA-63wg-wjjj-7cp8: +/// IPv4-mapped misbehavior update panics at address-book invariant. +/// +/// On dual-stack listeners, an inbound IPv4 peer arrives as `::ffff:A.B.C.D`. +/// The handshake stores the canonical `A.B.C.D` form in the address book, but +/// the mempool misbehavior path forwards the raw IPv4-mapped address. The +/// `assert_eq!` in `apply_to_meta_addr` then panics, aborting the node. +/// +/// This test MUST panic before the fix is applied. +#[test] +#[should_panic(expected = "unexpected addr mismatch")] +fn ipv4_mapped_misbehavior_panics_without_fix() { + let _init_guard = zebra_test::init(); + + let instant_now = Instant::now(); + let chrono_now = Utc::now(); + + // Simulate what the OS delivers on a dual-stack `[::]` listener for an + // incoming IPv4 connection: the kernel presents the address as IPv4-mapped IPv6. + let raw_addr = PeerSocketAddr::from(SocketAddr::new( + IpAddr::V6(Ipv4Addr::LOCALHOST.to_ipv6_mapped()), + 8233, + )); + let canonical_addr = canonical_peer_addr(*raw_addr); + + // Sanity: the raw and canonical forms must differ for this bug to fire. + assert_ne!( + raw_addr, canonical_addr, + "test setup: need an IPv4-mapped addr" + ); + + // Handshake succeeds → address book stores the canonical (IPv4) address. + let previous = MetaAddr::new_connected( + raw_addr, + &PeerServices::NODE_NETWORK, + true, + String::new(), + crate::protocol::external::types::Version(170_100), + ) + .into_new_meta_addr( + instant_now, + chrono_now.try_into().expect("will succeed until 2038"), + ); + + assert_eq!( + previous.addr(), + canonical_addr, + "handshake must canonicalize" + ); + + // Mempool misbehavior path constructs the change with the raw IPv4-mapped + // address (as initialize.rs currently does), bypassing canonicalization. + let misbehavior_change = MetaAddrChange::UpdateMisbehavior { + addr: raw_addr, // ← the unfixed, raw address + score_increment: 100, + }; + + // This call hits `assert_eq!(previous.addr, self.addr())` in apply_to_meta_addr + // and panics, aborting the node in production. + let _ = misbehavior_change.apply_to_meta_addr(previous, instant_now, chrono_now); +} + +/// Regression test for GHSA-63wg-wjjj-7cp8: +/// `MetaAddr::new_misbehavior` canonicalizes IPv4-mapped addresses. +/// +/// After the fix, misbehavior updates for IPv4-mapped IPv6 peers must succeed +/// and apply to the canonical IPv4 address stored by the handshake. +#[test] +fn new_misbehavior_canonicalizes_ipv4_mapped_addr() { + let _init_guard = zebra_test::init(); + + let instant_now = Instant::now(); + let chrono_now = Utc::now(); + + let raw_addr = PeerSocketAddr::from(SocketAddr::new( + IpAddr::V6(Ipv4Addr::LOCALHOST.to_ipv6_mapped()), + 8233, + )); + let canonical_addr = canonical_peer_addr(*raw_addr); + + assert_ne!(raw_addr, canonical_addr); + + // Handshake stores canonical IPv4 address. + let previous = MetaAddr::new_connected( + raw_addr, + &PeerServices::NODE_NETWORK, + true, + String::new(), + crate::protocol::external::types::Version(170_100), + ) + .into_new_meta_addr( + instant_now, + chrono_now.try_into().expect("will succeed until 2038"), + ); + + assert_eq!(previous.addr(), canonical_addr); + + // Fix: new_misbehavior canonicalizes the address. + let change = MetaAddr::new_misbehavior(raw_addr, 100); + + assert_eq!(change.addr(), canonical_addr); + + let updated = change + .apply_to_meta_addr(previous, instant_now, chrono_now) + .expect("canonical misbehavior update should apply to existing peer"); + + assert_eq!(updated.addr(), canonical_addr); + assert_eq!(updated.misbehavior(), 100); +} diff --git a/zebra-network/src/peer/client.rs b/zebra-network/src/peer/client.rs index 5b6e3d2843f..5db74efb0db 100644 --- a/zebra-network/src/peer/client.rs +++ b/zebra-network/src/peer/client.rs @@ -359,13 +359,12 @@ impl MissingInventoryCollector { } } - /// Forwards any missing inventory to the registry. + /// Forwards explicitly observed missing inventory to the registry. /// - /// `zcashd` doesn't send `notfound` messages for blocks, - /// so we need to track missing blocks ourselves. - /// - /// This can sometimes send duplicate missing inventory, - /// but the registry ignores duplicates anyway. + /// Only explicit `notfound` responses update the registry. Transport + /// failures and timeouts do not prove the peer lacks the inventory, + /// so they are not forwarded (prevents registry poisoning under + /// high sync concurrency). pub fn send(self, response: &Result) { let missing_inv: HashSet = match (self.request, response) { // Missing block hashes from partial responses. @@ -385,31 +384,28 @@ impl MissingInventoryCollector { // Other response types never contain missing inventory. (_, Ok(_)) => iter::empty().collect(), - // We don't forward NotFoundRegistry errors, - // because the errors are generated locally from the registry, - // so those statuses are already in the registry. - // - // Unfortunately, we can't access the inner error variant here, - // due to TracedError. + // Registry-generated errors are already tracked — don't re-forward. (_, Err(e)) if e.inner_debug().contains("NotFoundRegistry") => iter::empty().collect(), - // Missing inventory from other errors, including NotFoundResponse, timeouts, - // and dropped connections. - (request, Err(_)) => { - // The request either contains blocks or transactions, - // but this is a convenient way to collect them both. - let missing_blocks = request - .block_hash_inventory() - .into_iter() - .map(InventoryHash::Block); - - let missing_txs = request - .transaction_id_inventory() - .into_iter() - .map(InventoryHash::from); - - missing_blocks.chain(missing_txs).collect() - } + // Only mark inventory as missing for explicit notfound responses. + // Transport failures (timeouts, dropped connections) do not prove + // the peer lacks the inventory — they only prove the request failed. + // Treating them as missing poisons the routing registry under high + // sync concurrency. + (ref request, Err(e)) if e.inner_debug().contains("NotFoundResponse") => request + .block_hash_inventory() + .into_iter() + .map(InventoryHash::Block) + .chain( + request + .transaction_id_inventory() + .into_iter() + .map(InventoryHash::from), + ) + .collect(), + + // All other errors (timeouts, drops, overload): don't poison. + (_, Err(_)) => iter::empty().collect(), }; if let Some(missing_inv) = diff --git a/zebra-network/src/peer/connection.rs b/zebra-network/src/peer/connection.rs index 27cd9162c2d..99c0e37bdb7 100644 --- a/zebra-network/src/peer/connection.rs +++ b/zebra-network/src/peer/connection.rs @@ -448,7 +448,7 @@ impl Handler { // doesn't respond to our getaddr requests. // // Add the new addresses to the end of the cache. - cached_addrs.extend(new_addrs); + cached_addrs.extend(new_addrs.into_iter().cloned()); // # Security // @@ -1123,7 +1123,7 @@ where Handler::Finished(Ok(Response::Nil)) ) } - (AwaitingRequest, AdvertiseTransactionIds(hashes)) => { + (AwaitingRequest, AdvertiseTransactionIds(hashes, _)) => { let max_tx_inv_in_message: usize = MAX_TX_INV_IN_SENT_MESSAGE .try_into() .expect("constant fits in usize"); @@ -1154,7 +1154,7 @@ where Handler::Finished(Ok(Response::Nil)) ) } - (AwaitingRequest, AdvertiseBlock(hash) | AdvertiseBlockToAll(hash)) => { + (AwaitingRequest, AdvertiseBlock(hash, _) | AdvertiseBlockToAll(hash)) => { self .peer_tx .send(Message::Inv(vec![hash.into()])) @@ -1279,7 +1279,11 @@ where Message::Inv(ref items) => match &items[..] { // We don't expect to be advertised multiple blocks at a time, // so we ignore any advertisements of multiple blocks. - [InventoryHash::Block(hash)] => Request::AdvertiseBlock(*hash).into(), + [InventoryHash::Block(hash)] => Request::AdvertiseBlock( + *hash, + self.connection_info.connected_addr.get_transient_addr(), + ) + .into(), // Some peers advertise invs with mixed item types. // But we're just interested in the transaction invs. @@ -1287,7 +1291,11 @@ where // TODO: split mixed invs into multiple requests, // but skip runs of multiple blocks. tx_ids if tx_ids.iter().any(|item| item.unmined_tx_id().is_some()) => { - Request::AdvertiseTransactionIds(transaction_ids(items).collect()).into() + Request::AdvertiseTransactionIds( + transaction_ids(items).collect(), + self.connection_info.connected_addr.get_transient_addr(), + ) + .into() } // Log detailed messages for ignored inv advertisement messages. diff --git a/zebra-network/src/peer/connection/tests/vectors.rs b/zebra-network/src/peer/connection/tests/vectors.rs index 90b26e702dd..58ac878b63d 100644 --- a/zebra-network/src/peer/connection/tests/vectors.rs +++ b/zebra-network/src/peer/connection/tests/vectors.rs @@ -463,7 +463,7 @@ async fn connection_run_loop_send_timeout_nil_response() { // Simulate a message send timeout let (request_tx, mut request_rx) = oneshot::channel(); let request = ClientRequest { - request: Request::AdvertiseTransactionIds(HashSet::new()), + request: Request::AdvertiseTransactionIds(HashSet::new(), None), tx: request_tx, inv_collector: None, transient_addr: None, @@ -927,7 +927,7 @@ async fn connection_ping_pong_round_trip() { let ping_nonce = match outbound_msg { Message::Ping(nonce) => nonce, - msg => panic!("expected Ping message, but got: {:?}", msg), + msg => panic!("expected Ping message, but got: {msg:?}"), }; assert_eq!( diff --git a/zebra-network/src/peer/handshake.rs b/zebra-network/src/peer/handshake.rs index 5a0b606349f..ee49a6fd1dd 100644 --- a/zebra-network/src/peer/handshake.rs +++ b/zebra-network/src/peer/handshake.rs @@ -984,6 +984,8 @@ where book_addr, &remote_services, connected_addr.is_inbound(), + connection_info.remote.user_agent.clone(), + connection_info.negotiated_version, )) .await; } @@ -995,6 +997,7 @@ where // stream from the unversioned Framed wrapper and construct a new one with a versioned codec. let bare_codec = peer_conn.codec_mut(); bare_codec.reconfigure_version(connection_info.negotiated_version); + bare_codec.reconfigure_full_body_len(); debug!("constructing client, spawning server"); diff --git a/zebra-network/src/peer_set.rs b/zebra-network/src/peer_set.rs index 6fa6863d099..187b1d32056 100644 --- a/zebra-network/src/peer_set.rs +++ b/zebra-network/src/peer_set.rs @@ -3,6 +3,7 @@ mod initialize; mod inventory_registry; mod limit; mod set; +mod stall_tracker; mod unready_service; pub(crate) use candidate_set::CandidateSet; diff --git a/zebra-network/src/peer_set/initialize.rs b/zebra-network/src/peer_set/initialize.rs index 18ef5411f63..1cde0d678e4 100644 --- a/zebra-network/src/peer_set/initialize.rs +++ b/zebra-network/src/peer_set/initialize.rs @@ -149,10 +149,7 @@ where _ = flush_timer.next() => { for (addr, score_increment) in misbehaviors.drain() { let _ = misbehaviour_updater - .send(MetaAddrChange::UpdateMisbehavior { - addr, - score_increment - }) + .send(MetaAddr::new_misbehavior(addr, score_increment)) .await; } }, diff --git a/zebra-network/src/peer_set/initialize/tests/vectors.rs b/zebra-network/src/peer_set/initialize/tests/vectors.rs index b88ad365c9b..4c42f1659dc 100644 --- a/zebra-network/src/peer_set/initialize/tests/vectors.rs +++ b/zebra-network/src/peer_set/initialize/tests/vectors.rs @@ -1194,6 +1194,7 @@ async fn self_connections_should_fail() { let updated_addr = unlocked_address_book.update( real_self_listener + .clone() .new_gossiped_change() .expect("change is valid"), ); @@ -1204,22 +1205,20 @@ async fn self_connections_should_fail() { }; // Make sure we modified the address book correctly - assert!( - updated_addr.is_some(), - "inserting our own address into the address book failed: {real_self_listener:?}" - ); + let updated_addr = + updated_addr.expect("inserting our own address into the address book failed"); assert_eq!( - updated_addr.unwrap().addr(), + updated_addr.addr(), real_self_listener.addr(), "wrong address inserted into address book" ); assert_ne!( - updated_addr.unwrap().addr().ip(), + updated_addr.addr().ip(), Ipv4Addr::UNSPECIFIED, "invalid address inserted into address book: ip must be valid for inbound connections" ); assert_ne!( - updated_addr.unwrap().addr().port(), + updated_addr.addr().port(), 0, "invalid address inserted into address book: port must be valid for inbound connections" ); @@ -1543,7 +1542,7 @@ where PeerServices::NODE_NETWORK, DateTime32::now(), ); - fake_peer = Some(addr); + fake_peer = Some(addr.clone()); let addr = addr .new_gossiped_change() .expect("created MetaAddr contains enough information to represent a gossiped address"); @@ -1555,15 +1554,19 @@ where } // Create a fake peer set. - let nil_peer_set = service_fn(move |req| async move { - let rsp = match req { - // Return the correct response variant for Peers requests, - // reusing one of the peers we already provided. - Request::Peers => Response::Peers(vec![fake_peer.unwrap()]), - _ => unreachable!("unexpected request: {:?}", req), - }; - - Ok(rsp) + let fake_peer = fake_peer.expect("there is at least one fake peer"); + let nil_peer_set = service_fn(move |req| { + let fake_peer = fake_peer.clone(); + async move { + let rsp = match req { + // Return the correct response variant for Peers requests, + // reusing one of the peers we already provided. + Request::Peers => Response::Peers(vec![fake_peer]), + _ => unreachable!("unexpected request: {:?}", req), + }; + + Ok(rsp) + } }); // Make the channels large enough to hold all the peers. diff --git a/zebra-network/src/peer_set/set.rs b/zebra-network/src/peer_set/set.rs index 4da7f3ef60a..ed1be4ad831 100644 --- a/zebra-network/src/peer_set/set.rs +++ b/zebra-network/src/peer_set/set.rs @@ -116,7 +116,7 @@ use indexmap::IndexMap; use itertools::Itertools; use num_integer::div_ceil; use tokio::{ - sync::{broadcast, watch}, + sync::{broadcast, mpsc as tokio_mpsc, watch}, task::JoinHandle, }; use tower::{ @@ -132,6 +132,7 @@ use crate::{ constants::MIN_PEER_SET_LOG_INTERVAL, peer::{LoadTrackedClient, MinimumPeerVersion}, peer_set::{ + stall_tracker::FindResponseStallTracker, unready_service::{Error as UnreadyError, UnreadyService}, InventoryChange, InventoryRegistry, }, @@ -162,6 +163,26 @@ pub struct CancelClientWork; type ResponseFuture = Pin> + Send + 'static>>; +/// Classification of a `FindBlocks`/`FindHeaders` response, sent from a +/// response-wrapping future to [`PeerSet::poll_ready`] via an mpsc channel so +/// the stall tracker can be updated and the peer disconnected if needed. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum StallOutcome { + Stall, + Clear, +} + +fn classify_find_response(result: &Result) -> Option { + match result { + Ok(Response::BlockHashes(hashes)) if hashes.is_empty() => Some(StallOutcome::Stall), + Ok(Response::BlockHashes(_)) => Some(StallOutcome::Clear), + Ok(Response::BlockHeaders(headers)) if headers.is_empty() => Some(StallOutcome::Stall), + Ok(Response::BlockHeaders(_)) => Some(StallOutcome::Clear), + Ok(_) => None, + Err(_) => Some(StallOutcome::Stall), + } +} + /// A [`tower::Service`] that abstractly represents "the rest of the network". /// /// # Security @@ -190,6 +211,19 @@ where /// A watch channel receiver with a copy of banned IP addresses. bans_receiver: watch::Receiver>>, + /// Tracks peers returning empty `FindBlocks`/`FindHeaders` responses. + /// Mutated only from [`Self::poll_ready`] via [`Self::stall_event_rx`]. + find_response_stalls: FindResponseStallTracker, + + /// Receives stall/clear events from tracked routing futures in + /// [`Self::route_p2c`]. The channel keeps the tracker single-owner (no + /// `Mutex`) and confines mutation to `poll_ready`, where the peer set can + /// call [`Self::remove`] directly. + stall_event_rx: tokio_mpsc::UnboundedReceiver<(PeerSocketAddr, StallOutcome)>, + + /// Producer clones handed to each tracked request's response wrapper. + stall_event_tx: tokio_mpsc::UnboundedSender<(PeerSocketAddr, StallOutcome)>, + // Peer Tracking: Ready Peers // /// Connected peers that are ready to receive requests from Zebra, @@ -314,6 +348,7 @@ where minimum_peer_version: MinimumPeerVersion, max_conns_per_ip: Option, ) -> Self { + let (stall_event_tx, stall_event_rx) = tokio_mpsc::unbounded_channel(); Self { // New peers discover, @@ -321,6 +356,11 @@ where // Banned peers bans_receiver, + // Stall tracking + find_response_stalls: FindResponseStallTracker::new(), + stall_event_rx, + stall_event_tx, + // Ready peers ready_services: HashMap::new(), // Request Routing @@ -736,11 +776,34 @@ where } } + /// Drains pending stall/clear events from tracked routing futures and + /// disconnects peers that have exceeded the stall threshold. The peer's + /// TCP connection is closed when its service is dropped; address book and + /// ban list are untouched, so the peer is free to reconnect. + fn drain_stall_events(&mut self, cx: &mut Context<'_>) { + while let Poll::Ready(Some((addr, outcome))) = self.stall_event_rx.poll_recv(cx) { + match outcome { + StallOutcome::Stall => { + if self.find_response_stalls.record_stall(addr) { + info!( + ?addr, + "dropping stalled peer: exceeded FindBlocks/FindHeaders stall threshold", + ); + self.remove(&addr); + } + } + StallOutcome::Clear => self.find_response_stalls.clear(addr), + } + } + } + /// Remove the service corresponding to `key` from the peer set. /// /// Drops the service, cancelling any pending request or response to that peer. /// If the peer does not exist, does nothing. fn remove(&mut self, key: &D::Key) { + self.find_response_stalls.clear(*key); + if let Some(ready_service) = self.take_ready_service(key) { // A ready service has no work to cancel, so just drop it. std::mem::drop(ready_service); @@ -880,9 +943,26 @@ where .take_ready_service(&p2c_key) .expect("selected peer must be ready"); + let track_stalls = matches!( + &req, + Request::FindBlocks { .. } | Request::FindHeaders { .. } + ); + let fut = svc.call(req); self.push_unready(p2c_key, svc); + if track_stalls { + let stall_tx = self.stall_event_tx.clone(); + return async move { + let result = fut.await; + if let Some(outcome) = classify_find_response(&result) { + let _ = stall_tx.send((p2c_key, outcome)); + } + result.map_err(Into::into) + } + .boxed(); + } + return fut.map_err(Into::into).boxed(); } @@ -1268,6 +1348,10 @@ where // - an unready peer becomes ready, or // - a new peer arrives. + // Drain stall events first, so disconnects free up slots that + // `poll_discover` can fill in the same poll cycle. + self.drain_stall_events(cx); + // Check for new peers, and register a task wakeup when the next new peers arrive. New peers // can be infrequent if our connection slots are full, or we're connected to all // available/useful peers. @@ -1329,8 +1413,8 @@ where } // Broadcast advertisements to lots of peers - Request::AdvertiseTransactionIds(_) => self.route_broadcast(req), - Request::AdvertiseBlock(_) => self.route_broadcast(req), + Request::AdvertiseTransactionIds(_, _) => self.route_broadcast(req), + Request::AdvertiseBlock(_, _) => self.route_broadcast(req), Request::AdvertiseBlockToAll(_) => self.broadcast_all(req), // Choose a random less-loaded peer for all other requests diff --git a/zebra-network/src/peer_set/set/tests/prop.rs b/zebra-network/src/peer_set/set/tests/prop.rs index 6f64fcb15c6..7e657128149 100644 --- a/zebra-network/src/peer_set/set/tests/prop.rs +++ b/zebra-network/src/peer_set/set/tests/prop.rs @@ -149,14 +149,14 @@ proptest! { } // Send a request to all peers - let response_future = peer_set.route_broadcast(Request::AdvertiseBlock(block_hash)); + let response_future = peer_set.route_broadcast(Request::AdvertiseBlock(block_hash, None)); std::mem::drop(response_future); // Check how many peers received the request let mut received = 0; for mut h in handles { if let ReceiveRequestAttempt::Request(client_request) = h.try_to_receive_outbound_client_request() { - prop_assert_eq!(client_request.request, Request::AdvertiseBlock(block_hash)); + prop_assert_eq!(client_request.request, Request::AdvertiseBlock(block_hash, None)); received += 1; }; } @@ -219,14 +219,14 @@ proptest! { let number_of_peers_to_broadcast = peer_set.number_of_peers_to_broadcast(); // Send a request to all peers we have now - let response_future = peer_set.route_broadcast(Request::AdvertiseBlock(block_hash)); + let response_future = peer_set.route_broadcast(Request::AdvertiseBlock(block_hash, None)); std::mem::drop(response_future); // Check how many peers received the request let mut received = 0; for h in &mut handles { if let ReceiveRequestAttempt::Request(client_request) = h.try_to_receive_outbound_client_request() { - prop_assert_eq!(client_request.request, Request::AdvertiseBlock(block_hash)); + prop_assert_eq!(client_request.request, Request::AdvertiseBlock(block_hash, None)); received += 1; }; } @@ -281,7 +281,7 @@ proptest! { } // this will panic as expected - let response_future = peer_set.route_broadcast(Request::AdvertiseBlock(block_hash)); + let response_future = peer_set.route_broadcast(Request::AdvertiseBlock(block_hash, None)); std::mem::drop(response_future); Ok::<_, TestCaseError>(()) diff --git a/zebra-network/src/peer_set/set/tests/vectors.rs b/zebra-network/src/peer_set/set/tests/vectors.rs index 72fc7c6e756..082bfcc3910 100644 --- a/zebra-network/src/peer_set/set/tests/vectors.rs +++ b/zebra-network/src/peer_set/set/tests/vectors.rs @@ -28,7 +28,7 @@ fn peer_set_ready_single_connection() { let peer_versions = PeerVersions { peer_versions: vec![Version::min_specified_for_upgrade( &Network::Mainnet, - NetworkUpgrade::Nu6, + NetworkUpgrade::Nu6_2, )], }; @@ -120,7 +120,7 @@ fn peer_set_ready_single_connection() { #[test] fn peer_set_ready_multiple_connections() { // Use three peers with the same version - let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6); + let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6_2); let peer_versions = PeerVersions { peer_versions: vec![peer_version, peer_version, peer_version], }; @@ -184,7 +184,7 @@ fn peer_set_rejects_connections_past_per_ip_limit() { const NUM_PEER_VERSIONS: usize = crate::constants::DEFAULT_MAX_CONNS_PER_IP + 1; // Use three peers with the same version - let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6); + let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6_2); let peer_versions = PeerVersions { peer_versions: [peer_version; NUM_PEER_VERSIONS].into_iter().collect(), }; @@ -234,7 +234,7 @@ fn peer_set_route_inv_empty_registry() { let test_hash = block::Hash([0; 32]); // Use two peers with the same version - let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6); + let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6_2); let peer_versions = PeerVersions { peer_versions: vec![peer_version, peer_version], }; @@ -298,7 +298,7 @@ fn broadcast_all_queued_removes_banned_peers() { let peer_versions = PeerVersions { peer_versions: vec![Version::min_specified_for_upgrade( &Network::Mainnet, - NetworkUpgrade::Nu6, + NetworkUpgrade::Nu6_2, )], }; @@ -345,7 +345,7 @@ fn remove_unready_peer_clears_cancel_handle_and_updates_counts() { let peer_versions = PeerVersions { peer_versions: vec![Version::min_specified_for_upgrade( &Network::Mainnet, - NetworkUpgrade::Nu6, + NetworkUpgrade::Nu6_2, )], }; @@ -412,7 +412,7 @@ fn peer_set_route_inv_advertised_registry_order(advertised_first: bool) { let test_change = InventoryStatus::new_available(test_inv, test_peer); // Use two peers with the same version - let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6); + let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6_2); let peer_versions = PeerVersions { peer_versions: vec![peer_version, peer_version], }; @@ -520,7 +520,7 @@ fn peer_set_route_inv_missing_registry_order(missing_first: bool) { let test_change = InventoryStatus::new_missing(test_inv, test_peer); // Use two peers with the same version - let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6); + let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6_2); let peer_versions = PeerVersions { peer_versions: vec![peer_version, peer_version], }; @@ -622,7 +622,7 @@ fn peer_set_route_inv_all_missing_fail() { let test_change = InventoryStatus::new_missing(test_inv, test_peer); // Use one peer - let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6); + let peer_version = Version::min_specified_for_upgrade(&Network::Mainnet, NetworkUpgrade::Nu6_2); let peer_versions = PeerVersions { peer_versions: vec![peer_version], }; diff --git a/zebra-network/src/peer_set/stall_tracker.rs b/zebra-network/src/peer_set/stall_tracker.rs new file mode 100644 index 00000000000..9717b254096 --- /dev/null +++ b/zebra-network/src/peer_set/stall_tracker.rs @@ -0,0 +1,52 @@ +//! Tracks peers that consistently return empty or failed `FindBlocks` or +//! `FindHeaders` responses, so the peer set can disconnect them. +//! +//! A peer returning a single empty response may just be syncing itself; a peer +//! that does so repeatedly stalls the syncer by forcing retries to others. The +//! counter is per-peer and resets on any useful (non-empty) response. +//! +//! Only applies to `FindBlocks` and `FindHeaders`. An empty response to +//! `BlocksByHash`/`TransactionsById` is a legitimate "I don't have this +//! inventory" answer, so those don't feed the tracker. + +use std::collections::HashMap; + +use crate::PeerSocketAddr; + +/// Consecutive empty or failed `FindBlocks`/`FindHeaders` responses tolerated +/// before the peer set disconnects a peer. +pub(super) const FIND_RESPONSE_STALL_THRESHOLD: usize = 3; + +#[derive(Default)] +pub(super) struct FindResponseStallTracker { + counts: HashMap, +} + +impl FindResponseStallTracker { + pub(super) fn new() -> Self { + Self::default() + } + + /// Records a stall for `addr`. Returns `true` once the peer reaches + /// [`FIND_RESPONSE_STALL_THRESHOLD`] — the caller must then disconnect it. + /// On threshold the entry is removed, so a reconnected peer starts fresh. + pub(super) fn record_stall(&mut self, addr: PeerSocketAddr) -> bool { + let count = self.counts.entry(addr).or_default(); + *count += 1; + + if *count >= FIND_RESPONSE_STALL_THRESHOLD { + self.counts.remove(&addr); + true + } else { + false + } + } + + /// Clears tracking for a peer that sent a useful response or disconnected. + pub(super) fn clear(&mut self, addr: PeerSocketAddr) { + self.counts.remove(&addr); + } +} + +#[cfg(test)] +mod tests; diff --git a/zebra-network/src/peer_set/stall_tracker/tests.rs b/zebra-network/src/peer_set/stall_tracker/tests.rs new file mode 100644 index 00000000000..80d08343d6d --- /dev/null +++ b/zebra-network/src/peer_set/stall_tracker/tests.rs @@ -0,0 +1,58 @@ +//! Unit tests for [`FindResponseStallTracker`]. + +use super::*; + +fn test_addr(last_octet: u8) -> PeerSocketAddr { + use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; + SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, last_octet), + 8233, + )) + .into() +} + +#[test] +fn disconnects_after_threshold() { + let mut tracker = FindResponseStallTracker::new(); + let addr = test_addr(1); + + assert!(!tracker.record_stall(addr)); + assert!(!tracker.record_stall(addr)); + + // Third stall: at threshold. + assert!(tracker.record_stall(addr)); + + // Entry cleared on threshold — next stall starts fresh. + assert!(!tracker.record_stall(addr)); +} + +#[test] +fn clear_resets_count() { + let mut tracker = FindResponseStallTracker::new(); + let addr = test_addr(1); + + assert!(!tracker.record_stall(addr)); + assert!(!tracker.record_stall(addr)); + + tracker.clear(addr); + + // Back to zero: needs a full threshold's worth of stalls again. + assert!(!tracker.record_stall(addr)); + assert!(!tracker.record_stall(addr)); + assert!(tracker.record_stall(addr)); +} + +#[test] +fn independent_per_peer() { + let mut tracker = FindResponseStallTracker::new(); + let addr_a = test_addr(1); + let addr_b = test_addr(2); + + assert!(!tracker.record_stall(addr_a)); + assert!(!tracker.record_stall(addr_a)); + assert!(!tracker.record_stall(addr_b)); + assert!(tracker.record_stall(addr_a)); + + assert!(!tracker.record_stall(addr_b)); + assert!(tracker.record_stall(addr_b)); +} diff --git a/zebra-network/src/protocol/external/addr.rs b/zebra-network/src/protocol/external/addr.rs index 53281157262..6d3993ba771 100644 --- a/zebra-network/src/protocol/external/addr.rs +++ b/zebra-network/src/protocol/external/addr.rs @@ -22,9 +22,4 @@ pub(super) use v2::AddrV2; #[allow(unused_imports)] #[cfg(any(test, feature = "proptest-impl"))] -pub(super) use v1::{ipv6_mapped_ip_addr, ADDR_V1_SIZE}; - -// TODO: write tests for addrv2 deserialization -#[allow(unused_imports)] -#[cfg(any(test, feature = "proptest-impl"))] -pub(super) use v2::ADDR_V2_MIN_SIZE; +pub(super) use v1::ipv6_mapped_ip_addr; diff --git a/zebra-network/src/protocol/external/addr/v1.rs b/zebra-network/src/protocol/external/addr/v1.rs index a8be5a3355a..55ed1013507 100644 --- a/zebra-network/src/protocol/external/addr/v1.rs +++ b/zebra-network/src/protocol/external/addr/v1.rs @@ -17,11 +17,7 @@ use zebra_chain::serialization::{ ZcashSerialize, }; -use crate::{ - meta_addr::MetaAddr, - protocol::external::{types::PeerServices, MAX_PROTOCOL_MESSAGE_LEN}, - PeerSocketAddr, -}; +use crate::{meta_addr::MetaAddr, protocol::external::types::PeerServices, PeerSocketAddr}; use super::canonical_peer_addr; @@ -131,13 +127,15 @@ impl ZcashDeserialize for AddrV1 { } } -/// A serialized `addr` (v1) has a 4 byte time, 8 byte services, 16 byte IP addr, and 2 byte port -pub(in super::super) const ADDR_V1_SIZE: usize = 4 + 8 + 16 + 2; - impl TrustedPreallocate for AddrV1 { fn max_allocation() -> u64 { - // Since ADDR_V1_SIZE is less than 2^5, the length of the largest list takes up 5 bytes. - ((MAX_PROTOCOL_MESSAGE_LEN - 5) / ADDR_V1_SIZE) as u64 + // The protocol caps addr messages at 1,000 entries. + // + // + // Previously this was derived from MAX_PROTOCOL_MESSAGE_LEN / ADDR_V1_SIZE = 69,904, + // which allowed a remote peer to force a multi-megabyte allocation before the cap + // was checked. See GHSA-xr93-pcq3-pxf8. + crate::constants::MAX_ADDRS_IN_MESSAGE as u64 } } diff --git a/zebra-network/src/protocol/external/addr/v2.rs b/zebra-network/src/protocol/external/addr/v2.rs index d7fc14a7b5c..52ff42f1035 100644 --- a/zebra-network/src/protocol/external/addr/v2.rs +++ b/zebra-network/src/protocol/external/addr/v2.rs @@ -18,11 +18,7 @@ use zebra_chain::serialization::{ SerializationError, TrustedPreallocate, ZcashDeserialize, ZcashDeserializeInto, }; -use crate::{ - meta_addr::MetaAddr, - protocol::external::{types::PeerServices, MAX_PROTOCOL_MESSAGE_LEN}, - PeerSocketAddr, -}; +use crate::{meta_addr::MetaAddr, protocol::external::types::PeerServices, PeerSocketAddr}; use super::canonical_peer_addr; @@ -316,19 +312,14 @@ impl ZcashDeserialize for AddrV2 { } } -/// A serialized `addrv2` has: -/// * 4 byte time, -/// * 1-9 byte services, -/// * 1 byte networkID, -/// * 1-9 byte sizeAddr, -/// * 0-512 bytes addr, -/// * 2 bytes port. -#[allow(clippy::identity_op)] -pub(in super::super) const ADDR_V2_MIN_SIZE: usize = 4 + 1 + 1 + 1 + 0 + 2; - impl TrustedPreallocate for AddrV2 { fn max_allocation() -> u64 { - // Since ADDR_V2_MIN_SIZE is less than 2^5, the length of the largest list takes up 5 bytes. - ((MAX_PROTOCOL_MESSAGE_LEN - 5) / ADDR_V2_MIN_SIZE) as u64 + // The protocol caps addrv2 messages at 1,000 entries. + // + // + // Previously this was derived from MAX_PROTOCOL_MESSAGE_LEN / ADDR_V2_MIN_SIZE = 233,016, + // which allowed a remote peer to force a ~10.7 MiB heap allocation before the cap + // was checked. See GHSA-xr93-pcq3-pxf8. + crate::constants::MAX_ADDRS_IN_MESSAGE as u64 } } diff --git a/zebra-network/src/protocol/external/codec.rs b/zebra-network/src/protocol/external/codec.rs index df7017e3903..d8cf10f2454 100644 --- a/zebra-network/src/protocol/external/codec.rs +++ b/zebra-network/src/protocol/external/codec.rs @@ -15,9 +15,10 @@ use zebra_chain::{ block::{self, Block}, parameters::{Magic, Network}, serialization::{ - sha256d, zcash_deserialize_bytes_external_count, zcash_deserialize_string_external_count, - CompactSizeMessage, FakeWriter, ReadZcashExt, SerializationError as Error, - ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, MAX_PROTOCOL_MESSAGE_LEN, + sha256d, zcash_deserialize_bytes_external_count, zcash_deserialize_external_count, + zcash_deserialize_string_external_count, CompactSizeMessage, FakeWriter, ReadZcashExt, + SerializationError as Error, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, + MAX_HEADERS_PER_MESSAGE, MAX_PROTOCOL_MESSAGE_LEN, }, transaction::Transaction, }; @@ -39,6 +40,12 @@ mod tests; /// The length of a Bitcoin message header. const HEADER_LEN: usize = 24usize; +/// The maximum body length allowed before the handshake completes. +/// +/// Version messages are ~344 bytes max (including a 256-byte user agent); +/// verack is 0 bytes. 1 KB provides headroom for future protocol changes. +const MAX_HANDSHAKE_BODY_LEN: usize = 1024; + /// A codec which produces Bitcoin messages from byte streams and vice versa. pub struct Codec { builder: Builder, @@ -63,7 +70,7 @@ impl Codec { Builder { network: Network::Mainnet, version: constants::CURRENT_NETWORK_PROTOCOL_VERSION, - max_len: MAX_PROTOCOL_MESSAGE_LEN, + max_len: MAX_HANDSHAKE_BODY_LEN, metrics_addr_label: None, } } @@ -72,6 +79,14 @@ impl Codec { pub fn reconfigure_version(&mut self, version: Version) { self.builder.version = version; } + + /// Raise the maximum accepted body length to the full protocol limit. + /// + /// Called after a successful handshake so post-handshake messages (blocks, + /// transactions) can use the full `MAX_PROTOCOL_MESSAGE_LEN`. + pub fn reconfigure_full_body_len(&mut self) { + self.builder.max_len = MAX_PROTOCOL_MESSAGE_LEN; + } } impl Builder { @@ -278,7 +293,10 @@ impl Codec { // Regardless of the way we received the address, // Zebra always sends `addr` messages - let v1_addrs: Vec = addrs.iter().map(|addr| AddrV1::from(*addr)).collect(); + let v1_addrs: Vec = addrs + .iter() + .map(|addr| AddrV1::from(addr.clone())) + .collect(); v1_addrs.zcash_serialize(&mut writer)? } Message::GetAddr => { /* Empty payload -- no-op */ } @@ -678,7 +696,19 @@ impl Codec { /// /// [Zcash block header](https://zips.z.cash/protocol/protocol.pdf#page=84) fn read_headers(&self, mut reader: R) -> Result { - Ok(Message::Headers(Vec::zcash_deserialize(&mut reader)?)) + // CompactSizeMessage is bounded to MAX_PROTOCOL_MESSAGE_LEN on deserialization. + let count: CompactSizeMessage = (&mut reader).zcash_deserialize_into()?; + // Infallible: CompactSizeMessage wraps u32, which always fits in usize. + let count: usize = count.into(); + if count > MAX_HEADERS_PER_MESSAGE { + return Err(Error::Parse( + "headers message exceeds the protocol limit of 160 entries", + )); + } + Ok(Message::Headers(zcash_deserialize_external_count( + count, + &mut reader, + )?)) } fn read_getheaders(&self, mut reader: R) -> Result { diff --git a/zebra-network/src/protocol/external/codec/tests/vectors.rs b/zebra-network/src/protocol/external/codec/tests/vectors.rs index da6b3ce5830..d9b81ea13e9 100644 --- a/zebra-network/src/protocol/external/codec/tests/vectors.rs +++ b/zebra-network/src/protocol/external/codec/tests/vectors.rs @@ -147,7 +147,12 @@ fn filterload_message_round_trip() { let v_bytes = rt.block_on(async { let mut bytes = Vec::new(); { - let mut fw = FramedWrite::new(&mut bytes, Codec::builder().finish()); + let mut fw = FramedWrite::new( + &mut bytes, + Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(), + ); fw.send(v.clone()) .await .expect("message should be serialized"); @@ -156,7 +161,12 @@ fn filterload_message_round_trip() { }); let v_parsed = rt.block_on(async { - let mut fr = FramedRead::new(Cursor::new(&v_bytes), Codec::builder().finish()); + let mut fr = FramedRead::new( + Cursor::new(&v_bytes), + Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(), + ); fr.next() .await .expect("a next message should be available") @@ -249,7 +259,12 @@ fn filterload_message_too_large_round_trip() { let v_bytes = rt.block_on(async { let mut bytes = Vec::new(); { - let mut fw = FramedWrite::new(&mut bytes, Codec::builder().finish()); + let mut fw = FramedWrite::new( + &mut bytes, + Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(), + ); fw.send(v.clone()) .await .expect("message should be serialized"); @@ -258,7 +273,12 @@ fn filterload_message_too_large_round_trip() { }); rt.block_on(async { - let mut fr = FramedRead::new(Cursor::new(&v_bytes), Codec::builder().finish()); + let mut fr = FramedRead::new( + Cursor::new(&v_bytes), + Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(), + ); fr.next() .await .expect("a next message should be available") @@ -588,6 +608,72 @@ fn reject_command_and_reason_size_limits() { } } +/// Regression test for GHSA-438q-jx8f-cccv: read_headers() must reject +/// inbound `headers` messages with more than 160 entries. +#[test] +fn headers_message_exceeding_protocol_cap_is_rejected() { + use zebra_chain::serialization::ZcashDeserializeInto; + + let _init_guard = zebra_test::init(); + + let header: block::Header = zebra_test::vectors::DUMMY_HEADER + .zcash_deserialize_into() + .expect("dummy header should deserialize"); + let counted = block::CountedHeader { + header: header.into(), + }; + + // 161 headers — one more than the protocol limit of 160. + let msg = Message::Headers(vec![counted.clone(); 161]); + + let mut codec = Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(); + let mut bytes = BytesMut::new(); + codec + .encode(msg, &mut bytes) + .expect("encoding should succeed"); + + codec + .decode(&mut bytes) + .expect_err("decoding 161 headers should be rejected"); +} + +/// Verify that a headers message at exactly the protocol cap (160) is accepted. +#[test] +fn headers_message_at_protocol_cap_is_accepted() { + use zebra_chain::serialization::ZcashDeserializeInto; + + let _init_guard = zebra_test::init(); + + let header: block::Header = zebra_test::vectors::DUMMY_HEADER + .zcash_deserialize_into() + .expect("dummy header should deserialize"); + let counted = block::CountedHeader { + header: header.into(), + }; + + let msg = Message::Headers(vec![counted; 160]); + + let mut codec = Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(); + let mut bytes = BytesMut::new(); + codec + .encode(msg, &mut bytes) + .expect("encoding should succeed"); + + let decoded = codec + .decode(&mut bytes) + .expect("decoding should not error") + .expect("a message should be present"); + + match decoded { + Message::Headers(headers) => assert_eq!(headers.len(), 160), + other => panic!("expected Headers, got {other:?}"), + } +} + /// Check that the version test vector deserialization fails when there's a network magic mismatch. #[test] fn message_with_wrong_network_magic_returns_error() { diff --git a/zebra-network/src/protocol/external/tests.rs b/zebra-network/src/protocol/external/tests.rs index 52dea7df902..cf44b6884d4 100644 --- a/zebra-network/src/protocol/external/tests.rs +++ b/zebra-network/src/protocol/external/tests.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_in_result)] +mod addr_limit; mod preallocate; mod prop; mod vectors; diff --git a/zebra-network/src/protocol/external/tests/addr_limit.rs b/zebra-network/src/protocol/external/tests/addr_limit.rs new file mode 100644 index 00000000000..e320db7255f --- /dev/null +++ b/zebra-network/src/protocol/external/tests/addr_limit.rs @@ -0,0 +1,102 @@ +//! PoC for GHSA-xr93-pcq3-pxf8: addr/addrv2 deserialization resource exhaustion. +//! +//! A single crafted ~2 MiB `addrv2` wire message forces Zebra to allocate and +//! parse 233,016 entries (~10.7 MiB heap) before the 1,000-entry protocol cap +//! is checked. This test feeds that message through `Codec::decode()` — the +//! exact code path hit by TCP data from a remote peer after handshake. + +use std::{io::Write, mem}; + +use byteorder::{BigEndian, LittleEndian, WriteBytesExt}; +use bytes::BytesMut; +use tokio_util::codec::Decoder; + +use zebra_chain::{ + parameters::Network, + serialization::{sha256d, TrustedPreallocate, ZcashDeserialize}, +}; + +use crate::{ + constants::MAX_ADDRS_IN_MESSAGE, + protocol::external::{addr::AddrV2, Codec}, +}; + +/// Minimum size of a serialized `addrv2`: 4 byte time + 1 byte services + 1 byte networkID +/// + 1 byte sizeAddr + 0 bytes addr + 2 bytes port. +#[allow(clippy::identity_op)] +const ADDR_V2_MIN_SIZE: usize = 4 + 1 + 1 + 1 + 0 + 2; + +/// Build a wire-format Zcash `addrv2` message with `count` minimal 9-byte entries. +fn build_attack_message(count: usize) -> Vec { + let mut body = Vec::new(); + + // CompactSize prefix + if count < 253 { + body.write_u8(count as u8).unwrap(); + } else if count <= 0xFFFF { + body.write_u8(0xFD).unwrap(); + body.write_u16::(count as u16).unwrap(); + } else { + body.write_u8(0xFE).unwrap(); + body.write_u32::(count as u32).unwrap(); + } + + for _ in 0..count { + body.write_u32::(0x495FAB29).unwrap(); // timestamp + body.write_u8(0).unwrap(); // services (CompactSize 0) + body.write_u8(0xFF).unwrap(); // network_id (unsupported) + body.write_u8(0).unwrap(); // addr_len (CompactSize 0) + body.write_u16::(0).unwrap(); // port + } + + // Zcash message header: magic(4) + command(12) + len(4) + checksum(4) + let mut msg = Vec::with_capacity(24 + body.len()); + msg.write_all(&Network::Mainnet.magic().0).unwrap(); + msg.write_all(b"addrv2\0\0\0\0\0\0").unwrap(); + msg.write_u32::(body.len() as u32).unwrap(); + msg.write_all(&sha256d::Checksum::from(body.as_slice()).0) + .unwrap(); + msg.write_all(&body).unwrap(); + msg +} + +/// PoC: a single ~2 MiB addrv2 message exercises the full Codec::decode() +/// pipeline and forces a ~10.7 MiB heap allocation before rejection. +#[test] +fn poc_remote_addrv2_resource_exhaustion() { + let _init_guard = zebra_test::init(); + + let attack_count = (2_097_152 - 5) / ADDR_V2_MIN_SIZE; // 233,016 + let raw = build_attack_message(attack_count); + let heap_bytes = attack_count * mem::size_of::(); + + // Feed through Codec::decode() — the real TCP inbound path + let mut codec = Codec::builder().finish(); + let mut src = BytesMut::from(raw.as_slice()); + let result = codec.decode(&mut src); + + // 1) max_allocation must not exceed protocol cap + assert!( + AddrV2::max_allocation() <= MAX_ADDRS_IN_MESSAGE as u64, + "max_allocation() is {} — a remote peer can force {:.1} MiB heap allocation", + AddrV2::max_allocation(), + heap_bytes as f64 / (1024.0 * 1024.0), + ); + + // 2) message must be rejected + assert!( + result.is_err(), + "message with {attack_count} entries was accepted" + ); + + // 3) deserialization layer must reject >1000 entries directly + let oversized_body = build_attack_message(MAX_ADDRS_IN_MESSAGE + 1); + // Skip the 24-byte header to get just the body for direct deserialization + let body_only = &oversized_body[24..]; + assert!( + Vec::::zcash_deserialize(body_only).is_err(), + "Vec::zcash_deserialize accepted {} entries (protocol cap: {})", + MAX_ADDRS_IN_MESSAGE + 1, + MAX_ADDRS_IN_MESSAGE, + ); +} diff --git a/zebra-network/src/protocol/external/tests/preallocate.rs b/zebra-network/src/protocol/external/tests/preallocate.rs index e247aa958dd..011f1c6e4de 100644 --- a/zebra-network/src/protocol/external/tests/preallocate.rs +++ b/zebra-network/src/protocol/external/tests/preallocate.rs @@ -13,13 +13,22 @@ use zebra_chain::{ }; use crate::{ + constants::MAX_ADDRS_IN_MESSAGE, meta_addr::MetaAddr, protocol::external::{ - addr::{AddrV1, AddrV2, ADDR_V1_SIZE, ADDR_V2_MIN_SIZE}, + addr::{AddrV1, AddrV2}, inv::{InventoryHash, MAX_INV_IN_RECEIVED_MESSAGE}, }, }; +/// A serialized `addr` (v1) has a 4 byte time, 8 byte services, 16 byte IP addr, and 2 byte port. +const ADDR_V1_SIZE: usize = 4 + 8 + 16 + 2; + +/// Minimum size of a serialized `addrv2`: 4 byte time + 1 byte services + 1 byte networkID +/// + 1 byte sizeAddr + 0 bytes addr + 2 bytes port. +#[allow(clippy::identity_op)] +const ADDR_V2_MIN_SIZE: usize = 4 + 1 + 1 + 1 + 0 + 2; + /// The number of test cases to use for expensive proptests. const DEFAULT_PROPTEST_CASES: u32 = 8; @@ -108,8 +117,12 @@ proptest! { } /// Verifies that... - /// 1. The smallest disallowed vector of `AddrV1`s is too large to fit in a legal Zcash message + /// 1. `max_allocation()` equals the protocol cap (MAX_ADDRS_IN_MESSAGE) /// 2. The largest allowed vector is small enough to fit in a legal Zcash message + /// + /// Note: `max_allocation()` is set to the protocol cap of 1,000 entries + /// (not derived from the message size limit) to prevent resource exhaustion. + /// See GHSA-xr93-pcq3-pxf8. #[test] fn addr_v1_max_allocation_is_correct(addr in MetaAddr::arbitrary()) { let _init_guard = zebra_test::init(); @@ -121,16 +134,14 @@ proptest! { let addr: AddrV1 = addr.unwrap().into(); let ( - smallest_disallowed_vec_len, - smallest_disallowed_serialized_len, + _smallest_disallowed_vec_len, + _smallest_disallowed_serialized_len, largest_allowed_vec_len, largest_allowed_serialized_len, ) = max_allocation_is_big_enough(addr); - // Check that our smallest_disallowed_vec is only one item larger than the limit - prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == AddrV1::max_allocation()); - // Check that our smallest_disallowed_vec is too big to send in a valid Zcash message - prop_assert!(smallest_disallowed_serialized_len > MAX_PROTOCOL_MESSAGE_LEN); + // max_allocation must equal the protocol cap + prop_assert_eq!(AddrV1::max_allocation(), MAX_ADDRS_IN_MESSAGE as u64); // Check that our largest_allowed_vec contains the maximum number of AddrV1s prop_assert!((largest_allowed_vec_len as u64) == AddrV1::max_allocation()); @@ -165,8 +176,12 @@ proptest! { } /// Verifies that... - /// 1. The smallest disallowed vector of `AddrV2`s is too large to fit in a legal Zcash message + /// 1. `max_allocation()` equals the protocol cap (MAX_ADDRS_IN_MESSAGE) /// 2. The largest allowed vector is small enough to fit in a legal Zcash message + /// + /// Note: `max_allocation()` is set to the protocol cap of 1,000 entries + /// (not derived from the message size limit) to prevent resource exhaustion. + /// See GHSA-xr93-pcq3-pxf8. #[test] fn addr_v2_max_allocation_is_correct(addr in MetaAddr::arbitrary()) { let _init_guard = zebra_test::init(); @@ -178,19 +193,17 @@ proptest! { let addr: AddrV2 = addr.unwrap().into(); let ( - smallest_disallowed_vec_len, - smallest_disallowed_serialized_len, + _smallest_disallowed_vec_len, + _smallest_disallowed_serialized_len, largest_allowed_vec_len, _largest_allowed_serialized_len, ) = max_allocation_is_big_enough(addr); - // Check that our smallest_disallowed_vec is only one item larger than the limit - prop_assert!(((smallest_disallowed_vec_len - 1) as u64) == AddrV2::max_allocation()); - // Check that our smallest_disallowed_vec is too big to send in a valid Zcash message - prop_assert!(smallest_disallowed_serialized_len > MAX_PROTOCOL_MESSAGE_LEN); + // max_allocation must equal the protocol cap + prop_assert_eq!(AddrV2::max_allocation(), MAX_ADDRS_IN_MESSAGE as u64); // Check that our largest_allowed_vec contains the maximum number of AddrV2s prop_assert!((largest_allowed_vec_len as u64) == AddrV2::max_allocation()); - // This is a variable-sized type, so largest_allowed_serialized_len can exceed the length limit + // 1,000 entries easily fits in a Zcash message for both fixed and variable-sized types } } diff --git a/zebra-network/src/protocol/external/tests/prop.rs b/zebra-network/src/protocol/external/tests/prop.rs index b852087331a..6a3c6942225 100644 --- a/zebra-network/src/protocol/external/tests/prop.rs +++ b/zebra-network/src/protocol/external/tests/prop.rs @@ -89,7 +89,9 @@ proptest! { fn inv_and_getdata_message_roundtrip( message in prop_oneof!(Message::inv_strategy(), Message::get_data_strategy()), ) { - let mut codec = Codec::builder().finish(); + let mut codec = Codec::builder() + .with_max_body_len(MAX_PROTOCOL_MESSAGE_LEN) + .finish(); let mut bytes = BytesMut::with_capacity(MAX_PROTOCOL_MESSAGE_LEN); let encoding_result = codec.encode(message.clone(), &mut bytes); @@ -120,7 +122,7 @@ proptest! { // // If this is a gossiped or DNS seeder address, // we're also checking that malicious peers can't make Zebra's serialization fail. - let addr_bytes = AddrV1::from(sanitized_addr).zcash_serialize_to_vec(); + let addr_bytes = AddrV1::from(sanitized_addr.clone()).zcash_serialize_to_vec(); prop_assert!( addr_bytes.is_ok(), "unexpected serialization error: {:?}, addr: {:?}", @@ -142,8 +144,8 @@ proptest! { // Check that the addrs are equal prop_assert_eq!( - sanitized_addr, - deserialized_addr, + sanitized_addr.clone(), + deserialized_addr.clone(), "unexpected round-trip mismatch with bytes: {:?}", hex::encode(addr_bytes), ); @@ -153,7 +155,7 @@ proptest! { // Now check that the re-serialized bytes are equal // (`impl PartialEq for MetaAddr` might not match serialization equality) - let addr_bytes2 = AddrV1::from(deserialized_addr).zcash_serialize_to_vec(); + let addr_bytes2 = AddrV1::from(deserialized_addr.clone()).zcash_serialize_to_vec(); prop_assert!( addr_bytes2.is_ok(), "unexpected serialization error after round-trip: {:?}, original addr: {:?}, bytes: {:?}, deserialized addr: {:?}", @@ -193,7 +195,7 @@ proptest! { // // If this is a gossiped or DNS seeder address, // we're also checking that malicious peers can't make Zebra's serialization fail. - let addr_bytes = AddrV2::from(sanitized_addr).zcash_serialize_to_vec(); + let addr_bytes = AddrV2::from(sanitized_addr.clone()).zcash_serialize_to_vec(); prop_assert!( addr_bytes.is_ok(), "unexpected serialization error: {:?}, addr: {:?}", @@ -216,8 +218,8 @@ proptest! { // Check that the addrs are equal prop_assert_eq!( - sanitized_addr, - deserialized_addr, + sanitized_addr.clone(), + deserialized_addr.clone(), "unexpected round-trip mismatch with bytes: {:?}", hex::encode(addr_bytes), ); @@ -227,7 +229,7 @@ proptest! { // Now check that the re-serialized bytes are equal // (`impl PartialEq for MetaAddr` might not match serialization equality) - let addr_bytes2 = AddrV2::from(deserialized_addr).zcash_serialize_to_vec(); + let addr_bytes2 = AddrV2::from(deserialized_addr.clone()).zcash_serialize_to_vec(); prop_assert!( addr_bytes2.is_ok(), "unexpected serialization error after round-trip: {:?}, original addr: {:?}, bytes: {:?}, deserialized addr: {:?}", diff --git a/zebra-network/src/protocol/external/types.rs b/zebra-network/src/protocol/external/types.rs index 4825d93bd1c..2143be84541 100644 --- a/zebra-network/src/protocol/external/types.rs +++ b/zebra-network/src/protocol/external/types.rs @@ -116,8 +116,14 @@ impl Version { 170_130 } (Mainnet, Nu6_1) => 170_140, - (Testnet(params), Nu7) if params.is_default_testnet() || params.is_regtest() => 170_150, - (Mainnet, Nu7) => 170_160, + (Testnet(params), Nu6_2) if params.is_default_testnet() || params.is_regtest() => { + 170_150 + } + (Mainnet, Nu6_2) => 170_150, + // TODO(NU6.2): these Nu7 protocol versions are provisional, bumped above Nu6_2's + // 170_150. Update them when the real Nu7 values are specified. + (Testnet(params), Nu7) if params.is_default_testnet() || params.is_regtest() => 170_160, + (Mainnet, Nu7) => 170_170, // It should be fine to reject peers with earlier network protocol versions on custom testnets for now. (Testnet(_), _) => CURRENT_NETWORK_PROTOCOL_VERSION.0, @@ -223,7 +229,7 @@ mod test { let highest_network_upgrade = NetworkUpgrade::current(network, block::Height::MAX); assert!( - matches!(highest_network_upgrade, Nu6 | Nu6_1 | Nu7), + matches!(highest_network_upgrade, Nu6 | Nu6_1 | Nu6_2 | Nu7), "expected coverage of all network upgrades: \ add the new network upgrade to the list in this test" ); @@ -238,6 +244,7 @@ mod test { Nu5, Nu6, Nu6_1, + Nu6_2, Nu7, ] { let height = network_upgrade.activation_height(network); diff --git a/zebra-network/src/protocol/internal/request.rs b/zebra-network/src/protocol/internal/request.rs index bad1dcdab64..8e02b683a20 100644 --- a/zebra-network/src/protocol/internal/request.rs +++ b/zebra-network/src/protocol/internal/request.rs @@ -6,6 +6,7 @@ use zebra_chain::{ }; use super::super::types::Nonce; +use crate::PeerSocketAddr; #[cfg(any(test, feature = "proptest-impl"))] use proptest_derive::Arbitrary; @@ -164,10 +165,16 @@ pub enum Request { /// The peer set routes this request specially, sending it to *half of* /// the available peers. /// + /// The second field is the address of the peer that sent us this `inv`: + /// `Some(addr)` when the advertisement was relayed from a remote peer, + /// and `None` when Zebra originates the advertisement itself (e.g. the + /// mempool gossip task). Used by the mempool downloader to enforce a + /// per-peer queue cap. See `GHSA-4fc2-h7jh-287c`. + /// /// # Returns /// /// Returns [`Response::Nil`](super::Response::Nil). - AdvertiseTransactionIds(HashSet), + AdvertiseTransactionIds(HashSet, Option), /// Advertise a block to all peers. /// @@ -181,10 +188,16 @@ pub enum Request { /// the available peers. See [`number_of_peers_to_broadcast()`](crate::PeerSet::number_of_peers_to_broadcast) /// for more details. /// + /// The second field is the address of the peer that sent us this `inv`: + /// `Some(addr)` when the advertisement was relayed from a remote peer, + /// and `None` when Zebra originates the advertisement itself (for + /// example from the sync gossip task). Consumers use the address to + /// apply per-peer policies such as the inbound download per-IP cap. + /// /// # Returns /// /// Returns [`Response::Nil`](super::Response::Nil). - AdvertiseBlock(block::Hash), + AdvertiseBlock(block::Hash, Option), /// Advertise a block to all ready peers. This is equivalent to /// [`Request::AdvertiseBlock`] except that the peer set will route @@ -223,11 +236,11 @@ impl fmt::Display for Request { ), Request::PushTransaction(_) => "PushTransaction".to_string(), - Request::AdvertiseTransactionIds(ids) => { + Request::AdvertiseTransactionIds(ids, _) => { format!("AdvertiseTransactionIds({})", ids.len()) } - Request::AdvertiseBlock(_) => "AdvertiseBlock".to_string(), + Request::AdvertiseBlock(_, _) => "AdvertiseBlock".to_string(), Request::AdvertiseBlockToAll(_) => "AdvertiseBlockToAll".to_string(), Request::MempoolTransactionIds => "MempoolTransactionIds".to_string(), }) @@ -248,9 +261,9 @@ impl Request { Request::FindHeaders { .. } => "FindHeaders", Request::PushTransaction(_) => "PushTransaction", - Request::AdvertiseTransactionIds(_) => "AdvertiseTransactionIds", + Request::AdvertiseTransactionIds(_, _) => "AdvertiseTransactionIds", - Request::AdvertiseBlock(_) | Request::AdvertiseBlockToAll(_) => "AdvertiseBlock", + Request::AdvertiseBlock(_, _) | Request::AdvertiseBlockToAll(_) => "AdvertiseBlock", Request::MempoolTransactionIds => "MempoolTransactionIds", } } diff --git a/zebra-network/src/protocol/internal/response.rs b/zebra-network/src/protocol/internal/response.rs index c7b99b015cc..cf8ebfc7a13 100644 --- a/zebra-network/src/protocol/internal/response.rs +++ b/zebra-network/src/protocol/internal/response.rs @@ -90,7 +90,7 @@ impl fmt::Display for Response { Response::Peers(peers) => format!("Peers {{ peers: {} }}", peers.len()), - Response::Pong(duration) => format!("Pong {{ latency: {:?} }}", duration), + Response::Pong(duration) => format!("Pong {{ latency: {duration:?} }}"), Response::BlockHashes(hashes) => format!("BlockHashes {{ hashes: {} }}", hashes.len()), Response::BlockHeaders(headers) => { diff --git a/zebra-node-services/CHANGELOG.md b/zebra-node-services/CHANGELOG.md index d20973fd05e..628de71c69a 100644 --- a/zebra-node-services/CHANGELOG.md +++ b/zebra-node-services/CHANGELOG.md @@ -5,7 +5,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [8.0.0] - 2026-06-10 + +### Changed + +- `zebra-chain` dependency bumped to `10.0.0`. No other changes to this crate. + +## [7.0.0] - 2026-06-02 + +### Changed + +- Update to `zebra-chain` 9.0.0 (NU6.2 support). No other changes to this crate. + +## [6.0.0] - 2026-05-28 + +### Added + +- `mempool::Request::QueueFromPeer { txids: HashSet, source: SocketAddr }` + variant, used to attribute mempool advertisements to the announcing peer for + per-peer queue caps + ([GHSA-4fc2-h7jh-287c](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-4fc2-h7jh-287c)). + +### Changed + +- `zebra-chain` dependency bumped to `8.0.0`. Consumers of re-exported + `zebra-chain` items inherit that crate's breaking changes. + +## [5.0.0] - 2026-05-01 + +### Changed + +- `zebra-chain` bumped to `7.0.0`. No direct public-API changes in this crate, + but consumers of re-exported `zebra-chain` items (e.g. `constants` from + `parameters::checkpoint`) inherit that crate's breaking changes. ## [4.0.0] - 2026-03-12 diff --git a/zebra-node-services/Cargo.toml b/zebra-node-services/Cargo.toml index dbc6ec9bb6f..39feff72576 100644 --- a/zebra-node-services/Cargo.toml +++ b/zebra-node-services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-node-services" -version = "4.0.0" +version = "8.0.0" authors.workspace = true description = "The interfaces of some Zebra node services" license.workspace = true @@ -31,7 +31,7 @@ rpc-client = [ ] [dependencies] -zebra-chain = { path = "../zebra-chain" , version = "6.0.0" } +zebra-chain = { path = "../zebra-chain" , version = "10.1.0" } tower = { workspace = true } # Optional dependencies diff --git a/zebra-node-services/src/mempool.rs b/zebra-node-services/src/mempool.rs index 65135fbae6f..e39b89b5b5d 100644 --- a/zebra-node-services/src/mempool.rs +++ b/zebra-node-services/src/mempool.rs @@ -2,7 +2,7 @@ //! //! A service that manages known unmined Zcash transactions. -use std::collections::HashSet; +use std::{collections::HashSet, net::SocketAddr}; use tokio::sync::oneshot; use zebra_chain::{ @@ -85,6 +85,16 @@ pub enum Request { /// The transaction downloader checks for duplicates across IDs and transactions. Queue(Vec), + /// Queue transaction IDs advertised by a specific peer via an `Inv` + /// message, tagging each one with the announcing peer so the downloader + /// can enforce a per-peer queue cap. See `GHSA-4fc2-h7jh-287c`. + QueueFromPeer { + /// The transaction IDs advertised by the peer. + txids: HashSet, + /// The address of the peer that advertised them. + source: SocketAddr, + }, + /// Check for newly verified transactions. /// /// The transaction downloader does not push transactions into the mempool. diff --git a/zebra-node-services/src/rpc_client.rs b/zebra-node-services/src/rpc_client.rs index f61381b2d21..1b5427e3691 100644 --- a/zebra-node-services/src/rpc_client.rs +++ b/zebra-node-services/src/rpc_client.rs @@ -34,7 +34,7 @@ impl RpcRequestClient { let params = params.as_ref(); self.client - .post(format!("http://{}", &self.rpc_address)) + .post(format!("http://{}", self.rpc_address)) .body(format!( r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"# )) @@ -54,7 +54,7 @@ impl RpcRequestClient { let params = params.as_ref(); self.client - .post(format!("http://{}", &self.rpc_address)) + .post(format!("http://{}", self.rpc_address)) .body(format!( r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"# )) @@ -73,7 +73,7 @@ impl RpcRequestClient { let params = params.as_ref(); self.client - .post(format!("http://{}", &self.rpc_address)) + .post(format!("http://{}", self.rpc_address)) .body(format!( r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"# )) diff --git a/zebra-rpc/CHANGELOG.md b/zebra-rpc/CHANGELOG.md index ae140b559b9..4eefa770b11 100644 --- a/zebra-rpc/CHANGELOG.md +++ b/zebra-rpc/CHANGELOG.md @@ -7,6 +7,153 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.0.1] - 2026-06-18 + +### Changed + +- `zebra-state` dependency bumped to `9.0.1`, and `zebra-consensus` to `9.0.1` + +## [10.0.0] - 2026-06-10 + +### Breaking Changes + +- Changed `client::PeerInfo::new()` to take additional `services`, `lastrecv`, + `banscore`, `subver`, `version`, and `connection_state` parameters. + +### Added + +- `getpeerinfo` RPC response now includes `services`, `lastrecv`, `banscore`, + `subver`, `version`, and `connection_state` fields + ([#10443](https://github.com/ZcashFoundation/zebra/pull/10443)). +- `client::PeerInfo::services()`, `lastrecv()`, `banscore()`, `subver()`, + `version()`, and `connection_state()` accessors. + +### Fixed + +- `getrawmempool(true)`: avoid rebuilding the mempool address index per + transaction, improving performance for large mempools + ([#10599](https://github.com/ZcashFoundation/zebra/pull/10599)). + +## [9.0.0] - 2026-06-02 + +### Changed + +- `getblocktemplate` block proposals now support the NU6.2 network upgrade. + +## [8.0.0] - 2026-05-28 + +This release fixes two RPC security issues: + +- Reject non-ASCII `longpollid` values in the `getblocktemplate` RPC before + fixed-field slicing + ([GHSA-qv2r-v3mx-f4pf](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-qv2r-v3mx-f4pf)). +- Return an `InvalidParameter` error for malformed Sapling receivers in the + `z_listunifiedreceivers` RPC instead of panicking + ([GHSA-c8w6-x74f-vmg3](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-c8w6-x74f-vmg3)). + +### Removed + +- `client::TransactionTemplate::from_coinbase()`; replaced by `new_coinbase()` +- `server::http_request_compatibility::With` trait and its impls on `HttpRequestMiddleware` +- `fetch_state_tip_and_local_time()`; replaced by `fetch_chain_info()` +- `generate_coinbase_and_roots()`; replaced by `TransactionTemplate::new_coinbase()` and `DefaultRoots::from_coinbase()` +- `calculate_miner_fee()`, `standard_coinbase_outputs()`, `fake_coinbase_transaction()` +- `GetBlockTemplateHandler::{miner_address, extra_coinbase_data, set_extra_coinbase_data}()` + +### Changed + +- `client::DefaultRoots::from_coinbase()` return type changed from `Result>` to `Self` +- `client::TransactionTemplate::new_coinbase()` parameter changed from `mempool_txs` to `txs_fee: Amount` +- `BlockTemplateResponse::new_internal` signature reworked: now takes + `(net, precomputed_coinbase: Option>, miner_params: &MinerParams, chain_info: &GetBlockTemplateChainInfo, long_poll_id, mempool_txs, submit_old, [zip233_amount])`. +- `validate_block_proposal` now takes `&Network` instead of `Network`. +- `select_mempool_transactions` signature reworked: now takes + `(net, height, miner_params: &MinerParams, mempool_txs, mempool_tx_deps, [zip233_amount])`; + the `extra_coinbase_data` argument has been removed. +- `GetBlockTemplateHandler::new` no longer panics on an invalid miner address; + it stores `Option` and the request handler returns an error if + miner parameters are missing. +- `long_poll::LongPollId::from_str` now requires ASCII input + ([GHSA-qv2r-v3mx-f4pf](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-qv2r-v3mx-f4pf)). +- `zebra-chain` dependency bumped to `8.0.0`. +- `zebra-consensus` dependency bumped to `7.0.0`. +- `zebra-network` dependency bumped to `7.0.0`. +- `zebra-node-services` dependency bumped to `6.0.0`. +- `zebra-script` dependency bumped to `7.0.0`. +- `zebra-state` dependency bumped to `7.0.0`. + +### Added + +- `client`: + - `DefaultRoots::from_coinbase()` + - `TransactionTemplate::new_coinbase(net, height, miner_params, txs_fee, [zip233_amount])` +- `MinerParams` struct and `MinerParamsError` enum + (`MissingAddr`, `InvalidAddr`, `OversizedData`, `InvalidMemo`): + - `addr()`, `data()`, `memo()`, `new()` + - `randomize_memo()`, `randomize_data()` + - `impl From` +- `config::mining`: + - `MinerAddressType` enum (`Transparent`, `Sapling`, `Unified`) + - `MinerAddressTypeIter` struct (strum-derived iterator for `MinerAddressType`) + - `Config::miner_memo` field + - `default_miner_address()` function +- `methods::types::get_block_template::constants::MAX_MINER_DATA_LEN`. +- `GetBlockTemplateHandler::randomize_coinbase_data(&mut self)` and + `GetBlockTemplateHandler::miner_params(&self) -> Option<&MinerParams>`. +- `fetch_chain_info()`; replacement for removed `fetch_state_tip_and_local_time()`. + +### Fixed + +- `getblocktemplate` now precomputes a coinbase transaction for `tip_height + 2` + and, on a chain-tip change, immediately returns an empty-block template using + that precomputed coinbase + ([#10170](https://github.com/ZcashFoundation/zebra/pull/10170)). + +## [7.0.0] - 2026-05-01 + +This release fixes four RPC security issues: + +- [GHSA-jg86-rwhm-fhg4](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jg86-rwhm-fhg4): cookie file is now written with explicit `0600` permissions on Unix; symlinks at the cookie path are rejected. +- [GHSA-8r29-5wjm-jgvx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8r29-5wjm-jgvx): HTTP request bodies are bounded before allocation, with the limit derived from `MAX_BLOCK_BYTES` to accommodate `submitblock`. +- [GHSA-826r-gfq8-x79q](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-826r-gfq8-x79q): gRPC indexer streams use `try_send` to drop slow subscribers instead of backpressuring the server; the buffer was reduced from 4000 to 64. +- [GHSA-w23c-6rpp-ff87](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-w23c-6rpp-ff87): `getrawtransaction` reuses the caller-provided block hash and best-chain flag from the initial query, fixing a TOCTOU race against a third state lookup. + +### Added + +- `methods::BlockObject::n_tx(&self) -> usize`, exposing the per-block + transaction count in the verbose `getblock` response. + +### Changed + +- Migrated to `zcash_primitives 0.27` (and the rest of the librustzcash 2026-04 + release wave), which replaces the yanked `core2` dependency with `corez`. +- `methods::BlockObject::new` gained a required `n_tx: usize` parameter, + inserted positionally between `final_orchard_root` and `tx`. +- `server::http_request_compatibility`: + - `HttpRequestMiddleware::new` gained a required + `max_request_body_size: usize` parameter. + - `HttpRequestMiddlewareLayer::new` gained a required + `max_request_body_size: usize` parameter. + Both bound HTTP request bodies before allocation; see + [GHSA-8r29-5wjm-jgvx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8r29-5wjm-jgvx). + +## [6.0.2] - 2026-04-17 + +### Fixed + +- Fixed a [panic that could be triggered in the RPC interface on HTTP + errors](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-29x4-r6jv-ff4w), + such as resetting the connection halfway through a request. We do not consider + this a critical issue since the RPC port is security-sensitive and should not + be opened publicly, but we plan to update our documentation to make this + clear. + +## [6.0.1] - 2026-03-26 + +### Fixed + +- Fixed the computation of miner rewards in `getblocksubsidy` RPC + ## [6.0.0] - 2026-03-12 ### Breaking Changes diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 492d35168d9..1feb20cd907 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-rpc" -version = "6.0.0" +version = "10.0.1" authors.workspace = true description = "A Zebra JSON Remote Procedure Call (JSON-RPC) interface" license.workspace = true @@ -24,7 +24,7 @@ categories = [ exclude = ["*.proto"] [features] -tx_v6 = ["zebra-chain/tx_v6", "zebra-state/tx_v6", "zebra-consensus/tx_v6"] +tx_v6 = ["zebra-chain/tx_v6", "zebra-state/tx_v6", "zebra-consensus/tx_v6", "zcash_primitives/zip-233"] # Production features that activate extra dependencies, or extra features in # dependencies @@ -57,6 +57,10 @@ serde_with = { workspace = true, features = ["hex"] } indexmap = { workspace = true, features = ["serde"] } derive-getters = { workspace = true, features = ["auto_copy_getters"] } derive-new = { workspace = true } +thiserror = { workspace = true } +lazy_static = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } # RPC endpoint basic auth base64 = { workspace = true } @@ -91,28 +95,30 @@ openrpsee = { workspace = true } phf = { workspace = true, features = ["macros"] } schemars = { workspace = true } +orchard = { workspace = true } +sapling-crypto = { workspace = true } + zcash_address = { workspace = true } -zcash_keys = { workspace = true } -zcash_primitives = { workspace = true, features = ["transparent-inputs"] } +zcash_keys = { workspace = true, features = ["orchard", "sapling"] } +zcash_primitives = { workspace = true, features = ["transparent-inputs", "non-standard-fees"] } +zcash_proofs = { workspace = true } zcash_protocol = { workspace = true } zcash_script = { workspace = true } zcash_transparent = { workspace = true } -sapling-crypto = { workspace = true } - # Test-only feature proptest-impl proptest = { workspace = true, optional = true } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = [ +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = [ "json-conversion", ] } -zebra-consensus = { path = "../zebra-consensus", version = "5.0.0" } -zebra-network = { path = "../zebra-network", version = "5.0.0" } -zebra-node-services = { path = "../zebra-node-services", version = "4.0.0", features = [ +zebra-consensus = { path = "../zebra-consensus", version = "9.0.1" } +zebra-network = { path = "../zebra-network", version = "9.0.0" } +zebra-node-services = { path = "../zebra-node-services", version = "8.0.0", features = [ "rpc-client", ] } -zebra-script = { path = "../zebra-script", version = "5.0.0" } -zebra-state = { path = "../zebra-state", version = "5.0.0" } +zebra-script = { path = "../zebra-script", version = "9.0.0" } +zebra-state = { path = "../zebra-state", version = "9.0.1" } [build-dependencies] openrpsee = { workspace = true } @@ -120,23 +126,26 @@ tonic-prost-build = { workspace = true } which = { workspace = true } [dev-dependencies] +bytes = { workspace = true } +http-body = { workspace = true } insta = { workspace = true, features = ["redactions", "json", "ron"] } +anyhow = { workspace = true } +tempfile = { workspace = true } proptest = { workspace = true } -thiserror = { workspace = true } tokio = { workspace = true, features = ["full", "tracing", "test-util"] } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = [ +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = [ "proptest-impl", ] } -zebra-consensus = { path = "../zebra-consensus", version = "5.0.0", features = [ +zebra-consensus = { path = "../zebra-consensus", version = "9.0.1", features = [ "proptest-impl", ] } -zebra-network = { path = "../zebra-network", version = "5.0.0", features = [ +zebra-network = { path = "../zebra-network", version = "9.0.0", features = [ "proptest-impl", ] } -zebra-state = { path = "../zebra-state", version = "5.0.0", features = [ +zebra-state = { path = "../zebra-state", version = "9.0.1", features = [ "proptest-impl", ] } diff --git a/zebra-rpc/src/config/mining.rs b/zebra-rpc/src/config/mining.rs index 7db21086ff2..37197a7ca55 100644 --- a/zebra-rpc/src/config/mining.rs +++ b/zebra-rpc/src/config/mining.rs @@ -1,9 +1,13 @@ //! Mining config +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; +use strum_macros::EnumIter; use zcash_address::ZcashAddress; +use zebra_chain::parameters::NetworkKind; /// Mining configuration section. #[serde_as] @@ -16,16 +20,19 @@ pub struct Config { #[serde_as(as = "Option")] pub miner_address: Option, - // TODO: Internal miner config code was removed as part of https://github.com/ZcashFoundation/zebra/issues/8180 - // Find the removed code at https://github.com/ZcashFoundation/zebra/blob/v1.5.1/zebra-rpc/src/config/mining.rs#L18-L38 - // Restore the code when conditions are met. https://github.com/ZcashFoundation/zebra/issues/8183 - /// Extra data to include in coinbase transaction inputs. - /// Limited to around 95 bytes by the consensus rules. + /// Optional data that Zebra will include in the transparent input of a coinbase transaction. + /// Limited to 94 bytes. /// - /// If this string is hex-encoded, it will be hex-decoded into bytes. - /// Otherwise, it will be UTF-8 encoded into bytes. + /// If this string is hex-encoded, it will be hex-decoded into bytes. Otherwise, it will be + /// UTF-8 encoded into bytes. pub extra_coinbase_data: Option, + /// Optional shielded memo that Zebra will include in the output of a shielded coinbase + /// transaction. Limited to 512 bytes. + /// + /// Applies only if [`Self::miner_address`] contains a shielded component. + pub miner_memo: Option, + /// Mine blocks using Zebra's internal miner, without an external mining pool or equihash solver. /// /// This experimental feature is only supported on regtest as it uses null solutions and skips checking @@ -46,3 +53,48 @@ impl Config { self.internal_miner } } + +/// The desired address type for the `mining.miner_address` field in the config. +#[derive(EnumIter, Eq, PartialEq, Default, Hash)] +pub enum MinerAddressType { + /// A unified address, containing the components of all the other address types. + Unified, + /// A Sapling address. + Sapling, + /// A transparent address. + #[default] + Transparent, +} + +/// Returns the hard-coded default miner address string for a given network and address type. +/// +/// All addresses come from a single address: +/// +/// - addresses for different networks are only different encodings of the same address; +/// - addresses of different types are components of the same unified address. +pub fn default_miner_address(kind: NetworkKind, addr_type: &MinerAddressType) -> &'static str { + MINER_ADDRESS[&kind][addr_type] +} + +lazy_static::lazy_static! { + static ref MINER_ADDRESS: HashMap> = [ + (NetworkKind::Mainnet, [ + (MinerAddressType::Unified, "u1cymdny2u2vllkx7t5jnelp0kde0dgnwu0jzmggzguxvxj6fe7gpuqehywejndlrjwgk9snr6g69azs8jfet78s9zy60uepx6tltk7ee57jlax49dezkhkgvjy2puuue6dvaevt53nah7t2cc2k4p0h0jxmlu9sx58m2xdm5f9sy2n89jdf8llflvtml2ll43e334avu2fwytuna404a"), + // (MinerAddressType::Orchard, "u1hmfjpqdxaec3mvqypl7fkqcy53u438csydljpuepsfs7jx6sjwyznuzlna8qsslj3tg6sn9ua4q653280aqv4m2fjd4csptwxq3fjpwy"), + (MinerAddressType::Sapling, "zs1xl84ekz6stprmvrp39s77mf9t953nqjndwlcjtzfrr3cgjjez87639xm4u9pfuvylrhec3uryy5"), + (MinerAddressType::Transparent, "t1T92bmyoPM7PTSWUnaWnLGcxkF6Jp1AwMY"), + ].into()), + (NetworkKind::Testnet, [ + (MinerAddressType::Unified, "utest10a8k6aw5w33kvyt7x6fryzu7vvsjru5vgcfnvr288qx2zm6p63ygcajtaze0px08t583dyrgr42vasazjhhnntus2tqrpkzu0dm2l4cgf3ld6wdqdrf3jv8mvfx9c80e73syer9l2wlgawjtf7yvj0eqwdf354trtelxnr0fhpw9792eaf49ghstkyftc9lwqqwy4ye0cleagp4nzyt"), + // (MinerAddressType::Orchard, "utest10zg6frxk32ma8980kdv9473e4aclw7clq9hydzcj6l349pkqzxk2mmj3cn7j5x38w6l4wyryv50whnlrw0k9agzpdf5fxyj7kq96ukcp"), + (MinerAddressType::Sapling, "ztestsapling1xl84ekz6stprmvrp39s77mf9t953nqjndwlcjtzfrr3cgjjez87639xm4u9pfuvylrhecet38rq"), + (MinerAddressType::Transparent, "tmJymvcUCn1ctbghvTJpXBwHiMEB8P6wxNV"), + ].into()), + (NetworkKind::Regtest, [ + (MinerAddressType::Unified, "uregtest1efxggx6lduhm2fx5lnrhxv7h7kpztlpa3ahf3n4w0q0zj5epj4av9xjq6ljsja3xk8z7rzd067kc7mgpy9448rdfzpfjz5gq389zdmpgnk6rp4ykk0xk6cmqw6zqcrnmsuaxv3yzsvcwsd4gagtalh0uzrdvy03nhmltjz2eu0232qlcs0zvxuqyut73yucd9gy5jaudnyt7yqhgpqv"), + // (MinerAddressType::Orchard, "uregtest1pszqlgxaf5w8mu2yd9uygg8cswp0ec4f7eejqnqc35tztw4tk0sxnt3pym2f3s2872cy2ruuc5n8y9cen5q6ngzlmzu8ztrjesv8zm9j"), + (MinerAddressType::Sapling, "zregtestsapling1xl84ekz6stprmvrp39s77mf9t953nqjndwlcjtzfrr3cgjjez87639xm4u9pfuvylrhecx0c2j8"), + (MinerAddressType::Transparent, "tmJymvcUCn1ctbghvTJpXBwHiMEB8P6wxNV"), + ].into()), + ].into(); +} diff --git a/zebra-rpc/src/indexer.rs b/zebra-rpc/src/indexer.rs index 2b23bd9f6a1..4382e103240 100644 --- a/zebra-rpc/src/indexer.rs +++ b/zebra-rpc/src/indexer.rs @@ -38,7 +38,7 @@ impl BlockHashAndHeight { ) }) .ok() - .and_then(|hash| self.height.try_into().ok().map(|height| (hash, height))) + .zip(self.height.try_into().ok()) } } @@ -74,7 +74,7 @@ impl BlockAndHash { .zcash_deserialize_into() .map_err(|err| tracing::warn!(?err, "failed to deserialize block",)) .ok() - .map(|block| (block, hash)) + .zip(Some(hash)) }) } } diff --git a/zebra-rpc/src/indexer/methods.rs b/zebra-rpc/src/indexer/methods.rs index ea7241530fc..f2fd56fc9c0 100644 --- a/zebra-rpc/src/indexer/methods.rs +++ b/zebra-rpc/src/indexer/methods.rs @@ -17,8 +17,8 @@ use super::{ MempoolChangeMessage, }; -/// The maximum number of messages that can be queued to be streamed to a client -const RESPONSE_BUFFER_SIZE: usize = 4_000; +/// The maximum number of messages that can be queued to be streamed to a client. +const RESPONSE_BUFFER_SIZE: usize = 64; #[tonic::async_trait] impl Indexer for IndexerRPC @@ -50,14 +50,20 @@ where continue; }; - if let Err(error) = response_sender - .send(Ok(BlockHashAndHeight::new(tip_hash, tip_height))) - .await - { - span.in_scope(|| { - tracing::info!(?error, "failed to send chain tip change, dropping task"); - }); - return; + match response_sender.try_send(Ok(BlockHashAndHeight::new(tip_hash, tip_height))) { + Ok(()) => {} + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + span.in_scope(|| { + tracing::info!("client disconnected, dropping chain_tip_change task"); + }); + return; + } + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + span.in_scope(|| { + tracing::warn!("slow consumer, dropping chain_tip_change stream"); + }); + return; + } } } @@ -110,17 +116,24 @@ where // Notify the client of chain tip changes until the channel is closed while let Some((hash, block)) = non_finalized_state_change.recv().await { - if let Err(error) = response_sender - .send(Ok(BlockAndHash::new(hash, block))) - .await - { - span.in_scope(|| { - tracing::info!( - ?error, - "failed to send non-finalized state change, dropping task" - ); - }); - return; + match response_sender.try_send(Ok(BlockAndHash::new(hash, block))) { + Ok(()) => {} + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + span.in_scope(|| { + tracing::info!( + "client disconnected, dropping non_finalized_state_change task" + ); + }); + return; + } + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + span.in_scope(|| { + tracing::warn!( + "slow consumer, dropping non_finalized_state_change stream" + ); + }); + return; + } } } @@ -155,25 +168,33 @@ where tracing::debug!("mempool change: {:?}", change); }); - if let Err(error) = response_sender - .send(Ok(MempoolChangeMessage { - change_type: match change.kind() { - MempoolChangeKind::Added => 0, - MempoolChangeKind::Invalidated => 1, - MempoolChangeKind::Mined => 2, - }, - tx_hash: tx_id.mined_id().bytes_in_display_order().to_vec(), - auth_digest: tx_id - .auth_digest() - .map(|d| d.bytes_in_display_order().to_vec()) - .unwrap_or_default(), - })) - .await - { - span.in_scope(|| { - tracing::info!(?error, "failed to send mempool change, dropping task"); - }); - return; + let msg = Ok(MempoolChangeMessage { + change_type: match change.kind() { + MempoolChangeKind::Added => 0, + MempoolChangeKind::Invalidated => 1, + MempoolChangeKind::Mined => 2, + }, + tx_hash: tx_id.mined_id().bytes_in_display_order().to_vec(), + auth_digest: tx_id + .auth_digest() + .map(|d| d.bytes_in_display_order().to_vec()) + .unwrap_or_default(), + }); + + match response_sender.try_send(msg) { + Ok(()) => {} + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + span.in_scope(|| { + tracing::info!("client disconnected, dropping mempool_change task"); + }); + return; + } + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + span.in_scope(|| { + tracing::warn!("slow consumer, dropping mempool_change stream"); + }); + return; + } } } } diff --git a/zebra-rpc/src/lib.rs b/zebra-rpc/src/lib.rs index f2a4fdeff79..a1c797d0c0d 100644 --- a/zebra-rpc/src/lib.rs +++ b/zebra-rpc/src/lib.rs @@ -16,9 +16,6 @@ pub mod sync; mod tests; pub use methods::types::{ - get_block_template::{ - fetch_state_tip_and_local_time, generate_coinbase_and_roots, - proposal::proposal_block_from_template, - }, + get_block_template::{fetch_chain_info, proposal::proposal_block_from_template, MinerParams}, submit_block::SubmitBlockChannel, }; diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index df8d1645c7b..ff2927c9f6c 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -49,7 +49,6 @@ use indexmap::IndexMap; use jsonrpsee::core::{async_trait, RpcResult as Result}; use jsonrpsee_proc_macros::rpc; use jsonrpsee_types::{ErrorCode, ErrorObject}; -use rand::{rngs::OsRng, RngCore}; use schemars::JsonSchema; use tokio::{ sync::{broadcast, mpsc, watch}, @@ -59,15 +58,17 @@ use tower::ServiceExt; use tracing::Instrument; use zcash_address::{unified::Encoding, TryFromAddress}; -use zcash_protocol::consensus::Parameters; - +use zcash_protocol::consensus::{self, Parameters}; use zebra_chain::{ amount::{Amount, NegativeAllowed}, block::{self, Block, Commitment, Height, SerializedBlock, TryIntoHeight}, chain_sync_status::ChainSyncStatus, chain_tip::{ChainTip, NetworkChainTipHeightEstimator}, parameters::{ - subsidy::{block_subsidy, funding_stream_values, miner_subsidy, FundingStreamReceiver}, + subsidy::{ + block_subsidy, founders_reward, funding_stream_values, miner_subsidy, + FundingStreamReceiver, + }, ConsensusBranchId, Network, NetworkUpgrade, POW_AVERAGING_WINDOW, }, serialization::{BytesInDisplayOrder, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}, @@ -91,6 +92,7 @@ use zebra_state::{ }; use crate::{ + client::TransactionTemplate, client::Treestate, config, methods::types::{ @@ -1446,6 +1448,7 @@ where solution: Some(solution), bits: Some(bits), difficulty: Some(difficulty), + n_tx: tx.len(), tx, trees, chain_supply: block_info @@ -1661,6 +1664,10 @@ where last_seen_tip_hash: _, } => { if verbose { + let transactions_by_id = transactions + .iter() + .map(|unmined_tx| (unmined_tx.transaction.id.mined_id(), unmined_tx)) + .collect::>(); let map = transactions .iter() .map(|unmined_tx| { @@ -1668,7 +1675,7 @@ where unmined_tx.transaction.id.mined_id().encode_hex(), get_raw_mempool::MempoolObject::from_verified_unmined_tx( unmined_tx, - &transactions, + &transactions_by_id, &transaction_dependencies, ), ) @@ -1765,7 +1772,7 @@ where }; } - let txid = if let Some(block_hash) = block_hash { + let caller_block_context = if let Some(block_hash) = block_hash { let block_hash = block::Hash::from_hex(block_hash) .map_error(server::error::LegacyCode::InvalidAddressOrKey)?; match self @@ -1777,24 +1784,25 @@ where .await .map_misc_error()? { - zebra_state::ReadResponse::AnyChainTransactionIdsForBlock(tx_ids) => *tx_ids - .ok_or_error( + zebra_state::ReadResponse::AnyChainTransactionIdsForBlock(tx_ids) => { + let (ids, in_best_chain) = tx_ids.ok_or_error( server::error::LegacyCode::InvalidAddressOrKey, "block not found", - )? - .0 - .iter() - .find(|id| **id == txid) - .ok_or_error( + )?; + + ids.iter().find(|id| **id == txid).ok_or_error( server::error::LegacyCode::InvalidAddressOrKey, "txid not found", - )?, + )?; + + Some((block_hash, in_best_chain)) + } _ => { unreachable!("unmatched response to a `AnyChainTransactionIdsForBlock` request") } } } else { - txid + None }; // If the tx wasn't in the mempool, check the state. @@ -1806,48 +1814,80 @@ where .map_misc_error()? { zebra_state::ReadResponse::AnyChainTransaction(Some(tx)) => Ok(if verbose { - match tx { - AnyTx::Mined(tx) => { - let block_hash = match self - .read_state - .clone() - .oneshot(zebra_state::ReadRequest::BestChainBlockHash(tx.height)) - .await - .map_misc_error()? - { - zebra_state::ReadResponse::BlockHash(block_hash) => block_hash, - _ => { - unreachable!("unmatched response to a `BestChainBlockHash` request") - } - }; - - GetRawTransactionResponse::Object(Box::new( - TransactionObject::from_transaction( - tx.tx.clone(), - Some(tx.height), - Some(tx.confirmations), - &self.network, - // TODO: Performance gain: - // https://github.com/ZcashFoundation/zebra/pull/9458#discussion_r2059352752 - Some(tx.block_time), - block_hash, - Some(true), - txid, - ), - )) - } - AnyTx::Side((tx, block_hash)) => GetRawTransactionResponse::Object(Box::new( + if let Some((caller_block_hash, in_best_chain)) = caller_block_context { + // Use the caller-provided block context to avoid TOCTOU races + // between the validation query and the transaction fetch. + let (raw_tx, height, confirmations, block_time) = match &tx { + AnyTx::Mined(mined) if in_best_chain => ( + mined.tx.clone(), + Some(mined.height), + Some(mined.confirmations), + Some(mined.block_time), + ), + _ => { + let raw_tx: Arc = tx.into(); + (raw_tx, None, None, None) + } + }; + + GetRawTransactionResponse::Object(Box::new( TransactionObject::from_transaction( - tx.clone(), - None, - None, + raw_tx, + height, + confirmations, &self.network, - None, - Some(block_hash), - Some(false), + block_time, + Some(caller_block_hash), + Some(in_best_chain), txid, ), - )), + )) + } else { + match tx { + AnyTx::Mined(tx) => { + let block_hash = match self + .read_state + .clone() + .oneshot(zebra_state::ReadRequest::BestChainBlockHash(tx.height)) + .await + .map_misc_error()? + { + zebra_state::ReadResponse::BlockHash(block_hash) => block_hash, + _ => { + unreachable!( + "unmatched response to a `BestChainBlockHash` request" + ) + } + }; + + GetRawTransactionResponse::Object(Box::new( + TransactionObject::from_transaction( + tx.tx.clone(), + Some(tx.height), + Some(tx.confirmations), + &self.network, + // TODO: Performance gain: + // https://github.com/ZcashFoundation/zebra/pull/9458#discussion_r2059352752 + Some(tx.block_time), + block_hash, + Some(true), + txid, + ), + )) + } + AnyTx::Side((tx, block_hash)) => GetRawTransactionResponse::Object( + Box::new(TransactionObject::from_transaction( + tx.clone(), + None, + None, + &self.network, + None, + Some(block_hash), + Some(false), + txid, + )), + ), + } } } else { let tx: Arc = tx.into(); @@ -1909,8 +1949,7 @@ where let time = u32::try_from(block.header.time.timestamp()) .expect("Timestamps of valid blocks always fit into u32."); - let sapling_nu = zcash_protocol::consensus::NetworkUpgrade::Sapling; - let sapling = if network.is_nu_active(sapling_nu, height.into()) { + let sapling = if network.is_nu_active(consensus::NetworkUpgrade::Sapling, height.into()) { match read_state .ready() .and_then(|service| { @@ -1930,8 +1969,7 @@ where let (sapling_tree, sapling_root) = sapling.map_or((None, None), |(tree, root)| (Some(tree), Some(root))); - let orchard_nu = zcash_protocol::consensus::NetworkUpgrade::Nu5; - let orchard = if network.is_nu_active(orchard_nu, height.into()) { + let orchard = if network.is_nu_active(consensus::NetworkUpgrade::Nu5, height.into()) { match read_state .ready() .and_then(|service| { @@ -2269,15 +2307,10 @@ where parameters: Option, ) -> Result { use types::get_block_template::{ - check_parameters, check_synced_to_tip, fetch_mempool_transactions, - fetch_state_tip_and_local_time, validate_block_proposal, - zip317::select_mempool_transactions, + check_parameters, check_synced_to_tip, fetch_chain_info, fetch_mempool_transactions, + validate_block_proposal, zip317::select_mempool_transactions, }; - // Clone Configs - let network = self.network.clone(); - let extra_coinbase_data = self.gbt.extra_coinbase_data(); - // Clone Services let mempool = self.mempool.clone(); let mut latest_chain_tip = self.latest_chain_tip.clone(); @@ -2291,7 +2324,7 @@ where return validate_block_proposal( self.gbt.block_verifier_router(), block_proposal_bytes, - network, + &self.network, latest_chain_tip, sync_status, ) @@ -2303,31 +2336,24 @@ where let client_long_poll_id = parameters.as_ref().and_then(|params| params.long_poll_id); - let miner_address = self + let miner_params = self .gbt - .miner_address() - .ok_or_misc_error("miner_address not configured")?; + .miner_params() + .ok_or_error(0, "miner parameters are required for get_block_template")?; // - Checks and fetches that can change during long polling // // Set up the loop. let mut max_time_reached = false; - // The loop returns the server long poll ID, - // which should be different to the client long poll ID. - let ( - server_long_poll_id, - chain_tip_and_local_time, - mempool_txs, - mempool_tx_deps, - submit_old, - ) = loop { + // The loop returns the server long poll ID, which should be different to the client one. + let (server_long_poll_id, chain_info, mempool_txs, mempool_tx_deps, submit_old) = loop { // Check if we are synced to the tip. // The result of this check can change during long polling. // // Optional TODO: // - add `async changed()` method to ChainSyncStatus (like `ChainTip`) - check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?; + check_synced_to_tip(&self.network, latest_chain_tip.clone(), sync_status.clone())?; // TODO: return an error if we have no peers, like `zcashd` does, // and add a developer config that mines regardless of how many peers we have. // https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/miner.cpp#L865-L880 @@ -2343,13 +2369,13 @@ where // // We always return after 90 minutes on mainnet, even if we have the same response, // because the max time has been reached. - let chain_tip_and_local_time @ zebra_state::GetBlockTemplateChainInfo { + let chain_info @ zebra_state::GetBlockTemplateChainInfo { tip_hash, tip_height, max_time, cur_time, .. - } = fetch_state_tip_and_local_time(read_state.clone()).await?; + } = fetch_chain_info(read_state.clone()).await?; // Fetch the mempool data for the block template: // - if the mempool transactions change, we might return from long polling. @@ -2386,21 +2412,20 @@ where // - the server long poll ID is different to the client long poll ID, or // - the previous loop iteration waited until the max time. if Some(&server_long_poll_id) != client_long_poll_id.as_ref() || max_time_reached { - let mut submit_old = client_long_poll_id - .as_ref() - .map(|old_long_poll_id| server_long_poll_id.submit_old(old_long_poll_id)); - - // On testnet, the max time changes the block difficulty, so old shares are - // invalid. On mainnet, this means there has been 90 minutes without a new - // block or mempool transaction, which is very unlikely. So the miner should - // probably reset anyway. - if max_time_reached { - submit_old = Some(false); - } + // On testnet, the max time changes the block difficulty, so old shares are invalid. + // On mainnet, this means there has been 90 minutes without a new block or mempool + // transaction, which is very unlikely. So the miner should probably reset anyway. + let submit_old = if max_time_reached { + Some(false) + } else { + client_long_poll_id + .as_ref() + .map(|old_long_poll_id| server_long_poll_id.submit_old(old_long_poll_id)) + }; break ( server_long_poll_id, - chain_tip_and_local_time, + chain_info, mempool_txs, mempool_tx_deps, submit_old, @@ -2421,8 +2446,47 @@ where // Return immediately if the chain tip has changed. // The clone preserves the seen status of the chain tip. - let mut wait_for_best_tip_change = latest_chain_tip.clone(); - let wait_for_best_tip_change = wait_for_best_tip_change.best_tip_changed(); + let mut wait_for_new_tip = latest_chain_tip.clone(); + let wait_for_new_tip = wait_for_new_tip.best_tip_changed(); + // `+2`: we expect the tip to advance by one block before waking us up. + let precomputed_height = Height(chain_info.tip_height.0 + 2); + let wait_for_new_tip = async { + // Precompute the coinbase tx for an empty block that will sit on the new tip. We + // will return this provisional block upon a chain tip change so that miners can + // mine on the newest tip, and don't waste their effort on a shorter chain while we + // compute a new template for a properly filled block. We do this precomputation + // before we start waiting for a new tip since computing the coinbase tx takes a few + // seconds if the miner mines to a shielded address, and we want to return fast + // when the tip changes. + let precompute_coinbase = |network, height, params| { + tokio::task::spawn_blocking(move || { + TransactionTemplate::new_coinbase( + &network, + height, + ¶ms, + Amount::zero(), + // `None` defaults to a zero burn, which is correct: the ZIP 233 + // burn is a fraction of the block's fees, and this coinbase is + // for an empty block (`txs_fee` is zero above). + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + None, + ) + .expect("valid coinbase tx") + }) + }; + + let precomputed_coinbase = precompute_coinbase( + self.network.clone(), + precomputed_height, + miner_params.clone(), + ) + .await + .expect("valid coinbase tx"); + + let _ = wait_for_new_tip.await; + + precomputed_coinbase + }; // Wait for the maximum block time to elapse. This can change the block header // on testnet. (On mainnet it can happen due to a network disconnection, or a @@ -2466,59 +2530,46 @@ where ); } - // The state changes after around a target block interval (75s) - tip_changed_result = wait_for_best_tip_change => { - match tip_changed_result { - Ok(()) => { - // Spurious updates shouldn't happen in the state, because the - // difficulty and hash ordering is a stable total order. But - // since they could cause a busy-loop, guard against them here. - latest_chain_tip.mark_best_tip_seen(); - - let new_tip_hash = latest_chain_tip.best_tip_hash(); - if new_tip_hash == Some(tip_hash) { - tracing::debug!( - ?max_time, - ?cur_time, - ?server_long_poll_id, - ?client_long_poll_id, - ?tip_hash, - ?tip_height, - "ignoring spurious state change notification" - ); - - // Wait for the mempool interval, then check for any changes. - tokio::time::sleep(Duration::from_secs( - MEMPOOL_LONG_POLL_INTERVAL, - )).await; - - continue; - } - - tracing::debug!( - ?max_time, - ?cur_time, - ?server_long_poll_id, - ?client_long_poll_id, - "returning from long poll because state has changed" - ); - } + precomputed_coinbase = wait_for_new_tip => { + let chain_info = fetch_chain_info(read_state.clone()).await?; - Err(recv_error) => { - // This log is rare and helps with debugging, so it's ok to be info. - tracing::info!( - ?recv_error, - ?max_time, - ?cur_time, - ?server_long_poll_id, - ?client_long_poll_id, - "returning from long poll due to a state error.\ - Is Zebra shutting down?" - ); - - return Err(recv_error).map_error(server::error::LegacyCode::default()); - } - } + let server_long_poll_id = LongPollInput::new( + chain_info.tip_height, + chain_info.tip_hash, + chain_info.max_time, + vec![] + ) + .generate_id(); + + let submit_old = client_long_poll_id + .as_ref() + .map(|old_long_poll_id| server_long_poll_id.submit_old(old_long_poll_id)); + + // Discard the precomputed coinbase if our `+2` guess was wrong + // (multi-block advance, reorg, or spurious notification) — its + // BIP-34 height and subsidies wouldn't match the block. + let next_height = chain_info.tip_height.next().map_misc_error()?; + let precomputed_coinbase = (next_height == precomputed_height) + .then_some(precomputed_coinbase); + + // Respond instantly with an empty block upon a chain tip change so that + // the miner doesn't waste their effort trying to extend a shorter + // chain. + return Ok(BlockTemplateResponse::new_internal( + &self.network, + precomputed_coinbase, + miner_params, + &chain_info, + server_long_poll_id, + vec![], + submit_old, + // `None` defaults to a zero burn, which is correct: the ZIP 233 burn + // is a fraction of the block's fees, and this template has no + // transactions (see the empty list above). + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + None + ) + .into()) } // The max time does not elapse during normal operation on mainnet, @@ -2544,10 +2595,6 @@ where // the template only depends on the previously fetched data. // This processing never fails. - // Calculate the next block height. - let next_block_height = - (chain_tip_and_local_time.tip_height + 1).expect("tip is far below Height::MAX"); - tracing::debug!( mempool_tx_hashes = ?mempool_txs .iter() @@ -2556,14 +2603,15 @@ where "selecting transactions for the template from the mempool" ); + let height = chain_info.tip_height.next().map_misc_error()?; + // Randomly select some mempool transactions. let mempool_txs = select_mempool_transactions( - &network, - next_block_height, - &miner_address, + &self.network, + height, + miner_params, mempool_txs, mempool_tx_deps, - extra_coinbase_data.clone(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, ); @@ -2578,19 +2626,18 @@ where // - After this point, the template only depends on the previously fetched data. - let response = BlockTemplateResponse::new_internal( - &network, - &miner_address, - &chain_tip_and_local_time, + Ok(BlockTemplateResponse::new_internal( + &self.network, + None, + miner_params, + &chain_info, server_long_poll_id, mempool_txs, submit_old, - extra_coinbase_data, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, - ); - - Ok(response.into()) + ) + .into()) } async fn submit_block( @@ -2898,7 +2945,7 @@ where miner: miner_subsidy(height, &net, subsidy) .map_misc_error()? .into(), - founders: Amount::zero().into(), + founders: founders_reward(&net, height).into(), funding_streams, lockbox_streams, funding_streams_total: funding_streams_total?, @@ -2937,7 +2984,7 @@ where } zcash_address::unified::Receiver::Sapling(data) => { let addr = zebra_chain::primitives::Address::try_from_sapling(network, data) - .expect("using data already decoded as valid"); + .map_error(server::error::LegacyCode::InvalidParameter)?; sapling = Some(addr.payment_address().unwrap_or_default()); } zcash_address::unified::Receiver::P2pkh(data) => { @@ -3004,14 +3051,10 @@ where let mut block_hashes = Vec::new(); for _ in 0..num_blocks { - // Use random coinbase data in order to ensure the coinbase - // transaction is unique. This is useful for tests that exercise - // forks, since otherwise the coinbase txs of blocks with the same - // height across different forks would be identical. - let mut extra_coinbase_data = [0u8; 32]; - OsRng.fill_bytes(&mut extra_coinbase_data); - rpc.gbt - .set_extra_coinbase_data(extra_coinbase_data.to_vec()); + // Use random coinbase data in order to ensure the coinbase transaction is unique. This + // is useful for tests that exercise forks, since otherwise the coinbase txs of blocks + // with the same height across different forks would be identical. + rpc.gbt.randomize_coinbase_data(); let block_template = rpc .get_block_template(None) @@ -3048,7 +3091,7 @@ where SubmitBlockResponse::ErrorResponse(response) => { return Err(ErrorObject::owned( server::error::LegacyCode::Misc.into(), - format!("block was rejected: {:?}", response), + format!("block was rejected: {response:?}"), None::<()>, )); } @@ -3863,6 +3906,7 @@ impl Default for GetBlockResponse { confirmations: 0, height: None, time: None, + n_tx: 0, tx: Vec::new(), trees: GetBlockTrees::default(), size: None, @@ -3940,6 +3984,10 @@ pub struct BlockObject { // `chainhistoryroot` would be here. Undocumented. TODO: decide if we want to support it // + /// The number of transactions in this block. + #[serde(rename = "nTx")] + n_tx: usize, + /// List of transactions in block order, hex-encoded if verbosity=1 or /// as objects if verbosity=2. tx: Vec, @@ -4324,7 +4372,7 @@ impl Utxo { Height, ) { ( - self.address.clone(), + self.address, self.txid, self.output_index, self.script.clone(), diff --git a/zebra-rpc/src/methods/tests/prop.rs b/zebra-rpc/src/methods/tests/prop.rs index 4f1b28dfd4d..ff6a8c2fc73 100644 --- a/zebra-rpc/src/methods/tests/prop.rs +++ b/zebra-rpc/src/methods/tests/prop.rs @@ -48,6 +48,8 @@ proptest! { /// Test that when sending a raw transaction, it is received by the mempool service. #[test] fn mempool_receives_raw_tx(transaction in any::(), network in any::()) { + prop_assume!(!(transaction.is_coinbase() && transaction.sapling_spends_per_anchor().count() > 0)); + let (runtime, _init_guard) = zebra_test::init_async(); let _guard = runtime.enter(); let (mut mempool, mut state, rpc, mempool_tx_queue) = mock_services(network, NoChainTip); @@ -93,6 +95,8 @@ proptest! { /// Mempool service errors should become server errors. #[test] fn mempool_errors_are_forwarded(transaction in any::(), network in any::()) { + prop_assume!(!(transaction.is_coinbase() && transaction.sapling_spends_per_anchor().count() > 0)); + let (runtime, _init_guard) = zebra_test::init_async(); let _guard = runtime.enter(); let (mut mempool, mut state, rpc, mempool_tx_queue) = mock_services(network, NoChainTip); @@ -147,6 +151,8 @@ proptest! { /// Test that when the mempool rejects a transaction the caller receives an error. #[test] fn rejected_txs_are_reported(transaction in any::(), network in any::()) { + prop_assume!(!(transaction.is_coinbase() && transaction.sapling_spends_per_anchor().count() > 0)); + let (runtime, _init_guard) = zebra_test::init_async(); let _guard = runtime.enter(); let (mut mempool, mut state, rpc, mempool_tx_queue) = mock_services(network, NoChainTip); @@ -251,6 +257,10 @@ proptest! { runtime.block_on(async move { if verbose.unwrap_or(false) { let (expected_response, mempool_query) = { + let transactions_by_id = transactions + .iter() + .map(|unmined_tx| {(unmined_tx.transaction.id.mined_id(), unmined_tx)}) + .collect::>(); let transaction_dependencies = Default::default(); let txs = transactions .iter() @@ -259,7 +269,7 @@ proptest! { unmined_tx.transaction.id.mined_id().encode_hex(), MempoolObject::from_verified_unmined_tx( unmined_tx, - &transactions, + &transactions_by_id, &transaction_dependencies, ), ) @@ -714,6 +724,8 @@ proptest! { /// Test the queue functionality using `send_raw_transaction` #[test] fn rpc_queue_main_loop(tx in any::(), network in any::()) { + prop_assume!(!(tx.is_coinbase() && tx.sapling_spends_per_anchor().count() > 0)); + let (runtime, _init_guard) = zebra_test::init_async(); let _guard = runtime.enter(); let (mut mempool, mut state, rpc, mempool_tx_queue) = mock_services(network, NoChainTip); @@ -792,6 +804,7 @@ proptest! { #[test] fn rpc_queue_receives_all_txs_from_channel(txs in any::<[Transaction; 2]>(), network in any::()) { + prop_assume!(txs.iter().all(|tx| !(tx.is_coinbase() && tx.sapling_spends_per_anchor().count() > 0))); let (runtime, _init_guard) = zebra_test::init_async(); let _guard = runtime.enter(); let (mut mempool, mut state, rpc, mempool_tx_queue) = mock_services(network, NoChainTip); diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index e85d063a0b9..07bf587c609 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -9,7 +9,7 @@ use std::{ collections::BTreeMap, net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, - time::Instant, + time::{Duration, Instant}, }; use futures::FutureExt; @@ -219,8 +219,11 @@ async fn test_rpc_response_data_for_network(network: &Network) { .map(|block_bytes| block_bytes.zcash_deserialize_into().unwrap()) .collect(); - let mut mempool: MockService<_, _, _, zebra_node_services::BoxError> = - MockService::build().for_unit_tests(); + let mut mempool: MockService<_, _, _, zebra_node_services::BoxError> = MockService::build() + // This test runs multiple network snapshots concurrently; on busy CI runners the default + // mock request timeout can elapse before the GBT long-poll request is observed. + .with_max_request_delay(Duration::from_secs(2)) + .for_unit_tests(); // Create a populated state service let (state, read_state, tip, _) = zebra_state::populated_state(blocks.clone(), network).await; @@ -999,7 +1002,14 @@ fn snapshot_rpc_getnetworkinfo( /// Snapshot `getpeerinfo` response, using `cargo insta` and JSON serialization. fn snapshot_rpc_getpeerinfo(get_peer_info: Vec, settings: &insta::Settings) { - settings.bind(|| insta::assert_json_snapshot!("get_peer_info", get_peer_info)); + settings.bind(|| { + insta::assert_json_snapshot!("get_peer_info", get_peer_info, { + "[].lastrecv" => dynamic_redaction(|value, _path| { + assert!(value.as_u64().unwrap() > 0, "lastrecv should be non-zero"); + "[lastrecv]" + }) + }) + }); } /// Snapshot `getnetworksolps` response, using `cargo insta` and JSON serialization. @@ -1100,6 +1110,7 @@ pub async fn test_mining_rpcs( [0x7e; 20], )), extra_coinbase_data: None, + miner_memo: None, // TODO: Use default field values when optional features are enabled in tests #8183 internal_miner: true, }; @@ -1135,6 +1146,8 @@ pub async fn test_mining_rpcs( .into(), &PeerServices::NODE_NETWORK, false, + "/Zebra:2.1.0/".to_string(), + zebra_network::constants::CURRENT_NETWORK_PROTOCOL_VERSION, ) .into_new_meta_addr(Instant::now(), DateTime32::now())]); diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@mainnet_10.snap index 97eceb7a0b0..86df99f170f 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@mainnet_10.snap @@ -9,14 +9,8 @@ V5( inputs: [ Coinbase( height: Height(1687105), - data: CoinbaseData([ - 122, - 240, - 159, - 166, - 147, - ]), - sequence: 0, + data: [], + sequence: 4294967295, ), ], outputs: [ @@ -24,6 +18,10 @@ V5( value: 250000000, lock_script: Script("a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87"), ), + Output( + value: 15625000, + lock_script: Script("a914d45cb1adffb5215a42720532a076f02c7c778c9087"), + ), Output( value: 21875000, lock_script: Script("a91469a9f95a98fe581b6eb52841ef4806dc4402eb9087"), @@ -32,10 +30,6 @@ V5( value: 25000000, lock_script: Script("a914931fec54c1fea86e574462cc32013f5400b8912987"), ), - Output( - value: 15625000, - lock_script: Script("a914d45cb1adffb5215a42720532a076f02c7c778c9087"), - ), ], sapling_shielded_data: None, orchard_shielded_data: None, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@testnet_10.snap index 65f4adae9a2..6b2bf446e57 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic.coinbase_tx@testnet_10.snap @@ -9,14 +9,8 @@ V5( inputs: [ Coinbase( height: Height(1842421), - data: CoinbaseData([ - 122, - 240, - 159, - 166, - 147, - ]), - sequence: 0, + data: [], + sequence: 4294967295, ), ], outputs: [ diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@mainnet_10.snap index 5e1671ebb4a..42180145cbb 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@mainnet_10.snap @@ -8,20 +8,20 @@ expression: block_template ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", - "blockcommitmentshash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c", - "lightclientroothash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c", - "finalsaplingroothash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c", + "blockcommitmentshash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", + "lightclientroothash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", + "finalsaplingroothash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", "defaultroots": { - "merkleroot": "01b6d9d3deb96e6b776668073f8959c033cc2a8f034ffe32238817c9fb8072b0", + "merkleroot": "0dd4c87d6aba52431fef01079578826b547a22e444af986368c507918f893e8a", "chainhistoryroot": "94470fa66ebd1a5fdb109a5aa3f3204f14de3a42135e71aa7f4c44055847e0b5", - "authdataroot": "a54fe7e2f8ea0d6dc319470cb572d4147e27d7d1740ee4cf214cd05ccb766e8c", - "blockcommitmentshash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c" + "authdataroot": "0dbb78de9fdcd494307971e36dd049fc82d0ee9ee53aec8fd2a54dc0e426289b", + "blockcommitmentshash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82" }, "transactions": [], "coinbasetxn": { - "data": "050000800a27a726b4d0d6c20000000041be1900010000000000000000000000000000000000000000000000000000000000000000ffffffff090341be197af09fa693000000000480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e8738c94d010000000017a91469a9f95a98fe581b6eb52841ef4806dc4402eb908740787d010000000017a914931fec54c1fea86e574462cc32013f5400b8912987286bee000000000017a914d45cb1adffb5215a42720532a076f02c7c778c9087000000", - "hash": "01b6d9d3deb96e6b776668073f8959c033cc2a8f034ffe32238817c9fb8072b0", - "authdigest": "a54fe7e2f8ea0d6dc319470cb572d4147e27d7d1740ee4cf214cd05ccb766e8c", + "data": "050000800a27a726b4d0d6c20000000041be1900010000000000000000000000000000000000000000000000000000000000000000ffffffff040341be19ffffffff0480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87286bee000000000017a914d45cb1adffb5215a42720532a076f02c7c778c908738c94d010000000017a91469a9f95a98fe581b6eb52841ef4806dc4402eb908740787d010000000017a914931fec54c1fea86e574462cc32013f5400b8912987000000", + "hash": "0dd4c87d6aba52431fef01079578826b547a22e444af986368c507918f893e8a", + "authdigest": "0dbb78de9fdcd494307971e36dd049fc82d0ee9ee53aec8fd2a54dc0e426289b", "depends": [], "fee": 0, "sigops": 0, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@testnet_10.snap index e891cab175e..9e06ea6c6b1 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_basic@testnet_10.snap @@ -8,20 +8,20 @@ expression: block_template ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", - "blockcommitmentshash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb", - "lightclientroothash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb", - "finalsaplingroothash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb", + "blockcommitmentshash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", + "lightclientroothash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", + "finalsaplingroothash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", "defaultroots": { - "merkleroot": "2c8d99718285381ff38cd2605ccb7ce5dc7529756d2cc57fef99fbb26332dd9f", + "merkleroot": "453e5aa76d390b623e714cad2658c219393b3019e639e8d477f91eca7697b62b", "chainhistoryroot": "03bc75f00c307a05aed2023819e18c2672cbe15fbd3200944997def141967387", - "authdataroot": "f4c27e3c53e6796a7ee285bbb991eab054f963c85a2795ab57b7c82922a7eb18", - "blockcommitmentshash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb" + "authdataroot": "a44375f0c0dd5ba612bd7b0efd77683cde8edf5055aff9fbfda443cc8d46bd3e", + "blockcommitmentshash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422" }, "transactions": [], "coinbasetxn": { - "data": "050000800a27a726b4d0d6c200000000f51c1c00010000000000000000000000000000000000000000000000000000000000000000ffffffff0903f51c1c7af09fa693000000000480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87286bee000000000017a9140c0bcca02f3cba01a5d7423ac3903d40586399eb8738c94d010000000017a9144e3f0d9a33a2721604cbae2de8d9171e21f8fbe48740787d010000000017a91471e1df05024288a00802de81e08c437859586c8787000000", - "hash": "2c8d99718285381ff38cd2605ccb7ce5dc7529756d2cc57fef99fbb26332dd9f", - "authdigest": "f4c27e3c53e6796a7ee285bbb991eab054f963c85a2795ab57b7c82922a7eb18", + "data": "050000800a27a726b4d0d6c200000000f51c1c00010000000000000000000000000000000000000000000000000000000000000000ffffffff0403f51c1cffffffff0480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87286bee000000000017a9140c0bcca02f3cba01a5d7423ac3903d40586399eb8738c94d010000000017a9144e3f0d9a33a2721604cbae2de8d9171e21f8fbe48740787d010000000017a91471e1df05024288a00802de81e08c437859586c8787000000", + "hash": "453e5aa76d390b623e714cad2658c219393b3019e639e8d477f91eca7697b62b", + "authdigest": "a44375f0c0dd5ba612bd7b0efd77683cde8edf5055aff9fbfda443cc8d46bd3e", "depends": [], "fee": 0, "sigops": 0, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@mainnet_10.snap index 97eceb7a0b0..86df99f170f 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@mainnet_10.snap @@ -9,14 +9,8 @@ V5( inputs: [ Coinbase( height: Height(1687105), - data: CoinbaseData([ - 122, - 240, - 159, - 166, - 147, - ]), - sequence: 0, + data: [], + sequence: 4294967295, ), ], outputs: [ @@ -24,6 +18,10 @@ V5( value: 250000000, lock_script: Script("a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87"), ), + Output( + value: 15625000, + lock_script: Script("a914d45cb1adffb5215a42720532a076f02c7c778c9087"), + ), Output( value: 21875000, lock_script: Script("a91469a9f95a98fe581b6eb52841ef4806dc4402eb9087"), @@ -32,10 +30,6 @@ V5( value: 25000000, lock_script: Script("a914931fec54c1fea86e574462cc32013f5400b8912987"), ), - Output( - value: 15625000, - lock_script: Script("a914d45cb1adffb5215a42720532a076f02c7c778c9087"), - ), ], sapling_shielded_data: None, orchard_shielded_data: None, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@testnet_10.snap index 65f4adae9a2..6b2bf446e57 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll.coinbase_tx@testnet_10.snap @@ -9,14 +9,8 @@ V5( inputs: [ Coinbase( height: Height(1842421), - data: CoinbaseData([ - 122, - 240, - 159, - 166, - 147, - ]), - sequence: 0, + data: [], + sequence: 4294967295, ), ], outputs: [ diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@mainnet_10.snap index ea368f44bd0..23f08e3795e 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@mainnet_10.snap @@ -8,20 +8,20 @@ expression: block_template ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", - "blockcommitmentshash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c", - "lightclientroothash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c", - "finalsaplingroothash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c", + "blockcommitmentshash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", + "lightclientroothash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", + "finalsaplingroothash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", "defaultroots": { - "merkleroot": "01b6d9d3deb96e6b776668073f8959c033cc2a8f034ffe32238817c9fb8072b0", + "merkleroot": "0dd4c87d6aba52431fef01079578826b547a22e444af986368c507918f893e8a", "chainhistoryroot": "94470fa66ebd1a5fdb109a5aa3f3204f14de3a42135e71aa7f4c44055847e0b5", - "authdataroot": "a54fe7e2f8ea0d6dc319470cb572d4147e27d7d1740ee4cf214cd05ccb766e8c", - "blockcommitmentshash": "88700a41ec1166c44b7abe0f23d2c0a417438364bb18e324b5c1c61c9f379c9c" + "authdataroot": "0dbb78de9fdcd494307971e36dd049fc82d0ee9ee53aec8fd2a54dc0e426289b", + "blockcommitmentshash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82" }, "transactions": [], "coinbasetxn": { - "data": "050000800a27a726b4d0d6c20000000041be1900010000000000000000000000000000000000000000000000000000000000000000ffffffff090341be197af09fa693000000000480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e8738c94d010000000017a91469a9f95a98fe581b6eb52841ef4806dc4402eb908740787d010000000017a914931fec54c1fea86e574462cc32013f5400b8912987286bee000000000017a914d45cb1adffb5215a42720532a076f02c7c778c9087000000", - "hash": "01b6d9d3deb96e6b776668073f8959c033cc2a8f034ffe32238817c9fb8072b0", - "authdigest": "a54fe7e2f8ea0d6dc319470cb572d4147e27d7d1740ee4cf214cd05ccb766e8c", + "data": "050000800a27a726b4d0d6c20000000041be1900010000000000000000000000000000000000000000000000000000000000000000ffffffff040341be19ffffffff0480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87286bee000000000017a914d45cb1adffb5215a42720532a076f02c7c778c908738c94d010000000017a91469a9f95a98fe581b6eb52841ef4806dc4402eb908740787d010000000017a914931fec54c1fea86e574462cc32013f5400b8912987000000", + "hash": "0dd4c87d6aba52431fef01079578826b547a22e444af986368c507918f893e8a", + "authdigest": "0dbb78de9fdcd494307971e36dd049fc82d0ee9ee53aec8fd2a54dc0e426289b", "depends": [], "fee": 0, "sigops": 0, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@testnet_10.snap index ad9ae12d4d8..411adfce908 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_template_long_poll@testnet_10.snap @@ -8,20 +8,20 @@ expression: block_template ], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", - "blockcommitmentshash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb", - "lightclientroothash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb", - "finalsaplingroothash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb", + "blockcommitmentshash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", + "lightclientroothash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", + "finalsaplingroothash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", "defaultroots": { - "merkleroot": "2c8d99718285381ff38cd2605ccb7ce5dc7529756d2cc57fef99fbb26332dd9f", + "merkleroot": "453e5aa76d390b623e714cad2658c219393b3019e639e8d477f91eca7697b62b", "chainhistoryroot": "03bc75f00c307a05aed2023819e18c2672cbe15fbd3200944997def141967387", - "authdataroot": "f4c27e3c53e6796a7ee285bbb991eab054f963c85a2795ab57b7c82922a7eb18", - "blockcommitmentshash": "1d445cc3e1f19f1d8613bb50f3152e4fd28c57bc5a98eb35dc96830d17368ebb" + "authdataroot": "a44375f0c0dd5ba612bd7b0efd77683cde8edf5055aff9fbfda443cc8d46bd3e", + "blockcommitmentshash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422" }, "transactions": [], "coinbasetxn": { - "data": "050000800a27a726b4d0d6c200000000f51c1c00010000000000000000000000000000000000000000000000000000000000000000ffffffff0903f51c1c7af09fa693000000000480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87286bee000000000017a9140c0bcca02f3cba01a5d7423ac3903d40586399eb8738c94d010000000017a9144e3f0d9a33a2721604cbae2de8d9171e21f8fbe48740787d010000000017a91471e1df05024288a00802de81e08c437859586c8787000000", - "hash": "2c8d99718285381ff38cd2605ccb7ce5dc7529756d2cc57fef99fbb26332dd9f", - "authdigest": "f4c27e3c53e6796a7ee285bbb991eab054f963c85a2795ab57b7c82922a7eb18", + "data": "050000800a27a726b4d0d6c200000000f51c1c00010000000000000000000000000000000000000000000000000000000000000000ffffffff0403f51c1cffffffff0480b2e60e0000000017a9147e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e87286bee000000000017a9140c0bcca02f3cba01a5d7423ac3903d40586399eb8738c94d010000000017a9144e3f0d9a33a2721604cbae2de8d9171e21f8fbe48740787d010000000017a91471e1df05024288a00802de81e08c437859586c8787000000", + "hash": "453e5aa76d390b623e714cad2658c219393b3019e639e8d477f91eca7697b62b", + "authdigest": "a44375f0c0dd5ba612bd7b0efd77683cde8edf5055aff9fbfda443cc8d46bd3e", "depends": [], "fee": 0, "sigops": 0, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap index 3bfc189eb1b..b15bbbe95d1 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap index 1d99076c8fd..34c93ae1adc 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap index 5f77942899a..c49f5f072c7 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap @@ -12,6 +12,7 @@ expression: block "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ { "in_active_chain": true, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap index ccc78aad123..93985f5016b 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap @@ -12,6 +12,7 @@ expression: block "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ { "in_active_chain": true, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap index 3bfc189eb1b..b15bbbe95d1 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap index 1d99076c8fd..34c93ae1adc 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap index 3bfc189eb1b..b15bbbe95d1 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap index 1d99076c8fd..34c93ae1adc 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap index 5f77942899a..c49f5f072c7 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap @@ -12,6 +12,7 @@ expression: block "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ { "in_active_chain": true, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap index ccc78aad123..93985f5016b 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap @@ -12,6 +12,7 @@ expression: block "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ { "in_active_chain": true, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap index 3bfc189eb1b..b15bbbe95d1 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap index 1d99076c8fd..34c93ae1adc 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap @@ -11,6 +11,7 @@ expression: block "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, "tx": [ "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" ], diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap index 9783067bc39..af522c38533 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap @@ -91,6 +91,11 @@ expression: info "name": "NU6.1", "activationheight": 3146400, "status": "pending" + }, + "5437f330": { + "name": "NU6.2", + "activationheight": 3364600, + "status": "pending" } }, "consensus": { diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap index 18b4614ba64..22dccbb5568 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap @@ -91,6 +91,11 @@ expression: info "name": "NU6.1", "activationheight": 3536500, "status": "pending" + }, + "5437f330": { + "name": "NU6.2", + "activationheight": 4052000, + "status": "pending" } }, "consensus": { diff --git a/zebra-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap index df63866cad7..afa04cd85d6 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap @@ -6,7 +6,7 @@ expression: info "version": 100, "build": "v0.0.1", "subversion": "[SubVersion]", - "protocolversion": 170140, + "protocolversion": 170150, "blocks": 10, "connections": 0, "difficulty": 1.0, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap index 3256cf8d0cc..669e8c4bbf4 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap @@ -6,7 +6,7 @@ expression: info "version": 100, "build": "v0.0.1", "subversion": "[SubVersion]", - "protocolversion": 170140, + "protocolversion": 170150, "blocks": 10, "connections": 0, "difficulty": 1.0, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap index e5414718b8c..c2dbf1dbc65 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap @@ -5,7 +5,7 @@ expression: get_network_info { "version": 100, "subversion": "RPC test", - "protocolversion": 170140, + "protocolversion": 170150, "localservices": "0000000000000001", "timeoffset": 0, "connections": 1, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap index e5414718b8c..c2dbf1dbc65 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap @@ -5,7 +5,7 @@ expression: get_network_info { "version": 100, "subversion": "RPC test", - "protocolversion": 170140, + "protocolversion": 170150, "localservices": "0000000000000001", "timeoffset": 0, "connections": 1, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_peer_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_peer_info@mainnet_10.snap index 568bac9709f..1560614d0b5 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_peer_info@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_peer_info@mainnet_10.snap @@ -5,6 +5,12 @@ expression: get_peer_info [ { "addr": "127.0.0.1:8233", - "inbound": false + "services": "0000000000000001", + "lastrecv": "[lastrecv]", + "inbound": false, + "banscore": 0, + "subver": "/Zebra:2.1.0/", + "version": 170150, + "connection_state": "connected" } ] diff --git a/zebra-rpc/src/methods/tests/snapshots/get_peer_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_peer_info@testnet_10.snap index badd0067d4c..e1950336d64 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_peer_info@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_peer_info@testnet_10.snap @@ -5,6 +5,12 @@ expression: get_peer_info [ { "addr": "127.0.0.1:18233", - "inbound": false + "services": "0000000000000001", + "lastrecv": "[lastrecv]", + "inbound": false, + "banscore": 0, + "subver": "/Zebra:2.1.0/", + "version": 170150, + "connection_state": "connected" } ] diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index c73197899ee..a4a1bf3210f 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -24,7 +24,7 @@ use zebra_chain::{ }, serialization::{DateTime32, ZcashDeserializeInto, ZcashSerialize}, transaction::{zip317, SigHash, UnminedTxId, VerifiedUnminedTx}, - work::difficulty::{CompactDifficulty, ExpandedDifficulty, ParameterDifficulty as _, U256}, + work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256}, }; use zebra_consensus::MAX_BLOCK_SIGOPS; use zebra_network::{ @@ -307,6 +307,7 @@ async fn rpc_getblock() { confirmations: (blocks.len() - i).try_into().expect("valid i64"), height: Some(Height(i.try_into().expect("valid u32"))), time: Some(block.header.time.timestamp()), + n_tx: block.transactions.len(), tx: block .transactions .iter() @@ -364,6 +365,7 @@ async fn rpc_getblock() { confirmations: (blocks.len() - i).try_into().expect("valid i64"), height: Some(Height(i.try_into().expect("valid u32"))), time: Some(block.header.time.timestamp()), + n_tx: block.transactions.len(), tx: block .transactions .iter() @@ -420,6 +422,7 @@ async fn rpc_getblock() { confirmations, height, time, + n_tx, tx, trees, size, @@ -441,6 +444,7 @@ async fn rpc_getblock() { assert_eq!(confirmations, &((blocks.len() - i) as i64)); assert_eq!(height, &Some(Height(i.try_into().expect("valid u32")))); assert_eq!(time, &Some(block.header.time.timestamp())); + assert_eq!(*n_tx, block.transactions.len()); assert_eq!(trees, trees); assert_eq!(size, &Some(block.zcash_serialized_size() as i64)); assert_eq!(version, &Some(block.header.version)); @@ -518,6 +522,7 @@ async fn rpc_getblock() { confirmations, height, time, + n_tx, tx, trees, size, @@ -539,6 +544,7 @@ async fn rpc_getblock() { assert_eq!(confirmations, &((blocks.len() - i) as i64)); assert_eq!(height, &Some(Height(i.try_into().expect("valid u32")))); assert_eq!(time, &Some(block.header.time.timestamp())); + assert_eq!(*n_tx, block.transactions.len()); assert_eq!(trees, trees); assert_eq!(size, &Some(block.zcash_serialized_size() as i64)); assert_eq!(version, &Some(block.header.version)); @@ -616,6 +622,7 @@ async fn rpc_getblock() { confirmations: (blocks.len() - i).try_into().expect("valid i64"), height: Some(Height(i.try_into().expect("valid u32"))), time: Some(block.header.time.timestamp()), + n_tx: block.transactions.len(), tx: block .transactions .iter() @@ -672,6 +679,7 @@ async fn rpc_getblock() { confirmations: (blocks.len() - i).try_into().expect("valid i64"), height: Some(Height(i.try_into().expect("valid u32"))), time: Some(block.header.time.timestamp()), + n_tx: block.transactions.len(), tx: block .transactions .iter() @@ -1779,6 +1787,8 @@ async fn rpc_getpeerinfo() { .into(), &PeerServices::NODE_NETWORK, false, + "/Zebra:2.1.0/".to_string(), + zebra_network::constants::CURRENT_NETWORK_PROTOCOL_VERSION, ) .into_new_meta_addr( std::time::Instant::now(), @@ -1794,6 +1804,8 @@ async fn rpc_getpeerinfo() { .into(), &PeerServices::NODE_NETWORK, true, + "/zcashd:5.8.0/".to_string(), + zebra_network::constants::CURRENT_NETWORK_PROTOCOL_VERSION, ) .into_new_meta_addr( std::time::Instant::now(), @@ -1814,8 +1826,8 @@ async fn rpc_getpeerinfo() { ); let mock_address_book = MockAddressBookPeers::new(vec![ - outbound_mock_peer_address, - inbound_mock_peer_address, + outbound_mock_peer_address.clone(), + inbound_mock_peer_address.clone(), not_connected_mock_peer_adderess, ]); @@ -2065,6 +2077,7 @@ async fn gbt_with(net: Network, addr: ZcashAddress) { let mining_conf = crate::config::mining::Config { miner_address: Some(addr.clone()), extra_coinbase_data: None, + miner_memo: None, internal_miner: true, }; @@ -2317,6 +2330,7 @@ async fn gbt_with(net: Network, addr: ZcashAddress) { transaction: unmined_tx, miner_fee: 0.try_into().unwrap(), legacy_sigop_count: 0, + p2sh_sigop_count: 0, conventional_actions, unpaid_actions: 0, fee_weight_ratio: 1.0, @@ -2396,7 +2410,7 @@ async fn rpc_submitblock_errors() { ); // Try to submit pre-populated blocks and assert that it responds with duplicate. - for (_height, &block_bytes) in zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS.iter() { + for &block_bytes in zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS.values() { let submit_block_response = rpc.submit_block(HexData(block_bytes.into()), None).await; assert_eq!( @@ -2759,6 +2773,7 @@ async fn rpc_getdifficulty() { let mining_conf = mining::Config { miner_address: None, extra_coinbase_data: None, + miner_memo: None, internal_miner: true, }; @@ -2955,6 +2970,70 @@ async fn rpc_z_listunifiedreceivers() { assert_eq!(*response.p2sh(), None); } +/// Check that `z_listunifiedreceivers` returns an RPC error (instead of panicking and +/// aborting `zebrad`) when given a unified address whose Sapling receiver has a valid +/// length and typecode but a non-canonical Jubjub `pk_d`. +/// +/// The unified-address decoder validates only the typecode and length of inner receivers, +/// not their contents, so this used to reach `sapling_crypto::PaymentAddress::from_bytes` +/// returning `None` and then panic on `.expect(...)`. Under `panic = "abort"` (Zebra's +/// release and dev profiles) that aborts the process. +/// +/// Regression test for +/// [GHSA-c8w6-x74f-vmg3](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-c8w6-x74f-vmg3). +#[tokio::test(flavor = "multi_thread")] +async fn rpc_z_listunifiedreceivers_rejects_bad_sapling_receiver() { + use zcash_address::unified::{Address as UnifiedAddress, Encoding, Receiver}; + + let _init_guard = zebra_test::init(); + + // Construct a Sapling receiver whose 43 bytes pass the typecode/length checks + // performed by `unified::Encoding::decode`, but whose trailing 32 bytes are not a + // canonical Jubjub `DiversifiedTransmissionKey`. `0xFF` * 32 is the simplest such + // value: `sapling_crypto::PaymentAddress::from_bytes` returns `None`. + let mut bad_sapling = [0u8; 43]; + bad_sapling[..11].copy_from_slice(&[0x11; 11]); + bad_sapling[11..].copy_from_slice(&[0xFF; 32]); + + // A placeholder Orchard receiver is needed to satisfy the unified-container rule + // that forbids `OnlyTransparent` UAs. The bytes themselves are never validated by + // the decoder, so any 43 bytes work. + let placeholder_orchard = [0x22u8; 43]; + + let unified = UnifiedAddress::try_from_items(vec![ + Receiver::Sapling(bad_sapling), + Receiver::Orchard(placeholder_orchard), + ]) + .expect("unified container construction does not validate inner bytes"); + let encoded = unified.encode(&NetworkType::Main); + + // Init RPC + let (_tx, rx) = tokio::sync::watch::channel(None); + let (rpc, _) = RpcImpl::new( + Mainnet, + Default::default(), + Default::default(), + "0.0.1", + "RPC test", + MockService::build().for_unit_tests(), + MockService::build().for_unit_tests(), + MockService::build().for_unit_tests(), + MockService::build().for_unit_tests(), + MockSyncStatus::default(), + NoChainTip, + MockAddressBookPeers::default(), + rx, + None, + ); + + let result = rpc.z_list_unified_receivers(encoded).await; + assert!( + result.is_err(), + "z_listunifiedreceivers must return an error for a malformed Sapling receiver, \ + got {result:?}", + ); +} + #[tokio::test(flavor = "multi_thread")] async fn rpc_addnode() { let _init_guard = zebra_test::init(); @@ -3015,16 +3094,16 @@ async fn rpc_addnode() { .await .expect("We should have an array of addresses"); - assert_eq!( - get_peer_info, - [PeerInfo { - addr, - inbound: false, - // TODO: Fix this when mock address book provides other values - pingtime: Some(0.1f64), - pingwait: None, - }] - ); + assert_eq!(get_peer_info.len(), 1); + let peer = &get_peer_info[0]; + assert_eq!(peer.addr(), addr); + assert!(!peer.inbound()); + assert_eq!(peer.pingtime(), &Some(0.1f64)); + assert_eq!(peer.pingwait(), &None); + assert_eq!(peer.services().as_str(), "0000000000000000"); + assert_eq!(peer.banscore(), 0); + assert_eq!(peer.connection_state().as_str(), "connected"); + assert!(peer.lastrecv() > 0); mempool.expect_no_requests().await; } diff --git a/zebra-rpc/src/methods/types/default_roots.rs b/zebra-rpc/src/methods/types/default_roots.rs index 441e51aac8c..af7757a6671 100644 --- a/zebra-rpc/src/methods/types/default_roots.rs +++ b/zebra-rpc/src/methods/types/default_roots.rs @@ -1,12 +1,22 @@ //! The `DefaultRoots` type is part of the `getblocktemplate` RPC method output. +use std::iter; + use derive_getters::Getters; use derive_new::new; -use zebra_chain::block::{ - merkle::{self, AuthDataRoot}, - ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash, +use zebra_chain::{ + amount::NegativeOrZero, + block::{ + self, + merkle::{self, AuthDataRoot, AUTH_DIGEST_PLACEHOLDER}, + ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash, Height, + }, + parameters::{Network, NetworkUpgrade}, + transaction::VerifiedUnminedTx, }; +use crate::client::TransactionTemplate; + /// The block header roots for the transactions in a block template. /// /// If the transactions in the block template are modified, these roots must be recalculated @@ -42,3 +52,53 @@ pub struct DefaultRoots { #[getter(copy)] pub(crate) block_commitments_hash: ChainHistoryBlockTxAuthCommitmentHash, } + +impl DefaultRoots { + /// Creates a new [`DefaultRoots`] instance from the given coinbase tx template. + pub fn from_coinbase( + net: &Network, + height: Height, + coinbase: &TransactionTemplate, + chain_history_root: Option, + mempool_txs: &[VerifiedUnminedTx], + ) -> Self { + let chain_history_root = chain_history_root + .or_else(|| { + (NetworkUpgrade::Heartwood.activation_height(net) == Some(height)) + .then_some(block::CHAIN_HISTORY_ACTIVATION_RESERVED.into()) + }) + .expect("history root is required for block templates"); + + // TODO: + // Computing `auth_data_root` and `merkle_root` gets more expensive as `mempool_txs` grows. + // It might be worth doing it in rayon. + + let auth_data_root = iter::once(coinbase.auth_digest) + .chain(mempool_txs.iter().map(|tx| { + tx.transaction + .id + .auth_digest() + .unwrap_or(AUTH_DIGEST_PLACEHOLDER) + })) + .collect(); + + Self { + merkle_root: iter::once(coinbase.hash) + .chain(mempool_txs.iter().map(|tx| tx.transaction.id.mined_id())) + .collect(), + chain_history_root, + auth_data_root, + block_commitments_hash: if NetworkUpgrade::current(net, height) + == NetworkUpgrade::Heartwood + && chain_history_root == block::CHAIN_HISTORY_ACTIVATION_RESERVED.into() + { + block::CHAIN_HISTORY_ACTIVATION_RESERVED.into() + } else { + ChainHistoryBlockTxAuthCommitmentHash::from_commitments( + &chain_history_root, + &auth_data_root, + ) + }, + } + } +} diff --git a/zebra-rpc/src/methods/types/get_block_template.rs b/zebra-rpc/src/methods/types/get_block_template.rs index 4b0a8ed4711..cca85270607 100644 --- a/zebra-rpc/src/methods/types/get_block_template.rs +++ b/zebra-rpc/src/methods/types/get_block_template.rs @@ -8,54 +8,53 @@ pub mod zip317; #[cfg(test)] mod tests; -use std::{collections::HashMap, fmt, iter, sync::Arc}; +use std::{ + fmt::{self}, + sync::Arc, +}; use derive_getters::Getters; use derive_new::new; use jsonrpsee::core::RpcResult; use jsonrpsee_types::{ErrorCode, ErrorObject}; +use rand::{rngs::OsRng, RngCore}; use tokio::sync::mpsc::{self, error::TrySendError}; use tower::{Service, ServiceExt}; use zcash_keys::address::Address; -use zcash_protocol::PoolType; -use zcash_script::script::Evaluable; +use zcash_protocol::memo::MemoBytes; +use zcash_script::{ + opcode::{Evaluable, PushValue}, + pv::push_value, +}; use zebra_chain::{ - amount::{self, Amount, NegativeOrZero, NonNegative}, + amount::{self, Amount, NonNegative}, block::{ - self, Block, ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash, Height, - MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION, + self, Block, ChainHistoryBlockTxAuthCommitmentHash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION, }, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, - parameters::{ - subsidy::{block_subsidy, funding_stream_values, miner_subsidy, FundingStreamReceiver}, - Network, NetworkUpgrade, - }, + parameters::Network, serialization::{DateTime32, ZcashDeserializeInto}, - transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, - transparent::{ - self, EXTRA_ZEBRA_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN, - }, + transaction::VerifiedUnminedTx, work::difficulty::{CompactDifficulty, ExpandedDifficulty}, }; // Required for trait method `.bytes_in_display_order()` used indirectly in Debug impl #[allow(unused_imports)] use zebra_chain::serialization::BytesInDisplayOrder; -use zebra_consensus::{ - funding_stream_address, router::service_trait::BlockVerifierService, MAX_BLOCK_SIGOPS, -}; +use zebra_consensus::{router::service_trait::BlockVerifierService, MAX_BLOCK_SIGOPS}; use zebra_node_services::mempool::{self, TransactionDependencies}; use zebra_state::GetBlockTemplateChainInfo; use crate::{ config, methods::types::{ - default_roots::DefaultRoots, long_poll::LongPollId, submit_block, - transaction::TransactionTemplate, + default_roots::DefaultRoots, get_block_template::constants::MAX_MINER_DATA_LEN, + long_poll::LongPollId, transaction::TransactionTemplate, }, server::error::OkOrError, + SubmitBlockChannel, }; use constants::{ @@ -272,23 +271,25 @@ impl BlockTemplateResponse { /// The result of this method only depends on the supplied arguments and constants. #[allow(clippy::too_many_arguments)] pub(crate) fn new_internal( - network: &Network, - miner_address: &Address, - chain_tip_and_local_time: &GetBlockTemplateChainInfo, + net: &Network, + precomputed_coinbase: Option>, + miner_params: &MinerParams, + chain_info: &GetBlockTemplateChainInfo, long_poll_id: LongPollId, #[cfg(not(test))] mempool_txs: Vec, #[cfg(test)] mempool_txs: Vec<(InBlockTxDependenciesDepth, VerifiedUnminedTx)>, submit_old: Option, - extra_coinbase_data: Vec, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< Amount, >, ) -> Self { - // Calculate the next block height. - let next_block_height = - (chain_tip_and_local_time.tip_height + 1).expect("tip is far below Height::MAX"); + // Determine the next block height. + let height = chain_info + .tip_height + .next() + .expect("chain tip must be below Height::MAX"); - // Convert transactions into TransactionTemplates + // Convert transactions into TransactionTemplates. #[cfg(not(test))] let (mempool_tx_templates, mempool_txs): (Vec<_>, Vec<_>) = mempool_txs.into_iter().map(|tx| ((&tx).into(), tx)).unzip(); @@ -324,23 +325,34 @@ impl BlockTemplateResponse { .unzip() }; - // Generate the coinbase transaction and default roots - // - // TODO: move expensive root, hash, and tree cryptography to a rayon thread? - let (coinbase_txn, default_roots) = generate_coinbase_and_roots( - network, - next_block_height, - miner_address, + let txs_fee = mempool_txs + .iter() + .map(|tx| tx.miner_fee) + .sum::>>() + .expect("mempool tx fees must be non-negative"); + + let coinbase_txn = precomputed_coinbase.unwrap_or_else(|| { + TransactionTemplate::new_coinbase( + net, + height, + miner_params, + txs_fee, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + zip233_amount, + ) + .expect("valid coinbase tx") + }); + + let default_roots = DefaultRoots::from_coinbase( + net, + height, + &coinbase_txn, + chain_info.chain_history_root, &mempool_txs, - chain_tip_and_local_time.chain_history_root, - extra_coinbase_data, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - zip233_amount, - ) - .expect("coinbase should be valid under the given parameters"); + ); // Convert difficulty - let target = chain_tip_and_local_time + let target = chain_info .expected_difficulty .to_expanded() .expect("state always returns a valid difficulty value"); @@ -362,7 +374,7 @@ impl BlockTemplateResponse { version: ZCASH_BLOCK_VERSION, - previous_block_hash: chain_tip_and_local_time.tip_hash, + previous_block_hash: chain_info.tip_hash, block_commitments_hash: default_roots.block_commitments_hash, light_client_root_hash: default_roots.block_commitments_hash, final_sapling_root_hash: default_roots.block_commitments_hash, @@ -376,7 +388,7 @@ impl BlockTemplateResponse { target, - min_time: chain_tip_and_local_time.min_time, + min_time: chain_info.min_time, mutable, @@ -386,13 +398,13 @@ impl BlockTemplateResponse { size_limit: MAX_BLOCK_BYTES, - cur_time: chain_tip_and_local_time.cur_time, + cur_time: chain_info.cur_time, - bits: chain_tip_and_local_time.expected_difficulty, + bits: chain_info.expected_difficulty, - height: next_block_height.0, + height: height.0, - max_time: chain_tip_and_local_time.max_time, + max_time: chain_info.max_time, submit_old, } @@ -428,19 +440,116 @@ impl GetBlockTemplateResponse { } } -/// Handler for the `getblocktemplate` RPC. +/// Miner parameters. +#[derive(Clone, Debug)] +pub struct MinerParams { + /// Address for receiving miner subsidy and tx fees. + addr: Address, + + /// Optional data to include in the coinbase input script. + data: Option, + + /// Optional shielded memo for the miner's coinbase transaction. + /// + /// Applies only if [`Self::addr`] contains a shielded component. + memo: Option, +} + +impl MinerParams { + /// Creates a new instance of [`MinerParams`]. + pub fn new(net: &Network, conf: config::mining::Config) -> Result { + let addr = conf + .miner_address + .map(|addr| Address::try_from_zcash_address(net, addr)) + .ok_or(MinerParamsError::MissingAddr)??; + + let data = conf + .extra_coinbase_data + .map(|s| { + let data = push_value(s.as_bytes()).ok_or(MinerParamsError::OversizedData)?; + + (data.byte_len() <= MAX_MINER_DATA_LEN) + .then_some(data) + .ok_or(MinerParamsError::OversizedData) + }) + .transpose()?; + + let memo = conf + .miner_memo + .map(|memo| MemoBytes::from_bytes(memo.as_bytes())) + .transpose()?; + + Ok(Self { addr, data, memo }) + } + + /// Returns the miner address. + pub fn addr(&self) -> &Address { + &self.addr + } + + /// Returns the miner data. + pub fn data(&self) -> &Option { + &self.data + } + + /// Returns the miner memo. + pub fn memo(&self) -> Option<&MemoBytes> { + self.memo.as_ref() + } + + /// Randomizes the memo. + pub fn randomize_memo(&mut self) { + let mut random = [0u8; 512]; + OsRng.fill_bytes(&mut random); + self.memo = Some(MemoBytes::from_bytes(&random).unwrap()); + } + + /// Randomizes the miner data. + pub fn randomize_data(&mut self) { + let mut random = [0u8; 32]; + OsRng.fill_bytes(&mut random); + self.data = push_value(&random); + } +} + +impl From
for MinerParams { + fn from(addr: Address) -> Self { + Self { + addr, + data: None, + memo: None, + } + } +} + +/// Errors that can occur when creating [`MinerParams`]. +#[derive(Debug, thiserror::Error)] +pub enum MinerParamsError { + #[error("Missing miner address")] + MissingAddr, + #[error("Invalid miner address: {0}")] + InvalidAddr(zcash_address::ConversionError<&'static str>), + #[error("Miner data exceeds {MAX_MINER_DATA_LEN} bytes")] + OversizedData, + #[error(transparent)] + InvalidMemo(#[from] zcash_protocol::memo::Error), +} + +impl From> for MinerParamsError { + fn from(err: zcash_address::ConversionError<&'static str>) -> Self { + Self::InvalidAddr(err) + } +} + +/// Handler for the `getblocktemplate` RPC. #[derive(Clone)] pub struct GetBlockTemplateHandler where BlockVerifierRouter: BlockVerifierService, SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { - /// Address for receiving miner subsidy and tx fees. - miner_address: Option
, - - /// Extra data to include in coinbase transaction inputs. - /// Limited to around 95 bytes by the consensus rules. - extra_coinbase_data: Vec, + /// Miner parameters, including the miner address, data, and memo. + miner_params: Option, /// The chain verifier, used for submitting blocks. block_verifier_router: BlockVerifierRouter, @@ -453,20 +562,12 @@ where mined_block_sender: mpsc::Sender<(block::Hash, block::Height)>, } -// A limit on the configured extra coinbase data, regardless of the current block height. -// This is different from the consensus rule, which limits the total height + data. -const EXTRA_COINBASE_DATA_LIMIT: usize = MAX_COINBASE_DATA_LEN - MAX_COINBASE_HEIGHT_DATA_LEN; - impl GetBlockTemplateHandler where BlockVerifierRouter: BlockVerifierService, SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { /// Creates a new [`GetBlockTemplateHandler`]. - /// - /// # Panics - /// - /// - If the `miner_address` in `conf` is not valid. pub fn new( net: &Network, conf: config::mining::Config, @@ -474,65 +575,18 @@ where sync_status: SyncStatus, mined_block_sender: Option>, ) -> Self { - // Check that the configured miner address is valid. - let miner_address = conf.miner_address.map(|addr| { - if addr.can_receive_as(PoolType::Transparent) { - Address::try_from_zcash_address(net, addr) - .expect("miner_address must be a valid Zcash address") - } else { - // TODO: Remove this panic once we support mining to shielded addresses. - panic!("miner_address can't receive transparent funds") - } - }); - - // Hex-decode to bytes if possible, otherwise UTF-8 encode to bytes. - let extra_coinbase_data = conf - .extra_coinbase_data - .unwrap_or_else(|| EXTRA_ZEBRA_COINBASE_DATA.to_string()); - let extra_coinbase_data = hex::decode(&extra_coinbase_data) - .unwrap_or_else(|_error| extra_coinbase_data.as_bytes().to_vec()); - - assert!( - extra_coinbase_data.len() <= EXTRA_COINBASE_DATA_LIMIT, - "extra coinbase data is {} bytes, but Zebra's limit is {}.\n\ - Configure mining.extra_coinbase_data with a shorter string", - extra_coinbase_data.len(), - EXTRA_COINBASE_DATA_LIMIT, - ); - Self { - miner_address, - extra_coinbase_data, + miner_params: MinerParams::new(net, conf).ok(), block_verifier_router, sync_status, mined_block_sender: mined_block_sender - .unwrap_or(submit_block::SubmitBlockChannel::default().sender()), + .unwrap_or(SubmitBlockChannel::default().sender()), } } - /// Returns a valid miner address, if any. - pub fn miner_address(&self) -> Option
{ - self.miner_address.clone() - } - - /// Returns the extra coinbase data. - pub fn extra_coinbase_data(&self) -> Vec { - self.extra_coinbase_data.clone() - } - - /// Changes the extra coinbase data. - /// - /// # Panics - /// - /// If `extra_coinbase_data` exceeds [`EXTRA_COINBASE_DATA_LIMIT`]. - pub fn set_extra_coinbase_data(&mut self, extra_coinbase_data: Vec) { - assert!( - extra_coinbase_data.len() <= EXTRA_COINBASE_DATA_LIMIT, - "extra coinbase data is {} bytes, but Zebra's limit is {}.", - extra_coinbase_data.len(), - EXTRA_COINBASE_DATA_LIMIT, - ); - self.extra_coinbase_data = extra_coinbase_data; + /// Returns the miner parameters, including the address, data, and memo. + pub fn miner_params(&self) -> Option<&MinerParams> { + self.miner_params.as_ref() } /// Returns the sync status. @@ -553,6 +607,14 @@ where ) -> Result<(), TrySendError<(block::Hash, block::Height)>> { self.mined_block_sender.try_send((block, height)) } + + /// Randomizes the coinbase data, if miner parameters are set. + pub fn randomize_coinbase_data(&mut self) { + if let Some(miner_params) = &mut self.miner_params { + miner_params.randomize_data(); + miner_params.randomize_memo(); + } + } } impl fmt::Debug @@ -563,9 +625,8 @@ where { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Skip fields without debug impls - f.debug_struct("GetBlockTemplateRpcImpl") - .field("miner_address", &self.miner_address) - .field("extra_coinbase_data", &self.extra_coinbase_data) + f.debug_struct("GetBlockTemplateHandler") + .field("miner_params", &self.miner_params) .finish() } } @@ -623,7 +684,7 @@ pub fn check_parameters(parameters: &Option) -> RpcR pub async fn validate_block_proposal( mut block_verifier_router: BlockVerifierRouter, block_proposal_bytes: Vec, - network: Network, + net: &Network, latest_chain_tip: Tip, sync_status: SyncStatus, ) -> RpcResult @@ -636,7 +697,7 @@ where Tip: ChainTip + Clone + Send + Sync + 'static, SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { - check_synced_to_tip(&network, latest_chain_tip, sync_status)?; + check_synced_to_tip(net, latest_chain_tip, sync_status)?; let block: Block = match block_proposal_bytes.zcash_deserialize_into() { Ok(block) => block, @@ -729,9 +790,7 @@ where /// /// You should call `check_synced_to_tip()` before calling this function. /// If the state does not have enough blocks, returns an error. -pub async fn fetch_state_tip_and_local_time( - state: State, -) -> RpcResult +pub async fn fetch_chain_info(state: State) -> RpcResult where State: Service< zebra_state::ReadRequest, @@ -792,168 +851,3 @@ where // Check that the mempool and state were in sync when we made the requests Ok((last_seen_tip_hash == chain_tip_hash).then_some((transactions, transaction_dependencies))) } - -// - Response processing - -/// Generates and returns the coinbase transaction and default roots. -#[allow(clippy::too_many_arguments)] -pub fn generate_coinbase_and_roots( - network: &Network, - height: Height, - miner_address: &Address, - mempool_txs: &[VerifiedUnminedTx], - chain_history_root: Option, - miner_data: Vec, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< - Amount, - >, -) -> Result<(TransactionTemplate, DefaultRoots), &'static str> { - let miner_fee = calculate_miner_fee(mempool_txs); - let outputs = standard_coinbase_outputs(network, height, miner_address, miner_fee); - let current_nu = NetworkUpgrade::current(network, height); - - let tx = match current_nu { - NetworkUpgrade::Canopy => Transaction::new_v4_coinbase(height, outputs, miner_data), - NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 => { - Transaction::new_v5_coinbase(network, height, outputs, miner_data) - } - #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] - NetworkUpgrade::Nu7 => Transaction::new_v5_coinbase(network, height, outputs, miner_data), - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - NetworkUpgrade::Nu7 => { - Transaction::new_v6_coinbase(network, height, outputs, miner_data, zip233_amount) - } - _ => Err("Zebra does not support generating pre-Canopy coinbase transactions")?, - } - .into(); - - // Calculate block default roots - // - // TODO: move expensive root, hash, and tree cryptography to a rayon thread? - let chain_history_root = chain_history_root - .or_else(|| { - (NetworkUpgrade::Heartwood.activation_height(network) == Some(height)) - .then_some(block::CHAIN_HISTORY_ACTIVATION_RESERVED.into()) - }) - .expect("history tree can't be empty"); - - Ok(( - TransactionTemplate::from_coinbase(&tx, miner_fee), - calculate_default_root_hashes(current_nu, &tx, mempool_txs, chain_history_root), - )) -} - -/// Returns the total miner fee for `mempool_txs`. -pub fn calculate_miner_fee(mempool_txs: &[VerifiedUnminedTx]) -> Amount { - let miner_fee: amount::Result> = - mempool_txs.iter().map(|tx| tx.miner_fee).sum(); - - miner_fee.expect( - "invalid selected transactions: \ - fees in a valid block can not be more than MAX_MONEY", - ) -} - -/// Returns the standard funding stream and miner reward transparent output scripts -/// for `network`, `height` and `miner_fee`. -/// -/// Only works for post-Canopy heights. -pub fn standard_coinbase_outputs( - network: &Network, - height: Height, - miner_address: &Address, - miner_fee: Amount, -) -> Vec<(Amount, transparent::Script)> { - let expected_block_subsidy = block_subsidy(height, network).expect("valid block subsidy"); - let funding_streams = funding_stream_values(height, network, expected_block_subsidy) - .expect("funding stream value calculations are valid for reasonable chain heights"); - - // Optional TODO: move this into a zebra_consensus function? - let funding_streams: HashMap< - FundingStreamReceiver, - (Amount, &transparent::Address), - > = funding_streams - .into_iter() - .filter_map(|(receiver, amount)| { - Some(( - receiver, - (amount, funding_stream_address(height, network, receiver)?), - )) - }) - .collect(); - - let miner_reward = miner_subsidy(height, network, expected_block_subsidy) - .expect("reward calculations are valid for reasonable chain heights") - + miner_fee; - let miner_reward = - miner_reward.expect("reward calculations are valid for reasonable chain heights"); - - // Collect all the funding streams and convert them to outputs. - let funding_streams_outputs: Vec<(transparent::Address, Amount)> = funding_streams - .into_iter() - .map(|(_receiver, (amount, address))| (address.clone(), amount)) - .collect(); - let one_time_lockbox_disbursements = network.lockbox_disbursements(height); - - // Combine the miner reward and funding streams into a list of coinbase amounts and addresses. - let mut coinbase_outputs: Vec<(Amount, transparent::Script)> = - funding_streams_outputs - .iter() - .chain(&one_time_lockbox_disbursements) - .map(|(address, amount)| (*amount, address.script())) - .collect(); - - let script = transparent::Script::new( - &miner_address - .to_transparent_address() - .expect("address must have a transparent component") - .script() - .to_bytes(), - ); - - // The HashMap returns funding streams in an arbitrary order, - // but Zebra's snapshot tests expect the same order every time. - - // zcashd sorts outputs in serialized data order, excluding the length field - coinbase_outputs.sort_by_key(|(_amount, script)| script.clone()); - - // The miner reward is always the first output independent of the sort order - coinbase_outputs.insert(0, (miner_reward, script)); - - coinbase_outputs -} - -// - Transaction roots processing - -/// Returns the default block roots for the supplied coinbase and mempool transactions, -/// and the supplied history tree. -/// -/// This function runs expensive cryptographic operations. -pub fn calculate_default_root_hashes( - current_nu: NetworkUpgrade, - coinbase_txn: &UnminedTx, - mempool_txs: &[VerifiedUnminedTx], - chain_history_root: ChainHistoryMmrRootHash, -) -> DefaultRoots { - let block_txs = || iter::once(coinbase_txn).chain(mempool_txs.iter().map(|tx| &tx.transaction)); - let merkle_root = block_txs().cloned().collect(); - let auth_data_root = block_txs().cloned().collect(); - - let block_commitments_hash = if current_nu == NetworkUpgrade::Heartwood - && chain_history_root == block::CHAIN_HISTORY_ACTIVATION_RESERVED.into() - { - block::CHAIN_HISTORY_ACTIVATION_RESERVED.into() - } else { - ChainHistoryBlockTxAuthCommitmentHash::from_commitments( - &chain_history_root, - &auth_data_root, - ) - }; - - DefaultRoots { - merkle_root, - chain_history_root, - auth_data_root, - block_commitments_hash, - } -} diff --git a/zebra-rpc/src/methods/types/get_block_template/constants.rs b/zebra-rpc/src/methods/types/get_block_template/constants.rs index 3513f4b9a0b..598b5221442 100644 --- a/zebra-rpc/src/methods/types/get_block_template/constants.rs +++ b/zebra-rpc/src/methods/types/get_block_template/constants.rs @@ -2,11 +2,15 @@ use jsonrpsee_types::ErrorCode; +use zcash_transparent::coinbase::{MAX_COINBASE_HEIGHT_LEN, MAX_COINBASE_SCRIPT_LEN}; use zebra_chain::{ block, parameters::subsidy::FundingStreamReceiver::{self, *}, }; +/// The maximum length of the optional, arbitrary data in the script sig field of a coinbase tx. +pub const MAX_MINER_DATA_LEN: usize = MAX_COINBASE_SCRIPT_LEN - MAX_COINBASE_HEIGHT_LEN; + /// When long polling, the amount of time we wait between mempool queries. /// (And sync status queries, which we do right before mempool queries.) /// diff --git a/zebra-rpc/src/methods/types/get_block_template/proposal.rs b/zebra-rpc/src/methods/types/get_block_template/proposal.rs index 58bd4b3124c..90abfa8975e 100644 --- a/zebra-rpc/src/methods/types/get_block_template/proposal.rs +++ b/zebra-rpc/src/methods/types/get_block_template/proposal.rs @@ -212,9 +212,11 @@ pub fn proposal_block_from_template( let commitment_bytes = match NetworkUpgrade::current(net, height) { NetworkUpgrade::Canopy => chain_history_root.bytes_in_serialized_order(), - NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 | NetworkUpgrade::Nu7 => { - block_commitments_hash.bytes_in_serialized_order() - } + NetworkUpgrade::Nu5 + | NetworkUpgrade::Nu6 + | NetworkUpgrade::Nu6_1 + | NetworkUpgrade::Nu6_2 + | NetworkUpgrade::Nu7 => block_commitments_hash.bytes_in_serialized_order(), _ => Err(SerializationError::Parse( "Zebra does not support generating pre-Canopy block templates", ))?, diff --git a/zebra-rpc/src/methods/types/get_block_template/tests.rs b/zebra-rpc/src/methods/types/get_block_template/tests.rs index 1bf8ae7516b..71bb7d801d3 100644 --- a/zebra-rpc/src/methods/types/get_block_template/tests.rs +++ b/zebra-rpc/src/methods/types/get_block_template/tests.rs @@ -1,45 +1,98 @@ //! Tests for types and functions for the `getblocktemplate` RPC. +use anyhow::anyhow; +use std::iter; +use zebra_chain::amount::Amount; + +use strum::IntoEnumIterator; use zcash_keys::address::Address; -use zcash_transparent::address::TransparentAddress; + +use zebra_chain::parameters::testnet::ConfiguredFundingStreamRecipient; use zebra_chain::{ - amount::Amount, block::Height, - parameters::testnet::{self, ConfiguredActivationHeights, ConfiguredFundingStreams}, - serialization::{ZcashDeserializeInto, ZcashSerialize}, + parameters::{ + subsidy::FundingStreamReceiver::{Deferred, Ecc, MajorGrants, ZcashFoundation}, + testnet::{self, ConfiguredActivationHeights, ConfiguredFundingStreams}, + Network, NetworkUpgrade, + }, + serialization::ZcashDeserializeInto, transaction::Transaction, }; -use super::standard_coinbase_outputs; +use crate::client::TransactionTemplate; +use crate::config::mining::{default_miner_address, MinerAddressType}; + +use super::MinerParams; -/// Tests that a minimal coinbase transaction can be generated. +/// Tests that coinbase transactions can be generated. +/// +/// This test needs to be run with the `--release` flag so that it runs for ~ 30 seconds instead of +/// ~ 90. #[test] -fn minimal_coinbase() -> Result<(), Box> { +#[ignore] +fn coinbase() -> anyhow::Result<()> { let regtest = testnet::Parameters::build() .with_slow_start_interval(Height::MIN) .with_activation_heights(ConfiguredActivationHeights { - nu6: Some(1), + overwinter: Some(1), + sapling: Some(2), + blossom: Some(3), + heartwood: Some(4), + canopy: Some(5), + nu5: Some(6), + nu6: Some(7), + nu6_1: Some(8), + nu7: Some(9), ..Default::default() })? - .with_funding_streams(vec![ConfiguredFundingStreams { - height_range: Some(Height(1)..Height(10)), - recipients: None, - }]) + .with_funding_streams(vec![ + ConfiguredFundingStreams { + height_range: Some(Height(1)..Height(100)), + recipients: Some(vec![ + ConfiguredFundingStreamRecipient::new_for(Ecc), + ConfiguredFundingStreamRecipient::new_for(ZcashFoundation), + ConfiguredFundingStreamRecipient::new_for(MajorGrants), + ]), + }, + ConfiguredFundingStreams { + height_range: Some(Height(1)..Height(100)), + recipients: Some(vec![ + ConfiguredFundingStreamRecipient::new_for(MajorGrants), + ConfiguredFundingStreamRecipient { + receiver: Deferred, + numerator: 12, + addresses: None, + }, + ]), + }, + ]) .to_network()?; - let outputs = standard_coinbase_outputs( - ®test, - Height(1), - &Address::from(TransparentAddress::PublicKeyHash([0x42; 20])), - Amount::zero(), - ); - - // It should be possible to generate a coinbase tx from these params. - Transaction::new_v5_coinbase(®test, Height(1), outputs, vec![]) - .zcash_serialize_to_vec()? - // Deserialization contains checks for elementary consensus rules, which must pass. - .zcash_deserialize_into::()?; + for net in Network::iter().chain(iter::once(regtest)) { + for nu in NetworkUpgrade::iter().filter(|nu| nu >= &NetworkUpgrade::Sapling) { + if let Some(height) = nu.activation_height(&net) { + for addr_type in MinerAddressType::iter() { + TransactionTemplate::new_coinbase( + &net, + height, + &MinerParams::from( + Address::decode(&net, default_miner_address(net.kind(), &addr_type)) + .ok_or(anyhow!("hard-coded addr must be valid"))?, + ), + Amount::zero(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + None, + )? + .data() + .as_ref() + // Deserialization contains checks for elementary consensus rules, which must + // pass. + .zcash_deserialize_into::()?; + } + } + } + } Ok(()) } diff --git a/zebra-rpc/src/methods/types/get_block_template/zip317.rs b/zebra-rpc/src/methods/types/get_block_template/zip317.rs index 5747cf18e88..6139ac83287 100644 --- a/zebra-rpc/src/methods/types/get_block_template/zip317.rs +++ b/zebra-rpc/src/methods/types/get_block_template/zip317.rs @@ -13,13 +13,11 @@ use rand::{ prelude::thread_rng, }; -use zcash_keys::address::Address; - use zebra_chain::{ - amount::NegativeOrZero, + amount::Amount, block::{Height, MAX_BLOCK_BYTES}, parameters::Network, - transaction::{self, zip317::BLOCK_UNPAID_ACTION_LIMIT, Transaction, VerifiedUnminedTx}, + transaction::{self, zip317::BLOCK_UNPAID_ACTION_LIMIT, VerifiedUnminedTx}, }; use zebra_consensus::MAX_BLOCK_SIGOPS; use zebra_node_services::mempool::TransactionDependencies; @@ -27,10 +25,7 @@ use zebra_node_services::mempool::TransactionDependencies; use crate::methods::types::transaction::TransactionTemplate; #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use crate::methods::Amount; - -#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use zebra_chain::{amount::NonNegative, parameters::NetworkUpgrade}; +use zebra_chain::amount::NonNegative; #[cfg(test)] mod tests; @@ -38,7 +33,7 @@ mod tests; #[cfg(test)] use crate::methods::types::get_block_template::InBlockTxDependenciesDepth; -use super::standard_coinbase_outputs; +use super::MinerParams; /// Used in the return type of [`select_mempool_transactions()`] for test compilations. #[cfg(test)] @@ -60,26 +55,26 @@ type SelectedMempoolTx = VerifiedUnminedTx; /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production #[allow(clippy::too_many_arguments)] pub fn select_mempool_transactions( - network: &Network, - next_block_height: Height, - miner_address: &Address, + net: &Network, + height: Height, + miner_params: &MinerParams, mempool_txs: Vec, mempool_tx_deps: TransactionDependencies, - extra_coinbase_data: Vec, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< Amount, >, ) -> Vec { // Use a fake coinbase transaction to break the dependency between transaction // selection, the miner fee, and the fee payment in the coinbase transaction. - let fake_coinbase_tx = fake_coinbase_transaction( - network, - next_block_height, - miner_address, - extra_coinbase_data, + let fake_coinbase_tx = TransactionTemplate::new_coinbase( + net, + height, + miner_params, + Amount::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount, - ); + ) + .expect("valid coinbase transaction template"); let tx_dependencies = mempool_tx_deps.dependencies(); let (independent_mempool_txs, mut dependent_mempool_txs): (HashMap<_, _>, HashMap<_, _>) = @@ -142,51 +137,6 @@ pub fn select_mempool_transactions( selected_txs } -/// Returns a fake coinbase transaction that can be used during transaction selection. -/// -/// This avoids a data dependency loop involving the selected transactions, the miner fee, -/// and the coinbase transaction. -/// -/// This transaction's serialized size and sigops must be at least as large as the real coinbase -/// transaction with the correct height and fee. -pub fn fake_coinbase_transaction( - net: &Network, - height: Height, - miner_address: &Address, - extra_coinbase_data: Vec, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< - Amount, - >, -) -> TransactionTemplate { - // Block heights are encoded as variable-length (script) and `u32` (lock time, expiry height). - // They can also change the `u32` consensus branch id. - // We use the template height here, which has the correct byte length. - // https://zips.z.cash/protocol/protocol.pdf#txnconsensus - // https://github.com/zcash/zips/blob/main/zip-0203.rst#changes-for-nu5 - // - // Transparent amounts are encoded as `i64`, - // so one zat has the same size as the real amount: - // https://developer.bitcoin.org/reference/transactions.html#txout-a-transaction-output - let miner_fee = 1.try_into().expect("amount is valid and non-negative"); - let outputs = standard_coinbase_outputs(net, height, miner_address, miner_fee); - - #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] - let coinbase = Transaction::new_v5_coinbase(net, height, outputs, extra_coinbase_data).into(); - - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - let coinbase = { - let network_upgrade = NetworkUpgrade::current(net, height); - if network_upgrade < NetworkUpgrade::Nu7 { - Transaction::new_v5_coinbase(net, height, outputs, extra_coinbase_data).into() - } else { - Transaction::new_v6_coinbase(net, height, outputs, extra_coinbase_data, zip233_amount) - .into() - } - }; - - TransactionTemplate::from_coinbase(&coinbase, miner_fee) -} - /// Returns a fee-weighted index and the total weight of `transactions`. /// /// Returns `None` if there are no transactions, or if the weights are invalid. @@ -379,14 +329,17 @@ impl TryUpdateBlockLimits for VerifiedUnminedTx { // > and block_unpaid_actions <= block_unpaid_action_limit, // > add the transaction to the block template // - // Unpaid actions are always zero for transactions that pay the conventional fee, - // so the unpaid action check always passes for those transactions. + // Unpaid actions are always zero for transactions that pay the conventional fee, so the + // unpaid action check always passes for those transactions. Use the full block-level sigop + // count (legacy + P2SH) so template selection cannot produce blocks that the block verifier + // would reject for exceeding `MAX_BLOCK_SIGOPS`. + let tx_block_sigops = self.block_sigop_count(); if self.transaction.size <= *remaining_block_bytes - && self.legacy_sigop_count <= *remaining_block_sigops + && tx_block_sigops <= *remaining_block_sigops && self.unpaid_actions <= *remaining_block_unpaid_actions { *remaining_block_bytes -= self.transaction.size; - *remaining_block_sigops -= self.legacy_sigop_count; + *remaining_block_sigops -= tx_block_sigops; // Unpaid actions are always zero for transactions that pay the conventional fee, // so this limit always remains the same after they are added. diff --git a/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs b/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs index 04c5e7fe50a..58ae4ac4590 100644 --- a/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs +++ b/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs @@ -3,20 +3,19 @@ #![allow(clippy::unwrap_in_result)] use zcash_keys::address::Address; - use zcash_transparent::address::TransparentAddress; + use zebra_chain::{block::Height, parameters::Network, transaction, transparent::OutPoint}; use zebra_node_services::mempool::TransactionDependencies; +use crate::methods::types::get_block_template::MinerParams; + use super::select_mempool_transactions; #[test] fn excludes_tx_with_unselected_dependencies() { let network = Network::Mainnet; - let next_block_height = Height(1_000_000); - let extra_coinbase_data = Vec::new(); let mut mempool_tx_deps = TransactionDependencies::default(); - let miner_address = Address::from(TransparentAddress::PublicKeyHash([0x7e; 20])); let unmined_tx = network .unmined_transactions_in_blocks(..) @@ -31,11 +30,10 @@ fn excludes_tx_with_unselected_dependencies() { assert_eq!( select_mempool_transactions( &network, - next_block_height, - &miner_address, + Height(1_000_000), + &MinerParams::from(Address::from(TransparentAddress::PublicKeyHash([0x7e; 20]))), vec![unmined_tx], mempool_tx_deps, - extra_coinbase_data, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, ), @@ -47,9 +45,7 @@ fn excludes_tx_with_unselected_dependencies() { #[test] fn includes_tx_with_selected_dependencies() { let network = Network::Mainnet; - let next_block_height = Height(1_000_000); let unmined_txs: Vec<_> = network.unmined_transactions_in_blocks(..).take(3).collect(); - let miner_address = Address::from(TransparentAddress::PublicKeyHash([0x7e; 20])); let dependent_tx1 = unmined_txs.first().expect("should have 3 txns"); let dependent_tx2 = unmined_txs.get(1).expect("should have 3 txns"); @@ -73,15 +69,12 @@ fn includes_tx_with_selected_dependencies() { ], ); - let extra_coinbase_data = Vec::new(); - let selected_txs = select_mempool_transactions( &network, - next_block_height, - &miner_address, + Height(1_000_000), + &MinerParams::from(Address::from(TransparentAddress::PublicKeyHash([0x7e; 20]))), unmined_txs.clone(), mempool_tx_deps.clone(), - extra_coinbase_data, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, ); diff --git a/zebra-rpc/src/methods/types/get_raw_mempool.rs b/zebra-rpc/src/methods/types/get_raw_mempool.rs index f19f9a3d215..3776da46228 100644 --- a/zebra-rpc/src/methods/types/get_raw_mempool.rs +++ b/zebra-rpc/src/methods/types/get_raw_mempool.rs @@ -6,7 +6,11 @@ use derive_getters::Getters; use derive_new::new; use hex::ToHex as _; -use zebra_chain::{amount::NonNegative, block::Height, transaction::VerifiedUnminedTx}; +use zebra_chain::{ + amount::NonNegative, + block::Height, + transaction::{Hash, VerifiedUnminedTx}, +}; use zebra_node_services::mempool::TransactionDependencies; use super::zec::Zec; @@ -58,15 +62,9 @@ pub struct MempoolObject { impl MempoolObject { pub(crate) fn from_verified_unmined_tx( unmined_tx: &VerifiedUnminedTx, - transactions: &[VerifiedUnminedTx], + transactions_by_id: &HashMap, transaction_dependencies: &TransactionDependencies, ) -> Self { - // Map transactions by their txids to make lookups easier - let transactions_by_id = transactions - .iter() - .map(|unmined_tx| (unmined_tx.transaction.id.mined_id(), unmined_tx)) - .collect::>(); - // Get txids of this transaction's descendants (dependents) let empty_set = HashSet::new(); let deps = transaction_dependencies diff --git a/zebra-rpc/src/methods/types/long_poll.rs b/zebra-rpc/src/methods/types/long_poll.rs index b174275ed54..5369762473f 100644 --- a/zebra-rpc/src/methods/types/long_poll.rs +++ b/zebra-rpc/src/methods/types/long_poll.rs @@ -16,6 +16,9 @@ use zebra_chain::{ }; use zebra_node_services::BoxError; +#[cfg(test)] +mod tests; + /// The length of a serialized [`LongPollId`] string. /// /// This is an internal Zebra implementation detail, which does not need to match `zcashd`. @@ -267,9 +270,13 @@ impl FromStr for LongPollId { /// Exact conversion from a string to LongPollId. fn from_str(long_poll_id: &str) -> Result { - if long_poll_id.len() != LONG_POLL_ID_LENGTH { + // A well-formed `LongPollId` is exactly `LONG_POLL_ID_LENGTH` ASCII digits/hex + // characters (see `Display` above). Requiring ASCII here means each field's byte + // range is also a valid UTF-8 char boundary, so the slices below cannot panic on + // attacker-controlled input containing multibyte characters. + if long_poll_id.len() != LONG_POLL_ID_LENGTH || !long_poll_id.is_ascii() { return Err(format!( - "incorrect long poll id length, must be {LONG_POLL_ID_LENGTH} for Zebra" + "invalid long poll id, must be {LONG_POLL_ID_LENGTH} ASCII digits / hex chars" ) .into()); } @@ -301,28 +308,3 @@ impl TryFrom for LongPollId { s.parse() } } - -/// Check that [`LongPollInput::new`] will sort mempool transaction ids. -/// -/// The mempool does not currently guarantee the order in which it will return transactions and -/// may return the same items in a different order, while the long poll id should be the same if -/// its other components are equal and no transactions have been added or removed in the mempool. -#[test] -fn long_poll_input_mempool_tx_ids_are_sorted() { - let mempool_tx_ids = || { - (0..10) - .map(|i| transaction::Hash::from([i; 32])) - .map(UnminedTxId::Legacy) - }; - - assert_eq!( - LongPollInput::new(Height::MIN, Default::default(), 0.into(), mempool_tx_ids()), - LongPollInput::new( - Height::MIN, - Default::default(), - 0.into(), - mempool_tx_ids().rev() - ), - "long poll input should sort mempool tx ids" - ); -} diff --git a/zebra-rpc/src/methods/types/long_poll/tests.rs b/zebra-rpc/src/methods/types/long_poll/tests.rs new file mode 100644 index 00000000000..4cf9127603a --- /dev/null +++ b/zebra-rpc/src/methods/types/long_poll/tests.rs @@ -0,0 +1,95 @@ +//! Tests for the long-poll ID type used by the `getblocktemplate` RPC. + +use std::str::FromStr; + +use zebra_chain::{block::Height, transaction}; + +use super::{LongPollId, LongPollInput, LONG_POLL_ID_LENGTH}; + +/// Check that [`LongPollInput::new`] will sort mempool transaction ids. +/// +/// The mempool does not currently guarantee the order in which it will return transactions and +/// may return the same items in a different order, while the long poll id should be the same if +/// its other components are equal and no transactions have been added or removed in the mempool. +#[test] +fn long_poll_input_mempool_tx_ids_are_sorted() { + use zebra_chain::transaction::UnminedTxId; + + let mempool_tx_ids = || { + (0..10) + .map(|i| transaction::Hash::from([i; 32])) + .map(UnminedTxId::Legacy) + }; + + assert_eq!( + LongPollInput::new(Height::MIN, Default::default(), 0.into(), mempool_tx_ids()), + LongPollInput::new( + Height::MIN, + Default::default(), + 0.into(), + mempool_tx_ids().rev() + ), + "long poll input should sort mempool tx ids" + ); +} + +/// Check that `LongPollId::from_str` rejects strings with the correct byte length but +/// non-ASCII content, instead of panicking when its fixed byte-offset slices cross a UTF-8 +/// char boundary. +/// +/// Regression test for +/// [GHSA-qv2r-v3mx-f4pf](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-qv2r-v3mx-f4pf). +#[test] +fn long_poll_id_rejects_non_ascii_at_each_field_boundary() { + // `é` is two UTF-8 bytes, so each of these strings is exactly `LONG_POLL_ID_LENGTH` + // bytes long with the `é` straddling one of the parser's fixed byte offsets: + // 10, 18, 28, or 38. + let boundary_inputs = [ + // boundary 10: between tip_height and tip_hash_checksum + format!("{}é{}", "0".repeat(9), "0".repeat(35)), + // boundary 18: between tip_hash_checksum and max_timestamp + format!("{}é{}", "0".repeat(17), "0".repeat(27)), + // boundary 28: between max_timestamp and mempool_transaction_count + format!("{}é{}", "0".repeat(27), "0".repeat(17)), + // boundary 38: between mempool_transaction_count and + // mempool_transaction_content_checksum + format!("{}é{}", "0".repeat(37), "0".repeat(7)), + ]; + + for input in boundary_inputs { + assert_eq!( + input.len(), + LONG_POLL_ID_LENGTH, + "test input must be exactly LONG_POLL_ID_LENGTH bytes", + ); + let result = LongPollId::from_str(&input); + assert!( + result.is_err(), + "non-ASCII long poll id must return an error, got {result:?} for {input:?}", + ); + } +} + +/// Check that `LongPollId::from_str` round-trips a well-formed ASCII id. +#[test] +fn long_poll_id_round_trip_ascii() { + let id = LongPollId { + tip_height: 1234567890, + tip_hash_checksum: 0xdeadbeef, + max_timestamp: 4000000000, + mempool_transaction_count: 42, + mempool_transaction_content_checksum: 0x0badf00d, + }; + let s = id.to_string(); + assert_eq!(s.len(), LONG_POLL_ID_LENGTH); + assert_eq!(LongPollId::from_str(&s).unwrap(), id); +} + +/// Check that `LongPollId::from_str` rejects inputs whose byte length does not match +/// `LONG_POLL_ID_LENGTH`. +#[test] +fn long_poll_id_rejects_wrong_length() { + assert!(LongPollId::from_str("").is_err()); + assert!(LongPollId::from_str(&"0".repeat(LONG_POLL_ID_LENGTH - 1)).is_err()); + assert!(LongPollId::from_str(&"0".repeat(LONG_POLL_ID_LENGTH + 1)).is_err()); +} diff --git a/zebra-rpc/src/methods/types/peer_info.rs b/zebra-rpc/src/methods/types/peer_info.rs index 0aa62b2abb0..1750eff3bc3 100644 --- a/zebra-rpc/src/methods/types/peer_info.rs +++ b/zebra-rpc/src/methods/types/peer_info.rs @@ -6,14 +6,33 @@ use zebra_network::{types::MetaAddr, PeerSocketAddr}; /// Item of the `getpeerinfo` response #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)] +#[allow(clippy::too_many_arguments)] pub struct PeerInfo { /// The IP address and port of the peer #[getter(copy)] pub(crate) addr: PeerSocketAddr, + /// The services offered, as a zero-padded 16-character hex string + pub(crate) services: String, + + /// The time in seconds since epoch of the last message received from this peer + pub(crate) lastrecv: u32, + /// Inbound (true) or Outbound (false) pub(crate) inbound: bool, + /// The ban score for misbehavior + pub(crate) banscore: u32, + + /// The peer's user agent string (e.g. "/Zebra:2.1.0/" or "/MagicBean:5.8.0/") + pub(crate) subver: String, + + /// The negotiated protocol version + pub(crate) version: u32, + + /// The connection state (e.g. "connected", "never_connected", "failed", "connecting") + pub(crate) connection_state: String, + /// The round-trip ping time in seconds. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) pingtime: Option, @@ -28,9 +47,28 @@ pub type GetPeerInfoResponse = Vec; impl From for PeerInfo { fn from(meta_addr: MetaAddr) -> Self { + let services = meta_addr + .services() + .map(|s| format!("{:016x}", s.bits())) + .unwrap_or_else(|| "0000000000000000".to_string()); + + let lastrecv = meta_addr.last_seen().map(|t| t.timestamp()).unwrap_or(0); + + let connection_state = meta_addr.last_connection_state().to_string(); + + let subver = meta_addr.user_agent().unwrap_or("").to_string(); + + let version = meta_addr.negotiated_version().map(|v| v.0).unwrap_or(0); + Self { addr: meta_addr.addr(), + services, + lastrecv, inbound: meta_addr.is_inbound(), + banscore: meta_addr.misbehavior(), + subver, + version, + connection_state, pingtime: meta_addr.rtt().map(|d| d.as_secs_f64()), pingwait: meta_addr.ping_sent_at().map(|t| t.elapsed().as_secs_f64()), } @@ -41,7 +79,13 @@ impl Default for PeerInfo { fn default() -> Self { Self { addr: PeerSocketAddr::unspecified(), + services: "0000000000000000".to_string(), + lastrecv: 0, inbound: false, + banscore: 0, + subver: String::new(), + version: 0, + connection_state: String::new(), pingtime: None, pingwait: None, } diff --git a/zebra-rpc/src/methods/types/transaction.rs b/zebra-rpc/src/methods/types/transaction.rs index 4e0e40e289f..bc84faa02ce 100644 --- a/zebra-rpc/src/methods/types/transaction.rs +++ b/zebra-rpc/src/methods/types/transaction.rs @@ -7,24 +7,36 @@ use chrono::{DateTime, Utc}; use derive_getters::Getters; use derive_new::new; use hex::ToHex; - +use rand::rngs::OsRng; use zcash_script::script::Asm; + +use zcash_keys::address::Address; +use zcash_primitives::transaction::{ + builder::{BuildConfig, Builder}, + fees::fixed::FeeRule, +}; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, value::Zatoshis}; use zebra_chain::{ amount::{self, Amount, NegativeAllowed, NegativeOrZero, NonNegative}, block::{self, merkle::AUTH_DIGEST_PLACEHOLDER, Height}, orchard, - parameters::Network, + parameters::{ + subsidy::{block_subsidy, funding_stream_values, miner_subsidy}, + Network, + }, primitives::ed25519, sapling::ValueCommitment, serialization::ZcashSerialize, - transaction::{self, SerializedTransaction, Transaction, UnminedTx, VerifiedUnminedTx}, + transaction::{self, SerializedTransaction, Transaction, VerifiedUnminedTx}, transparent::Script, }; +use zebra_consensus::{error::TransactionError, funding_stream_address}; use zebra_script::Sigops; use zebra_state::IntoDisk; -use super::super::opthex; use super::zec::Zec; +use super::{super::opthex, get_block_template::MinerParams}; /// Transaction data and fields needed to generate blocks using the `getblocktemplate` RPC. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)] @@ -95,7 +107,9 @@ impl From<&VerifiedUnminedTx> for TransactionTemplate { fee: tx.miner_fee, - sigops: tx.legacy_sigop_count, + // Report the full block-level sigop count (legacy + P2SH) so the template `sigops` + // field matches what the block verifier charges against `MAX_BLOCK_SIGOPS`. + sigops: tx.block_sigop_count(), // Zebra does not require any transactions except the coinbase transaction. required: false, @@ -110,37 +124,151 @@ impl From for TransactionTemplate { } impl TransactionTemplate { - /// Convert from a generated coinbase transaction into a coinbase transaction template. - /// - /// `miner_fee` is the total miner fees for the block, excluding newly created block rewards. - // - // TODO: use a different type for generated coinbase transactions? - pub fn from_coinbase(tx: &UnminedTx, miner_fee: Amount) -> Self { - assert!( - tx.transaction.is_coinbase(), - "invalid generated coinbase transaction: \ - must have exactly one input, which must be a coinbase input", + /// Constructs a transaction template for a coinbase transaction. + pub fn new_coinbase( + net: &Network, + height: Height, + miner_params: &MinerParams, + txs_fee: Amount, + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< + Amount, + >, + ) -> Result { + let block_subsidy = block_subsidy(height, net)?; + let miner_reward = miner_subsidy(height, net, block_subsidy)? + txs_fee; + let miner_reward = Zatoshis::try_from(miner_reward?)?; + + let mut builder = Builder::new( + net, + BlockHeight::from(height), + BuildConfig::Coinbase { + miner_data: miner_params.data().clone(), + }, ); - let miner_fee = (-miner_fee) - .constrain() - .expect("negating a NonNegative amount always results in a valid NegativeOrZero"); + let default_memo = MemoBytes::empty(); + let memo = miner_params.memo().unwrap_or(&default_memo); + + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + // The upstream ZIP 235 default here doesn't compile, and fixing it would also + // require deducting the burn from `miner_reward` above. Use zero for now: + // ZIP 233 was dropped from v6, and upstream has since removed this code. + // FIXME: drop this block during the next merge. + let zip233_amount = zip233_amount.unwrap_or(Amount::zero()); + builder.set_zip233_amount(Zatoshis::try_from(zip233_amount)?); + } - Self { - data: tx.transaction.as_ref().into(), - hash: tx.id.mined_id(), - auth_digest: tx.id.auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER), + macro_rules! trace_err { + ($res:expr, $type:expr) => { + $res.map_err(|err| tracing::error!("Failed to add {} output: {err}", $type)) + .ok() + }; + } - // Always empty, coinbase transactions never have inputs. - depends: Vec::new(), + let add_orchard_reward = |builder: &mut Builder<'_, _, _>, addr: &_| { + trace_err!( + builder.add_orchard_output::( + Some(::orchard::keys::OutgoingViewingKey::from([0u8; 32])), + *addr, + miner_reward, + // A coinbase pays the miner reward in native ZEC. Custom assets can only + // enter circulation via an issuance bundle, which a coinbase never has. + ::orchard::note::AssetBase::zatoshi(), + memo.clone(), + ), + "Orchard" + ) + }; + + let add_sapling_reward = |builder: &mut Builder<'_, _, _>, addr: &_| { + trace_err!( + builder.add_sapling_output::( + Some(sapling_crypto::keys::OutgoingViewingKey([0u8; 32])), + *addr, + miner_reward, + memo.clone(), + ), + "Sapling" + ) + }; + + let add_transparent_reward = |builder: &mut Builder<'_, _, _>, addr| { + trace_err!( + builder.add_transparent_output(addr, miner_reward), + "transparent" + ) + }; + + match miner_params.addr() { + Address::Unified(addr) => addr + .orchard() + .and_then(|addr| add_orchard_reward(&mut builder, addr)) + .or_else(|| { + addr.sapling() + .and_then(|addr| add_sapling_reward(&mut builder, addr)) + }) + .or_else(|| { + addr.transparent() + .and_then(|addr| add_transparent_reward(&mut builder, addr)) + }), - fee: miner_fee, + Address::Sapling(addr) => add_sapling_reward(&mut builder, addr), - sigops: tx.sigops().expect("sigops count should be valid"), + Address::Transparent(addr) => add_transparent_reward(&mut builder, addr), - // Zcash requires a coinbase transaction. - required: true, + _ => Err(TransactionError::CoinbaseConstruction( + "Address not supported for miner rewards".to_string(), + ))?, } + .ok_or(TransactionError::CoinbaseConstruction( + "Could not construct output with miner reward".to_string(), + ))?; + + let mut funding_streams = funding_stream_values(height, net, block_subsidy)? + .into_iter() + .filter_map(|(receiver, amount)| { + Some((*funding_stream_address(height, net, receiver)?, amount)) + }) + .chain(net.lockbox_disbursements(height)) + .filter_map(|(addr, amount)| { + Some((Zatoshis::try_from(amount).ok()?, addr.try_into().ok()?)) + }) + .collect::>(); + + funding_streams.sort(); + + for (fs_amount, fs_addr) in funding_streams { + builder.add_transparent_output(&fs_addr, fs_amount)?; + } + + let sapling_prover = LocalTxProver::bundled(); + let build_result = builder.build( + &Default::default(), + Default::default(), + Default::default(), + OsRng, + &sapling_prover, + &sapling_prover, + &FeeRule::non_standard(Zatoshis::ZERO), + // No asset is ever newly issued in a coinbase: its only output asset is native ZEC. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + |_| false, + )?; + + let tx = build_result.transaction(); + let mut data = vec![]; + tx.write(&mut data)?; + + Ok(Self { + data: data.into(), + hash: tx.txid().as_ref().into(), + auth_digest: tx.auth_commitment().as_ref().try_into()?, + depends: Vec::new(), + fee: (-txs_fee).constrain()?, + sigops: tx.sigops()?, + required: true, + }) } } diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index 3faea551f05..d183e624f47 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -14,7 +14,10 @@ use jsonrpsee::server::{middleware::rpc::RpcServiceBuilder, Server, ServerHandle use tokio::task::JoinHandle; use tracing::*; -use zebra_chain::{chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::Network}; +use zebra_chain::{ + block::MAX_BLOCK_BYTES, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, + parameters::Network, +}; use zebra_consensus::router::service_trait::BlockVerifierService; use zebra_network::AddressBookPeers; use zebra_node_services::mempool::MempoolService; @@ -117,13 +120,17 @@ impl RpcServer { .listen_addr .expect("caller should make sure listen_addr is set"); + // The largest RPC request is submitblock, which sends a full block + // as a hex string (2x MAX_BLOCK_BYTES) plus a small JSON-RPC wrapper. + let max_request_body_size = (MAX_BLOCK_BYTES as usize) * 2 + 1024; + let http_middleware_layer = if conf.enable_cookie_auth { let cookie = Cookie::default(); cookie::write_to_disk(&cookie, &conf.cookie_dir) .expect("Zebra must be able to write the auth cookie to the disk"); - HttpRequestMiddlewareLayer::new(Some(cookie)) + HttpRequestMiddlewareLayer::new(Some(cookie), max_request_body_size) } else { - HttpRequestMiddlewareLayer::new(None) + HttpRequestMiddlewareLayer::new(None, max_request_body_size) }; let http_middleware = tower::ServiceBuilder::new().layer(http_middleware_layer); diff --git a/zebra-rpc/src/server/cookie.rs b/zebra-rpc/src/server/cookie.rs index 407be8efbd2..20f52f34b77 100644 --- a/zebra-rpc/src/server/cookie.rs +++ b/zebra-rpc/src/server/cookie.rs @@ -10,6 +10,9 @@ use std::{ path::Path, }; +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; + /// The name of the cookie file on the disk const FILE: &str = ".cookie"; @@ -34,16 +37,47 @@ impl Default for Cookie { } /// Writes the given cookie to the given dir. +/// +/// Uses restrictive file permissions (0600 on Unix) to prevent other +/// local users from reading the cookie secret. pub fn write_to_disk(cookie: &Cookie, dir: &Path) -> Result<()> { - // Create the directory if needed. std::fs::create_dir_all(dir)?; - File::create(dir.join(FILE))?.write_all(format!("__cookie__:{}", cookie.0).as_bytes())?; + + let cookie_path = dir.join(FILE); + + if cookie_path + .symlink_metadata() + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false) + { + return Err(color_eyre::eyre::eyre!( + "cookie path {cookie_path:?} is a symlink, refusing to write" + )); + } + + let mut file = create_owner_only_file(&cookie_path)?; + file.write_all(format!("__cookie__:{}", cookie.0).as_bytes())?; tracing::info!("RPC auth cookie written to disk"); Ok(()) } +/// Creates a file readable and writable only by the owner. +/// +/// On Unix, this sets mode 0600 regardless of umask. +/// On Windows, default ACLs already restrict access to the creating user, +/// so no explicit hardening is needed. +fn create_owner_only_file(path: &Path) -> Result { + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + + #[cfg(unix)] + opts.mode(0o600); + + Ok(opts.open(path)?) +} + /// Removes a cookie from the given dir. pub fn remove_from_disk(dir: &Path) -> Result<()> { remove_file(dir.join(FILE))?; diff --git a/zebra-rpc/src/server/http_request_compatibility.rs b/zebra-rpc/src/server/http_request_compatibility.rs index 0bce85f3597..488ecea13bc 100644 --- a/zebra-rpc/src/server/http_request_compatibility.rs +++ b/zebra-rpc/src/server/http_request_compatibility.rs @@ -7,7 +7,7 @@ use std::future::Future; use std::pin::Pin; use futures::{future, FutureExt}; -use http_body_util::BodyExt; +use http_body_util::{BodyExt, Limited}; use hyper::header; use jsonrpsee::{ core::BoxError, @@ -56,12 +56,17 @@ use base64::{engine::general_purpose::STANDARD, Engine as _}; pub struct HttpRequestMiddleware { service: S, cookie: Option, + max_request_body_size: usize, } impl HttpRequestMiddleware { - /// Create a new `HttpRequestMiddleware` with the given service and cookie. - pub fn new(service: S, cookie: Option) -> Self { - Self { service, cookie } + /// Create a new `HttpRequestMiddleware` with the given service, cookie, and request body size limit. + pub fn new(service: S, cookie: Option, max_request_body_size: usize) -> Self { + Self { + service, + cookie, + max_request_body_size, + } } /// Check if the request is authenticated. @@ -122,12 +127,12 @@ impl HttpRequestMiddleware { /// Maps whatever JSON-RPC version the client is using to JSON-RPC 2.0. async fn request_to_json_rpc_2( request: HttpRequest, - ) -> (JsonRpcVersion, HttpRequest) { + max_request_body_size: usize, + ) -> Result<(JsonRpcVersion, HttpRequest), BoxError> { let (parts, body) = request.into_parts(); - let bytes = body + let bytes = Limited::new(body, max_request_body_size) .collect() - .await - .expect("Failed to collect body data") + .await? .to_bytes(); let (version, bytes) = if let Ok(request) = serde_json::from_slice::<'_, JsonRpcRequest>(bytes.as_ref()) { @@ -143,22 +148,18 @@ impl HttpRequestMiddleware { } else { (JsonRpcVersion::Unknown, bytes) }; - ( + Ok(( version, HttpRequest::from_parts(parts, HttpBody::from(bytes.as_ref().to_vec())), - ) + )) } /// Maps JSON-2.0 to whatever JSON-RPC version the client is using. async fn response_from_json_rpc_2( version: JsonRpcVersion, response: HttpResponse, - ) -> HttpResponse { + ) -> Result, BoxError> { let (parts, body) = response.into_parts(); - let bytes = body - .collect() - .await - .expect("Failed to collect body data") - .to_bytes(); + let bytes = body.collect().await?.to_bytes(); let bytes = if let Ok(response) = serde_json::from_slice::<'_, JsonRpcResponse>(bytes.as_ref()) { serde_json::to_vec(&response.into_version(version)) @@ -167,7 +168,10 @@ impl HttpRequestMiddleware { } else { bytes }; - HttpResponse::from_parts(parts, HttpBody::from(bytes.as_ref().to_vec())) + Ok(HttpResponse::from_parts( + parts, + HttpBody::from(bytes.as_ref().to_vec()), + )) } } @@ -175,12 +179,16 @@ impl HttpRequestMiddleware { #[derive(Clone)] pub struct HttpRequestMiddlewareLayer { cookie: Option, + max_request_body_size: usize, } impl HttpRequestMiddlewareLayer { - /// Create a new `HttpRequestMiddlewareLayer` with the given cookie. - pub fn new(cookie: Option) -> Self { - Self { cookie } + /// Create a new `HttpRequestMiddlewareLayer` with the given cookie and request body size limit. + pub fn new(cookie: Option, max_request_body_size: usize) -> Self { + Self { + cookie, + max_request_body_size, + } } } @@ -188,20 +196,7 @@ impl tower::Layer for HttpRequestMiddlewareLayer { type Service = HttpRequestMiddleware; fn layer(&self, service: S) -> Self::Service { - HttpRequestMiddleware::new(service, self.cookie.clone()) - } -} - -/// A trait for updating an object, consuming it and returning the updated version. -pub trait With { - /// Updates `self` with an instance of type `T` and returns the updated version of `self`. - fn with(self, _: T) -> Self; -} - -impl With for HttpRequestMiddleware { - fn with(mut self, cookie: Cookie) -> Self { - self.cookie = Some(cookie); - self + HttpRequestMiddleware::new(service, self.cookie.clone(), self.max_request_body_size) } } @@ -235,11 +230,13 @@ where Self::insert_or_replace_content_type_header(request.headers_mut()); let mut service = self.service.clone(); + let max_request_body_size = self.max_request_body_size; async move { - let (version, request) = Self::request_to_json_rpc_2(request).await; + let (version, request) = + Self::request_to_json_rpc_2(request, max_request_body_size).await?; let response = service.call(request).await.map_err(Into::into)?; - Ok(Self::response_from_json_rpc_2(version, response).await) + Self::response_from_json_rpc_2(version, response).await } .boxed() } diff --git a/zebra-rpc/src/server/tests.rs b/zebra-rpc/src/server/tests.rs index fd44820ed8a..b46631c840a 100644 --- a/zebra-rpc/src/server/tests.rs +++ b/zebra-rpc/src/server/tests.rs @@ -2,4 +2,6 @@ #![allow(clippy::unwrap_in_result)] +mod cookie; +mod http_request_compatibility; mod vectors; diff --git a/zebra-rpc/src/server/tests/cookie.rs b/zebra-rpc/src/server/tests/cookie.rs new file mode 100644 index 00000000000..532894b9858 --- /dev/null +++ b/zebra-rpc/src/server/tests/cookie.rs @@ -0,0 +1,47 @@ +//! Tests for cookie file creation security. + +use std::fs; + +use super::super::cookie; +use crate::server::cookie::Cookie; + +#[test] +fn cookie_file_has_restrictive_permissions() { + let _init_guard = zebra_test::init(); + + let dir = tempfile::tempdir().unwrap(); + let cookie = Cookie::default(); + + cookie::write_to_disk(&cookie, dir.path()).unwrap(); + + let cookie_path = dir.path().join(".cookie"); + let metadata = fs::metadata(&cookie_path).unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = metadata.permissions().mode() & 0o777; + assert_eq!( + mode, 0o600, + "cookie file should have mode 0600, got {mode:o}" + ); + } + + assert!(metadata.len() > 0, "cookie file should not be empty"); +} + +#[cfg(unix)] +#[test] +fn cookie_write_rejects_symlink() { + let _init_guard = zebra_test::init(); + + let dir = tempfile::tempdir().unwrap(); + let target = dir.path().join("decoy"); + fs::write(&target, b"").unwrap(); + + std::os::unix::fs::symlink(&target, dir.path().join(".cookie")).unwrap(); + + let cookie = Cookie::default(); + let result = cookie::write_to_disk(&cookie, dir.path()); + assert!(result.is_err(), "should reject symlink at cookie path"); +} diff --git a/zebra-rpc/src/server/tests/http_request_compatibility.rs b/zebra-rpc/src/server/tests/http_request_compatibility.rs new file mode 100644 index 00000000000..154421de1d0 --- /dev/null +++ b/zebra-rpc/src/server/tests/http_request_compatibility.rs @@ -0,0 +1,92 @@ +//! Tests for the HTTP request compatibility middleware. + +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use http_body::Frame; +use jsonrpsee::{ + core::BoxError, + server::{HttpBody, HttpRequest, HttpResponse}, +}; +use tower::Service; + +use crate::server::http_request_compatibility::HttpRequestMiddleware; + +/// A body that always returns an error, simulating a TCP RST during body collection. +struct ErrorBody; + +impl http_body::Body for ErrorBody { + type Data = Bytes; + type Error = BoxError; + + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + Poll::Ready(Some(Err("connection reset".into()))) + } +} + +/// A mock inner service that returns a minimal JSON-RPC 2.0 response. +#[derive(Clone)] +struct MockRpcService; + +impl Service for MockRpcService { + type Response = HttpResponse; + type Error = BoxError; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _req: HttpRequest) -> Self::Future { + let body = r#"{"jsonrpc":"2.0","id":1,"result":null}"#; + let response = HttpResponse::new(HttpBody::from(body.to_string())); + Box::pin(async { Ok(response) }) + } +} + +/// Verifies that body collection errors return `Err` instead of panicking. +/// +/// Previously, the middleware called `.expect()` on `body.collect().await`, +/// so a TCP RST during body reading would panic the process. +#[tokio::test] +async fn request_body_error_returns_err_instead_of_panic() { + let error_body = HttpBody::new(ErrorBody); + let request = HttpRequest::builder() + .method("POST") + .header("content-type", "appliion/json") + .body(error_body) + .expect("valid request"); + + let mut middleware = HttpRequestMiddleware::new(MockRpcService, None, 2_097_152); + let result = middleware.call(request).await; + + assert!( + result.is_err(), + "body collection error should return Err, not panic" + ); +} + +/// Verifies that a request body exceeding `max_request_body_size` is rejected. +#[tokio::test] +async fn oversized_request_body_is_rejected() { + let limit = 64; + let oversized = vec![b'x'; limit + 1]; + let body = HttpBody::from(oversized); + let request = HttpRequest::builder() + .method("POST") + .header("content-type", "application/json") + .body(body) + .expect("valid request"); + + let mut middleware = HttpRequestMiddleware::new(MockRpcService, None, limit); + let result = middleware.call(request).await; + + assert!(result.is_err(), "oversized request body should be rejected"); +} diff --git a/zebra-rpc/tests/serialization_tests.rs b/zebra-rpc/tests/serialization_tests.rs index 4db5254bf47..6a8cddaf82a 100644 --- a/zebra-rpc/tests/serialization_tests.rs +++ b/zebra-rpc/tests/serialization_tests.rs @@ -8,7 +8,7 @@ mod vectors; -use std::{io::Cursor, ops::Deref}; +use std::io::Cursor; use vectors::{ GET_BLOCKCHAIN_INFO_RESPONSE, GET_BLOCK_RESPONSE_1, GET_BLOCK_RESPONSE_2, @@ -32,7 +32,7 @@ use zebra_rpc::client::{ GetBlockchainInfoResponse, GetInfoResponse, GetMiningInfoResponse, GetNetworkInfoResponse, GetPeerInfoResponse, GetRawMempoolResponse, GetRawTransactionResponse, GetSubtreesByIndexResponse, GetTreestateResponse, Hash, Input, JoinSplit, MempoolObject, - Orchard, OrchardAction, OrchardFlags, Output, PeerInfo, ScriptPubKey, ScriptSig, + Orchard, OrchardAction, OrchardFlags, Output, ScriptPubKey, ScriptSig, SendRawTransactionResponse, ShieldedOutput, ShieldedSpend, SubmitBlockErrorResponse, SubmitBlockResponse, SubtreeRpcData, TransactionObject, TransactionTemplate, Treestate, Utxo, ValidateAddressResponse, ZListUnifiedReceiversResponse, ZValidateAddressResponse, @@ -208,6 +208,7 @@ fn test_get_block_1() -> Result<(), Box> { let block_commitments = block.block_commitments(); let final_sapling_root = block.final_sapling_root(); let final_orchard_root = block.final_orchard_root(); + let n_tx = block.n_tx(); let tx = block .tx() .iter() @@ -254,6 +255,7 @@ fn test_get_block_1() -> Result<(), Box> { block_commitments, final_sapling_root, final_orchard_root, + n_tx, tx.iter() .map(|h| GetBlockTransaction::Hash(zebra_chain::transaction::Hash(*h))) .collect(), @@ -296,6 +298,7 @@ fn test_get_block_2() -> Result<(), Box> { let block_commitments = block.block_commitments(); let final_sapling_root = block.final_sapling_root(); let final_orchard_root = block.final_orchard_root(); + let n_tx = block.n_tx(); // We don't unpack the transaction object because we test that in the // get_raw_transaction test. let tx = block @@ -330,6 +333,7 @@ fn test_get_block_2() -> Result<(), Box> { block_commitments, final_sapling_root, final_orchard_root, + n_tx, tx.iter() .cloned() .map(GetBlockTransaction::Object) @@ -933,7 +937,7 @@ fn test_get_address_utxos_chain_info_false() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { [ { "addr": "192.168.0.1:8233", - "inbound": false + "services": "0000000000000001", + "lastrecv": 1700000000, + "inbound": false, + "banscore": 0, + "subver": "/Zebra:2.1.0/", + "version": 170140, + "connection_state": "connected" }, { "addr": "[2000:2000:2000:0000::]:8233", - "inbound": false + "services": "0000000000000001", + "lastrecv": 1700000000, + "inbound": false, + "banscore": 0, + "subver": "/zcashd:5.8.0/", + "version": 170100, + "connection_state": "connected" } ] "#; let obj: GetPeerInfoResponse = serde_json::from_str(json)?; - let addr0 = *obj[0].addr().deref(); - let inbound0 = obj[0].inbound(); - let addr1 = *obj[1].addr().deref(); - let inbound1 = obj[1].inbound(); - - let new_obj = vec![ - PeerInfo::new(addr0.into(), inbound0, None, None), - PeerInfo::new(addr1.into(), inbound1, None, None), - ]; - assert_eq!(obj, new_obj); + assert_eq!(obj.len(), 2); + assert_eq!(obj[0].services().as_str(), "0000000000000001"); + assert_eq!(obj[0].lastrecv(), 1700000000); + assert_eq!(obj[0].banscore(), 0); + assert_eq!(obj[0].subver().as_str(), "/Zebra:2.1.0/"); + assert_eq!(obj[0].version(), 170140); + assert_eq!(obj[0].connection_state().as_str(), "connected"); Ok(()) } @@ -1302,13 +1315,25 @@ fn test_get_peer_info_with_ping_values_serialization() -> Result<(), Box Result<(), Box Result<(), Box> { let specification = stream.specification().clone(); let value = stream.value(); let value_zat = stream.value_zat(); - let address = stream.address().clone(); + let address = stream.address(); - FundingStream::new(recipient, specification, value, value_zat, address) + FundingStream::new(recipient, specification, value, value_zat, *address) }) .collect::>(); let lockbox_streams = obj.lockbox_streams().clone(); diff --git a/zebra-rpc/tests/vectors/getblock_response_1.json b/zebra-rpc/tests/vectors/getblock_response_1.json index 7df2ffbb7fa..cbf2d65cdef 100644 --- a/zebra-rpc/tests/vectors/getblock_response_1.json +++ b/zebra-rpc/tests/vectors/getblock_response_1.json @@ -7,6 +7,7 @@ "blockcommitments": "fa1c98f49f74831f8a74f1b8908b6c8889a56ffebc20f62c114a7a007efc4dad", "finalsaplingroot": "5fe3724bebe096cc42c951803dd01e9657ead338327439902d19417ae9bda7e2", "finalorchardroot": "9cbf6c20965e2ae361322b9e7fd92c7df8b3c6d41040f544a205a4a2d75abf2a", + "nTx": 2, "tx": [ "21f2c7d10cc8857e60fd225f0f089126d9274e892f8d4e135e9e915adb06b72d", "603df6640eade811df35190e544560531b9e4fbe3e13e423a191fa5598b2a0ea" diff --git a/zebra-rpc/tests/vectors/getblock_response_2.json b/zebra-rpc/tests/vectors/getblock_response_2.json index 9523d812eff..27981ae8648 100644 --- a/zebra-rpc/tests/vectors/getblock_response_2.json +++ b/zebra-rpc/tests/vectors/getblock_response_2.json @@ -10,6 +10,7 @@ "finalsaplingroot": "5fe3724bebe096cc42c951803dd01e9657ead338327439902d19417ae9bda7e2", "finalorchardroot": "9cbf6c20965e2ae361322b9e7fd92c7df8b3c6d41040f544a205a4a2d75abf2a", "chainhistoryroot": "2a206a88fa0417a5a63b93c5155315abd4cbdf91801342ead7948e8bcdd1a45c", + "nTx": 2, "tx": [ { "txid": "21f2c7d10cc8857e60fd225f0f089126d9274e892f8d4e135e9e915adb06b72d", diff --git a/zebra-script/CHANGELOG.md b/zebra-script/CHANGELOG.md index 11dca3feaee..9d795551fad 100644 --- a/zebra-script/CHANGELOG.md +++ b/zebra-script/CHANGELOG.md @@ -5,7 +5,100 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [9.0.0] - 2026-06-10 + +### Changed + +- `zebra-chain` dependency bumped to `10.0.0`. No other changes to this crate. + +## [8.0.0] - 2026-06-02 + +### Changed + +- Update to `zebra-chain` 9.0.0 (NU6.2 support). No other changes to this crate. + +## [7.0.1] - 2026-05-29 + +This release fixes one consensus security issue: + +- The last P2SH input sigop count fix was incorrect. Properly fixed it to match zcashd + ([GHSA-2prc-cj5x-4443](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-2prc-cj5x-4443)). + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +## [7.0.0] - 2026-05-28 + +This release fixes one consensus security issue: + +- Route P2SH input sigop counts through the C++ FFI (`legacy_sigop_count_script`) + instead of the pure-Rust `script::Code::sig_op_count` so the result matches + `zcashd`'s `CScript::GetSigOpCount(true)` exactly. Prevents a consensus split + on attacker-chosen redeem scripts + ([GHSA-gf9r-m956-97qx](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gf9r-m956-97qx)). + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +### Changed + +- `zebra-chain` dependency bumped to `8.0.0`. No direct public-API changes in + this crate beyond the consensus fix above, but consumers inherit + `zebra-chain`'s breaking changes (notably `transparent::Input::Coinbase.data` + changing from `CoinbaseData` to `Vec`). + +## [6.0.1] - 2026-05-04 + +This release fixes one security issue: + +- Reject V5 transparent inputs signed with `SIGHASH_SINGLE` (or + `SIGHASH_SINGLE|ANYONECANPAY`) when the input has no transparent output at the + same index + ([GHSA-pvmv-cwg8-v6c8](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-pvmv-cwg8-v6c8)). + Follow-up to + [GHSA-cwfq-rfcr-8hmp](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-cwfq-rfcr-8hmp). + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +No public-API changes; internal-only fix. + +## [6.0.0] - 2026-05-01 + +This release fixes an important security issue: + +- [CVE-2026-XXXXX: Consensus Divergence in Transparent Sighash Hash-Type + Handling due to Stale + Buffer](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gq4h-3grw-2rhv) + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. + +### Added + +- `CachedFfiTransaction::p2sh_sigops(&self) -> u32`. +- `p2sh_sigop_count(tx, spent_outputs) -> u32`. + +### Changed + +- Migrated to `zcash_primitives 0.27` (and the rest of the librustzcash 2026-04 + release wave), which replaces the yanked `core2` dependency with `corez`. +- `Sigops::scripts` now returns `impl Iterator>` instead of + `impl Iterator`. The change ripples through every `Sigops` + implementation: + - `impl Sigops for zebra_chain::transaction::Transaction` + - `impl Sigops for zebra_chain::transaction::UnminedTx` + - `impl Sigops for CachedFfiTransaction` + - `impl Sigops for zcash_primitives::transaction::Transaction` + +## [5.0.1] - 2026-04-17 + +This release fixes an important security issue: + +- [CVE-2026-XXXXX: Consensus Divergence in Transparent Sighash Hash-Type Handling](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-8m29-fpq5-89jj) + +The impact of the issue for crate users will depend on the particular usage; +if you use it as a building block for a consensus node, you should update. ## [5.0.0] - 2026-03-12 diff --git a/zebra-script/Cargo.toml b/zebra-script/Cargo.toml index ad76bd07d6b..df3b09af5a7 100644 --- a/zebra-script/Cargo.toml +++ b/zebra-script/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-script" -version = "5.0.0" +version = "9.0.0" authors.workspace = true description = "Zebra script verification wrapping zcashd's zcash_script library" license.workspace = true @@ -26,13 +26,17 @@ comparison-interpreter = [] libzcash_script = { workspace = true } zcash_script = { workspace = true } zcash_primitives = { workspace = true } -zebra-chain = { path = "../zebra-chain", version = "6.0.0" } +zebra-chain = { path = "../zebra-chain", version = "10.1.0" } +rand = { workspace = true } thiserror = { workspace = true } [dev-dependencies] hex = { workspace = true } lazy_static = { workspace = true } +ripemd = { workspace = true } +secp256k1 = { workspace = true } +sha2 = { workspace = true } zebra-test = { path = "../zebra-test", version = "3.0.0" } [lints] diff --git a/zebra-script/src/lib.rs b/zebra-script/src/lib.rs index 618f968edb5..fa66dc57f7d 100644 --- a/zebra-script/src/lib.rs +++ b/zebra-script/src/lib.rs @@ -15,7 +15,7 @@ use thiserror::Error; use libzcash_script::ZcashScript; -use zcash_script::script; +use zcash_script::{opcode::PossiblyBad, script, script::Evaluable as _, Opcode}; use zebra_chain::{ parameters::NetworkUpgrade, transaction::{HashType, SigHasher}, @@ -125,6 +125,22 @@ impl CachedFfiTransaction { &self.sighasher } + /// Returns the total number of P2SH sigops across all inputs of this transaction. + /// + /// Mirrors zcashd's [`GetP2SHSigOpCount()`]. + /// + /// For each P2SH input (where the spent `scriptPubKey` is P2SH), the redeem script (the last + /// data push in the `scriptSig`) is parsed in "accurate" mode and its sigops are counted. + /// Coinbase inputs contribute zero. + /// + /// This must be included in the block-wide `MAX_BLOCK_SIGOPS` total to match zcashd's consensus + /// behavior. + /// + /// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852 + pub fn p2sh_sigops(&self) -> u32 { + p2sh_sigop_count(&self.transaction, &self.all_previous_outputs) + } + /// Verify if the script in the input at `input_index` of a transaction correctly spends the /// matching [`transparent::Output`] it refers to. #[allow(clippy::unwrap_in_result)] @@ -132,8 +148,10 @@ impl CachedFfiTransaction { let previous_output = self .all_previous_outputs .get(input_index) + .filter(|_| self.all_previous_outputs.len() == self.transaction.inputs().len()) .ok_or(Error::TxIndex)? .clone(); + let transparent::Output { value: _, lock_script, @@ -159,20 +177,80 @@ impl CachedFfiTransaction { let calculate_sighash = |script_code: &script::Code, hash_type: &zcash_script::signature::HashType| { - let script_code_vec = script_code.0.clone(); - let mut our_hash_type = match hash_type.signed_outputs() { - zcash_script::signature::SignedOutputs::All => HashType::ALL, - zcash_script::signature::SignedOutputs::Single => HashType::SINGLE, - zcash_script::signature::SignedOutputs::None => HashType::NONE, - }; - if hash_type.anyone_can_pay() { - our_hash_type |= HashType::ANYONECANPAY; - } - Some( - self.sighasher() - .sighash(our_hash_type, Some((input_index, script_code_vec))) - .0, - ) + // Inner helper: returns None when the hash type is invalid + // and the callback should signal failure. + let computed: Option<[u8; 32]> = (|| { + // For v5+ transactions: reject undefined hash_type values, + // matching zcashd's SighashType::parse behavior. + // Valid values: {0x01, 0x02, 0x03, 0x81, 0x82, 0x83}. + if self.transaction.version() >= 5 { + let valid_v5_types: &[i32] = &[0x01, 0x02, 0x03, 0x81, 0x82, 0x83]; + if !valid_v5_types.contains(&hash_type.raw_bits()) { + return None; + } + } + + // For v5+ transactions: reject SIGHASH_SINGLE when there is + // no corresponding output (an output at the same index as + // the input being verified). ZIP-244 §S.2a marks this as a + // consensus failure; zcashd throws in `SignatureHash` and + // `CheckSig` catches the exception to fail the script. + if self.transaction.version() >= 5 + && hash_type.signed_outputs() + == zcash_script::signature::SignedOutputs::Single + && input_index >= self.transaction.outputs().len() + { + return None; + } + + let script_code_vec = script_code.0.clone(); + + // For pre-v5 (v4) transactions: zcashd serializes the raw + // hash_type byte into the sighash preimage (only masking with + // 0x1f for selection logic). Use the raw byte to match. + if self.transaction.version() < 5 { + let raw_byte = hash_type.raw_bits() as u8; + return Some( + self.sighasher() + .sighash_v4_raw(raw_byte, Some((input_index, script_code_vec))) + .0, + ); + } + + let mut our_hash_type = match hash_type.signed_outputs() { + zcash_script::signature::SignedOutputs::All => HashType::ALL, + zcash_script::signature::SignedOutputs::Single => HashType::SINGLE, + zcash_script::signature::SignedOutputs::None => HashType::NONE, + }; + if hash_type.anyone_can_pay() { + our_hash_type |= HashType::ANYONECANPAY; + } + Some( + self.sighasher() + .sighash(our_hash_type, Some((input_index, script_code_vec))) + .0, + ) + })(); + + // Workaround for the libzcash_script callback API: returning + // `None` from this callback does not propagate failure to the + // C++ verifier. + // + // Instead of returning `None` to indicate an error, we return a + // per-call randomly-generated dummy sighash so any signature + // fails to verify with overwhelming probability. Note that a + // fixed sentinel value would be unsafe: an attacker who knows + // it can construct an ECDSA signature that verifies against any + // 32-byte value under a chosen pubkey. + // + // This shim can be removed once libzcash_script propagates + // callback failure to the C++ verifier. + Some(computed.unwrap_or_else(|| { + use rand::RngCore; + let mut bytes = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut bytes); + bytes + })) }; let interpreter = get_interpreter(&calculate_sighash, lock_time, is_final); interpreter @@ -188,8 +266,16 @@ impl CachedFfiTransaction { } } -/// Trait for counting the number of transparent signature operations -/// in the transparent inputs and outputs of a transaction. +/// Trait for counting the number of transparent signature operations in the transparent inputs and +/// outputs of a transaction. +/// +/// Mirrors zcashd's [`GetLegacySigOpCount()`]. +/// +/// All transparent inputs are included, including the coinbase input script. zcashd charges +/// coinbase `scriptSig` sigops against the block `MAX_BLOCK_SIGOPS` limit, so Zebra must do the +/// same to avoid a consensus split. +/// +/// [`GetLegacySigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L826-L836 pub trait Sigops { /// Returns the number of transparent signature operations in the /// transparent inputs and outputs of the given transaction. @@ -198,56 +284,157 @@ pub trait Sigops { Ok(self.scripts().try_fold(0, |acc, s| { interpreter - .legacy_sigop_count_script(&script::Code(s.to_vec())) + .legacy_sigop_count_script(&script::Code(s)) .map(|n| acc + n) })?) } /// Returns an iterator over the input and output scripts in the transaction. /// - /// The number of input scripts in a coinbase tx is zero. - fn scripts(&self) -> impl Iterator; + /// For consensus sigop accounting, this must include the coinbase input + /// script (height prefix followed by extra data), matching zcashd's + /// `GetLegacySigOpCount()`. + fn scripts(&self) -> impl Iterator>; } impl Sigops for zebra_chain::transaction::Transaction { - fn scripts(&self) -> impl Iterator { + fn scripts(&self) -> impl Iterator> { self.inputs() .iter() - .filter_map(|input| match input { + .map(|input| match input { transparent::Input::PrevOut { unlock_script, .. } => { - Some(unlock_script.as_raw_bytes()) + unlock_script.as_raw_bytes().to_vec() } - transparent::Input::Coinbase { .. } => None, + // Coinbase scriptSig = encoded height || extra data, which must be reconstructed + // for sigop counting. `coinbase_script()` round-trips through + // `write_coinbase_height`, which only fails when called on a malformed in-memory + // genesis coinbase. Any coinbase that was successfully deserialized round-trips + // cleanly, so this `expect` cannot fire on validation paths. + transparent::Input::Coinbase { .. } => input + .coinbase_script() + .expect("coinbase_script reconstructs from a deserialized coinbase input"), }) - .chain(self.outputs().iter().map(|o| o.lock_script.as_raw_bytes())) + .chain( + self.outputs() + .iter() + .map(|o| o.lock_script.as_raw_bytes().to_vec()), + ) } } impl Sigops for zebra_chain::transaction::UnminedTx { - fn scripts(&self) -> impl Iterator { + fn scripts(&self) -> impl Iterator> { self.transaction.scripts() } } impl Sigops for CachedFfiTransaction { - fn scripts(&self) -> impl Iterator { + fn scripts(&self) -> impl Iterator> { self.transaction.scripts() } } impl Sigops for zcash_primitives::transaction::Transaction { - fn scripts(&self) -> impl Iterator { + fn scripts(&self) -> impl Iterator> { self.transparent_bundle().into_iter().flat_map(|bundle| { - (!bundle.is_coinbase()) - .then(|| bundle.vin.iter().map(|i| i.script_sig().0 .0.as_slice())) - .into_iter() - .flatten() - .chain( - bundle - .vout - .iter() - .map(|o| o.script_pubkey().0 .0.as_slice()), - ) + // `zcash_primitives` stores the coinbase input's full serialized scriptSig (height + // prefix + extra data) in the synthesized input's script_sig, so it is included as-is + // for sigop counting. + bundle + .vin + .iter() + .map(|i| i.script_sig().0 .0.clone()) + .chain(bundle.vout.iter().map(|o| o.script_pubkey().0 .0.clone())) }) } } + +/// Extract the redeem script bytes from a P2SH scriptSig. +/// +/// Mirrors zcashd's P2SH redeem-script extraction in +/// [`CScript::GetSigOpCount(const CScript& scriptSig)`]. +/// +/// Iterates the scriptSig opcodes and returns the last successfully pushed data value. Returns +/// `None` if any opcode fails to parse, OR if any opcode is not a push value (zcashd: `opcode > +/// OP_16`). This matches zcashd's behavior of returning 0 P2SH sigops for malformed or +/// non-push-only scriptSigs. +/// +/// [`CScript::GetSigOpCount(const CScript& scriptSig)`]: https://github.com/zcash/zcash/blob/v6.11.0/src/script/script.cpp#L176-L199 +fn extract_p2sh_redeem_script(unlock_script: &transparent::Script) -> Option> { + let code = script::Code(unlock_script.as_raw_bytes().to_vec()); + let mut last_push_data: Option> = None; + for opcode in code.parse() { + match opcode { + Ok(PossiblyBad::Good(Opcode::PushValue(pv))) => { + last_push_data = Some(pv.value()); + } + // Non-push opcode (operation, control, or bad) or parse error: zcashd returns 0 sigops + // in this case. Match that behavior by discarding any data collected so far. + _ => return None, + } + } + last_push_data +} + +/// Returns the P2SH sigop count for a single input. +/// +/// Returns 0 for non-P2SH inputs, coinbase inputs, and P2SH inputs where no redeem script can be +/// extracted from the scriptSig. +fn p2sh_input_sigop_count(input: &transparent::Input, spent_output: &transparent::Output) -> u32 { + let unlock_script = match input { + transparent::Input::PrevOut { unlock_script, .. } => unlock_script, + transparent::Input::Coinbase { .. } => return 0, + }; + + let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec()); + + if !lock_code.is_pay_to_script_hash() { + return 0; + } + + let Some(redeemed_bytes) = extract_p2sh_redeem_script(unlock_script) else { + return 0; + }; + + // Count the redeem script's sigops in zcashd's "accurate" mode, matching + // `GetP2SHSigOpCount` -> `CScript::GetSigOpCount(scriptSig)` -> `subscript.GetSigOpCount(true)`. + // Relies on the patched `zcash_script` (see `[patch.crates-io]`) whose `sig_op_count` no longer + // short-circuits on disabled opcodes (incl. OP_CODESEPARATOR), which would otherwise undercount. + script::Code(redeemed_bytes).sig_op_count(true) +} + +/// Returns the total number of P2SH sigops across all inputs of `tx`. +/// +/// Mirrors zcashd's [`GetP2SHSigOpCount()`]. +/// +/// Coinbase transactions always return zero, matching zcashd's early-return for `tx.IsCoinBase()`. +/// Callers are therefore permitted to pass an empty `spent_outputs` slice for coinbase transactions +/// (which is what the block-verifier does, since coinbase inputs have no previous output). +/// +/// # Correctness +/// +/// For non-coinbase transactions, `spent_outputs.len()` must equal the number of transparent inputs +/// in `tx`. If the lengths differ, `zip()` silently truncates the longer iterator, causing an +/// incorrect (undercount) result. +/// +/// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852 +pub fn p2sh_sigop_count( + tx: &zebra_chain::transaction::Transaction, + spent_outputs: &[transparent::Output], +) -> u32 { + if tx.is_coinbase() { + return 0; + } + + debug_assert_eq!( + tx.inputs().len(), + spent_outputs.len(), + "spent_outputs must align with transaction inputs for non-coinbase txs" + ); + + tx.inputs() + .iter() + .zip(spent_outputs.iter()) + .map(|(input, spent_output)| p2sh_input_sigop_count(input, spent_output)) + .sum() +} diff --git a/zebra-script/src/tests.rs b/zebra-script/src/tests.rs index 51fafb1ba03..686a5489948 100644 --- a/zebra-script/src/tests.rs +++ b/zebra-script/src/tests.rs @@ -3,14 +3,15 @@ use hex::FromHex; use std::sync::Arc; use zebra_chain::{ - parameters::NetworkUpgrade, + block::{self, Height}, + parameters::{Network, NetworkUpgrade}, serialization::{ZcashDeserialize, ZcashDeserializeInto}, - transaction::Transaction, + transaction::{self, HashType, LockTime, SigHasher, Transaction}, transparent::{self, Output}, }; use zebra_test::prelude::*; -use crate::Sigops; +use crate::{p2sh_sigop_count, Sigops}; lazy_static::lazy_static! { pub static ref SCRIPT_PUBKEY: Vec = >::from_hex("76a914f47cac1e6fec195c055994e8064ffccce0044dd788ac") @@ -229,3 +230,1461 @@ fn p2sh() -> Result<()> { Ok(()) } + +/// Construct a V5 P2PKH transaction with a given sighash type byte in the signature, +/// then verify it through the full script verification path (CachedFfiTransaction::is_valid). +/// +/// This reproduces the regtest experiments from docs/analysis/sighash_consensus_divergence_report.md: +/// - canonical_hash_type: the HashType used to compute the sighash for signing (must be valid) +/// - sig_hash_type_byte: the raw byte appended to the DER signature in the unlock script +/// +/// When sig_hash_type_byte differs from the canonical byte but canonicalizes to the same +/// HashType via from_bits(byte, false), Zebra accepts the transaction. zcashd rejects it +/// for v5 transactions because SighashType::parse rejects undefined raw values. +fn build_and_verify_v5_p2pkh( + canonical_hash_type: HashType, + sig_hash_type_byte: u8, +) -> std::result::Result<(), crate::Error> { + use ripemd::{Digest as _, Ripemd160}; + use secp256k1::{Message, Secp256k1, SecretKey}; + use sha2::Sha256; + + let secp = Secp256k1::new(); + + // Deterministic keypair (32 bytes, nonzero) + let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("valid secret key"); + let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let pubkey_bytes = public_key.serialize(); // 33 bytes, compressed + + // Derive P2PKH lock script: OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG + let sha_hash = Sha256::digest(pubkey_bytes); + let pub_key_hash: [u8; 20] = Ripemd160::digest(sha_hash).into(); + let mut lock_script_bytes = vec![ + 0x76, // OP_DUP + 0xa9, // OP_HASH160 + 0x14, // Push 20 bytes + ]; + lock_script_bytes.extend_from_slice(&pub_key_hash); + lock_script_bytes.push(0x88); // OP_EQUALVERIFY + lock_script_bytes.push(0xac); // OP_CHECKSIG + let lock_script = transparent::Script::new(&lock_script_bytes); + + let previous_output = transparent::Output { + value: 1_0000_0000u64.try_into().expect("valid amount"), + lock_script: lock_script.clone(), + }; + + // Build a V5 transaction with a placeholder unlock script. + // For v5/ZIP-244, the sighash does NOT depend on the unlock script contents, + // so we can compute the sighash with a placeholder, sign, then rebuild. + let placeholder_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&[]), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), // OP_FALSE (burn) + }], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + // Compute the sighash for the canonical hash type + let all_previous_outputs = Arc::new(vec![previous_output.clone()]); + let sighasher = SigHasher::new(&placeholder_tx, NetworkUpgrade::Nu5, all_previous_outputs) + .expect("sighasher creation should succeed"); + let sighash = sighasher.sighash(canonical_hash_type, Some((0, lock_script_bytes.clone()))); + + // Sign the sighash with the private key + let msg = Message::from_digest(*sighash.as_ref()); + let signature = secp.sign_ecdsa(&msg, &secret_key); + let der_sig = signature.serialize_der(); + + // Build the unlock script: + let mut unlock_script_bytes = Vec::new(); + // Push signature + hash_type byte + let sig_with_hashtype_len = der_sig.len() + 1; + unlock_script_bytes.push(sig_with_hashtype_len as u8); + unlock_script_bytes.extend_from_slice(&der_sig); + unlock_script_bytes.push(sig_hash_type_byte); + // Push compressed public key (33 bytes) + unlock_script_bytes.push(pubkey_bytes.len() as u8); + unlock_script_bytes.extend_from_slice(&pubkey_bytes); + + // Rebuild the V5 transaction with the real unlock script + let final_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock_script_bytes), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let verifier = super::CachedFfiTransaction::new( + Arc::new(final_tx), + Arc::new(vec![previous_output]), + NetworkUpgrade::Nu5, + ) + .expect("network upgrade should be valid for v5 tx"); + + verifier.is_valid(0) +} + +/// Baseline: a standard V5 P2PKH spend with SIGHASH_ALL (0x01) passes verification. +/// Both Zebra and zcashd accept this. +#[test] +fn sighash_divergence_v5_p2pkh_canonical_sighash_all() { + let _init_guard = zebra_test::init(); + + build_and_verify_v5_p2pkh(HashType::ALL, 0x01) + .expect("canonical SIGHASH_ALL (0x01) should be accepted"); +} + +/// Baseline: a standard V5 P2PKH spend with SIGHASH_ALL|ANYONECANPAY (0x81) passes. +/// Both Zebra and zcashd accept this. +#[test] +fn sighash_divergence_v5_p2pkh_canonical_sighash_all_anyonecanpay() { + let _init_guard = zebra_test::init(); + + build_and_verify_v5_p2pkh(HashType::ALL | HashType::ANYONECANPAY, 0x81) + .expect("canonical SIGHASH_ALL|ANYONECANPAY (0x81) should be accepted"); +} + +/// V5 P2PKH spend with malformed hash_type 0x84 is now rejected. +/// +/// 0x84 has undefined bits set: 0x84 ∉ {0x01, 0x02, 0x03, 0x81, 0x82, 0x83}. +/// The callback now checks raw_bits() and returns None for v5+ transactions +/// with undefined hash_type values, matching zcashd's SighashType::parse behavior. +/// +/// Before the fix, Zebra accepted this (canonicalizing 0x84 → 0x81). +#[test] +fn sighash_divergence_v5_p2pkh_malformed_0x84_rejected() { + let _init_guard = zebra_test::init(); + + let result = build_and_verify_v5_p2pkh(HashType::ALL | HashType::ANYONECANPAY, 0x84); + + assert!( + result.is_err(), + "Malformed hash_type 0x84 should be rejected for v5 transactions, \ + matching zcashd behavior" + ); +} + +/// V5 P2PKH spend with malformed hash_type 0x50 is now rejected. +/// +/// 0x50 has undefined bits set: 0x50 ∉ {0x01, 0x02, 0x03, 0x81, 0x82, 0x83}. +/// The callback now checks raw_bits() and returns None for v5+ transactions +/// with undefined hash_type values, matching zcashd's SighashType::parse behavior. +/// +/// Before the fix, Zebra accepted this (canonicalizing 0x50 → 0x01). +#[test] +fn sighash_divergence_v5_p2pkh_malformed_0x50_rejected() { + let _init_guard = zebra_test::init(); + + let result = build_and_verify_v5_p2pkh(HashType::ALL, 0x50); + + assert!( + result.is_err(), + "Malformed hash_type 0x50 should be rejected for v5 transactions, \ + matching zcashd behavior" + ); +} + +/// Negative test: V5 P2PKH spend with malformed hash_type 0x84 but signed with +/// the WRONG canonical type (ALL instead of ALL|ANYONECANPAY). +/// +/// This produces a sighash mismatch: the signature was created over ALL's sighash, +/// but Zebra canonicalizes 0x84 to ALL|ANYONECANPAY and computes that sighash. +/// The signature doesn't verify → both Zebra and zcashd reject. +/// +/// This confirms the issue is specifically about canonicalization mapping to the +/// correct sighash, not a generic signature bypass. +#[test] +fn sighash_divergence_v5_p2pkh_malformed_0x84_wrong_canonical_type_rejected() { + let _init_guard = zebra_test::init(); + + // Sign with ALL (0x01), but put 0x84 which canonicalizes to ALL|ANYONECANPAY (0x81). + // The sighash mismatch causes verification failure. + let result = build_and_verify_v5_p2pkh(HashType::ALL, 0x84); + + assert!( + result.is_err(), + "Malformed hash_type 0x84 signed with wrong canonical type should be rejected \ + by both Zebra and zcashd (sighash mismatch)" + ); +} + +/// Build a V5 transaction with two transparent inputs and one transparent output, +/// sign the input at `signed_input_index` with SIGHASH_SINGLE (or +/// SIGHASH_SINGLE|ANYONECANPAY when `anyone_can_pay` is set) using whatever +/// digest Zebra computes for that input, and run that input through the script +/// verifier. +/// +/// This reproduces the ZIP-244 §S.2a "no corresponding output" scenario from +/// GHSA-cwfq-rfcr-8hmp: the transaction has fewer transparent outputs than +/// inputs, so for `signed_input_index >= 1` there is no `vout[k]` for the input +/// being signed. `zcashd` rejects this at script verification; before the fix, +/// Zebra accepted it because librustzcash returned a digest computed from an +/// empty output list instead of failing. +fn build_and_verify_v5_p2pkh_single_with_missing_output( + signed_input_index: usize, + anyone_can_pay: bool, +) -> std::result::Result<(), crate::Error> { + use ripemd::{Digest as _, Ripemd160}; + use secp256k1::{Message, Secp256k1, SecretKey}; + use sha2::Sha256; + + assert!(signed_input_index < 2, "test fixture only has two inputs"); + + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("valid secret key"); + let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let pubkey_bytes = public_key.serialize(); + + // Standard P2PKH lock script reused for every prevout. + let sha_hash = Sha256::digest(pubkey_bytes); + let pub_key_hash: [u8; 20] = Ripemd160::digest(sha_hash).into(); + let mut lock_script_bytes = vec![0x76, 0xa9, 0x14]; + lock_script_bytes.extend_from_slice(&pub_key_hash); + lock_script_bytes.push(0x88); + lock_script_bytes.push(0xac); + let lock_script = transparent::Script::new(&lock_script_bytes); + + let prevout = || transparent::Output { + value: 1_0000_0000u64.try_into().expect("valid amount"), + lock_script: lock_script.clone(), + }; + let all_previous_outputs = Arc::new(vec![prevout(), prevout()]); + + // Two inputs, one output: any input at index >= 1 has no corresponding + // output for SIGHASH_SINGLE. + let make_tx = |unlock_scripts: [Vec; 2]| Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + inputs: vec![ + transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock_scripts[0]), + sequence: u32::MAX, + }, + transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([1u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock_scripts[1]), + sequence: u32::MAX, + }, + ], + outputs: vec![transparent::Output { + value: 1_5000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let placeholder_tx = make_tx([Vec::new(), Vec::new()]); + + let canonical_hash_type = if anyone_can_pay { + HashType::SINGLE | HashType::ANYONECANPAY + } else { + HashType::SINGLE + }; + let raw_hash_type_byte = if anyone_can_pay { 0x83u8 } else { 0x03u8 }; + + let sighasher = SigHasher::new( + &placeholder_tx, + NetworkUpgrade::Nu5, + all_previous_outputs.clone(), + ) + .expect("sighasher creation should succeed"); + let sighash = sighasher.sighash( + canonical_hash_type, + Some((signed_input_index, lock_script_bytes.clone())), + ); + + let msg = Message::from_digest(*sighash.as_ref()); + let signature = secp.sign_ecdsa(&msg, &secret_key); + let der_sig = signature.serialize_der(); + + let mut signed_unlock = Vec::new(); + signed_unlock.push((der_sig.len() + 1) as u8); + signed_unlock.extend_from_slice(&der_sig); + signed_unlock.push(raw_hash_type_byte); + signed_unlock.push(pubkey_bytes.len() as u8); + signed_unlock.extend_from_slice(&pubkey_bytes); + + // Other input is left empty; it isn't being verified by this call. + let mut unlock_scripts: [Vec; 2] = [Vec::new(), Vec::new()]; + unlock_scripts[signed_input_index] = signed_unlock; + + let final_tx = make_tx(unlock_scripts); + + let verifier = super::CachedFfiTransaction::new( + Arc::new(final_tx), + all_previous_outputs, + NetworkUpgrade::Nu5, + ) + .expect("network upgrade should be valid for v5 tx"); + verifier.is_valid(signed_input_index) +} + +/// V5 SIGHASH_SINGLE on input 1 of a 2-in/1-out transaction must be rejected: +/// there is no `vout[1]` to commit to. +/// +/// Before the fix, Zebra accepted this because librustzcash returns a digest +/// computed from an empty output list when the input index is past the output +/// vector. ZIP-244 §S.2a requires consensus-level rejection; zcashd enforces it. +#[test] +fn sighash_divergence_v5_sighash_single_no_corresponding_output_rejected() { + let _init_guard = zebra_test::init(); + + let result = build_and_verify_v5_p2pkh_single_with_missing_output(1, false); + + assert!( + result.is_err(), + "V5 SIGHASH_SINGLE with no corresponding output must fail script verification, \ + matching zcashd (ZIP-244 §S.2a)" + ); +} + +/// Same as above with SIGHASH_SINGLE|ANYONECANPAY (0x83). The +/// corresponding-output rule applies regardless of the ANYONECANPAY flag. +#[test] +fn sighash_divergence_v5_sighash_single_anyonecanpay_no_corresponding_output_rejected() { + let _init_guard = zebra_test::init(); + + let result = build_and_verify_v5_p2pkh_single_with_missing_output(1, true); + + assert!( + result.is_err(), + "V5 SIGHASH_SINGLE|ANYONECANPAY with no corresponding output must fail script \ + verification, matching zcashd (ZIP-244 §S.2a)" + ); +} + +/// Positive control: SIGHASH_SINGLE on input 0 of a 2-in/1-out transaction is +/// valid because `vout[0]` exists. This guards against the new check rejecting +/// legitimate SIGHASH_SINGLE spends. +#[test] +fn sighash_divergence_v5_sighash_single_with_corresponding_output_accepted() { + let _init_guard = zebra_test::init(); + + build_and_verify_v5_p2pkh_single_with_missing_output(0, false) + .expect("V5 SIGHASH_SINGLE on an input with a corresponding output should be accepted"); +} + +/// Build a V4 P2PKH transparent spend, sign it under the supplied raw `hash_type` +/// byte using the V4 raw-byte sighash semantics, and run it through the script +/// verifier. +/// +/// `zcashd` serializes the full raw `hash_type` byte into the V4 sighash +/// preimage (only masking with 0x1f for selection logic). Before the V4 fix, +/// Zebra canonicalized the byte before computing the sighash, so a tx using +/// e.g. `0x41` would be accepted by `zcashd` but rejected by Zebra (digest +/// mismatch). With the V4 fix, both implementations compute the same digest +/// and Zebra accepts. +fn build_and_verify_v4_p2pkh(sig_hash_type_byte: u8) -> std::result::Result<(), crate::Error> { + use ripemd::{Digest as _, Ripemd160}; + use secp256k1::{Message, Secp256k1, SecretKey}; + use sha2::Sha256; + + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("valid secret key"); + let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let pubkey_bytes = public_key.serialize(); + + let sha_hash = Sha256::digest(pubkey_bytes); + let pub_key_hash: [u8; 20] = Ripemd160::digest(sha_hash).into(); + let mut lock_script_bytes = vec![0x76, 0xa9, 0x14]; + lock_script_bytes.extend_from_slice(&pub_key_hash); + lock_script_bytes.push(0x88); + lock_script_bytes.push(0xac); + let lock_script = transparent::Script::new(&lock_script_bytes); + + let previous_output = transparent::Output { + value: 1_0000_0000u64.try_into().expect("valid amount"), + lock_script: lock_script.clone(), + }; + + let placeholder_tx = Transaction::V4 { + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&[]), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let all_previous_outputs = Arc::new(vec![previous_output.clone()]); + let sighasher = SigHasher::new( + &placeholder_tx, + NetworkUpgrade::Canopy, + all_previous_outputs, + ) + .expect("sighasher creation should succeed"); + + // V4: use the raw byte to match zcashd's preimage semantics. + let sighash = + sighasher.sighash_v4_raw(sig_hash_type_byte, Some((0, lock_script_bytes.clone()))); + + let msg = Message::from_digest(*sighash.as_ref()); + let signature = secp.sign_ecdsa(&msg, &secret_key); + let der_sig = signature.serialize_der(); + + let mut unlock_script_bytes = Vec::new(); + let sig_with_hashtype_len = der_sig.len() + 1; + unlock_script_bytes.push(sig_with_hashtype_len as u8); + unlock_script_bytes.extend_from_slice(&der_sig); + unlock_script_bytes.push(sig_hash_type_byte); + unlock_script_bytes.push(pubkey_bytes.len() as u8); + unlock_script_bytes.extend_from_slice(&pubkey_bytes); + + let final_tx = Transaction::V4 { + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock_script_bytes), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let verifier = super::CachedFfiTransaction::new( + Arc::new(final_tx), + Arc::new(vec![previous_output]), + NetworkUpgrade::Canopy, + ) + .expect("network upgrade should be valid for v4 tx"); + + verifier.is_valid(0) +} + +/// Variant A regression: V4 P2PKH spend with non-canonical hash_type 0x41 is accepted. +/// +/// `zcashd` serializes the raw 0x41 byte into the V4 sighash preimage. Before the +/// V4 fix, Zebra canonicalized 0x41 → ALL (0x01) before computing the sighash, +/// producing a different digest than the one signed and rejecting the tx — a +/// consensus split where `zcashd` accepted what Zebra rejected. +/// +/// With the V4 fix, the callback routes V4 transactions through `sighash_v4_raw` +/// using the raw byte, so the digests match and Zebra accepts. +#[test] +fn sighash_divergence_v4_p2pkh_extra_bits_0x41_accepted() { + let _init_guard = zebra_test::init(); + + build_and_verify_v4_p2pkh(0x41) + .expect("V4 spend with non-canonical hash_type 0x41 should be accepted, matching zcashd"); +} + +/// Unit-level: for canonical bytes, `sighash_v4_raw` produces the same digest +/// as the typed `sighash` API. This guards against regressions in the new V4 +/// raw-byte path on canonical inputs. +#[test] +fn sighash_divergence_v4_raw_canonical_matches_typed() { + let _init_guard = zebra_test::init(); + + let lock_script = transparent::Script::new(&[ + 0x76, 0xa9, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0xac, + ]); + let previous_output = transparent::Output { + value: 1_0000_0000u64.try_into().expect("valid amount"), + lock_script: lock_script.clone(), + }; + + let tx = Transaction::V4 { + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&[]), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let sighasher = SigHasher::new(&tx, NetworkUpgrade::Canopy, Arc::new(vec![previous_output])) + .expect("sighasher creation should succeed"); + + let script_code = lock_script.as_raw_bytes().to_vec(); + + let canonical_pairs: &[(HashType, u8)] = &[ + (HashType::ALL, 0x01), + (HashType::NONE, 0x02), + (HashType::SINGLE, 0x03), + (HashType::ALL_ANYONECANPAY, 0x81), + (HashType::NONE_ANYONECANPAY, 0x82), + (HashType::SINGLE_ANYONECANPAY, 0x83), + ]; + + for &(typed, raw) in canonical_pairs { + let typed_digest = sighasher.sighash(typed, Some((0, script_code.clone()))); + let raw_digest = sighasher.sighash_v4_raw(raw, Some((0, script_code.clone()))); + let typed_bytes: [u8; 32] = typed_digest.into(); + let raw_bytes: [u8; 32] = raw_digest.into(); + assert_eq!( + typed_bytes, raw_bytes, + "sighash_v4_raw({raw:#x}) should equal typed sighash for canonical input" + ); + } +} + +/// Test that `HashType::from_bits` with `is_strict=false` canonicalizes undefined raw hash_type +/// bytes instead of rejecting them. This is the root cause of the sighash consensus divergence: +/// +/// - zcashd rejects undefined raw hash_type values for v5 transactions in `SighashType::parse` +/// - Zebra receives a canonicalized `HashType` through the FFI callback (which uses +/// `from_bits(hash_type, false)`) and computes a sighash for the canonical type +/// +/// See docs/analysis/sighash_consensus_divergence_report.md for full details. +#[test] +fn sighash_divergence_from_bits_canonicalization() { + let _init_guard = zebra_test::init(); + + // These raw hash_type bytes are NOT valid for v5 transactions. + // zcashd rejects them in SighashType::parse (transaction_ffi.rs:267-273). + // Valid v5 values are: {0x01, 0x02, 0x03, 0x81, 0x82, 0x83}. + let invalid_raw_bytes: &[(i32, &str)] = &[ + ( + 0x84, + "0x84: undefined bits set, canonicalizes to ALL|ANYONECANPAY (0x81)", + ), + ( + 0x50, + "0x50: undefined bits set, canonicalizes to ALL (0x01)", + ), + ( + 0x00, + "0x00: no signed_outputs bits set, canonicalizes to ALL (0x01)", + ), + ( + 0x04, + "0x04: undefined lower bits, canonicalizes to ALL (0x01)", + ), + ( + 0xFF, + "0xFF: all bits set, canonicalizes to SINGLE|ANYONECANPAY (0x83)", + ), + ( + 0x85, + "0x85: undefined bits set, canonicalizes to ALL|ANYONECANPAY (0x81)", + ), + ]; + + for &(raw_byte, description) in invalid_raw_bytes { + // Non-strict mode (what libzcash_script uses in the callback): SUCCEEDS + // This is the lossy conversion that causes the divergence. + let non_strict_result = zcash_script::signature::HashType::from_bits(raw_byte, false); + assert!( + non_strict_result.is_ok(), + "from_bits({raw_byte:#x}, false) should succeed (canonicalize) but failed: {description}" + ); + + // Strict mode: REJECTS (matching zcashd v5 behavior) + let strict_result = zcash_script::signature::HashType::from_bits(raw_byte, true); + assert!( + strict_result.is_err(), + "from_bits({raw_byte:#x}, true) should reject undefined bits but succeeded: {description}" + ); + } + + // Verify that all six valid canonical values pass both strict and non-strict. + let valid_raw_bytes: &[i32] = &[0x01, 0x02, 0x03, 0x81, 0x82, 0x83]; + for &raw_byte in valid_raw_bytes { + assert!( + zcash_script::signature::HashType::from_bits(raw_byte, false).is_ok(), + "valid byte {raw_byte:#x} should pass non-strict" + ); + assert!( + zcash_script::signature::HashType::from_bits(raw_byte, true).is_ok(), + "valid byte {raw_byte:#x} should pass strict" + ); + } +} + +/// Regression test for [GHSA-jv4h-j224-23cc] (Trigger A, "Coinbase Hidden Legacy Sigops"). +/// +/// zcashd's `GetLegacySigOpCount()` counts sigops in the coinbase input's `scriptSig`. Zebra +/// previously skipped the coinbase input entirely, so a miner could hide up to ~98 sigops (the +/// coinbase script length limit is 100 bytes) inside the coinbase `scriptSig` and avoid them being +/// charged against `MAX_BLOCK_SIGOPS`. +/// +/// This test builds a v5 coinbase transaction whose `miner_data` consists entirely of `OP_CHECKSIG` +/// (`0xac`) bytes and asserts that `tx.sigops()` now returns the expected count covering every +/// `OP_CHECKSIG`. +/// +/// [GHSA-jv4h-j224-23cc]: https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jv4h-j224-23cc +#[test] +fn count_coinbase_legacy_sigops_includes_coinbase_script() -> Result<()> { + let _init_guard = zebra_test::init(); + + // 80 bytes of OP_CHECKSIG fits within the 100-byte coinbase script limit + // even after the height prefix. + const OP_CHECKSIG: u8 = 0xac; + let miner_data = vec![OP_CHECKSIG; 80]; + + let dummy_output_script = transparent::Script::new(&[0x51]); // OP_TRUE + let output_amount = zebra_chain::amount::Amount::try_from(1_000_000)?; + + // Use a height after NU5 activation on Mainnet so v5 is the effective version. + let network = Network::Mainnet; + let height = NetworkUpgrade::Nu5 + .activation_height(&network) + .expect("NU5 has a Mainnet activation height"); + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![transparent::Input::Coinbase { + height, + data: miner_data, + sequence: 0xffff_ffff, + }], + outputs: vec![transparent::Output { + value: output_amount, + lock_script: dummy_output_script, + }], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + let _ = network; + + // Before the fix, Zebra's `Sigops` impl skipped the coinbase input and returned 0 for a + // coinbase with no OP_CHECKSIG in its outputs. After the fix, every OP_CHECKSIG in the coinbase + // `scriptSig` must be counted. + let sigops = tx.sigops().expect("sigop count is finite"); + assert_eq!( + sigops, 80, + "coinbase scriptSig OP_CHECKSIG bytes must be counted against \ + MAX_BLOCK_SIGOPS (zcashd parity, GHSA-jv4h-j224-23cc)" + ); + + Ok(()) +} + +/// Regression test for [GHSA-jv4h-j224-23cc] (Trigger B, "Aggregate P2SH Sigops"). +/// +/// zcashd's `GetP2SHSigOpCount()` parses the redeem script (the last push in each P2SH input's +/// `scriptSig`) with `accurate=true` and sums the sigops across all inputs. Previously Zebra only +/// did this in the mempool policy, so a block containing P2SH inputs whose aggregate redeem-script +/// sigops exceeded `MAX_BLOCK_SIGOPS` could be accepted by Zebra but rejected by zcashd as +/// `bad-blk-sigops`. +/// +/// This test exercises the free function `zebra_script::p2sh_sigop_count` directly on a synthetic +/// transaction with one P2SH input whose redeem script is 15 x OP_CHECKSIG. +/// +/// [GHSA-jv4h-j224-23cc]: https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jv4h-j224-23cc +#[test] +fn p2sh_sigop_count_counts_redeem_script() -> Result<()> { + let _init_guard = zebra_test::init(); + + const OP_CHECKSIG: u8 = 0xac; + const OP_HASH160: u8 = 0xa9; + const OP_EQUAL: u8 = 0x87; + + // Redeem script: 15 x OP_CHECKSIG. Each OP_CHECKSIG counts as 1. + let redeem_script = vec![OP_CHECKSIG; 15]; + + // scriptSig consists solely of a direct push of the redeem script. For a 15-byte payload we can + // use the literal-length push opcode (0x01..=0x4b), which is just the length byte followed by + // the data. + let mut unlock_bytes = Vec::new(); + unlock_bytes.push(redeem_script.len() as u8); + unlock_bytes.extend_from_slice(&redeem_script); + let unlock_script = transparent::Script::new(&unlock_bytes); + + // scriptPubKey: OP_HASH160 <20 bytes> OP_EQUAL. The 20-byte payload value is irrelevant here: + // `is_pay_to_script_hash()` only checks the length (23) and the surrounding opcodes. + let mut lock_bytes = Vec::with_capacity(23); + lock_bytes.push(OP_HASH160); + lock_bytes.push(0x14); + lock_bytes.extend_from_slice(&[0u8; 20]); + lock_bytes.push(OP_EQUAL); + let lock_script = transparent::Script::new(&lock_bytes); + + // Build a minimal non-coinbase transaction with a single P2SH input. + let input = transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: zebra_chain::transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script, + sequence: u32::MAX, + }; + + let spent_output = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script, + }; + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![input], + outputs: vec![spent_output.clone()], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + assert_eq!( + p2sh_sigop_count(&tx, std::slice::from_ref(&spent_output)), + 15, + "P2SH redeem-script sigops must be counted against block-path \ + MAX_BLOCK_SIGOPS (zcashd parity, GHSA-jv4h-j224-23cc)" + ); + + // The `CachedFfiTransaction::p2sh_sigops()` method must delegate to the same counter, so the + // block-verifier path yields the same count. + let cached = super::CachedFfiTransaction::new( + Arc::new(tx), + Arc::new(vec![spent_output]), + NetworkUpgrade::Nu5, + ) + .expect("network upgrade should be valid for tx"); + assert_eq!(cached.p2sh_sigops(), 15); + + Ok(()) +} + +/// Regression test: `p2sh_sigop_count` must agree with zcashd's +/// `GetP2SHSigOpCount()` when the redeem script contains a "disabled" opcode such as +/// `OP_CODESEPARATOR` (0xab). +#[test] +fn p2sh_sigop_count_matches_zcashd_when_redeem_script_contains_disabled_opcode() -> Result<()> { + let _init_guard = zebra_test::init(); + + const OP_HASH160: u8 = 0xa9; + const OP_EQUAL: u8 = 0x87; + const OP_CODESEPARATOR: u8 = 0xab; // "disabled" in the Rust parser; valid byte in zcashd's GetOp2 + const OP_CHECKMULTISIG: u8 = 0xae; + + // Redeem script: OP_CODESEPARATOR followed by 50 x OP_CHECKMULTISIG. + // zcashd's GetSigOpCount(true): lastOpcode is OP_INVALIDOPCODE before the first + // CHECKMULTISIG (and OP_CODESEPARATOR after it), so the `fAccurate && lastOpcode in + // OP_1..=OP_16` branch never fires; every CHECKMULTISIG contributes the fallback + // count of 20. Total: 50 * 20 = 1000. + let mut redeem_script = vec![OP_CODESEPARATOR]; + redeem_script.extend(std::iter::repeat_n(OP_CHECKMULTISIG, 50)); + assert_eq!(redeem_script.len(), 51); + + // scriptSig: a single direct push of the 51-byte redeem script. 51 <= 0x4b, so the + // literal-length push opcode is just the length byte followed by the payload. + let mut unlock_bytes = Vec::with_capacity(1 + redeem_script.len()); + unlock_bytes.push(redeem_script.len() as u8); + unlock_bytes.extend_from_slice(&redeem_script); + let unlock_script = transparent::Script::new(&unlock_bytes); + + // scriptPubKey: OP_HASH160 <20 bytes> OP_EQUAL. The hash value is irrelevant: only + // the shape (23 bytes, surrounding opcodes) is checked by `is_pay_to_script_hash`. + let mut lock_bytes = Vec::with_capacity(23); + lock_bytes.push(OP_HASH160); + lock_bytes.push(0x14); + lock_bytes.extend_from_slice(&[0u8; 20]); + lock_bytes.push(OP_EQUAL); + let lock_script = transparent::Script::new(&lock_bytes); + + let input = transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: zebra_chain::transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script, + sequence: u32::MAX, + }; + + let spent_output = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script, + }; + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![input], + outputs: vec![spent_output.clone()], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + // zcashd's GetP2SHSigOpCount() returns 1000 here; Zebra's pure-Rust counter + // short-circuits at the leading 0xab and returns 0. Failing this assertion is the + // consensus-split bug. + assert_eq!( + p2sh_sigop_count(&tx, std::slice::from_ref(&spent_output)), + 1000, + "P2SH redeem scripts containing a disabled opcode (e.g. OP_CODESEPARATOR) must \ + agree with zcashd's GetP2SHSigOpCount; the pure-Rust parser short-circuits on \ + disabled bytes, undercounting sigops and opening a chain split against zcashd" + ); + + // Same expectation through the block-verifier entry point. + let cached = super::CachedFfiTransaction::new( + Arc::new(tx), + Arc::new(vec![spent_output]), + NetworkUpgrade::Nu5, + ) + .expect("network upgrade should be valid for tx"); + assert_eq!( + cached.p2sh_sigops(), + 1000, + "CachedFfiTransaction::p2sh_sigops must agree with zcashd on redeem scripts \ + containing disabled opcodes" + ); + + Ok(()) +} + +/// Non-P2SH inputs, and coinbase inputs, must contribute zero P2SH sigops regardless of what bytes +/// appear in their `scriptSig`. zcashd skips the coinbase input in [`GetP2SHSigOpCount()`]. +/// +/// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/bad7f7eadbbb3466bebe3354266c7f69f607fcfd/src/main.cpp#L770-L772 +#[test] +fn p2sh_sigop_count_is_zero_for_non_p2sh_and_coinbase() -> Result<()> { + let _init_guard = zebra_test::init(); + + const OP_CHECKSIG: u8 = 0xac; + + // Build a coinbase tx with OP_CHECKSIG-filled `miner_data`: legacy counting sees these (Trigger + // A), but P2SH counting must not. + let network = Network::Mainnet; + let nu5_height = NetworkUpgrade::Nu5 + .activation_height(&network) + .expect("NU5 has a Mainnet activation height"); + let dummy_output_script = transparent::Script::new(&[0x51]); + let output_amount = zebra_chain::amount::Amount::try_from(1_000_000)?; + let coinbase_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![transparent::Input::Coinbase { + height: nu5_height, + data: vec![OP_CHECKSIG; 80], + sequence: 0xffff_ffff, + }], + outputs: vec![transparent::Output { + value: output_amount, + lock_script: dummy_output_script.clone(), + }], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + let _ = &network; + + // Coinbase inputs have no spent output; zcashd passes an empty vector. + assert_eq!(p2sh_sigop_count(&coinbase_tx, &[]), 0); + + // A non-P2SH (p2pkh-shaped) lock script must also yield 0 P2SH sigops, even if the scriptSig's + // last push happens to contain OP_CHECKSIG. + let p2pkh_lock = transparent::Script::new(&hex::decode( + "76a914f47cac1e6fec195c055994e8064ffccce0044dd788ac", + )?); + let mut unlock_bytes = vec![0x01_u8]; // single-byte push + unlock_bytes.push(OP_CHECKSIG); + let input = transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: zebra_chain::transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock_bytes), + sequence: u32::MAX, + }; + let spent_output = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script: p2pkh_lock, + }; + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![input], + outputs: vec![], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + assert_eq!(p2sh_sigop_count(&tx, &[spent_output]), 0); + + Ok(()) +} + +/// End-to-end regression test for [GHSA-jv4h-j224-23cc]. +/// +/// Reproduces the consensus-split condition: a block whose true `zcashd` transparent sigop total +/// (legacy + P2SH, including coinbase legacy) exceeds `MAX_BLOCK_SIGOPS = 20000`. Before the fix, +/// Zebra computed a reduced total that fit under the limit and accepted such a block. +/// +/// This test exercises the same accumulation the block verifier performs in +/// `zebra_consensus::block::verify_block` (`block.rs` `sigops += response.sigops()`), where each +/// transaction's contribution is `tx.sigops() + cached.p2sh_sigops()` (set in +/// `zebra_consensus::transaction.rs`'s `Block`-path response). +/// +/// Asserts: +/// - the legacy-only total (Zebra's pre-fix accounting) is below the limit; +/// - the legacy + P2SH total (Zebra's post-fix, zcashd-equivalent accounting) exceeds the limit, +/// demonstrating the consensus split is now visible to the block verifier. +/// +/// [GHSA-jv4h-j224-23cc]: https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-jv4h-j224-23cc +#[test] +fn block_sigop_total_includes_coinbase_and_p2sh() -> Result<()> { + let _init_guard = zebra_test::init(); + + /// Matches `zebra_consensus::MAX_BLOCK_SIGOPS`. Hard-coded to avoid a reverse dependency on + /// `zebra-consensus`. + const MAX_BLOCK_SIGOPS: u32 = 20_000; + const OP_CHECKSIG: u8 = 0xac; + const OP_HASH160: u8 = 0xa9; + const OP_EQUAL: u8 = 0x87; + + // Surface A: 80 OP_CHECKSIG bytes hidden in the coinbase scriptSig + // (the coinbase script length limit is 100 bytes, including the height prefix). + let network = Network::Mainnet; + let nu5_height = NetworkUpgrade::Nu5 + .activation_height(&network) + .expect("NU5 has a Mainnet activation height"); + let dummy_output_script = transparent::Script::new(&[0x51]); // OP_TRUE + let output_amount = zebra_chain::amount::Amount::try_from(1_000_000)?; + let coinbase_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![transparent::Input::Coinbase { + height: nu5_height, + data: vec![OP_CHECKSIG; 80], + sequence: 0xffff_ffff, + }], + outputs: vec![transparent::Output { + value: output_amount, + lock_script: dummy_output_script, + }], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + let _ = &network; + + // Surface B: each non-coinbase transaction has one P2SH input whose + // 15-byte redeem script is 15 x OP_CHECKSIG, contributing 15 P2SH + // sigops (the maximum standard P2SH redeem-script sigop count). + let redeem_script = vec![OP_CHECKSIG; 15]; + let mut unlock_bytes = vec![redeem_script.len() as u8]; + unlock_bytes.extend_from_slice(&redeem_script); + let unlock_script = transparent::Script::new(&unlock_bytes); + + let mut lock_bytes = Vec::with_capacity(23); + lock_bytes.push(OP_HASH160); + lock_bytes.push(0x14); + lock_bytes.extend_from_slice(&[0u8; 20]); + lock_bytes.push(OP_EQUAL); + let lock_script = transparent::Script::new(&lock_bytes); + + let p2sh_input = transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: zebra_chain::transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script, + sequence: u32::MAX, + }; + let p2sh_spent_output = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script, + }; + let p2sh_tx_template = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![p2sh_input], + outputs: vec![], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + // 1334 P2SH spends * 15 sigops = 20010 P2SH sigops. + // Plus 80 coinbase legacy sigops = 20090 total, > MAX_BLOCK_SIGOPS. + // The legacy-only total Zebra used to compute is 0 (pre-fix coinbase) + // or 80 (post-fix coinbase), both well under the limit. + const N_P2SH_TXS: u32 = 1334; + let spent_outputs = std::slice::from_ref(&p2sh_spent_output); + + let coinbase_legacy = coinbase_tx.sigops().expect("sigop count is finite"); + let p2sh_legacy = p2sh_tx_template.sigops().expect("sigop count is finite"); + let p2sh_per_tx = p2sh_sigop_count(&p2sh_tx_template, spent_outputs); + assert_eq!(coinbase_legacy, 80, "coinbase legacy sigops (Surface A)"); + assert_eq!(p2sh_per_tx, 15, "per-tx P2SH sigops (Surface B)"); + + // Post-fix accounting matches what the block verifier accumulates in + // `zebra_consensus::block::verify_block` via `response.sigops()`, where the response is built + // in `zebra_consensus::transaction.rs`'s `Block`-path arm as `tx.sigops() + + // cached.p2sh_sigops()`. + // + // Non-coinbase txs contribute legacy sigops from their inputs and outputs; here `p2sh_legacy` + // is 0 (the redeem script bytes inside the scriptSig are NOT executed at the legacy level for a + // literal push-only scriptSig). + let post_fix_total = coinbase_legacy + .saturating_add(N_P2SH_TXS.saturating_mul(p2sh_legacy.saturating_add(p2sh_per_tx))); + + assert!( + post_fix_total > MAX_BLOCK_SIGOPS, + "post-fix accounting must exceed MAX_BLOCK_SIGOPS to demonstrate \ + the consensus split is now visible: got {post_fix_total}" + ); + + Ok(()) +} + +/// Calling `is_valid` when `all_previous_outputs.len()` differs from `transaction.inputs().len()` +/// must return `Error::TxIndex`, even when the requested `input_index` is in range for +/// `all_previous_outputs`. This guards against verifying a script against a misaligned +/// previous output. +#[test] +fn is_valid_rejects_mismatched_previous_outputs_length() { + let _init_guard = zebra_test::init(); + + let transaction = SCRIPT_TX + .zcash_deserialize_into::>() + .expect("test fixture deserializes"); + + // SCRIPT_TX has exactly one input. Pass two previous outputs so `.get(0)` succeeds + // but the lengths disagree. + let output = Output { + value: (212 * u64::pow(10, 8)).try_into().expect("valid amount"), + lock_script: transparent::Script::new(&SCRIPT_PUBKEY), + }; + let mismatched_outputs = Arc::new(vec![output.clone(), output]); + + let verifier = + super::CachedFfiTransaction::new(transaction, mismatched_outputs, NetworkUpgrade::Blossom) + .expect("constructor accepts mismatched-length outputs"); + + let err = verifier + .is_valid(0) + .expect_err("mismatched length must be rejected by is_valid"); + assert_eq!(err, super::Error::TxIndex); +} + +/// Calling `is_valid` with an `input_index` past the end of `all_previous_outputs` must +/// return `Error::TxIndex` instead of panicking. +#[test] +fn is_valid_rejects_out_of_range_input_index() { + let _init_guard = zebra_test::init(); + + let transaction = SCRIPT_TX + .zcash_deserialize_into::>() + .expect("test fixture deserializes"); + let output = Output { + value: (212 * u64::pow(10, 8)).try_into().expect("valid amount"), + lock_script: transparent::Script::new(&SCRIPT_PUBKEY), + }; + let previous_outputs = Arc::new(vec![output]); + + let verifier = + super::CachedFfiTransaction::new(transaction, previous_outputs, NetworkUpgrade::Blossom) + .expect("matched-length construction succeeds"); + + // SCRIPT_TX has one input at index 0; index 1 is out of range. + let err = verifier + .is_valid(1) + .expect_err("out-of-range input_index must error, not panic"); + assert_eq!(err, super::Error::TxIndex); +} + +/// Regression test for the libzcash_script stale-sighash-buffer bypass. +/// +/// Construct a V5 transaction whose scriptPubKey is: +/// +/// ```text +/// OP_CHECKSIGVERIFY OP_CHECKSIG +/// ``` +/// +/// and whose scriptSig pushes two signatures over the same canonical +/// `SIGHASH_ALL` (0x01) digest, the first tagged with an *invalid* V5 +/// hash-type byte (0x50) and the second tagged with the canonical 0x01: +/// +/// ```text +/// (pushed first → bottom of stack) +/// (pushed second → top of stack) +/// ``` +/// +/// Script evaluation then: +/// +/// 1. Consumes `` via `OP_CHECKSIGVERIFY`. Zebra's callback +/// returns the canonical SIGHASH_ALL digest, the C++ verifier fills its +/// stack-local `sighashArray` with that digest, and the signature passes. +/// 2. Consumes `` via `OP_CHECKSIG`. Zebra's callback sees +/// an invalid V5 hash-type byte. Prior to this fix the callback returned +/// `None`, libzcash_script silently wrote nothing to the C++ buffer, and +/// the C++ `CheckSig` verified the signature against the stale +/// SIGHASH_ALL digest from step 1 — accepting a spend that `zcashd` +/// rejects and splitting Zebra nodes from `zcashd` nodes. +/// +/// With the defense-in-depth fix in `zebra-script::calculate_sighash`, the +/// callback now returns a per-call CSPRNG-derived sighash when the hash +/// type would have been rejected, so the second signature fails to verify +/// and `is_valid` returns an error — matching `zcashd`. +/// +/// The bypass requires release-grade C++ optimizations in `libzcash_script` +/// (so the stack buffer is not zero-initialized and the prior digest +/// lingers between callbacks). The workspace `Cargo.toml` forces +/// `[profile.dev.package.libzcash_script]` to `opt-level = 3` in all +/// profiles so that `cargo test` exercises the vulnerable code path and +/// this regression test catches any re-introduction of the bug in both +/// dev and release builds. +#[test] +fn stale_sighash_buffer_v5_two_checksig_rejected() { + use secp256k1::{Message, Secp256k1, SecretKey}; + + let _init_guard = zebra_test::init(); + + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("valid secret key"); + let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let pubkey_bytes = public_key.serialize(); + + // scriptPubKey: <0x21> OP_CHECKSIGVERIFY + // <0x21> OP_CHECKSIG + // OP_CHECKSIGVERIFY = 0xad, OP_CHECKSIG = 0xac + let mut lock_script_bytes = Vec::with_capacity(1 + 33 + 1 + 1 + 33 + 1); + lock_script_bytes.push(0x21); + lock_script_bytes.extend_from_slice(&pubkey_bytes); + lock_script_bytes.push(0xad); + lock_script_bytes.push(0x21); + lock_script_bytes.extend_from_slice(&pubkey_bytes); + lock_script_bytes.push(0xac); + let lock_script = transparent::Script::new(&lock_script_bytes); + + let previous_output = transparent::Output { + value: 1_0000_0000u64.try_into().expect("valid amount"), + lock_script: lock_script.clone(), + }; + + // Placeholder V5 tx used to compute the sighash; the V5 (ZIP 244) sighash + // does not depend on the unlock script contents, so we can sign, then + // rebuild the transaction with the real unlock script. + let placeholder_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&[]), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let all_previous_outputs = Arc::new(vec![previous_output.clone()]); + let sighasher = SigHasher::new(&placeholder_tx, NetworkUpgrade::Nu5, all_previous_outputs) + .expect("sighasher creation should succeed"); + + // Canonical SIGHASH_ALL digest — this is the digest the attacker needs + // the stale C++ buffer to still hold when the second CHECKSIG runs. + let sighash = sighasher.sighash(HashType::ALL, Some((0, lock_script_bytes.clone()))); + let msg = Message::from_digest(*sighash.as_ref()); + let signature = secp.sign_ecdsa(&msg, &secret_key); + let der_sig = signature.serialize_der(); + + // scriptSig pushes: + // 1. (bottom) + // 2. (top, consumed by OP_CHECKSIGVERIFY first) + let mut unlock_script_bytes = Vec::new(); + let sig_with_hashtype_len = (der_sig.len() + 1) as u8; + + unlock_script_bytes.push(sig_with_hashtype_len); + unlock_script_bytes.extend_from_slice(&der_sig); + unlock_script_bytes.push(0x50); + + unlock_script_bytes.push(sig_with_hashtype_len); + unlock_script_bytes.extend_from_slice(&der_sig); + unlock_script_bytes.push(0x01); + + let final_tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + lock_time: LockTime::unlocked(), + expiry_height: block::Height(0), + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock_script_bytes), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: 9000_0000u64.try_into().expect("valid amount"), + lock_script: transparent::Script::new(&[0x00]), + }], + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let verifier = super::CachedFfiTransaction::new( + Arc::new(final_tx), + Arc::new(vec![previous_output]), + NetworkUpgrade::Nu5, + ) + .expect("network upgrade should be valid for v5 tx"); + + assert!( + verifier.is_valid(0).is_err(), + "V5 tx exploiting the stale libzcash_script sighash buffer via \ + OP_CHECKSIGVERIFY + OP_CHECKSIG with an invalid second hash-type \ + byte (0x50) must be rejected, matching zcashd" + ); +} + +#[test] +fn p2sh_sigop_count_uses_accurate_multisig_mode() -> Result<()> { + let _init_guard = zebra_test::init(); + + // P2SH redeem script: OP_1 <33-byte pubkey> OP_1 OP_CHECKMULTISIG (a 1-of-1 multisig). + // zcashd's GetP2SHSigOpCount counts the redeem script with GetSigOpCount(true), so an + // OP_N-prefixed CHECKMULTISIG counts as N (here 1). The legacy mode counts it as 20; + // over-counting here lets a zcashd-valid block exceed Zebra's MAX_BLOCK_SIGOPS and splits + // Zebra off the chain. This guards the accurate-mode P2SH counter. + let mut redeem = vec![0x51u8, 0x21u8]; + redeem.extend_from_slice(&[0x02u8; 33]); + redeem.extend_from_slice(&[0x51u8, 0xaeu8]); + + let mut unlock = vec![redeem.len() as u8]; + unlock.extend_from_slice(&redeem); + + let mut lock = vec![0xa9u8, 0x14u8]; + lock.extend_from_slice(&[0u8; 20]); + lock.push(0x87u8); + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script: transparent::Script::new(&unlock), + sequence: u32::MAX, + }], + outputs: vec![transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script: transparent::Script::new(&[0x51]), + }], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + let spent = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script: transparent::Script::new(&lock), + }; + + // Accurate mode: 1. (Legacy mode would return 20.) + assert_eq!(p2sh_sigop_count(&tx, std::slice::from_ref(&spent)), 1); + Ok(()) +} + +fn poc_p2sh_1_of_1_multisig_scripts() -> (transparent::Script, transparent::Script) { + const OP_1: u8 = 0x51; + const OP_HASH160: u8 = 0xa9; + const OP_EQUAL: u8 = 0x87; + const OP_CHECKMULTISIG: u8 = 0xae; + + // Redeem script: + // OP_1 <33-byte compressed pubkey> OP_1 OP_CHECKMULTISIG + // + // zcashd's P2SH path calls GetSigOpCount(true), so this counts as 1 sigop. + // Zebra's vulnerable path calls legacy_sigop_count_script -> GetSigOpCount(false), + // so this counts as 20 sigops. + let pubkey = [0x02u8; 33]; + + let mut redeem_script = Vec::with_capacity(37); + redeem_script.push(OP_1); + redeem_script.push(0x21); // push 33-byte pubkey + redeem_script.extend_from_slice(&pubkey); + redeem_script.push(OP_1); + redeem_script.push(OP_CHECKMULTISIG); + assert_eq!(redeem_script.len(), 37); + + // scriptSig: push redeem_script as the final push. + // The actual HASH160 match/signature validity is irrelevant for this accounting PoC; + // p2sh_sigop_count only checks the spent output is P2SH-shaped and extracts the last push. + let mut unlock_bytes = Vec::with_capacity(1 + redeem_script.len()); + unlock_bytes.push(redeem_script.len() as u8); + unlock_bytes.extend_from_slice(&redeem_script); + let unlock_script = transparent::Script::new(&unlock_bytes); + + // P2SH scriptPubKey shape: + // OP_HASH160 <20-byte hash> OP_EQUAL + // + // The hash value is intentionally dummy. p2sh_sigop_count only needs IsPayToScriptHash shape. + let mut lock_bytes = Vec::with_capacity(23); + lock_bytes.push(OP_HASH160); + lock_bytes.push(0x14); + lock_bytes.extend_from_slice(&[0u8; 20]); + lock_bytes.push(OP_EQUAL); + let lock_script = transparent::Script::new(&lock_bytes); + + (unlock_script, lock_script) +} + +#[test] +fn poc_p2sh_accurate_multisig_should_count_one_not_twenty() -> Result<()> { + let _init_guard = zebra_test::init(); + + let (unlock_script, lock_script) = poc_p2sh_1_of_1_multisig_scripts(); + + let input = transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: zebra_chain::transaction::Hash([0u8; 32]), + index: 0, + }, + unlock_script, + sequence: u32::MAX, + }; + + let spent_output = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script, + }; + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs: vec![input], + outputs: vec![transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script: transparent::Script::new(&[0x51]), // OP_TRUE dummy output + }], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let zebra_count = p2sh_sigop_count(&tx, std::slice::from_ref(&spent_output)); + + assert_eq!( + zebra_count, 1, + "P2SH OP_1 OP_1 OP_CHECKMULTISIG must use accurate sigop mode" + ); + + Ok(()) +} + +#[test] +fn poc_p2sh_1001_accurate_multisigs_should_stay_below_block_sigop_limit() -> Result<()> { + let _init_guard = zebra_test::init(); + + const SPENDS: usize = 1_001; + + let (unlock_script, lock_script) = poc_p2sh_1_of_1_multisig_scripts(); + + let inputs: Vec<_> = (0..SPENDS) + .map(|i| { + let mut hash = [0u8; 32]; + hash[..8].copy_from_slice(&(i as u64).to_le_bytes()); + + transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: zebra_chain::transaction::Hash(hash), + index: 0, + }, + unlock_script: unlock_script.clone(), + sequence: u32::MAX, + } + }) + .collect(); + + let spent_output = transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script, + }; + + let spent_outputs = vec![spent_output; SPENDS]; + + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu5, + inputs, + outputs: vec![transparent::Output { + value: zebra_chain::amount::Amount::try_from(1_000_000)?, + lock_script: transparent::Script::new(&[0x51]), // OP_TRUE dummy output + }], + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let zebra_count = p2sh_sigop_count(&tx, &spent_outputs); + let zcashd_accurate_count = SPENDS as u32; + + assert_eq!( + zebra_count, zcashd_accurate_count, + "Zebra should count each accurate 1-of-1 P2SH multisig spend as 1 sigop, not 20" + ); + + assert!( + zebra_count <= 20_000, + "A zcashd-valid block-level sigop total should not cross Zebra's MAX_BLOCK_SIGOPS" + ); + + Ok(()) +} diff --git a/zebra-state/CHANGELOG.md b/zebra-state/CHANGELOG.md index d62064326f7..c7e7de7d274 100644 --- a/zebra-state/CHANGELOG.md +++ b/zebra-state/CHANGELOG.md @@ -7,6 +7,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.0.1] - 2026-06-18 + +### Changed + +- Increased the local rollback window (`MAX_BLOCK_REORG_HEIGHT`) from 99 to 1000 + blocks as a defence-in-depth measure against sustained consensus splits + ([#10650](https://github.com/ZcashFoundation/zebra/pull/10650)) + +## [9.0.0] - 2026-06-10 + +### Breaking Changes + +- Removed `deferred_pool_balance_change` field from `ContextuallyVerifiedBlock`, + `SemanticallyVerifiedBlock`, and `CheckpointVerifiedBlock`. The deferred pool + balance change is now calculated on demand and passed as a parameter to + `FinalizedBlock::from_checkpoint_verified()` and + `FinalizedBlock::from_contextually_verified()`. +- `CheckpointVerifiedBlock::new()` no longer takes a `deferred_pool_balance_change` + parameter. + +### Fixed + +- `QueuedBlocks::dequeue_children()`: fixed `by_height` index handling to remove + individual hashes instead of the entire height entry, preventing loss of queued + blocks at the same height + ([#10604](https://github.com/ZcashFoundation/zebra/pull/10604)). + +## [8.0.0] - 2026-06-02 + +### Changed + +- Release for NU6.2 support (updates `zebra-chain` to 9.0.0). Internal refactor of the + chain-tip mempool-reset height computation; no public API or behavior change. + +## [7.0.0] - 2026-05-28 + +This release fixes four state security issues: + +- Drop rejected block hashes from `SentHashes` so honest re-deliveries of a + block are no longer short-circuited as duplicates + ([GHSA-4m69-67m6-prqp](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-4m69-67m6-prqp). +- Apply transparent address-balance updates per-transaction in + debit-before-credit order so same-address self-spend chains do not push + intermediate balances above `MAX_MONEY` and panic + ([GHSA-w834-cf6p-9m9w](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-w834-cf6p-9m9w)). +- Pop the matching Sapling/Orchard subtree when popping a non-finalized tip + that completed one + ([GHSA-2gf8-q9rr-jq3h](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-2gf8-q9rr-jq3h)). +- Reject repeated shielded transactions cleanly before the defence-in-depth + `tx_loc_by_hash` assertion + ([GHSA-hhm7-qrv5-h4r6](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-hhm7-qrv5-h4r6)). + +The impact of these issues for crate users will depend on the particular +usage; if you use it as a building block for a consensus node, you should +update. + +### Added + +- `CommitBlockError::misbehavior_score(&self) -> u32` (currently always `0`; + mirrors the misbehavior-score API in `zebra-consensus` / + `zebra-network`). +- `SentHashes::remove(&mut self, hash: &block::Hash)`, used to drop rejected + block hashes and their tracked outpoints/buffers. + +### Changed + +- `service::write::BlockWriteSender::spawn` return tuple gained a fourth + element: an `UnboundedReceiver` that delivers hashes of + write-task-rejected non-finalized blocks so the `StateService` can clear + them from `SentHashes`. +- `zebra-chain` dependency bumped to `8.0.0`. +- `zebra-node-services` dependency bumped to `6.0.0`. + +## [6.0.0] - 2026-05-01 + +### Changed + +- `zebra-chain` bumped to `7.0.0`. No direct public-API changes in this crate, + but consumers of re-exported `zebra-chain` items (e.g. `constants::MIN_TRANSPARENT_COINBASE_MATURITY`) + inherit that crate's breaking changes. + ## [5.0.0] - 2026-03-12 ### Breaking Changes diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 9e918984e26..63a43272159 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-state" -version = "5.0.0" +version = "9.0.1" authors.workspace = true description = "State contextual verification and storage code for Zebra" license.workspace = true @@ -78,8 +78,8 @@ sapling-crypto = { workspace = true } elasticsearch = { workspace = true, features = ["rustls-tls"], optional = true } serde_json = { workspace = true, optional = true } -zebra-node-services = { path = "../zebra-node-services", version = "4.0.0" } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = ["async-error"] } +zebra-node-services = { path = "../zebra-node-services", version = "8.0.0" } +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = ["async-error"] } # prod feature progress-bar howudoin = { workspace = true, optional = true } @@ -109,7 +109,7 @@ jubjub = { workspace = true } tokio = { workspace = true, features = ["full", "tracing", "test-util"] } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = ["proptest-impl"] } +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test/", version = "3.0.0" } [lints] diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index b7566dc4a1a..0f8e6899ddc 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use zebra_chain::{ - amount::Amount, + amount::{Amount, DeferredPoolBalanceChange}, block::{self, Block}, transaction::Transaction, transparent, @@ -85,6 +85,7 @@ impl ContextuallyVerifiedBlock { ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, zero_spent_utxos, + DeferredPoolBalanceChange::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] Default::default(), ) @@ -103,7 +104,6 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, transaction_sighashes, - deferred_pool_balance_change: _, } = block.into(); Self { diff --git a/zebra-state/src/constants.rs b/zebra-state/src/constants.rs index 9e46d79e185..acaabcb185c 100644 --- a/zebra-state/src/constants.rs +++ b/zebra-state/src/constants.rs @@ -13,22 +13,10 @@ use crate::{ pub use zebra_chain::transparent::MIN_TRANSPARENT_COINBASE_MATURITY; -/// The maximum chain reorganisation height. -/// -/// This threshold determines the maximum length of the best non-finalized chain. -/// Larger reorganisations would allow double-spends of coinbase transactions. -/// -/// This threshold uses the relevant chain for the block being verified by the -/// non-finalized state. -/// -/// For the best chain, coinbase spends are only allowed from blocks at or below -/// the finalized tip. For other chains, coinbase spends can use outputs from -/// early non-finalized blocks, or finalized blocks. But if that chain becomes -/// the best chain, all non-finalized blocks past the [`MAX_BLOCK_REORG_HEIGHT`] -/// will be finalized. This includes all mature coinbase outputs. -// -// TODO: change to HeightDiff -pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1; +/// The maximum chain reorganisation height; it bounds the length of the best +/// non-finalized chain. The value lives in `zebra-chain` so tooling (e.g. +/// `zebra-checkpoints`) can use it without depending on `zebra-state`. +pub const MAX_BLOCK_REORG_HEIGHT: u32 = zebra_chain::parameters::constants::MAX_BLOCK_REORG_HEIGHT; /// The directory name used to distinguish the state database from Zebra's other databases or flat files. pub const STATE_DATABASE_KIND: &str = "state"; @@ -99,8 +87,8 @@ pub const MAX_LEGACY_CHAIN_BLOCKS: usize = 100_000; /// 100 blocks. (1 fork per 20 blocks.) When block propagation is efficient, there is around /// 1 fork per 300 blocks. /// -/// This limits non-finalized chain memory to around: -/// `10 forks * 100 blocks * 2 MB per block = 2 GB` +/// This limits non-finalized chain memory, in the worst case, to around: +/// `10 forks * 1000 blocks * 2 MB per block = 20 GB` pub const MAX_NON_FINALIZED_CHAIN_FORKS: usize = 10; /// The maximum number of block hashes allowed in `getblocks` responses in the Zcash network protocol. @@ -111,8 +99,10 @@ pub const MAX_FIND_BLOCK_HEADERS_RESULTS: u32 = 160; /// The maximum number of invalidated block records. /// -/// This limits the memory use to around: -/// `100 entries * up to 99 blocks * 2 MB per block = 20 GB` +/// Each record can hold a chain of invalidated descendants up to the rollback +/// window ([`MAX_BLOCK_REORG_HEIGHT`]) deep, so this limits the memory use, in +/// the worst case, to around: +/// `100 entries * up to 1000 blocks * 2 MB per block = 200 GB` pub const MAX_INVALIDATED_BLOCKS: usize = 100; lazy_static! { diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 54fda445ee1..60fbd3130db 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -73,6 +73,11 @@ impl CommitBlockError { pub fn is_duplicate_request(&self) -> bool { matches!(self, CommitBlockError::Duplicate { .. }) } + + /// Returns a suggested misbehaviour score increment for a certain error. + pub fn misbehavior_score(&self) -> u32 { + 0 + } } /// An error describing why a `CommitSemanticallyVerified` request failed. @@ -430,3 +435,26 @@ impl DuplicateNullifierError for orchard::Nullifier { } } } + +#[cfg(test)] +mod tests { + use super::*; + use zebra_chain::block::Height; + + #[test] + fn commit_block_error_misbehavior_scores() { + let context_err = CommitBlockError::ValidateContextError(Box::new( + ValidateContextError::NonSequentialBlock { + candidate_height: Height(5), + parent_height: Height(3), + }, + )); + assert_eq!(context_err.misbehavior_score(), 0); + + let dup_err = CommitBlockError::Duplicate { + hash_or_height: None, + location: KnownBlock::BestChain, + }; + assert_eq!(dup_err.misbehavior_score(), 0); + } +} diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index d3469627089..8e38ca88912 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -264,8 +264,6 @@ pub struct SemanticallyVerifiedBlock { /// A precomputed list of the sighashes of the transactions in this block, /// in the same order as `block.transactions`. pub transaction_sighashes: Option>, - /// This block's deferred pool value balance change. - pub deferred_pool_balance_change: Option, } /// A block ready to be committed directly to the finalized state with @@ -377,7 +375,7 @@ impl Treestate { /// /// Zebra's state service passes this `enum` over to the finalized state /// when committing a block. -#[allow(missing_docs)] +#[allow(missing_docs, clippy::large_enum_variant)] pub enum FinalizableBlock { Checkpoint { checkpoint_verified: CheckpointVerifiedBlock, @@ -408,7 +406,7 @@ pub struct FinalizedBlock { /// The tresstate associated with the block. pub(super) treestate: Treestate, /// This block's deferred pool value balance change. - pub(super) deferred_pool_balance_change: Option, + pub(super) deferred_pool_balance_change: DeferredPoolBalanceChange, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] /// Asset state changes to be applied to the finalized state. /// Contains (old_state, new_state) pairs for assets modified in this block. @@ -418,10 +416,15 @@ pub struct FinalizedBlock { impl FinalizedBlock { /// Constructs [`FinalizedBlock`] from [`CheckpointVerifiedBlock`] and its [`Treestate`]. - pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self { + pub fn from_checkpoint_verified( + block: CheckpointVerifiedBlock, + treestate: Treestate, + deferred_pool_balance_change: DeferredPoolBalanceChange, + ) -> Self { Self::from_semantically_verified( SemanticallyVerifiedBlock::from(block), treestate, + deferred_pool_balance_change, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, ) @@ -431,12 +434,14 @@ impl FinalizedBlock { pub fn from_contextually_verified( block: ContextuallyVerifiedBlock, treestate: Treestate, + deferred_pool_balance_change: DeferredPoolBalanceChange, ) -> Self { #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] let issued_asset_changes = Some(block.issued_asset_changes.clone()); Self::from_semantically_verified( SemanticallyVerifiedBlock::from(block), treestate, + deferred_pool_balance_change, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] issued_asset_changes, ) @@ -446,6 +451,7 @@ impl FinalizedBlock { fn from_semantically_verified( block: SemanticallyVerifiedBlock, treestate: Treestate, + deferred_pool_balance_change: DeferredPoolBalanceChange, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] issued_asset_changes: Option< IssuedAssetChanges, >, @@ -457,7 +463,7 @@ impl FinalizedBlock { new_outputs: block.new_outputs, transaction_hashes: block.transaction_hashes, treestate, - deferred_pool_balance_change: block.deferred_pool_balance_change, + deferred_pool_balance_change, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] issued_asset_changes, } @@ -525,6 +531,7 @@ impl ContextuallyVerifiedBlock { pub fn with_block_and_spent_utxos( semantically_verified: SemanticallyVerifiedBlock, mut spent_outputs: HashMap, + deferred_pool_balance_change: DeferredPoolBalanceChange, #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] issued_asset_changes: IssuedAssetChanges, ) -> Result { @@ -535,7 +542,6 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, transaction_sighashes, - deferred_pool_balance_change, } = semantically_verified; // This is redundant for the non-finalized state, @@ -565,14 +571,8 @@ impl ContextuallyVerifiedBlock { impl CheckpointVerifiedBlock { /// Creates a [`CheckpointVerifiedBlock`] from [`Block`] with optional deferred balance and /// optional pre-computed hash. - pub fn new( - block: Arc, - hash: Option, - deferred_pool_balance_change: Option, - ) -> Self { - let mut block = Self::with_hash(block.clone(), hash.unwrap_or(block.hash())); - block.deferred_pool_balance_change = deferred_pool_balance_change; - block + pub fn new(block: Arc, hash: Option) -> Self { + Self::with_hash(block.clone(), hash.unwrap_or(block.hash())) } /// Creates a block that's ready to be committed to the finalized state, @@ -602,18 +602,8 @@ impl SemanticallyVerifiedBlock { transaction_hashes, // Not used in checkpoint paths. transaction_sighashes: None, - deferred_pool_balance_change: None, } } - - /// Sets the deferred balance in the block. - pub fn with_deferred_pool_balance_change( - mut self, - deferred_pool_balance_change: Option, - ) -> Self { - self.deferred_pool_balance_change = deferred_pool_balance_change; - self - } } impl From> for CheckpointVerifiedBlock { @@ -638,7 +628,6 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, transaction_sighashes: None, - deferred_pool_balance_change: None, } } } @@ -652,9 +641,24 @@ impl From for SemanticallyVerifiedBlock { new_outputs: valid.new_outputs, transaction_hashes: valid.transaction_hashes, transaction_sighashes: valid.transaction_sighashes, - deferred_pool_balance_change: Some(DeferredPoolBalanceChange::new( - valid.chain_value_pool_change.deferred_amount(), - )), + } + } +} + +impl From for SemanticallyVerifiedBlock { + fn from(finalized: FinalizedBlock) -> Self { + Self { + block: finalized.block, + hash: finalized.hash, + height: finalized.height, + new_outputs: finalized.new_outputs, + transaction_hashes: finalized.transaction_hashes, + + // `None` is correct: a `FinalizedBlock`'s issuance is already settled — verified + // via sighashes on the contextual path, or trusted on the checkpoint path — and + // `FinalizedBlock` carries no sighashes to forward. + // Moreover, this impl is unused in the current version of Zebra. + transaction_sighashes: None, } } } @@ -867,6 +871,10 @@ pub enum Request { /// /// /// + /// Note: Zebra's local rollback window + /// ([`MAX_BLOCK_REORG_HEIGHT`](crate::MAX_BLOCK_REORG_HEIGHT)) is now 1000 blocks, larger + /// than the 100 quoted from the protocol specification above. + /// /// # Correctness /// /// Block commit requests should be wrapped in a timeout, so that diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 04b6408935b..8d74bac4e91 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -90,7 +90,8 @@ pub use self::traits::{ReadState, State}; /// A read-write service for Zebra's cached blockchain state. /// /// This service modifies and provides access to: -/// - the non-finalized state: the ~100 most recent blocks. +/// - the non-finalized state: the most recent blocks, up to +/// [`MAX_BLOCK_REORG_HEIGHT`](crate::MAX_BLOCK_REORG_HEIGHT) of them. /// Zebra allows chain forks in the non-finalized state, /// stores it in memory, and re-downloads it when restarted. /// - the finalized state: older blocks that have many confirmations. @@ -156,6 +157,15 @@ pub(crate) struct StateService { // TODO: add tests for finalized and non-finalized resets (#2654) invalid_block_write_reset_receiver: tokio::sync::mpsc::UnboundedReceiver, + /// Receives the hash of every non-finalized block that the write task + /// rejected, so the corresponding entry can be removed from + /// `non_finalized_block_write_sent_hashes`. + /// + /// Without this, a rejected same-hash block locks out a later honest + /// re-delivery of a block at the same hash as a "duplicate" until restart + /// or reorg. + non_finalized_rejected_receiver: tokio::sync::mpsc::UnboundedReceiver, + // Pending UTXO Request Tracking // /// The set of outpoints with pending requests for their associated transparent::Output. @@ -183,7 +193,8 @@ pub(crate) struct StateService { /// A read-only service for accessing Zebra's cached blockchain state. /// /// This service provides read-only access to: -/// - the non-finalized state: the ~100 most recent blocks. +/// - the non-finalized state: the most recent blocks, up to +/// [`MAX_BLOCK_REORG_HEIGHT`](crate::MAX_BLOCK_REORG_HEIGHT) of them. /// - the finalized state: older blocks that have many confirmations. /// /// Requests to this service are processed in parallel, @@ -231,6 +242,7 @@ impl Drop for StateService { // This makes the block write thread exit the next time it checks the channels. // We want to do this here so we get any errors or panics from the block write task before it shuts down. self.invalid_block_write_reset_receiver.close(); + self.non_finalized_rejected_receiver.close(); std::mem::drop(self.block_write_sender.finalized.take()); std::mem::drop(self.block_write_sender.non_finalized.take()); @@ -368,15 +380,19 @@ impl StateService { let finalized_state_for_writing = finalized_state.clone(); let should_use_finalized_block_write_sender = non_finalized_state.is_chain_set_empty(); let sync_backup_dir_path = backup_dir_path.filter(|_| skip_backup_task); - let (block_write_sender, invalid_block_write_reset_receiver, block_write_task) = - write::BlockWriteSender::spawn( - finalized_state_for_writing, - non_finalized_state, - chain_tip_sender, - non_finalized_state_sender, - should_use_finalized_block_write_sender, - sync_backup_dir_path, - ); + let ( + block_write_sender, + invalid_block_write_reset_receiver, + non_finalized_rejected_receiver, + block_write_task, + ) = write::BlockWriteSender::spawn( + finalized_state_for_writing, + non_finalized_state, + chain_tip_sender, + non_finalized_state_sender, + should_use_finalized_block_write_sender, + sync_backup_dir_path, + ); let read_service = ReadStateService::new( &finalized_state, @@ -406,6 +422,7 @@ impl StateService { finalized_block_write_last_sent_hash, non_finalized_block_write_sent_hashes, invalid_block_write_reset_receiver, + non_finalized_rejected_receiver, pending_utxos, last_prune: Instant::now(), read_service: read_service.clone(), @@ -602,6 +619,38 @@ impl StateService { } } + /// Drains every hash queued on `non_finalized_rejected_receiver` and + /// removes it from `non_finalized_block_write_sent_hashes`. + /// + /// This closes the lockout window where a rejected block keeps its hash + /// recorded as "sent", so a subsequent honest re-delivery of a block at + /// the same hash is not short-circuited as a false "duplicate". + /// + /// # Correctness & Performance + /// + /// Like the other drain methods on `StateService`, this must not block, + /// access the database, or perform CPU-intensive work, because it is + /// called directly from the tokio executor's Future threads. + fn drain_non_finalized_rejected_hashes(&mut self) { + use tokio::sync::mpsc::error::TryRecvError; + + loop { + match self.non_finalized_rejected_receiver.try_recv() { + Ok(hash) => { + self.non_finalized_block_write_sent_hashes.remove(&hash); + } + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => { + info!( + "Block commit task closed the non-finalized rejected hash channel. \ + Is Zebra shutting down?" + ); + break; + } + } + } + } + /// Drops all finalized state queue blocks, and sends an error on their result channels. fn clear_finalized_block_queue( &mut self, @@ -663,6 +712,12 @@ impl StateService { tracing::debug!(block = %semantically_verified.block, "queueing block for contextual verification"); let parent_hash = semantically_verified.block.header.previous_block_hash; + // Drop hashes of any blocks the write task has rejected before checking + // the SentHashes membership below. Without this, a rejected same-hash + // block would lock out a later honest re-delivery of a block at the + // same hash as a false "duplicate". + self.drain_non_finalized_rejected_hashes(); + if self .non_finalized_block_write_sent_hashes .contains(&semantically_verified.hash) diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 854f76b0c83..0bfd6178484 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -118,7 +118,6 @@ impl From for ChainTipBlock { // Sighashes are not needed here - they are only used during block validation, // which has already completed before a block reaches `ChainTipBlock`. transaction_sighashes: _, - deferred_pool_balance_change: _, } = prepared; Self { @@ -585,7 +584,7 @@ impl ChainTipChange { // // https://zips.z.cash/zip-0200#chain-reorganization - // If we're at a network upgrade activation block, reset. + // If the block *before* a network upgrade activation block becomes the tip, reset. // // Consensus rules: // @@ -595,11 +594,32 @@ impl ChainTipChange { // // https://zips.z.cash/zip-0200#memory-pool // + // ZIP-200 phrases this as "when the tip reaches ACTIVATION_HEIGHT", but the mempool + // verifies its transactions against the *next* block, i.e. the child of the current tip. + // A transaction in the mempool becomes invalid as soon as the next block it could be + // mined into is the activation block, which happens when the tip reaches + // `ACTIVATION_HEIGHT - 1`. So we reset when the *next* height is an activation height, + // one block earlier than the literal wording, so the mempool is already cleared by the + // time the activation block is mined on top of the tip. + // // Skipped blocks can include network upgrade activation blocks. // Fork changes can activate or deactivate a network upgrade. // So we must perform the same actions for network upgrades and skipped blocks. + // + // The soft fork that temporarily disables Orchard actions is not a network upgrade, but + // it has the same memory-pool requirement and is verified against the next height in the + // same way, so we also reset when the next height is its activation height. + // + // The next height always exists because the chain tip height is far below `Height::MAX`. + let next_height = block + .height + .next() + .expect("chain tip height is far below Height::MAX"); if Some(block.previous_block_hash) != self.last_change_hash - || NetworkUpgrade::is_activation_height(&self.network, block.height) + || NetworkUpgrade::is_activation_height(&self.network, next_height) + || self + .network + .is_temporary_orchard_disabling_soft_fork_activation_height(next_height) { TipAction::reset_with(block) } else { diff --git a/zebra-state/src/service/chain_tip/tests/prop.rs b/zebra-state/src/service/chain_tip/tests/prop.rs index 744bd0be097..c5ca59d7194 100644 --- a/zebra-state/src/service/chain_tip/tests/prop.rs +++ b/zebra-state/src/service/chain_tip/tests/prop.rs @@ -128,11 +128,22 @@ proptest! { let old_last_change_hash = chain_tip_change.last_change_hash; let new_action = expected_tip.and_then(|(chain_tip, block)| { + // Mirror `ChainTipChange::action`: the mempool verifies against the *next* + // height, so a reset happens at `activation_height - 1`, and also at the + // temporary-Orchard-disabling soft fork's activation height (see commit + // 548672c5e). `next` always succeeds because the tip is far below `Height::MAX`. + let next_height = chain_tip + .height + .next() + .expect("chain tip height is far below Height::MAX"); + if Some(chain_tip.hash) == old_last_change_hash { // some updates don't do anything, so there's no new action None } else if Some(chain_tip.previous_block_hash) != old_last_change_hash - || NetworkUpgrade::is_activation_height(&network, chain_tip.height) + || NetworkUpgrade::is_activation_height(&network, next_height) + || network + .is_temporary_orchard_disabling_soft_fork_activation_height(next_height) { Some(TipAction::reset_with(block.0.into())) } else { diff --git a/zebra-state/src/service/check/tests/issuance.rs b/zebra-state/src/service/check/tests/issuance.rs index 5f15e22f298..b22e56322be 100644 --- a/zebra-state/src/service/check/tests/issuance.rs +++ b/zebra-state/src/service/check/tests/issuance.rs @@ -80,7 +80,7 @@ fn check_burns_and_issuance() { }, ); - let CheckpointVerifiedBlock(block) = CheckpointVerifiedBlock::new(block, None, None); + let CheckpointVerifiedBlock(block) = CheckpointVerifiedBlock::new(block, None); let commit_result = validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block); diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 4a8063eac14..5dea96a47a6 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -969,6 +969,237 @@ proptest! { prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db)); prop_assert!(non_finalized_state.eq_internal_state(&previous_mem)); } + + /// A block whose sprout-shielded transaction has the same hash as one + /// already finalized must be rejected with `DuplicateSproutNullifier`. + #[test] + fn reject_block_containing_sprout_tx_already_in_finalized_chain( + mut joinsplit in TypeNameToDebug::>::arbitrary(), + joinsplit_data in TypeNameToDebug::>::arbitrary(), + ) { + let _init_guard = zebra_test::init(); + + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + make_distinct_nullifiers(&mut joinsplit.nullifiers); + let expected_duplicate_nullifier = joinsplit.nullifiers[0]; + + let transaction = Arc::new(transaction_v4_with_joinsplit_data(joinsplit_data.0, [joinsplit.0])); + + block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); + block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); + + // Push the same Arc into both blocks so they share a tx hash. + block1.transactions.push(transaction.clone()); + block2.transactions.push(transaction); + + let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis(); + + finalized_state.populate_with_anchors(&block1); + finalized_state.populate_with_anchors(&block2); + + let block1 = CheckpointVerifiedBlock::from(Arc::new(block1)); + let commit_result = finalized_state.commit_finalized_direct(block1.into(), None, "test"); + prop_assert!(commit_result.is_ok()); + + let block2 = Arc::new(block2).prepare(); + let commit_result = validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block2, + ); + + prop_assert_eq!( + commit_result, + Err(DuplicateSproutNullifier { + nullifier: expected_duplicate_nullifier, + in_finalized_state: true, + }) + ); + } + + /// A block whose sapling-shielded transaction has the same hash as one + /// already finalized must be rejected with `DuplicateSaplingNullifier`. + #[test] + fn reject_block_containing_sapling_tx_already_in_finalized_chain( + spend in TypeNameToDebug::>::arbitrary(), + sapling_shielded_data in TypeNameToDebug::>::arbitrary(), + ) { + let _init_guard = zebra_test::init(); + + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let expected_duplicate_nullifier = spend.nullifier; + + let transaction = Arc::new(transaction_v4_with_sapling_shielded_data( + sapling_shielded_data.0, + [spend.0], + )); + + block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); + block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); + + block1.transactions.push(transaction.clone()); + block2.transactions.push(transaction); + + let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis(); + + finalized_state.populate_with_anchors(&block1); + finalized_state.populate_with_anchors(&block2); + + let block1 = CheckpointVerifiedBlock::from(Arc::new(block1)); + let commit_result = finalized_state.commit_finalized_direct(block1.into(), None, "test"); + prop_assert!(commit_result.is_ok()); + + let block2 = Arc::new(block2).prepare(); + let commit_result = validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block2, + ); + + prop_assert_eq!( + commit_result, + Err(DuplicateSaplingNullifier { + nullifier: expected_duplicate_nullifier, + in_finalized_state: true, + }) + ); + } + + /// A block whose orchard-shielded transaction has the same hash as one + /// already finalized must be rejected with `DuplicateOrchardNullifier`. + #[test] + fn reject_block_containing_orchard_tx_already_in_finalized_chain( + authorized_action in TypeNameToDebug::>::arbitrary(), + orchard_shielded_data in TypeNameToDebug::>::arbitrary(), + ) { + let _init_guard = zebra_test::init(); + + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let expected_duplicate_nullifier = authorized_action.action.nullifier; + + let transaction = Arc::new(transaction_v5_with_orchard_shielded_data( + orchard_shielded_data.0, + [authorized_action.0], + )); + + block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); + block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); + + block1.transactions.push(transaction.clone()); + block2.transactions.push(transaction); + + let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis(); + + finalized_state.populate_with_anchors(&block1); + finalized_state.populate_with_anchors(&block2); + + let block1 = CheckpointVerifiedBlock::from(Arc::new(block1)); + let commit_result = finalized_state.commit_finalized_direct(block1.into(), None, "test"); + prop_assert!(commit_result.is_ok()); + + let block2 = Arc::new(block2).prepare(); + let commit_result = validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block2, + ); + + prop_assert_eq!( + commit_result, + Err(DuplicateOrchardNullifier { + nullifier: expected_duplicate_nullifier, + in_finalized_state: true, + }) + ); + } + + /// A block whose sapling-shielded transaction has the same hash as one + /// already present in the **non-finalized** part of the chain must be + /// rejected with `DuplicateSaplingNullifier` — not panic. + /// + /// This is the BIP30-style duplicate-txid scenario where the prior + /// occurrence lives in the non-finalized chain (so the + /// `no_duplicates_in_finalized_chain` check in `initial_contextual_validity` + /// cannot catch it). The fix in + /// `zebra-state/src/service/non_finalized_state/chain.rs` runs the + /// shielded nullifier check before the `tx_loc_by_hash` assertion, + /// so the duplicate is rejected via the existing nullifier check. + #[test] + fn reject_block_containing_sapling_tx_already_in_non_finalized_chain( + spend in TypeNameToDebug::>::arbitrary(), + sapling_shielded_data in TypeNameToDebug::>::arbitrary(), + ) { + let _init_guard = zebra_test::init(); + + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let expected_duplicate_nullifier = spend.nullifier; + + let transaction = Arc::new(transaction_v4_with_sapling_shielded_data( + sapling_shielded_data.0, + [spend.0], + )); + + block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into(); + block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into(); + + // Push the same Arc into both blocks so they share a tx hash. + block1.transactions.push(transaction.clone()); + block2.transactions.push(transaction); + + let (finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis(); + + finalized_state.populate_with_anchors(&block1); + finalized_state.populate_with_anchors(&block2); + + // Commit block1 to the *non-finalized* state so the duplicate-tx + // scenario is purely within the non-finalized chain. + let block1 = Arc::new(block1).prepare(); + let commit_result = validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block1, + ); + prop_assert_eq!(commit_result, Ok(())); + + let block2 = Arc::new(block2).prepare(); + let commit_result = validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + block2, + ); + + prop_assert_eq!( + commit_result, + Err(DuplicateSaplingNullifier { + nullifier: expected_duplicate_nullifier, + in_finalized_state: false, + }) + ); + } } /// Make sure the supplied nullifiers are distinct, modifying them if necessary. diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index 7ce8b348100..0987736e5d7 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -19,7 +19,12 @@ use std::{ sync::Arc, }; -use zebra_chain::{block, parallel::tree::NoteCommitmentTrees, parameters::Network}; +use zebra_chain::{ + amount::DeferredPoolBalanceChange, + block, + parallel::tree::NoteCommitmentTrees, + parameters::{subsidy::block_subsidy, Network}, +}; use zebra_db::{ chain::BLOCK_INFO, transparent::{BALANCE_BY_TRANSPARENT_ADDR, TX_LOC_BY_SPENT_OUT_LOC}, @@ -387,22 +392,35 @@ impl FinalizedState { history_tree, }; + let height = checkpoint_verified.height; + ( - checkpoint_verified.height, + height, checkpoint_verified.hash, - FinalizedBlock::from_checkpoint_verified(checkpoint_verified, treestate), + FinalizedBlock::from_checkpoint_verified( + checkpoint_verified, + treestate, + calculate_deferred_pool_balance_change(height, &self.network()), + ), Some(prev_note_commitment_trees), ) } FinalizableBlock::Contextual { contextually_verified, treestate, - } => ( - contextually_verified.height, - contextually_verified.hash, - FinalizedBlock::from_contextually_verified(contextually_verified, treestate), - prev_note_commitment_trees, - ), + } => { + let height = contextually_verified.height; + ( + height, + contextually_verified.hash, + FinalizedBlock::from_contextually_verified( + contextually_verified, + treestate, + calculate_deferred_pool_balance_change(height, &self.network()), + ), + prev_note_commitment_trees, + ) + } }; let committed_tip_hash = self.db.finalized_tip_hash(); @@ -597,3 +615,27 @@ impl FinalizedState { std::process::exit(0); } } + +/// Calculates the deferred pool balance change for a given height and network. +/// +/// Returns a deferred pool balance change of zero if it cannot be calculated. +pub(crate) fn calculate_deferred_pool_balance_change( + height: block::Height, + network: &Network, +) -> DeferredPoolBalanceChange { + if height > network.slow_start_interval() { + zebra_chain::parameters::subsidy::funding_stream_values( + height, + network, + block_subsidy(height, network).unwrap_or_default(), + ) + .unwrap_or_default() + .remove(&zebra_chain::parameters::subsidy::FundingStreamReceiver::Deferred) + .unwrap_or_default() + .checked_sub(network.lockbox_disbursement_total_amount(height)) + .map(DeferredPoolBalanceChange::new) + .unwrap_or_default() + } else { + DeferredPoolBalanceChange::zero() + } +} diff --git a/zebra-state/src/service/finalized_state/disk_db.rs b/zebra-state/src/service/finalized_state/disk_db.rs index 8bd24c9b067..eca94c3f792 100644 --- a/zebra-state/src/service/finalized_state/disk_db.rs +++ b/zebra-state/src/service/finalized_state/disk_db.rs @@ -671,7 +671,7 @@ impl DiskDb { // Level-by-level file counts (RocksDB typically has up to 7 levels) for level in 0..7 { - let prop = format!("rocksdb.num-files-at-level{}", level); + let prop = format!("rocksdb.num-files-at-level{level}"); if let Ok(Some(count)) = db.property_int_value(&prop) { metrics::gauge!("zebra.state.rocksdb.num_files_at_level", "level" => level.to_string()) .set(count as f64); diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs b/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs index 91ddaadc359..44108cd4f53 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs @@ -94,7 +94,14 @@ fn roundtrip_transaction_hash() { fn roundtrip_transaction() { let _init_guard = zebra_test::init(); - proptest!(|(val in any::())| assert_value_properties(val)); + proptest!(|(val in any::())| { + // Coinbase transactions with Sapling spends are rejected during deserialization + // (GHSA-rgwx-8r98-p34c), so they cannot round-trip through `IntoDisk`/`FromDisk`. + // The arbitrary `Transaction` strategy still produces them so the + // `transaction_roundtrip` proptest in `zebra-chain` can exercise the rejection path. + prop_assume!(!(val.is_coinbase() && val.sapling_spends_per_anchor().count() > 0)); + assert_value_properties(val) + }); } // Transparent diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs index ad059d7d844..6c29d578510 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs @@ -7,16 +7,16 @@ use crossbeam_channel::TryRecvError; use itertools::Itertools; use rayon::iter::{IntoParallelIterator, ParallelIterator as _}; use zebra_chain::{ - amount::{DeferredPoolBalanceChange, NonNegative}, + amount::NonNegative, block::{Block, Height}, block_info::BlockInfo, - parameters::subsidy::{block_subsidy, funding_stream_values, FundingStreamReceiver}, transparent::{self, OutPoint, Utxo}, value_balance::ValueBalance, }; use crate::{ service::finalized_state::{ + calculate_deferred_pool_balance_change, disk_format::transparent::{AddressBalanceLocationChange, AddressLocation}, MAX_ON_DISK_HEIGHT, }, @@ -176,34 +176,13 @@ impl DiskFormatUpgrade for Upgrade { } => (block, size, utxos, address_balance_changes), }; - // Get the deferred amount which is required to update the value pool. - let deferred_pool_balance_change = if height > network.slow_start_interval() { - // See [ZIP-1015](https://zips.z.cash/zip-1015). - let deferred_pool_balance_change = funding_stream_values( - height, - &network, - block_subsidy(height, &network).unwrap_or_default(), - ) - .expect("should have valid funding stream values") - .remove(&FundingStreamReceiver::Deferred) - .unwrap_or_default() - .checked_sub(network.lockbox_disbursement_total_amount(height)); - - Some( - deferred_pool_balance_change - .expect("deferred pool balance change should be valid Amount"), - ) - } else { - None - }; - // Add this block's value pool changes to the total value pool. value_pool = value_pool .add_chain_value_pool_change( block .chain_value_pool_change( &utxos, - deferred_pool_balance_change.map(DeferredPoolBalanceChange::new), + calculate_deferred_pool_balance_change(height, &network), ) .unwrap_or_default(), ) diff --git a/zebra-state/src/service/finalized_state/tests.rs b/zebra-state/src/service/finalized_state/tests.rs index ee11a74bd08..f70c55bfff8 100644 --- a/zebra-state/src/service/finalized_state/tests.rs +++ b/zebra-state/src/service/finalized_state/tests.rs @@ -3,4 +3,5 @@ #![allow(clippy::unwrap_in_result)] mod prop; +mod transparent; mod vectors; diff --git a/zebra-state/src/service/finalized_state/tests/prop.rs b/zebra-state/src/service/finalized_state/tests/prop.rs index 322185a6446..2a1b85a2697 100644 --- a/zebra-state/src/service/finalized_state/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/tests/prop.rs @@ -73,6 +73,7 @@ fn all_upgrades_and_wrong_commitments_with_fake_activation_heights() -> Result<( nu5: Some(35), nu6: Some(40), nu6_1: Some(45), + nu6_2: Some(47), nu7: Some(50), }) .expect("failed to set activation heights") diff --git a/zebra-state/src/service/finalized_state/tests/transparent.rs b/zebra-state/src/service/finalized_state/tests/transparent.rs new file mode 100644 index 00000000000..21d79071dc2 --- /dev/null +++ b/zebra-state/src/service/finalized_state/tests/transparent.rs @@ -0,0 +1,206 @@ +//! Fixed-vector tests for the finalized transparent address balance writer. +//! +//! Regression coverage for the credit-before-debit overflow panic: applying a +//! same-address transparent self-spend chain in one block used to push the +//! intermediate address balance above `MAX_MONEY` and panic the writer, even +//! though the final consensus balance was valid. +//! +//! This test drives `DiskWriteBatch::prepare_transparent_transaction_batch` +//! (the writer's public entry point, whose signature is unchanged by the fix) +//! so the same source compiles against both the buggy and the fixed revision: +//! it panics on the buggy revision and passes on the fixed one. + +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; + +use zebra_chain::{ + amount::{Amount, DeferredPoolBalanceChange, NonNegative, MAX_MONEY}, + block::{self, Block, Height}, + parameters::{Network, NetworkKind}, + serialization::ZcashDeserializeInto, + transaction::{self, LockTime, Transaction}, + transparent::{ + self, new_ordered_outputs_with_height, Address, Input, OutPoint, Output, Script, + }, +}; + +use crate::{ + constants::{state_database_format_version_in_code, STATE_DATABASE_KIND}, + request::{FinalizedBlock, SemanticallyVerifiedBlock, Treestate}, + service::finalized_state::{ + disk_db::DiskWriteBatch, + disk_format::transparent::{ + AddressBalanceLocation, AddressBalanceLocationUpdates, OutputLocation, + }, + ZebraDb, STATE_COLUMN_FAMILIES_IN_CODE, + }, + CheckpointVerifiedBlock, Config, +}; + +fn new_ephemeral_zebra_db(network: &Network) -> ZebraDb { + ZebraDb::new( + &Config::ephemeral(), + STATE_DATABASE_KIND, + &state_database_format_version_in_code(), + network, + // The raw database accesses in this test create invalid database formats. + true, + STATE_COLUMN_FAMILIES_IN_CODE + .iter() + .map(ToString::to_string), + false, + ) +} + +/// Cross-version regression test for the credit-before-debit overflow panic. +/// +/// Builds a synthetic block whose two transactions form a same-address self-spend +/// chain that re-creates an existing `MAX_MONEY / 2` UTXO twice, and drives it +/// through [`DiskWriteBatch::prepare_transparent_transaction_batch`]. +/// +/// - On the **buggy** revision the writer credits both new outputs before debiting +/// the matching spends, so the intermediate balance reaches `1.5 * MAX_MONEY` +/// and panics with `"balance overflow already checked"`. +/// - On the **fixed** revision the writer processes each transaction in block order +/// (debit-then-credit), so the per-address running balance stays in +/// `[0, MAX_MONEY]` and the final on-disk balance equals the original +/// `MAX_MONEY / 2`. +#[test] +fn intra_block_self_spend_chain_in_finalized_state() { + let _init_guard = zebra_test::init(); + + let network = Network::Mainnet; + let height = Height(1); + let address = Address::from_script_hash(NetworkKind::Mainnet, [0x42; 20]); + let value = Amount::::try_from(MAX_MONEY / 2) + .expect("MAX_MONEY / 2 fits in Amount"); + + // T0 spends a pre-existing on-chain UTXO of value V to address A and re-creates V to A. + let existing_outpoint = OutPoint { + hash: transaction::Hash([0x00; 32]), + index: 0, + }; + let t0 = Arc::new(Transaction::V1 { + inputs: vec![Input::PrevOut { + outpoint: existing_outpoint, + unlock_script: Script::new(&[]), + sequence: 0xffff_ffff, + }], + outputs: vec![Output::new(value, address.script())], + lock_time: LockTime::unlocked(), + }); + let t0_hash = t0.hash(); + + // T1 spends T0's output and creates V back to A. + let t0_output_outpoint = OutPoint { + hash: t0_hash, + index: 0, + }; + let t1 = Arc::new(Transaction::V1 { + inputs: vec![Input::PrevOut { + outpoint: t0_output_outpoint, + unlock_script: Script::new(&[]), + sequence: 0xffff_ffff, + }], + outputs: vec![Output::new(value, address.script())], + lock_time: LockTime::unlocked(), + }); + + // Synthetic block. The header is a dummy from zebra-test (round-tripped); the writer + // doesn't validate the header — only the transactions and the block height matter. + let header: block::Header = zebra_test::vectors::DUMMY_HEADER + .as_slice() + .zcash_deserialize_into() + .expect("DUMMY_HEADER deserializes"); + let block = Arc::new(Block { + header: Arc::new(header), + transactions: vec![t0.clone(), t1.clone()], + }); + let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); + let new_outputs = new_ordered_outputs_with_height(&block, height, &transaction_hashes); + + let semantically_verified = SemanticallyVerifiedBlock { + block: block.clone(), + hash: block::Hash([0x00; 32]), + height, + new_outputs, + transaction_hashes, + + // No sighashes needed: this test only drives the transparent output batch, + // which never reads them. + transaction_sighashes: None, + }; + let finalized = FinalizedBlock::from_checkpoint_verified( + CheckpointVerifiedBlock(semantically_verified), + Treestate::default(), + DeferredPoolBalanceChange::zero(), + ); + + // Inputs to `prepare_transparent_transaction_batch`, prepared the way `write_block` does. + let existing_output_location = OutputLocation::from_usize(Height(0), 0, 0); + let t0_output_location = OutputLocation::from_usize(height, 0, 0); + let t1_output_location = OutputLocation::from_usize(height, 1, 0); + + let make_utxo = + |h: Height| transparent::Utxo::new(Output::new(value, address.script()), h, false); + let existing_utxo = make_utxo(Height(0)); + let t0_output_utxo = make_utxo(height); + let t1_output_utxo = make_utxo(height); + + let new_outputs_by_out_loc: BTreeMap = BTreeMap::from([ + (t0_output_location, t0_output_utxo.clone()), + (t1_output_location, t1_output_utxo), + ]); + let spent_utxos_by_outpoint: HashMap = HashMap::from([ + (existing_outpoint, existing_utxo.clone()), + (t0_output_outpoint, t0_output_utxo.clone()), + ]); + let spent_utxos_by_out_loc: BTreeMap = BTreeMap::from([ + (existing_output_location, existing_utxo), + (t0_output_location, t0_output_utxo), + ]); + + // Pre-populate `address_balances` with A's pre-block on-chain balance, the way + // `block.rs` does via `read_addr_locs`. + let mut existing_abl = AddressBalanceLocation::new(existing_output_location); + *existing_abl.balance_mut() = value; + *existing_abl.received_mut() = u64::from(value); + let address_balances = + AddressBalanceLocationUpdates::Insert(HashMap::from([(address, existing_abl)])); + + let zebra_db = new_ephemeral_zebra_db(&network); + let mut batch = DiskWriteBatch::new(); + + // On the buggy revision this call panics with "balance overflow already checked" during + // the credit-first batch (intermediate balance reaches 1.5 * MAX_MONEY). On the fixed + // revision it completes cleanly. + batch.prepare_transparent_transaction_batch( + &zebra_db, + &network, + &finalized, + &new_outputs_by_out_loc, + &spent_utxos_by_outpoint, + &spent_utxos_by_out_loc, + #[cfg(feature = "indexer")] + &HashMap::new(), + address_balances, + ); + + // Write the batch and confirm the final on-disk balance matches the consensus value + // (existing V − 2*V debits + 2*V credits = V). + zebra_db + .write_batch(batch) + .expect("ephemeral db accepts the batch"); + + let (balance, received) = zebra_db + .address_balance(&address) + .expect("address balance is present after writing the batch"); + assert_eq!(balance, value, "final balance equals the existing balance"); + assert_eq!( + received, + u64::from(value).saturating_mul(3), + "received counts the existing V plus two intra-block credits of V", + ); +} diff --git a/zebra-state/src/service/finalized_state/tests/vectors.rs b/zebra-state/src/service/finalized_state/tests/vectors.rs index 9e4f1abb171..0bf5ba80900 100644 --- a/zebra-state/src/service/finalized_state/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/tests/vectors.rs @@ -455,7 +455,7 @@ fn sprout_checks(incremental_tree: SproutNoteCommitmentTree, expected_serialized deserialized_legacy_tree_as_new.recalculate_root() ); - // Check reclaculated and cached roots are the same + // Check recalculated and cached roots are the same assert_eq!( incremental_tree.recalculate_root(), deserialized_tree @@ -509,7 +509,7 @@ fn sapling_checks( deserialized_legacy_tree_as_new.recalculate_root() ); - // Check reclaculated and cached roots are the same + // Check recalculated and cached roots are the same assert_eq!( incremental_tree.recalculate_root(), deserialized_tree @@ -585,7 +585,7 @@ fn orchard_checks( deserialized_legacy_tree_as_new.recalculate_root() ); - // Check reclaculated and cached roots are the same + // Check recalculated and cached roots are the same assert_eq!( incremental_tree.recalculate_root(), deserialized_tree diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 85197ed3707..786f3cfa78f 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -526,7 +526,7 @@ impl ZebraDb { ) -> HashMap { changed_addresses .into_iter() - .filter_map(|address| Some((address.clone(), f(&address)?))) + .filter_map(|address| Some((address, f(&address)?))) .collect() } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs index 832649b9c15..82a214099c6 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/snapshot.rs @@ -551,7 +551,7 @@ fn snapshot_transparent_address_data(state: &FinalizedState, height: u32) { // The default raw data serialization is very verbose, so we hex-encode the bytes. stored_address_balances.push((address.to_string(), stored_address_balance_location)); stored_address_utxo_locations.push((stored_address_location, stored_utxo_locations)); - stored_address_utxos.push((address.clone(), stored_utxos)); + stored_address_utxos.push((address, stored_utxos)); stored_address_transaction_locations.push((address, stored_transaction_locations)); } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 444d496b5f0..411a0576a9a 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -13,6 +13,7 @@ use std::{iter, sync::Arc}; use zebra_chain::{ + amount::DeferredPoolBalanceChange, block::{ tests::generate::{ large_multi_transaction_block, large_single_transaction_block_many_inputs, @@ -137,13 +138,15 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, transaction_sighashes: None, - deferred_pool_balance_change: None, }) }; let dummy_treestate = Treestate::default(); - let finalized = - FinalizedBlock::from_checkpoint_verified(checkpoint_verified, dummy_treestate); + let finalized = FinalizedBlock::from_checkpoint_verified( + checkpoint_verified, + dummy_treestate, + DeferredPoolBalanceChange::zero(), + ); // Skip validation by writing the block directly to the database let mut batch = DiskWriteBatch::new(); diff --git a/zebra-state/src/service/finalized_state/zebra_db/transparent.rs b/zebra-state/src/service/finalized_state/zebra_db/transparent.rs index beade3489db..a5aec765587 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/transparent.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/transparent.rs @@ -15,6 +15,7 @@ use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, ops::RangeInclusive, + sync::Arc, }; use rocksdb::ColumnFamily; @@ -433,18 +434,32 @@ impl DiskWriteBatch { let db = &zebra_db.db; let FinalizedBlock { block, height, .. } = finalized; - // Update created and spent transparent outputs + // Update the in-memory `address_balances` transaction-by-transaction, debiting inputs + // before crediting outputs within each transaction. This ordering keeps every + // intermediate per-address balance within the consensus range, even when the block + // contains a same-address transparent self-spend chain whose batch credit-first + // intermediate balance would otherwise exceed MAX_MONEY. + Self::prepare_transparent_address_balance_updates( + network, + *height, + &block.transactions, + spent_utxos_by_outpoint, + &mut address_balances, + ); + + // Write the new and spent transparent output index entries. These passes no longer + // touch `address_balances`; they only read each entry's `address_location()`. self.prepare_new_transparent_outputs_batch( db, network, new_outputs_by_out_loc, - &mut address_balances, + &address_balances, ); self.prepare_spent_transparent_outputs_batch( db, network, spent_utxos_by_out_loc, - &mut address_balances, + &address_balances, ); // Index the transparent addresses that spent in each transaction @@ -466,6 +481,92 @@ impl DiskWriteBatch { self.prepare_transparent_balances_batch(db, address_balances); } + /// Update `address_balances` in memory for the transparent transfers in `transactions`, + /// processed transaction-by-transaction in block order, debiting inputs before crediting + /// outputs within each transaction. + /// + /// This mirrors `zcashd`'s `UpdateCoins` and is what allows a same-address transparent + /// self-spend chain in one block to be applied without the intermediate per-address + /// balance exceeding `MAX_MONEY`. For any consensus-valid block, every per-step + /// intermediate balance stays inside the [`Amount`] constraint of the enclosing + /// `AddressBalanceLocationUpdates` variant. + /// + /// This function does not touch the RocksDB batch; index writes are still handled by + /// [`Self::prepare_new_transparent_outputs_batch`] and + /// [`Self::prepare_spent_transparent_outputs_batch`], which read but no longer mutate + /// `address_balances`. + fn prepare_transparent_address_balance_updates( + network: &Network, + height: Height, + transactions: &[Arc], + spent_utxos_by_outpoint: &HashMap, + address_balances: &mut AddressBalanceLocationUpdates, + ) { + fn update_per_tx< + C: Constraint + Copy + std::fmt::Debug, + T: std::ops::DerefMut> + + From>, + >( + addr_locs: &mut HashMap, + network: &Network, + height: Height, + transactions: &[Arc], + spent_utxos_by_outpoint: &HashMap, + ) { + for (tx_index, transaction) in transactions.iter().enumerate() { + // Debit transparent inputs first. Coinbase inputs have no outpoint, so + // `filter_map(Input::outpoint)` skips them. + for spent_outpoint in transaction.inputs().iter().filter_map(Input::outpoint) { + let spent_utxo = spent_utxos_by_outpoint + .get(&spent_outpoint) + .expect("spent outpoint must already be resolved"); + if let Some(sending_address) = spent_utxo.output.address(network) { + let addr_loc = addr_locs + .get_mut(&sending_address) + .expect("spent outputs must already have an address balance"); + + addr_loc + .spend_output(&spent_utxo.output) + .expect("balance underflow already checked"); + } + } + + // Then credit transparent outputs. + for (output_index, output) in transaction.outputs().iter().enumerate() { + if let Some(receiving_address) = output.address(network) { + let new_output_location = + OutputLocation::from_usize(height, tx_index, output_index); + + let addr_loc = addr_locs.entry(receiving_address).or_insert_with(|| { + AddressBalanceLocationInner::new(new_output_location).into() + }); + + addr_loc + .receive_output(output) + .expect("balance overflow already checked"); + } + } + } + } + + match address_balances { + AddressBalanceLocationUpdates::Merge(balance_changes) => update_per_tx( + balance_changes, + network, + height, + transactions, + spent_utxos_by_outpoint, + ), + AddressBalanceLocationUpdates::Insert(balances) => update_per_tx( + balances, + network, + height, + transactions, + spent_utxos_by_outpoint, + ), + } + } + /// Prepare a database batch for the new UTXOs in `new_outputs_by_out_loc`. /// /// Adds the following changes to this batch: @@ -475,7 +576,10 @@ impl DiskWriteBatch { /// /// without actually writing anything. /// - /// Also modifies the `address_balances` for these new UTXOs. + /// `address_balances` must already be populated for every transparent address that + /// receives one of these outputs (see + /// [`Self::prepare_transparent_address_balance_updates`]); this function only reads + /// `address_location()` from it. /// /// # Errors /// @@ -486,7 +590,7 @@ impl DiskWriteBatch { db: &DiskDb, network: &Network, new_outputs_by_out_loc: &BTreeMap, - address_balances: &mut AddressBalanceLocationUpdates, + address_balances: &AddressBalanceLocationUpdates, ) { let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap(); let utxo_loc_by_transparent_addr_loc = @@ -499,52 +603,16 @@ impl DiskWriteBatch { let unspent_output = &utxo.output; let receiving_address = unspent_output.address(network); - // Update the address balance by adding this UTXO's value if let Some(receiving_address) = receiving_address { - // TODO: fix up tests that use missing outputs, - // then replace entry() with get_mut().expect() - - // In memory: - // - create the balance for the address, if needed. - // - create or fetch the link from the address to the AddressLocation - // (the first location of the address in the chain). - - fn update_addr_loc< - C: Constraint + Copy + std::fmt::Debug, - T: std::ops::DerefMut> - + From>, - >( - addr_locs: &mut HashMap, - receiving_address: transparent::Address, - new_output_location: &OutputLocation, - unspent_output: &transparent::Output, - ) -> AddressLocation { - let addr_loc = addr_locs.entry(receiving_address).or_insert_with(|| { - AddressBalanceLocationInner::new(*new_output_location).into() - }); - - // Update the balance for the address in memory. - addr_loc - .receive_output(unspent_output) - .expect("balance overflow already checked"); - - addr_loc.address_location() - } - - // Update the balance for the address in memory. let receiving_address_location = match address_balances { - AddressBalanceLocationUpdates::Merge(balance_changes) => update_addr_loc( - balance_changes, - receiving_address, - new_output_location, - unspent_output, - ), - AddressBalanceLocationUpdates::Insert(balances) => update_addr_loc( - balances, - receiving_address, - new_output_location, - unspent_output, - ), + AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes + .get(&receiving_address) + .expect("address must be in address_balances after the balance update pass") + .address_location(), + AddressBalanceLocationUpdates::Insert(balances) => balances + .get(&receiving_address) + .expect("address must be in address_balances after the balance update pass") + .address_location(), }; // Create a link from the AddressLocation to the new OutputLocation in the database. @@ -580,7 +648,10 @@ impl DiskWriteBatch { /// /// without actually writing anything. /// - /// Also modifies the `address_balances` for these new UTXOs. + /// `address_balances` must already be populated for every transparent address that + /// spends one of these outputs (see + /// [`Self::prepare_transparent_address_balance_updates`]); this function only reads + /// `address_location()` from it. /// /// # Errors /// @@ -591,7 +662,7 @@ impl DiskWriteBatch { db: &DiskDb, network: &Network, spent_utxos_by_out_loc: &BTreeMap, - address_balances: &mut AddressBalanceLocationUpdates, + address_balances: &AddressBalanceLocationUpdates, ) { let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap(); let utxo_loc_by_transparent_addr_loc = @@ -604,36 +675,17 @@ impl DiskWriteBatch { let spent_output = &utxo.output; let sending_address = spent_output.address(network); - // Fetch the balance, and the link from the address to the AddressLocation, from memory. + // Fetch the link from the address to the AddressLocation, from memory. if let Some(sending_address) = sending_address { - fn update_addr_loc< - C: Constraint + Copy + std::fmt::Debug, - T: std::ops::DerefMut> - + From>, - >( - addr_locs: &mut HashMap, - sending_address: transparent::Address, - spent_output: &transparent::Output, - ) -> AddressLocation { - let addr_loc = addr_locs - .get_mut(&sending_address) - .expect("spent outputs must already have an address balance"); - - // Update the address balance by subtracting this UTXO's value, in memory. - addr_loc - .spend_output(spent_output) - .expect("balance underflow already checked"); - - addr_loc.address_location() - } - let address_location = match address_balances { - AddressBalanceLocationUpdates::Merge(balance_changes) => { - update_addr_loc(balance_changes, sending_address, spent_output) - } - AddressBalanceLocationUpdates::Insert(balances) => { - update_addr_loc(balances, sending_address, spent_output) - } + AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes + .get(&sending_address) + .expect("spent outputs must already have an address balance") + .address_location(), + AddressBalanceLocationUpdates::Insert(balances) => balances + .get(&sending_address) + .expect("spent outputs must already have an address balance") + .address_location(), }; // Delete the link from the AddressLocation to the spent OutputLocation in the database. diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 0ee720a0957..8caf8162b66 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -25,7 +25,11 @@ use crate::{ constants::{MAX_INVALIDATED_BLOCKS, MAX_NON_FINALIZED_CHAIN_FORKS}, error::ReconsiderError, request::{ContextuallyVerifiedBlock, FinalizableBlock}, - service::{check, finalized_state::ZebraDb, InvalidateError}, + service::{ + check, + finalized_state::{calculate_deferred_pool_balance_change, ZebraDb}, + InvalidateError, + }, SemanticallyVerifiedBlock, ValidateContextError, WatchReceiver, }; @@ -607,6 +611,7 @@ impl NonFinalizedState { let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( prepared.clone(), spent_utxos.clone(), + calculate_deferred_pool_balance_change(prepared.height, &self.network), // TODO: Refactor this into repeated `With::with()` calls, see http_request_compatibility module. #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] issued_assets, @@ -689,7 +694,8 @@ impl NonFinalizedState { /// or `None` if the best chain has no blocks. pub fn best_chain_len(&self) -> Option { // This `as` can't overflow because the number of blocks in the chain is limited to i32::MAX, - // and the non-finalized chain is further limited by the fork length (slightly over 100 blocks). + // and the non-finalized chain is further limited by the rollback window + // (`MAX_BLOCK_REORG_HEIGHT`, currently 1000 blocks). Some(self.best_chain()?.blocks.len() as u32) } diff --git a/zebra-state/src/service/non_finalized_state/backup.rs b/zebra-state/src/service/non_finalized_state/backup.rs index cdb284e7862..4fc205ba499 100644 --- a/zebra-state/src/service/non_finalized_state/backup.rs +++ b/zebra-state/src/service/non_finalized_state/backup.rs @@ -9,7 +9,7 @@ use std::{ use hex::ToHex; use zebra_chain::{ - amount::{Amount, DeferredPoolBalanceChange}, + amount::Amount, block::{self, Block, Height}, serialization::{ZcashDeserializeInto, ZcashSerialize}, }; @@ -236,12 +236,9 @@ fn read_non_finalized_blocks_from_backup<'a>( match NonFinalizedBlockBackup::from_bytes(backup_block_file_contents) { Ok(NonFinalizedBlockBackup { block, - deferred_pool_balance_change, + deferred_pool_balance_change: _, }) if block.coinbase_height().is_some() => { - let block = SemanticallyVerifiedBlock::from(block) - .with_deferred_pool_balance_change(Some(DeferredPoolBalanceChange::new( - deferred_pool_balance_change, - ))); + let block = SemanticallyVerifiedBlock::from(block); if block.hash != expected_block_hash { tracing::warn!( block_hash = ?block.hash, diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 498b5a2cf69..8a0d2e32c02 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -1292,6 +1292,25 @@ impl Chain { .remove(&block_height) .expect("only called while blocks is populated"); + // If the popped block completed a Sapling or Orchard subtree, remove the corresponding + // subtree from this chain too. Subtrees are inserted by `push` keyed by the highest subtree + // index, so the last entry's `end_height` matches the popped block iff a subtree was + // completed at that height. + if self + .sapling_subtrees + .last_key_value() + .is_some_and(|(_, subtree)| subtree.end_height == block_height) + { + self.sapling_subtrees.pop_last(); + } + if self + .orchard_subtrees + .last_key_value() + .is_some_and(|(_, subtree)| subtree.end_height == block_height) + { + self.orchard_subtrees.pop_last(); + } + assert!( !self.blocks.is_empty(), "Non-finalized chains must have at least one block to be valid" @@ -1706,6 +1725,31 @@ impl Chain { orchard_shielded_data_zsa, ) = transaction_data; + // Shielded-data updates run before the transparent updates and + // the `tx_loc_by_hash` insert so that a duplicate transaction + // (same hash → same nullifiers) is rejected with a clean + // `Duplicate{Sprout|Sapling|Orchard}Nullifier` error by + // `add_to_non_finalized_chain_unique` before reaching the + // defense-in-depth assertions on `tx_loc_by_hash`, + // `created_utxos`, and `spent_utxos` below. + { + #[cfg(not(feature = "indexer"))] + let transaction_hash = (); + + self.update_chain_tip_with(&(joinsplit_data, &transaction_hash))?; + self.update_chain_tip_with(&( + sapling_shielded_data_per_spend_anchor, + &transaction_hash, + ))?; + self.update_chain_tip_with(&( + sapling_shielded_data_shared_anchor, + &transaction_hash, + ))?; + self.update_chain_tip_with(&(orchard_shielded_data_vanilla, &transaction_hash))?; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + self.update_chain_tip_with(&(orchard_shielded_data_zsa, &transaction_hash))?; + } + // add key `transaction.hash` and value `(height, tx_index)` to `tx_loc_by_hash` let transaction_location = TransactionLocation::from_usize(height, transaction_index); let prior_pair = self @@ -1720,21 +1764,6 @@ impl Chain { self.update_chain_tip_with(&(outputs, &transaction_hash, new_outputs))?; // delete the utxos this consumed self.update_chain_tip_with(&(inputs, &transaction_hash, spent_outputs))?; - - // add the shielded data - - #[cfg(not(feature = "indexer"))] - let transaction_hash = (); - - self.update_chain_tip_with(&(joinsplit_data, &transaction_hash))?; - self.update_chain_tip_with(&( - sapling_shielded_data_per_spend_anchor, - &transaction_hash, - ))?; - self.update_chain_tip_with(&(sapling_shielded_data_shared_anchor, &transaction_hash))?; - self.update_chain_tip_with(&(orchard_shielded_data_vanilla, &transaction_hash))?; - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - self.update_chain_tip_with(&(orchard_shielded_data_zsa, &transaction_hash))?; } // update the chain value pool balances diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index df579e9196b..31f3a28cc58 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeMap, env, sync::Arc}; use zebra_test::prelude::*; use zebra_chain::{ - amount::NonNegative, + amount::{DeferredPoolBalanceChange, NonNegative}, block::{self, arbitrary::allow_all_transparent_coinbase_spends, Block, Height}, history_tree::{HistoryTree, NonEmptyHistoryTree}, parameters::NetworkUpgrade::*, @@ -52,6 +52,7 @@ fn push_genesis_chain() -> Result<()> { ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, only_chain.unspent_utxos(), + DeferredPoolBalanceChange::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] Default::default(), ) @@ -150,6 +151,7 @@ fn forked_equals_pushed_genesis() -> Result<()> { let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, partial_chain.unspent_utxos(), + DeferredPoolBalanceChange::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] Default::default() )?; @@ -170,9 +172,8 @@ fn forked_equals_pushed_genesis() -> Result<()> { ); for block in chain.iter().cloned() { - let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( - block, - full_chain.unspent_utxos(), + let block = + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos(), DeferredPoolBalanceChange::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] Default::default() )?; @@ -218,9 +219,10 @@ fn forked_equals_pushed_genesis() -> Result<()> { // same original full chain. for block in chain.iter().skip(fork_at_count).cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos(), - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - Default::default())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos(), DeferredPoolBalanceChange::zero(), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default() + )?; forked = forked.push(block).expect("forked chain push is valid"); } diff --git a/zebra-state/src/service/non_finalized_state/tests/vectors.rs b/zebra-state/src/service/non_finalized_state/tests/vectors.rs index dcc7700d2f5..6c923105e72 100644 --- a/zebra-state/src/service/non_finalized_state/tests/vectors.rs +++ b/zebra-state/src/service/non_finalized_state/tests/vectors.rs @@ -3,23 +3,28 @@ use std::{sync::Arc, time::Duration}; use zebra_chain::{ - amount::NonNegative, + amount::{Amount, DeferredPoolBalanceChange, NonNegative}, block::{self, Block, Height}, history_tree::NonEmptyHistoryTree, + orchard, parameters::{Network, NetworkUpgrade}, serialization::ZcashDeserializeInto, + subtree::NoteCommitmentSubtree, + transaction::Transaction, + transparent, value_balance::ValueBalance, }; use zebra_test::prelude::*; use crate::{ arbitrary::Prepare, + request::ContextuallyVerifiedBlock, service::{ - finalized_state::FinalizedState, + finalized_state::{calculate_deferred_pool_balance_change, FinalizedState}, non_finalized_state::{Chain, NonFinalizedState, MIN_DURATION_BETWEEN_BACKUP_UPDATES}, }, tests::FakeChainHelper, - Config, + Config, SemanticallyVerifiedBlock, }; #[test] @@ -825,3 +830,140 @@ async fn non_finalized_state_writes_blocks_to_and_restores_blocks_from_backup_ca to the previous non-finalized state" ); } + +/// Regression test for +/// [GHSA-2gf8-q9rr-jq3h](https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-2gf8-q9rr-jq3h). +/// +/// `Chain::pop_tip` used to leave entries in `sapling_subtrees` and `orchard_subtrees`, +/// so a chain forked below a subtree boundary inherited stale subtrees from the +/// abandoned branch. Forking should drop any subtree whose `end_height` is above the +/// new tip. +#[test] +fn fork_drops_subtrees_above_fork_point() -> Result<()> { + let _init_guard = zebra_test::init(); + + let network = Network::Mainnet; + let block1: Arc = Arc::new(network.test_block(653599, 583999).unwrap()); + let block2 = block1.make_fake_child().set_work(10); + let block3 = block2.make_fake_child().set_work(1); + + let mut chain = Chain::new( + &network, + (block1.coinbase_height().unwrap() - 1).unwrap(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ValueBalance::fake_populated_pool(), + ); + chain = chain.push(block1.clone().prepare().test_with_zero_spent_utxos())?; + chain = chain.push(block2.clone().prepare().test_with_zero_spent_utxos())?; + chain = chain.push(block3.clone().prepare().test_with_zero_spent_utxos())?; + + // Inject a Sapling and an Orchard subtree whose `end_height` is the chain's tip. + // The block-push pipeline above doesn't complete subtrees on its own for these + // fixture blocks, so we install them directly via the test-only helpers. + let tip_height = block3.coinbase_height().unwrap(); + let sapling_node = sapling_crypto::Node::from_bytes([0; 32]).unwrap(); + chain.insert_sapling_subtree(NoteCommitmentSubtree::new(0u16, tip_height, sapling_node)); + let orchard_node = orchard::tree::Node::default(); + chain.insert_orchard_subtree(NoteCommitmentSubtree::new(0u16, tip_height, orchard_node)); + + assert_eq!(chain.sapling_subtrees.len(), 1); + assert_eq!(chain.orchard_subtrees.len(), 1); + + // Fork at `block1`, so `pop_tip` runs twice (for block3 then block2). The + // subtree at block3's height must be removed; without the fix, it would survive + // into the forked chain. + let forked = chain.fork(block1.hash()).expect("block1 is in the chain"); + + assert_eq!(forked.non_finalized_tip_hash(), block1.hash()); + assert!( + forked.sapling_subtrees.is_empty(), + "fork should have dropped the Sapling subtree completed above the fork point" + ); + assert!( + forked.orchard_subtrees.is_empty(), + "fork should have dropped the Orchard subtree completed above the fork point" + ); + + Ok(()) +} + +/// Check that the `deferred_pool_balance_change` passed to `with_block_and_spent_utxos` +/// flows through to the resulting block's `chain_value_pool_change`. +#[test] +fn with_block_and_spent_utxos_preserves_deferred_pool_balance_change() -> Result<()> { + let _init_guard = zebra_test::init(); + let block: Arc = + zebra_test::vectors::BLOCK_MAINNET_434873_BYTES.zcash_deserialize_into()?; + let prepared = SemanticallyVerifiedBlock::from(block); + + let zero_output = transparent::Output { + value: Amount::zero(), + lock_script: transparent::Script::new(&[]), + }; + let zero_utxo = transparent::OrderedUtxo::new(zero_output, Height(1), 1); + let spent_utxos = prepared + .block + .transactions + .iter() + .map(AsRef::as_ref) + .flat_map(Transaction::inputs) + .flat_map(transparent::Input::outpoint) + .map(|outpoint| (outpoint, zero_utxo.clone())) + .collect(); + + let expected_deferred = Amount::try_from(123_456_789)?; + let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( + prepared, + spent_utxos, + DeferredPoolBalanceChange::new(expected_deferred), + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + Default::default(), + )?; + + assert_eq!( + contextual.chain_value_pool_change.deferred_amount(), + expected_deferred, + ); + + Ok(()) +} + +/// Check that after committing a block via `commit_new_chain`, the non-finalized chain's +/// deferred pool amount matches what `calculate_deferred_pool_balance_change` returns for +/// the block's height and network. +#[test] +fn commit_new_chain_sets_chain_value_pools_deferred_amount() -> Result<()> { + let _init_guard = zebra_test::init(); + let network = Network::Mainnet; + + let block: Arc = Arc::new(network.test_block(653_599, 583_999).unwrap()); + let height = block.coinbase_height().expect("coinbase height"); + assert!( + height > network.slow_start_interval(), + "test block must be past slow_start_interval to exercise the non-trivial branch \ + of calculate_deferred_pool_balance_change", + ); + + let mut state = NonFinalizedState::new(&network); + let finalized_state = FinalizedState::new( + &Config::ephemeral(), + &network, + #[cfg(feature = "elasticsearch")] + false, + ); + finalized_state.set_finalized_value_pool(ValueBalance::::fake_populated_pool()); + + state.commit_new_chain(block.prepare(), &finalized_state.db)?; + + let chain = state.best_chain().expect("chain was just committed"); + let expected = calculate_deferred_pool_balance_change(height, &network) + .value() + .constrain::()?; + + assert_eq!(chain.chain_value_pools.deferred_amount(), expected); + + Ok(()) +} diff --git a/zebra-state/src/service/queued_blocks.rs b/zebra-state/src/service/queued_blocks.rs index 0bc5ee533de..f9e50ae1f7a 100644 --- a/zebra-state/src/service/queued_blocks.rs +++ b/zebra-state/src/service/queued_blocks.rs @@ -107,7 +107,14 @@ impl QueuedBlocks { .collect::>(); for queued in &queued_children { - self.by_height.remove(&queued.0.height); + if let Some(hashes) = self.by_height.get_mut(&queued.0.height) { + hashes.remove(&queued.0.hash); + + if hashes.is_empty() { + self.by_height.remove(&queued.0.height); + } + } + // TODO: only remove UTXOs if there are no queued blocks with that UTXO // (known_utxos is best-effort, so this is ok for now) for outpoint in queued.0.new_outputs.keys() { @@ -373,6 +380,27 @@ impl SentHashes { self.sent.contains_key(hash) } + /// Removes a `hash` from `SentHashes`, dropping its outpoints from `known_utxos` + /// and its entry from whichever batch buffer holds it. + /// + /// Called when the block write task rejects a block, so that a subsequent + /// re-delivery of a block with the same hash is not short-circuited as a + /// "duplicate" against a rejected variant that never reached any chain. + pub fn remove(&mut self, hash: &block::Hash) { + let Some(outpoints) = self.sent.remove(hash) else { + return; + }; + + for outpoint in &outpoints { + self.known_utxos.remove(outpoint); + } + + self.curr_buf.retain(|(h, _)| h != hash); + for buf in &mut self.bufs { + buf.retain(|(h, _)| h != hash); + } + } + /// Returns true if the chain can be forked at the provided hash pub fn can_fork_chain_at(&self, hash: &block::Hash) -> bool { self.can_fork_chain_at_hashes && self.contains(hash) diff --git a/zebra-state/src/service/queued_blocks/tests/vectors.rs b/zebra-state/src/service/queued_blocks/tests/vectors.rs index 203caf706e6..1fdc94edaf9 100644 --- a/zebra-state/src/service/queued_blocks/tests/vectors.rs +++ b/zebra-state/src/service/queued_blocks/tests/vectors.rs @@ -9,7 +9,7 @@ use zebra_test::prelude::*; use crate::{ arbitrary::Prepare, - service::queued_blocks::{QueuedBlocks, QueuedSemanticallyVerified}, + service::queued_blocks::{QueuedBlocks, QueuedSemanticallyVerified, SentHashes}, tests::FakeChainHelper, }; @@ -137,3 +137,111 @@ fn prune_removes_right_children() -> Result<()> { Ok(()) } + +/// `SentHashes::remove` must drop the hash, its outpoints from `known_utxos`, +/// and the corresponding `(hash, height)` entry from `curr_buf` (or whichever +/// batch buffer holds it). Without this, a rejected same-hash block would +/// keep a later honest re-delivery of a block at the same hash locked out as +/// a "duplicate" forever. +#[test] +fn sent_hashes_remove_drops_rejected_hash_and_utxos() -> Result<()> { + let _init_guard = zebra_test::init(); + + let block1: Arc = + zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?; + let block2: Arc = + zebra_test::vectors::BLOCK_MAINNET_419201_BYTES.zcash_deserialize_into()?; + + let prepared1 = block1.clone().prepare(); + let prepared2 = block2.clone().prepare(); + + let mut sent = SentHashes::default(); + sent.add(&prepared1); + sent.add(&prepared2); + + // Both hashes are present, and `known_utxos` contains every outpoint from + // both blocks' coinbase + transparent outputs. + let utxos_after_add = sent.known_utxos.len(); + assert!(sent.contains(&prepared1.hash)); + assert!(sent.contains(&prepared2.hash)); + assert!(utxos_after_add > 0); + + // Remove block1. block1's hash disappears, block2's stays, and the + // total number of known utxos shrinks by exactly block1's contribution. + let block1_utxos = prepared1.new_outputs.len(); + sent.remove(&prepared1.hash); + + assert!( + !sent.contains(&prepared1.hash), + "removed hash must not satisfy contains()" + ); + assert!(sent.contains(&prepared2.hash)); + assert_eq!( + sent.known_utxos.len(), + utxos_after_add - block1_utxos, + "remove must drop only the removed block's outpoints" + ); + + // The (hash, height) entry must be gone from the batch buffer too, + // otherwise a later `prune_by_height` could re-insert into `sent`. + assert!( + !sent.curr_buf.iter().any(|(h, _)| h == &prepared1.hash), + "remove must drop the (hash, height) entry from curr_buf" + ); + assert!(sent.curr_buf.iter().any(|(h, _)| h == &prepared2.hash)); + + // Removing a hash that isn't tracked is a no-op. + let block3 = block1.make_fake_child(); + sent.remove(&block3.hash()); + assert!(sent.contains(&prepared2.hash)); + + Ok(()) +} + +// Ensures `dequeue_children` does not remove same-height sibling blocks from other forks. +#[test] +fn dequeue_children_preserves_same_height_siblings() -> Result<()> { + let _init_guard = zebra_test::init(); + + let root_block: Arc = + zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?; + + let left_child: Arc = + zebra_test::vectors::BLOCK_MAINNET_419201_BYTES.zcash_deserialize_into()?; + let left_grandchild = left_child.make_fake_child(); + + let right_child = root_block.make_fake_child(); + let right_grandchild = right_child.make_fake_child(); + + let mut queue = QueuedBlocks::default(); + queue.queue(left_grandchild.clone().into_queued()); + queue.queue(right_grandchild.clone().into_queued()); + + let height = left_grandchild.coinbase_height().unwrap(); + + // Sanity check: both entries are indexed under the same height bucket + assert_eq!( + queue.by_height.get(&height).unwrap().len(), + 2, + "expected both fork grandchildren to be in the same height bucket" + ); + + // Dequeue only one branch + queue.dequeue_children(left_child.hash()); + + assert!( + queue.blocks.contains_key(&right_grandchild.hash()), + "sibling block must remain in queue after unrelated dequeue" + ); + + assert!( + queue + .by_height + .get(&height) + .unwrap() + .contains(&right_grandchild.hash()), + "sibling must remain indexed by height after unrelated dequeue" + ); + + Ok(()) +} diff --git a/zebra-state/src/service/read/address/utxo.rs b/zebra-state/src/service/read/address/utxo.rs index 645a126ec39..a9c4b38b553 100644 --- a/zebra-state/src/service/read/address/utxo.rs +++ b/zebra-state/src/service/read/address/utxo.rs @@ -166,11 +166,12 @@ where // Get the matching hash for the given height, if any let last_height_and_hash = last_height.and_then(|height| { - chain - .as_ref() - .and_then(|c| c.as_ref().hash_by_height(height)) - .or_else(|| db.hash(height)) - .map(|hash| (height, hash)) + Some(height).zip( + chain + .as_ref() + .and_then(|c| c.as_ref().hash_by_height(height)) + .or_else(|| db.hash(height)), + ) }); return Ok(AddressUtxos::new( diff --git a/zebra-state/src/service/read/block.rs b/zebra-state/src/service/read/block.rs index e292925e0db..2c6200dcbb1 100644 --- a/zebra-state/src/service/read/block.rs +++ b/zebra-state/src/service/read/block.rs @@ -170,16 +170,23 @@ pub fn any_transaction<'a>( // // It is ok to do this lookup in multiple different calls. Finalized state updates // can only add overlapping blocks, and hashes are unique. - let mut best_chain = None; + // + // Capture the best chain tip before searching, not inside the search closure. + // The closure only runs when the tx is found in a non-finalized chain; if the tx + // is only in the finalized DB, the closure never fires and best_chain would stay + // None, causing tip_height to undercount confirmations by ~MAX_BLOCK_REORG_HEIGHT. + // See . + // peekable() reads the first element without consuming it, so the iterator can + // still be used in find_map below. + let mut chains = chains.peekable(); + let best_chain = chains.peek().copied(); let (tx, height, time, in_best_chain, containing_chain) = chains .enumerate() .find_map(|(i, chain)| { - chain.as_ref().transaction(hash).map(|(tx, height, time)| { - if i == 0 { - best_chain = Some(chain); - } - (tx.clone(), height, time, i == 0, Some(chain)) - }) + chain + .as_ref() + .transaction(hash) + .map(|(tx, height, time)| (tx.clone(), height, time, i == 0, Some(chain))) }) .or_else(|| { db.transaction(hash) diff --git a/zebra-state/src/service/read/find/tests/vectors.rs b/zebra-state/src/service/read/find/tests/vectors.rs index 1069fe5e449..2bea3570fba 100644 --- a/zebra-state/src/service/read/find/tests/vectors.rs +++ b/zebra-state/src/service/read/find/tests/vectors.rs @@ -4,17 +4,20 @@ use zebra_chain::block::Height; use crate::{constants, service::read::find::block_locator_heights}; -/// Block heights, and the expected minimum block locator height +/// Block heights, and the expected minimum block locator height. +/// +/// Cases are computed against [`constants::MAX_BLOCK_REORG_HEIGHT`]. static BLOCK_LOCATOR_CASES: &[(u32, u32)] = &[ (0, 0), (1, 0), (10, 0), - (98, 0), (99, 0), - (100, 1), - (101, 2), - (1000, 901), - (10000, 9901), + (100, 0), + (999, 0), + (1000, 0), + (1001, 1), + (2000, 1000), + (10000, 9000), ]; /// Check that the block locator heights are sensible. @@ -54,7 +57,7 @@ fn test_block_locator_heights() { ); assert!( height - final_height.0 <= constants::MAX_BLOCK_REORG_HEIGHT, - "locator for {} must not be more than the maximum reorg height {} below the tip, \ + "locator for {} must not be more than MAX_BLOCK_REORG_HEIGHT ({}) below the tip, \ but {} is {} blocks below the tip", height, constants::MAX_BLOCK_REORG_HEIGHT, diff --git a/zebra-state/src/service/tests.rs b/zebra-state/src/service/tests.rs index 083b3c3ff77..0323260eb33 100644 --- a/zebra-state/src/service/tests.rs +++ b/zebra-state/src/service/tests.rs @@ -10,6 +10,7 @@ use tokio::runtime::Runtime; use tower::{buffer::Buffer, util::BoxService}; use zebra_chain::{ + amount::DeferredPoolBalanceChange, block::{self, Block, CountedHeader, Height}, chain_tip::ChainTip, fmt::SummaryDebug, @@ -424,7 +425,7 @@ proptest! { // which is not included in the UTXO set if block.height > block::Height(0) { let utxos = &block.new_outputs.iter().map(|(k, ordered_utxo)| (*k, ordered_utxo.utxo.clone())).collect(); - let block_value_pool = &block.block.chain_value_pool_change(utxos, None)?; + let block_value_pool = &block.block.chain_value_pool_change(utxos, DeferredPoolBalanceChange::zero())?; expected_finalized_value_pool += *block_value_pool; } @@ -451,7 +452,7 @@ proptest! { let mut expected_non_finalized_value_pool = Ok(expected_finalized_value_pool?); for block in non_finalized_blocks { let utxos = block.new_outputs.clone(); - let block_value_pool = &block.block.chain_value_pool_change(&transparent::utxos_from_ordered_utxos(utxos), None)?; + let block_value_pool = &block.block.chain_value_pool_change(&transparent::utxos_from_ordered_utxos(utxos), DeferredPoolBalanceChange::zero())?; expected_non_finalized_value_pool += *block_value_pool; let result_receiver = state_service.queue_and_commit_to_non_finalized_state(block.clone()); @@ -507,9 +508,14 @@ proptest! { for block in finalized_blocks { let expected_block = block.clone(); - let expected_action = if expected_block.height <= block::Height(1) { - // 0: reset by both initialization and the Genesis network upgrade - // 1: reset by the BeforeOverwinter network upgrade + let expected_action = if expected_block.height == block::Height(0) { + // 0: reset on initialization. + // + // The mempool verifies transactions against the next block height, so the + // reset for an upgrade activating at height `H` happens when the tip reaches + // `H - 1`. The only activation height whose predecessor is in the test vectors + // is BeforeOverwinter (height 1), so its reset coincides with the genesis block + // at height 0, which already resets on initialization. TipAction::reset_with(expected_block.clone().into()) } else { TipAction::grow_with(expected_block.clone().into()) @@ -531,12 +537,13 @@ proptest! { for block in non_finalized_blocks { let expected_block = block.clone(); - let expected_action = if expected_block.height == block::Height(1) { - // 1: reset by the BeforeOverwinter network upgrade - TipAction::reset_with(expected_block.clone().into()) - } else { - TipAction::grow_with(expected_block.clone().into()) - }; + // Non-finalized blocks are continuous with the finalized tip and always at height + // >= 1 (genesis is always finalized). The reset for an upgrade activating at height + // `H` happens when the tip reaches `H - 1`, and the only activation height with a + // predecessor in the test vectors is BeforeOverwinter (height 1, predecessor height + // 0, which is always finalized). So no non-finalized block triggers an activation + // reset: they all grow. + let expected_action = TipAction::grow_with(expected_block.clone().into()); let result_receiver = state_service.queue_and_commit_to_non_finalized_state(block); let result = result_receiver.blocking_recv(); diff --git a/zebra-state/src/service/write.rs b/zebra-state/src/service/write.rs index 39c33bbfc0e..530b1886b1f 100644 --- a/zebra-state/src/service/write.rs +++ b/zebra-state/src/service/write.rs @@ -12,10 +12,7 @@ use tokio::sync::{ }; use tracing::Span; -use zebra_chain::{ - block::{self, Height}, - transparent::EXTRA_ZEBRA_COINBASE_DATA, -}; +use zebra_chain::block::{self, Height}; use crate::{ constants::MAX_BLOCK_REORG_HEIGHT, @@ -89,7 +86,6 @@ pub(crate) fn validate_and_commit_non_finalized( non_finalized_state, chain_tip_sender, non_finalized_state_sender, - last_zebra_mined_log_height, backup_dir_path, ), fields(chains = non_finalized_state.chain_count()) @@ -98,7 +94,6 @@ fn update_latest_chain_channels( non_finalized_state: &NonFinalizedState, chain_tip_sender: &mut ChainTipSender, non_finalized_state_sender: &watch::Sender, - last_zebra_mined_log_height: &mut Option, backup_dir_path: Option<&Path>, ) -> block::Height { let best_chain = non_finalized_state.best_chain().expect("unexpected empty non-finalized state: must commit at least one block before updating channels"); @@ -109,8 +104,6 @@ fn update_latest_chain_channels( .clone(); let tip_block = ChainTipBlock::from(tip_block); - log_if_mined_by_zebra(&tip_block, last_zebra_mined_log_height); - let tip_block_height = tip_block.height; if let Some(backup_dir_path) = backup_dir_path { @@ -133,6 +126,14 @@ struct WriteBlockWorkerTask { finalized_state: FinalizedState, non_finalized_state: NonFinalizedState, invalid_block_reset_sender: UnboundedSender, + /// Signals the [`crate::service::StateService`] that a non-finalized block was rejected by + /// the write task, so its hash should be removed from + /// `non_finalized_block_write_sent_hashes`. + /// + /// Without this, a rejected same-hash block locks out a later honest + /// re-delivery of a block at the same hash as a "duplicate" until restart + /// or reorg. + non_finalized_rejected_sender: UnboundedSender, chain_tip_sender: ChainTipSender, non_finalized_state_sender: watch::Sender, /// If `Some`, the non-finalized state is written to this backup directory @@ -201,6 +202,7 @@ impl BlockWriteSender { ) -> ( Self, tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver, Option>>, ) { // Security: The number of blocks in these channels is limited by @@ -211,6 +213,8 @@ impl BlockWriteSender { tokio::sync::mpsc::unbounded_channel(); let (invalid_block_reset_sender, invalid_block_write_reset_receiver) = tokio::sync::mpsc::unbounded_channel(); + let (non_finalized_rejected_sender, non_finalized_rejected_receiver) = + tokio::sync::mpsc::unbounded_channel(); let span = Span::current(); let task = std::thread::spawn(move || { @@ -221,6 +225,7 @@ impl BlockWriteSender { finalized_state, non_finalized_state, invalid_block_reset_sender, + non_finalized_rejected_sender, chain_tip_sender, non_finalized_state_sender, backup_dir_path, @@ -232,10 +237,11 @@ impl BlockWriteSender { ( Self { non_finalized: Some(non_finalized_block_write_sender), - finalized: Some(finalized_block_write_sender) - .filter(|_| should_use_finalized_block_write_sender), + finalized: should_use_finalized_block_write_sender + .then_some(finalized_block_write_sender), }, invalid_block_write_reset_receiver, + non_finalized_rejected_receiver, Some(Arc::new(task)), ) } @@ -259,12 +265,12 @@ impl WriteBlockWorkerTask { finalized_state, non_finalized_state, invalid_block_reset_sender, + non_finalized_rejected_sender, chain_tip_sender, non_finalized_state_sender, backup_dir_path, } = &mut self; - let mut last_zebra_mined_log_height = None; let mut prev_finalized_note_commitment_trees = None; // Write all the finalized blocks sent by the state, @@ -310,9 +316,6 @@ impl WriteBlockWorkerTask { Ok((finalized, note_commitment_trees)) => { let tip_block = ChainTipBlock::from(finalized); prev_finalized_note_commitment_trees = Some(note_commitment_trees); - - log_if_mined_by_zebra(&tip_block, &mut last_zebra_mined_log_height); - chain_tip_sender.set_finalized_tip(tip_block); } Err(error) => { @@ -372,7 +375,6 @@ impl WriteBlockWorkerTask { non_finalized_state, chain_tip_sender, non_finalized_state_sender, - &mut last_zebra_mined_log_height, backup_dir_path.as_deref(), ); continue; @@ -412,6 +414,17 @@ impl WriteBlockWorkerTask { parent_error_map.shift_remove_index(0); } + // Signal the StateService to drop this hash from + // `non_finalized_block_write_sent_hashes`, so a subsequent + // re-delivery of a block at the same hash is not short-circuited + // as a "duplicate" against a rejected variant that never reached + // any chain. + // + // If the receiver was dropped (the StateService is shutting + // down), ignore the error: the lockout cannot matter once the + // service exits. + let _ = non_finalized_rejected_sender.send(child_hash); + // Update the caller with the error. let _ = rsp_tx.send(result.map(|()| child_hash).map_err(Into::into)); @@ -429,7 +442,6 @@ impl WriteBlockWorkerTask { non_finalized_state, chain_tip_sender, non_finalized_state_sender, - &mut last_zebra_mined_log_height, backup_dir_path.as_deref(), ); @@ -473,72 +485,3 @@ impl WriteBlockWorkerTask { std::mem::drop(self.finalized_state); } } - -/// Log a message if this block was mined by Zebra. -/// -/// Does not detect early Zebra blocks, and blocks with custom coinbase transactions. -/// Rate-limited to every 1000 blocks using `last_zebra_mined_log_height`. -fn log_if_mined_by_zebra( - tip_block: &ChainTipBlock, - last_zebra_mined_log_height: &mut Option, -) { - // This logs at most every 2-3 checkpoints, which seems fine. - const LOG_RATE_LIMIT: u32 = 1000; - - let height = tip_block.height.0; - - if let Some(last_height) = last_zebra_mined_log_height { - if height < last_height.0 + LOG_RATE_LIMIT { - // If we logged in the last 1000 blocks, don't log anything now. - return; - } - }; - - // This code is rate-limited, so we can do expensive transformations here. - let coinbase_data = tip_block.transactions[0].inputs()[0] - .extra_coinbase_data() - .expect("valid blocks must start with a coinbase input") - .clone(); - - if coinbase_data - .as_ref() - .starts_with(EXTRA_ZEBRA_COINBASE_DATA.as_bytes()) - { - let text = String::from_utf8_lossy(coinbase_data.as_ref()); - - *last_zebra_mined_log_height = Some(Height(height)); - - // No need for hex-encoded data if it's exactly what we expected. - if coinbase_data.as_ref() == EXTRA_ZEBRA_COINBASE_DATA.as_bytes() { - info!( - %text, - %height, - hash = %tip_block.hash, - "looks like this block was mined by Zebra!" - ); - } else { - // # Security - // - // Use the extra data as an allow-list, replacing unknown characters. - // This makes sure control characters and harmful messages don't get logged - // to the terminal. - let text = text.replace( - |c: char| { - !EXTRA_ZEBRA_COINBASE_DATA - .to_ascii_lowercase() - .contains(c.to_ascii_lowercase()) - }, - "?", - ); - let data = hex::encode(coinbase_data.as_ref()); - - info!( - %text, - %data, - %height, - hash = %tip_block.hash, - "looks like this block was mined by Zebra!" - ); - } - } -} diff --git a/zebra-test/Cargo.toml b/zebra-test/Cargo.toml index 28dc924bb55..a678ef35cde 100644 --- a/zebra-test/Cargo.toml +++ b/zebra-test/Cargo.toml @@ -23,7 +23,6 @@ insta = { workspace = true } itertools = { workspace = true } proptest = { workspace = true } once_cell = { workspace = true } -rand = { workspace = true } regex = { workspace = true } tokio = { workspace = true, features = ["full", "tracing", "test-util"] } @@ -41,6 +40,9 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-error = { workspace = true } tracing = { workspace = true } +[target.'cfg(windows)'.dependencies] +rand = { workspace = true } + [dev-dependencies] tempfile = { workspace = true } diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt index 0eb25f29a28..2fbccf1be84 100644 --- a/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-1.txt @@ -1 +1 @@ -0400000027e30134d620e9fe61f719938320bab63e7e72c91b5e23025676f90ed8119f02fe15832e9814d9ad9bfe40a29eccf046b33d4fb6eaa09c6c824de6255da02334fd4784adf28a776c789c9ae78be5a6f259a461a04b8313612c08ddb6abd6709ef2fa494d3fa60c200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000010000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff06517af09fa693000000000140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a197700000000290000000000000000000000000000000102b069dd03055094332cec6e51d80e4521217ff7d38cb93846b1923f97d39976293434e754ec026a3aa3df455bbff33ac5b272e5f86970c39dc29b61c869d47d1ba4527b73e6aaaf6e505403736fa68750d5e81749b6f9342d2650dae430c4b9b3060aa2e840061bdf8bfb96cd99d39fb1b1f88036e8bf9de867c1eea1b4c6d40a9cd9ba7039f038312ec732cb7f9dcb6a88dc94f12b27c272783a90effed30fba51c34ae568ef766ba0b1f9a8ec8e5b2c0fe167c0d8eeff3dcb07d3e9262d272bb2775126fca03df875e721c708bd3576d99ff8f14851e1c77fb3eedeb8a4b2a2ccbd3f4c7022f2eaaf49aed4401329c111aafcf6f9942f4803df07a147d5458512367e470069eaaa921a49cc0e755faecba1b1095be46e675dd9834abf1c1a239017086f918940b2665927580d85c10d3ab71de2c01e50d698e1e7d1635d121366b64f1e75d88f6b3f59314a8e62f83d4c53daa410bf62656adc627fefe2f814dab99f623956535105f4b628bbc4f5e9f34c1206d281bc224ffbbf3b5f8e63941d09d322bef8b39a434e339c09882e45d1d9fd240bd5d4b1f7e37cef1a133261da3a1e61c2702ac1c3d3e9107a3f35f48b3567dc45f28b81f26272fff3f0e58bddfad47ebb4e97e22fbc9839b57c90ba79f875bc24234f257ad28ccf83e81cde362f75cd6e731b86d03fb0ff7a5c322475f8f51a6c61752f337400339733defdffbba2c341dc6020cbb70a725ed978f9636ce72290244ef3b58573481a14b86d315a1f48d2ac22406228525405a6d8abcb1141d6d03f004237af9afae80feb4687a8763b67ff3dd91abdbc0c5c13d2dc1da7d9cfe700570894635df4bc5348fb300eb089e8afd031a40dcd522804371d5573aca3e2c293a3aa6b4d5aeebe2c53ea64b64d1d533179268d83694668237953ba8c2c5124bb79ec6ea54c7671a250620a1b062a1b7c7641f4bf2daf5f5baa51e50a316a9e6bd2307cbe6d27505092c9f91fd05d7833521d6f20cda3646d1413e9c8c2a648f0324190b1abb68fe2be447dc6fe23c4bd032413dff27d0181067f791c0bd571ba97a92ebfe73d53aef74b0e615bad03c4f8d200a431f2832cc4df5a35a1c4249c05b7aa3e7296d8d3d02c0a4ca2e04c7ac98da0323f322b4a01f325d5b4f3f086af9b570c70fb36fb26d2242ed452443fcafe467887b7f9907a27214a3a7c268c47fff2de7408049251774ba5e3f0edf9b8a9f56b0355427871c98c9e8d7a60663428e712daf54a21162c047d9eaa22a6db05faabedcba9dc1d4fea8923e6922aaea9e49f6e358d5955745092adb4fc1ac2640312a88be2b54bae48ef89271d6d7ac3d5eee2d8201039624bc46ec77fce330e8b701f94a45bc553458d32ad0296b8bc2f17df1ef5fb64508a907e64b8f31604022cc5bab43210f37ba087d01a03c2b5b77bfdfc26d889bcb49183be4acfea7340683eedab741e767930ac9e1b509c259e174ae0d97f396de4d20e3aa3d7c4f238cd146c072819518afc6518256b4a8e803553c0866d133da3d1b64c5d7ed94e3dfe1e42f9fb584491d2ae7e95aae4328e8b9d6dafcc6213da28185c51b4e28f56e4379713eceb5721c9ef991aef744268949ca53074760ceb7a8f2f1c40a53b86ec7f142b122b61bce3bd514fc846e212e7af7fbfc12f08942bcc92a9ab866f40c02de720e82300f94eda033e4f1967c0f3b74b90a6667ff4582b623af1d7c82c751e5ff821e52d065167f7486823568de554af102f3a91e3883cee7f35fa76db3c0ee7df99f3850bc50c784fe091adaf866cc21856631053e58a514e4d6ec54559385f57be36c1e59d1e6e3a5ba820d8393b34c7820173e5b5c265fcf7b98c9a229345cb4f306553fa1c981f860d58efd6cc00332b278f8a7850d5e1af26d82cdb6edd7eca96dcd5217b92df4495dd718495f8b29bcc6f99bb828556bcd8a6e33341287a4a2206dfd3d440b2daebde5a9454e9501a17ad9412007aedd0d3a63dee8818f0a78967c81b87a0ce3c46e4aadfe7005c2fbab2723216569b4a2c4366a9ad938b8572f35d829f3efa61806d45617f9e886a1b488b1a2be58e8e0a1489b90ea0ca8dd83f681d3b8147142f4a91b2210fa168b27e2124d02fd8a423c19e05283945364d58d4d20eb4a6110ca67a1bcb8b1bf374f86b1ddcdb7427da5b66df97a4b20a2848a5adcb7996a64589a1a214d0526f433461cfc316ddcf9bcd2efe34e0edd863b24742db062e50adf0c3e078667704192d25fd1818ea9c9a80c019875e221ca33cadef6ff446fd3068e9b23cb26ffe31b5f4fd24ece3b74c8abc150fff8251916d26e5ec85be3de600203ba681f3992105a2ea14020f5afd10edd754d385a30df387a6ccd4d710cb24a080b65ea6300afd8d23c5170a8bdf07ae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82f0000000000fde01cc457d852440de8ab8d2d2fd8c86600c5b00285f19bbe65e840af6d926626fe36f38609c1c77c723d8a2666a7d5f0facd072a2cbb3933ec21f4f4a4cbb2df42367345f48639b5c657927a8914c074bf7da0cf6e74e4d65d8a47260e449cefe6286645ce49a83f3cdf55e4c9ea2d6985296b359bc3d832c44e1587ed3d0d6db51418707f13cb2dd58c4c8a2141756d0cf75a1b3e352e7e37c9ad6b45bb11efbaabb14910e12e747a7b679f08060dc833d31685abe6a5279ab3430e3cfbb0676f2c445cd30e006d8c037f19e463a383f3e942f95b17f2c12adc73f063bf3e83d005759ad73c916521aa515f8842384633d2cd6fb55c6663bd3b82c05f4d69b295b93c558b096a30d9dbdbe3342549198c8b25fca2a08c843bbe35af368f7124f12b9b79dba785032f9ab0480aa984b023b95393f7a0a99841bbb0c8728e72441c96d77921eb1209e43ecd54ef80a1345c0475232bdc5ae5972eacba1ff838999d11a7f42169963886bcbaaca940505a5abff35725db9565370aa87a9858e98d9f1393004f08af61c953ea6c0f78248368a94773d818c8aef4a0d0d63b0788732c1fd59e73272fda612c677d4289c1baf02a1491724d7ede13dfabc7c243ba2f582dd35e63ee33ac6cdf94f8c4c35e985421737d02d1b1f871d9d9fc090e5c82ee94531ca6caac5ceb752f42d3c5373a7c4cc329cac38e14b1204288a5f974c66d368564af500f13fa98511d5421b71f9376eeb925a695ce067021d720e1b12fffbdcf06d37f2f9316ca92942681a08574023e6e1dbbd7539f058bb6f81183aa05ad1b5152722221c45e01e553f7bfe001de4b59eb6675677c285984088c5e487d01561d5850a350860a51d016dc240991b302a6fb77037f97ae118fa615ac1e5b173acf60bc24932433b80777eb3758359deecaa3a6f24523a9ff85fcddd467a88aa397d2572f5dd0168cc1c2c4911c830c73ee587dc33862e524cbdd4fae843333d647f72aa4212f8217e50ec01e274d30f4606ef6e70ee65bdf9800d295f2579b3a7e083392ce5d8d5464e79172a73c8047e7cb6af064640bf50f85a37620ae23052a614a6a8ec8e7b0b560c4fb52ac70160e69c259a5d37e03d034e26ff02ab12e57e20bc80c1c8e207da3fd47e3272d1b914dc029834a99b7d42855b8540624cf22480ba14a018451e4e782513c5f4bb8bd8f780258fab38042cc1f4952ac104bbd4bd14eb09e254085876e68f6f55fb063bbd5503ae7024fbba497cae8cfa42ee670fe3ce3c2b44880118ca1acd8745c99e072aca97e1bd6d5c4ee3145ffb8916d7ef098c8eec09c9087e8515e455572ee05815f8587b27a8af4b9cc9f792067dabb425c4a332e6fb20c426917b9aec4233da842027f02ccd5c16e17e058abf4f85780fdf00459d8002058c0dccd72dec6df4eef5444ffd778fa23432d28128e16380c268b1cf9ac68adf4a7eee870b2b3768bdb2b9c1da39db73b064f2b92767538b9796d9d29f5b91121a67cef5cdcf938e55232866853291dcfa26557b4a89819035094ceab434032fbf197dcea849b88a0bd5f1ff633a0cd02c8320f25feb7b77198fc2886de4be55077d6240a27a4e27cc6f33150a92e5fd5ee25d391d491f870d053a0d04b3b57d4608db2bf8284e560b608641c38893b4866572ebab8c29a1a000f3986a0d66eb081225078864a5bf28ea85b5df4153e0897c4f38ac26bfdfa246e8f09383ac2de31d4d418cc0c1bc283a316e33d9cd2a2ebac1c15f1fb7b8f6744f3e9735d2c24d1037f2bca73da0632d53072ed9c1379b357adaefdf13dc4769fc91d5e47447ad913c14b09568b75b315f9b10ab2149262ba4f3014b50905e7d1229a686777b7fa7fb03530c4c0d7f744cb8dd2fa6f45108db49490c84cef4151fa8021203052c6edca8f3bec64de0b6a4dce22ff29a8e7a12785a0a5afa792dc9722807e79289e7eda4c6bedccd4aa790f52a3007c0b4a51333ca72fe94add379b8e89132a92df4a0219a1700bdf1ae4ba0d283849b9a77df7230afd8e69ef29c0fbc00863afb78ef6cb6bc62da37f3b7681ba6cb870500fa39300f147c5ff3832491703240fceb3014de481e331fe90d132e55133b5a50d63a5bad3a530cacf4a81bd1ec1e597647e601131f2e59c211db3c6158e255066ff98a0ca77f5fabc5ac435ec7e7490ed9e8f29c62bcfa1b19417e4d0ee58edd66300dda8313f3602a9d3370060875ad28179a52bef6992eee9423cdd9b7d10874a3c1ef2c0001d3e5f1057fa939f184cbf91d5368e05a5b5497d699aaab29dec4c9531000ca2b206ad5ec133e34d2fdcd2bf38421bb69be67c560facaf068a951f924bed32717684f19bb8308e77f29efa83eebbfb0af861c3da539571c13733449b56ffe337b8afe0bdcd93aa7d0e3e275b0bacae2c485f285e0d4e07dada915f3bd3b574dbced787680186b4a7af369c1e6553bc5c86392e5eeaaef1e83bdd42117919287a58c743e536651cb73b6e854da90fbe76d4a7927335fca6d2f725112e34b6a228652baa9dcb89e7f567d28264218ce61b831e7c26faeadd1a806b0e0d78760a4b18ccaf96ce4271b7f1cf3abc672d56bd525a771a08c07ab7e039d509401910a456eceb4735e34fbc7f413e32400b8111aad35e103b34498055ab8d3331cf678e2ce281e5a857b092d58fb6bd9f2d2efce8775d2ba3948bd3db6fb807cc7cef383ff4a4a3f8455f5ef51518679b05d3215b455734324cc4366fa502277bbddb8ebba5d0fddf24fbe0c8f5a39491083b0e7a268b67c4456de266f7f018730832dd487c492a43e138c62d8abf49448a0acc556a73160803aee412f2101a1df08f6e5d07fadc93d40f86c03ae81b5b0ea5bcb1dc098f71158692f0764a2b6b6353da889e06c5945d39673f96ddbf671077880bafed39b7460576883eac02aa3b00d9e0c49ce528a80783641089eeb106ac80f1c0637c4d92240014f9e801a26bd46efc6987895d565acc5591c239cd19be06a092549070e7e81d2b50a929534898c1970b26283737183e21a03546ce58fe89494c268221662663bd553a365ebe5dacc92731da6be70fbfc30c84311d7ba99e30e13a064600cf610f9dcb0afdc6d1b94d21efea1551da593ba52f4a586e5606d4da4a5f14872539bf0a1c084aac387698c84baff9e96b3611eb3af12ed3057fd912987c288e7d995a0d3a3f94f0ce228af116d132154942d47f85ad8e4ba4660efae639bbf1d34c825a453bea60a8c8d0f045d2314738a4bb9089ae9ad5e64e10f6de869ec328af2062e92dc4d9e9ccdd9d8e1893fa6d9c0b815d4b6fd7099d7d0ba0fc2cf002fdf03276352ced479ee9ad756305a75349749c5096b47d7e01cf398e0df9731a9a317f222d5b8de4c13f04b53030f16d57cc4e0861ba07c9caf488693baea8affbb214662aaeeb247d001aeca18ebf45bf00bab388ce396420274e4c17b383e9624fa4b73085fd47c31b9ad87723695f18f01283bd6fd5e5875f721a27ee505f77d909f52005ce747e721a8671a03ac3d3a20e6b8b55685b4b32ab9a1166453a3784ebf803827c179e87f92142047e90c008f4726a60a094272e6559106e6fa6ccabbd9a1848f583b6378b03a8768434ca540c4f38a44f88f8417dc4ff0fbbe662c7233819ef656f64faf10a8925fb29cd160535f663e96cab1db3fcd5032ab3235c5fed2c5665561f52a1d0560022826b13ab72ab23c7f23d74fe035e7498ad7d7953a3269006ed2698792b5450f3957cb9ccd836fc249458f42069e560566b2270ff3111499f5a83e0bc905b1ef981e17472713cab0893b146acee3fa8c8fd6c9e6a682bb12ca530593b1b739656cde9716c80f6357a6ce6612cbbd84ee62bd971e52e09ced9423eb8eba279cc6d691ee725e9caab5465c0d27d4332eb140f7bd12f38397f60d4a23275d3f36e1429d5df0bfc33705e7700e646abc05b3d25242242b8263cea5c995a5723e953368be506ddcd1f7e8724d620f4a38b8ffec38e535d9635787efaace76fbdb98940f6772d1a436a5b8ad4874ed01ed094e32878eedda212325affd64ebe9fd976d0cf588d8651e0a56b14ad57fe6a0874b0409eb73ea720ced14748d12539adb453a1f22d1e1ee27bd8a66aac30e1784a1f3e2ec5f9091d6b08cb8a9a1223d23dcefbdccec74ca552ebd519f85f3e360cb82ac3e149bd1eb866ba23e16abfeacb6e02fa1aab22e1094be74118efa0a9b7f55f1e924116012d812c4253337d6387ac438b86831d314f2acea223319b8fb12b6bec4b0e1422971f5ef44fcd346210dbf9b8fd8bf47bbc95bb1f3e6641301ea10beb5d827a1ac6b675c1d622622ebbd81da5f4d23e27582bb1089a2c46d363da3f2be240a82e4ed495712fac6e3b4ee25950ab15efbd4768d12690cc6301ff98ee921aef642b5dc3164370b758c19c769dd84167937e7beeefab82702f8594bb231b1353b106a8181bb4024d73c71116afd39d8015a3ed950e917d6b13553ca9956fc92f831a506ddee088f03ad78aaa299189a077d71c23580d2f59e3009a88821e6b17db1467c809198ca6022553d06d6e8e71175ddb50a3fc459a26825bf0b2e40e8f74398c861a6220273dc32689c63b92b15230dfa63be38d6018997ebb457304c4c30330a642d1a5e524eb3db155240f5abb987e88a78ff05880594564db25dddf44179de220a15ca938f842cf171ccc69bb53aab6ca28ed1e7236460d063cfca10a3b4fe3efcd02cd972bde7f7117dee4540a33b3dbefa246073bdc4b7298fd4e35120a5a1a3e8101c6a1c48c11f3f61ed4e86ffc5bf916e5a77864817825aa1a8c03e883876dca798b8e067879a9cd494772f2c5a7db6472277ef0d32cfbb6fb711c48cd9241e7f136bfd2248ea4702badca77e5baf98309e3a31ef1df8c0d0c2637172a824cb3fb43dd58daef57dba18cf1fad213f246b4d2559b173a58c7a48d2053742de544278a42c85ce46c6496fb3369004bf26c8cbf6772793134c721ad1ea920d00683b44bbfdf5b596540642deb9448a06ee3809adb4436a509597ae92eb16fc45cc060909f5f4a292fc130157251721329d7508cc5b418627ecf986a15e9a426a1ef29abdc618489c14a4ac2eb4864510742a377232cfedbcf8f8254205ea78cda33c6ebdacfbf6afe672d91a1a04a33024499a33600b1bc0818a643300b09ff415dd6d8b8e873b532dbf3cc8facd53d763b9b34ef62b4bd5efe278b29810f9728c3b13f9e89f21b33a3cfff9b3e3c28091c019ab16aecc18830ceed03d66a57f97e759cf7c6c47c6f749598c1b6ac6f3c774d230b968441cc95eafb2102a33a641b3f3fca17012bcbd33887753708b2196a56eed3f3dee94fc800221fb39cd56fc33c88a4d768dbed33b7d212aa2b56db3a3b6c5685956d206ea73f0a1a9606e75d29aea30247432eac45bf051c80d0ba0e1bc8154e474c4751b2f2394d60ddfee4edd6a3b67a86e7f27ef8da72e776840d71512d1beb3f4bdeff49344617a99a298d17f59da5465baf3dd80e9ef804260e3af838c7680c91850a571403c2fe0ab4fa77dc62a792553ad8c318ce70a421779294a595ae19497d8b7803455b63987064b55f1ebcb8cac4a58fa89d3f9c63cd80713db164d27974088203a1b441a42a7320cde0110791fea53554a92dfe291d0877185fc4b0cdb245a9084f0b9620502b2314a14f7f9e8868378f38a4df62686c7c9a5c1fc6ac8b8f75056d881d688d42259ad1abd240a0979ae6b7dddde0450113d1a70a3b126228661490abe41c8fb0bf94a9aefe9d5440a63efaffa17a2b9ea0618fd7999029fec03e94374c435c584bc3508e11f5ffd38722277773e60378853cea55200e09e742395032e9e6e00180029eec0bd07b727ca8224160513aeac266347667648159ca3aa27889c5a740aad92a87791d784ed3efb51c8e6aa69868b8285cdc3720b35d2629fc10edaf21b09c3e8a9a5903f0fcbe6421deb9b0d9608c8ad68fa5fcf2b30192e53f0e4e4aea59a2abe7661c7a37e2824735f257e6d7b2face8dc43903a10fb7c3372099f34f31956126cf3ffd09d5ac0117eb837c057772992e39fe02312e6ca5a250a40eaa7d74016510048d58935865374b3d32c83c47c2602100b9ad3626eb0c3482783c1371cbabd183e626987d77d98d367d478e638d5c99999f5c12971dc86c2fc343fd7f770bd23bec22e6e69d2c02bfd822eca7a417d4a54eaf139d031a88531467fd20d80f715644264954636cd5a75ce789fecb9224d9af8a34f0ea24769b040cd7ed64a444fb3b0148470b11177ecf88681a97fce0831a3e0b2142b1fb274541c08da071c9194ef126099f90123101f9ef3f2bc40b38821d39ee1f30e1d145dffd37d4a0279819529762a902bf82ec45d9acf57c3773f5f22cf70afbb77b52109a56e41e843b1765213b99b321dd8de65666a8ba2d75797a189354bde0ac3a2a8ebe599181ae90942132ce0a48fbcbb039c1317682e8e0ec236e3c8a11e830d86bcae16359fbaccee6a4a39f5e9279460be850157a69739c1b82f6b7d427fc80bd00939042ec6e33db7e1ac00fca7f2514b66f7bfdc9623115791574d4a4a9f50e548d9bf5690809201cff860a61fea0e49fd1de99868917065e01c3f1c165a22c59f305ec9f3ad35bb123b2b5b721bf9f4020678eea9e4436a1053f40573f5bd685301460fa02e745beb2d0e691e0da8d1d6b9eebc8b2322b975665f2abe6071595888735d4b26fb6158675c10e093bc81c17a656f15889187ee6e5ccf8631c52f1b42444496e002f0b4f9445fbba05e4f422b3261af18f0882087dc9d7bbdd4ca9f4112da647503acacaa55c26768a91cdedb98f3a69b035ee64c80485df03641b3a47b2a3c5b842916834e995399d81ca5552dafd2de022d920bdaff4b7fcd9fbfe54a49e32a7ba9fbe14baa1b3f8bd501dceaea69c2d3e59dcca703c83176830872de521f73f5f3812e1d49602af3df3254e279771642a11c8e8bbf6f71cd9cce6607d553ba3c7a1414cea08dfe9ce14d5d11f63d0e41a7fabe033cdd2ff9def3cc0448b06467282d061755e5da6a16052b90974dc6004083a1c44c46222bd9014867a9985cbeb632aaf38098623632dfc12a3d9e1e10dab6e9d3197fb183ca91753c6abeff1ad2f9bdd15d797af37589223dff23acb0651bda89fc9bd93d96c164b6c8d1ba863949e8f73b89d929bdfd29f221a72172807fa8c7135da44f29d6a226d622c54330d88bd0000b6ce470ba574bb8d703f2be0652f43b36ce5f5a71504c9006dc4a1cf1bf2c2042b913ef1fa894a917769274646171f4ce6db9d1a1f132fdaed74cfb6d1518f17660b87047e2f5b979321127a1517b7e4467ac22fab306ba42701c76a1526565cb75a6f17a77e0263192f39473e456624f01ab79ea88e73bc234c37f58a25de6c1cb65ca9bf6101faa5cf3c68e6e0cac9e2ef8380ebb48ff8e365cdd6914059f4e548f828d495e8d36c1a3c1a984cee12e7beb2eaa27d7c615795d60a191fdcec2ef9b22c1c29c415ed1b236b8a55a91b6d490451031b0fb7d968d9d7e8ca2db8c5f7c262371016be9fcc1d1b85d230a830d67babd0d07e598a243e590c4962f79595092333f96b97296f2eac29edc168e285074cb93984f197d3743a573d88c155524c1b6f726ca9254a275769cd7c6825acaee0cc0f92a39b7da38f313addff4de2a15511567cce2fbf3b2c9573c6822a47e54f6a38b2e2c474978bbc063b13481c7f87bcdad9644a500478f21bdd0734153e05533d92f540d80e8c4259a200133dc23f9329053d21bf2b5c4d13cb45053599ccf5f50f80d2f51c01aa7c5a38d5ff2e169fe9b0fcd1cc1dfe27116cb7986e0a9641158d346dea4019ce0ce9a702daeb4e34e2b0f7b5e10064563868cb06554cb90b53599bb0fbc79aa785a68d4d6291948a0e249ab756147b38786dcd7db52108dda6f319daeba500ccdafa4a195e29ac674a46a8576a0d584bc8d8e6e3d43455559bfa93c88af1ecbe23da5ff16d0495cfc18e8f714d084306ed3cabbb648f89e72157eac1dda2094705d86a3e7a8750f64f68bfd9073a8e3a1a270819bf5b9a013403a2d401bfc9ee28ecd67b2608dc589082ced59227baac5157a6d5371e61571a127d831ba4e05d91bc33f0c22795dfb48c9de85015f1b0d8a5a416afd36aef6a40fcfb90d265669182888fdb7665bd8c3ba53d7c2954ebd7653220569f6041fc6ca62ca34fec8da9eeb65237cfad38332991804607762de31c7ee4587d9838d24a3b78f78eabd13a5d00723b570cdd5f882bd22303a42bdc904b0999e722b5e0e09e7d79b44616fc98a33b3b90ab37469143eeca2e84213fd95e948bcc4b37175ce824540afa5cfff7a3831ffa61e916d53603d50d47026c2b00a3d146b8b932bf993ad3440520ea4e2070ce7baf355b9af4db2028e132b006303ccd91ca6e44a8ad21560a860658be560f155487a4c49a1de11e10871c6f53c5b4157752e79c09c129763fae5ff0ede7cd17073b4a88f5622b34125911c5790558497df05172be12351a59d73493a47309bae23be20c768a3d8f033f470c5ffa6b3435078231af0c7367174947b461205f068d0db59ab6ef5b3d0d29b76a138228bc760de4f2cc72521723f04921e624ff15d968b8fa3b6f7e040327c447b8546aee725198a69dd718b52c9f14dc78b2fae7fb36a6be460772f506995b0b505320e20b68bcec084e1c6d417c4cb659249b61882bd1e80dddaf5a309a8cc7756721a1b07c0affbacc416585be91882fd76ee1dff251a93f20711a32f6e06edbae4c44ebec8d2679b695d5fd666effb2702c03a666fb4a33173996218f747d2010078fdaaf1af07303e958fbcf051626bc9be392777412296ded2e047ddfb94ad44ad0d3f8bf89e2ac5640fdd86665dfc50261f67745e00e2144751a05aeb20b442213067e91ac46530cc277c95289fdaa483fa47acdeb54a0ae5c126fdd4fc90d89e0e1fef0309200c51dce99afb6eba46601556b37d8048ec0031faad607693044f96cdbd997937856da37a34c057eaae6a92dd66eb3600aab9e32238c58235ad4d7ffff4a64accfdf194d49ccd85ec312c36a69fcc28a504c003a9f3b14eeae9520c55235436eb41b99b0ae1d8155d21692b33ad9a44df74bf53c6db97c1db9e2dc8a6c64a54f13f5d44153930ea27abbfa9452e4ae06f85d713d80fd3d8a45b890a970ea1685dc52bdc8f1e0fa08e183a32b26b6914aa74bbf0ac572425f762900bd4fcaaba146d43d136c4b6a1c27ff4a40a6b7f5654ce2b33679621d720325b1ad1e737d500f741a68cef8b6525e1157c5b2653a19f079923ace9fb3bef72836ce9bb77485dd2da68d6373f78c5bfa6d09f9aa981ebf592935d158d8d8d77cbb70d4ec477b1bcaf3535e5fb23528e6771c664853593fe4360ad2309ada06e5d66e52e7ebdf454c485cc57e8142b2e22d909a38fbfa20a3beaed03f873d5b09a36caab0aad67e5105804cf88def380e6cfbef8945e4f393a90b7dc8547c5eb0b448626d8280695fcdf0bf7615e51e03465ff892ca7d49f2a38ba8b590befca23d379021ad25642ec3b25fd4d9c9af5699a62ff834e634223c36811240d356747d4a952384ff4921e15f46dbdf98ddb63f5ad003eeb8b7c5830aeeac810a6aa896e0fda3048695a45d30a0948143103dc84bb1efe41e9a6f3f824d01cdabd23c938cc0320fb50a84a3ade4812f897671baff1ee555ed007a4404b2df38e3e9ac1d6fc8b296db3736df94197f95bdb3efb97200d699cb70c9be927b251550d127f262ac6e57e2a6819f6569bde8012d2a178578d5e8cf98ecf8b80e9fee44c61213aaa2203b043b54e40b8cfa4a930c1313c603b668629450f2b2e7fefbbb0c8308ed7d1a4e283be89a6285c526c409f63559113d2ded9fa8d63dc0f38aeb09fc68b87d9832779f9257842457267f76662956512406c55eed89291a908e4ab205b45c064fc94ffd0be11e68742560bd9a9e8545fc3a0bdc26ebb81f066a124da1271d7aa73a0e913a9a536e3f814e6b0e05c0a933fb3c26663c1ce3d8c84706c08cd9a5afdfc4b174d53fd3597217287814c57343dcc102144f1650daceb8d4e9063de92a12849b37487a129937bbb8cb7323717f025dcdaa640c83a6397de61c5b2fbe9f4743c2fa62b2dfe03e66723d3ee92d219597ece1a43063debfdea21c6ec195d78649b0b48cee8ed562551a12ce5acca9f74a920186083d7ff34c56cc511b491d34e073bd1326eb466d0f824a8faa874c199a1a4f8cbd2aa87856e4a082974fdd85e362835dfb4bace179d1cb530fb3cd9dd8682c7033ae773a5a88ab8e61374eca232a212ddee5237484cdb02c5587d0166eff91e008237732cb5ae7b67aee248f4b4daaa5fd5ea640c13887fa446a94b015c9e84f110100944ccef04e119a019602c86565b846b255d99213964e468ce0ba525e29fa5a825f5f1885605b219fc56140833de4d587026c4c31bd8809d420311a3d31ab610301003390d87cc16de320d24ed1426623d71cdb20609651cb2323efdbfac5a79232814dc52c5608e96b6a090ac1e9266d5ed22a37b32676c3c18d7c4ab1dcc571652900000000000000000100cf36026ee0fd9601662cc39e50b9393a3be2ab97da50278b5fb49f2b73e68504351a57b44b2ded4b8ec9a658566c1fbd6f4c0749c15927405c908dd9a00a302d21009aa99143bb05394cdcaa3c383b295dbcf4251ac0e1a66667c94386cae62d6e8201ede0d62a36ec540a26dd480c71983b59b9b19d95fa413aaa3f996af9d2db42c602cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b000000000000000009d46f4341f532a739e7e74779f1ee4c4f7bd399194e6e62631b9091a5d4d0c1239d35204e2741a2a84060c0f584a4599de988fa901843b1067283626e9d2125e51238458b0d0dd6d493ec964a7462117237ea214ab8bb54b5a7d9e005f5606865f5c7c04adb7149725e02ae8030000000000002080ebfcb336d2942863d3ce6f7f93cbd11e3ecd30836e4ce3511a2ec2b2de0d0bf4d6beb25712fc02445e0d99a208af5d85f5466064c09c373c6930007d2749000100410021a85f4d580ca32f3b56c49516a42d24255f2d297bc6b46b44dafb3bf816c880ca154bc309c84f373c891f07c83255a64e423cdac4dac1e03b44c38fd0a8fb45 +0400000027e30134d620e9fe61f719938320bab63e7e72c91b5e23025676f90ed8119f0298fcfa307593f8ae18f7b8879e85fff44f515670ec1a60ebc441839c580333fca8409ec6591156a5ea3710ff0fe00a334d8a86919dc258cef4f4678b990244adf2fa494d3fa60c200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000010000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a19770000000029000000000000000000000000000000010203b765a9fc5e04e56ba59c73f5015a95cfb3b966d48d66838fc9748f98e8530a9f0765c432280efd39ef617fa3e332329a1c953f6dfbdbad3d7620bc7567d93106649ac837b1c70d966041b32da5fea3e85a71e51f819614823043ff1fa69088527de8d99bef40ceadbfe086b2a3c1c135f9b3af4688ac8a09f990d5f6750b04da66250b51457d0409bc295a4910893f10ff6f2ea0d255bacb5482e0b4b344b11dde99a4ee1e456ba7aa16e1b4908d3bc7ce314dbf636a9e2f364e51a0a774e46074defce3b2908c75ed2d2d0014d3613956f2314da68ad4fe0d12a3141681b0ad09a95ba068850f9d429dc3bd33892229076e32b8f692d1c5d1c73a913e2520f9bed944c2258946e5b849a862514e59e9b849aa02eecfd70f99fb26c9c40e9602ba8b33cd2a8a7ce55aa55b4f12b0b2d4f59203fe417e763275c73ab4d7daeccaf3f55f411dc29f76d3703851c0a5de216e90c71104f9868a660ec5d3bb6eb47045c1ec2c8d275098828136130ba225eb6c5700f284759f1c1cbf7bdae30bfe0b32cb6ecc25e97a31663467fcf8573c6c883cab000dd5ed232d0c454eec4411f3c60a2bd59170538a864fb5c29ee0877ab5f06f9d7f81ac0ab8a1f1812125b47c874bf927709f64a9530899151aa2783f7931f19ba07ada47e93b7091b4afe20adbb1891724260e0b10ea240b1489b1ac1766dee633ec5ee42fb29fc8f82cf95f045de2fc65b0bac7f402c097d05e3fdd25f0dd034c94daf8e6176a3c9adb6d3e2100c928f8c95837364f84f451a0aa90c23c202c7cdf366e86e89c4fe61f0cb973fbbf84e5a7fee54e6424d844376cd6cad2c20207242e8d198faab6fd38d0fdf28f897cf5aaeb47349377fafe6ea93aaf528cc8ced733b72e319f4bc7a69d6d3cab2410640670272785d4454ae8666cda756630d0a50b80ac3c15efe8cd381d7fe69eca5266f62be87e268769739e63ebe2d7db2f1bfd5e26e691e30b674e82d32d1b11117c44f188f9bf3c3a6e4cf082108c299bd235a8816286edf7f38b10a68effebb0ee182a706ee37be71439a3d8d983be246dde0f9abfa8f48f606aadd9ba1b103b1ca5e2ef4ddec32513bbc7523bd8c868b36786fb2e4fc0060612ca2f19c01dfade0837f588ed9f49848b5f02ddb2ace57a8cbea03bd1dbb60d6d0806def442569f697a987ef14987c3f3fd8ffde3ce3766490b5b249a345c91d97cd202d0f2c2cb3f712c6cec8fe9ec79be036d3faad8b543c5fb4284ead71f1a66fe2559cdf92a421f8b12b89bd922d7873eb0141acd673d18a0baa482d5f92d03596cf2286c60461d9d2a0184597861d042869125ec2195bddfa587ae619625c40798a6716b37618ef8bc81007f0399e147e0389c4eeb78843113d1ff276bfcc757d734e041c963a1bf14bec090788d13f1a596405e4a2ce9cd2f73b4d3c52c97643af7d5651e81f77c7a6b860ee526c04483a93574878d6b9ced547251f9e7c6967fe3408e6fd522769b424d3b843b0e682e551339cca3f2e90c2aabb2e75d33a587af31cbf1ddcdf4916f8ef369a2d524da337ce2726b4a85dc32ee5e633204ff149dd1f04dcbd37cacfeeb0e9a050a6372240e860f64bab4971d7ee1e3d264c62314d9296b02f245aeb13b1a8c6ff14b0c0ff29970f1a220370c2fa99e12e1440275319a07189e47d0de408f6ca914933b2a839ecab6614eac7c71df57248a146266aed8b3f4807fc433bf9984676f243a056b175055f15ca9d79b07b8412a08e28d0a3fe7e09cde3b638c60eb678e2c2a64075a95c5195aaeba8342c0cc1b70693c5f73df613f687ccefbb527107bf9b6077ae0eff095bdbb7c65cf1bcf0530f430e2d751f408d95cfa6747f89dbff9d6d2020e2a4c677e3f85fa754909d9f53f8d21efc8e4597d3c7885479781974b001c393b1004514e715d8b85ec1d6b2a6ee44d701afb91993e854531e7e3a413bb12b68eb6741fdaae4161b8aaa40e52b19966a2ef5b979c13d70e9826589b8828f6c87ef9999f45c068187cfe7ae7287dea2f5554ca45bf709c0d6a196913d86ef4c42ec9cc17e0480d63068a94f873da8c43e01163aac62e05e53e0ae8703b4ed6bc97cfacc717e25bc286163f7af359981b0255b19e03e239ff65d701ac4868627ca24c6a9caf1c7fdd8e6a75a96f1ed27072cf780133e78bdea3cd599e089b9fd40d367f268ff1e5a1747c039ccc957498bf2500de3a14ddaf2cf7c0434188103ecd5cbb82f3269289b10830d5bfac7481a0ab4d4b4cb22bc30d3bb1d44914445bd7957069f1fccc3f23d89b51fed7f1d27f8077d2dde01c3dcb84865d7a9ccdbf15e9aa2a7d03c0728e5dcd87cb0ae80429a0cdbf6003040c425ac0e23e8c553d03c8e33c02890d31dcb955236359bfa3cd1e3707ae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82f0000000000fde01c45d6bf67d25151f2f1075286cbe86253d1d9ad8534ec2686072c3bcca40bd82043bcdd357a66a1d39ba0de21d3608af42cfbeee4f734a3411cafaae5ffbf548880188d7263085b22327c13a39474bcec25ca56fb1738e952a8b020032bf40402dd81a03e7dfe86af8af9400182ec5100eb2ee3345cc737726a3a6874ae3509b31d43fe6c028fe9be15a5ebe5dc3bfc048f726553a08cd640c64b3112cd6cc10b846b4b6cf2588ccd5a41cb30cc92021d539c69cbc60e1246f0e4a5d3abb19b930447a9c0953d0b33981661797e4a57d821748afd89760c087f278aaea6f34f2426cfcaa038a64646e58963b446be7ec6055a045b14f4f87a582de2665800bd1424db10e326137958528b2c1333354a5d21b3648a87677e2bd02706155d45271531e0a6f98107e0a6fb23c4b2bf3de615b058e8a4808ba5dc87460da38b68c6366fa616a75f6d3584ec5ec2f790df00a5485ecdf846bbd23e692ec852fcef59319524cd7bc8c8254b5eb6c7ceb8fb1f2b9319d92b9eac12d95471d0df63d00392ab6a4b7efa79c62d53e7782253b8a9d63cd488a62055fe1caebf757f624f8e340a578084a8d8485f9b95f98775b323f61ff4288732f75fa20dd91e4eb35cb2baaabf0b428a94323786dcbe0fa4437043e5b455fd52b7846399623975d92e528ded8ce49ff3f5f70e674284653abd88bf28dac4d0bfa2f4d71ede226f01dade3db1f5d8c21bbfa2e0a0bfa536c03e1b6a8e3d51e4df679cdd1fbb57f74d370d92e4f0009881dc9ccf7a758a569ad809fd3c4c27787d882b87c272114f736a920c9b7d6fbae8fc1441363e39eb047e42e3f99bb8b9ea054d1a140911501201e481449daee88addd929b043a1cd80a298d9a86817f8dce84ca50df6b237b820ceaf942ef8fbf1dbc04a0c99893b3e175e897b0436125821ffe45b79c7bfcec9b9848166053af5d3eba35062f94770ad1c0b5953f7210b5f0d28186e68a77e124b2e30d07f690f735dc7aa674e99cf8fc0d4e84c75dfd2eb79ba7bae2aef917dc20bf3af59c7bec37eae63f1e48ef76bf96e5e3fedfd260ecdff58b11a2202f0861607aaaa35737fbf2e0925fcfc90ad31aec17eac33ae25583a321e3306f7efb12ab5bef4697c117734b1aab3fd0edfb928fdc75907a7035efb5bf66eb0b8d456a61814d2ea604185a84073049f908ce2df667fd0f0a0d545fba938bfd2136dfda5ed3f7ed3b2472c17b7e7717f4cca433d7b6446e6e736b6ea90baa3530a0af125922458e26803d3bb3236f7ce1627f229e1556ba7b1a70e7d95c0dc9c1f39a2046fd7b538725955d3930fa7d9ba0fdae0da436048bc7cf04eab0d90712e0a4f10cc25f45a1ff6aa326b1bfa3498a26bb170ed635320b3db150e172a72f15739a80fc53c0b8f37261e0fa785799448fd31df6a7827eceb72620d280dfc84e5403b8121cacabf192d3191beceede0d043a81d95346a979a3e42f809c20d83ed8036d2d6eaad5d0ba12854269688c89ee3eef9c690a1030b9a9be4d0bd5bdcef833938d26e7db7efc2d865123fe27d3238bb2ee72a1b63bf960995994ad17dcd85b00855d265f69355c7d46c36c01f419cb487779182891d058a96ae49825fc0b1b1235163ac1ed6e0e8941dbd32fb097a57f3449a955a3784104454f7f0d2068b0f21b797e9f01589ed30625c7d09f3ef339748bde735d691dc55a2106dacea54360bb8b8e72832a9e236466187d4a58b0e469086aa55d4d2997735f745ca0b6a98dcc24cf8cf9cdac6cbc07873de8ecfa27b4b4f3540f33155f8b09178b6922d0a79bd2950dcb84cded169ec52e15b00b86b783bc23a7c788b0764e68af7d514217651018007a14b663cec5244fcd2d98456c9f212f4accf048b0563b3471bd19bf208bf65e25c3acf1f17d02221f7d13c5257dfbfe2e924c88b8c813a6b2b60bc7060cadf2709f0e7098d26bb56aa33217d31ea2f4eab49b553c4513e24e06fb3c4061f22445f5903a8b3e725e5fc8b0cf589aa155f8ae11e66c45c58195ee1974f15b667c937549dd6c5bb0f50ac8acc70847c9c7fe54f8b3549ca30070f0ea3d542fcca0c16199b81db8be02f9400103ec967fe75a87acd86ad98ece86f3414a77334eb3436057ad88b816862e2f63385c5519853dc391c7d77c007072b193f7c8cbdafc594a600dd3c884d221d399b2bff828d68cb3440d31c13fbd54aa93fb2517280031460ab7fbc18f0d0be2c8cf7d39364765a83bdd465457d2e7560b7517c2e5fc70e27efc97edb094042637b9bbf002f2e2580de3944228ee634fb20b2bfa2ab2f7c54690878eac8116883b46a5dbf494806e8804e8550709bbca5045ed039975d21016b9631406c0005a721fec57db5d2d6ae9c878908ed355905a78dcf0f6c2c0da60a9fe824f7fa58d0eb64a1022ae802be48afd6753c172b2e1c0c9b34808bea1c5fdab39de11199588c2788649e5d51005b04004cfd8036e73198d318f02f160fcc5602b0fd5554ac32cc739ac4a8d2d58a6d2cefa5df86db099ac16d34928b77e8692dcdd48965ae372d49c7dbf488826d9efaee6f7a8d9d2d6443d0a3a872b976d148a90622f6f70b1f90b3ca8df1fcd2c0886271506f5621dd8e7a2dc18c9377b561f80fdc69dabee0917e67fa6d1af2f13c0c2fae352530d0fa07d0d3fab5dfa45093315db7308ad9d58712cef81be35cf78bf6e9e02f3b07548658beaff167a8557b366e738475bd6d42b0463d0a99328b4bf35c10e80c2f33182faa4580a7c5c2eb5fe9fcfd1d401339c9c89d42aee1cd20d8dec2982f7178159108956901e47afcb0df7bbb599af70580e6640431c5528e08fac5d70c432a3293aa325802c4b8d57b24ff287f3242d725929b0603a835c775afd81e29776ae5c5d4a41a9fcac96a27b1f787d4baf0e4290c5b8324d068b24d85e56a37ae5db2c9b461116868fb33149447c9f21f074277dcdf2d830ce74c614701ce00e08799d8cf6368e7cdb76eff1d996ec6a1b6dd72867431c5acc0678c9bfd562ecc993d6640b36839307f437f8f8efa10fe3543330515cead0095f27305702617d5f4c4b6bafaee577c9526e54abcd6746862d5c4d9709150001d252513d4673999e74875b04161ff6476a5f9643d59b16cd681893a1cdcb555c9deb303ad031a818392d926c2a7492b78e9c8ef476ba929a5d33be3f0f0d7cce53602de057f234043d5f950a0ae50cd286cde0ed5a2ffc1ff7f839b87e1794624565016081e26ec9ee2b94eb2d3aaf30f3734a85a49fcc6cbd4583b5c0a6a08b64f092d1bab0a78f1bd6882f7e15b9b0d4a089a6a59bf68314785f80d6d89c4f36661f55abf2c430758c5e35e624e69df1deb916036f44ce89c1edee1fdfb7c84cca4abd2d207c082c1828e43c026ac15f454ed54b44a259554a90d281f9e914c545fdec994362f79d2b6708890c795895a3d8bce92e513ab727987f1a9b7e9624b7594a9d5058ef3140cdcee4aa2bc69a224c43a2a65bb97557c98697806bcecc0c7081b6505acf2b99511568a4f931fc243a89ba0c053aa3924f62bc5d8a36482638e718c2afdf09088345ffa9c4144fc0478e0cc266694f9fda33be5e6ce25fac1eeb72d2138e166b2c0825700f9a57382463ba4b03ce025f82fbfa7da5fd61fd7c053fd0bb0e3a0d6b69d3d376dec7de782658ee935616e3d53bc688d221ea785b11d4a38481e2740ed505ceeda4d1c78760ec308150fe9b0790ea8cf89ac3a630c06db23613d8179b0fbd2916464c2be317d9d3d1cf68bc4623ba70dbd24530380b20b0310e096f053ead17473b61cc4c1a40d377aa028d56d2e53bf32600868f3a05f0ef9d0e396c64ecf1612e1ffc4e145f33ffe72bcae38f9445bc248c81c06fc9d0d61c82b71f487c19556eaee0d7a5380cc6cf745583cc9112872f9663c6d0f35046545a9eee6ec5838b3eed0ec1789d5c962f9a82de115e468689659f4c49fe7281e1fd3dd6e1f5ae8e4e61599bb024817a8d5ef0d230a9bb2bc4fc85d3a5ea03807b3d58bc3f6b81f76818843fc86b4ee7b7aea4567ecc46f915a9c8399bc0f246447576c36e75618425a20b49cf89d06e71c7664435bbff57a1f8c68c2165e382dac5b1db2b64d1ed846836c06fe6c06d84068d6e30ddf7b92c72cbbaa8f770bd53c64c3368c451b17129559b91f566c04eec6deee23ac293563f6bf5a108314ef3d0785f947b076d5b4aa64d9ee6c48569d79b0d414e526b32a2da282a5ba0327be977745ed6bbf1badf29be36b0e0371543244a16f2593998ea997a2e35e3cfc4eb8aacd46accf80cbd0a63370921f37915d11bed913f878c784881944a03b7c5a67dfeb21ad029570c79ae1a8ef1a689f8f619fd642fd50154894330e3f1d6346b3179a18ce43aadc7e8369b9f3e31c3875bd0f9e7b6a2fb9179367155626843831d60ced4a76ad99cb7c0e3fc73621de815acecb9aaca12317dd25ec5b2e8b1b98b073a24ec75c26f6b4a971fe8860777511df6b16ae66b8e6765b48a2093f9a4b920706c65f5351fb1695a08ce1d2c222585ae3a4b76b31e2b76cead4233cfc4ff81e6919dc0ef1c7a4d7780dc92efe30c4e3f15ef7ba75d85269e4af26fbe4948fa8ca8595b7a76b9b06ab3dc59081cab65ae65f1a663b6697a38bf83b5a5f6f7e8499cd96a4ecb21231cb01667daa2c5f54981b8b78d2d44519ed193760af2b7fb6c60fbfba3657c32abab721531ef3609caf388d1129c1b8707c7d289ef141d8d576a034bc003d69ef28cb6b23e5ee142dd62b82a321dc4ffb63e9004315d8d22bee31cb5883118523fa2ef0ec095f137f2855c1bafa52a935cd7827d84978d8c97280132b9ce43ad5e1300488d2e413942b6fcbe002efd7cafa5e0dc275d7e92993a6258652c2b806ef3a1adc4b6f83fc5c2771a55db94806207506f82b0d93bd641bbdef330f931d509068b8ec213be9512a9c1d8903a21ef5083159e0d0f385dc04a31ad28ad25064b9a42a73c22851fad507c81a218ecf922d1395e7a02f78c7b9569c508a249713731dda638c0c3cbbf987b2107af732507b241dff2d1e7dee5fc5d9aa37259993814f3f946b0beda2f36f8e16f3d2c42a552f1bc4727aad1ef395ef4c7007b336f90d6f148d1eaad0d3dec6b71444272f7038d6e328baf7847edd88b66aa2e460e0f195afc81d110a2ea14c60d0f66d3f1908084270f68ef0d808ab54539e2b77fb6285cd28f9a5fc836aaa093b42ddab1a30f25322ec2dfc79f0c6b8c1bff16a5db5df35c871455535cbe39fe1cb957e9c0a5ca20dfd69a4748c369f92cb3737a93fbaf194a1fd1d3cc5e5d948f2f4069a318a6b40590341159afd1e789087b488525de29d4793f645d7e83eaccd35613f341bf9e2851f8e0e4a37adf9c33588d830ce292d1a3327c8529a5f794accfe1618990eb8154e8659b35db9c21a6b11b9d1e1691ce4821e92dc2ac4d758c0e70600ce3fe8cce692b4d200f65088d4b6fa6ee6694c0c266c338588a4d8b61ac9ab038e8d18606f538d76c6a8005c3bc1fd5bfb8854b550f28ec4023b91b5128f53279d07d1f018f58712bde9280fd246b0795d6b34d543fac8095bad34d8f8e00e09eefcb9105bebf4e15c17e754d312a694a799f261b76f4ba51b79317bf9e03e1e23747fdf932e5f3ad22a1c234a376f6bba52d0a1d24d24697447a46a04782a3c74b67bde61f5bca6a772b200028863c468b1143835b8d71aa883e4434c4ecc3bce13c126a6c8e29244603f2c6a52a0d17fa9d721a017ea3e3ad7d47402cd0d227ed3738344e99e6f08aae098c9eca0edf0676f09ea91d979f03b9d8515cb0c0803211a3a027b65d2e18b3991586ca10314d4b706f22cd0d72b57b8d4cd3623045d231252b027e72973f297f4880ea12529ef2c7e4ab7cc912ba1ada8cfcd7527c2636a8915515c88b596dd33ed1d44e6e2a497e48b376e0b9eedf1ff731d9b0c5bcbc123b1a378833e9dc791cc0a5c3ab4875edd204eceace077e41f80e894307a4b8107abeb649e88ec7c318ced68bd25167614c7684449e45938f7accb8a030f054e77eefeaa4c6b7d0d2992b181669d3daf7c2aaa0387201fa115f7d0301baa17b9c328debd543d48249048acbe92c9f6c629c3c4cfb5991dfa094708c234393fb4f3b27a11b7a8d3624a6bfd72569a2bfcda15cee5a1b90e3e6a20b2fb3970c66082f8e29b526db81fab25b678d42c8953f218cc433c697a414ec2bbdb0a2aacdf8c4a769d10c7351ebf55f1f217036ab2a7124a16d8c64559d044156923a9149388522e54b421baf05c4776d2ad8112e48f375b80f3467453bf47a02033bc66cf2a859098b01984ba8b53f9d3da301234d7d06056259c62654b1f630a04f8226e147e08739808353ed8fb022df187ac006277351b6342fa6a97e1088e0a6e3043681547626b813bbf642faa860adf53b28b3f4e6130696798b8c790292dc7c95030d476b2dc3b1a8a6a32d57fe74e3939c23d53c238ae4da3604ae7bb1b570e6f38301843a5393ebcae21030ab685b7b3ba48f7e84d00528c7519e2dd13ba017550a89e109729cb76629965ef0f7f89bca63d8e9f24459b07ded2478911a571571fea944de0b11c5a195388dc7a07ace45036c044c54064ce351f28e40a83379f8d0afeb70099ee6b66640046d63066e706f3a4e9aa5bb3b67b53849a21e6dc6797cd910e2c5e4cfd6dc7000c56efbbefd2be2f50e411695f1b6553e123c729f64468ebc722c8e97a5befd22947d730e63ee8b6456686fcdb5e843c620db3b57eaf9f776d29f3fce609dde8d082d0d2ac8b7446d7375c8c83f70622a128c2b3b87c5c64e0ee42a561be568d1a5db9d5c9d807b2222ac6345eeaec74952725719a53f2ac2d4aeefaadcacfc7146034f6030df6281b351e31fdf4a9a80a2e9ec40ce77021de91ded0eff75868b4ed18bb455b3da37b0f6178708bbe8c2f1cf048651ed509c76f7933209618c9d69240d414cdeec72edcbe53e61ec62a722abc246ecfe56243638b28f88773e67ed8bc54ee222def2bbd0d3147b12223e53dcd3ac8abc2ecf8c4be626cb4a5d7a09b2619946807346b683042eb634314a717e0fabdebcfceb124769221f27576307d4e375e2db68156f983bc4fc02317e22c561227c06884ac3ca9cd42c32f732c20a1be32fb3899ca43cac9f82184c40603b1d9a106161fa3e58f2a0913f56cf52099c15cf8a178a5982954023d637efe01b6ba9a350131b04f5fd0c59f9ff426c79dc3fed613372596f6c60eee933219033f06e5919d4828a0e9e5c5109499f9eff555664596946da6c8896ced3ed38b35cf6d7b0249073a2b084c66d03c3c78dba24dd2d066da9f538c794752ff38ae14b9d989bfae8a47d0fe19c80132aa0bdf224d48f540edf7dfd841eb971200ba2ad1bc3427e0ac1de735c6d0d619c6f17438c39a4fffb69ed0b6a9654d806f2f3a92fdd8b79d5cf4eb36b88a145935a439a3a3ee606268490034fcfa36d2a21d3cea4b61acc4f6ff21da3f2a0115358c335253a7acf4788b9b458a2dfa751ba532bee4fadddad7cfc5a0e8226cd32aca877f25da54625ed0af0889fa9773f64722f0488fb758f3e40dbdeba0e43dbada6ed520cbaf01f5ac7214411b890084833ab2dec338ff88895fa0f53093be18b3fff83fe4b0b435c3524e5fdfa131b1db190aca8e7cc03e422ee6b426727f10bb679fe4ae655f9939c881109d7dac2c0239ed8bcd8e8f8d00d0556acdd114ce9c403f62d0b569d77531512e82bb9e747f0253bac3589d38fd28f14200f224d863f99fedd3d438cd46502d1ba3e88dec51178339305553e9d692af9df38d6ce766263480d5d18194dac906fc0e1ac5c72d3499676e1ff3c427b11577ea943452824214cd39a7335cae0731f928eab92d483b90f4f271bf7341e20ed24c12b9629458dc673f2f8449e35ef3d4baf50c276837522926e2484809f86f91a26d3536968cea9897d1025ab9f0fc240160cd413e24954ffb9dd4d8a5a7d837cda35bc341f2c29493c92f1ab0b9fcd34c91e963d52d4551c39763307e10123e086fd59eabc5b4ee9f9b16515858f662affabdd12a32a10c50697a0a7ec6209f592517f08de46fcd2ae9b44bc32a72cb24f6295a0a3e068c10bbc88fc4cdbbd5f2ab01f6f5462593352c136a18e4769bdb3c70a5ee2e0338e7024e15be4fa206a68d2581ef62402eb7f9c8c7167089a4e58b88588813616dabf647d6a555284d20a4e34705b4162489e5d6a4238da8eb7b6d408fa21590d90ce805116b5d2eb635a9c984a0fa96ff774dc573c35263e165c26d4d1a168b6ac8fe5f4464f92df805f5e1dcf1ad9cbc4d2bb8eeb54a5559d3667d184f05e2e6876fffd1ce7fae09d52ac5054a9b1b556b68d059158b474de4d591847a0835256c1973dff64154b344ec867a079c1bba1e2b5b89204f5c7f52300791551681ddc4047da97bff4b87d71aac4276cb4bdde61c9b3881238da6ad7972810d2685c78579827bb80a010a6a7d0d2e1891a2afcb974fe4e7fa1c42ebadea7b2f18c9e6eb359502ace138e57f8a5972e91665023ca624684b32668e7a25b6492934f8bb11164189ba0703a3603bf9f5cf719eb9773b0db675280cb35d153c185e05122275aa371123e1960e97ce993c8db65e643761521f29dc2644e61233be673885602004b1f5dceb9e07771beecd54843a22f2d6321028ad10ca760b4c02023c0f62f780f9d617179759ecc4187ec35d05054c550f629bbf44f50e4cfa0ec91cddfa9b9c5e66f78e69428c9237f54c00f59c96103025151cb2963af4013f8d3fcbe8c664f1d9c2ff70f689f33fcd78b20b94d6791b8ad4c0811444d2ccd25200fab602a1d4d399a04fb4bcaf88bb5f23479be16ff36309244a609bf7ce4aff2477232afbad7f86066e3d395ebe1c48e00d19eec3adc3f4162ba89ef46d86c81714372cef356cdaa36c377c7c12ea44d14267806e9e7751168a847593d9ad712ae10783632fb508f77986c3b36f8184afa53d5d687b48d55a330bbbdf50c5fd2a5006d35ea9303b11092c278519f2c3acfb74b47ee1717e2f1ccd723aff213db763d79e4e5c03d861b86cef970aa150172dc1819b536888189bc445f1f159f720845940446e3c92201dd5d57ead179c101389189f58af03083d671d18d687d92729a18507cc3c584d4050f1cfebc2a8807af60561e5564814bc1d66790782ed3213e75d87ba49f70753d08ce2247ef5b3e7064b4af804aa96c78e566dc7840e0f32352ad494f08ad6c41484adce428ccf5ed420a8b7b51563dbba2152d7335e39cdab87fee896854f204bbf4530748c92483170c3f69c78b1624f4dbf9ea34c139da86818d6fc5cd47d082f023281f5b5eab7e6f77236587b169aa68130221e986606cd4f8c8866268166f61a6fe0be8d958f4182e07c167d2e4a5f2aefea6b32bd29cf774edaed9601f36371621b1f3dddb89b43e672f4aee722875439c0399ce41a52006a1376d492896708e69717ca3c554dc255a3df1f2f6258440058483c9e718eae1a989dfcda2ea039c47384819beb2267110c99069ab30b9f0e9e4a368e1beae02819380444f4f2bcb062c33349b402506b9d4407727bb91d591ebe0a60dcf2cc04970f2900f34c7c799911f900a70e4162c3e0dd1af0be7c1e512d390dc6ad28d1250b0faf467ff772d0cd79a8c2988dd615f27acec51dcdd19c7308e54ea4f832ec05c31c632ab2ca807e644bd8fa8fe4f95cb950fc201ba2ee92934d073a517e36720e5f5c1649b6447a45cc2ac63ddf53d285a615d1d29ffc96242d6677b574eebd911d0c705fb2485ad0ae29f7a46704e8dfb9df0e09b44bde2bf9891685c4f0ad444ded0d43716c0a5334a1d4051479ca131682ed30c0bf338197fdf239de553112b46a33c3df03fb8760bbb7df3634c8a5d5703ca985568ba42de8e552d6c6ef1577d4aae799fe81f8f08a006de2b45d1566ee4b963f67338555a1b9ae13ab85f62104c74d54c215beac38bc6a9e9fd28954c0b25f5938330a427cb7ee6cd84201c7ed297d41bf251376bab81e8ada35c21ace6a081c5fc38902a38d3b95a7136374d043001521c350bf51e40c0543afc0dcd1c9ba281b5e0c890950f0b14d3d23cc33c7ad99d49fe83526e54075dd9b3a3ce02e3c8ecff581a813c66f2fb42024e31a89b949fe4d064cf664747f37aa4e65d393393ca0c0b09a4a96a512157dde89ec79bc015e16de720ec09e7be7fc90f6af4036d70ce30dad2469bb8e3d73f4e7323d690e09b7c5878640feb7cc12790447786d6552efba4435f49f52b2a477fea202ec3c35b1bd974d41f84fca983dfb42e6cf04221b824330a18c1caf3134208d81d86bb886d2d4268211e2db040253ffef125b73ba0839405b1f5b4df158174f7612167bbcf1b53dde03dd9d80fe97a0a9b584c4562e0100684b7b14804d9cf619732b81a8114fcb2173554d5fe407051e5080714400148ee37f5e60e133abbd7a06862d9d86b3236c0f4b8fd52f1fb9e0e8fd6341b4cd1f0100ee8b7a4e0ee75e7c302bb28b308cb4a90b7395bcbaf9022e4881fae15d6b389aef2e9e782e904d3738331f4bb8a923cf6b55d40da562f684dd6aa7dc8676223d00000000000000000100157f5e9cbd00182fefa8b49b6cefcd3a0f2543b9e4b17c0ffd799a5589d138311148e6e6ec3b4121aeacc137ffae25f0ee8cdc4a158b2b175611a8a82772601021009aa99143bb05394cdcaa3c383b295dbcf4251ac0e1a66667c94386cae62d6e8201538a571fd7d618d45d0b2177ecf6e95cb378ad747e86a24a247f8b89dea23b8c02cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b00000000000000000b7fc88fb7673f786fc177dac6a1df2b8cb4ac92fea39de22d184ea5c8994db319e1f4130530403459f152eef980fe0caeac8a6623207b3fe920ef8932935825851238458b0d0dd6d493ec964a7462117237ea214ab8bb54b5a7d9e005f5606865f5c7c04adb7149725e02ae8030000000000005238e6887efa3641d8b0fa4444a5767e79c9bdfddb272fb9cdedfee087be5e35a0f65368419a28c36d025a4bfbf3ebf09cb64565360badc672283052147a80b50001004100d3d24bc73805ae5c0de2ccd8b1d82feaf143534eada4a764970bc2a3a932d9430858606e28f1c7eae258e93ed9dcf38a4b6c0f04295770eb88a3d65def40b2dc diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt index 13b83d88ea6..121ede601c0 100644 --- a/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-2.txt @@ -1 +1 @@ -04000000b87e13aa72cc7a7a7b373443536d201f494c8850059e413d0cec6460312669209b5decffbb909449f36cc26c2c2b261835ec94c7a6adcbd40f0f417718455fc75dead1df18b7f93983db4245f4439ed840a52dc1afa68197eb4c8eadc7544ed80a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000020000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff06527af09fa693000000000140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002a0000000000000000000000000000000102d4fc51846154390a4281feef4c64cb309d6d0ba8564b64f48d1a3dd50672d42744312109864f928a548bd533accbb2d142f8ca73e2732ca262ca7ee304ec30092af791c21a4b738508e0751d739d17db8d7ff55377196beea401751599a6a8997b8d486858196d3f7dec584c912cbbf6973f6609d05307908c52bcfc11732d27e063a49be5cdf19dba82b34a10b8eba0f2b03e37d54f5b07d291ce80139c842d07a1cdb1df99c46a20fd3480dc2baed88d2097dbdeec8a8ecaf582805493d0957acf2cf891d95de7b6ab160d3454d3317695c1af1f5b4f8be1a591b710612800fb14182b3c0699cb63bee7a40f6906465a5671bd9d2db8919679bcde99e1cf3925773f37dd58dcf779f99bf3019c9995df33d0b4bb3dadc1362976eff2f8f3a76e6f69cb18f32d0b6c929b8ea2e46a075991cbb8c063426bbb2ddcde9c37acd3a085f6fdb630106ec3c3708b9154029bf1ec05f9ae39cc6c91bea8fa124e1c9678bf038d3ec73d00b17b4ff53ccbbf91e3df1b8f8400857dcd4c66cca30b24106a59b91b08cbd9d6d495900080c97507da2233464b99c86f443382824963af9d115b9b8d6d677e98ed76d90d58306a99712b7d0a5d6d1530c6024d6dd2b5d525919c0af6e14119d04a11d543b34a6cffb5c6df620dfad84f94ac5399d00fcaeeba833e4993a4039afe3980a23cab01acfeff482eed368d24f32095fe9069967f387cc028a6038c0042958de1ffc1ec1b2361285339d282c77bcc00f181e7d0af5075c6f475ffc34c25b0a3653dff1807988470abbcf3721904857d27153386cf6fb616ae904da48f957161ba03fb4e706961fd1ea578a3536d450eb0069b740f4841bbdeb22c20ba89493a1358759828e5a1144db114bede819ad30c3617a2f5f053de5e24d27e3b3fee71830100673b476326667c9dfb94661be9ff614ada205cbed4a8b66d371dbc1c7485ec70babc809a437b54de7c97f3329ae2529692b41597a274296a8ab87669dc30d77efecd18bba75358011bc68b93589f4ffe0fbbaf0c080106d2fafac7ee11e11d364d6eaaaf2f799ddc4b77156c3bbacf7ed79b0779f4695c38ac540a11433df33f06be77603875716950dd90c462ed1035330f4cbbea299efcc080c3f9b88faa1cffbd11b0d496c4df348978dc107ab587d1aeede8e1ce91b7f662df1f296305cbe2a211ae4cadeee58fc4b9f5b7002533d10c8daeb4264a3a82147860b7e0823850089bb94d9bee0bb8f74bcacf6072661a16e810f005abcb152210664001b64425c0c6544f0b9a66c298a8f67e295bedcacc50798292f6ddaf95e740957484904240494adbb7847e43995178f465536219d262ad25d8c1a87baec538a030a0f9d1717a40db1c6d0391fca4c48af556268534137937b8292b37bafb1d3a98d8c70eb764a4179e97f5e1e7e7dbd35cd2960208eef434fd81978fca766742e7909fe70d433a779c723de83d6735569c89d26dc9c515aeb4b462f79a6fe75bbcb197eb933ac96181399ddf19f70e7e85d61d98fc65b3066eff9156750e9317b71ca95785ed63f6d225976c499d76822517d0c1ae0ce3c2f870387b9211c7a59fa7836337a3e5e4435c5e1b85efe37c66745bcf0cb1a8b4ff3134b4063986ede9a1f861868d9f15b5a76451183b5ec89b7ff689cece39871cfd960f52b0d019b4958a19f63b2dd86feecc3287d0b9fd1fffda8823e57124b617ef7cf8211539e807cd908b65157186ac92ccc2b1f3a76828c6f0591e2f2ec1f757a46d3931a3e69f90897d5b82908cea8d7ee758ed6f6834f02033089cc182821375d33e114e9c47e777de576d379a2389549174ff654f5c0042b66391c4d3bbea9676248a6557579cfb3efdc60e5423915f89ad98e61c3914d30ef32bf3811e7910e3cbe3f3c16cf39759f3e54ebdc85d4ff34230b8f777bfed8a05715ae8318e79ab3501e39beb188f4867bbdb05389b61f3ad7670ee3260c19b963749d6b77bafafa878111e94f8b27c8b2b148bfd3379a389dafce39a3734f9ae479d6779fa62c079c13ee700698601ef5968e63963b849a0e5e9c149c138864f3ded220b2c2270c998775f90fdd9daaebe22718a179bdd334abec2a3d328d5efc8a0c4c02013a63de03a0ec93fc046bb65d36a27e8f611caf5c4355d289a1d559f752563b661f07c1c25eb410622352c92b381220d021b0d01fb561a75b7fe9ea6037413e5472ecbe6a2338faa6e3180f9863d29b799c4b5e166a1dd6b96c2fb9de9a5d1fe31d6a4275f13bfc4b8e937267e96a43dd6caa9e5f2681ab6dfbfb6455b13516cfa6acb51522f5061a36f193fcfc4e7f18d2548d7e04d27f937bfbe641b0e8713a6d3fd3a87ded1968d3a41b972c86ae2df5bf27a4101246145b585b52e930708844207c7cd511bc046ad72e004f53379115f30d0923d52fd9c92f257f26e6c87be69280000000000fde01cf6d67d62a324bafc3495290425b5ba9dd19b35a1a1a10d7071979e5aa657eb21ddb7f1dc51457d933c0bf99bc823c383283d348f65300bbffe58870b1f5a0fa372ae64a771f69686defa3fbda4d16bbd29e275b8d70dbddb23aaec84005e692bec8182665fc4ceb819d2b7fa41abc6524df7a9bcb22ddd392d7de10fc6857c819efd22592bcd11c0cdadcef27b3d9a381f4211a796098a649f186300245fe60be83f39eecd345046888debb8f501ddab1c10a4f2f5570a1ce168b7597ed4f7adbcc71957432c5c492babe667ad19cb79f72fcd61dc957dadc80fe3cf893ecbbb59744df67b5039002a55c30b64fa0bc9b465056b2229f1a35c5f9d6b59ad193fb261dcabefb5feb81aec4be4160b2d7c1a5ff24154d026d93c89cc932e2e4629e8842e45e7b42ffd183f59a20ff1dee82d080e546498c6113108ebc7e2efaebe1c7d6408b01107edf77e7f3d3cfc6ff73d21c8488f925eab47a67987df23dfb291eafd984a656b958e76b7cd558895010b820941f529e6ebdd75a8e0d86c5519ae642c9f05c5125b0155873ef81b183d1c15e21362a0679bdb1fa1fd21b27c1fb1ccd05655c103a6556313f83f2c7479d8c59fbcef0de49253323ce6253ec9193e785186d945aec99a586411e26ee1db900cbf87563d62d21d604eef1698773f5cc46d052760cb2679e7505476d92c3daae38fdf92d0ee0506eeebab8eb9842ecadb33007893fa02500a66938ee06e8c2645ae74dc023005b5ce42733704af0ab94210477e5182fe1efed7df555a186de8e4b894f2201a7f11200bac1050962c66fee604202ed874d20cc6ba9d32ebcba182537e269005b16daf15922512221dbe5a458be429ad4945e1fcac6c8ff07e220c12846e9b2523a773f2e069b5de3dd635cc04b812a774af8df369ed8c41a09f06398bfcbbb333e5e629c5ee660d29d97b0f6d0129acb7c10000c377508e130dbc2743a8c79af61e48685d51b4b63a2690681963a719a4630f5f8b26050cdd20e5892fbcab35731b81ff78b86ea488ba6f557485c0baf1b99ba03595c024d185c6012f66dc6c0ebb189b5a66ea6e3700c03ffdf4bf1d47a8d6d518a6fd95ecf61c6ea37ee4c19a4ab0200e2c7ac9b429ef9d1aa28779ae8edfa0815a3a7d1bbb3a2cd41fddba0cd9acff4fa557758b7e1a0612370818ed8a955b3a33248452a1f7bc9ba54993486afccf44469615bf3981ab63729e4574b38166872906de920918e4fbdba6a82889ad1171eaf5c0b835447608dfac9c544c709285827df9a17799d5a06972073494b8c224d9ab46309137b099fb16875ca24982b337bfbaae49801ac866ceb1754563d496b4383da60e20a890b8d4b390bd570e81f4d78234e8b4ce8b214d839bfc531b4c8752071363ddae443bc0b4a233db96b85ec115ade0b7c43cebb09386503b60d05beed48f002d5960ba3858a7e4e7a17a53a7aa095ef881de72397f3dd7035cd6abd116036ce8ad21d36a33d760f3b02f68a5efc4752b7f70e4718541d85ebc4fb43b15a7e233815ed8a2a4315f616f3c34fb052fa797b50a23b308730c7ce6d0b17c86b813001c08b1c3782b6685b6df7e8b0d09e9682fd6fc014e3f8dd5d1c7195eaf9d84b539a8840a65e601b36fb067a480adeac2a0dc06da5da1ac59b250c834d01d26d06498ff58d56de9b3de6487b5ddf3c001e6ec9cb51dcbeb3d4ac8946a2a873795d41a7f466346cd8aaf73740ec843a7cceb587fc5aeebe8438f0275b93e1b34610be2d6dd0d2c3d262ab0f41f192b313f92ea95e8aecb79b02f591ef4989bb49d8d62e3d5bb4a9fbdbdf8df41e680fedd11387805c06976551b1f26af5094fe2a5b49853a4da236cdedd8215529f5af05583893d3f862187db0ef08d67d9c4fe65d6cdaf36f6583f845f100ac5a06c585fb8b91b750937efe5eb87d6b693d277a55005f7990691b1afe3ae56a9f0f25e12e9cf0315978a1e73a680ccac38880fd08a79ca15178a48478aa6e21199107d1662078a923e2518f06dc2d4a45a029c90298bd5167e45a252db76668f9f2aabaafb0b67d357619400cf77e465b2d97a4b28522841b2539d4b67a719d62dcbbbfdb68119b631338d2fda9de5f9db4bda7969b74bf37ddfe9c1cdbb06d6d482c182dd6cf55a6ea5e7c1fc05fb036a6d4319149a1eaebd374acda96566e92fbb7619d4fdd0ea62669ed907b86b46815fc0343e7435a28efe3dbbfe731965b2d8d5b2b472f8b770dbd49605364ba7e38b6b723efeabbadc8674271264008dead898a86ebcd8107cdff8c4f901fdc613531900d2729487e316c7d626d9c6297cc072cd0e12d4d992f62aace1e653e0f0d6282f022ec4efee6cb63a8fd961a7d18730ae3af435e7d86664b9d88873119982019ec0223ccc0bf11184cef5daf0675727dd51c2c131416dc775ca9161eff1b32f1af0d15fd5fc1e2026d140b554ad2133cf1b8afe1cc2861719e2d70db972798c8f3d80056f49fd2f1612a99e8a2336d85ae6f2f4360b69efdb75e516157017bac8b9f01f3668975650a4f230605f01ffe6f9c1522615d6c6f4f1b35d53506c9f6a8d10e54d2359b7bfe588e7ec8118b90047c31e211f27d3eab08768d553d6d5914b134251468f2c3abdf52f9309343f0c9f6097f0a58d90fdbb7aecdc42f8ca124c66418e2aeb1f7d63cf6d03787b2716cee178b47b4deef5ca9f6cd4817bdfe5d3fe5d8634e6ba3d7c9583b3b0714e3a897b7758bd9b703fc9ce4f2e13af2dfda9e173cc0805386abe5a379805e3b66216abb88a31e9f6ef019ae6c9a3cdf3650dc3cc05b8747ece58117f959496299c36c0ed4fe562ec619b00b175a2a3682842f9cdd14850b3506d36eaed932bb6e818be6f11dec936c789089843c3e5d1e04b9e20bc8d204ddcbc3422ece6d1bcef6fae6b5fb958dd42f7d52edba3a777ef457671d301036fd3417c4ca27f9ad11907543c7c3c16e5e0bf171f96c213f9faaf8a09f2d46328ade22effba8593a9216ab62b36785ef08e6647a019b28b58abe76f0dd79925b21a2f523bfd10a0a096ae1d5c0cdfe797e831983ba43333000a0347ca89a4d9b4feb117e6895ecebe2051b47f9dfc57f87b6380087693a089d325159d2d262656a43489e20390ea8c2895016de1be0d83f12a8b602dd34d928b9c9c05bf8228b1609e19c767153e83a0797ad318cc6571dbbc8ade504361cf7b392aa3cbd4259402004e0c572c251e4c5b3cb8f9ea1ea5f04195ba3902fea74ba66b9e96f92bffd16f36e640fe81d7c571dcd03a2f0f5948217b2963d186c4f9eddd74ef50e72906669c2d5a87729cd3050a93602ba3bb8a74174385d0ae615157cc36fba11e9bf40fee2ad7b03f750023d7872f285258ea0da24b53630971165b992f3844c1c9ca7598cd6d6537156060fc967de7b60f8e68f9d4a4f20bbd3a7e6375a54076f658f79bc427e68004a1516d407971d5d3bc094e9831e14e9288c7a6c4f63d1569d85f44cae0ae43c8e44f3b4b09004de8a1456e090531a4297628af9afc1cba559e0502c43cc3889b338bcce29868e29ad096aecc3852d4a44707571d4f9a544e5313d39967d861c64e32f421dc93422c6c3fff4c5441b113f42e3dc30623cbc0aa9d0cda227a225c7882a5997cad2a6d30f82dcfbcb2daba4cb1d003794f0dd04c2b07c2bca602dc3197a1a098d0fe9366f2bd993af30748ac0df2d8af44a7fcaffff86912226cc706904e07772bb7260769e89b356166495c20026f48f3520c6a1c03f7c4fbdfe5833a5e4f3e5225012a6ffe6aecf1fe35483a7019b910644e2dfe7dd9bf6630ff02283cb2cf42d401709e25ca24e347f65e381b3077905ea4819253f99ff7f00ec87a6f467fd69d881f33c306639220898dcb7be2e5e7a3adac4e31997159708ad9e4b3b01af6b19e2b1a3281fa307ffe64dd7f33901b94b82c8605634ac9d5e0d5efaf459fd60d7f8aa7889f1b82b7eb97194ff9e7e2125957ff17e290cea78358c6b4ea2b84a28c22cc23f24a637f0737c36bc764e948bfb8c038789949889add6380cef438930075151ca5cbe2cc03d1d50dfe6fc709a69614af3f98b373ea3f5b64570cad4f477810d70ced20c8251ea502ae6e9d5c017c6703b2816ddcc15c513691cbba965c5d02cbdb487085cce931d170ca1834135588fe35267bdc43cfef8c7796cc40069ce7e7247873a7db90f912594164acd864c47b588f6a4c9eb9adc7b15a7ccd09b579a133a1f2872de09d7696e99934dbba04762844d1460e51da1b7ab102200d78b4f602bc5211b1512741074f68ba5c08fbfaa27f4f394b2878678bc27ce8b1706479ef1ef312f94249c42532f63bc2966883d72b461fca57b99d4a91a2efb68965fa5575d3ffc40119d8ce5469d6b4db41b89a2484e6ac38e152e1c382e76bcbf6a9705bf3a2f9eddf9c4687219ac02271197dddfda057b00fd137a0405bf3d01f1cc69cd01ffad79de3b077d944d99d757b87122eccb9f91d7e25e7f0368e81bd22781de1cd9dbaa1ec7b79ed01a988a4ad44443fec9ab5b2a78a5c17e2b581b9fa1383d37c850248f6efd6ed7ce08d3a36b0b8545a16f7fb5b4342e3cf3a3c2fc0aa4241382319502c81481aff497df9640cdd42b1f3c7a16a11140463625823f1bfa0004a87bc3b0239a84d465b13b26dae95a20115972e59baf288202aeef43dea10b33b6b58e409fdb7a2373ae71c02e15fd3d5ab6d61773fddf2b6790014f7853d00a38374bfe61d272cc1602948326d241a395df0d3e562bc59338773f0650aedb16e4e0f429cb0198134dc37f9d06e74bf16cfcc8446517364782339867f80f741bbc25e7c7f70c5f3c6cd6ca477c510d8fe6b6508a9346f5f2e724cc4e27a4392473ad51f77d59b95d959497686b7b85f8c7752a6b86d02279a50b0b67e86c4a275ed9c6492713405f9272e88c2dbc3687e030316a14ed141705bceda88c566014b354fbc9425a74812ea7832a1a8ccc76bdd4d7025e425b37b9546f0922e54f0eed2ca8c2c29a5d855f9dd48a0edf8e9bca7143f1d7e4b028a2dea65dcfc6a3028791afbe63825ae21cc130322f0f49148fdaf5e53e3937fa19c58e898fd50f07bfdef187fe490225c0c52e2393fdf8852e69e09a749077f3245333f08854183bcf65d690497b9c9098f5189c66468deca6afdd0e60aa9cdade36b4271f838819ea5dfb2b6f1ad8857143696de65e045dd57b2dae9f0db3d37f49e94a6f38b238b96483e9f5b60025e2d5df60bb5e4645184c0985697a0493279e6db715c96839c0228c1a357b0d69ffecff73972f9741971b1853067e076c639850bdb11bc30ebf2b3fd2f225f914e632b5003cf6f0706ecb776d01f13919b54815c3da07720945b6a6aa3b3b9a9314c6a276e00c511717c9d1e54146f4882dc49dd7edbf95211f215715abb7c6964c59a7d5af34fd3aa24396e8269204f50566c69e4c0c763ce7c10f8c5b59914b73e27cda877a0b6033a365dd163bf2cc7273edf0e3c3b628c5ab2313db4b3dcaa7618d0572cd169a01d1274b8d25e91085cfc05e49a9ab3e2ec6e33ceac0e8e2f2d696517b057a0956e5de160bd72ff04fffbb9159b0a32cb03ba858e9570218c72cf67c3f074b9d36911c20f91a09fc8dde3ad47f5fd4313cc64e9e4a1cd86c72c571b035d22b17a0ed03a20d3c954ce26ec916ae7d893a877ea8abc5a7838fbb0cdc26962826c39160b779e5a4c33e0d970df33785cc38bedc79c72e7a3b500cc6352923df26a6cb4372213766a6f45816aa1a439dac24777bb99d9799b1b7be1afa314704502e85f0a771faa7ffb1d6efc269268c6025b7baac35f4827018f1a42e1872ac7a110df9b1be451e721069cc3ec98216152f751dee6b321a3ad05ed92cf3fc5b5897bff8cc5657f74dd97d0aa48e57aca4177f4fc1342fa31fd1bbf038b54627f281896a312d312814745117427c17cb65075be38637319b68b189c06b5f59d454b7747c5bbc4e875eeb566b3703a465d92b5f3db6d1ae1d97fb2374dea6f9ab01664a23e06e2bcd5ebb37df214cae08f5047db596687cf19407f3d71dbad0e6c8bb9e776daab0e6bb746cd99f578d7e1e0ac267b9838e7d0cefb135663c269f8fc9b8bf88dce6704c2afa7243c9c3cb6401748dde412fd08d079f6014cc5fe1d31e77c36243cc9fb3a08cc4905caff4aa2ce118e6df61c251db17744e95c94c4512f20bf6bdcf9ca21858edb2fc21c07231e0a8f61ea269eb6e8ed378b1b8a8a898cb0ff41a3c7e9cc345351125fcb37e3c12abe884221b451f1e5ad3f8ae5f71f3cdffa3241c23e03f790c38b2a0051f0bc1988556c66304c316929e92e4315a9a199618d558a351ddfa65e9e37888e2079534613940995481c9037b9085cb309077b2924a15821ff322786dbdce65ab3533401a0dd2030ac7c818a0864c57f29ce67cb44836c55c6f5faa1efc64a9670ab42f02b55c41fcc756c11a3d7698f4db7c7b3d23c5c8c778775666e009c8551b72f4083a35faef23fbd22175e9270899ea2142163d1d1350f6f9eb5b90f5080d73fae5c57a68d144c1257b1a1a3dfb648b1df40a715ea65a4454d7758b0eba09414e8ee76ffb86f9033e96d3547cf4191269798892fe368f6e6c0393bb1ce91ff4fb82b5437028c8a9405fd99be3a41b523600ff954e68c2b4e3bd068795c6317152adace5b769faa3adb3abfc6932bdc3d6638ff567215e374e8f3bbcbe7e1b4e6101bf853470e72dba0d41205df0d41d76945b895be31ee16b7f7a85230d07af76bd249a2dcfc02a82aa1badd3542f3e5ce6a205c8e9e5d494f8fe4efd2827e3313a6191f55543b631e8f7dd0eb09da5e6fd442d738b4b0716f7ad4648d610065f7f964018d857091b56f77243199f028a5d7b8bb79dfe55f314b3cdd5d2029c9fdb88c456c153989191878814042b4c228ff1dfeb8184e43a0ed9f84f4335b95109242509e2659fc6a38158d8243ebc1be88cac68277bf7a19260fbdf980f13d1f3451d8a69bea35f17178811e7896ba6d678945d402c0b80acb1b7952431e19efbae9057e382f964e2f420507461e536ee193d39254b340ba39b15371e36434c62fdaab0a5caf9ba019812ad83ae9d28e9885a6589e378a16ae6c69ec8371df5b083fc0d8fb7221fd6e68dddc95a605923e9e3ebcb52ef3001cdf7b79e01bf190ec6c58e82dc2e267b0a6ae95cd64ed89cbc5ba3dfba4425a390fa52d7387dfd3371ed0cb2b21a6c7281af3e44c16d6c735d468d9efb23c9e91e3265f733f06ffeb44640bb884daadec032e940b7e217527267f92e65845fd0dfbfc278071b0d5dd71e9459778b32d078e8451a04b520397a095b208970c47b68ccb1141e57d59bce8906f7cc401187343617071b86c6f68afc52222b04b30320d928aa06c2bdbb2638bf7280892fc9e410dcd3587a3feb8c9b325620e24bb64583cdf1331350ad98cf65a44b400452c720a2b68135f8e663c777162e1e201c2f5ee4be24696e20419313815238c74a8d2144c4c43e6c711e9de806fdeaf1782a0fe303058887fb4744c8e9971d2511773706292f9ac1d3299dd312493d377a35f2cce30738138a1c1fb9ee3e78410b0bafc346197bd9d86bd93a3fa731ceef8622474e2663024c88c8e5d39a931ccadb915dc14b6d9dfcf3c6d5fb49721909e1547f821f5bae06de12dbaa0e4be4ae750498d6b0d10fc8c3bf3a2165a52bc65e782f3738b2034c3420f4cf8401227ef6b8d6964823174979840772a8176a63940dbc41296b706ef479f2ff1147343d6182d75c1b50c26847377513d29018d42479f2bc291764720b8105b25c6ff6b80be2fb5e03f45fe53b8f22b36589b9c9381f072a33d288187b90539a2b380b3669490c81af8318b8d83da15a53447eef91da11c42335b8da4e8d1bde2d28478d1272c542def89c6685864e5fb50bcfaf6d2214f225534b4dd6b8ddc99f0a97846dbd59aa60b8d8cd045083b1b03b94f86d51dd6f0fdc949898be005326e34ca339da8b26f6a1517224ddc3c4405e9304311b4dd2152bebe1b22d3b6baa26d92f45a9e9ef1072bede594053d489e7b6d5b7ca69832db9cc9a3b32366c3ff69764fdfd03d43e0d995d0be7b25b9e4a271c89e256881d9c9ac3387fb5e00cbde4d08e433326b184a2fcc203a0ed87d3eedb18ed509836e67112b606dbd647ce265095999601c2b282f9b32678558adf6dfd8fac1ffc0bea4ca5aa83a43928714feb5e0810f470984ba751303e4232546e36edfd7daf301dd8dc89816465098339ebd4a29ab7ee804122cfc7f4961e160cc257a689a11dc8a0d8961b52bf958807284d0b50690f1429f21250731be32d5d3e5746c4a62fccfcfaa28a5b50e11a704dc7f63de64698852eba8ecd28c2c098216f91640c081a148ca1984051d35279bc205ae012bb47ee78ed5129fd228c0f6c8b0f2577096f6a2ac1b1461237bfe97aa171fd8789f9822b889f5ca3d06a340d3bc809242a3504f1a8ab4b97f06a93783e71ff2276ca07e6627e7ea6b2c0ce3b7dfd425d16729bc92aa724aa6c6d41fbd410a2abe6673308affdb25a44b12d8f314b94f61cf159c3f391c2cc6426483922943009865faf8bd93d9728ab1787a50c9cf6f537ebb40ae923177586567da36e0de4fcfa8bdb96f7beca3ed0c6a262c92ef4281129192ba0349a5a88ef729caa7fe41768819ebc775703b5fac212dd9f21cfd82ce2e673e50eb1c02f8eea62f06787ccecd1d6f9976b498c4e9c57d809aeb74b34456f9575bfd26bbd606ed9d48dd322f4fb9ecf4a96d9c90c535bceca3e690913c3f633bd56eb5760c6f1cd17d838f8fe01a1886ece1d3c2912e6635ca312b2318445f9c75d58b1cbadaacab7d34ceced3bbabae5fef1cea76c51666b3b412b1d1e1dfa84fb38d36f4c3d59395598f8ad5c12b4e6a56c195453c82808f5f86c0c1017b32c6b08c1be1370dca306d755d571278cf86acf8bc118b8bf0f7980a715f22d56e1b9f26a3d93333e4965f17b5ff8410cd23a1efb90a0e660e5ae8a8d07f569015e05d3c64023c4240b080b95abbc5f20f01b499bdda6228f9662345326e0bc5a1bf56f0e11106f9e4a8674b6c9944256bc921342b0e5a8dece4cebfb280304ea5ad03e1ab27832a5657d9ce7918e5a1a03dd0564d8db2882f5259f533d8b9b0de6fe1bdd31feaf9a6ff530c55564e10d360f1096b3e86026542ff47c04a9197c0787f2e3d0faa47891047bd60b45cd542bb7f2a225bcb287992dacb031fd4eade55c628cbf018f6f7fbf02fb2182dd2d934b2c612801c040daf898e907e409cb4094fd43db8b6529ce091839dd5190710cf23de3de514bbfbfaf500bb1d4a71ea87f284f46d258c42ed588c70efe6f0b9acb992cc198b6b24389985583bccfd7c0dda8bca3ffe87bdf8e4b1f7dc29bef4537ff5da07ef400b332793fab5b22c9838e833748ccaf2866d6ad26ef029954c8a5798208025861109313f693d904e0cf86a4a1097ce8b2a095af985f59208564fcce90475f9dd043b49521bde18daf9d52f47aa195bce81ac7f5aa50e36c1bcc55a5c8da51d2992878448a31f31fe43bd1b166b5eefa29e4898f0cce14a747436b6546103e9332bc1d71deab536e46b48e8c979fbc5011dc939ef637ac05b046d9ad5f77b8e160a462f57e20973865bf6a5dbdbc87ddf396d9a69428a735b0cb8d9792cf9320e30823f7ca1cc269f9bf160481dff58384a7048a3ea9be3953b7effdf7b312d40a9b5399c60a3dd9af55360c773da8aa741e86be8bebd753abc1e7171e3558f6e36906a98c005947665241f4735d7423af2515176692a9a744700f062841ea64f28e6d7b49037a1564b436eefee8cd9403bb7d3684bc8ac7be415dfe2a6e87d893573e4a29074261d6d69c3815377e5c3e0f85359eb827597e52e62e9506c5aa834155264d8bbce2281f4f97a26f104fa630c8d9bf4ce0ad132ba0be79e4cd67b4c53876ed0140b5623ff877d84026839c775bc14756308d247a2778a4a3830c3abd685a0302359149cc10782455d00e47bace52a4c6723a141675c9bfe7033b8392d7d9ba956e17e12ad9f016c8663829835c1a56109e38a8c43224aa71cc3b31e831581c20342d75ef3f8a4f6ef6f629c57735e018e782e464bac09b7f78bd7ace8d031dbb014b6725a4c405250d4ed24b3fdfe2d4f3e86ab15facb5cfc944d52df3cc2d91e36670192465fa0bcb731a64d0099ca36822de10f0cd230f8c2ed713390a4526bb79d66197d3fd2df31487a465b463cf0db8c1b15be8f7c1122682e2b1fa899ba0246e141c53ac6278a899aa89859f24e32f246a35c649c947ee0b0c63957a2ebeccad3fbfc2d7dfd4419b278839b3d41fc3be5fd44cf9bbf5d54fdb25e646176228bc2375f5e52dcc5c1cf78431cc013ecb0cca5b64aa64501b906e4946881f010092e4efa13b181d0a7905a1d7ebeddff5a225f4b99fdd69f46807af84ffbede1aa233494c9a7a480bca4b136326639845ad065b98b3861ac2b1a21809ab9c4126010042560b39f8b364c84d238135336c6b9eb0a4a0be369303a0f1002e8d2662c00ecf640935574144ec50b999e2fc00d0387692fefe88a060962e4c858cb576123e00000000000000000100af97314a7a8065e0db6d7d6e7f2d2d2fc03cf7081d7221d09049790785c378a0c617f8a730b18b3a57942c68c952340367102f019a9e5f828934bc1b5376b71d0000 +04000000322630b4fdf11c9a90948ed2f61649ccb8188710d6197c26dad20738182e3decab146771838b4333cde961e688b1cbac02c5e272fbdb99041935158113b3cd27abe8d847e61f0129b7d54b3e270d5020eaf6f02d7080b98fe2a7d4d377cfad9b0a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000020000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002a00000000000000000000000000000001029c345abaa0db8d41632efac9e6a9ad18a0ce809a495d563bb26151c0f756188e243c7dda047e98a29a09c575b4937fa79af442dbfe0f5f81b2c7f4afafc9e521bf9840e6eb44b45fb566579a2e874b433f8e7ec1ef138e72c14d1ffb5757418d06f75a6db8d427b9683282ce55cd370f923002bd136e927a420b221261e1632920c34286f17b5cf317525e1f0ac3a3961fa8982730613bef476cd6d4844708060853adeb8e3140698afe5146202781777525251c979616cd134a16cf78b2ee2a49de5a7f6b117f2de39352eb50df23f155f2830c740660010ca156be0a7540c17c891b5ee0fef65291ca3b48eebc40684d8db71eed3ca29544b13c2f501302afc3c48e0b0d35efc3b751f48a079093a87b9f055302fd32bf5c16cc8b662d4cc666ce726382a9a829affda9a2aad60a4ac78f5815e84d101d5e3db4fe2e6ce821345a948aa49adfbfdb96f669c665c22dda7d16540dfd6b7b979ec32fe943cc1503464e5b93523845ab7762cd12b47003bf99884f96c4c6ce18b24504dc9691c82c336435abb50bb8c3289a740ed06b305adfba5adc9007dd3afcbce412df55c8a2161b29e615894d3de76e61417778586fd9954184aae6ca5fa01af025e99ff3af80e64eb8fb369d46dd9a42457bdab297eb29dd9d59f09d1b88b1ca3e391ce55b0e8e7e3f5fbd30e26b59a4643b83addbba257a589c36a931a27a73d5b76b0bed4d0fe61810b5d4f17c4017b25f7d5b38bda5633d4ea3d75b53cc5b31a742d38a515629da18d9a8a3cdb3a1d79d808e3fdc078b7d48892923210a35abe5d7190c8c6a238e06e64f7ff1602e52e90d7b1d8bb8b377f93c8d6d8006a3abe50f72c7b80b1ccf0f6ad447e3dba76494fb55773c54295e31d56471845b5eefe87a6571e7f75309e5ea9377e72e2a4c4efb363906fd0f85c9a881d1192dc79375b1b9cb868a02ce9866217f81f2ffed01a78e56a985d843cd394a4f8f9b9ec9a7520be747442f4674587c038fa0b4bc184ff9cc511017b9f80930c2639de7327b37a22c55a830fca769b04d934a6fddf4d9f64ab1e00134daba0cabb754155661c1c2de7780ccda708f563f5ff32bcd14fc5e4e561f39de677d6e4cb51bade0f634c02e5787653e938aaf01620c82ddefef239da67537dbcf437f6f540ae0a5abe08272a8ae08d0d70866b7e25c3bce9ee1bf54225b4752d891debd4694b26f3240f6520710beca1ebf72a43dd17e4de10e84bfa3ba3a6fc05deb59f633b932ed06ca0ecdc8d6c58c00eedaa5387e78960e114db854081ea2b1dbbf88ec68c3d4788f95c929bd8b29b3cd3a08c63134f73b21df341b04b3d9804304d1b1b2e9880efec5a0c0c31288418faeb0babcc0ed56770f60460da9b0df6bce466d9595a53657485ca6e4ced47a239d8909114205915cb199523c75a26c523f026f67096f1cf1523bc64f1b2b0d8217cec70e550225386b56e1707d18ab37814e3382847ac06191dd88668d2b94586ee2350b7dc6151212fb2e4e87a5cdde864f492d947c44d748f7d775cca95351fd15b77b17edcea7c09a5fe51b4ed01b75fc5742080a7c462e0128523cce13087a836d181486e62a500f7dc8a9f8ed895df416976d1f4cb41c51f4c78259456239bcdead19623c4355e3f892d8afaf28d96ca7d48ab89fa9bf4db1da96341cf10dcac6eb79489c3932951b618fa71a0f45d5a39d9fd049a4cad6bfd995a676e4f7a4bd579b3065c7ca89ba5718d58fac55e1da9f8b60ee91ffdce1d35fb2e47d113bf963ccaffa147437d24ad5fded1f1435102ac96629a5bf61e2667a9905b3332c99ea835ab86ee31a66a132dae4e58ec1bb018d65afc96361cbbd98767bf7d7fce80caffd5c4e6ce5d2045e301ce4697424935cd8ac92e97f20ed64cf86507c4ada48eaba1d9cfe3f42acb5a0d8def87376d02f21eae3922892cea624e99a01995d88802a72282f1ae16670c675124a97514a6feba1eefeefee68b0f2768e6fa371544b0d23b7546fadcf93b3495ac978e65c262d6dec2be398924336feb4ea5559e363fb2b088410898726894a0ab22d493631f4aa43edef49ad6d983412d5c00aa684809855d05f4a5bea06356f4227a3721811cfd5843d589cca480ac590186f646629fda7630770ba650540328e9c9a827ee5da2a7521769bfe6c45a821cdd0d6ea11ac0e2d00910395a735f2251c7c7b7e423dd94199a26adbae9fe14bf95d7546bc935668e80ba0ed36cd148384f5af8b00d02485e94a15ed34aa6afbc731b936c740adf9f19c99f68f2c47792bdf054e2fce8232dfdbe05b9bdf8784ad63970af1c8588efc542ce1a7982d6615cd9766279f08aa6408fc2f65e62c2882d81b4393c22b8718c14c28f86b1c03c592b429574f28a07fd158af39671b076904fbe7a922d7fefd60990e60f19317acbbb28d21b8b06ecfda033c9d90b0260000000000fde01cc120c56a58829d74973ed5cbfa9317198f82927b0df7db836f137f654540a9187cea447564753d8ed5a2639512ffb6c4d30034bdf75c272e13423d33d83c538d62b4b4b973735c5c578982087b2a8caed093db91b167afe275ced518139c5980fd7f6f6c4da4eb8722139b37e0a129d02ead7f46bd44b03db548bd94b148482b017ee12f9bbb8d1a5a8d15d448aa24eef9247142c5276b2d2c112a8b9880a58080ead42cce91071058486a9cf30c04aa396640777054e7da3cd6c0ac8592d70aa87a51d5fd7d3bf736a1c414d6c7b0c8f92fea8500fc8975cdea770b633b03b3f45be2123c400e8e0e3bf875fe02195c1bcd612fccf5fa96eb2725366c45c92b9bca0d6e7e11fc96ee65dcd6ac53669ab51cdb32837ee32d6a74750535c13ba03ae38ab9610b1d59a042e8dba64a0fd70c2c4ac9513e6b8c924b929a7dc7c8a38e0af732c44237804e6bc6d8d8f6d5af361858e4ee63c667a67ed7f9c44d478f625b9f2b5dd52dd2058e83d377bdb0d82f3a8cfd036d0cdf7b9bf5b9e57551ae78902398f7325b4d7a2f549aca0e4ed52087496023447de4b5c277bf41bfc7128a03a46a17d7c1864c1ebee7573e2fbbff0f5dac1900e3d512e15ac6b2699626269db282a27177115fa6e20d0db7015d3b0e2971d1fd8101325579f6b62c47bb489ce86fb8feb068381c8a9711efe8b01877bb524a76b26c189782ad0bdbb3aef8e5185351ac5a8fb66966ec0727f2bf745611c80fe0cda2ea32565f83a04f8242be006764e7f2b5a400a843ba048faf81f345c970955b3343e5a72fa258c7a3855c2602dfa135c6443a5e54cb990df553cd1bacd49915cf6787c3a0eb1ab8931303950c8420c9c4ea1f686e4f479aa72d9ad01610ba727a777d6e3e0ffdd12d287f3367165b4093d172c519092746c918b3793a655499ad9bd4260aea916a2b3acc18d2115990550e2b5794cdb28acd9edf8bc5a6eb5c46fdf09ad9de5eb635fc0ac1e1ada7e80b212b462de1cfaf805a6469f1b0e6959c7f1eb9666cca642c155dfdcff951a43af1cd8a0dd94bd9bd1ccc5b10250aad62910ee17846f1b48f24cd7e391384a25a90054f53588af81b1253ce8697d21e7df4de6c84e44520aa1545d4d685567dff2a1c2dbb7d5cd80f21eaed0b8566c80c24b8fd5057f93d3d8e79058932a0206ee3e40b82bc0e36d011d2ea2b91dcee1b524311d5dec2b59925ea6c2fd07d0443639a75c188e03d8d074e999fe32c29fd37d07c2d502ca7376781dac4c98fdf0e38ff153a185153b2b39f1b99671618e85b1d74a8560b0739aee11dad20e03e142f041d8bf38926e12f4987091fe15f74d6154b7bc158322302f911b714b3c25ef8e45ef6f036799c444f8c7339288d8bf286fb5cd4617e130a77e2a30ac777a9abbb97a5977014f28637ec32b33b78ba6a5374ea27fa1db640d1bc9afe31f640977035464c9f14fdba8456f119aa8af05b6b5a37229148b6827ad9236d311b9d5b5a8fcdb5035854f0f8735be6e91d0c908dfbe1a5af1c35c63b10c566ced85df768ff9579fd1ac46c4977b579ae9d03e8e6770253275f27f352abe395da2e3c39228e60c5db4fb1ba929f885cb58fcc00369564d8986a293076aefd870b3503c173716291a143968f71ac284ab6db88e3a5767ec264993d009526b0d1baf34d8fc7030d538ae1764169ca0515a43510e89557ea2fd40a18756c13af1aeabdade26b95016e854b06c4ea0a305f0e62e817053eb6e52c032c112f019f17940c0bbc433f801ca34521b5e64188a2d4e3de30d663bde925d8258dad12cba35c812dd5f2b42dbd603d87a0e4bc70f533c32b5843564036a080975317ef90ff3cfdeaa6d23a287e6458ca76bb770722f0ef95e9074fd99112eba67064daf5fbd4796003cce2f9edd21b07b8ac50cd399399af37132bcd8faa4d8e82dd67e1fb608d9d72091c67e7e05d14a8b79a1891c4bea36f12583e3bd7b40e95180c9850fd48e640512a13c3d0b0072abd59365812c08f93261f08fd07c703efc44ebbd5c2fd8aa516e6d9a7e42c5b251695c0e47897f1db9a8d3b65232284bbf52d74f782f21f3221d406b47b45778cdad5463b4dc5a9c1c656d08e86afb6a02f0912474d0e1a03fdde1d16f7dfe3c25560a52740b07409bb1370be50843ff5317cfb99053bcf96e3fc767ca817bc307fa2c26e1a55ba09389ec04136ef9eec5932d11fbf50b65d5235b2613f5941308357dfb4e54086aa786b3b76858914d3f9ca29ef1c02ba94456d499a49927e784456f4e2a6aed6aa3b0c66f3e98b343381e244466393a7f585c24fb45e935c1d162eb84b31c191c130ccba656396a2e25c7c4da29174ccc302fec0974cef6eed4fd4ea6e108d3cd4aebdc34f52aba884c8f2a140f24a612dfed664de5551f768641735cfb7f6b85f34eb7bfa190c31d130280741a00e3c27e2c3822f1af38522ce93c3dc5395a035f94e44ab5754083e34eb880f8e0c6458b30c3505dad668bf4a13d4e220cd9a82b9015a00f10f1e331d2be1cf64341308eb5bda335bd029b35078b4715216191fce25b43e78531798cfcf8462cc8c7046c93050ad6ec9803649687953fe06b2b2f5a438d672d02070d27bd46a6eb483edf0746bef960c2bc9857b79b64a3e6f3ea0b8cdb7482a3a2f379512f3bbda84bd35a61455f769e8680972dbaa717941bf33eee171f14d34185389997dfc730d16f2ce314943fed4e9c2a0dc0acd23356742c6a77284da171fd92734e48cc103d2f66f76da8177d3ac252ad9b080719583313c8341640c09f065d2d3bde9080031e4a70e756d56fa2c7177d80160365ad792619e6b5ece032fe05c8ebfe93a4c9ed55d9fe51cbeecc1971903ecd8166b40e249ffb5f9010a25236978ca79566f9a1d6b27c2802a70e3426d0b644ae277ac8b735ff978e52e39e366db67f93f52c6214326a7a7f273d3133606d8fcb3281d40010dc64a7f21fef9ed41c9444746312ca414b091d09f349430655a88f786efed025ff6ee6437d8990a27754d2bfd556e8f83a54ef396084dbeec6e1542ece7d03b81fb107331d531f6a24a6de9a7fcd5995bf867d06ba1e775c2e1807388406d654196a59a383a8ec53b9af7fa32f46316bb447f7238cfc92d92bdbfcc0752812831f5921c2b49f7757037f18c298f3d662c4aca38de87b7283cd5a87b031071f366f897211c25d70f6e2ea022bbdb33017aaafa188bca3fe4e8bfdcdf04320e71ee7ef2842ffc2fd0c8a10ab1e826c3728a4071cdcd8f0811a59681c5758a5968572dc3270aeab31aecdf8da8cb9d757b84ff86caf6303000960e4c1cb7099eeaf526296d30a0a30df359864f876ec8d886c55cd8498ecb1cc6c98870f73ca9b82087d5bc1818e1cf51acbe64f3b01e15869b8d1f74504b473d6e374f569ce97ee0a3e9360b585e3f438aef2a63e2d99d1863df68f49468dea5c991938ec4be31197951e734c4c935cab7c7a907779c71c17beef84d0856dbeaf80d3c0a44a72fc1ff93022acd05e7e8dc2b90e29f3847a24f214e976fbb9f7a58fd3cc3ba071914a828f902d9698ceadaf4f2f7276748722bd580d4372f933ef0321ef43da749857e340a2225042d787f585c548fe51b72f544b4b1f29c284cbb29998402808464c5851139e5accfbe9b1e964de5bc447876e4e41518fc520ceec18d4ca948c0c8af17ae3fb8bb54cbc59ac08dcc7d3c70315bd56c6c66a4e9eeb472e9a2187648e4993c1512bdac6a1adbae37c80ce7fbe5304f1cf1ad35bb42172b9c92bde6747056ad0c02eb3b31b3917eaa0102408b16e2291bcfd35f498a002e74d2d796a82971f73b84fac2b7680c2c9a636fc632c93b5dcc595b1b03a856932043ef81fe75935134b3ce2397d3b3b6619ef9e47a300068f782f1271e678938dd0b41466f81dbc6059032f644e0279a3f1d913b144734718a9fa2f787ce7d8aec4df0dee9adf47203cd9bd5f3a690187a2942b303272377717cf38a69a55a69a18591b5a699cb772185744c7593f093883f9b1c4f3183f52bc63ce2ddd69743a50c1c2a7959d6053147555307992db51c204fb27e00d9f7b6074230b950e385cff37db692f5fa43342616601a213051d464490b6d81bffb56186e056a30df520d61a76d9cd0dc4d20850a8de34feca911464ae3fdeb385ed549391a25295a342f83e0b8efdba83f3a27b8b4c9f4b49fc4fa6abd915acd82a5e38c489ed709a084081918897d993d264dd0ebb25fc4540c66497f15921bcf566c046fd3a3053d7c6523adafeb3a621d4d020eb92da960caba57418f3345a43b9d477921c0f7f5c398f42c1b9a745f0f23fa7e15cf696aeffe12da099b1b4c378e092ebbd8d270aeb7bd45c7af6d113a7d6364d8b2d9eec79b2ac985ce63d77e2585b2e391f4450b3ebc6f3b83355b2d0f1ce2ad049801347d54a8a1dcd78be34c88d06c5b93fe9b48910b20b6275c25f3722d899ed02616183b5e198373abad2dd4812a40dc7380d4272cc1726e30303d1624af55d9b3bfb157426eb972b1a96b2b123d317b12eba4c1b5f510417524e40c9e2a953c9e21868f39a48db3092e6a09e037c9f036503cfb4072c24eda374f30dc3a3b00f97f1cdf7f89abd54fe508dfce4221113467d8ddaf4ba9c9b5160b2307ad3ea947dbfc17eb843a8dbf9b5bde3bda8cc865db0c05fbb32d553c0d65a30d43b5ec206a7fafe48b13d695507847d6332765d64280634f3373e7fc3fa4e6c3d816bd8a4344e61d9f58d1c6ab67f31d8c0fda8c6b37fb2ca4ac74f62d6c67e504ae5b124539342abf52fdaa961bac3c7f552dfb06cc0d85525031a6252b0413ef68c1cf150aa3eced322bbb33439363337c07be11ad4a36d8381b2e0b0ecf799a401f06c6c2859176aff38444a309513bff4980663c78a4b9e758e810433b37ad3b7581fd80dba58aa3afd19d64e991fffee91a287b3b807ead13352c38d3e8dc5ff4953ce42c842420716dbe82a76aeee1b62a3d841baa55e72a781b34d0d1086e42858f7da014dbad9e86b22766b659305244af696c86a3013d2a1949617a179de482aab0365c2e3362006b5bd0d81be983c456a068e2634e02e43e975b430382b96c0410c6198c34877fb1a1faa4a9a8510e9a09d8e00d3456243e7fff36c6aa4cb60975780d3ed12737c0e7a537924f93530ff973663bc1764222ba55cd44306572dd603eb3343be33a72f05e8c53dbc8920ef57850fba4899319e2eecbed7460dc5ccfe087774c689cbb7d540ff23a36165f395af1fcf2e94e12d5099f02e7658899399000ff1fc5c84b22637f294952504f1116cc48de50353f0b856656cf1ff4515eab30c4d9fadfa91f7f008e787068bee48f8b597a7f000a8d09bc914635a2fe3f4671b56a559a4a48dd989b02e0e8f28020dc1e76c5162f28e521e544b9b2460ba9e544a3f7e12934609b32b91a123b6c40f60d5c9d4608dfddcf52212818ffd09600764b6a7e5084db6b923c41535fc47fa26b19842021e6abb311ee53143c8bbfa6cabc9675fc29e6cf34e89313e33c63d6df94ff0a2b97d77ccbadf38b8f9c7329a6c18764b810a4d851c472d7d3030c4e5a32173c302d2eb89fb34788dea064af46d5ee0dd1d65b90046cac5ece167a77075af13d13ee566a8d6a706b71e35261e2b05246e1e950adde01849c45c649b1fe9ee8d03dd2370af456723e26e44f381102f771cadf4ad44245149a6f606e2b756f792d1c0e3709160593a4577ce355b4663f4a66c18866bcb8a76b264ef83046b4644a3cd5e9e86f53426f802114a9905630bb3cf1374dafb0249ffbf836c57fc9ab570deb5645998cf600d10333348c26692ae0b8410e7cac538d1e740f7dd7cc0da50ebf0e6c9a7df382f68d2de4a5cc8e56722ec6b0a05c20352568ed533f53726b2017279ff2499d9deb1cfb02c8270cd5bba58584724e63072ccde9faeda01d6b0d541ef95b46b4a95a1d7c70a2f5ff9680c0972ddbfb31f59d3a38ba77e5b2a216d1a3689eb630b69d71813ed03667880527d785d606f5ef36bfd1aedf44db560d761dbf77468807d810a5603eba4ce5503f5aa067db36621722fa6b45a6ca0e35d8a8cccbd892ce66e76f521999a449dd626855b46409bd75c030dd51529b1d060c1d003f9b0108bb14a525a188ba446f657e296b8833a2b69c5c2319812191273c30d225cdc562d26d6ffd2e126a5f277d88aa36fc5f8846d797143fbfc27626faaebfe832fc1892f6485cd4f74ae56d5358f8d4edf655c015ce6b1a4b0bbb02230a01ddcdd45f33ac8365e887004bec2ed350bdaf41a56de81e141ca5519105fea386b35256aa7e62f0a23a4d4d8e046de03c43e62187f43703f4a5f0a6c22d65ba0021fedc825848ad87ff4d726dafe0de3bacd71f701d714e219c3ba8430b9580846a63156a15fb6de0551d17a95228610bc4cf70387179fa3301bbdff32b993d6a38388a89eaf3a3bb472fa9179d89a74dac83a7097c45d43c272b46243ff7945f4a20a923a37a939d0b1f0594b319d16060ff2006a5233c05514982bb26daafe5f9eb65f2af38e7c2244e29f7755151ed4fb780668a876682d7853e82273b336248463b68ead647ad69f88761bb085d3084c017abca3abad908f28e4d06239ae0b0fd09833808a1545bbd066bf48335eb4d6d731e1fb9fc50113f157b04167ec6b02e949cd9ec30ff11412399a01a02bea1cfd18d6b0f6fa4c54c3cff2e47fd634846310a0cbb11023c3631fefe2407ce160c40ad17f92115a07fc726034a65c847b7010d1b3bec975c48e4eb38d730c92225b698fbafead2b4e0279233741fd86c0544f1475be9e4a7d32dd4ca19db4d6b942004383297ad367ec1f41384b182069cc459ea4e934b7e1da7444d71ffe95611ec9ccff0d272395da0ee3c92660d1a48fb5cd7b0768627c01ac9b3bc4fa11d832d9f6a961048f304dc430b4382f68cada3e267ea0c88924659eae7687738e6b3307e39e96ffecb61fae10cad0ca19a407d7e9bc0b57dba918d5d88520fb7b718ef759294b67d33c5e19d37b156a2726a7e937c935aa3003e0743b0062730d997517249c6a3ffc11efb670da2d23e130f14182abef9260cb2aa8e14fd2ae0c8e985d4659415383b3b796e2d97c5cad452e40952716ed197ff64a1508c000767855fd3c7391901eacf1b340c6f1a708de934a58b5c800fdaab84037edc8d662d2b2d5e1985968e713410a4324811818f932ef70c2ccf9c9d91088e75a423d1b30d58945ebf3e45ad2e2ed31022f14d777f91ac6898ba77f36595916a948c6d3080719b1a7012d86aa6ff1604c1a95475631acc4944ebe16a55dfdc870e73f758adc1da4028748bf5361b0f3a53687b08dc076675be575d056c867cd658c1232a6d24aa91aad4fb0f5818da1bb88f2ed75a1b3381676edfc4dfd0b259e4f097a8e3f2705af18d7024b2a9e23f205bcb4f1a067dcbccecda220754ba3be3e63c3c18c4b11be09ac7f4090f7e3f154e5d734e986ab4bb7678daf7d82b794022c717fdc5f4d0047c4bcfd61ce1001fb47da486fa6c57795efb37cfb8c896083c2da277ababf3acaf6ea5118fd00a3efdf57e588bcc7f1961e525e149043ff5d4c112d2698dc7ce1150d70c3da53af720de98dbe8c816e8869f31bffdb60af2e7b8fd06e88246bec5c0ca87dad828700acd2b7a91b3dd487be06521e325513812c016170031e96fcd3c91ea0c141e4cbc81fd2d1aa41bb1cd13977237fc883c1d45441863d1ea8e0dfa34bd509e02009c56c389cfc47f5192e1d724a4152eca0110e07acb4dfd7539f22d88fd91253e03d0a4f7227899d34addd752cfa0b58e9c2976d9f87f00e9ef22ca00ea4b0f16f2d0c9140f0c21fdb78976b830d669bdb09df88033f2fe3878132877a1fe264d263eb816ccf121b640ac15209aa35cc5d5c40936903a7d691cac5facf8fc2fc212f57bbf6a3434686bbbdf604e3d0dc595f2a6f96d15a785acab6c27b236354e0dd1e80bedf1a55754d7bf743f677d581b2637f3c9e9cb5fc96af01be6631c1d4e8d8f47def0b0656593ad1281ea5a5c1cee3a31201f5a0c477891ec692e1f836ca2193ad7cfa51d8d71016b82663054b3b3ecf04fd08b5cfb54704ce5700156de46e5fa652b0bbbab5e93ab12590977d63f81454ed3711b0eef13879ce32c32cd8b65b7245f1c62aa70d0e2d73d3284d4c12a70d57e156308a425162c2c3ade84455333d32bd5501f91c4d95e7f2068392ae6762df0983d78cd2cf8e76208f2d694458636bfdd6815d952b9dc75befaad7f5bb4a67f5d65f75b6ef86b6f1f912c7b3417df1e3afeffe4592b2a7ebbb5234ee5daf5db4f9076b66da0b1f20d522f7f422e650c3b5df0fe4fc10fadd0079d6dc5a594c46ae68c597655b24c138dc972c9bb3901f0b5da2562bd7e5bac6fd7a5880a951e56d61b3a67ed55032020b522fea46acefb3d16b2abef644ff435e925535d893266e6b0065088452a156c43c741f4ccb8ed1a48db1d980c3bcc85ed56d7458b8329510572a202ba5501397acb8d85dc9d001661ad8dfaa9dd3c0e3dca1bc0b5fa97e2d9610ab208c61cbc6ac336e745cdb776645ec4f1522b7378677ffc558c01a0d72ebf2a6a8ff50e64c4a4e701c92cf21f6c4d4f252635fc87af0a94f886ca670d92059bf79ef81a53145b4accfc1bd9504ba0ba1d9a8c2bbb2e23e546ef2fe499a507a47252133c1bd71da7aa0ecdde8868b355297e36fc50f9d42baaddfe4c151620e9563fbe37b3a68509c1f9a4b9f48c4c41087705e5f6ffe7e306270c2a3e6b629b8d4ac834678184bb60cb61aec9ceaf3c6cf88f895efe2c5305bbf889caf852e0d0909e319fd3bf338c5ed902435ce7653ebd840f7b9086d20470d3406465a79aa607cd1a8b43669c9a8e9eb4fcf783441bacc4dfd0c8a58c7cb1d54a3a28e60918c4ff260a516b01f077f8e314e25af4279d8355c9b565e2a5944585c8883b79f5cef600385accf2b89f54c4e5fd05e769f6cb346e8f3836cc1377741c0ce5cc4d45e8165cd8e79939979c8db624797e557c02a31314fdbb776a4f7472e7dff93d27fb046f68a5e556a8c60e9be069f1702e5ae21c9557a2af40aa948b3a1704aacce23503f277851d3d4af56b14bc3c5b87e65cc1db5e121670bcffa7a9c26ed6501b18862f0853baeb914c0f9b787a8e831a9f3bc00a405afd311404bd64a21e59691902eecfd7a9b7750bd92fc7573b015da4430d3e6d4cdce9aa57ffc70bc650980dd9108bac9968333da0a260480d29d3540b946433c97f9d92f496850afa75710c6b9cb47f198715ce94c75ff301d56ab0ea07e41183e01af57f364adcf080b01212267fe78c8bd5999e0a8e4556299e74d60ddade610a58f48d00050bc1649a8329098198e4f731affffdafe3b5d525eedd5f9f3056c372548e9422f0a14e738f9866828f6fe4e531091e7a0bc436bf1e2906ff3204f7b36d8873a622594d08a6615a3bd2b8f1d04834aa27256e622f241fb5c7b040613637c6f49099b3091ebb5d68ab674ae4dee73744f9a444b42a63b7ee4fd36eead3d6cb03b3123a90ae9a40f4302912e93216b18737b442f0f79bd563405c88884bd291118844d12f8b1047dc5fa5893ede021615d98c32611f7d65951a992ff0d5cb4922036353f0e13bd9a9090df896ba49a09fef27a67f403a1b66cd2ab1184892395a8e970d93cf84b68714fa3f6f8d24fab8dddd82eb7c8cd021273ebf8ffd8e1743eab5200aeaa3157eaa2a6eae9f557ad129abfbdb35fd16d81f1649c64d95abe04545391e291389e9bfb54491468a834c93d4ee022d4db0fa16b83980139d24286f1f972556aa47a2972c1746fcd77750094761f877457c854a810969b2718e64ccbc845a13232e24929e0fff34489c9d82568a40c270bc686791af2d73707c31e40ca17a569e98ea5c46b2442d06230860d296ef5959f39435064197172d19a09ff92d91d623a8d1478e255f2b956ccd669920f37314876e4625ac5fda2091a581f7761f7b2227e9acce89f34fef84b79328700e6d7aa3908f9ff2342854be483a0f9b350daab37f7a336af44134095902cc293409bf16903fdee335add66e3ca80c84068f8d8ade65d5df0616946af6da145da6fcdca83fa11ad948e76a7b3b420b12f2310021e3f08bcad3e6483e39b6e824e2eb66dea27a3612895e42a06483c704e8c51f7f6c8fc4b7ee0794c4df89456bf3745f33573e6a76b1189dc4dc3ad4cae34589fc4c22de09407c2a59a457abe0bd210c69b703877304d02fe4cccf55bc61eb0780ed19f5cf4fb311d97efdf63c2ff4d9f7aca9b20c09ac81c66f0226c48c372b8ac1301329960f9c079719d80c772c2f869683625bb934602693e159cbb49f0caac7820135fdee2285f927c3725b698355b479c4bdd9a72d761da0ab1fb7110201005e64bd8074af955de6afa62e26634361d9cbc61fb3dd7a91737211e4a6a38c3f1458e99e5243003665421cd126c28b4a429488d98107a50896662fa86e8f413901001d723c20fac2ef773e279d02771da88ef84b3b4cf4b2aa83d2bbaec7a5e6810da8cd51682823ae98d98735d0fc3eed6bd9d1ac4feeef2d3f32845ea0b2ac5c2c0000000000000000010027983ecc972b3e0143eb9123ff4f36229262e358b744f2a51287582908c0710cbdda2996a44427ce8b9974f2ba7db0cdacad07312001511e82cba47ecf1d7e070000 diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt index 679af809d54..4b70756984a 100644 --- a/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-3.txt @@ -1 +1 @@ -04000000b6103f2c4e4378491a0652d3d930dd1969b60767a03a445797bd1381a0720e6c2d42f9757b3fd99eb6a3c5d573a03e738681c64dbb774285b193b41bc0bc71f52f459f0ff405996cf10aa50468103e9f61b0a0c51eec2445acb63754a32c4b300a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030600008077777777d80a197700000000030000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff06537af09fa693000000000140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002b000000000000000000000000000000010213297186787a5743245a7181bf7ae843ce523d7ee5aabe5009772069ca2e448466a0542f2b3ecb4fd58059ef711bc38392114bb22abd7c770c39a00bb17d6c14f0ecb41ea7fae4f9363af3d70a1901e1e563c24ffb8385ea0af842c962b36b097871be741dd8553fccd1343dd803f76eb7c275edaea8835403b0bcbf9edb2627f1127934c6f8782a386605fab786142bfdfd98fd1a2a04f738ed59c5fda050113f3d72fa08c07081e629de9386107a5eaf07b189fa24dea7f7c4f24c95cbb5ba1751ac6afad626e98544de86e82a5f19f26a117fd0666c3a84b174f1a80fadb1eda52b1fcdeca9e2865cf8f73799496fc9824ebe859ce5c366d771abee1fa1208f4cab0a9fa0dd36a9cd437b06c55e929cca830615a8433f0bcf31bd78c0a8db0aa3945eb452415faa5c20536b28908b676efe9a6239412fc3f24376a1896f390c77319ef41e8dbeda2295267270ba275be57fa153c3e8b54507547bc2ae3cb149ccb661b841318af7049b6cacddca890b1e7e22f3621b384ccbcfb25294501cf2d08dfaa21e4d0fcc715fa3d0aa6ad782bcf430a11a4788c172b2d141d308f3c79e89c4a65b239621f00d86cc2a0e81d30f4bccc3f58c9732e8433645e397b81ae02ecca8ca78ea190d1fe01401b12ceae61730530ad3fc9d7bac9b687b8627e3aafa19992bb84f274e088eb8aea1e0e16b0ed874450ab0bc76a29685a51ea29f6227034fabe6d33fe4e079ec629ecb4480c603285c1c8fe125960c9f889ae28fd90378295a7efe49af23aea620a3231c1021c540617c89747621b10f0a1a53a7d8679f783d35a2a039cce1d91d7c82ab490624e782efa04b70feaefd4165e10ebec015dd570688d15fcd4416122567f18b31b60ee786ffc51dd5f7ed4b39eb795d85784b285d34aa8ac86392dc2537550b5f22bcbe92ab3dcb827e77f237e611ed1e46d0d204ef18cb2d468b39ce6e58bdfc53e9475e3b4e273233ce6e262bcb14b3c9fc0108ba9f2720d57d0d8476d9e005fd57e3aa6ff3a7997fa60a04d399e0135e81478adcadec21528a635d2a0341afa2ba6ce4860412241bdcb45f003f8ce5f3f5bade52697b905c409112b58bfe203b80620c2c0290e634dcff8e9cea147c10e9f4554cfead7c88a59c9010dc2dbe5962275e3cf15f58a14b993859e70b852e9e89c9ff7f75bd7e83dd051ff4f5d60feb0f311cc6080cbe09974957815bb35e4f78a420cad5374588a4bd584a953e8095811f95e1607223b907e4e52df54adc4e7ca0f0944632b4029f522261e1e42fbc900ac526e77e40de0c7f701a9b2c6539c73b5c4070be5547304c0111746ab34a0b2c9a27ac1edd32c218573e22025849ca4d7d42733cbcc95b3ff7fe16a638bbd54dab7e0badeaa4a10da81bc8fafbf566d4e0566fbe6f1ab2b2b0834d811b4f4457bc0d7c315bf80c1292ba6432c845e53553626bebd70a182c08660d506046f0e94252253206b61e61cb628258e01739e0eb586461331c67becdc720d32ffc7b45e45bbfa61448552ec9384f20c3b03c8fbd7f81fcc584855b3c4e9f9d6cc38107c0f2d5647dc1895980f07453605595f11c3c23846534dcf892f2b5b269c6ee38f4ab76a6be12144dba9fbcb6aea0b5503de5ff2790ce7e009b6fdb012e71c53a969f6bbd9ce94680a63dea33ba6001cc78d851c140fe7109efa3f1c27177fee5f7acc790a676097389faf4acbf31ac52f41167dd3646079f83c4e54c75fb4d752f51f50e718b9c473ecfae54a6d869289f6bd8c65c4e3ace494875ef3bf3cf9d7436e29359a771003653bbb03e30114417eb2997d242a887cc216290b91384fcf08f79d54582330c3887b91471df117d729a6ee9855da3138fb871cfea1e6ad6f8935b84f1abf04df162640eeccac76cdd5a87a8ca805af4fea059e097c51d3479fbaca2e570c9b60a645e968bb5f8c4116595d26198ba92d2238754632d27dac452d06c03127620dcd9804de70c112c1aeef47438ecbacdb1016c6e0c1bb18cdf98408097d94e36c033d7e8d7e3cb715400df39e71db4568b11a09140cb4c5f9563f5b84cfb8e15a2a6ee421d0e06f877a60a53d1e0864f98aad8d9b778fd0d5dbf7b1e35704a29370f61d5ea0663f9a09af636cde53ebf5fdb375c73d54733a5c693a9d345cbdce05535d2345c7821d573d031681ccf71a6bcfe7c26d5ab07344661a04bf951d435b0d4e11f172562adbc529becdbf68ecdc7813e744e893d5eeb63e66b38b34c2c7be4ed1fd2b58c57c0adc0523ba65f68fae425684b242b5a6e246f73dcb5364a3701546e4d3e6909c230386748b334a074bde08048146cd98d02ea0d6ee202e182bcf3bdb682c68510f1dca615e01096844b17612851d9208380112ac4c4e447287776bf25cd8a020780814ddf9f9d88c543f5a4fb030daaa2e2ed13cb566a3c51d5aa9589e9ab98230000000001b5e4959370723ce413ad006d457bfa9b9d1af42af2d44068d81ff8ab67e5dd390700000000000000fde01c44e280119e817133ef76cf194afcaca71eb8b3986206cf000c4a2b0bf1d063353d759dbeb7d30871185e56359ec2c00a8decf3a15f4c93dcfcef1b6c46c0a1396144e9c44560ac68810556d43ce3b98dbea8e90cd33117e19ec33f03d92140881dcf157e9224db10623eb6a565f3f8a0ae9f9a9b5314166a8995c2e8e287e790bd25e217a55fc01d24899e92c3ba87c3cf64f0236931135f94ab1caefa72ca915a95b9eac7fffc61c268a235c8f49a37e1f1c2aa2cd1b05e3fc931bf1355362111d4b4fcb109ed6be5a7400974ca6a44e07e1f3931138dae0190819b6b4d8413bb833e3a5ef80f990e0468d7717b77520fd57582e81d31350c75b76712b330b1676cc436022ca71372354bc12560eb102eda0df5a98930371c1e05836b8d8ea62133c5f05c91fe2cd7017ccefedaade80f9a862f2a84654b018abe85c475e6a07e00560bee414cbc706f3f08efc16696209db9df1705a15bd16e42ee6081e5a2a9123370046893bbf108323f53695af0a0577adce9f225b01131bb0bbc36a09aff9afda1afa44cc0d88ee9799598f434dedb0ebd2a992e83d8898df12924c198cbb1240f0b6882400dba20b84af482be6ec6b3844388b890a39783f55350dab88670c949cd7de744e6a10a8149a8f1a6bc97ff220dfe25aec7c0d74363ae0a9415c656af92ef9aa763ed7b52826cbdba07c0d51c23ce7bb1a294b5e63821ed3e3c9bbb19d611d5db419d1b95aac99df7527fb442225da5829eae5f6f82187c2b2f2cf2fd7909c0e270196606b95201c1c3990e7b879f895149fd2470da3c1c0fbcfc53130b8a9c8f884abe23571ac927c5c8d0140d287d6181fd03b133fd381fffec2a99f311f413ff51789842d1920112712f3688ac07e025c26823a4d95400c7ce0671fd8fb806a85638cb2b21b56748548c099c102cfaabc225e7a82553b0f4feafb2393e148d5ae7cc921e717802453f6fd05351eda81dc7e39be517c32ec3b7de4d5d5ce7fc1a8197be3b34a03fdf2ee1d4c37e498af041d4a1a915930176456b1f880678eb8ff923d167ff38a56ac73375fe6b982fb605ad92535d95331c32acc518c52b2da08042792236d6b393b35f07b938e6467ee474671d489222a12201dcea437e19bb016dd38dbf170a2a666da317d5e6c61a327104657b2d25363a6a9511dae3b13a88291f0356aa2c28cfa21cf71f6e7141153ead80d0e3acabd81ca789657fc7179791b241db8df042c9ff849ce482dd2ed0f488b8b996aab72ccf48926ead1f8ca70dc8e20eb4dc0f121f84bc506898f1244fda0a6f07b43be67fc1066463daf175ddefb212fe9f3c465fd3f32b157b3e6d88262f14651ce5c293cc4b9d2cea9b4c2dcb13fc084a5ba7755332be6bf3f63ffb5309d862815aedf9ee0628fc126b36256b1061a8a08f0883ce9c95e449987e4d29b71e05a1565f09cc59e2b3cb492e5d3066f47847ee39888de9111344322a00de1db4a5a0bb41e87c3d25c8c2dc561f16f135327906975f0ea782a3cde32af3f5e206ccad33edb28ba4732f39d0852b9eec7cbcbbc55f00efbd22c247fb111b723a77a788249b15f593b774772ce8c3f70018b8cfc8b5e662690c6f24af23055131ea0f90fbb9bcf771dd62e46f1b137f33f56df0b17687a442bec012ac57295dce49f28ffa5cadd3c4493ef3cd1de908e6ab83f2ae4754c699fe034b9ae137a1af23e9304c68fcd8fb1eed100f008a177ce5154807167528d1e35c8d8056d0af1fbe30a764b9d1a56c8bca3f9f9ec638aac989cbf1b646dc28a443eff4690a117bcbc23e7195a1a5b28840cf8f7a4b1b7879d3358c1c820d11a0d3c9b3e77666a7e20993b4fd6c9cbceea67d5cd0e180c7c5987ec68848318ea5aa9668d77e79502cd51ed71421a72d7107e71ee1449a980c27e405c2c88b9a1f183f91505461a7f08d8542275d7e548920188a2948b939dd2dccbd60e921466ce89a26d7c9969e16cb23e4521a66a334b9a5a73fb24f3cdefdb4c052be47ac8f17f43b717265945c5f2652e969db506103dc688e498e15d83569852a8ebd8f138f48864cfcf9d6877090dbef8df2d996f2cd466797bc807a3231e5655bcb2de0168b99c84ad2056c8d98d54489b4cfe0fccfef8ca79403a8d9e9290c06dfaf9cf3220e2a956321409621a08b3f927a6cb0931a122e6ff03cd976a278e80c005309febc819a7d4e528e0ed28889048ebce28b251ba3a9ff3600f6de9f8621bf5f88abd3890d0ba4fbaa253afb0458c03bdacd8680146c24d8cd947cff2b57afbe62658a73d594cfe88633c91fa12467411d6a78d9172fab6fdcad5900f988b2fba25b260fdc1e6162229eb3f275ee3cfbbee37748649bf72ded48076c40d928c46f11f99e5874b704978cedb0e4aaf6f9ed4761bb7d144863ef6a4df3cec10ab5dfb1efbcdd75dc4d4a1c3390fdd2651698006421e22c83f1dde464a96b5d8f49a75451da2a5fbb581d001b29f469eaccd36a8309db62d9f5357fd5b257f056217728ec6f32e84259c6304e4f4ec9d3238f56dd016259dc5d784cb7bd00734a3a462c4752222ac3c57711af2b07565a962d12d3c66a888c7ed8fe97cc3ba7f22e0a66f4eb7e608cba5e0f92445f1ff29d10f5d8265cc4c0ad1e77d38c7f2eceea6ea431c480405a19c5114f971d53261e860d156fd64be696b54b3fe9b0b916e52c2470bebeccf4030502e8722d623ea670cff9d89420c338fe27c1303e933ddc035cb6d8f6b9cd983122b17078ef81069d11a9c7f6f5ba8f5e8a5b68dbec000a08de604b25e74af167056652d89024b2cac4a739fd178cb6b1da013d8f982d1f52ef4ddae4f84967380b0207e7548221e06bed4449ca2aec8c206dffbd56618c801fbe48dd8f3f1e4f1a299e8f0678b1341b9b204fd681928b933c14b97f3f016c965a1bc9c934899e317aa40aa4f97ea2ef681d8d33daa2b4701190479242cc3d49893f45871831091ccda9502b7698cf2af38366098b742c22e06b65eec21d60777cbd6e17ca64a11f1ea5a9c20b5c51129b997419fc8642ee3d029f8ab4589baa13c24199c335263ab8b27986b3dd47b244fc6c8ca49d4401d8acf8ab4329047eaa958af3bbd36020fa2d41cd88d8c2baa8e900a169cf9c5fe3a5903ca540c54f1e6bc2fe66b84d1da38b76795bf15e8118c5616066019bfc137bb4821488d07cddb0026249671302992936a5b59d0717127498710a1a73ac0b4d9624bd21c5840f8fa59422de0e330fc4ece95807cc2ac34ca3342d26e7f771ce6d74668174e614a1b5e45e872d37cdd40a9ac9905aa3076c923fe3119f3af0f465092fd03eccb86cbbfd8c48201706dbd45e5372a532667c049be64f95d1143a54c9ba2b266e8c51a5e3c862073e0dd30f8279ab71715d98d9e28bf9312348ca97b2070c8de1f6542896ead33007285ef9c1d431a4173f11161c210d17ea538467591be5093036cb0744a0a6ac0a6b421dd14c1fab28f12d8a3385671d0534e07a1e6269a8526e05b3b8f2a0eb2960e6ff1eecc77f8d9958714d99aff391d1903064f5078d50f1e03e6935fce439b13efb447900c24ae87958d6a8cc5806f276e53c27ba5e6db70e1e0a81e163341d8649160201366f4cf354b24d1e2b436db9d5a59d398f5b0bfd13d5d169b11679e32a16b93165806121c934d30b073a34562f01a1373d83fcc86b1126c1fb0ec60019aacf59eea72b6b3f2bd675c6eef2e8405b5f6fd620fb25ed802f1660242ac45a6f2b1a3e212b068570571cd5dba33f5c5bcccd96fae106f099b7eadf202f8e09bb7d4ab09652c6ff2b7235b0808b6d4ef08e4b94331e9638ca9c367c3e088108161cff2703f8e57380e338b20f12ba547f41b0edcba4ece8bf4d36b82205f1aa7c5dd87f7a9aaf091265036f2f21f4172132a0e06f8131983948edb105376010dd3fb752b9f7e6ee52bd3f65fb257c3ae7c907482bba95e0a0b893d5318110da63a40f460b2edfe755e8e6b0fa6779fa4dadee08f651f057bed45d9837b5153d9bba0e036f50f19754d771ab460d18a46fbfda565da83bfb2ee16e601d6e2d28838baeb0baddc5e9faf3860cf9dfb86714b41a621c834c23fa21499b2fffaec5426eb51aa58da61a2e8fa789167de7fba2eb7df63a23d2c9ca9cfffe3412e19c8d46ffcefb57029d7a59c52706181ccdcd6af5cdad136ed05b713bfb151ffdc5ccf6c2e3af44928ff2d4da8e38b6b47903df09a716ead5a2048e131732d15ed1390c0fff23751d5c2138486daa0a7d2f6213797cbf1f22e732d87bb41526cc0b26507c09849d7e55a9e5e47495def4872bee6f3ee44c604a19083fbf387dc93ee8929627bff9dd6fc5d3130a926b6e310841d191d6b52c10d816917f38b457dd0228293d153937ceec11872c72b0f4fc1e90fdca487e01efe8119f8d00adb549fa12b7d068278935c09c50f7c499333b1803aba13bb9539ff3e51dc012caa04fbb8c90d8000844d3faa1fd725802cbfbec9f8b96bc2847519d225537345ac37550bba901fcd83297bf77c7cb65ed9d4ba1dd68a65a1fdeeebfc355b43df020bd0a74515785baf231ac7efb980af6382407c5253d86bb808b65b663d913670a149c65b402dad72bf55e6e0f0195b3f3a1ec914540ed9840b233dbbeda138bbf9ba1d9c7bbb2de98348dc5a61e1cb1267bd9f7ae03976531bd606c4e6e21a856c5cf44c4b8d5db41fb9a8ccbb037f07cf92afeb77b5fb4faf192f9b0830b1272f4cd09bae4752fcb78d3dcfd2497fe18ad999cb31968ac0e84b3112f8a2d761dab17e91f129527c1492dbb315c7db2265324592b846d32e9b0869e305901a56908748dadd75e84275bd4f36f4377f27cd6f53e1fd133552aa61f26ffb91344631e0656c0ba41635433f28ad90ba2ea81ec5c85d74f54db4e334713048506fc03a335e33c18f8ba110aaffa02f7d844891b1e3be8429045b7748aaa22bf2f471f070599116bee5c02f36e99f23c5b1ba3a96b2ecda92bf44cd0e647f4381868198a23d8e81d5a9b92c88ac6e2692b6e4e84ab713b047d86406b39471b382dad909a13259bfd0d5b5452379d6d66b13f1cbfa2d7ae0960930d18af2644783f751f3bbd176adb946af1700d143a5736e83035e94528a90cb5f23b452275791e2d943547c2dba4a1644378a4c8cb27460b8fcb0a2aeb8f77f0f3ca12d1c0812d3c0e63a326bd0fbc6bf7c902f4e2112826e660d972f4e921f986345cc338791f28cf24cfa6c9f584fd735de8cbcbf3d3a4ea1eb5b8286898f6bec77754440b161eb3ef0a1382711906835862d23bedab1954d31b7f28ad7db29534f3ce44c4048bc14877725657219c28791f27839ba8bec3ade8e4f8c6680357a4f4e8c1df2c5521bf05cf36df431bbaaf9184cfc35236c364d0e69e5284fbbde038b2ec5e0044038bf9a79dded1effe571521702271b7799a2d562550d75d4d73075f7cab2425aa7bd320c37a6a614c93e7c18a74693f869755d47e0c0a8c5ef90417d289363a5030203ef81fef19232313a212429acfd8c9390cbc8688bb171304bed3af0a3d26b6a49d0f3012322649b9f13248b2137d1d94fa1ab4983b9ec2400fa2a224e17abdd6fa531992629009b2f51eef30f3c3de340cf85b0d0decbdf04c9272377e66dbb428ba94329271494eef2b087f76d39fbbe0c812734a2348e54037363b22358b2b2587d2ee57eb7f0b276231d207ab9582d1cbfb70ab6c0dc7f76f502cba5cb65877726128957caf142de18fd906c3ff1181273f4e447a3b139eb2c42bcbf1143ee7b90f80bdfa09bfac0d59546d664dbeb808044504f4b91e8af4073496a266af1cb5de83c073f3275470b8bcb89335745dab55aea14928110e7f520ed014fe882056656c88846a469d66d089fc3fbff16b18df0cc22073df8b17de28a07304bc73e858c9c9e0bde734f28dc062358a4350c7efd711175585cdd386361199cf88ce19664021a69140c744e028532cf1d00d83b7472c5e935e1efa682d969e4b241cb074f73811daf6b171ee99b7777ebb47be6dc5514163dc4b15e60a4996365883183d8c892e5c9c6c8d9f1d9eba459d19bcd20f7336a5a8d583d125545f02f372c78253611b33025dd864e093c66d41bced42acca4413896412b736c2793e25cdcdf9b4656b598121241b5bcc3748d59ed3ade239a7e456aea30f38af310c553f2cddd794ee1ab36dab8c48fee91bae1a7f3ea04c10f8a1925ad32a61b8c5daf15075ab6b8d1b307c2138ee31c6247bb8f6154725671890e47ccc356ec027a0403992248131c65008c7e88b57b50856c652f95a51e6d626665c6f195f8e00bf1f264785104714fcaf4bbacaf9deda599cd47b263341648bb0327309a941d265f471ad498761c989f7294bf884ae6b897f2512e44845620e9863311b4fb6afa80193711643d25c7dc3ebc285cc591a929d012d7bbc80223b79c1163dd66f7a39c10b7ec7147f2ef2ee0e1e8f84aea715282e42eb8bc6d3f432e4a12145b89198c2f2afac80bc014efff6c17bc7042abe919c6dd28a148da22f6ab0144c7ec39dea1081417d720bc105a086cfadfe5980bd1a95bb3ece22c964c902368ba7a2968e269f49b3d5faaea7b8afc8c967e75e79d0023389d0ed10b9f1d815139e36321b904df4f9202d26d091980ce4f56561e135aa59bfbb73c42c54993e7ee575240c2fa94eb1cfd8fcf45b7764556cf3edc8c34f85f0eb46637e9f0b029183c59b1ee3214fea7e63424a6208d11883deac573258b21c5d1f6164014c09a25ef33372d1315dbe39895141981c890e826472731dddfe721c9fba442dd430dd50b66143d292241bc8178d81d332548e81bbfae387ebce08cf174686326f026c3bb0f77bcf7176314925b94c6074b8abe68f1cc3485068ac7201e983f05f35ed44731353592cba0f18464393060530bfb7684e54e071bd7bd1238eda74300e6f33032e2b7d9671346178dbeb5da6f1ae1965a5ecfa4d15033f676e4265e20f672eaf4572754954fcbe23191b91b4cd85318b480464309f74844717729cc5251d479eeab4f922fe9a0429998e6b143b3e0c5e3904544eece23719da0eddf432f3f90d48893979bc775183ab18252550817fbd37ea9596bb25e10dffae9ec027354c542de559576074a02d94b4b72506f62bccc6027649c9b8b46925b3dccc29126a3945a4d9f6982b15cd02df756ae84bc5bd3b78418ae1df688b4a1d57df20d8bd7057b3a51d4b9a277a63f0802d57dbc1ab298036bc0412db6979019cc63e058fab14f1e3f457c5095b3dd186a97a22ab2f223ff7bb6423c48bdbf8d5a909859a619f7bc009176caadaa2843a0e7182a135c59adda584d381aec3bc060e13369366bd0967caaa6b35495c07defa77249ea1834d3f278d3ba52e17dbbba43ff5b2206846afeab92f33ef1e0e98072c4ff4c9e17b02398b23ab31a3bd3eeb27152818794c486fb161463e8cc260b34fc5474ae0e97920d44391cfd53f95ba1f722e3782c827e177b4eac22a7e442903762f8136a9014a8875c60d6ef086cd1a32b2d2b2d419412dfad8a46eeeb9159a7ab810539cfcd229ec1b0c75c720cf0ab92beea956800ec62b368a7da2c9cb489cee5f47780f9e31f171f4ad50113e2afa6a3a002afd3cefd77534b8fe456c4e95c3dcf738887c89b6eb99817d05d63edb4ddfd6703cae1357cc91a5b9c4135a8c3e9fb0dc2785bb3c9d5f96a1b0260f7209f04c7507dcd3c44916d72d2e825d8fefa15c70d00ce32566c80d70215f14bd4a88dfcd3404d95d5a6c984a77c1851faf6ad3ff0ba748ab7e72e944c1b60ffe0ee243e409c3c2e5fb761b98effa815989a169dc09095abc8a0ed3317ac321bc088d96818ee2d9284e975343fa2312d35a33dd1129675a17aa2a87b1d2ca3f9a0faf87f425d877439ec8ab2161fd0f0e54e0b3064d4f67f4f94c8d9c3df221da0baaef31e132450d8fdda8724f628502be2bbb1fbf2b6727b1adb82c68080af89a150d59738b54f7be6fe31ba03e382870a83662cfbae3b5fafba68b70060a11bff8f33fbf27c37b6c567e49d8d2dc60ca2fcc14167a11352c4728c1b29c0a1988e6ddcf9ec6d44daf6331ee3096280237aac3ad24b92d5a7ba0d1721115032b108413fa4035fa3b8e04a9adf84b72a3bfb145f24242b5e492053148cec33052597c893d720599b65010b7e9b6a88d5d46b816fdb60f3008213041e5c431326a0abdb04fcbe0bf97f6e1d4bc7ab00ef468572f188304ea251add32a00aca3846d3161dcc8787ad6c7d387ca90238879fac5f370ccacea0df1ce3ba7738ef2a4b17e7b207ca2bd1050306eb6b1d90ca259e06b5bab4a650cd1e35c4bee05b070a75c25fddf1a5f1ccd07043523fc1224042954cc4c176b7cace67c4bf95cc3ead591bdab9c23e8e93f21707c2dd99aa79a81803580c6662c0b7e705c6c33918faa6970ef3c4d586d1a47c939423c85ae10fc8c0bd33420634649a0ae287a23384d045a52b874ebd770263de26a7f1bde2113f5acfcd4f9f6c1c9cfaaa7cd9056e96ae9d436a68d448be848e7f731cc18e71b111001ee0fcfa939198f38a271340aa4bb857f1e7abb757394be0648096d31f861cc6beaa08f499b0e0cb9b1d0e5d44699a7e106668a5184c0a68a8cc361b23d2beeb2f764e2ada4d8339b5b00a7bdad9b0ede7a8f8b9a1d66add96520f96b932332fef695ef2f9ad71e5c81f2d0b95fa15c0e8a6edfdcc7d1c88c9cce1c92677844435fba706c9beef1c3ada0b7929eb315be8ce2f52cf7fc2fec70eac01eba379fc44f0ad7d2a5749018cda0075f3c00c362853a19b0abd11eb72f6884c235ecdad33df0d685d9b5efb019223414a1e14bd700fb6496ad40336bcc14949104ea3b8133da0c14022a70ca7382663b428992550de7a27e84adef46fd0b48caee121afeacdff02c92c4bc35d7c1bd3fefa631ff142b82290d85f38a59ddd9e5cc7e98eb3d563c68cd6015c101628720ff439426aa91cce3ba739ab71d100b64fd3dec8de893a2cf73bfa74d581345721313b64e6e30f2cb76bc51f91f3c397444a17e9d3745aef5d2c552d5048a622ccc0ae6c51a68c72cb9209a170cd37d5fdbcf1d0851a765ef1d3b219d45916927ebe76f6025e240f349f7aff659234ffe0b438f1c45b1a747dbe0b6cbc622171ffd8d22da0714f893bc8d5be01e8561d1a91a38638c9488ab2e84af9ec8606f8657ea152b5861d12466857a6f52c9249a8655eaabbaa028c5c468259c5423c5f08b47ae8dbaee8f50c8feea0b5edf4947ec6822fd1bfddd5196d4001551d134975db9d026dddac0333df459a7206a4811ba681d597ba90dacd0406a9d82e3fda25f4f62d7769a4c7f2012e9c140c058b3ad6012e84c879f3766413784d0701238fa29ac13d1d077e42f878dfcf88c865a5a4ee0899c945eccde2151a25ec1a399aecea7e294791ad1024ed3ad6a9022e127cdef3d6b831c6d1e4fa5d40769167d6f57a478218910725492a2b5900514f8b5fdbc6da985188b996e052387c970941cfc90144e1135c07c0398bcaa969daa359509bf9dc47e13f1c331bed910f22b1b88586fe1d06b6b4c8ea42d7ce8f386f600b07926752af53f3018c99f3bb5e379dbc998fb73c4cb1aef867d18c69a8f5c0ed56e3fb0fd4b0d59d597b6c1016629fbc338ede49d4348635b530cb1357fed25ab201b4892c4a377c8de12c8eb58a237c0118faa74470e62e2e50bd253de987b916e741743cb5d6ff6cd20ebe0fcccf9acbcd5b619c4004afb5a3c41e383f7a9dfee05345f66b0a50b5b5c6b1953ab7acfbe3820c0cd7c16950735d4e7c4b2543ac4ac2fd790593e1d225ef096bd6b0c2ae89d7618d6597d784be63c8104c9967b9660264dec1e704dda6ca2ecb40b82c771d664c2e441add6ed226737d9e41a097cca366e585a8981b8d99beb0bd69d80c5d160e0bb43177f40350c256b9ae0c61d4d7cbbc223fa16f10a037b2d995928f5d4317be2270fa58a85068604c1f8f2221e9201a0ecbd1e33b073d381910c76fa965a41c5ff1280dac2ef2c7afc62a73a2628712b6639760670da317a8c7a519e14de2ff7955c790b0ec9bfc17cd95a2624b0cc57b7c535973f38cca86979a0c056232b6dcd826051a1f501cd705768d999eb9967e2c23bd9bb10132565cb62be1e862af68b4c4d5003fe065ca4467f8a8eaedcc7b154519afee366fef12dce0a1dd759f0a1db3704b530887e3e2ca894d91253a4b8b7c52969d318fbf71d1b8a49c4914e76fd96b31b02962205a47f80d2db7ae4b75d738fecb232838c71cf72ca7208e9388a4f6b4ae689489165feddec2fa3f44869662dce42ab092f020bc3259de5670cbd6628a709a9bb292f21d7a5eb0a5262416dc1be91772cd47609d94e7d91ebd5f186e0bbabcd25c4142dc5edd48eb577925dc03712f0100e5d04ad2d3faf592a71743a3e89ca7dd79172d5d0a6002c75bb6809cf526082a05d18ee79e4cdcef09149b4a23d7c0d0cddc7dc050991ede546ba6a9b05e3b020100ee2c5d0690bdbeb01e35da3103aa239131679c10aa73da3b9af9207a7afca78717bd32c8056459fdee88033166f4104bc7cfdc07c85310d115029ad7b6e8280b0000000000000000010029c8c69594d18b934f6488ba3cde3e96b59ca03eb175c3e6b57cc5d8331853ae4a85ec6f2e394a02346a2e20fa6a658eac0634db211e86f20542aef486251b0800000600008077777777d80a1977000000002b000000000000000000000000000000010213d42bf398bba437cc3ac924b33594ba6888dbd01f98439d0a13cb381c7ff9114a6698cd0f582ada2fb898ae54f8fa76acea6a9e7d167351f2d86b128ba6dd31fed56771243fad8916fbd989cfa38f0b2b8e1e66ba327e45a990fb08d9754a8cce8e7efc8e79f9ade00525fb37075cd2d0498a8427117d75bc11ef1ad0523e20605429a5a617b6474d91f3df44af8c4e398e01537d8a5bae0f675f4ddb08f73801367b83440ef12949fffc56ea65be261606ce20fc734b92809b611effe04720c296f42505b34b79168da937b49ae4bd3f0cf4c69efc6656e28ca2cfb8b8c02d07294a4a447ae7c17bc9ca679ed51335465c0ec7f7f92f5dd24781f9c39bf577a5c82f39bed6527e8f45797cdc82b3982e069b7068b26f5eef38ed99de4eb2bd7b225e64f85fb2f40f1e0982c89682c68dc1281ffabc38c3070e3bf6d231719d2aa0350098028f647aaa4bda28a5629f117f287e9b99457c9915c34f8c461586262208faef0a1f08b906ce30c79ee6ed966baf5a66e6442dbd70de93e13d76d1abdacd257bd92cbcedfeb7beb9673f92e071283b094f4acabbac88d1683776d2aec50e4705c0ea2e20e74f263a9b875c5114b57ee7da56c25bd13028fedd2f8484f60f22fa208bc05d3c737349ae763af37e6a2b1d335aea120f3062389081b3bca32bdaa20a82fac0cffebec16c75962936a4b1f28b20da06a6bced848c641d4354497ce8044856e2e487e6e0c997e851f6fab04a0fd8fa8874d8e2800053c2c9dcc8eb4c72918aaeff183c5a76dbfdfd3904040a7833644713e817574dbb6aa8cc7cd96d5e6c72f55e9c4d3a3385d53d8e8a47061c42c1dcf55af06088164e99d025602454ff47cdccc8269eaa129066678d48859787e86b7d6d8137c9042be36ab6d9ed37a5c192f5ba250d748dc9411170f9bfe29438465b7c1e44b6c3309f9b78c84fbc51446add066cd65a386fc7e3c7b0a1c5c1c514c0ceeb6a7a2fbd3a4cffdc684f6efa323408f5a9a6be6a1002979292f5affdc3f80957fdc7bfd373677aeb525b3c721d71d031e9f11940bcd492ba2258049d6988ce4052e58941fae84840c32bdf7feeb6cd03725e97b2a7f7af1b62c8758f834932f09e697c677086caf22c2b570d02e779636d9d78eea648fa47aa50523d08abcfc81a53b686f4ef7365bcd70d95b4ca7f0edb6467692efb3412bc1509d91c057840ad935b73e6648b1ac895095990298decaa2799bcfd34c52a820155528382240119f35d7ee9eda24ae1bd9708b18ef725c07dcc91f8f2a22335744cac5dd57561334746352578e0c32e7dc31d9f129198d0d3430c6c71f6aa9cc8067d07d66b049741be5e92f95c7e207f1137b43fb9e5150259316b0355274ad5e3f58aeb5ca5288bdbce5e3fb2c6254a58fa484add5f17b37498d21d972d8373440603ea6e938822020ac50bd90b7d389af3e11f05222a0bdf33cb155121eb5276f8a62ce4ae40634b8235e4b4482b632b0da546806364364e2fd5c40e16d0657e2c3480204d52932059ffddee3b206c8b1ec62b48d6d1412976ca612bf9ddd40eaf94040d617f739a878ac818d7291ee0ab9f7af0f595a88dbef53e6b41368f9522962f63bd39c507c8618b14dab621d9783d98e8c1bee504f15cb5a44a4246250fa9be868232bd12d59ec9bfbc6d6ee838ac3ebd6d6b5dcd15f9f146f835eb4ab2f6f6b524efd049caa99ab6c3b830654ddf72ce8a1544911a34e0c26559f6967bc9e67276d8cbb23fe0e698552feba416584fd26705a35d3ad4a95a88ee9543bebf753ea0dcb9b8fc276d2a1d77eeced0067afb5c4944680346408d155198d9f11903b2b44caec65154b7f68ba5a23a770adfd1da8957cbc305fe0028af94d1859178044f2d2efc432aa2a364432c8f8eba98cb1d8559239f568d4964a382aa0788cc345c94acdd3a6d668d568ad204507003adb9628dc2129e6cb4606c18779f26be318eb76e1637f64e43f69b41bd5b02d146a24c7c5e1974d9da0d142491de83eb4ffc834c6ac75c39af64b4388279b3a5ee801dcb575befbb5baf1bd3a557a23a515f34e572e6b896fe62cf902065a1e0335537671b585485ba99aeca308f5e0770012fd96b34951295fbcc7fb9313d17f2e5d7544149ce3fd0315cb686bcf3a27515d7b30e1cd927eead090f05d2f654d182a5d351ba31024cdaabf91595d6decff136f9ce33de2bd8defe57e730a60895f33579ce0beb32f950805c01dff0e74d8745079f94b58750daa55cbcd4ce66e085c0c5994d29f6bfcd936caeb0dd1de4220dfe383859d4def206fce9af209626ad1cf128ea133acdad55c858a09d6e5f333e6dcc0273cf143624799e9dd11474d18660bf7b4e803cc671f3fe78c513111e89e389d18f6ab0dedacd22b00780814ddf9f9d88c543f5a4fb030daaa2e2ed13cb566a3c51d5aa9589e9ab98230000000001b5e4959370723ce413ad006d457bfa9b9d1af42af2d44068d81ff8ab67e5dd390200000000000000fde01cf9721fbcd92a75a289bb9defe7121643928a7ffb4289d5fa82c9c909f14d95a95a676eab542ccf1934ab7a6eabe2929fa0fcdfcb3ded02243a2fb069d835d59ad1cffe7aee3424f9585a352ec05ea423525b29a7066a55c5a678dfd601dae989816c0c30c88e9d456734654d0267ea5e0cf72c036515de68892881d562f17727e52ce20392b7b9e0abf03a4bb531c5a558f1a7029826607b040c5decb14dcc15d30c6b8b1fc087d7ebe995260addc833f0de0393ea18e50c441bd4956ac06f8ab85a1eac8a639ee2227d5772def5a6b003bbe45638d61c2e590a720c132b6c8dfe5a5cc5ceead5ba7af2712cb086b91e09fcb8940906f373b4f881571b4626361f517ebc156d98535c5202d7d2e8dcd34cbd0a5fd66295b5fbda69f22cb17489c16e1e592f60ecdbff9601d511eff331816c1ced1e8772a5f475b72f39cdb9819ec565767c3311e85ee80bc7f7b4ba9d86c51c3b4cf04588297e9bbfcec8e9118d495e48f97fa4a41c5560aecc1683177ffac352df5a8314d818eaad0b391208b16d99e1f1232db7fcb8c1f40482b95db12e770e63010fe8f3383a48ff2c9d9d085ce2f1a90cc66cec5aefe80310a18a1ee0e769b5da80725d721bdef05968bb4f328afe3f0cd21d1d15890b913721dfa82e58fa910882a3941618aed3922033b3bcb84ec9d1a55039521ad9b66b1447aa80181d3ca10fb90119187a1082be295cf0d1f1c80eff112123ee7356beb9c40623d7b9686cbc00261eca062e4f85960f4dba6166040a72e88f38094e312626014dcd02e7d00d8e6b9ebd5318f60580aeddfaa11fb04a479c82e5ecaf3e3abc964bcabb11db6fe29ea0fab939691698fb08ebcf5d3a1d5e72424311a0c2b0250e735bc4a1e40df0a1db7939c048661e23bce1342f68b8c110913109690fff05f96b597980e5e4823a4376afa8540ca4af6fc5eface78751a3f986097c2319078963116046d437d349f6892e08da313d3df856b28e4f9fe6caef35baaeea87aa25c0df75729d78c2067f805168deb4988767650e98dfa6207391be54faeff3b2809de0250728ff4c710e2286f29c1e8a0dc527f0b8b632b698aae910cb62d961477f7988fd4fb424e8818da5bf3f9005d0931fb2bb2f3288f8ab857114e7e02658d44721fda6618152f876a340d9682dc45d28c00fce274ba976ed5d0fc1022e334086849ca36cbd9ae5e9928ec9d00671d7681b606b11f6b8da16070d8bcd9eb6301dcffe72307b3ee6e0f7b1e65fb328f22518d89c70517bcaa75c32f5e5ffe74d1a8f40da0a7f9cff7266dc52903354c333bf97e4336a14d1e1954aea7ddbaa95ac752dd29ebd9835a6e82f3f56950d9c6c7072d892be9b54569bcb05eecf2b65e9d80cf9695ac45325e094718e078e2be2ac22ac482fc9c17aea9570769ab0eb572a3e59d14c7a2a878d373323af02fa6fea981ca932359feeebd1ea3938a8a6d467b50cff5cbeb3ffc7dc58e3983661819a74104e55aed33895bd93f646f3e135164e792228c312a0dd05fefa86fccbba6fc3e3c47af71100b23df5098f06adf6cb84f366dba65bb85959427aafe103519ce91dc33dab6b73a4e82e53680829ada3f0a237adfeb59cfc934ee8bb710d77afebc79aea6b1b4d618c2645cd74fe88f987215d01b73acde5fe49310873091733352c076025ce669380bb44fe4a169af35e669ae809381ce1e3a7ae0ba019952081759d755f6b1d1073a87d6bcb842141106831de65381f20d5d2c6b049d2c200efc1b020c57802d2d242cdccdbe97ed23ac16dcd6f430cdad599e339cd248d6a1a1de33e2c5f08d65e8b0d88f48ab37c65e9e72c48c8a8f3f4c7a309ac7f5b3e39b48f8d5e7d6b32cf7d215a0fd5467d70ebe7965cd4841feedb0089d8beeaa3fcc1e9bc5be5a40a3d77776c2c0ef083f50d19dbca41f2d716bd6cb7dd4f71e4d6aadd6ac1b167cd90b1a5cb2a0c6a885d73c424fe1b01a9e9de4a1505b8d89f9fe7fbc31fcb29a6153f5f12be22b2e7f5acb7903489b7f5417035279b4d89978b6ae1bb1af7b55dcc0961b606c4c9e169e0de726f55d09e9c4f9922cd003b9515bd5535a0cacd696345e5242b2af8e7ae203520726f3f2f08c508aa4b086a205e5d456e0d0b33a72db18392a37c195efbc26b7b4b6bf907cd65de84ddb15d62dcb11600a0d417945cd12be4cd6ff42bdcf9f4412fb2fba69d2276203e8f4bb36b804f622a5c4d36b06d16692c5296bddfae21ab2259ca19635f2615028afa601956ca0fd6243cc3d5e33c9e22c4c7c47896c7d41c02ad0f7233f113c21f17558c50eb8531e9f9ae116c2961d0a92db911e37b0f7c977bffcc83c78810f7f83682a5badd8f8fe6f0e4afe01ee1d933fd33939a20a981765ad4c144a886fc4c2ad228d6e51f314c549ca79ec4003a19b3a12fb558671ca1d2a96b5c019f294a853fb8bd019238d00d2c6c79653dfa13aeb6abcd9982c305a6a91df70b2ea282c84e1a09ebd4df832bd19f05b85829bfc4a17433304273d3a7012c0124fdd32f00aac5127d761fff04bca8793f2f1f68d701f92e18ced3c2430a75032c95f56e58c780757987c015992311d764165f3745ff32dd792a88a0721827001e3982ab8dff3e9d979c4fb2e9fdf5c275a19ba524adeae45fb93a2057b81f7d34ca15370c4b1c664ef2e1423bcca147f6f6d9cf1a5177dcb07638a4a344005b056bb2c6c36dbbb6446a1714ddbe24922fac41c9b1fea59bfc33b0011db212ea015588a83082d3d7a30dfd1340580ad2517d00ad96d2975e92d04e78c3e79d6f1ad464f4235be5aa8ffc8ae2ff29ddbcf70037ba242a476d41cff7bda9fa55f00aabd5aea329426f11dae241bffe3e831561b4d7bc2e8c73b00c53b43e35ce873caf8076874583b009879014d66035ff7ce71f144bd1acbbaa5b8e5e1e6b450f0f7e8d9d0a8e0fe27673ae863ea0ec477e7bd0878767e2821d37ca5f4872624a039570975b5c76a6fae562a3c647788b14694a10027e6f6f14ab6156d04750253b456faa11d8b3f4bd51334b4ad6bd61777521cd47024e8997670d58e57961ec3e6aa3a36dff08ed6cd43b411ace0660f000cbee986d2b3b02b994ef4b7dc6e534c7060774b28736056cec2337500f359f683b9ecff0694b0f7baef6a957c0e30dde68b4c175d32cf3315bae9825b46f1f02e135822d704ee788fde8689c89762f18cc87e2321e608e9182ec958410ed1714e524b2a3c670ff5730943bc3857c1d87417f7ccb69706b55c7a2424fc4c59be8240fb35fc54bc41f42fa71a0e98b3c88cbc3fbf55f32effad1fe14d0e7e1fceac5b46dad353d825c02edc72f563d3ffaf5a04546747cb511eb63abce12c8b78085c240484a3818e9784a9ebe8c8038c6993189fef4defdbd0c390860c7c90c30cac613c525b45b4b6693bee7993036ba2f7e31ec2d630029907ba5f150b72b4123ab3456aa5a22e77dc3cd8d460725169feb63b144ff3d7fc0810d8ebaace8c81be37fb2f6fae0601bbcd1db5e260237a482590311e248a80860c8bed21497a47afdcba7fa799e0d0e7d7873c055133e7037829a911442a2f177dddc7ac283ace2b10facfe6010ed432fb9aee5f40697eda30f1190efbd776cfae94597b70b8657d6cbb163f98445ce59a78aa88d1dc99925c6a89128c0b6e16289642f50241a4985c27177e0c8f088754d69370e1d7ad8965086e36430cee54046c0f7b367a1bfdded0bb07995a7bcda394797052c5507ba8c897341a4cf783ebdb59b05d89a1de046d226126df6f684ba5c415d19d214eaddbc8b902d80bb103073adc4ead1c3fcf6a5dd10eac4c1d03f071e2420dd85843ef76aa7e4feb0c2c7a381a118ca73aafc68dd105a3804f88bc71d5d33ef4b258a66bf23fcd93f8acda244a087d8df575cae7c43f180a5489e7f75ee133c972a300928ade1b8eb6166dba77461e35bbb615070a8086f6c8dd7386af10e1e7c1c87f5fa1c7c9c6469dec7345d24e9731b33cb056fd3b7c2a8d8dc1d7c023ac8b5531495fb7ed82a4a55821254e9b1acf92291221e6c5829cc425708073ebcca8944afbea8bb295a45e427608a5d5b870c4f403100d96f5cd700ff5ab000d97562880270f4904cf999753c6da442fc342bf2e3fe9823c42d8e126ab9522c5dd635c568da8659abc73d424dc36c6e1eb9177e8f9920c8260cb2b7db81cf1ab81efeb7858b0cb719cf1ea31fecdb6a0e43a1af058942c65330f512f02dfe0c40d4d46b2f0ac525721a910be1fe0527fde94e90fb71dccb3fc703dfa8b22b36264ce662250e4f9833c9297932ab4bfa211cdf4ae177de6bd5e64fc3d086142e749590372688156ebfe980784ac8b44331c586450826525d5d5e24bfdb71192bc15aab6ee211a64914c2bdc20b729c6e93ad5c246fa80ff0ee250969e575720c3f15e70cd76d04ef07125d2250f64ab4d578feda1a80967e4a5c6c59f8812d158bb59ea242e7a18ce620b498d876ca5cc40eee6d616a24c02fe292f1d80e7636da88f6da93b15c9f095148762b02b3035036a3a5884b5b3cae513ff90ef5582f8c59ab70a2714fafb01917b516120dd68baf69858651e52468be82aa1f1d5c147fdb6aef3ff9a2efbd0b8aa5651daef3c89f59fc4205501b9811c4d8aa9d102e1b867afb6dd4cf4759631e59edea593609457da3a3d47136efbdd687f328de03bbe9866eea7f6f2270f8409e2bdec4cb21a3f47de838fb7433ee4a0b90fdd018df78c1dc560182abf370eeabccf8b0021f2eab76642768bd72534940e3c9d30513d67b76c0128cd2aec9ec8fc948d1a08b795c9f49a8aa135d119c913f65b7153424f48cff48ffc41a1ecbf852b850546f31079ce4fb1f6017d9cb6112c0e23bca58f08dc55a3450bf151ab4378dcaaf45ab7c09a91e36a0510e54e9bb544a3cded598c2670368b83a22487debcad3ac3dcd8dac99f07594893fed59250b7a1b6ce2432ae3a60e454c2e8759353ec205c2acfd9784c1aead4d275fbbd7fc541e64ad4ec05ea2f414cc9dcaff58d60feed1e19c6414f7b4cc206fe776544dc1034d73efa122baa6469c12826fb3c81c5e51dc1f80d16825fc5f8e2f5c06553835f082a4d87346e7e794f0a3c79e6a5ca11b981148ab202004686c0ca1c3358421d9d7df0d7539c162294f848879fe91ebce7759ef190f29fc5b25cffe34b6e3236cf0e8b947122d6eec79ff027a06b6bde6fdc722acb59e9f27d5ab66680b103b3f725f0a3b96c2fc5bd8ca1533b8ad090ac4e64631bd6e50000535d5bb88e2094bb66b08468a26e4365382b2d47dab1b8c3596dd646fd0331f9590dc09196a03c167d4754a06ed293e4d99e540c77ad03c5726443ed5f86fa4638c7c5f9b70008420d3c5e95f65e7ff075e8d0f29fe09078022e4629ef8f65cf3ec9e5cef4f1d24223c6cbba2b7ad3abbcadb35d3244e2a274056b55d94ad108291fadef1ed37339b0764689028f4fcb3bb816777035428b25df0ef068d2821ae273469bbc10f881f20a2cbbac81d5dc74a007e929cf0e865b2665adb17bffb2a3e545ea5f10402f3ba64f8495f4e84526dd7dd21455196b4055893e1716f301f7b3e3ef8b1236172308f58af0b3693bd925c2d530dcf6ec0cd922a4199eac494ca2871c9892c6ff8d945e90bc4d08b674599b6b2a87d31291b41d44eb784d2db0c781c785e12e0176898e33919e05fac3824efe66e34a51896dcdb3847a797e959b37df5060a966984912a9c6717a753cf6a0c2ca00cbfc19a13285f450729d9707fd371e211dc068c4b427be76e0f90b96a1611d5274868aa859ee5f2e24e981f1fb07cd40f9f7dee674d9ae54065136400bbe299f75d4c895844b6a26aaf83d2dd360b31308ab81f58bf66cf8eb0738089c4a655d57ff12bdef0a34cb87164f55c2ff77118797055201c0d035aa41332796de3f546639e12dcf12c48b0cc45fab6c6ef2929338f5f11d54bcca129132fe6398b1c84dc9cb0fd760ee6e57e5bf88b44a5123e48c250887a819c10d40f850921ee4ff96e3fd17513a070b43e024dd35e9e37071024a006419ef1a3a3db2ab88f3f08375b0440268d97497ca237208bd71e580859f75c25d847489dbd4a431ab555c96b3e26a15901d584aa65e591c8f3149813267d9c63891703045b2ab53688d708d0dfc5656b3a6dc619b1c696323f2b8c03c49249bb9c32726a2fb200b93e12d2328d1350c73d0dd490b091eb37b1fb672a872d43fe023be4264ee86298d9e29e142a1e5a103a1b65739c5d75b9ef7de8307922397bc913f23655a52190715c66b5c7b85893643623994f2e13926c5eeb1c9559590409593a7106976a4ffd8fd7e1cc78dec9cafff03aec969551d552ba0fac61cf932833e169260b90ea2d9ba62de04a2b5c2406d76e0ee1ccfe5f6c5319466a8a12c2ab89f60011207c2dc6e3807b705afa343d04780d50d3c6c82dfc3df170d1266e8426a2336020c679adab458111814f2fd57b934bd2260d23d0493cbbcf77f961742d4b6e7eab08ee1930dfb28ec2617f9b30e157c620121ce9600f706390dce2344d3c8dd092a5dc4680acca5e7809eb7407946f5a54896ffd6618fa527f44e9dea05406a04e29c43b8a8099a66c7eef0808b4ddc40992d7e20d2992d51574a5a8d97a53f35a46fbe25baecc56ca76d9d438b20d3abcb7a3cd0d33c07483b1d222ee3aafd4a1ff8537a3bf1b43432827160c018a366fd854022516a3439d82a1142a8340dcb99cf7d54385df4bce84100d7e39b2a7653cb105ae0ad964f38d890ce35f6b2d0d6827b6d172a026c4735caeb701850a9e094b40e63658cdf1146fccc7152dc12946ff8705f99a8021ae555a81f25eb0b8eaa3aaeb2be3f6d19873d9a5278766a12624394296d72bb6d4b7227bcf5ff409ba12b9e321b8774de5a6ca7a882a8a493f9ab0da69c1841a4273cdfc2f9b5b03cb5ccbc8175c6c0ccd757e89f67157e5f517eafd2d03dc25dcb72837a460014aa54603b23f46668a094ff354f0e5dd9e26c3c76913eb2e032bf6c390327c1731015272c51509c6a40181eeb666e255a423563305942f8468d80e62ea4ee00deaec0fcf022f19e33fe030aa76fcfdcbefde516dda6d3b4b3a66d9dfbca6c0591b55f30a9e1f7e60c75ed97f12d087fce41374d82871235c328a32a1dbce03857c7ba3a87d1c0736543acedcd4b0faef153ccf430b935759384d6e9465f0e642f7b2d5284214f79fccf0a93f32870cef47b11e33b42b54b838076d7cb91e7c6070b2060dce2587eecbdf6d675c9d13f7fb58eab055ae23abe3d6f22a21a8214dd363aea22b30c162465b7f015b0c549f21b93ed995c2d555ecaefaf43f22e3812a97425803225a1a9b5491fd0ad8c17d787c5acacfdbd13c0d8b30c41623fc025fef15a3a70da5ad5994c5fe4d95df74f7125be91c45ab3ae3418ef6f0597281a74d06650032f14e3d21a7120782cbac23bac94f5781f615a3a83ccdb1bdb13b075131984f372ca83ddf3c2d83c40c1421f2558bf772657684eb15f5de1bf2ff6ebd43b19200ace923a7138ff5db1f97a91b0d958d0553b2fb8271b110d5a4b247316c548c0729e12374d042cf2e5650bba243886fb9c9687dcd13cf1a9c2901e8d3ee7e2b002b1a28116f478996b0a710bccb3c17b4a28ead01a23c3c0b353e8e152a93e50a4b8cad5e1909a9be420fd2d38987cdb0f62b8bcd7019fbd458602856d7f69d3b83a2f5b1ac3d4e5ed2756d37cb2d67d337397066b609a9c1b58f1339dbd9031d917f9b907ddea96c9c32c68a82ee3c8b706d0458c842b97e5469da9b6d49bb0a9653cef130f723c45cee02815c5b271b2746a436ddad06f305edfedf014e8728cc713234bd784073134a93e7fa180f8a36ab22ad29c5e1cbb0bdc84b7c432402125ec999b7487299bf9bb8efaa0977648d2e598ea22fff7c15f419bb1c9ecf0b9bab725e1545a311cc68a9041f870607b8f8d8aa3b60da5b0b5b9b9875e5512c648c9723fb1cc0b016a6577b0db87ee03f93a0ebeb69423baa0cbd863cb5e936c8154af442c4d6ef1304c258b69f98e7c66e93d7a20679de3c253f3958909b26a141fbbea8e72401eb75d500eac9dd3cf2a6fa25651b56a4362080e5e1dc9f17ad9ed25eb2b127330defdfb6c1453cc9412b7dc772179d04317c5497179f1e0df5828c891d27d2cb7b7d439c6f20e329ef5270577730d7ab5556ee40b4f1862bbe6225ba2e1a65646b2f88aa9e7b284a392ae285dd9f83a60f38c3f2b84fb223de041dc033b55d3e878a235a0503403b96a196e9a9eefd496a4fb8ec564aac1c6caa365a718815d5f608ea7d9742adc332e8afd0c4569302fd8ff269af53f226f2b2937a0ec2b8d1ea8cfd5391bae6d3b6ae0e8eafceeb89785dcd62036e0e182007f487269ed58ac2e43c69a39d4dd87308904447bc05e383e5592e3be6bd0f43ac4925de4cb13e13401ab46cc52b387a0b9e908b1c9cfe3b99e6325e11cd3d4795d71b7ffcc313be04db156091428ee4fb1580164d6d1a70a906434539e320dd2a8ef8bedd5da17fcf8c1127720d29001c31b868e886a6c44395e17631701bc50eb6863a5717379d3fd6922e6d9f7c6dcd406ba33ead72f42361c876b6882e0dd82ebe17309dd4cbda1f6e183be3668a33fb17b10cb30fb447132549892226251d6e1357a8d8ccbf45797fe2db2a17725097481b5c742df35848aae2d6043fd3e65205cda2c3642792b057839b181fce5c3fcc6e69bed8ce7111ca2241b21faa876587cc150b68a68313254d35aeef15ae2aaf934da061c829322c4874a33ea336f3549e257e3fd7e7f00c009fa7b55c14b64888296c7f2fad12eac647a3206ace1d34a8cf2f7cc99613105aaa588ba91c86cdf5ee5fac804f9dbf5608fc3bcc606c3829271dd6390ff2e08c91c693b289a91cfc5db4685c2f9c8629632f3ef71322efd85dd29a6faf5be037b7c582f66a9ae2357dceb33f1237c1debd4d3d86fb2becd5dd6f868f0224825565f3a3f2ecd317be1fccb908dd8964c71824206e52b69e8e69a13a2066dfb579bc7143b1ab51d9dea8cf31da621b035623fdb7b783421d411d463f0e8d70e92860061b9d4d880bb2d4336d46a33495913a1d3c9a0bc90a12886cb3ce208a68780f279fc2b8a4a33fb1f7a3809679aa09373310b7012312f067e5c0bcd3ce888f4826334772cd910578a4592a1e00d9ffd6ae159af5a36265a81016b1c243667e959a6d2095864a2853461c1d166e7c004018053cf4ead3b8f0e11c0cc3fcc29a191f4307433a0766bc522d50dd3fbd19400734a725f78d7ea5307a36469f55f7297613dd04e1f920aa4e14095746323c784d8ecde1ebb1b6b767bb240f251853a951f3e5c87b2eda4ad87ffbcfc6d5f702df31097dbff6200ee3f08cf0c8dcb2d4b61a598640e8f1e4ecf2554c0f20476d132572bbb852fa77675080c42ba82d3848f9ec082a4951bc05f8fef9d74487f14a14a2d6b8e2c51b9442970dcafb4a795e56b1a285bcdaa802df440a4be960d841b2c94d73b8174681971150c5a212610a54c6add858c3fb6be614282abbd6d6209cf57ffd27b74b121c26a96041195c7070f90382d49104918995141def2ee45002c8db4557c188fc65ebdcf52f0fbecec6f267568699c70e13492ff96a5a9b831dbfbf3cfbcadc3944f7d81e3d795cd691c1965c16a7c0f98688e8e39adb5ab2b24d096b0bb5c0949eea5f01888c9f717f7d43bea23adc92750d8adb19b23c7e31342a06143f59e2be9e7b4b9bb0adfc0837b069a07ac36ab919ba3dcd3aa063b6211a4d1a711462b3ee71329f4922beaa3bcbb274f17f26c20f4a7cd995e3f9b82da19c17ada46529ed51eda2d94047ee618286f0c0494d5b6a855ca1938e09b748a8cea4d52808fd4e9dc5ac39bf6a5ff6e0b23faacc02108a9d68d5c83fd1bcfe9ed3cfa1757fce985efdb2c26db86f2de9066efbab8eb6ec52ca07f05aa5ba9e0f22a6479803b64be08d3c0db1526b9b3bfff89707b6a434bc6f6d4488a6933ded046bee03e8e2a698ea61c9f4f42fd6af3f5dd2e6dc699d5f7356458b73b2609be9ba56e947774cb07a96fcf8d3b2fc81e39136f0f460795ecc4377577eb142f87266e9e855e0b532e2488a430ae889936407ac74624ea64478c11e39d719253eb22d04994e0a37329bc538fed6ff99e679268b42e5868b5ad11071d0c1034e7775a02977010cb84d039102cad3bb1100d893ff889facb6281f322190f1127657493b44d7596244cd326a35f842552280cc5678f3cb12cc47a45084de981bd6674fbd52a959d80a92ee971a1b082a74151efc804fb10681ddbd4a99d448bb320e0ca960a2212d627916c5cddbfaea9e61e1bd87aeddda1f7e8851481b3f39453ecbd1438685da76da9523fd5beda1aaf5334fbdddc38d6f242c326e5c560601001aab40527af8fed9036d8d02367b5eb475f4de47ccd793e4078c2274fa46f2bbac196def3e0afd265d81c293a0e523867595f9e3b43f753711b6620829abc6110100838940297a6e445cfac104ee6e1e4e8599ee0efa440d0bc2a85858f42ecb550f56e40d63274c4cb34b30f9e39ef0f1e71677ef32f8f6dcdb45d7dcc9e0fbf13a000000000000000001002c9244ee16d682ac24eae5933887738d5b85c6a52be9c0673f1a4f89759e0e8fc99cfc6854c980a9f13f8477db7780681069e6c57f931b9428d19552a810bb300000 +040000005e870732e453e2345b30a84df1ddb9a6742e5851688d482c65d03954e17428b6e5160161a566e2fe596c30fce434f7a46fd093d7a442105d13b3ebdb7f6d5fccf068791fa03de394c98f6a61e4c3c3211619bd649273d4fe743cec6af087345e0a104a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030600008077777777d80a197700000000030000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025300ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002b000000000000000000000000000000010283b25a5b757648eb67ff554b53be8ed492d59be7c9b4bf86bc08857c69c8df9dff838ae1e28142019c657e72569957820434ae902fe92a28ac7f21ac0eefc21aa1f9e086e6a59a55af671b953eebf8efb28e253ad578091a440dd034aad8751c42e96c922b406937a025e088c14095d5cab1f428961721257537fd00ebf99e35bbca14a776a3130e04ad56250bbda58f1909691be3642e82223b3714502e5733a8d388107b9d88c4cfad5a71fbfcf7eef27ee5a216ba72f5599e7df14684e5571c0048d2f86012a8fdc2bef5b7700c5e21e165742fe453bb4218847ff2d952f95fa6e4507f6f96602b52c87001fffb6996ece67aac8706c9a9935b70cdaf23f16a686d7c637a64502999fa7ea0987aaf93628649c8149117f530a615ba54a1edce79d431e132e8f2b60dba39ee6835a80d9f77ef93b3ce98d47942372a425e5d872343063ad41c2ca2adf4e777cba72451cb694385f64b300c9a7b80c62ed0ec4a3799519a54db282af8e017cb50646cb8081fe48329bcf435359c6cf050db3a850241570cef0344657d572988c4b8513bebe5f7c3c7b1ff0161e4417aea80fca8714faaf55ee3379aa74a5d41e50677672323c9b84bdbc9a1b8fd36f7fc8881d13b1d1a639655e0f96a6e8e3f9e3e5831dcfb91086471e7ebb0f63e0e5f2380e81ae389e6dbcf2c225d9ecff1369883c87993cef1396d51d1d3abe1a6cde7f3c9a270ac85feb7b76cc134a8bd8ebbbffa065891174fc8e5e391bcb18aa437635892922ae73574920dcb715c02ba9ddf799b47e62c0eac83f543a543970cbb4b33c297fd00867f1e72e997d7a02e83676ee84908b205f185224a528d5c1b2ad6668013b9d8479760a469827b207b2357cc42596ee7bb728831c341850fb4bd12547879d3e65d86180b35a842fcb79455a0edaf77908b526b6dd50590122b5891ca716b00832fadf5c98985384cdf4fb04876c0c3584271bc2937e6b9ee94fb91eca0eb8ee24c61a36033634144aaf696ba4cc992fc4af62f49294a142168a89a2a2226f1412c949fbfdf5509c1351be0ff419d887c86d47bfa37888d722cce3d45a80abc410d91d9213a09c6f0248be9517144425690c63f3e6bae3463f41afa5dfb14b40faf1523071141c376b983bb5eb930daff0d11a12d0270900afa32872bc55235f1124bbbebc4309f34d28181051fa2e96c2a53355e4e45d88503d1aa26a8c1975cc01f532a04f0fdd686e0c9cc94ac1fdc13ea2c90eb599e207e37138c22cc92dfc10e16065a1272559eca9634ed4400843643ea40282dbc3d5ea06827f8ae134e4f5205463c44dbd0962d38f61864354aca28744695524191cc04192f8b7d4d6bbdea7cb571816b3c9c5479267a602f32d36a3d2fc0afd58ef6c26ae133d77c47d9baad0207ed0988ebb7b3cfeccd19d6993e74f1b9f3963f21ac3e885f17aa48646b3641dc9949b557f0440b7cf704dd7a8c0ba20b2805015f169c94434810e611ad8f32b5eb79d43506daccea2ad2746b73f67fc1f3d68c46e28a216c9df526d4265211a347e707a085de161b4b7473df455e04dfae2e3ca09a9311dc25822c1ee1451c95d2fb40cb5adf9f87617022a34f38337cdd56b7b69bf3b05e38d7584fdf36bc2f19446d5881a8eb3c03ea63a3756b1ece10152718744d2ac3990fec62eb24113077c9f2869850a1811cfd0fed34d1646b24d1eccc1d12792b9e777d440942df491bac174f252f0e25c97653f52fbeeabecf225c431b7404eff9f08f4bbeb78db917f8af7298127ce183a58a854ebe668f1cb80c2fea6376d915926f59c6c99e7238ea149214594c1de9b8a362696b786eb00e5656f0cda65402a54bdfdc088272f5b2257a85266349e79199fb9f31c267156e62ca54f6edc28cf183967a6622aa6f8f47f984c7c587e1d22a496acef9371351ab99569f96e1cc260069edeecb3229b56b12adce057e83f26a6333b1bfad604c9c0d15c2c42a73f7a5d7919ed7c687eb144e2b8c7bcaa2e69a7fa30f5c023d1213a0d2185b295d87e3049d5f48c0c87bcb020344d9806155ea9de0110f26d1e610fa1804538f9e6bf5af6ee463722d65f645b16b5e2ddff4c567c7231d4ddbf2e23513ed56f802e9122101eb99482a65859b8f4145ed62fa64219bbe8b9a4f59a12fc31c2ceafe62fbb8e141cb5e9459932ad2bb584a0944c4b5214d59bc8846dfc5facf724e306caa36acf921e35f1534d35f39e9c57764930d293a27a33fcd49a5d6b460a429825f3d753fb87f93ce61182a1616294be8c68d7e7a0c093f008816344ce9cbc3f1fec901e24b2139a3892774de70451177a59f71ec1706c8bdc39be158edf9c77f7d84847b251e4de35034bf0ab7370fbfb07a3f6caac031add497891823b645abca23d58f07049a443f6e811b3a1dcb77b39474e3493b5f4016c3898a35dda5ce4f9114b02b0000000001f400e52c830670876bc97a39ee14df337627f1625d3a1bc81a842abb7dc5122d0700000000000000fde01c99709b35b28a4dc69c1171ad44a15f460917ecdc0f00de6c1d61169df971dba1883acaa76eff685c1ffd1272e5a856262df6b0511569bbc8533261acca09b218eba7ad82ce0bf78721353f43a8ae32ce9e354f5ad1008705fb4f8fa91d9046b0f255ada276aebe749c36f4da8fc874e68fc070b7636b174c532f6e2179d02697b865285ddafa6ba27697443b7115d4843681100decf2458a4296bbf35a8b9d3ccbe6568cd574c48f1295c39899112e4cd42cf5a760d002b76809add93fd36c07717793d50b8419346498767533a838db2eb89462d39c3f766feebbe34e67a59a7c5861b37f4faecf0c45406436b023323469062e7b50f578964a6237785a7a3218c5047cbadcd44e82dd506a5d23a811de3b64526faea7a1381e76f1a39d5895bc0858eb64de9812c65ca658b474bf1def4102fb65f6bca78ecb32b6402ef329cfb793c53e66e723490f0a441adc762b5ff70c96fe618adbc37cd3b2c6d58f1e81e04211d07decaf8fc376d2d71e99dc98b4bdcc40b5ed53597f6daab46b47a3e14605cc42b9ac53432f20a2be3e8def011ad618da4a360d6c766bc52dc273039c019506ba570f804ffb2179a4a3f6113c0ef257ebd6846046069dcd3878d104db4c65940f09f58ed4ca2c6724e69ddc359419d8992354718c577db52653a00b6a449f96e89b8841bf5b0bd2c0a8e201c7ec00d2d409beba480f24cf1de07594b97d25312b88370dac63769a9d79a6f4f1ef251693c7114bb57102a953f24d3ece2258af290d1d048d6336a036bd545bab3665baa9528bbe022cf107eef3e5331cec4d909885caed3db62e52b10712585e9cceb47d1771faa0f65e0e8133f99a328d1f2778463259e58db7464569434e455d925ba716d9d2a8fc2de169ff05b41ded5c2b0c7408fb7a88cf28f1f1f0855fd313b38d15b4add63905c50e91911a238b44a5952294a0df7d22c362a1230c724e1fe928eeb8ec3e98d41864e2b9351dccc617d07ddd574c97570f6dc93c348c3cd77e2e5f10bff88a937a96199b082670a3c28f9fc0f77f4ee89648a3bdcfe65b88779bb6cf6a25ba90052062b5aa1981695243164e0965e19e9658f8376a6f9d4af5186eaca5ae6bf7f1242b3133e1a90e4136aee91f15b7219db3cbe73867b77b7de38040af225d1eecd38b6caea3d140059bb8855ac66c412181f327ab0c14aeb7e00a9b0d76106bc67e62e9311fc1ef196903d413ace5f04e99e380015dd938d948b7e5c8c389cd9a3a60611a109e6d9a73167141471a3626960cd974338932d2e83399061433b00114af5d0e5eedbb4527903ecf77b976155a91cc07d0ea41f8ae3ac43b48c21c2d54bf8236bc0b8eca8deab468fa7decbefc454057d489e79d278f1c11d91190af9066b99c8875fd756643b1e70a2f039a6bfe01e16246c9f4c8971f50dec0dd8d90160f85523ec42f1b7be1d53ce0ba866e5944c6b65b3ae6e631e452306defe03658b4bd8eb479270bdd0bbaecb50a546f852fa2874d8839f9bf36290bc2a06207d55c1a1549161f090d89648b94df5601af95842dca01647d547c4fac6eb0012a1e868eccc9102f0c70d1c3baa04bb8706b60f1177c28b16f1d708fe7eaa615c463152b411415944f15936c1da513511137d2e140f040f0065ed3ea7168f27d3a42ab94501645f4ec06e207ae22d377b2f5b55cf74c66561702c042c52f487368514b8b19661317a32316e9ba5ae37a27a281f4ea6c58df5c7238951ce7ed0d485a0228c6b9a86da5ac424b9f4a847513eb9bc6df7dd24c381754a5a8d8ca090e2d07baeb4aca7030bf7690bf5c023f047675291263831f5c0af32e6464343e6bba9c8ebd69e8897870a0d358be9616a97890f116c92c023eec506a39e0b9bdd8eb799d44f7b7f8061976d85f39ba7ec725251d8a4e779ba6768cdc4942a245b04fa7308346349369ef26cdfef01c4f5185e0e2344eea3adac4b508b417f43b5de6713035d107038d7b7338dab050b34949efae5e9edb108d8f3f7fbf91b9e477f45421852c41fe27f890974fa0cc95ad89174e6e3caf02c16ebb5ed01a1384251339aa810cbe38f7c671a35e1deefe07c3991c0ccf51fa667eb2756cf87d961bbb67860a0abb3f6df4ec07b05053ceee4ee5644f80153d344e632736661ce9019e3caf1e73c9065b5ef8896b24b7b4ae6ba76626c76ca0c11523613e9751008b548f8f8cea0778d417efbc542e57120aaf908e6078fc111749464f87d33370874f9d846904ab8d71756faae7c8ee564452b76dade82a6050e193f50dc6f1e81d7ceab2a5b9eaaabafcfbe86ee404d4baa03f1b3c3d15f82c8195e0c46c52e6f77452a72aa646d3b794309bb1657c96020ac2a1e92674b1c8a8743dc64ae63398160135bd46525fd045ab642ea4621f166ca9ab66b04d3d6905cdf1cd90ac844e090916d104daf81983583ff7b580a91d642a12d009a33e19e83ac42a4f0cbcd1ee5528c7eebd4067a4bfce2cd48aea7a074fa986b226e2d859828d63b4e080fd94420fc92ed0656c08e3a56999ae8efba427a41108331c7dfe77af3b06133c1bc3b826d18d255e93bb84a5c348023567410bf239f3e68c81112349870766a4cc0fa61d49e5aec7e6104ac2c7791843e1dae20d7ef6cf111d57b6e90a4160524c02ca103f8b627acdc00747ab8a2d0c36c0af4a5d8962b1948c19cfbf3200aa3ab7530492d25642b7ca8d35b7eaad3d6f04cc72bc96cbfca14d5659952daca9cb5a153016b87336bd6eccb853cc0446b61451e4b966fb26b1839a3b5d94c1ea1eae1227737ba02908470b52536caba3065bb1c9f9957f39e191a32b0d874fd0bf8c441bb43ae0f57c675378d0859b972df3416303195a41693daab255b11002d8dd7c37c2c07a4385c8edc578e7c03222a0848db083ee57b9c811e21a239cbbe3f5531b8b96e9f2eeed39b59f5e41f436db93797f51238ee62eb735268acb273df85b1439e49fc086e43835afe17d3d43252472118efb5dd99560620515d9fa5ac0001fbcb8069311b20b78c6e93c467dc51dc17caf1324842cc1296e271f8f4d22a40544b354c426b6078029822c18dcb8947cede329a8306f8d9b4174c0741b585e01ba36b3ba14a361186a25372fb255f940da46c1e0f84e4a461b2d94f12a38d13295c8064d896eb831b6b856e008d355424cce623a7e891ffa82534e4f0725ff154f24c3aaedd6e585f6b873a1c2e68abedfb11992360729525e9cbbfcb59f5918e20bb4223bf4d25ce7c0f902526835540af2ddab39f60863d629d0d04210c92be14fc76632c18dd7d9b8e4665ded1a88d4c116efa86d2bd50e502dd6023a33038dd865b92692687799f3cd23e1428e90dbb2fba249170ebe7c339ddd987fb11bc0bb11db7f5c42b58e83ede7557de99386f153e9c23e4da5ff1277b84de8fc2d5d7eb5eb8e0682e574e264704a812ee1a5d025925b55be02bd6be4af980b3a03207e96f5905de143787c4d75f63174c598df61c0ef6968206acd504602be1017008faec60d37a673b3dc50793e1ff200e7fcdbd78ce6d51ef36d0d2f6af5e011f00598b751e577e6afcc431eedfd98779c3248d042b606ac8648713c15ed4b19eddb378a5acd3fd1d9adf6e2871ac46f063bdcf878af0774e74a2fe09ebfac3ee1d39897b13a7e1a9a0c0c9e905345170dab11f5a1c0950cfb2dc426b23a2d34dd11e628c44b7556976186ad089d50fb116e7125f737435ea5d965f6fceeda1703effc836125c53e6ef256b3071b491fa895c2c1cdfce5ac24f3000e50307e2e194174f4451bd815f67d41ba6379ca652af43f093009141a30c4a991c516680fb2d4028a473993d3e72f444a389e07d3a9015b25509a84c06d12c5206bf82b0059660a2780d76e489cbaa1a580430541122084c6e7e52e9875d0fda67d311d03095abb90a931790232b7fdd234140cf263c6087774d4f5c03c477d861ed5732540e6096559e3a7f91a58303f127851462e0070a2b4b0082914fd69d110df4532de3cf48d725cae0390f6ca280d8c242e74ba1633289e615bfec4359f7f5a92205814eac7e76d5fdd13391614d927e0a3411d9977650f2ab5d41fa5ee02e7363114b60e5dde46df40365f772cf32eb177a3fac1a8740de6c69721f437ea3f391a06d29d1e4a8cb1fbe644cd23103c84ab7a6c376c0b9dee84df0b2358d5eeb53fc6acbd67551fff2347a2a32d97cd6d2c915cf4e7661601b9f46746bcaa0b0e27ee858fadf14ef1808c158797b570882a35c21b47ac3a5ba918a208b49757e413e1678c17229a54cfebd8412e1715f493c86c0ce849b2d6423ea17453ba77b5120e484beeaacd7566b2384eaf1481012ecec518701a926df77eb24c2cdac5db04e0198b7aea7a00fb4efee01c6444977c880c672dfe8d83c81327bb8b935fd013b9a77b86d2d65f23b94070d51baec400a32fbfaa3602716b1a84ddf138c1ad24ccd3b73e912c423853b86dbd7548a37c7856a5460f2499066ece5d5a7f56521fc5af3e3ceb5fd98588c14eb3f071bed9e1097b7603adcb9b778fc21dbd902129791fa16afd250fa8e48aa03391d6a14dfa81313b669994f161fde776efe70b119809d0ecc569518fe9b13b5f5f26210f2b95d965f2d9c2cc397aed007e8d110a935272d4611e8d1e06ee7e3a747a0e3ae6895437339517a929a5d3e07c8f9a2d2335f9480283f6c47b4422907e03e3dae3d4aca7b4168c0f60c36bf8df858e0eff412878e1965538f772132648bf65eb075b75eff292ab7359e7954a511a4d33b43992c0a913e9bbf6ff079dcf7050e6b8b37b321503f796f84d0e718ab1423425debc2994fdfa46e054c192e4195b8d3e6523f37585a9741f6b7d0c8a9e71266b69689afb8e768179f722cd6027af94322630a9463cebc454e5ed3ec799cb34a972c496c61cfd8c4faa299c3d2f625ba1c39c78db7d0c23dbae1a315c190d236869d3454c0d7716ab603382dcfd8d96ade7421aa90be06512571518e7bb451b34524d688cc2eba39c5f53fef1b4a193794d214a2d90de429dc5e2a4bec2103f565d9adcf267817435a457d972460c0089861fe25e644bf260b838cd69d3460e94ffc99a73ae9f5aa82cd527472d9af4c916d4c651c2cb2f4bdeeb666d49e10b166326fd9820785841ea019be7dd2a9992a2946f2689158b2e59a7a67f72b504e3606b99ccc6f91a613ed72e77c900eef91e53bf8e330795da91370cf583fb0f283e4f25414ba1287a9daab60145ad097069efccc075205ac97f75d37c89dc0b84fd01ee4efd2bec228bb16ca1c201f3a67ae2b8500abad8b44863027805bc0eabede33d2ec83fb6c47a46c931bd1004e93e5a776bce6bcba254768ac0bc643f4dcb022c2a5d181fb0e05ba4853b243de4e90b1a2f87255d7280ba74a8aa0b345abfcf527da118970d6f955dd196a54070a4cb12be4230f31d87e30471f94e349077442432834128b33746c19a3a5eaee288d6e01578d7534fbd13d4291fae19a634d4654ee45da59fb117b0adfa23a78c8fe026ae5963cad438493cb6f28438821702f776da74329ffa2d077a871a72bb7752ea21115bf40c7e2fba7e6d88039ba76c3444f4b63744fe1a6b8a2a604a7eb6de493295aa533b2552f431e2fa28493f06aa50982ff682ad4379182709c7b28a1a34bd350558d6842df1b780ef130f084b4633442948181361da0b0051923b5d43452f8318b9f83fa1bdd13b96209e437c4fefb0cc5230270425961567d77d84a0567c8d2225c366b7aa338858038c824ac2164900017e4c973fdbb0f7005e1a0244575c39300c95b13deb739024cf39e71326352e124bc4a9b7b9fb9548e8db7c87447aff37f1b23ef148dc2915cbafa9880e24c6cf6990c43da81811a0d4d7323e87bb21f734093bf0a080083b90ee1f0bb293ec5fd9b21afde5ea1b9467c1cb4793c6e02d70c41f0bfab5f313b448cd47f1ac19e60161c3d87f3ffa0cff89031787cf57fbc4c593c4f8e6a9153b8e54c7bd7b70c61f57a851822019f42eb625f20373cf16960141607e31c432aff9342ea4ddae96bf406a7d8d38371c2646f729196aaf554a6ce0671c0b873d7bfe097f28a136875aabe0ef3160b32be25836df37c5c189cb3fcf1c2dd35019cccd1d85278493a5f0402b5d717a5f10cd753aebf5ec79455b931e4c1f0a151685ff3e5f5f98a388854e82b1516995f6e56fd8dc3b7b9f1543982c1e78291d04f69ba5715e2daff353504c199028b0c52b24227e85ea1a350b75f012e7b5ee28f0763229292cda84a931e1b3b6652cb12f92a2a96d6ce7bfb2a27e22f456733a62508c44c4e703c6b54b33564a199884d4d4384d3cebd918d310a837460b1c28d65bb245906b4133325a6d36ba7601c2cd8342ab46401f9002aa448818ef4d2788371a81c0e924382b8c027278aabf6de44369fac8a826606cea227fc7c9d72cf82e9eb0cec72b9fcfbe7c0e2ac392dab3f1df46f916eb29720c260a38b0fe1174e8e8088a10f18883b530b9da5e0ffc06de933a64ac3c6938a03af105dab5017f88cbd7d157b99395101f83232625674bec897bcc916884f35180db2e18b327de6edba8d549d640e0a3efecc4dc1622515fe4cf95b77cad586928c4a25ca708eb40cbf6f796df954565de80b027a009363bfdbe5966ee57dc49344212c0132b0aa64bb4de9053c6ea6f85acbdb36782a702f009108550f27ca58418baf3fa003de42d485b6c3fdba67e5eb92576b6fe9bf16ffed3f4b881178594ebf6a5f72524121dde9f79abbc9a3d7227cb12e0a7c861a3323ef97a8cd6542a1556e30f15e2160dfe61a376d777b05420df0663b7240515ed6dabcd7b0e3ef01ec490bd2638690f4b277eb42ed8aba42e1b1f3d5c7b45b4203a90188e02575e186bdb122cb6dede71376db5f7f198ac7835dad1ee0707a0158fd62c7925c0b13334f25509146bff6bea463c6e1d67266f774cdc53fbbd5f581522a14e3460afc97371f1047b1208143ea0415b14d563ac40b01db47628158a78f77ef1b9d8a4d5f9792b015b7701641c9cfacd04e124544dbbfc25664197e8c050f7c8fe65d6a1daafeb002596a32593d18638ec3efe92e16e8a283c5b11fe63914d18c37a9365d1c1262340b32f30a14b63ba9f532669f8ac763cf58e4f8db2bb0d736fd034a3b20ea53c48d9325186c3c2e109115559ae76a2ebc37426aac0ef1579d46be73fd03933212d7c0e954236b2d41e3eb645305fef201ecde67d129a05ac095173971b98d30121615b9d3708322311c84bc60634962b6e6e791d87641e89fe05dc7ca074c714c8b2b3835952346af0f7b1ac21b025bf369837e94cef51e81172e2134e973523e2437807da6ad92ee83d162f63305fc71b7392cc969f83210cb2d8ba3948330536e75cd0bc1dfba77db25f3de63abf44638850ace09a22268bd20b4dd3c4e41dcf305a225dc97eae3d22f6166d5d967baf1fb84e6eff3d1763dd0b94eec63e26fc2ac8f2c3e80fc12ac1bd732007fb4bf6ba85c45295eac209d24365c6bb5127698367cc6184c7b1b0035e2c252311ae92dd0773a4b34cb39f6acc96a1d31d1543c1a56491873202f960093cdf55100dbb603630ae0d69affd36cf76a88c3e006e8cfe5033cc8a81d6b8a20e381834bcdb6b1466c22e1dd5c09d13861d04b4301fd02467db90c39388aea96110f463db5fcbb29004cceb3637f9900a3580d53db3dcc8aa34d33839b269c0791a4e93a9c3f44eec7def17ad63ad6a65b4178f1fcf98bad7cd4be7a864f4597d7096940c2ea94de1952e9d37cde7985cfd911b172af5fbcd34ea734c0478d71d8c64c32140cbadd312a0540477fdded46a3c4d359e7a8784645d8e5ee00cf52df4fc987d6c9ee2df5aac52f993fbacc0c277342c4ee31a260af2774201dc68e42aff8333458ce438df59ec0cc3c8bf5e65b673186d6eaf0882c3869c0148779fad58edd961cd9fd8f21f90c7ae1742d71522fe2507c44de8f4a25141d4f1e22feefc03e3df2aca7bcc43a0e0103c5afe771cbf022e158fe3fdef8972a08ff863517a59d50bbbfdc98083d26687ccc5ca8a02f31264f4282a69417c56d22bc1b52654030c2c95e6d2b4edf244f5bf6f46624ee039cbef1116dd3e0a952fd9962a34fc6495c8d45493aaed8c80d35f0d3460f94404cd09665684b1a4f5ab4250dc7d6d82fbe5f85d9e05f8bacf9eb8f5b9d786151e295718aacc30a73e8ee081d9ac7353e690005d422321a2e96b9f826cb34cb00852cea11e86f2941c021422e84f09baafa1e193153d8a785014cc65ea55ed8612bbca8b7038bd96e80e7a2aa86f923459c0b40fb2f5f53be4916f7922ff5f150b163d6b12d5c4f7a8858abb1232d8f7fb1b5c60dbb0bd37606a7d732552f293383ade6961f3d4b8fdd11fa99fb205bd5d509e5b1f7ea24f8539bd700a07d6e02290c9fb471bf369cb41a935ffa4897fcd423792828bb5ef1940a81432b2b013095c2a54aebb750fb9b3c163553a0b9524e14c8323c1c01353c3e82135479da939e74dce61b210fe9bb31b4de23990a2c161a4705522cd8804db87a0361909611e23df3983bf88a2c561410c688c31251034bcfc1ad8a23d3a210f9d67b6078f1c6e59adfbb34ab066db90a10ce9bcc97f1f42bfabe9b4eefe0f5425233ac99808d24c6e9f2814602b0120dcd707e69c6765ee25531110034bbf0217f1bf47d32f45eaab6278e14c026502e454fd85f0f7fe0c3be9e43cd1c2ad651ca44eed9001f928ecad949af370a0020a8c83797eb549daf5c1ee5c961e72d6586a2358f22df022afcac590f337e85f74f06a7de2c6550b23d4eaa03255ead81d4543c3c03354246cf79b839b413842e29dc55a10b0e06f50b0f36506fded418801b3f2a71af8638280d730f89c8691d1a94f0e3509e8037fd329f2a61fe1c443caaa2fe42ea7e1ee4b997eb00da9475f36f8c20fb88f4ab9ce1c6e57e79811b31f57ee69217329ce1fb2b9f49670d1e6167a2fc02483f5706b39956e0e9d7d4f218f18c13a74842e2653e0fe0835a8b724e80366df714a0ebe96138c14ba5886dff28808bc81e271a37717830e5679f50ab468c1cf0d4db882144c8eac7177c26ae6a8d70030b1031a60c41d52244007ce21c26eeccafade4e786a592958cab30b4aeef20c4bf0c56df39828deb854675cb037847255d012f99a969fe41b5703e7b7fc4c2e9968b3ae30f54176e753dc2ac1cc8a23a961a39e98b844f955f73c81e0907224273d01f58a6721aa8a7e01b0aec8de66a44efc701924615efc1968ad53bbd81a1fe7938db95f050aee72e79dbda45d83ace8245fc6005875e75d681e463c1d8501a98ad3a10923a1695f280ab960e4c05aeb404daaba12f000bc4bc5fcfa6ca15527e1db28b2e6fc993c4721c81bc9e3d469926b072b5c2bf1cf939f86647ab743f5fcff11765eca4448b7a60d0be531e4b0b371b31f712b5ceb8a650d022b1d5d0b8fb2a38da8832a4894d878dc415d3c3905788d9e2071a42aae777df30181e364f06946662b9c9369b0f7cb4c4800fb932b72681afc0efeb76abb0a4e7391c287fef3b7e574dc6d374fc291d396588797790641832281681e7d4b65406039920c4f591f923c4e4e4bc7a5fd08afef181e237a96a5a534703926faae3fb4ba76368ac0d54aac6aafdb268f6e2e39d324e234c9eac2aacfda9546283320a23e11d722934e9194f6f584597a6ed86d75f0633d6f338b7edfade726fd895d0a8db4dfe326f47c21dfce2ff938e042a0d824dae5d7d474fcb6ff1e65cb9e0d21a620329815a949a65142fc513003b31c458586953c26d90246c4950e1833dc3a0ef4aae8c473b5be07f3c8fb01e0743f4976c7d9c9486075d6a0585421ae2cdc3723fcf92e0c38396044002f6378f6617779871748bab200db5a7e907fe6c18b9ae6265ad23176df5cad80eaccd0bcb308048b4689ad29115e9fa739ac6e483633cf7b90732dac2333943cf1dca94542a4b63796a600a6b0a5146df75b59d78c2f7f507f733342e0a51561f6b7b10cb2b37c2bd04bdb855f590735feb02d507b80ef4f245aa61c7f4e6de91a6bcdd3d9e0b1afef48fca40b7a44b266b40ffe811d327d6dcff94e0c65bfcd39c2c080a07f06780194e68b131895efeaf76849ce0d3a158fa879fa820c0d859478d26b4d9747941be9c01dca892dfcd5d93ee223aa381a84c915b4e2c8daa1d7df7e17db976b36a28db9d1a77532b7ec2bcb867a98950e3e53a8110f6cdcf3208c7cfa3995735c73a6a91d10e3b30d1d917295d5a8fc534b687d77cb936d9067d21a36867301d08135d3776c62dc4561459179820a9c06704d426f4403129037f5b3a5aa4766aba84f9e9155d59046a70917b30b000d5399bb478765564272f41794d20fc2abce12923e07ded50476257e8f2b3d3a0100d01a190ccec2c75d48a33a3f3e59d732236b817dbea0d80ce9f0d7de6310d430e2bd3c3f5bcca9907e43c52c59541f84c095c29f2a8f6d191200959d2da9603d0100ec926c55fd97dfe88e5b5b2e8aecf242c1cf7ff95df0d061d5cc3b1637b9843b69ccc809bb8f985551fb0af5c723d4c59c9025032971594104bd7ea03a41a83c00000000000000000100f9dbfa4e5d793641710b268dc158966f02c7830e7b8d6d1979d06f6e1dd9a4323a3e843beaae7318367444018bc732df8bcd660e94d22ce33f959e448b810c2800000600008077777777d80a1977000000002b000000000000000000000000000000010218d5f8972018eb119e96564eef6a3ad2c6033842301d651eb24e4d77c8312e82ebdbebb944a9caddb6c825df14b7dfa9b033f844fe5b4f68fcd04069e5ca29034b5fd5633573d199f9594f5bbc97f814c52496b67555bd8264ab7f06929c92b5aba6bc7ae80ca6c44fb1538118bc641ac69d5833ca4855a285bc10af8f3cd007323de642fbf7845e85f9c5d0ab0c32af3bf99dbb18de98243b3d9acf5d462da02589a4de273d5d57709f71885489866722830c348a9fb8df8364e86d4e287ba5ef63eff418a0ea64b43bdfdab919a02433ea9fa2b0350af95d1ff352103584821b367b69a140970675201920e1c90707552f4c97476bf4dd110e74b33b58960faa060d5aa55658ab2cb64113c4bcae6d55f3f317b5bdb0c876890a63e9767a5d455c9938689f9d71a9bb68b513c7c4f6b75cffdbf5d4f886d04e7804bcdc27c3e2ea8251855f7faaa0adedafb99e3174c1f12c1fb18224f88b386aae84627494d7cf062f92ddc04ee8605ad9830d947348964a0bcd2628450f6544110c900f474ad10f86408abd074ff12bd36976af8695615cda5eb2b8f0a0b100f246e2e54886739d5e449fb0e39edfdfd9d60359fd76b98838546744d99c268bf0055a25499a44fa32b917d7ad46628ea24c11edd8d4f3e9d2a29f91da212e5f25540931806003184293564a13013d9ba0448cc661adb84e92a6d1f9973f016b2adcb2748522f6b9298c998291d362b39fda69f79d59c83198f9bdd31133e553b22869c69e0f7f2ae8a3779b604bf48c0b32f84e4937d4b4fbac94ca7162ee7269e5a958d55f7320fc4e404210d57efb20d684142f779d97e7fd0ffe7d0aff669260d7995bcb20c5a977463f5f599f702a3a8484a9f01bc9979f554abdd3becf5f61d921a585f8951ec77e2f6273fa4ad37c092723a1dd082ad1a77c61dc101632217f13bd0249dbd65d4efe353cc1610d59e57d7fdedf84b589c88cc39218180bfcdf135e79a5500f73be3324da8e3b5335a53bc44be127a909cac0f8cb692fbeb7683a5f8b666029c8bf0434ca53160a9417acba1d561240389bf78b37bca9371dee75d40c4c03ca8d806fd8483dcc9b53a3c921d3d8e2744d051a58513f3e7e401338150752f6cdd6cfeec9643ce4db42bfebf5815f1dd6f86a17e9674770d8695d1cff02d9b555d14c1f99bb0dce96c121368470d7976548b120ef0e43d9275c1516f45610a49da004260bb7fbf42a74e4e6714130639bd25d97a7408dfcddd9de2d4f334afd2c1b885ec3b1635aef8b03e55467c26402043da2184206ac351b1eab22bbe1b0b0c3083f0f8bb261d480cc75be2d67072525b0a989e0bfea7fefc76170dffc060416cdfd8330f0b1fe9a0a645316a2bb25b7e8f7b7d4ac7a5ee174f3fe033dd1e0967b93e0da255c334d1db011bb1ff313cf2f82e9ced4d9a32d6a46121073a08d0bfb67ef077b4594741f7e25e1b49cf49ad0e11c64c59b127693b0721aba9a6f1086f759b6dc97d2c3b9e5854739a255b54835779a6c7b409be662fab1d3e91b2e35e6a5da9ec4b0644cdcabb639a76c59165a717239475706825e8ff18aa0671bfe19423daec6e32c21b2070f0b98d6eb4ae80114d638098302cb4948c353f93f981279c8a98a5ecbe43be2f23bbee17798c3bdfe04108b6afc1d5c44a52e0e540dc01c4030964b01e352b45428273754552678f9f35e6e87645a945de492fac9c978531813c5e97200e46aeae68e1d020eb67f224e3991d87fa7a86019523f07b4be75591e188f348220fb42db7b87862c046032126cbb4528e437100ac06c722234c29cebc6ef419c95b497020bca9720ffa6544e6bc24d1f90018583d89d4e9ba63ba7ccaa8d035ec4d937743b0babe63307f5062f5713002a3daeed0e6f98122c05f76baf11d4b5d2ba90b85595a48213dc7f2ffaff84aaa7280ccf3f09dd1d86e05c35c25e38c41339c6652c9f30f28176254bb305ff66656aaa0598ccf459dd8463135b641c4507d3c81fcafade92844a667bdcd32174707b67c66f83749ea69cf179b0c5bf584ab5225c305ebe579e44846483bff544e20401d0d5e4165c935419929bdc6308b212dfb86f53d33e7ec108a2515ffa14ac9846dc3b7c2b159f94a475ff9ff54119756f7a45f5b9113ad816d182244df09d1b29e68d165ee123e0c7fbdbbcb306fc234a8ea93081706662f83d774eedac77f320b98774cf9ba5f6c59bb0b1a594a0fe2a5c8242a29490334aab3e9214f09bf1b5ac155a7ed1912c86b04ef33d1f11a51d9374d687847f312c7bb0983814b230a1ebedc386a6225875a68e4e20b1486a525d1e965b41af9887d6121261c456e0e897c0434d470317920308b163a98c25b83c3eb36d0054fd152330133058c3c9176091a2c0d5054e07049a443f6e811b3a1dcb77b39474e3493b5f4016c3898a35dda5ce4f9114b02b0000000001f400e52c830670876bc97a39ee14df337627f1625d3a1bc81a842abb7dc5122d0200000000000000fde01cd92429cef12c32db2e9f4622227f3f1db68a4989dd575ffc8f44bb600e59f1117a3f279aaca6eec96856558d2eb0a625889c2efad3cd5378fffa7c03db688791e5ff79a5f50333fc4ceb31831dcb04d0d2c5cba5463b0fa21eda80c62f909d333771673dbdedf7bd877d562d1d9837b50fb3e43da3f436de20edd6c628741a13af080eba0a0f4a580d80c15d794e9726b638d2a57e3ea83d8b9db02347a5781e3c0448ce76b71d7193d14d87db092634a167c59284f5b0d552c06896636703911b5b49f6b0af3cff119c6f85ff5741d30a38800957c6d988af903c098f361f229b314de0e335f33d4c6156f044e2b8449a928a8c603caf4c275b153af3bcb91b1691d6fd7aaad21e5ec21c35aca31ebb87635118f8dd9f5464ac724e0655421df37040284b78ea0bac3e340fba98dfb0f071f8972baedcc4108cae3e3fe4bfaf7a8339a820ac61140bbccb38000a9c89f73f1fcaebae2a06eafe3f26d4a8d5abb29042028936696be97510b2027ccac7f8ddfa68c53d3e3bb9810fe4a3c74f3d83b426562d366d34313a646d473f2ac5aac0b2b6b55d65f4afcf2bb6f7d7f6b0776f1c00510c810ce3f00e700552b0f450d2d65c092a5045cbf05e7c5fb1a8908b755f0dc0a0dda8de63e70ae33f45591d8761678ffce13b2263f9e3d93c9f032cb8cd4972c707f2538d7c837e10cd955040d6a9b7766d53e4fa13381223bbb6c3f9bbe40b85f8053c07b00f173cb6460ff228722bb7a07dc8c961f790f34e97ea6a7b148a7e775efae8a94a86a45af60c50eda23de7bd6e5f5cb284f48f27aa794442ba3bed826cac151fb4658d92f284ae0ff0b3907b8f2ecd7b1910bf8410b98d52f4f6ebdbfe0f51a28971db30fce08cfc9a7825d7f6e94a7fb2c0d2053e433b2b0382f75b77261a156577f443e0a929bc6ee38a9b9108a0fd2d771ede1c27a7dc832f7c95c37adc5c5a8bd18c72a95a0fd0cdd7fa6bdbe9e9ffacf9d21782b8c0a3ae2e6105d0f5577d761c8dd41d568b09d498cc706b16ea68a88d138362099486fcd95b6ec74218213d55ed13e84722854455dd88c87d15e6abb54003268b8b746dd282fc9fdc9159e7ba573e5155f0ad81348b8a566dfdb5e4d4291bbe8a35db7942652586bd0f4cf93a4da6da153568533f1819d5ce2555ee2424bc976e02f0d290a37fb91a12bf04e5b1203a58a608c17503d971d3c1918fd62eb661c253a674155c60b12c4df39604e886d2d599a847fb7f24981b7ceaf38320910e3a635156783199ef36cc1382f9add0005ab9c09ae86ea8e6ba35dea7c7c5a09cc958c91dcda8b006e5ac73a0491875b0cf81bad7dbcb718a7cf6175f42319bf97dc2a53c072ba561fd78ba37ce3e3824f861d4fdbbd699c4ce2327adcd540510930fca1ac8ff930a7356869e1a175035a6403a90f3a4e9d7481892a45e758cd6453c3809d26596b8344a8b459a3e75d9f3e2e885a74c031bb48119d11cf30f10c11477365dbd64b8b12d044a83289369b6e4b63b37f8d3529eaeb2b1317d880ec8f80c20a5bc305136eebb2b6412f8a8e9ef6374f1adaf760a11a6e118a896979efb870a45177139921b7a993fce6e4940c26aac5e25aa8b2989c05c8ef5bdc275922d77b76032a08d32a0d737ea4b520e0192690c2d4a46a8a9106270fdb12fb01dc1ab6814f7aa34feef41e6c8e4f12d8c09830a5848b79083c0b9103ab269c51292bbdd59a61a574dd15eb7e899a7243086bfa82314b35ffb465f9e38139c5b35f7519fc63a4ecfee9cf0d82ba166c43139bd0df169a57d57ef101d3c06fb3527673cb66439de290d833c331dd691d0317cfb134e2efdaaa41203c4e1bd0cb7511b5be0750c508efb0d19a74b367551c069bd528b89df430355139a8ca5fbfcad32e75dc8d5801c7616ec24ca96b35ab32bbc73b67754c47ed247adf2bdd5a3965a3f1f88497c17aec21a82ea8413630fa89a4a68871bd9afcc4b0dbd3ee9cd3eed4b9b97ff78e3430d9d5baf31a80c073ccd5a6e5df1c2d0493a970d8cc233c16d9ccae3907d1f1ad20b58a4f57dad9ad4d179afdca8371560ac70d1b20591f8627413fec9fd72c0a2161a0f5258805acc7d9881e8010e03af8c7fcf23275a64c33a682a0c3655ac1d2478e14540d318024bb7d07b98dc43e555e6679c0faa5913f1399b300b2e39b88a780aa7d32652cf6fa819c69b7484b7585c2dab73c04da38ae3b257252ae80b7f8d8df2a0f035066563826d0fa6a95787568a91a412db1174c43a31e057549baf93e7d79b8b7151ed4f692c366db89425ec16ad8e2c7afad6f10c6fce1fbdd79b75fbdde52abd5598fce9646622330c994a373f302bceed30f3a317c86544e36a7d6dac0dee6ff68c11e4a82acf27cb5654ae1a2f82f9d9be131787e19bfb662001d850892b18013d3ae213ec3f1a114ec95a3bb8ccdf78c55685d419110838e4894d2c293bcf41f6f006449545100fe46bbf219c79859187275017f405f30535d2aa47ea74c4000d28ac6d53d552eca58e8a3c093370f9ea45712e01c6a55fa32963c9af28252cb28472e8c33a30588fe5bc0d5434f67de8c2cd525e507b928b5b98f5e3e360d1b564d992fc0485060c78270f82eb045ec4059fbe1aa8d7206e01274387bbad8e6bab539ff92b24ca3abb5d28d70da681e44dd27a9e28c7c9af8d3b7417b12895d5b1a68cb4fde6e1cd145b3887b5703cd6d1ffabbdfe315b66470869ac7a2c8012744081846c658b0868a3206c8fa267dfa5e7f9f3518ccb4efdb67e1dee132b023baecf1a56825dc827d139266f532c863acca4733eb7f042a6a6dd1c3e61133475096e5c0f931fe0901f0bf297d1e2e9afe0ef40e40c6aa19e90bf38c3d277ba84ebacf0a35ab0ca88a60b9be7825b74f5819315d9261a6e4b272210241f8d63810abcdc7b732713c48d1257c84bb69f8a07c623015b6fbcfa45bdcc17f0c7184c9a008f8fe9c07b51811f22c08366934fa8922f6f5d9283b1136b0df928f8284edbc38e5f16b151eff32ad66ec856c4a8279d60cc5f35872178a4e08504876f28b54676018e4ec6cd0f25cfd8754f4b6854a4495eded07d34941cdc704e9ac842c234183163caecd8122b107d6c99294d5a65a560b7fb72406f9436e4e723ea69d38220284482dfa0a62743dae431dcd883d9194d9d86c2427a19694f4f9c8baa577b6602bb3287f2ab25e09c8c0736b922e7ef82da078753c7355d192e7ae72d4d96bfbab4c2aa25b93b2040f7626997eec2e6c603688013fb17111809d7ab3b97670af310821f014c3516253e9c8dfb7fc9daa1213bdbe6677d7f3d6a5eaa8032401471ccd4e8574e29527d938086a6296eba2bb7703b2700848727bc8ff65d0aeb142045a9da21141acbd68f9b6506ff79bcdfe96044481d833ce674afe358dbb84c4cc2b556dee93a8eb5ac859e9530f4eaacfe6743a075f17da7097f0cb81c9815367ee056bd932fdc424e1920c3c8614b4438c3332784ef0c69d48bdc0ab0d79e763c162cae223587a331f7d6ea62edd73f3e7d77343e58994c56b079bd58e9494d0e5228acea01822295012b9117763551dbaede96c8eb38d64cdb3670d099e87b03b087d2812a38dfa9b38078b730627f47cfe0b1432358631969a5f64f93371a2d279cdcd20a92a3360059d877e91b5ff8bdede6cacc91896cd255a34462c4413b05aed61a049f5d50a1e9104db86e970736970f0e447aca0e104d37ad5f6ad984a5070dcb3972dfb1b4998cea2c334f445f917c67b041d1db9bbfdb3408e84026bd1586ae2b739528656ed2254b4fdf083e5ab9aa1604bd0eb937b93bf04462edea5edb7c217236c77447f0e952861e67253033bf33fff34290513c83a150d543f43454e62489b23140062a72c9669ed8456d601f14b5ae543d4105c205bd921d9177477330e9511f27d98b4d641f3d26529f7100efc85ad21bf621d3a489ce506b5b4ca52d7625d3caa7f3f4c2fce8aeec1f3e9889542ad9296576a368d5b659518026a70b30d65084ce2d8ca32df43fef95e3897d973787cee7a7e1a8a19f06e16bf72f12b3c2490fd59433a5aebf467c17fbac76dcd7cda115624060bb269e53cd564c0c82840246f9ba2072a80ce95ee36113155b36c2d04c7a2fc5d3cfd9746c17313c3c34320d3650f826cafeeb51d1d8d18b36628e081463b765b8a4d9e23057d20d374bdbeea2af66dd6e1bccc2c66aabc43c7457ca341e2e0c48689df6707a8f3dc2ca659b130ef6f6a5221f57df12d01e9ee154f8d6e1939708767454ba41f330094a90d87578c914a877b079a4a477840ba4705143652cff5a772b1ea2c33e0b6428b3425b5260832c7ce27f99716cc3b09f875d3cc2db69b6f7bbcaca842b2591c4d76d2a82e801e7574a8bc2a25b888f9afaca083155861a31b953eb360234e42559fa78163e3c7f01bae80e8cc29a6b5103527aec311dc49c9259f7155c02c5bd66b2a827b9715be4036fdc36b4b9fa080779e825a7c12f2c74131053e627408b92862042e6f5e62a1fa08897a39a5a2d4e97eb7ff219b2a37d650be4942281b0947da53101268db2f87d459029b26048b9a2dd5cec24356b98c3e93e9b24fd01204dd982b5b1f1f920846a485b905cdd0bf5b12d47898f2cca5693d0a42cade6b9b6fabd5445bbe6fd55dd072fb7a2a705c0933bf52248762867070b761c790cbd0a7c423b3c77fb733437abf404f74a7866b13a92554164ffca46d940040eff8d5bb4ce937359d2161bd3ae1db75313383aff0626ea35da8cab82089f1265a299ec8ebd2453e85543f1d7e9aae44fa4c09bc7bbcffbc9dd907d715c192ea026a8e931d9fd28f2c95dd440535e02919100c738703c31f2410e728848a60d9fe7ca9d91031e110866bd5341f279453e1ab7118debdfcd26b2e16b35577414eda0e4cf7538cf328343ea5a015f35f9254a320255561ecc329d1f88414d410847535c3a39fa3c97a1f4a3a3bfcc97abc742a72c45424a685646aec3e6f10e36cf8d2fd0e9b86928e3cbf0355435f1b3d64065c7c86e57cdddabf8bb5a8f1a2c3a53fd742bfd5aa42a6b62f8c0198b5a9443f5ca7b97929e6f8f6fe6a05afa1448869c471c8c35926e6f801eca2fef43f8b3bd7ba7b79c50f0e9b36e74ac632adf39ec397a254f345610f0cd1ec4965a273153827f3e1329f8ca9d1c40246a31b177b8e696b3323e3983781987d9adbfe8b36804873d6d20d646fe4e84a8723d0de4e9dc2fc8f2cf226f361dd0647301b38978a6277c13dffb85359e9f0d430cf7050fcc96a0e0b4d722d33536c735ca335d77db679be76dbe81846caddc962fda71037ddb15c0a425bf8a6af78932abbc0cff1d06d15f6bdafb3f1da0b4940343f2ee0ddf4f20795451ac1b923cd14941a1ebcea2849f2053f0b6ff6721ea30ee68d1cf7017dba2d65c73c3cf327b56aeb13264a15aeecfe8a132b532bf8d3895661760c8f330193b43fe2ad4671310d714e455284447df207b53f19151cd0f04db5912b55afe6080174e4e7c028ce62dc7b9a3b83624a8a83c1e3310787716a0aee4dcae035547a9a4eaa21415b0a786627c941107e234b2651935350a480035fdca9775a5db5e0803c6597878f58bcb8de1a364f1de8857c9d55f21548805055c1bc12536f94d98fa3c746f3456f99c94300daa2b9fea5258dcc5d9db2d286daa6cbc849d708a79b5fc693b5aaa4f13e2f49da283d4458eb848782b8cd5003fb4e764e1ddf0d4635f81b1384d478c7a994bc6f45ccb63e70c3384f616d90a945fa04d341f14b7bb7312ec1a17f7b759d52da56834e21a9c7c04c13a5ca01e892f81ccc2790e905fb0c80427d0519cb8159dcf8119c34b64291dff54e9102e1fa1388d4ad22295742fdc8de9d46bf3389d1dc58158a89e1b07114b373bae24dffa2e0b5e13f4f5d742b26b0350fc03b9a60c508d2b68d5c3166b3dc4cbb31ae52ea49118c5194d87b1364c12841bcf0fe60b4e606780b14a9f54c704176f29fa19956cf2bb4eb524e2c4c6b260883b6ee0626059f2e5d56ac6b813efcd533a1f9b2183a5dbeb33575fc97ce37c8bb3c4a151c0df514440181298a383858a3be9abf83adb596ede8174021192922f0866f35d252cd60e69480e19a104cf3d3c4b83be91d7a138c0cd09c144a791fac4fc32e7ccc46752485129ae756bbc6c0c1ceb2db22999fc57a4082043f2fb110304a5b94eaca04b87d26a5b9fa2618739645b24c82bf74d362a2d000e6d0de4cbb8b42bd4f2247b0b7754f2826ac9ff375fad4e11afd9bbdcdbed3752e5f051045f0f6bfb81ee34a85aa673ec1d345d26e25e298c32fe710d65cc04760c2ec51638dc3cd5e8d250ebbfafda553b96b1213cdd42c9bb667a044c11e656bebe913063ebb8be5f07d0b7b2f27832a4aea820ba1cb57aeddb05c6c4f3b9cfec0fde43e408320a93e2e9cbefe61bce1f41e8312e1ae329771fddfd1e384a9a1cd8453530515b87e7fff7ad04a7e0d1990f8c3782351ed8d9eb0be225ee21c109f36a1bb2faeb905c7180e2adfe7b7d979ab12090e920a3259ab5803f544919fb2c7ad392d29efd338c13cba8f1de6fd0221f2b6d99b203b1ab553e0b01463b09786f436f6d6635fb5795e428acd1ed3302d9214dfc1f354b43aa7f36ca30638114db9aedab5b04cd4b3538244e16e581cdb63602bbebd27189a0983a30178fae820af348447e04a9113d66ee111aab8695c9382526e1fd72fc221256832be3b617aa078434aace33badb44a6c8c97dd02f081d9ff33d3b4c6f6c09c39c3462200d8b9232f7a7b3db9cf82ac106075b1281cf28c711d7b9093de5f4f138ac21b25932bd7d9e76fe4f304bb0b2c92f42b0c70d2687123a5c832b7349ca1f49920ccad0a65f010deda1061c1d53c97765bf2f933ca3f1a08004ff3773a2a532d7ed3d0f826bc0e2eeca390d3080abdc5e9a39ec0a82da384fc5d6a9e8f510decd12e5034a389f2dcfce90186c744e6afa229d5435153a195d0198613e44cef9bf8111ccbc01c46fbb7d33876c7f67a82131a8c12c5bb99ce5145b18980026878adde11fea8eb47c1320b1fa0fa3868fd020d9762e3248d07865e4416152c44e8de3ebffb3eb4f52410322ae05fb3784044d89f5198719e2b8139718b5517ccf4c6db5059474061b284562631f833bdcf0e4825a06031a320d278c399876dff700f531be303430c123cbb7f6de8319d452934dd0357871542319608b17b8848d5f676052f25e0215007f6dd0646ed2b4f06715b33c8f5f72c8b7fd8ab4a54e2268e7458dacee7e5d0fdbef76f60f0caa73cc91d22b8fe0873e0ad397c8004c6e3b200cfbe50c78a6af6952874b6cefa8a77aa1bd35a8d6d4885e6066072ec22a55ef171caac3f78c1ce2b16953844fa1a4b9c28715e4373dc31b54dd00363faf4b551d9b44b7ef0d7f9af83f127655ae50a955392a951e2e50f038eea243354dae199356264c2e83ea592e13af9a28d2bc85ceb10363f6d2e8e92287000d6a62c4470eb80dd526d3a5a65ed5bbfda0198a41f8461e02291323ffdf2373b08b7604592a4f733a582f06cf7b8d88b5b42f0fcf4112122a6af3589fc73c96f9707e8df6864ba5a7343e4b1a812ebb67267b496ddbdf3dce40e869bf0fe67496627ab53ef850ef9bfa75d1b79128b0cf2f66903f2e4a23f3dfff66d5676ca89a951f15735c07cf345eb8bf252ced5afbf55c063168f51bb057a9829d42d1265f29b45378eba823a4730452b3c346932d42270150a22d088c42cd71d0c3719a15c935833430d02cbea184cf3493dc92eb785dcd7ac263018c222865ea63e0f4390ae7da1179ae4f6f0cf574107ceb4361e9ba1e61081234af115d24f1e532d1ad0c8740f3fd7378630da3dc79849dd39669f9be7dc37008ba2b9be2490994d401614b2455c15faecd37833fd48e8ee2c9b54e254a109e34c6a5b5af240b369d3377f84ef28997de713593eb862ce8ce023af9966316a120c998614eb9b6bc07cac01f58a08959b043ecc28584da5f495b544d774a88c0143ae8cd42f2a6ae2a4097b17979589c3713901348d9ab6f6ff5856b43e847761e50756ca1c335afe4015eebeebde9c77da98a14a53966de83b23d5e210b3a6c0a13f00b8a1645cb29ff2755d47bcbfc82c67ccf145551aeec0d632842f5865c11700defaeef420b00b17f27fa6c899117592e10272ee952d2703f4c0cb1832423e7407f338e6c013a0246425bb2f62b42a2daf3c3303a3f2143086269f4c0dd01f86cedef812b9d3dcc8be752e4ce26fd7ff1c4a0089500032d83b3498d16032e4ca70337e8161a48c6837d52ce3d254a04b2a572b2d90449eddaf5cec504591140d051495fde0bb9e11041e8c8388c77eb9d4d76cf678d8c8b43ae7ca6fa822740de3b4baf19826d747cb30c85a2c48500661ab8a1d2bf318720448140419b0068709354a8332ace2bdaaf6161658d21824e4322615346a44a8683d9d618b632defca6935a08faf8e140edf324a78a1f8cb9313dd314409a8ab7c54cf8297f194556f02570730624a46b9038f6f514b68b928c262d1ebaaac34a57ac82e0e81b7471828e2e0e6efac46f1307dcd858bad71c628e50533bc47fb496d638abae195f5fb61f394d1a5db851863b05df72b73b549ff1262dca1278e8f27dd0c7b1180bc06f511ec50515a12aff562566a95ea6eb9c2ec08b419832fad5f07e86ff0cd768d579cfd193bc5f25b10f1ebbc0e071155d2efe14123bee6f010de309153f9170f22a0f652883e16732a2b50e9cd8bbd3841371b3054c6123d229a9ff5a1159763ac67c8b2d6fd8f9b4add5e3879e1374315e53124d6d393ff0218e572525902fe610d3053aed874b6d8efe596c82ca5a1cd4052e89c7a693fff71fefc9351ae83317b61e18a388117c4c6cf4fd8f0a0b13247cbf97f978dc0987650c65206376f36af484ed359bd16c9cae71f9f0537ab4c1394a7703bee8702260480d1ebf35b3815d17b193b2ff7b8d556520bcf6d1cb1000a2187ff5951c381830e8051cfd9135e83524184b60de058ba5cb676b93c6678fa0bcca4099dc859131df302f5936b4422407d6b92f49f1e9f179ba73b0b5b260d8ffc0d930333630f618169b91cc857f44a2f01a123ec25f74faebe95902ce0bf3a17442dca37050fd231cf0051f97f53d63f3b9f4db62d21c1d517f319350b1c5690ea6ee819feaa8ab238078cecaa39238694ac968be50563584a5f2a9f70f6efb7e05bc473bc82a193cb045aa9ae030e6d035fbefa0e56bf81cab7fdf282a9bbbd4d5ef1e1ef18507170fe19123215b74dc6f5b9f0545925b40e51f0f4d717c895eff367f30bff4b0aa02e7de0be295de8e790aa7ed74a84c7e4a5857c609d6df15ed667aa92b7c6b216e981f297e7e9212368ab8d12a09f4a604ae120ba159fca6426ba68681966aa3cedff0bb46391f5496c18ae68ca4cc9bfe7e8b4ca41eae9bb2971c4d90935b9616137f94c13549cd1a5b02644cda29e4d7b47a32b2cd8284d5618c39e84ce29cb972e3dc01552495212dc7c4c51ae2e1803fa110cd9f8c621345c363d73cf0bc0182289231eb08e7e22d2478c1ee1a2d198bc68dd5e57eff62e9151ba90d2f10aef00abf1d81e204e0f97dea953a9380cddd0847febd24be1977e22fea6fafb77992e67e25ff8247f8f2396b45334367e1e6f2adeb18480dd379f6fbdd189a009442ae12289c40b62e92a278d1772ef101e5e8f9345723942a07ef4685412e3bcd27862470d82ff1cfde43099d53d4d87eb468dd5ecc191bf43b9f55a0f79c9c5661f41e64235d1677d3a47704e7e7eda251e4df848bb6431f048ea40a3311be515b1474ddad53a86019ab3a55170726afad3ffcafd92553a5d3b9bfc1b4ceaa91b9d7d6f9d7c4fc2e72d09ced8ff94e6fef28dad840d20bab7cdb88e611f5991e31dd1f6c86f670b3d9b8ff60b1e49ade7a1a9b622ecff083cc7807d4f2b1360f32ba8e851d4448b1ab3ffa4839e4e9c5b982fc0d3939fe95d86cc47840b4bf551d251e25145a420932b93ba117ff0f6e8b451021821f935c918cfd38177886161f2bf316965ab7b0d47289ce851de379cecc1763f098cf2f77aba45a55e717c7a2b0d3c139a49b55de81fea111220b23373d6703401e32c2a8800d83d7bb2d53d35a20bd479dbea4b173c5c625195ad475881de197e2dd9f874eba4142e101078f3097d08b39211366d1b11b52a780105441324df9902d84155e53a237a902670117bd5b11f40cc4477dea56c591c23bb7de4c79047225da5a79492b67849ca68a3e3757b1ef7fc646b5abc6afd6d06f27665968b64be1889329b980079a3dac8d872d11b62467c5afb6b701ab070b487b5c752f4590c520a824ff5ffb5804fc74307e5000dd364c5ca7c03d78396bcc8d9db03170667985488f18bc0e470e01009cae4cb5b5ed632ca403448104087697aa5ed2a4e745ed811d5c90ee5f062482841433d91c54d55579b30074b655362b01d8f5b02491dffe97f8c0a27620aa1b01000870c67fd27b9189abcff3f89e7e7c5534bd386c9787a148fd2b36973429269857b176244a08c4c151fccc18061af7440117ce484530e83253850a2b8b9dd80d00000000000000000100ff83ea356bc67b63c1d0599d28391a30504f2dfbbf7202af12145a27b7f4750847bf843f4ab0a219fa9e0d63cf33cfdb836adeeef72cd68d2136ded95447f3260000 diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt index 3abca88392b..82e7eb1cc25 100644 --- a/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-4.txt @@ -1 +1 @@ -040000005da80102d8bd32f69bf342a966ac17350fa01f549024e9374d07a32463ed6f4e3c6d4e1fa40c7f5c5a4303e3bea543c7c0e0e5519759131fa8af8f7e0c342c9a264f287653dd7f78f8ec747c8955cfb8ff4ad30330d37cf326dcda63089e750e22254a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000040000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff06547af09fa693000000000140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002c000000000000000000000000000000010228c976b2fdd498d527fcead407fc04fce966afcc329aede1019793a7c202fd0b34178567a0e6fe5d2fc73dd36f129843b4338a316ac2c72af146d8e2976e5c10c490f6bb54e33f7c6572d674bd1b828f65fba14653ce852cb6ce9026b07be0ad12cbd980b5491da013c3da404f277187f282b0e8130cf372adc1a2e14f7619022035aca4162d88aa8fa4fca27614f9e1b627654ba12870270af0445b90a3c7035b1064abb1de4228185b12950a210c560799ffc28070dac7728d767a8ad6d68c16d20559d95862b068141c47a68fabab4be91b8ba788b52b5ee935f40ead62f35d181e37ba5332dd1b7fec7b0735e981a8179a70d7f4e9dc0f9b277c5ded031c4c055f3654a196d32fd7c94ca4998f7116c6b0ef7de6e72d6d6ddb117bd9850770363e2325f013289f2abf054c2f0830c60ca3919512bd7eac9418d28f130e098c1a9b19e5b17a69f90d61972859294d7cc66a5e6652211480bb8ee9dc7a66f7d1cae4c0fca3d0989c6a768374961895638bb740329949c3e8d7e6a234bbb4481cd7085a831273a949db55ed559b2c2a072d6cefa79e7904395688896802921e066247a67647a5ce6370c44be1dcb6c18141086336252e9792372f3156ee92e244324c7b91c9f24fb653e654bbb04278afc82a441dce5378a193ed4f9af1c47ef7fcccf5cad89bff67d5743c33f6c972af6a2b49069dd4d34579971327a383942c516cb3a18e7d745a4fad050e6a501820a7cf8886a7571e3d3a0a7bd1ccb8f08055e0d77b55efb7de8056b52fa1aa99d3512240a817f4aad2a35fad86f71e6f0bc7c59f855428d13d14fd41019d19393154675a4a2f5b400b3abf458f690071ac69d42d029035effaeef3305263bb89e592a790413efe01e0c02575f30f573502dbfa3ae53f2a158873d93aeac94671b2b6f18f89fad271d2c24bc9e69d76d8d3966eeb4b0a27b7878a1b4f4b6187998230609295c2310975c0c931ec77eeed22e9b62b86895aec417b0a7111944a560ebdab49591af125a8ba79a8133c19aec10caaebeabe5c4ddc8490bb11e4bbf6ace08e233982c0f5bfaea68fbabe9766a26fd50502a1c5d8b446f67c69d55cafb200bf9b89c42dabd6b5ebf98c4a04d34149aeb721343661bf2ab384e3aa908f94010183f79781354fdb842436cd05658caf69869c08f80a73749230e6aac402f62f17ebdcd555a8fc7eb580b51895ebc164a5bc9398f526c2dee3831721f05e8582cc3a713e4b8a9fe8bd176b812c65b2df8f9120f955fe7941e2ff2b9261b1dbbc74270ff0ec4be36f4c7371912ce7bcfd36195809690482f0763b4f8ce4467ad07f143a21b120f417e2fbd38517370822ad809c6c1e70de4babc21dd5cbc2ef149a0faccc08702d3a121b1321c80e67b29b1d5cb08261dfe8010c8a7180d0d287e937be1f33a546cf3ab671cb8e912765a573676f5accbaeaf26b37b7ca39ab84ca040a4396a9b5b11822720ae2ecd8e9162e6c2b9552ea67fef8e6f335fa65dcc24038ce8f930a4dd4e56f883bf94373d5bfd1d549f16e22d8fcab5f97b95409ffe2a2d25e02f295afd5b477d9638585e016ef248ae730c263f136629d91c2fe3aee95002ca9b6f75e462c00e81eb57d3b3f45a257c9f6c41bd558f19423d4bd2356a1369a8533d4da72744130700dcfdba312555c59713d8730dc758f29c61f8ccadfb7f981b9f297922b01d1182e75ce8eeee8dec4b6c2889b0f89421f159936a7b9cbbec5d3765f7acc5278891a5c50f9678f847d425d93b6ad639771a2965af30405224f201f79c89bfb0245aa1072c09af44739f5f6351f2fea8de9a7c4aa8281cf990ddf31f5d55b069097fa92a620b8a769ca1ad82cf94843eb7a91566299fdd0d7c20d967cdc9e1874fa4a5b67dcf6a50e0d0d37f05b8a58874673a66423f1f40732cb81cc1e5e52517e3fa701f50ffda12591f9b585771f916d0dc4f621b0e300530fbf1d265b6e0cb79bf3b5c5aac7002bbf86391b39aad5f29c0723d9d76cc8741d2c43f7393c0b09608d6fa3e52a7e0afc583ec0cb3d79fadf99678bd9e0a38f9ec1c1a56428f630b0b9c8b715eb3f9300755e9a4b839f8d33b9e14a94d6d19994569a7cf95f2d2281c3f5429c71a57ed8881f5302574d77d3f9981a10ba4c49f71f12818b0ed6a15be3d6d87846a71b2ac32503ad208b895365287d9b3e28a977688cbdba208b0143ed84ec9c3ec59affad3731170501554f11748be5167068fd27b0a4df55a9648bfd7a3b62c63a280308dff2d0b9eeab068daae1e6cedd1976086d2ea43555cc904ac3d0d9fcb563cf95d4bb7506eeb19ca9239ce1cfaa7a01cd6f2fa2aefe108de75bb76fa861b6884ac094e695cf3ff8f81393b3a6952ebff37cc5de5d846bd9e245e8731b2348075f4e0ee38e079bd94483bff86a4fa97f803f6fcd89fe60b83afa26970827dd340000000000fde01c3ab26819e9596f243a5cde52714f4acee3a2c86ed0c444c82e764bb7939eef1ff87136e6e6935f8499b5a22cdaf6677aebfe70e6bcdb3098fc4c72edf51401923fe5599cf33065dc2ead934664a17825f5522540e6711de49ac1742ddc8941a2a485372f74ee82e8167d285026b0e373e8e947d84fbe2db9a49d26f062b749bb3d1df9801ef41a3b388b67be7d1a99f66cf455344f64bd76ef0cf32d4655e788b231fc525cd5d6a0530b86a1ee19c8f6dd712d2f99388d25bcd0c7bfb4acd7388a51a67d20acdea41e6205a2cffe6968aefaed038c1b45132fbb723b47a622abc465f903d2a437677ec6d907acc1acc9a12d47efd0157ffc4351e2d36262f5ac522d651e96d1249ea92ef05d5d8e08d22075e7c9e9fd2260fbf10b610485f70349df918e27c61e2cc4debb3a2a3825529c631e1575534508d7162891998f8d896eff75f1bcc67e2e5919f4116867beaf9edeef47e7199e8fac82b3098a089c81454b896057d5b970b31cb76667c958221db4ec5583b38c427dcddf0adfff47902036eb4529b9d9f6276cd4add708ec76a31178efbade53d524af0aba1608b0ae38f7837cf7dcb8ef7598eb60f5633bf535c7a26bb23cd8308207196e2c971f292b0df0b177d37d406201622b697e9d1dca47c55d5a3fd1493a713801841721b921ecbc2f99609d97223840ea731d60d9de386fe97d41ab43bba3e71607f3580d3176eb26ea92d3fb3a5bd675f517edd883ed7ecc040d1fdcb1cb43427297aaaffa9cbc69d4695ae49f0e15bcbfe82d0525f296ad3d65bfef8762bb2cf99c1e2e68107a5cc87134e44387bf0db04d79957f22969912f13ae74ba3ba722dfdc0821efca5f3e591fd73e81cc560729c0ef03ad69b3cc53393730eff0d74e73d5ea8a9046a06b0315e646e61b72bf8e7ab36685f9aab55701557f34fa2109044ab81a61a433d43ebfa8fe7b8b330bc0283ae6b077919136bdb48d773b70005b4a49911609b882e69756a92dc8aa2c509135b5d8c73eee457b6afdcb9fb8820feab82c94c704cadb08038f433e447d214f253faff87dece65557a23a58ca45c917987b678c50d28dd8d5d8f1d0a3525409615d87dbc51fe7c4e7f053b2637e0cccf90a8b51225f0b04f974ef4b85301ed8e51aad35fd040420b6ea51da8f1d42de3938d8cc30c2b229772681d15955782bf4a52201261a41958bb96a200572170e91602a5c47ceefc72f091afbd275e280b10e5014f8ecb6942b6cbc8b5788bd797171fc2baf9bc41d1dbc61dc27b672cef0f2333b622e83c0819e072d44359d23e85e8a2a0caaf15d601f2e981abd51b7da6e6e697b26910ab01824289a203d4d9be413c5e009b8d4c9fed13ae83063646cde1ad1a883efa37741c1ba1842fa7d9379f78b76baf5f7d8429263fe74a179160fcceeeac2747075eb25dc1fdc95fc39f156e3448ad53fdf65f8e90d573ce1662d5aee43de6e9ac93d6cad67410862011983f9261112874a2c18ae683847c3ba91881da047aac989ab8e1af4e9689372e57e9dc295a2e11c326f905f97498339e6af5610a3e88c247c2efa407729ec8ab3d8523c8382770123feeb19d34a596cb4d1f177c8a41ea3b8d3e9ce531814b2bfc99d0e78b0a9d044f27afa6bc66fb0b8098e2cf17ec8647341d4dbb26b5549453af755c682c907704f0d01860c829d55616c2229f876089d2503af70ac03fa433a40dc6d98758ef252387d0d0b5cd8d7115a5e3e344e17846f6404a142353a5776bfcfa511f836a6931669f23d78847e6062dd516c76acebdd341bb47daf2b78508a21689f0287e8f23564adde38455930331becb37be1f1f3212359a79c19c8a15a1a12687210da16828dd846827bed41b16893604eddcf1548796bbab1225985cf98325238e151702c6f09f549bcb98688a537991ead738f346641c5b108717e61300ccaaa0ff829f904218db0d574abebdd0d731fb52bc7912b23f5fc12261b2d090e73453055aa6767f6899c1827070aabc42b70a097af2660f2a5e73afe4f801083cf9c1bba1c83995de03c5a7fbf9df4501f01ec710053712dc48eaa2a82c5d22573991a2b6115053b33e9fe01dee70172de7f0a564369434fc244abd2ac79c7ee8e567442be09ca7d8e6ebc4407be75c7f60c402002e12b488435f2aba88eedf11c872a8047030e3bb6e1c46525c3f83c6097b57df88b44a67741c1d81564817ddd788346bea880ebc50d5016752ecc0914fe5b380d34d5a8c57ec094cabbda6cd20ffd26d57197b51d76cf775a70169579ec131a8f76f9bfede8a8027f1eb01a9d423c55f85bdfb026b8d8d2fe448f359427264b5ba950395f81588106b232db2c394a67642328b53a40aa16887d905e31c40cfb96ba4d3d7783c8b483fb841141d676212c192064b5e992d48f50e2d778f51372e3bcfc1352a811c77c2bb2d3fa30f14be36f983ac831acb80142bfb96e8fdf00310321e4940fb0d7c2aaa0564d6e058368069c33a723f7eff8e372d7aa60ecc4bd89ce79503e3349bd4a176946a570e760acc2de09e8f97b7eec7ddbc33c321a8b2902dc02d592c4d0572d3b1d293048771541abbe66ffe611fdacfcd66fb596fe6670f07c78409b3c65f42520cb204ce417db185d5acf3602bcf1b63be28873a18c2cc68cbd02f76b4f7be3e750364590012df8856306e262d5047daa51f20086090ed5bcc271b69ed54fa60a209a0b36ae59cc4877fcbf2a9759f0e2edf342c48ad9a904f2e098fb0d75baf246cbd5443629e5499acf9d1bba05b1dbcda741a4a533039bf7f3943cf28d4bc351abcadc52ef3ee669c2c6f32a92f0f58021acf5f3def506a0c06285368554dce39ffee54d4687fe01453117fef6ef509cf4ee00291913265450691078180583436db55d32693afd249e8d050379e3a92dae28c0fa8eca4d969395d879a09dde108daac68b08ae2685c75fd7923ceacd94aee8486304ec27321372bb5044201ef3f272775079ac230e06d9818a017d1f1a83e75663904f078b705dbfef291f5a3c88df0bbabc19d27ddfaec39445d2f8f4a5c5ade8cfc143be513687bb7cd0c29521a2b6c6ed0ac42b347c13a3ee23ab996c3388bd4ce6a9fb036d3b5bfd7599a324d44e306fee30f546dec2dd454278f2bb1402b192ef198b41ee6e8da04939ccbe7f3e44911e10d1644d94e9c9791264fdcb78646a8b068730882a34f45d409be418fc968b67814e7df7a2fd70128dad36c805311ba87a42d2f5d645c7491988fad161b3f70007f946492ad34eb223d7a4c7aad6e623182531ca02458af850ac5c15d7fc4f7323daed347327a29a40fb575f52cc82d3315fe39bc789ef2f97588445bf13f983a7ef115f7422c8a6a40cf70072b351fc1915a188bfdb1ef93933b1773914c77d907d2ff64b81a79a11ee6f9fac928dcf591ab1cffc1e38f7156f959e0d8cfa2b466d6234919ada47f12b450ed3fbe8df9a2a409afc89c8bd1eb873cdfb94acdbf32192bb77f374c2f8f81805fd73adf3e8c37139535ee2189ab0db266f81d7525f0fa9b816f2bc98affecced141ee564abfce1f8bd1eebe2893b7d94fd512f0506a38d0b13265ab0c3783fa569f031543789d2a36251ed10b254bf4cc358cb94f7a4400fa34ec29305688ad93da6ac4787f200da840c72f59d9ef6f6ce12b2f23441715d4afa121b91e630bf308d962d46a2d2c9b7ed900b264debec1cedacd4a8ab749cf5616f6118ca2777ddf23f0758f2412e06c4e8cbaf9afa8b4ec33f6ec855e54da8f921f73ed320345287af572efd93c46411bd83617b209e604ec3a3bf6ded5ea0d8598bad687d3af78a03b0a773e0094745256a92b41065c6b44f6d28aa75d4007026c389cca8510569e321ab6c8358faaa3eb12c2a1edd0040c42a4d5e01a3b3f2b0eb001fb2b1d80bb532ae55c161d8364c5d8914d44ec1b4a1b7bcbcb464a13f057b2017dfbbca6e2e105b1db12855d78053c1bf1bea87c8f3129b815e9295830e58a099ec97d52082851ba07129e1acd5f713693a737dac461a6f880def55093f3fead509cb63a9491f73de93435f2c6a8eaa62d424df515a96ed89890efd1dd95a447b8c76347c60f1ee42e3adbddc3697f7c51b68e06b885dd2cd57031f3a4b5c04760f81083eda5e1c63217bfd8454185315641a10c26342fe7cd0ea4c2fc17c8783eb326df48c8628df21b322438ab418ed5b7cc5f4d992f81e00c275d5c28be5ef04fd77b5f9da4f5d0057b5e7b0d5c3ace4eb4c7ea110c7c6e2ee3fe42045c0e59f9a3e7f8e81b702926f38eb6d3ae28584e91980adc1d35d8d0f08b7daf83b127f04ee813dfddee8312c4546854b34f38805adc9787bcdabc7c0eefcac3462d4af79cdd7acebcd28c11844918846b2002973d4bd94cddf204f8486b0ef10aa4ea9b1aecea211c8ee118b39287eeef525f85f1836f3dbeea234645b9fb6e4cbb44b18deb434fce01273ef66f591b4aff1b26c3bd1a928af8d59e98f74cd6eaa6ec95bdcdbda419f080381ace0bb14472a5824854750f81648e9201ab3384ac667ab03bdcadb0c2f93c38078f96e76f334b5eedbbd49514479d012ddb977a5216b075d5872938a7654d389846d490ccd8bf4c93739035fd5f4d33e87ca72a49d25014ecb528be95dc5712ba1e0cec9f0394969a883fa1e31c2ce4f859879792fc0da56ce41dec893f280939758a95027508a22903e7157aabc3af2698bdff601d4f71362d5863560daa1820a05fcf9a294cfa1ee6264987050e1728032238bfae62fabfcecbad875ca5024cdcd1031d0b18334dfc65acccbb97e15c213f19dc07de8ff9877f75872265245682fc4ec3379e464247d8d1905140b2f8f96ba4f2d7a988d2d7a8750aab9b15f5d133487f74de29a1b3a06bb86c05bb822cde5bb599a553f7cd97f58ebc422bcc2a4d0e9e309fc9ee23f52b6274b18a2c583b5f31f3f570d28c712769ceed09dabc9e5737f99d999751f901ff49d66dd22c3c0533e774c627f6b7b07e9b33205840849755fa56c9e8fbcdbc87f14bbee6d6f7aca94c06a7cd1c9af57f6e581be9d3866301bfe5cb3e519c42ef0f8be3368368246efab690095d0f2e0f6c6405b5e7c79a7b129e884ff31931a9bd5d7922c495dd733f9ccc2c609f394bf2f038ecf7dd417712dd2da9ecda3e0ac9086d1953a185f09e1a40ab9cc5403f60ae1c672880a4036e754c2012fb5c356e313490df69ef5b062a967720592228bf182eba8c69bf20e225a77c319c7d2bfc1ee508d4271a62928a38bb616101669be3127642578c3610a9212be099a9e345409abc22b938f071033b392d9578ab697e060363cff0791fc0c110dc5bbd0b24368bfa568bf2c4795cc2486e990031aac73d6ec74b316126fc2047b0f017a924ffa581fd4526f1c68402e8cdec6ab0283134aacc11f874feda018f45f25c62ff6dabf0dee5d0e12f72e7fea55bdd3561822a11be33bd9a92aa4eeaf58ca7021ac6f0cccae158134c7af63a1c89c53dbe0b18a01be28b2e8541973d8def68ba9a04ffac2bcb9bad60cbe305bab94c9270bd349ff2a29bb4e338e6343037b655a77c3a8ede7461f50d077da5437d83712d6e182a9399ecf38209861bd0c0a8999ec6d696d70eca3d3bf63e5a9a4d8eecc84f350072eeeee957a0d2158059c9e991872eeb529eb4d1bafff6434359e9f417550369f9724e0eff439dd76fc2eeb2021957c55da2bddc72ce4c3b45e801ffb17b17da63c7fc53f46b520517961c737df2778a7d326c286ce7b1c19abddbc6b774270d616fd889d31af6b564d01189a7187849dc4f4496b36c270d42a853540d9f194e1499e96c4907aaab1c636c0385ce15f0d0000ecff1c1f2afb88294f0e45e267c4bf968f953bc40177e948b858ca45ad175924c892eb041c874eeb76af3e00db2c87f998ec5c59e516a11d9fad5bae259b951cf48c085ba62d85a54a589c207e38a684b0816eecedf47025ba22f3fcca3e3b6dd25595764fd981b18380e143dff331364186150ff5b0fcdd2cd6480653a1e6c1894d20c4309a05ac734871c194ac1e070603cf3a9ccc9206c5f3b54c7fad8e8100e4f1901c80676b05a29b0359e014beaf61a95185fe942add02da2dc7c248513ddebd49ee3cc5bb6970c6934d7792c0adee7779935da9e49ee421b9d2cbd6d6b941042b79935b816f966112af3df0189da8a135a9e5fae13ccbc34950b5ad0ddef45b86126a222c2030379232e85803dfee34efaaa529911750dafa2486676da660f7d186974ee71a708281ccc0f45205615383f0018a996441d1440c8934fb100be54a970d0ce8e78a2282aec1e57c51692658cca3d7dc609f61ceb5e33bd7813088f527e98133db41ae031f682c254daa4630dd2ecf1f85bcb66742129f69b1d8b5fc736f2e3531e0b85080a14d08ca1c91a945698730c46dd6d38353bd5e3f8c2a035a733478030f11e3628d14b5c3d595ab1d0a9eaa261d1a17ad7bbf36766d00d29b31716cca5b77d0ac7cb2488a1359ec7f13d582b7df431e2248959de496ccb95b29e96bdbb500c0b48991b896e58e46f39534d2ceee3c7166a8cf434a0c081c84687d9f1a01bce0f671e680989d9609fda6a9db11c2347574faf20825682087e4891075b554bf503d298df330c9df91e76b25591df1a3e178e21cc3811f8f6dcc2d63b1b96224a3df17643fa75c939efc6064e62bf66105f623f607a3e17b1c1bef6bcd56c02e23d144dc65d43791f7e36e51e27c5bd2b521d2ce7cfa9d2a3727f95c200ec9f4d24cfeb464871cb2bf5e1b75b3daf614a41daa3542b936c6334d2596001283c0516c63ade1e9b0d16cc3f0e247bb83a479747da4ddbe6d190c85e91d4f60a4f4b39bce9319981ad83ff9d45ce03e15722ce5b34b565c74627542a298ec1180d3e09fe59456e8128087a4704e50b22b42f8dc0272abe82bcaec9a11623b48e75c0003ae137f88c083100b3cf0c51ca3e146a0a906fa28504c31e35b0ab61f3b4af1e9f9a05396af9f923225fbbc3b5a54daf8a0ea6d38cf44ba34f26c6728458e90dcb905525d5739c50e32b09c0bf40162ed779a345435cc2b9c8f5c7db7f5f502807b8e4110a3a616eb4b8ca0b3c2c45e838df4a3fe7a179638e217e8cdde16c3dc935059c1a207095db5f3cd670de78ccbf68f0607436e50055761634c871c71740ec50bc1ef309fd05986142111f9b55ca47ecba3903dd1851b90100e319d33990bc5dda8ff651e6122d744bdcf87d50afb52011a0bed8fadbdb7f05b665bd03c00dee86f5ca354b00375db4c11e94334ba4ff41b09da22a29a2308455244c349f3079c89f003727c402a54d17037cbe8489d3c4071d5f485ada485809e32811609f6d18e2d6713a04ed43082dc14bfa54a34626088178bcb267ef110473523290f063506120e78164c9920353a90481a959617d6295f819e2d590ff80fab839de8f294b08af7fed2ccba464fe8c22a53d23d744217617fed36d6d6c2289431952c808a28e00d4a8ce60d59d8ef4b0552a0b3218e4e45a64b34ff9095e344f095ba1e7196ca9f4e80920d71cffad8677eaf371ce3a0228d1795ed5c489e3843ac4122acfe141091ec816b2d538e6a9011fa2f0efb7faea197f299b4690e0952f446d48174037cab01a9950907d0f34f59af836faf1628da41629cd36c6912e3f215a230fd994eafcedd2caa2027d683ea6caf0708bcbe4c068562b70a8cd382a810aa1480ffaee540f47546b0e7295c768ac328b882aedcd10b9d9beb36bfe18da67efc73d74c29d160df8277218fc249fcefd9d951b30a684d95b253ecf763ce2d4eacbab28e9b8cec2ce3c9fcac754d1b830b70d91953ccb9b4be46aecd92395cc4e85dc5e302d63ab0df54bf8ea6907a08743a6b9d7dd9033ac5093012c2b6a1f7566e9209a364a5fadfb93127264562ea678289d69e092503108df1a162f2843a3d809b665ac08d6ea0caa702b67d92f5e5a6c5ff453daa48a561df14c39b56ec32907433c5e43dadfef55c09bd3823fd6b1351e6ff750c56a3b0ab9710edefa5182df6f1239f8042cb5781b7d5427ce42fe407ba7e29ee84e22e0f9683f0b9f53af7ed73e593f0abb8788f03e1d91aaeb1179fede7c2eb5b2547902590658efe28b2dcbdc311940bbba85a939001e1b393c1c8e0be4c8f7eba0945b280a17dd5e3ee91e7408d98c35aa48f2c3e4b7c3a64060d80e99f5d9b2627694702885088d5e1ceb40ae8dbe277f802fbe06dc2f848b6db70a94b9fae1d8707d6736e1d199cc0c61603cb151d1d176e8204648e4d38ccdcaec0bd6b3d664e0bfa31dea78b52da9e8c83a4ec5006a2ccb5ca866dbbe522e50b43e90087c1328e92323f4be45762b810d85450ba581e598d014933f3c75aab797a66b5ea02aad92fb1bc24255b46b9f20accc3f0b744b6f58df05dff389c1afff22c2dc10c7a3bc4628a075efce9b44b9af4cd66e5cb85214bfe682353129d4a48ed76fdc3064656332e0e1397d7275b90fac3e18e1ca7f6772e6783cc00da90bb44308f0b5bcfaac20e54ac6578f196f927c148f6a89e5e19b5aa3ae7ccd06ccf73058ca00e1aff01a94b9854ed9a81100220bdd2443d7330e56ed381594f6d490cdd9336fc92622063f63f6689214c8183bbd0ba933b373e5cca71cfa0bba744f766492761187421714c14427ba7e70599409d5972cdc67e33ffed21524eec71e5a3b851099b851001d1fdb4050390b0250628e68f81620d2f329da249d9369f7e819c0764138d905b3d4487fbceae7fd817b1bda15550941ed190d3049ef58afea761eb611637205d77f858c4d7f8134975213b8263a31789df2dbb89ce46ec16f2ec6425aa6822556610a3154fc95ae7d6f3c175e5b031ed9690d439272ace6e4a3a7dc1da6b912359733002c7e2edfba87cc8d99dddb4e50cfbc6944d8e37fc323cf3d5a0f89108d729f8dea67d714251491351c6ba3314975a4960e1dc6d7bc3f1b46d5da161d361482ff1b22cc72f4cc00d250ea4dfb2b8e97545529ecaeda0ec5e97681a036e154e83260619af67cd8abece3e126b9ab393d4568e3605b689d797d09de5cbf6c312873706c61887e9d9372398e66cedeb13a43a1eff4db4c6cb8f074ead30df0c12605139b749bb02f2e4148358adf9f42386ee57be1933eee911e838ff612380009807c7299e58f2b217999c7ef8216dc99420830385271477a79bdee223b702050a3d3d55e94f99ee566533a2b8e1af142de40e43664db422be7fa57273522624e38a436149a50a64312f2412db62355b204f79d29ebb68efd6b051449120664e244f72f616148a0c5cecefa3345707ca832423f117ced68fee5c400bd10dcc8884f2456a42cd4ef196da24529803e8d66389e40f001398284c58e62a30cefc591ab26ccf480386f768bce4adda9392be29363b3629ebb2f46adaf08481be377c8b80fdfb56fe2ac0e3cf69404755750f6a4fdea1f0e0af62cf937d99a9f1416c0c66d55400ae8ac7ecf2d52f02c3d33b05fcd4dd72a753b49f96f1de8ac74e2a3a67389069d5f557c92b0ce0641c2a9212d46bc77a2546f6c144e1aac0aa0e2e59707117ca13868387fdccedec42fe7e043297b01d1a0da2beff7d9b2167f1d79ed205d798b5179095a328b70247efb68080fe12898e7552389c8bd8100bbd2dacaa1378046fa2658ac73c8dac1bc4c4b72e9320c79012516ac47d9ee053f49c3359201c73a5e683f2e03645b0b8c536bca40a09a3f75d1e30cfbc1d701f08f332bc53d5457aa447a80c3f869199136cb3d794287ae211a6074bb2b093a46caad5ad593caa3c15e3f08736341d46f630324fe6766ff13f089ee1a71862f1e3f733fb130033050c9e3c7e4e3b1fa64747e5cc4d89c3035c8766df87d2aac31b343b0f34c5c9c2ad12271036b410f015b55f05eddba337be999b48db30ba8e87e223ec9c82b4f480cb780ab9d36e9597ec802824addd3ba3def21dea33c25573014f9e3a37fcebcc03114c0811e28c2e112022b503b1c71ae526ee8da00ad5d3b9fe5a9f0f4df78ad2a462d64662211241e52635c204eff6f91aff56cff02cab6722073b88e3ab691bb90015cdaaa277aa7dfec3b085a793fef36feeb1c184fd4bee93d31a6412897a1cbcd80664d95d54f23df4df3c649113cf6492e9b0f8b9e6efd6556e13a6520f2d157aa45944f2039647cb662dbcb7168d69f53058bd43db9ad9adfad1b4a37f044ff4dafb6ce52c42066a65f4d3171d74797340fbdb8bc284201fab654896b7bb5c6f83319ecafa0f6bf51af712f3c7f7f982c4c076b9a9426159dedb94d3a9669119b418eedf9eec0c5a6f7d1cd92c16761f0af0811b81962630afef2fdcdec74f05efe034d26a2a06870b01684d6fee2f390bc2e9367f4041d8aaeafa14f46f087ec31e50fc8ad5c310c0dd009d20612ea4119120100a7cb5dcb3be9dde13270a9293565106d4936bd7a58ea85c92b594dbcd45dbb00e1ff340f5fd443d6ea94cf1dc5ed8c7f2602f99cddd17f0e8665752f9513f0130100af6451d94f85fabdc11402c79db40ccfe2a8b580e75fcc7ff86dee45a74b231958f57c2a050086876494a15c4641565b983cad7c255478c36c9dcee0e7686d0000000000000000000100cb06ea296333a8dfd60355ec74445f6add44b75fa7189c1ecd53c5d21aaf3816af9ea5dc1686d7ac1c6bccbcfe9859b70231c625afbfe5b1c4f09ea684f8d43b21009aa99143bb05394cdcaa3c383b295dbcf4251ac0e1a66667c94386cae62d6e8201ede0d62a36ec540a26dd480c71983b59b9b19d95fa413aaa3f996af9d2db42c60001010041002cb6f72504d25967a937a813e667bcec904524170ce1ce8c9fb942094e975ff06755a7fd8449d64bfb9bce404d44aa4c30298ab79a97d1d9809108b51a068161 +0400000071fa00b1524e333018893955dd94bc4b1c049c3d96cfbf829668f3a164cf0c3289ac290e3c2c730b04364505e09fc0c49548ea18f10d517f0514cf2b5e96813b3ce2652094d56bde8a2a0d3798ff5c85d88f03cc1c5cfebc242d5b6e1b6ba36122254a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000040000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025400ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002c0000000000000000000000000000000102dd693336001afec897b00f1cde69960386c2043eb584085ac316415a1a104eab557e7666c32e75b878401afef8831a3c4a9462ea7fc693c6620721d6531d2a1ede5e48b868081cd642a9709721b6dd857df5a89edf2da18426128bd5b2da730854732aa9ccc2228f196f6862fcfc197335c5470f9cdbcf466dd82f06bc4e5a306a8219ebe6b0361338cac3866022af3b2e8eeed6613ceec8969e4b3e24487a1fa8c9a8bd05d41d42a0bf30d1afd3f1ff1584916fa037284c79f19e9b72742e370624b4a6037864c7cfb09a3baf94f1508d506d704c837d977b146656734fe82ce055cf946caba27d2c70537fdb425eea81379dfeb550f305cde53dfe5d107429d482d0d22d1a8579bbc7b8a9fb169d31b3f9f7e11195ccfb949b27328baa71a79680783555e64ea41a09e21ad94510074e961b90d49bb6eae6ffda0d7aadd8ed91634ef87d1e3faca5954e4bfb85dba247d6fe507d94609c10c26c6a21b5352a41fc5173daff544715712510551e1661fb54e621981a03a72ed8cc9398045b7a20e0bf95b616a40b7396fd22ba464ba8f41c4eb83cfcea1d35705b38240450b0a8e63c5c8099f6232519fbd584c13686f9520123b9c5ceaa04dbb75842863afaab7d50cc7579ad5aa788decca98013059e13951d81a7e086173343e161e1da5efed2cf348c1c0304c16d28e8278854176c01c41be9ec2fc110d328133aea09bb25bd54c9eb2f369ce2011f8a46eb3fa306fc1bd642b8df4817bc8dcd73e6a35fa70458fed544db61448cbae2d40e0b9488cb0a08e78ad332f24d1f22a631a99aca5456175211097a22b2bfa5ef0de7f966157a460fa62b52df4b0670dfb43ccdf77e421a724fde82e512400784dbdf77b7f57b5ec8ebb172324845db7568db5f9433a0d466be67e632dae3567d286f341f273c7cfecf854731ca5619ab0c8f7634b49783c272f1c9958151b2c558daf9c4eceec4aa06ec70db4d7b8d667f395e0bf9133bfae457b0b5c0072d386654eeac88ef4bb3b6ffcad237f02e280bb4b537f74d257f408fa1be4d512833626994b61337c6a7420f5b35889183ee55b6ac9145c5503cd99cc076a331aa654126d7e5fcd711d9524c1d771a5326b1e158fd28f85df8f5f72f9f6c3693a362f8a00024ca5b5e613d959adfdaca954c4395fadd606a91e96c54a8f8aebf503d36628a57ef306f029f6913c6c865d421f35463274fab126bb8abedd6a8bde9993861fc87024693dd34526081d5644de5fbe0aa5fb7f1f5cf12475c8437541d0dceda5c86a8ec35d28b12ddd7459cb405cd5f38db2e629f16b08f85d62aadbe25341649b96775110c4c6bf8c79776689a25d914c686db4ec734266a467d2b08176955545278c61ff00324d8d70767961436f42a3fe5ab9fd2d0b9b78de44c6c8b8c59d43274b22f69c145c2212f506d7029e9f714e828c7877414a5828ce78c6ff4ef0c878c1bf5552ec0ec9c30271c8f43d9be3d8b4c28518bdeadad8a5bdcea4eddb4c837054a3ebc1eab37afb35e557767ff607893348d1815c818fbbcd543c2c006bd9408e2535a07ff8f398c66046d0027cbd74cc1eee83b7d6455ade36b5ee8a940ee50580addbb54a1b6528c90e0c97c273c019f5b121f9443e8648d0a983908d3611d11e17903553c3aee0abda3224a00ea269c0514f44907f04f060e36f846da664d2d610a1118435352b6797e19d214bd597b4ef3f64e1686f1510a1877f943b27b09be120a740641c75fa1ee68eef907a4f7465e6b90ed513c6d4f0fe287db4e78c40e481dd7be1adc8f84b1570c4c6eb0e08f3785bee6a5980018daf1c1ca13ab0231c9b94246b8c625ad2d19b276c0acd2cbb69a929f13c2ed9b51fec6ba3a840f852aae736db8587c1d15af26175b24dd5947ec1fcbe1060c2a071201233fd646ff264757d3631a1560208ff68c98300fb49547788e80bf9ef97fe545cbeabd372d476f7f80d7b3a46d1ed6180d82350b792a9b444448183d3d2e0297d48cd1073538582ee483f3194d95a3d48b1b45ada8b605d169fcd8f5a56b103a053201be5d4f6e4b13e761df1aa16d4fd168af28fe742459316001d002da8efb5fa073a2b51050e0015412275868f2a49af614ada4c46b443aebfebb311cbe980e5beb4890293778725d989650a4a63b46f65183aa9350308e623439ea2e5969f2d9f8d9ca2e16ffe37e28f997deef48c77f0989ae071b6675ad334775f77508388582d6e91815772779f77a0fb9361e74598ad22c67b6de92f6370add552a34927039f43da9bbbfbbbff040fd7962222abc9b04d728fb47bbce063022867421140e9568059eaff05ebf180cd4da2a2feb52f6eea32345d6639d63a225f47430c2dfee2bcaacf9cd667c428913de210dd29ea401b376d90807aba25910b52c325e611a1404812623025540a3a254fe7c602ade8168e90319210000000000fde01c42a2241125b8f38c86eddc582a860c058b535e25f0c19799761c31e2509848919862c737a21b17a41a6da2b9fbd245548f588f9f7ed6b92eaaa49065d9eeac3799019a9c56d355577dd1e65bf70b79e9a66cecac82076bef38976c9f8c35139ce44c1e0449bb91c254a9a2c7e294c0c5bdcdfe4f12983ad8a1959a7228a453bda7504ecf5f94410e099848c2202eac6ae5e2641205bb5f7d06b846c105f29816a371a3b34d206862a7d2d3a83d354121302e4242f3c8c12cfbead21a8b8b9d904aceb2c2ffc8d84f75eaeb0b396daf493d295b014c1cdbf9a57ee9b3cce16897493a1481817e9a3adb7b9f0558a8da25e181026cb3692ff34133558a2ee80c18e9608c001fc45f3553aeacb0a2cf7935e0829925b58efc1aac0cece2ee946d213a1ffe163ca83062413a37734b4075a6381b68e2a4f2e2ae87cb5a772ea3bf9c3c96bbbf3adc373850ab7bfce6c0e4a741b8a7613abb23bc5c3ab0418e806f1ccf8e27ca3f03694527ece3256b8c7fc8200061cbc3b5ac8baddfcff43daf699e419765a368c42de5e5a33c5a3c4669a5bf1a4941710d4aa398175217d40255b580e5f3bb1958e546335c37b21a0dfc3e325a4fc662530ec718dd0d70f268aa3e2c32a44ddb8becb5483eadf95392bf1ea9ae739f7e1474e445b5c290cbbadcbe12393f8522bc5e0464342c444e85130610cc43ef9306d14ab47b66edf1d46490583754ae83296be1455d40d56bc826bd01239a5dbab7556fd61c60f520850f94e7a166665ac43c0b71de207fece1526220201cff6851177bf35600686fce201760914e3a67a8f85e8c5df3ba9809368fa23d8c0460447fbbdf22ec2a322a95af331a8bc7c12bb1b879054e92beff1b7611980656f0c41ed9a595ba0aa962b4003e2743504d510a291aac2acbe3a54a00d22f2142fb8c082cc60405a8f920558db330075f16de2e3cd085a4201cbfdad4fd4f5877c79a30a5c16a78683a5bce1223366b8c36630d547d24931e11deef70a193e147a3f14e2d491276b74ad1e38e062ff2dc67e41c95c1acc5e79202d273f6fc8dd62e0312f94a0a66590362652a8bdb12acb24563c500a43160742836fa501f67901690fd7e78d3ddcb1730740ab468c7d3fdfae4ec4c95a5feb5740216189110f3c9cc6c7088285c96c299d4bc27f3e3994a0543c6416ebd49c66a56e1f44abfdda2259dfec6f523a01a1dc98bf745a193b16adab8c01bd5a3efcffbbb11a432470b1a73b5492d07c01004452d88ae778f8cd36d901e8d9e870481f807831d5e4ac8324d141bfc5c92bb08b42f0348d548faa7d811edf821862dfbaa4249659e9466c5dad9aeb5e5ca26ebaf98b55b66c83f4bbedefc595e4f3b131886e49e9e9179aab17e04cdc6879ebf6d3d108aaf64ba93c311b7f18743228c58c363d3aa84e93b46055588d14af95eafb9a2632081f2903ad3a4c8561200ba75e5cb026e0a7d625defb9eb9774ef9bb0b3bfc461fed34c8e01d1f0e0c0a1e6618e317acd4dd7fe63c53cfa5cdc578eaf0a9fd91237350b186808e37883591c4d96a2decae308f7a5627b46bb177a4c1a124229890f8b56523268a38937d6848699942bf0f1b21eeec500f04dacdd0845a296cfea722b3e91635161d867e9e28230ec7a565a237ea96581bf69a23cf7e4bfce010670257320eca1a6e3733ee5b788cd33a7c67ada72ad0931cae0e34e3ea0cc726b3b61fcc8a9bbbb9bf653016e3aad1c98091ccdb1ab5630890a14fcaabb90e89c6c708489c6046edf71ba1f67f0386790d781696ac270badda90a65d3b19767e68efa076ae82ee7210d8fd02779cc7d21e3b3c4df93bfd93df5b069f2a2662ecf65e033053246943ee80d7848c5dba69a0b815b44101d8eb24bb3c7cfbcb405be53b750ff8346bc2790998eaed1a92840278736aec67e6f2cf0fc5a853b8c99c0c6f3f947fdff3db3088d7c598f82b3d48f95373b80b742d066bbcb6d8ac5d326a1ffe8917dbdc5798ca2549b1042d36709b5d0727f9b1b5d207617c18e973444bddb29fa921fc60079e69279f1de28b4345b955160ee587e33d9e53c1a77c0864e72560f7d84541288094f610fa36943836cbe110b8720203ebd107621a082acaee08ae52d08716da5734b70b84cb0a23e95a6adcfd525f3d9efbc7128309cd800efac9dcacbf15c7c299cb9bcba74a0f98ec1a620cfa08b47ec2b3abd44e4a4fcc61dece574ad2b9206095bd784a8825d6f018c8930bdf878e8036a151e88506c60551a904c32df3dad7417cf6e1c6bc39a4032709d23513959058f2fe611aaf1433e224b755930d03fd85def267ccc05369323133d4fcbf50f1d868341d049239f947e08f1506239648e00d3e824020459c8846426e4af561da25002950adf84cc3d6e7df8b6ce3fe2189ca064620f531be62e3564fd7f4bb081d518d8c762f183c0e46934bcb61be1f9a1926539eaad6ff3c880f96eb0e3d5b3bb0867ece96927195b8454c2a3498e0bee6720cdbf8fea00210628644f973da01929642deaba72528bee6b3e48557f6166611c9d6e903ca406e24d368cbdfe29d91a5d9dbfa3775b6cbe14704cb576fa56e1d7e15b4ff6ce590c384ee9dfcebb10259984ce07910ca85256c7b104def3f174dce6586d43b647573ef17dbd97e1f73319e4ea2d980be0bc4400a287abbbf8c656b79e5d3e14c191ba4c102b2e984c392efa2c94d0b395e3489773ac4a36668784001560cd9b49d06e85620c4f6c152f177db5ad93f5872d951114a9e8716d62c0fba1e300844fb7834dd82e51729b2480d71c2b7a5bfbd0bd82052fec127ca3336d82a8d104b57c0f6c9b9b144837281093f107995604340aae0307654047dcec2f4bf5215e39fbfa6046acc01a282b7d37bb8ed23ba40c951060b0b6f7021cf36ff1c22eb5521228023f400c705b3cae7cefeab104094dd324c205ff42f8853f21e30b8347e1ea12f91b10dac6b63299b00ea14fcbea513391c59dad4133dbd6f6756b3c397bab3a172942ca9b5903113d25eb0efccea29d707e57aa94dc61c2ba6d1c1d48c862fdd021a6375944289d02bf9834d48535b78d73966fa535c45f195814c50d829d2dcc50821fe91f0b27220f782d4c5d719e447e7fcb0d8202151524482f54c6f988d3e2c05f684901685bd7e6e5826b51dc747f45780ed7e8d56d18f915b90c122f5decef5a8f1f1e0076bad72c4fe6e770c7a0da9900be83a2b61202db4e35b319346f8385fe8b1150e5e6ca5c30226a202f432d09418bbd15ca38cc0191c07af12365412170502c5d278e91dcefe667ff7d4ecff20bcb2a087ffb6f16ef96a39ccf5d40fb3c230ac191e52f3a0518d51321ca7b57545309d728dbc681df1d0b9a578184855aef2f738c5e525fc7408caf695dea2cfeccdf39e9e3b66932ed651702d5b88d1afd0e3dfede4f3e5ef8e1dfe0e658dd55165d89ec1646cf0a76af5b58a24789db7105a7cdffc0c8d4f5392951d5e51e0d512aaddec3349a443bdd38fb038eee1a541f39746d63295e68deeefeddd748b7f4c07b549a9c327b190ebd61012577768b040ce3aa1b9dd30d696edc6ba86ea711b2e347bb8238c6ab47d16748ed73b181271c9da0bafbb2c864ff3ba2e92f2b81c4c9cfb44553491354b668b60e152eac2f0670898c4f44cb33d46f10ec2afc0d335dc53e86d7b62d29f3983407aa0f433a639bfc1ebf51b6f56eb0c174cd4c41086bb47ff75c004d1eaa60f921c253953aaaaaf040feffc77b3a71f224e06fa19f53e11446fd13b290c0f7a9b1e39f3f3e78d9ce4ae25ab9460d9f48587f68ab9cdc5208869d9dbd1ac69c4ab90b49aa247289a372e3c3b3ae10045a5713c6fd20a4354bb7a6bc37c25e8401b00ec6c322b9afe16ad2949184a1b85887f6542cb7347b71b45f79cb6f8165e20bc59a43309d8eb4cf2ab514ce71ee030b106b8d9864b3ac1ec837f9c84aa3ab1668e9211125fa68b15c713d1d7060a716bc36f3afa227327e6c481da2299443dc3fd1cb3a2770e69d81ad14910f1688c3d735d01773ba1a43a7f6f04f8d51659df5ff4126739619ebdff9fbc1706d2da7aa369319337b38b4314409bc743600087fcbb530a024bf51077268c1654c55b720fb6f64d4d58ce9e7b9452fe2c1f4779239163469e18ee06becb57453b00b4e7fb7887525a777a15490f6cf2da0c5d100b1d7372349728505e7c4e172a227e25edf565ed31b7a29bf33b7147dedf3fac3a6ca053f2d653ccc18de7a2b5c9e07c296ce957d55a7d2d37294a8c9b3c21a85335c07cce67f6ada05cda2b3a8e531b98a71974505852bce96407e2cb435f4543bf82f276add821e56e5107818a73a72e34167cf72f4b9963d56b5a1710da0d31da631a4ceb535b4c7848f9da3492b0be114399c82126a489593b36a29078ea1598827be400d927acc76f3e202cdc97a8bfca669aac1f9cd5c621bdf512374429f370e18d4a396c7f12bdc9c5d39c6ba15127d7897abfad17b65f0c55f052a04ef2211a592188e86af5270422a04ab89c03488fdd17692f87d3c3efa6eff8f0408841cfa04ced3f75b646b6ccf341a3f2e54cd0ec9380d5044d9d10e298937f88736089c6e2ca90493692eddeaa6b8e3246bbf773693f2db5d38780fa62aa5f16c650e20309068fc440724582213ef2f345a87eb1131837f6f0ca3782ca9e0c5fccc12392909f605a4d2770dc1aa90168aec9ea26207104ad7fb9a23e210eeac86bf328bd41c97493d64a3c9283fd7f04b400bc541b7c67cdc3e4263f2f374e332531cc6c6b28cc3ca121be6442709f332f99eaf400f7cc10b4b1a6ca3beead7e0352ced26082fd3235c71fa17fc7dbf2f75dc42811b3a93cf1ea1d03cd260beaad32121596499bfa1efb4e68a3368b847329b6ca6a2db7cd761fff867246e83e28a34b77c0a1976cb22315fa1ef58989b78467b27981c1f5437117f0aadb3b3acc22eb430c6a89e8d2ee5fe788ec86549b949c97cb6542cd70cdfeb8e241b16d23d2b59c0d036a6bffd29c2aca6a51a6f0adf657523b8af014648c2f8757fdbd2343c55828b812d9d38095f238bd707e7a975b3e319d7bcc487329e19d4c9df681e220f049a92ccce7af47c5b4fcdfd920d752c45883de1805506d8c70a3c7042ea0f2927f2bddc48b3b7f8813df2eec4d654575a09b2a4f51654ff811be48ba483380e6a2e8ef5444d4912ebc970f2290ea9e05260e8fad38694582bfc3d96aa7a39e81cc5545327249d4897119df3a8094a0a2ffa1b71b56d535cc7b7334debea1b30cab76ca57882dcf974fbb3f52f52b23e86749262ac9f69d2bcaa4b504e842fd27f8b2072b1904d485723be5599f314ca6f8ce5beefe3d28da8e33dcc75a42a77370c616e632801768bab743ee87a95b216e2fd800014f6105a2ea05758ad2bf1b1b038b9bef6ed97565b9679130fe93c6278db201e6d517ea6015275d53a3f42744e0ffe1fbc9143fb7fb97c687661b16a76801711f725f0c003fd0116523290847180ca9f41d027d855741f934c372517399b6d0aa7788bf7cd9d40375e32c74d63c6724b9b3a62a275fb81d071642e44b956085e63a03a2a348178d10f3ed39f818e821893ca4cd7959ae5756251319cb009b928eba4a095ec21b22bfb1e3d0540ee7db14683c840951bf569f723e21c2131a7b544a8554e0aecbcc5773a1aa9976555101383f1ca35255f1359be157d5873fd2c0a2e4ec1f83d469c0f33f445b4fd3233a955e075fda67dc9f7a0a8cfb2d82f580caeda4de416e747580c79495965ccc6b2f5713735bf8c103ab74ce5b89a429bb84f65122d7445b1a82df2137d853134ed1750b0f9271056b03d12af03deaa95f0b99fe8711b3dc13712b8424e8263a7ec23284fe98d9419dddd5acd6da481420a98b1734a65762abb0e6086ad0dcffc6db587152453550373cb30f6045e387cc380e214053ae7907a01ec115491188b9f1c0ba97d56cf0a0a87790adfea0993c318245717427c346a3aa795779865f42d11ccb3278c5a01687f8d6e78d85f6781a145dd6f83caad3d324a046013606676ff651cec30a8f31fed1a10166c1623c9ece792387a91582218434d7bd6851eb175d01a6b6377fba77439c4f09c17f7aab12634d64bdcc4c81d66ede3e537c32b11771c2cbf36fa40d4c9fc50c79847326b43be183d446ed23e0d4880dc850ede61bf07100572b49a6ca5a40fa654fc436661be9349f7287c3f0c456b175da9bff7b726064e4c98e3aecdf8b6c9e9ef1ab167543870ccf2b101ea709acfa482b891b0b13e0fa6b650c9848390fad859af82a44363f93e1ec60f2a2f88d958d49bc43ff56689808e910c668ce8f0c459be3f970b70a0ff04fd34d42900bc0b41324d4afc4dde7c45f9684f21fdbd73769e68ffd0f0a081c03f166aaf12fd803e2d1281c1a501f13f4e0b7bb12bfc1fd032ec4bde7f88aafe2339dd95cf226caba2f26cc562514fc9c0dbc262b01d94b041adee25f8e5c0a05f2b950d29822b2d1ef4a94f1835d2b5d58df235556a15ece0b814d0853e56bd471d060894592433f719adbb72088db1e7d52fe42beae797bd25a90240e19653733eceb68b137504783c971f8e2b6e2bbb965673b58a9e8385b85010d13fa0c5d60d24f155f0d7d9e25c28c53edd1aab721d26782447c672bc122145a937275e673fc2c9649a4d93119a045d16829c0230841cb806aea12f6311e0ddad7cb9c82833594a8d9726bb2446d67d4a9857d7b178b3d3e3e8ef5bf2ad6eb561dde7508e335e4db5834e0abfe211d4de131054864ee089950e99e0c23bdfdfbee1dc071a1dcfbeca636bb1c11ffe05e147664fc8ab9f0a1416a65a83537da2e68a4bec2411eee74592337858b29b730de4e07a70ffc11976a955577ae8835fd792e3860837d4383fabd4a73d6a5d3580d9d53a72b8f670b166d99eff0960b4774bc931160bf0d331cf279bde487e573c999edc3ed96144c6683e55df856f8bf0ac281059121ebb50dcec87d94bafacacb9b35527e09fe7be6bb9c9d2e17dd4547ae92f471e21a2daf02a02ce83232b4be821e1f8d3007651efc512c922e2f5ae607ce71d2fa8bf795ea652e264c013111817de8c20555172c6636781052a6b97a73b8d8d31416c94e6e12503a5b3b66e91de79602f2ec46de51397afd1fa793d55c827453d83ee247537da89e4577d502c5302108a34321785f0785177328a4c1c4035c03855ce04f3e97faaaf68e1fafb657472d84a5ff6d726e423a253cecd18cbc2bf125b420c337ba27fd16f0ed35e0660a282b0ef11d37383b3a1c605dab2ff22cf14bec4415a1db39a7fa1a1468e75e12542cd051f0eced8e4fe9d3cc2508a34eb3c3143f65144915e4a02773036cd0cabc8a11b583ff3dd5c31ae430be4ff6f330a37d227cb9481ee685e76482841ff36f5b32c653a0ad91417d541cebffde45f19e035c938904201ae36ccc18402ab06873877f2e3e8878688f6f5a1daa5f17e0daef788e2ac80889beca2f6447608069ed3ac06aa806b2536560b310048a5ed07c5b793a15f59486101c33d06598830a9a61ca6d9749e0954fa8b6617dde9122d915ed13d62df058efb0bae4f75c97017d81cf541aab38566cb04dce9f1e30a0f5ac4fbcbc55b1a0bc81871cd1036d6a74249c90ad33413b7fcfea4dad4d3c10b5cf5dfe400494301a98f9a2285218ba103b57cf724c331448d40322d9248ad0193251900850310cd7c1ad9d5ed029ba6aa7341a1324e5e315fd706a327464e34564a05a1df390386b6e232814f7e06324df44f090babfa176606b519c3e6cc09c7d27841688a9b06021808fc679685b0d9994978562db31b0f86f311c6531b062d2046809376e9e4386523079eeb284a1910fb0c70eb97ca3f6c136d9ff36432a94469f6dbfa06d68f6c65259824e86319ef36c349c85a14e7ce0d205656881318e1d6c44a5ce05d633bb652db05879d21f7ff4fa58e5ed25c015b193d2ea51195d46de3d2c3355c5f1e2185c8e1bb67aefdd671bcab4f27f920a8df0710c23571b8d4752f4bed6bf7e481279ffbfdd3fe036527441423852bb001ee7eef0d29a4c7ca8a09e013ad16216d4319ceba0448cf6f6fce0c670a83890f73b43f3d1dbeaea491e57f05fe91320a4b07b4eb5e0c834099328123e658de741568599237e148269dfd86932b5d2aa467bfe32d60371d430b0fde2576878ab7286a1d143faeda3edb03e6c78c80263b25f98bb34ae6b05e28afab10db912cf5681debd63cfdcc91eb1ad996388e2b30a5c3073f6feb83511c158cfc93ee9998f0ee857f0e629530b231313b494031b690fe983883e114cdfcd30af862951bda1db014432c5fd14f2be59090902292c3c97cb3d924cebdf9a70c5f24825dcc77830394bf398007d3aff0ab6b0a129e94c8ea284d3baa296612971068ed3ebd09f8d35fff3ec837111b9755e8616e3be4bf75222707320091bf58211733a2433a5aa402381054e74a9fa5c03cc4d5d54d0764f96673425cc7f03d66133b1d504daa4c1b611e25276be4ee36b78fc05e28e4d495d60d0baad8b9c66d42dc6c7274195368f23ab43a7c5c29d9bcb9feb56aeafca30f6d1275cf91fbdbb54e0e41a1f2db2b7417bfe7f490d3476dfb7a1dc2f3de6f45cf17d486b206a98909a5f5e1768cbf030df935f282ff89e2947b6068faed7048cbbf449e8f8bf6346058f906a2646a9c226e00c2c14c7f344dd17acaba1f8bcfe248f7109ae6ed9d9a3865415aacc890112b5ba39c0ed05681e8df0521687a78056394e0410f8c496bc17f4e0b83b1ad02fd9f7b93513e9c371d7672b0386af1f7839c00af9f4fddb4a6f3c4cc0b1e2323c50437d200bed8e9c113d7fc81f1e8011405e68c1c8383bff2c5d4113d26cc0179a6e1ca45ee1917300359e48e0a3501f432dcd0f686048269ad335f23ab331958539a81e3458182ad24cf2f9cda12c76af231e8dacb8cb60086e650bd7b9028e6446a7a86b2ddd65d760e42dfc319c7a41d68b94c8297d38749feb9d86ada16875c3b2ce3c2ca79aaeda1babc47abbe1de0d898975686043c05ef5a21322d1fa309d2c04a8efbb4946979b8d3a79840fc281d5a9da7e79bc56edccdd79b9a0a6cafe907197c38c1c70f666679b3f7881e482033c99c43bc7f876bc8363a370954900c32a2961603fa7aa085453fdcceb36ee94563d294d5c460680b88fa341550f5ae85efaefe24074ae6ad0bc89035766fb58f98139d20bcdd94e63352371cc637530f6ad09f78a6054f4c7fd69dce106ce36a866859b5b011230775e2783d776543b8fd58fc63d65148639682d656679ce868c65e4598e081d572fc35b606f5e0660efb50bbbf7c733a270b36a4687394000a60f766a86882afbf5c0b0395095599670b5e3e3c8c0b515dd3b0a888ee6ed16f06083b288da8671907233c357cd78e203ab488860502301bb9ad76f1c59df4aa2751cc76bf7c3385d19ea5a126edf9340bae6a03c726e67c46c27f23f0355939f6b92835394ca1eb3456a68e1e4f2f002a955f756310113138ad9e69cecc4beaf5a917612d3e29afe0ee0ea78f1ca661c092ae4db6c17a42dc93b32bff1058cd0f8eff6af92e9300deae2d0114b3ffa670e651155225c1746625436953c0d1f8e24ed55b5710faf87fa8082596fb82ea8c21f136242ac02f75fbfae7853d7cbab010698204135e368dc82786a475ca79c201185d0d6f212926699dc5fb1d536a3c7a54e0242e95128f7d3333363b7c8d1abbcb8c74d780d88c88c6c546296c20a5c391d2e419193eb87b0734b82d96c9ed62e6052fea88144ed7f2862f020c0abb3c376b00ef2518290e6d041a8ad8e710418164865816bc90e1ed8eecb4bea0ee1062f258b5e4c49606518b663ff5826e47dbd0b6b20ca3424243860475c947a437c98d4060c77c3e1481b6f3dc3c41a874ebc982c6162f1327a1fb0a832aaa982e5bdfa43f22f072881031ac59e1304bbb8433d82e7527657b8b9b2349c038b305cea590dc92a4006def9b6df2011078b32b289925f01cb44c884006c5e1a22559d79a85e654c0e8945e21da6dc2abae53ebefef9211b1983cd58222ab1151a626cc587290680a7b8700a9c66cb9fd075cb546f07175b530bdfa8af06b9cd6222b7c606da38a08a478792d7a9d9f404191416a9830f4b5f351638b6c8ec67dcfb46107dd9e69627911a92c14054a722758c316c5488f61334008ec29a592b050b6da9cc65a45ccb79e479774cb3cb83a003b5da87ffc309413e41aafea1b68db7eeff0e2dbe99584cbf238189e83715ce44fd42b213e97aee007751dd6860d198ab54505f86ec8b736d30e574224a230e66dc2ad447266140fcddf3c9b38116421c6ed87abeee4a5fb962a3a113e04fcd45fec0ef053ad8065fe9269e926f9e81ac1d7ef867015d812630f0100113eaaf0c770876f50c0ed0f00d39383fa9992a4882b410624c6c3dcc041e73ffd6a0cbc4f564d940b64f4c4d8f431fd531220b925518000bdd987d2acefa93c0100a347e9cce3f8a61fe591680a93e3444b787e41daa74ab626509950f81141f28bd2939b59087af474bc7264b05a4edca62bc6ca4087fb1add8e55286f7f73890300000000000000000100d4a761f478dcf9b5055d9c352ff294f2c40724fc81248179b641fbb46f34f90513ae1795be2204515f741f5acde79f28cf99ff2b9f0a4d92591daa0ce903d00621009aa99143bb05394cdcaa3c383b295dbcf4251ac0e1a66667c94386cae62d6e8201538a571fd7d618d45d0b2177ecf6e95cb378ad747e86a24a247f8b89dea23b8c000101004100923cc9f8d8b5b92b804450ab9989e682b9bf7be4ec9d53b19009a8472706363cad67de611f778b252cfd4c660fd6dad69f92c8c702bd6ece1956e96aea8960cc diff --git a/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt b/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt index f9748afdafc..af145c6c156 100644 --- a/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt +++ b/zebra-test/src/vectors/orchard-zsa-workflow-block-5.txt @@ -1 +1 @@ -04000000451bd4b490bd192166eb6dbd2785f5d7533c474cd8e25d8522b1786d2a24fc062fdb3ed2934b09076686c1e42fddbadb915e8ef9b40b02b7ba7e44db4e1d89ac551b1827016529d4ccda194b50213f3481d92cf6943db47e5be3be1838eee11a22254a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000050000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff06557af09fa693000000000140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002d0000000000000000000000000000000102de855692d73820d062868873823f0eefc413efe68db086a9c4db56b0fa6b4115f2a6639c3600a74b7e55f70ecc25405ab2601aeff1da79233245aa0396951b2fd709891d6214913b06f9809be8dcb1252dff559ec165450073ee7a40d7274aa13daba26552ce28509c482d11e5d935ade2ded0c2859cd2d1a1f60d9bb21b813b0faf9338752e2fcf51526c98b79d861e169234cfe3d9ef42315e1121cfa3a6a3451f253337a1882e27b87da37529622a1fefc33bb40fdd4b992c061eb925bb164295447c0247bd029c5acbad9d30412db72110040315655a22d12ae9ea1f2582c00f6f85487197d259777777fc800c925d3e7e136616e11439a83a434fe107a7a4bc930c2eeb779f01f0f6374978e179421ebcda9bc2ed25c25e71b46f786d6c52496c7b5807a0ae8fc72c53110b1dd013b0b588efbd21634fe5637d5a0a0b19f997f89f20bd0965d82ebe0dfc204effb38e032f024b8ce78e7cd1b8a7ad92ef2fe9ec741fa14ff62330c939c210b9bcfc55fbdd112e5e114e35fc764d00093a077e5c4bc40a76b6c22e7a006130c7585ac3ce7f9869881804f894075e3d82b4486093f5a19fe0c72c12e6984e24f3ae1fa868274852507fba7343fa353674bac2edbbc55e5631b61569b1a36d7a2c8a29636843fcbe041198ef603738b8b19e0f062d6ed6ff1ea23762920865f7f90f32ee068cc14774d01a23df464d73477a8bb05293614a66d840324f8fd62c2b0aa9ec3c0bc16ae2720868c943a984b2ab801000156b8256b5c5960249e0ac7a9fcfa9ce8032d12b1d9a6ff0f0e10e9311bdf504de6d6ac7a193a598537a03449de3f35785bc603c9f018d9bd852bd938bf9004c45fd17967d4b9379713308e5b0dccfbfde394aed3526f6af8b5694e7e4cd3bca1fc569bbc7bea26f394d8de09f7f493ac3ed622f8eac2e9f6add2ca93803c25d843a97e46b2a328098d1d7f4506ca44b815e8c06549532277bf1b75b2d55226bd0e5fdab60bf7899ff1fdf84908d0660ed536084768ebb3d7f8efa103466ef0be86f69e719d14aa6791a7bc4eb0fc62abc6ca1d20d1fa3ae5ea771d53f8ad0451d268b26162c42d860e75ed2efedd25e0e0ed143f84a7881c6b683477d39162bca3e7c6cf3b4e6f53c595ed9bb17293b3cfe08f4f50adea78ed7d753abcf06604c3d30c735bb13f5015246e9ffbbe8c8f7245f1e6aa2fb1003a8a0162410556fd20b1321b3a04a8986933ae66f66ca832000442fab99d40634826725d4eeacc2d56d8be08f6700369565f59bda01874622611257a5b61a1f60e385c0dc9f91c8edf9be3e1867a401ec2cbde2aab5bce806be8acb3812d6abd3520c293cf1023b15e2caf081b004091ecf90455a05cd2239f468fe6971f489918cf1361a753dfb665048c420d9b4f6b8d04ce9b5dac298288aa5fdea8b7ab73f21df76cc50ad9be832d4fa3a3e1e3ac7844fa00d2b106dcb60135c010f6ab06d2f2b686ef5be3d0759167a779c6a9448638b17d1430b3148313d3c02c3b22238672a09dc65008cd88b1f65848ea58743df91969d2aa86d53d002a327cb7102e2a26db60078d7b42eae6cecb5689d6c173981347d91e3164f1e8db4f6937b2114ff9e7cbbfcdad387542992ace82c4bf40c5532f1bdf4efa2adde6d62e8fb7e08f5563f3e4ce7eeb359a5bf155da175cd5fdeb301518b3f9d1cae293f83a19177cce21cc22ec0cf3ed3b0cf231596a0cb212f8ce8766ce95e14eb5ad5e89e429530eb235d732f853a22b538b54216d6efbc3dcd13e115295458e56f291e299617fb13b1bec58944d2e275a34a737a914426bd1d1ed3a82c6cdf9db5a6b418a49163b375e39ce8c371c37c59a03f2e48437a14162f564eb8bb4f203f56ee79153f8b07b6189939c8102341f014ba0e1f619492d4ed73333a98e6ca953681f35f300500b79946c63a04a09ee6d3c76555a0c727cfa25fdc821dd3da568465037b6072d8993bb7d88ff365696ad4cc60b6e14d6cf97876e00f09371a80cb390a61fec0410800f06d243077163a20a4a3c1fb1636236a5084084cb49d33a3b83aa1abb8d13fa32e6b606e483d3d6404660ccb907488b22b5d93daf07c147c54ca4a38e936e8c29ed8e72779691e3dc201015a486d29755759412751cdcef6c638e880d6e2d7a6ec9dd4810430416ea57a4abf21b76fe4c3cc92d97cd194b54c01f4e34f7f6a3cc802ec066405aa125c8bd156b4dc4317dadcb87d982bd531ec68a7fdd2799e89d07d50283cec51e4e5f871e4ab7b3fbf54205723cc30ca84914357625f92a02e452eb25e5cf1d36414bcd0891f67558154f60a2703e27d7c050e5be2435eabbab6b66db084079e0c9d5cbfe3e6a03ec02c6dcf2a0793862add671612f6663602b0e9a1e64d8ff2ed07bea62f563270b4eb703ff7b6eae4e2ae0fcdcf2dcf7591ab2519c31d2545043b0000000000fde01c1ff302164a8d078695b096d1b2deb31725e729881b2ceb104666f7a87b305bb836d419e7c78ec984e97a923b256c5c97732e6814de660c581ca2b4dfa9b72110c872c5ca08036688de47831992322f9b90828c728218680fd28db549d2444324e8b6f0596ffc31475f0ab3b03c5c1afc3f1fde5c1ebede2f172cabab10df6b12f19d4f9df80481bd3c25eb1cbf0c381f3f2f95ae7ae3148fa0f19f7d1ce87eb68fc5a81b1b1027e244b291806cb1b9e144c4d51c642867ec1411eb423e0e6d1abef4d6d746f7f34bd4b855acbbf16ad14daf768df842040a431ed67e2ba44db0df98224a756eb9b2d171b25762ffd18673328b0ca3e0db4b64e62f083f8d2ba34a5069a128e5e8249536cabdbe6b7ab656c3c5ba51f41501ea06de2050b0e62cc14e9fefd08fe24b1c5bbebebb681c98a260d621ca2f26f1fa8f3ece27fe45aa72a0a20af6d2f35fe8af0f3899354f097adfdff9d012b56d6811691b96b67b95a0c266f664931798b5a21e479a4d2ed514e3368ff4257e2d52b400c06bc76e06db9e3b0658e46ac84f17359ae641566623b67c78714c16ceedf199fa451bde327db7b7d6e8f9805aab4bf6964d9f400faa8d0ec3a9487941feeddf039c590e00a95ce9495c5e65f67dd14b5edec1f8f52cac6089ac45f377afadee37d8f7069f6102d087a8f5a9a2ead74777d0e40cddca5cf92a50eda804c3922b8e685216966a658af6a1fde920cf4a514660c87eeacee48f9840c8089a37fee7afd020c11f952fc95685b371c6eae6a2de06ef8e8055bb831670fe63297bbb4415026063a0de30f3fb6357518e13cdf91643b1c4cb43ab176de034d58c4e5dc840ea2d0a0efec5bf799c9def9957740a970b10eecac32797ff0eb0f5add6b398d1276bc2920e7f91313f2f3202ed4dde07c9175cffd4f4e385705e39744715b939c57e6794d2aed44ad9302cb046422b4dfe1af3c9bdcc66ed6f383a108ab3a6c7d429e22b74513dc4e3aaf1356b39582cd4e658f60be103c019d1a1875115fa44adb6a11506b9d08f96a47783b09034831f23376deb3727ffd8e5d5e42e6fe883f3870737e8199ada33c005ccf7fa89aa16bd16f464ca78af83db16f3c7aff5075d594db1bd49495e01b400765f63040bb1f52bc4dbcd6cc01ded26e24e27b96d3d65ed2c6bdf784ffb7803f81aa5b8ea6fc4074cc66df97364f4abdd6266ee5065a81234dfee51c4ea57dadb521024aeb6d258f59da3cc5d49fbb86982760cecc340659af39e51f9ce01378edc1a5e46b519ba80ce62bc97b777fe710f4b837ac67e4b906c555c6ec3646a92e58038729b6ad9144d469f0f48b80ec1faea876ae38d8fa738f12e8dd5d6ed4db8a7e3d22c06cd42f936495b86a8c3d0bd6072b99ff78e8ab6abd18011b4c6958e96a239433b21eedb70ff1a47d435e064f2e38572820e0589a7669eb12e57fcc6f68c0461f20b792b039c150aeca33baf2d6a2f631c8d8303c1c4714b27a21faefa3039d53ca849916d318ab65153850e4520511e36159addce87d3cd3d33758302286c1668fdfc8bb2a5997841994a909d78f45d9e911dda7ab0268877755089761cb9884075e5173928a4e72aa9ef2698738f35a5f8ae03bc81da2aef89b58cdc87e363c935dcb25396e77b8234c4b3ddf5ec7075079ea1d7fb7d9c164b0cf5de910a363a8152d0aa00f1970068b1752a2c263113d59a10b31b34068c39aeb7f55f8ec7c7cf629bf3f93020c64f08cbd7aad3b96f721f9d31040aecdd83294b841e2068fd87de7b756dbf87b581a7471bfbd5490bc2a4af6a8de14eed2dfcd5564ae93d725defb00582d9ee4e5515c70a92ff6c2094a6e571797f8a8a6c4bff302c031b9b08dcb70fdb0ab3a106e9ed2c928f07a99c0d2f81c078db550971ed00add706b2ab67fb602165c2b2abf344e0d029b4288226dd010f6526c66b9860a1c82fa1381ccf1081dae063c1f342a04f73837a50f8126de6aeeba5e6786b87903844c10de472f0bcd3542d2b1a3cb57bd176376953905bab986e699e75a6fd9e386f6e2a4f818ace4ac8c008be3f769cb82e484c58b26d24ef02a03d85c29617fb6a69bb92bdd2328399ec94dc15991e419eff4d42172b8540c9262319697ef5b944664d6240f592f28a5fdd7cafa8ca62e711eee939438ffd87f01eff411760f7cce8bb3be919ed67f3063b7f83670db0e0d071ae95b7701a7982dc4932eee32410b8d311cdd0a0e19dc9051f4e45f837f5583eb73641ccda38668ae9f657d6ac19bda3dbc6cec1ae39790c6d884a065f5231b20e3172cde571fa96c84b5eb1080d87cae4424dc3882fb4e95d6e453addea3973df1d9401bdad8a4e42725facdf03f908cdc6a5dc864c51f65ef0d3d1f563add9613b1ae78fc9030c042ed697cc04687dde8daaabe57bd886efa5c7260513c720a62ba7e74ddbc5fec309e168957f47f1e958f22094274e89da522c65b664f7406521180e0e07106aee2792cd4902fee843daad68b6afa8da5e0574ba3e0efe495b1f15c47f9a3860772123c9e6e99e67ea0d60e37d0b1250673800d27250f946663c920b9a0d052c3690baa86c8816f9087b4409f21f34a89d9af6e084ce243095138b4541157a2a9ed6cbb1268ef408bff5782cf7e1981dde454307438c1b65f219fffd2c216e86f245146380680deb84b0bac58ff1eefbe0979cfe35dd2811a12c2567d9c9fa67aa7aedac5cba37221f7bc83ecf078cbf1672c7a309d5ad9b330937796cb9557c3a93792d98588f74bdb5cd602247fca76ac9bd767b060c23970c9f714297f8e2f2cc3363c6aed271fb281127075b79686ecbf050760094ad2f2e2901fd0223c28a96203110d82b3721d853af37b512d3f3c303c84f42790e9e0f4e66c7874d5f436195c818e824cdd169f3d5d3c1f3f900cb301eba990c85cc2e1a93e959f1e28f4b504d75b83371d6d7e587cad5cc15aedeb97b0fa3b8b85520ca7b1d219c0f8cc79bd1e2ae3f2dadfdbc5fa99ccabbe28d5065469dd23090283acc0eb96aae9bbdfb0026b7dcab034f48feda54acc32744df2abd93754b192ca08b7bb30326a311d417ba2330b35e4e844868371a70d106b9a0bb2b27153331409ffe7016b919180aaf8968f13e73ed4ca425de02a75c9c828c2ae9c49f0333871bb8d9ca5ebd39072deff4483ea510a89e5dc07f0aac52a2c54ee2d39b8c00bf68cb160d55152eb5fe1ea3a0871900020b57662c70ba031f443b37f0d4911e1282d347e7e171d634fe7e18bab8dae2ef4882c5c18fba65e010937dbcd38729a5609d4f0faff23974be7da591d057a657b1a3fc25f6a72948a38e3a574bd53cbe966ad09a83e92b0e2eed887748a5bd667bd27fe578cbb385b0239e57e26636c4f74066b27800fd095cbda28987c6a106ea1f5167622235acd9eb1fd787b328a6bbe6c8a4121b54a9da643b96cd37d1fada52d4907865743fcac8911a7eb80d13ba6782f7f0efdf6cf4f063369b2ea3ec3c6b8483f8c1afb548b1e56a24cc048c43fd5921266c2b7ec90266402eb2b4e7af7338f6f12a2eee90af008066ae0c9cbd83640a0f8cbebe0f066a21163634e648fa8129c4c2f713db380b314b6d39bc12c4c64fd854ad8702c328243922576c0b756e5c2e21a03ea77968727b1c0af33ecc893445d8a83ea1f984f580b40ca83d4866fec7275a4011e5aa8b0ad11786db9c9ead3fad9065292e268f44816cbf7aba056c7966ccbbc9815d781fd42e041abbacf58bf14ee789b3088a182d35cb46fe48c1b48c96caee59ac597bb33dc2a9e440bdfa2d4db79390340c479119140c9da50780777c6fd23961f549d23dba6c30583c88267aa7fb0b48e6fa63f90505217d102c0a03c9fc6d9bd53326273e7e6b48849ff3c943bb04041894ca78c3782fefaf7e5d89cb228e03d73ec927127f8463439e82197ec16fc9b60c91cb7f2f7059172d47a8c4a4dd40ba02ff2fd047661c9499efcdca9dd6ef277dbc6c826c3697abd6c47bb779abb6fede9309151e8550a7ebe74b2d78c2d2922f3f730176fbd01b5464b6c280103e7d3f590742e16bf0ba93a180ab11106ace8249ac85af74f94c33829908fbca2653e8e03079f7de56f86ddb15e9bf2c5ae8f26ba5aa0d1fc907422fa2f94af955cc586e0d6121b6478f8a0aa403c0e46152ef7ab987bfd8df0c9b443ae6261b43d625c40f5029676d3ab17942a618660142ed9f03c2fbe6651bff02a1320f06cbf1f5a0150aeb50fbebd526b7a1a0ad1fcf17a517f427f2ce7feabf26f9851ab0685669139cea6f0437f689105f0d7142f18d1a29af45c7341c1e8e34ef9ebec99043d9019ed6ceacc4788a2da26b06633c4224c6e3614dc4c74cb7d5f86222b1faed710d4bf894af886dc2b2476eae79c0085ffaeaa2f2624f38569009a0e91aea02941967087000ced029db2c8f61ab221c07cd6ec9e24ac0351489b31eadcc99deee227f3b2bf61e602eb146a6bdd6dc4b68be0c2203ca0ed1ae8a30f689342e07be328dfdbd7d627f87087137c2758dee94828b3369e1e87a630608f6c309c0f49a31c79424021ea5948417e9d83ba8dbe9bc57cf08610f871fb7e8a95a64f4fa220442b2fd213b20d9c8f3242a2c8947b2e68f948e59d824b81720db12ec03037a38f65d8d436ebfe12337d9716122815b5f906d4fcda7fdf822339015d12a3d0322610adf0e9683e5271aac4fa7d3b9aea61d37b7b116626d6dcad31f5bb8bd092ae33f7d1e02604e098a7308fa9da37a0153cb8547904c4c7d9baf6fb98839aa2678de9d51aded24137bba58803ae613ea6346cf7b9ff01b0aea3686755a165522201a242e1c314992f72b3404e0e361a37231b6f56db3cbf24cd6f49a2b16352f3319c655e2764915e410d70b4b3ab4d5bf526eb1bdbd4949408138264183ff1840af26ffc6ccf85177e598fff6ccbd41cdd04700a4ca360afed848156bf0722647ba0187dff5056143c28b925bb995e3ea83edb228d719ae29d9fef78c7caa104758e25b6c0fd94d07b13afd9f9b064337ef15bcbe28da85db3606102390241164691044fec1a6ee9e4f59982bdb9c0bd9e46be2f41ad60f21f6b045f8f5931b2d399cd59a0f609cf433e1eb65bd925e402b54e5b6c494868d816bb3012e7e31a1897806076cc318a8212772f5a8be20313106858ebd07385b8710c7689ee312d11251c0ad78b35dabd0733493a2497ee9baa75c8aefaef2341a0eb848b44c14ba86ffb9582cd1d70e58d2d2cefe96461aa6f483d8fe092f893d4bf8a378c4095765c6b263df02fab0c32d02aca4f1216af2390f808122a54d2e046858f4f320e33de0ce0e182ec7051387f2c680b795fa226aea15fb0af9f10e3d5ffb870c2d50f8606eb9f26a48ce2d7ea67d91550613fc9d392f1b5a4acbbcaa9e0771710f339d26e97c52497d7d7acd096e9d7e5311b3348e9abbda9d3611758f59507e2dd42833e22cd59ca98d28fb667c53807d56bf78c999dc3951892db9137b8fb33cc9e0d82f4f8b7e0dbdddf53db6bc29258987aa20260071ac1aafbd97031e9b08fd66e054a3518e7ad5a93cd4ff42e27dd074813d0c05c47e9712692189fc6b06fb7b712ffdfcb46c452d1d736940fde8b35377664abb870606b0aeccc88078239a31ffc52290ac9dae70f8d1630c37d1458d0d802b3b851bdc7a983c5d1c6538b8fa232cf7aeb55d7027a4b6ea4133a83149d50b62d78944e30dbd6bb357fa29eafc4c267834b98d2b6d6b86503bdaeedbb829f99c1d03ba1584d062aa22f23b3c256491ba9c58585b25458bbd04b2622e159c205a26a73aea1b6a1175cd02112030b4cc15b7a0a6a271f5c1be3ee0c6a0a4bc907bcedb206c11f6973bbea01851340da3a00bac2545d377bb49c498277d70766174a79c4584c8fd14fdf4bb13d065a08354a92877ffff0954d4d719be6ac08b7d9be31aa7eac86af1e8f4703bc6c0ad08bd0aad9c66bbb2b29a0adcf87cd1135f3da5dfee09da4a0d74a5093dff2a395855107f800514f277ac5fe57da6d4f2a4ff9c6aa68f8efb7eea8b830854f612b8a6d3ce08f0f53f0acb615a9532f536bbd14642953a01382ee970f8193afbfa40dc15331e115113959405307c488878bee2c679cd73e7fa069503153fc47dddd318689b298001671213aa0dc3336664adc5246ad9d9465029534c6024935fd780c153fcbf3b1147370c7365b4365b6a0b4d8734b8df67771d974b8521250e7fba7623271e8df1c88733eeca314bcf82f354fcf11dbf05225b6e1bec19054e4b30700aabf2cd5626cfb2f633c30c4c60307ce7abddd748ecd5038733082d2277937e885e35ea6cb4243eb222325ea362e40156025c6d8d6ab2bcd8d42f2665a6b0cdccce23d96e73de194cc120e88cb819b8ff8a9037bdfe443ab91817ce6a5f4f4258acc5ab6a8525395ff8259b51acb2bd7e19154383abdc3b49592112067b97b34e7ba330acccd4d2e84af6a7d3b63255f23e4386ef6123c491511dfbcd0854c3719870bc46d9ab3eecd2e0a6d7fe3ea2c0b63787f3e4f6f097ef096411a5704d83dba1164c6eb21d941b5aad9344493bbd9adad3a9211726748102659a9acbf37877b8611b26f52886ee18c67e2f1d5793f50f2c01f135dfba202cb6c7e8fa9fcf4bc2d4d3e53f73af47753175c87e1d70e62599a89b58dbc9ef0a2b50edf808e2e338539f572572f43ef8728977832336b78c599b0a16c6e0a306488e4ef743079acc911db25c2e4699c23bb137a08af79e7b23d3984357783d303d3b1c26d85f8ac8ee5c6eccc0608601cf363e081fbf291c98e944b0caeb472825447819d66047a84e46ed4afd364975eed3ff3386a366c23ff196abb0cddf13587d0d9f62dafd5b57575493a375a533ce8ba6307cf69eaff2ab534b0a2b2d28074563407cbe4f6785e262b1c2c4588bf77260d0445dcd351352207fa0fb3836b5464eaa8a2091434b9b13aac578ad70025a38007cbcb52ae3b5f31bab5a3210e588a4a973d3bc6b188d197a05309ebd9860dedd26552350d79ccaa85554e90cf2d30c6171f35dfb3fdff7c445bde39e83587099b0f00208857ed6ee2cfb791eaa4b2c215f01fe62a436f4fdb3bb8a0b49f511fc46e3c154690a330b3c1a7a121e7a6b39f2a32832783cbdb4c46ea8fb27e3106abc16161bad2e46126e5e413d178e158ab9f3f0aa900c59681ada07a8987fcc9ebc2d15653b4725da7c2c982293bb518f95a27acf6d72f80f70580984a3ddad72ce663abe968e999eb5292e3567bdd794cf8e3ac1d8ea385ca8c799de8ba793bd232d74e9d9eabed75910b3212e56a17381c446d84e9ea152e6d50729fae5e3b4bddeb5f9e60fd0715581d51c569a4a0eda48722c0ce23b6f79070f3cf62659c914eb5e9e39432a5ee29b023e4d221d6111af4bcb0035abaa2f1b455e30e9fafcdc899ce46b99e9a08b96173a0e41bf60acc62ca77a09605f4075e6bbc398f8423ac8e0af14ffc836667be820da52fc8f54cad01ab82e9d37e55dd47554b09bd80e901c14ba091b339536053da467bca334a636828397debfd1a1ed6ad268d89b1f3a8b52a2924758c77e982e842b47884dc947321c99776d2e4d2cdfe8fe7530e8a5880f21ed49116913b93ee2a05d90adde24cca951cb6d43b20748b5ce2d58c8bf85c9a519114984d449062366b8c03077200db3969e9c518a0567db2bfdd06f80cdd32911e3426f51ff2f4269576579149e1691cbec5c4a1ca75a961130c24efef2fb7f8c31ee3234f228fec6f4a83882d88bd3afb14579699caa79ba83c22d9f6b847eb4ba46cd4413176701566a3b3071aa4de688d53d9dedc43eeb8f8c747309c55760b36252667f2cd1662ab35da31d5f7c57e2ba0c6c816725bf9dfb188eaa9c9111b5b2e11a7b05f81c7eaa81f6d32d31aa38c280283f2a25e912bd85b026d232bc6382cb42603b950865b0c0c97daa229faa268f746922cfee57434be927580dc5710e55d78a109addb06e6bedcbdf538aa23b44ac36708b13f7ff36e64328c285f7638ff6ff1f9358412e7e6ee9fa76de6cddb46f3f9fad1926f79730572ee33b21474fb7b510ae9cff75945ad096c0cba26c89fa8be62bb75f2f019df118e6e8ba03649cd60d6f522a172be8d5385b1f32a692d46a152957c9d2c4af00962ee5ae5bc463c31a4445b1fdbd6ba1a8e1fa456cc660034aa2dda0ac1b5f7d5557b05f6401be8227e02bc9c886432dac1f98f85cb06665c79e3bcdf19efc1b2f1ef6648b2b3855200efa6db57f9627e714dd21a2dfad1bb294367598c1eea15592ae450cf2476614126214f7673d6d6e32dce3f5e6c5ffd69ea81453943a3f1f5381196858417f1dcb4c3144133d150a06814690d359dd2a1981b0ee2e541716291171dcc5a07900e204b67396b508b70efc9d5238dc55105e3417ec8059e0d0f7fb953256d09f28d2c2acaadae487383af3b5d77c992c0c6f5c551f8e50b63f55fa07f5c1bb261aaa9a2a4fb8648066272eb9fbce73a2187ba781fc920baaa6522d48e0bc4dd938d9545bb8b3f2c3b0e0182a574014bac065245ba915fccfed7f60e71a5b76093e33eefb37ab63673cfc0d6c9e9560ebee5c11a48c54666c544f7e2cccf400e008fe65aefe7fbd8a731d56520fb2586a659b794bb2ac88be2516d02f420820ff23b9baa86a7d9829daa15d73a8abc91d29eb35dd3e4c9e5f4cb86be132b824d4072bdea886bfadb3fd9271f228e827223fc8af69965b28d3c85bc8366ca18d401c0cc8742a24b7d284bdc16d4b20024648cf296775081bf9dc7ec1b1de7168f5164d2e2b67b05a3b95cdbcbcd42bf4a548c2a9606742cdcfb104450c8ad940d833d55c6681f8e6879a256760c6fa3be66be7b56f3ef8f0299f0370a99274eb640ac5fa4257c7518969706a992010446f901699614627c9d8e183a7ef2d3098bc1577addbbd1fbc1d97408624679f82b3824c91eb9c7b50f861c62e457a7eb0db06dffd22553b2dff51c653c1bf2b8229d3027cfaf0a21be08b781dcd0bd154692fa6932ac5177f2e79d07463d5b9f372e7c6561a0a4d4b2af4a2bcc263012ee4abc5b88ff267be07b3473c17f5a605d80e143d3a9b30e06d84f5b0c52627a49f038ddb46c25c80e9a03657bb253ab89a4dc29abc38385abbaeb8dbe2c84473d42c513274f2e4be7ef918204be8aef05b486ad4488c734f35fbf56f9f44fbe148071c4543b7d2e141c2df9e5ef350ecf4eb1bbae75adbbc697fc1a197a9aa1a8a1849322b11f99b4a699961709ef7a25208af599fdfdd7c446ce5f8fe1ce73e87145e0ac7af4df5b5f6392cdf8824838ea5581bba0cfc5bf945c18488d0bf817118d109b33f588ac60e5b0b13154ba0c4e7e68b4c4e935f57828c6d69b54374f6204ee61adc764427aab38995fab59ffc6089f1f126c83cb2f9df26aad01e8d7b9688da3d6aae61f3191aca1be61423e4358568b9cc6b00f9e52c8c71591caeebb0247febe095d85f7706f9c6931588f397ddbf69df200bd2319bcf6f20182f3e02a31b002a351eeda4c5d21b593affb46ecf87043d2185daec97fe6ac105ccec3b926e7db9db87b2c7083c8210a49c9432e54a8b28f6090208625d53f15383580425e0ad63292b1aaba1ff035a84167c2f9f3e3d8a296d8843b734780592c04ba5205a75f7f4ded2384e3d83c65b845f4a50dfe204419a031321498566da125baa240b08de1846f434405d25141100ef46cb7c29605b36c2a0fd07644583642d3489fc3227854a857c9df2c7ab94739056e0d8cc15abb14e74ca9984e9a7d40d0c67d3570032c2942705aebf9fbe39a67833fafa2da4d44692bd30cae2e4908225c5d217c91a6b5b96d8de3669c6fbcca110481ab9d3a9c9835adacae5b4f87b0277099f64b222f272b528e6cef0953fad5f834e9b553c893b022631d14ba878a14fe89fbd5292d04b04aa87cedf4ff0531819518ed490f9d5fbcb53d56676efb819a835587e4b0a53161802835902cdcfe2041cea51dc6c22f828050715ba44a66a60e7dd84eb805de69cc962b21492d8ff6c4f41df3180763156f7d642c9fba0be0915ca737ff5877bda260d4a7492b456ef030d3a16a9a566d991a33e4fb5baf6cfb160d40c35367486d062e3de3cb8888ab90b2a5c3f853197a34a18031c24162d738ae98e16564771621fae866b16b62763132590e8195656fab555dae322378ee9b3cc00d306ca9ee279ad8473898b13e1f2913d836384fc82b09e003ab4fd09e59aa7ba913a9cf19aa70d14bb41a52d60085ad964a96873b8800c3b6981181f6a0bb06833d2edece98f8dca15e9295ecef9ce2c2acbd7f1a07ae8d3eb9bfee7574cd6bde9da835fef368c880e023eea2b749ccf8b781eba2d4bb861820e768624ea907a28b3b38857263cabcb033f84b322b249f5f85d0fd968e1801021010062c1b48880e99e51aaaf3f8a3ad599e35cbaa13e26f431679324aedeca9fec20c4a2092ca3bda319a5a690ee0325f970dc666e84500b842c99c56f3d10faa51401007caa2ad2f85402662ddb177780446e2527b1dab2c45f8998fc38e9de27077fae9e1c59b89f1985aad577f46fd8678a27a8453eb4d53db78a1c31fb996430160e000000000000000001000bef2202d260de237279b733e15d5656b6b83d271850e0d9f62bebcb4c3231b3cb078ce2a6d057b6f3ee28cfa63570d5e1a59f2e3b99160048a04bad692b480a21009aa99143bb05394cdcaa3c383b295dbcf4251ac0e1a66667c94386cae62d6e8201ede0d62a36ec540a26dd480c71983b59b9b19d95fa413aaa3f996af9d2db42c602cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b000000000000000000ea389f42f0fae919b089e6a552c9bb94a2b393639079292fe0efb882d512e30966e82f641460b93ba4bfb1d6088e3ec81412cdcd5d65cac6ff4a6550f363dba51238458b0d0dd6d493ec964a7462117237ea214ab8bb54b5a7d9e005f5606865f5c7c04adb7149725e02ad007000000000000a8d2baa74a4e584dc4950e001de4eba6e78ef8c1f014dd0d6c441fb126bd112dfe19227ab1f647fbff2902980828713d839657cf0509a8994106145e549c4cc400010041001ba3a30a34793ed7358bf9b4b5e1957f0c291a6384a29b354616bc3c171e76d6e8397edfdbbe60793626ea527ebf7bda5d6018329c185b5244783c4b40ed38ff +04000000fc07658084c9fac4049efb82ece2b172773010843e28a298566fd7dfcbf9bfb2a4e40191eaee60ab649e14c01069e32b33e48b16e6afd2a33e1927ee1e486d37e75328859c35302ed8b2a12b909c718566c9167f2f8ab5e9f863f206e3ae2f7022254a4d0f0f0f200202020202020202020202020202020202020202020202020202020202020202fd4005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020600008077777777d80a197700000000050000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025500ffffffff0140be4025000000001976a91475dd6d7f4bef95aa1ff1a711e5bfd853b4c6aaf888ac010000000000000600008077777777d80a1977000000002d000000000000000000000000000000010204e62b350cc3fb9e534e9a7fe8d5eb7c1077346f496496f4c802dbde8ea7ce0261a2219fd8f49e19a250dd06610bc454d2aabb22818f8642b6cc58748d4f2f30171713c8f7639b14280e4c08fbc779529582a77bb095b7fa6e1edca2969ba62324eaeef1b138506cfb304bd05b8ef2a1d5bcbf50f94d963c0a655b13c1d255172b1445c44a6e1be88c5b23e90d7ace494fa297b102d4f20013cb31b19f8ab81d1353737e550975db0e4b1200b6fe3c30e3ecfc82a744c11ab8c64e22c75c21d70ecdce8d8ec90664c25612f21b1b7d9b3179289e1546bdf76ee9144da6393ef833a890e2ddbebd6c82538e992f23ee3c92b01a0ab0e13fe258452ea33638ead32ad899676b5ae35741262744889b72303580842b203356cfc3cd854bf5f0388ec950bd1de99b4246c40739fbce1b01bc80c2d4c6496885b47cc01a7dd0b152b39e016f99c4799e238faf6e5f17de80297ee54d915cd6b76cce810b82906ce0b1a3ef0cd276048c94683283b6ced11566196b35e9fa74c828272d182f42175e71e0cdff1425ff9e6c274d969c65ce91f5d0dafb8e292657f8538228ab58f9a03980a16bc78e76029a151f5c24fe0794ea793ead54230772606e2b337442acc7914b479565a6a10262b8cf34cbc858eeaa6a89e82ac6998b22a30db36a0caee0298733f33618a82ea3057eb8a9c03a16c23baf26e6f99af864fc31fdc17aa1ccf941e083005e6d7a8710a4a8a0f837cf08659efe3f6a523c4bc368a18dc31458362276406516b9b6d74605ce3e0b8fb5de508a392c08fa4d5f4ec8f6b2e2fbb71033385b37d0c4878963a21f0226d5e9028659386c15f8dfe1718f7cc819cef278e779e82dcd9640ae5b8afa36f6cf0ff7ec09c07d2a97330f587c08a7a69320eb6c32fc6157befb6e3c36999e0140ed0e942108d49b6da947fea16eaa2e7374af7bc7f924e2c06af0c2d870b3ae1938e765bc43b372716a74e7995f94fa1fdce8fd6fee1f54c893a8e8c51d64afaf76840a0e00787bed3777598a214f1211c6e7a2e5e3e40349f887ea4340461bcca211dbc661917ca282964e538cd5a2cee444cba85ae382bd6b1e58f24646ad9f6e02dc8f13c9b0fd7b7012d359659b60020d1abcfdb6d13285323f072bc1fbac6e5c5ac147b3178e732859543a47a2641c3876fd4e7f3b6461a1dd9995a923f4d498a8ebbfe147053deb2d5f3147eb6465fa620fc01d70cd3fbe1576237ddc418f067df1b88329865e6dd878815502d570738bea518412d002b3aa4f3294779eb12d92a6672d8cacc8ed83b0725a414e4b1dd265d38cb80b5205c1c391b457094e27e8ff318b15f3b8e70173eeb5057c13c20b5b79281ce203c22f157e7b7c2414a0e28e5d0d43a43e6b2dcbbbd3259ec8d616046cbf6e9b90ff13a3580fae491693021a9812784fd59b328c0f0624a947cb2b119e47834265af5632981935a2485b111e96fa3a8492fafd1dda772b6c206f2408c2b05c6d4aaa263b0a2f01a07ec875fce26625cd99783b166b09b526f99d3a14645e20d9c5c3d48c4360146253dd45002c103094fb765b251741be3475149926d443a63f83a6020fd91526ae834e0d26becf2a7dcdcaf221166e1bb859043f5c2b717ce0631146216eec72f31ad571773fea8321fa89e8e0cb50068fbf16f92a7dfa968c624e50d3dc3b12fe2d97cafec9e4ff96e0de91ef1396fe30cd3f2b8468308826cd52fa468aa78376bfb06994e580a4d6293df6233b713a407448f231af2ea792206f13f38bd3f5490df420cd1c6f59aeb9d9af98a9dcfd3ef9250b337b7c683e6f709bfedd76b528405d00268da7de378c0db0ccc814d88687f2256b25a72c630e8a079a16e302f13ba91c341345a7dae6c749b0ba2eab5753574341b1c34bb43d00b20ac55b47717632efe41ac6d2cc54873312f58ac78d97d0aa6cd26283d126e21159a526ac879d4588bf354179366afac9ca0551819c6b8d36075acd5cc378d7151e81d2124d19b1cbbda6afafedae6fa5811e69d7cf052bcb295b6c4f63f1ac7963f19b6f22f6382cb85f01ebe3df8b7cffd58b909c1b8b3d73d0bf641ce432dac3582882a8548468227ab76f44e9c8bb50276367b64719ff87c1db3cccd018cb1c387e04eec766323856f8f2759fd57576a52a876bac28894bb61010bd5aa0277e76679e8d56161c310da35a5082fb328ee2d838f036bad6ebe41f6a1440899dd7434f44445bb4eb13b13bcf63f849eece9f625047319a1e7ca3226223b5392b8c7259e42c9867c1551ab9bedc2cf94f65af685384d7e4b3aaa1c2cb5caf9df653280082b02a90d008bc553ea21be9f33c737a07642c29729f2ed91cd064bcc1229ee078c71522d0d9c8f986cc9325130e04ea6e4cc284baaa1d8b8305630207276e12bae5d450e56a6d644914ccfe9c29d77d58ed14c7d1b1529a74007de4080000000000fde01c3c31f241df60ec2fdbcc5fecf1063856060dd67068c1e904d44ad54e8d1390366382072a240f7503425bea378635b68508ecc51c71b385e73d409c497c8bbb1c30087484f9064d26a968a73274d9f490013910ac5b91cc5e8b39ec41088db40e521e38fe226e01068d250aaccb3c5594dd6007bd2424ef6c229e2178ed7f8d2095bcb32d190c4f692677b455465b11a43d1c0c06797229bd9e2cefd447ef3cb1a0bb165ab2108afeffca4fae9cadfe435aa94218bd9012e9837dd664b5e807103707b47061bc0f1478c2d7e0662199f9f44f71c0068f26b08ec22e699534b2b88031c7d3123dbb7128018bd2bd5b779283fc20178bd21610c22f7964cf830dbb3d433da5f817f4e78506accf457f1483fb9b40d4d59acec2abafc9e15426ad22792ff356731ee19e6c047f379d8d0d426f2f2e40e330005034c3426197467b31f2c187ccafd8bd5bb0e29d001bbabbe82896b17037ccafb3c0caf670d6dc002a82445cf62f54df95087f5dac4043e328aef2eeaddf620c278bbe1a079e1e0527ed52322d7f2f2e8682c1375728ef21ab0fc6687593baca8e7083807e9491ce827eb6347f4e9e77741820e19fb121ed0e11f1a04331010f472c7db2f665ff0929c71702a4881068c050efa8b751fbbe3ade4ff557d67150bf41bca7518219949a3af3c10287a318ffe2b4afe6163a18f9d45a7fb65bbf635e0c9fd16d867bf594d7384fd07600d93bcaae0498de1b6501a1684614e1fa0ba0ee384aae08b8bcbd6421a8850a4a11b7c6a285a82bd6ebd4a4e95bdc5f301ae4e59ca023d2fce08f145b7206bb18c1b50825256d63121dae1c5fd9969e5c319f19c05e8b4958e10ef5622990084ccbc87cc32773eb3cb5f838a7b793f8f02213987ec80706926c81f5b776457a1b4f5814254b9d0aa77ed1a69baba94db13f6b94fdb49b3ecf40b7ded001f8e4e08ba8e8afd8e0bc306aa9de4c65b835576000a65184c9c56ac82e6f356b2adcf00301bd613445c35054178afa9bcb4de35adb01083be4438e11a6e7168d8724a21304da3eaeeece010ba3a1de93226f09d5451fe4bec6d97d1e26f6d691ee270d7b040b41b4806401655e44ec37fa74c5c1a0a82f12ba0286289d930fb24145b9fa22ab149524850df109abebb37fac428aa4cc0d85360a9d7b034ccdd0df6d7fe251d6cb5c5523182a9fc30d2f71c658339ce901eb47bfd38518f45a994cb6167ee6d09c5358842fd7f0633d9cc73666d1e8180623bd8c214396c4eeb3403fc9f92421359d7741385f0a080e35b9a6abeafa6b6f6e8cc5520029f829c6f058de0ce6dda90471e675f1b79d2b9443b29f01cfb6f2b4974f597021879971eaeaa452d7d503da67557bfe5fd5d654779e265e43b450665892afa3bc1afdff17c70f36d84e0e63929bdf9ee56449708f57e4094adc52000ef77b4d20fdf6beb5547f2112723fa29fde6161586b59a75e6f6f89889f284aed4d423088ff79308bdff37cb26e53f4a327c9404ab7dd4bfb8105743a6fbd9617a515b8beb1b292dd2ac6e45f9b4aab50dfa946e070f3bf1be8a62e913eac6a521cd82130a243db4c18c3f3488597a00776be080c8781f775b83f1fb6b23901684288a48204e2cc060287c87b583a45663ea13f5a3b9278c6a65566bd9c31127dce99743ccceff44ea95d27fb215169e530d6366869c4275bc0b922e5d55be0fd79278b03108b635a312853b8a08179aa8dfc16a8a7562150fd1d336d6d447e0a049df73aa638199239d964ea3351f4d532f1e5e9c0c72b2c4137dfb212c23a86b9b48d0b6d079d8302aa5f17c5d37651e5b143d71feffb802e462bfa2ea22caf982bc7b7a33edfae0258689b3a6c15e612f876327045054e74b4f972cd78c8609afb0f1c4b231aeb18ab466eb75e55057ed265dc393341bf6a663ba55468ef34848844993da101a1271735cb7d527a847730fa874f007231a1c6ae5684ddee27f372691179ca850036bd1fb13d7cd06549e146ca40236f23f4cbb9d4f0337cf16379c7a1367b5871960de2c6616b6cc5b9b0c8b09cc83ad4083788138900ab77c16d8f0f989d05dfe33e4e899526aa97492b842b877eb509fbc0b740a7e72ba158fe6eb848849af120a5ab62226096a40a404f80e8b1c2f1a41b9d08aa006bab10453c083656db4493559aeee65791545993e9adfb8629350af7b81345155a7037b73387589fd4c49e0c6557cf96ff30705c22338cf0b756bd83a5c19bfbffd1ee49f906f630280d72794b851670dc0db727eb96f522ece4ea175f9ef4c10e4d7998b2ad0b0e80240e385171447d416edf320213ed1d0aa98b26401226b494571f34a82514e6fe40c9fc240c64ba08b1680a5454df3ce2fa0dda059a248d6f347dcbf939783f55469b7a37777216d3a1daa2377786b64712e8c8d1ed8d3f9e57c1ac3413ddb97306801707e3db6dbfee108bcd202680188a1886f970796159710074b11ea2873641198af9aa9d4b060cd70d8281fe4be7dbf1700b0ddf2932a7c16c5b1e4cbfcd5cdc8daa67eea226d06a8dcfa5d441c5608eb35a1ffd927541afd1bf226becf35f95f230f206bdeedcb3a53158518dbde8360dec5780bdfd4694ead93424d895485d89d635612c71a913e7494fbcb4b5a9446abb772b86d4ff02b3072eb5c0ad799440fbcd125d2d36538ca8bd4efd63b83b960ad024eb505a72a81c1b0359ddcf3dfd90a84611e22a50c8d7ba5c06c1e37fe85e4271a9f4191368b539d5adac971a149ccad5927195d5464d036f9152b0366ffdb1dd6f4154d6cf973c63b0f00c13fc9cdcf79f81e36edae45900686ce8a1839822427677a65ee994313bfdf871db8c5906bf32536e503dd12cc37e0b79bb7948076cdd2cf3c7a43938f56e09767da6e4eba5b5b6c4fd90d03058d19e2a67902b0c7df50f287b4aac144b71a7d3b66e86d365e9248c0e8cd70ef04b82e35982240540abe624132cf73f2f62d89aa39565a03a4ef1b8bc65725f61bdbdc1cb845a74ac861fbfd8540b194119908bbd16f82f5ddd82c3dd6750165ae4d9a4806d390cce706c841d02bd05c3e5d6653f62fbabd0a60c5c3e5513703482fe00be3ddbb5164536a79d50062c17e68a09b758353635517e5b86a8a8c2b6ca4c82510b4f720c337c77817b7411d96f76f15bcde149ea6999078e104f23fc6d4fa41b3d931c8345eb99afe130088ac58ce7984947634d3281c929a80631fe9e5db7d28e1675b97277bfb5f22435d7fe213b22c4bb04e01650fc7aa07837cac7c9edd1f14e6511933ff994aada1c28cbcd6d9456b9a170c4c845ecb6bd2f336ad23cfeda7174ca350444746490053fdd190faf349816aa8056ca720a71b1a48c78897c6d18d2e1726950fa41da2066f23fbe70380952b8e5425e8606a460ef995096dffd5852fb68c23c68260a3fc270d63b9035d79bebc3233ba20e3881e4dd1a61d18b6b4ccd890bbe22a88f33285adb696fc49f93a1c8591ae3c1f73678622a918ac14295ad151815cb64fd040533e823dbcf3065f321cf2fcffd37c95fb8947b509033b46e102c1112fdf213e112f39470fb4c7ea75cb7c7511464b4391a6a565a0d363f5f7ece69bd9d2e263edcd62b947c15752ced5bcaca18baa53f9e2be549eb2dad53b0e89cbfd4091589ed4fbce392ac4e9c637b36931c585a9443c943a90ceccb9a3680b79cabb619f8b8506a3fabe55293d689ea35ea27982bf48d786c3edcdc8911ca887abb8917db5e2b5ce095d21fd04673bb93d829b6802fbf7c2fbd286bdee993f57407621aa9d4060201881c186c0c8781a6ae099f6eaccd6282e0ee28f8f977e2f182312d6211e932dff2a123d4fca6f69fe40e3fcd97d09310d94bef54369ddcbaa40a3305139165e724e5d548eefb88ff8a6a537c841abb7cdcfac640d557899ab2252ec7dfbd10a100f433a3759597774ae6bc0bb59291a0680b81a4a5d5ba45563e2108d592a3a79f7715bb25adcfe19df467c26c5629a1afa58816081cff2651a003f8074c050f647b6f143457e273442e451f48dd51753d9619f0e911ff2302d5114217ce764cf1e2f6650df954476dfe27c8a1cbfb1fc63e11bd959ecf5f4715093e3eeac1c39b64ef9f3aeb1276e36ba5e4b508550ab42bf0f7dda2fabdb2dd181ac5c771f4665a737352baab11cc7c770c11b63b506eb9702e65d401bf0b2c30f3a4e5feeed4f692cb9e66b8d3f417832632cc4afdca8e2e30c0fec68b49323f99cb92a1ae820544d4c348f9e9068ce9bbb3680c3e532bf2856ed1577861460561616d1ee85a62f344c1854515f0498bf35d6e73d0a50d80044c7028b4586b1d3fb16a01727a5fb6e8bcf10062c2997d1319b3313dfd3f20dfc10d1d08bfdc0fc038f4cf64bfadbe28b46e470ae5b0f655fc748258889cd514aac8be44e6d50ccaa42395171c5146d9991e35ed56ac5eca5c9bc3acc29c5ac76fe8b68577631612fd780f1d10ac6f54ef978a1388ac58af646ba9a4d712e57b2819b974e1d01743ee05f2726c4d49308c8f7f4a87dd8ec26d700429f9f807b3948e0b24038904034d75f7460bd7a9ac1c2e53176f1fc939fd6413896eb997e7759fa59e36b222797d344c889448003461ee0fa107941335b85d6dd5b743512f99896d6c141010456eb1bf4c225d98e64b3f9220c41cc9f2542e1c8773bde290fdfcb6d1139a0c514ddcdac8923ffa48c98ea8a6c6d66dc981d6a22e52b9ba9aefede64abc4c0fe68fefb262396b5195f9a013be51196a3f17ab82ace66e92e3de66162857ae0c4b59f0cce30e07913e6a2d1afed2c430c9618979499862c6d9f148cb67816d3baff0a4e3b8d4ddce70ea3bdce97ac4b523ed3fd5684d9d7c1a18952559aa7122d530911c72dd455b25213e60f6b8d73fcdc022946604e25a239c7f1a8f2d5c06d54a45f95664c758e53e7d06720c3433da00da62f90345dd6e0723d29ba3aa2c4f434c2724b06c0148168e7d787e282cc5dce5c362c6dddcd021b4d74ff0fc2fc4f87623eb7f61a46ea7a25be96d22ba6bbb1ff32797e610c5db39bd119bf90e39eba64b785112143f19aa543ba296710488b5da525a01b123b0c845ff953606508c5c0ceaa0bd39e51fd999f45dce06bab2a4ec10ca1be00d19bebc9e74c413460d68e387e3a909668988793b6b6c232b7dad15daaf65a27e74205240263011fe31e9eda93fe83ad1c87527b1ae0294c53ed09086dbece3732d581ebb90cf398d0f7c88d2277c01db924bc3cd2924f086bfd43fe9610406770ad7ad0f0a7a1205979dded77a12fc19b494547982d51ab8caa30277f29781cac5f9a549631a174cbdfb101c7d92748d455d80fffc8bb02e84bc1c559b9b99ab8f7c27b994af38ab30f1def655e631b71ad026248e1d7faaabd8bf3baa2f9f4279e4495761793d291086aa2cddced6e4c867936d3afcb714c38a892b58543a9d01283c8d4b550f36342a521a756723d373ecc421e033bfafb7d21ee09faba05dbdf5e9e4bf86272db94bb2ff7452aaa06f4e257443d03c85f85596d79f75dc37316999ac7e5a1dfee2c5913a87440d23452a8be77f6762bbc721f99700a31c02501818d50def3bfabb1a97a5dcec6fffa9dece47f53b9bf6d4b348a4fd703405e6707b466a8a073d345ac8ae95885cfc0d2d822f93088a84963dbc727296bef55be83bf134b42e0f02a0dac0cbf0d15c8455161ee202c84cf1de8c526d290560a3f8bf2f4c6f1abb938a845973831c92a7eb86ad56399f2767459296a26e256c7e4b3e035d4513d8033744bf9006b2c7746ef85970706855ccf45bf5d18e0ad062b2226c5ced144e45fb9b81d77cd2afa53803e392e1842098837fa60e96eb177408b42946032d1b83695a391a474cc71c652cb35564ed77a2fac680872e6e35d72dfb7f569829d942606f7bffb0f8cc31e8729b327eba12f96447f2bd1a957b7f5afa004c97032bce06fb7842d2d15a0d9f209708000bd8c8df2ed8dc1e42d71c2e47739bed20a27b07a77f273363cabf0436485e2da529d5e15e93c403bbe2c73df628dd493a00bc936cfc3755ef937eb57f25e30a1a0e0b5ff162ad612882a7932ed73d0f1c0ca2e35d70b2a0d54b072f0755398c2101a5de012a9685cca8d77642de53bd3ed5f92ec4bf6e0a0b963557c5b63faecf8d496b1e543dbff027fd7f2e829e101ac378fe439854fe662546c541a935b8e4a4a246b3fcdd47fd4aa4c9cad41f222bae2ab7573d8d1a0b96e4ff0262ac83f95f00884745f768000f0d9a861106bb2459bdcf337810036a6de9f18cdd8f60359a255cf0ca80b425cf76eafe94600b2fbff6415ab81ee7a6ba5bee24c53869dd6b993f90a301172d0163d5d83434863f661879e6a967df25afa744e421b508a1d29af418f8834623551d9a09f5552e201e7fe7f2c7b8ddede38419f168800b2f9ce5bab8fe761bfbe720f69bc62ad90b9f9664febbae9d93b372d64b6cd3f768da8a26ed6e159f3a0fee26be4d649c2965ef316aeade7a0d955333064f19498024fb98e2d20fb1f3dd612fdb187c18290479aa0efd3bf09e92dfb8492471617f34e64db9d43c2dc17777140306e7d603d453fd0d4c95a0216d8e9a7d910cd934903a19784678ea61be5fef17e27a840bd91c723d41c3520e9c25f52c531f362b1880614e7d49212749e533fb430c793f056a6992c34db6c67ec58e5c73453aa2182ce2167f1cb9d3157e49cf48e8770171e9fa66e4ef743a66dc02bfecb9aee6b50ff20b1538e1a8ff61c5072c072112f48e2f86845c075d5243dbfc9ca5a26e6e47214dd32c7e09eb9cac3bb1cee506d44ca2e101256a630dfdb7b2457dba53623cdcff38133f513a5377817bf1661e295681a05902af9db5f691af8cc0f3a4afd935dc6a9622c6b71f58cfe1bcea332de93ed300201424be86040b6a309bafd9aaf720160fc809ef5daa01ee71ce3a74450188bba86b65dc62791c6300a068177afbea8d62645528056c142526110ca8c1a955a5557c7798a9c99aabb448ef95a32689e7981f0327debb7c41747b1ed7d2cb4e472f332236085d22dbf69a2c4dc90e35d11335b0f46995e51c5b680c86464ef15a720e816cb0640908d60212415e07c1bfe50fee23366abcd0ef9c3f447d5d5812535c7160751072ca2e3b6a22ad9936310309998af954c6049c9c133a90679766d85f5ad1a55c3d2ec80581fa3842272afcc45ed2adc6a483250c32ec4c0aee7829f31ee0eba74df54703f29b28c28f6ddc9e14bb60fb7f28d0f807ef7fa89ea3535c067cb1c8da1aba31a8efd96eccb7c1e015f437beffe73da809a147d10ad67c28aaac70503418ad38b0a1bf967bc8d392d7ffba8407b96096397258160b7370495b6fca037c939dabca7beb97bab57315e51a6e48a9b664592572fcf83877e2489ea678898d0eeb980049cbde018289eda508f3110819dfb6127662d17e2f48372a6572b9fba5e5f48c389ab5afe6eced9654fabb2566cd3b1c48278a7129121b3d8a2f077e721bd35d899743e3c195675ce91cb74e595bca15ce272cfedc54e3f1282488745e57b9c094209418545205e1f2318a7bffef5824f78548ba152ddad28510eaacc2f774a553cfd11313364ad3751bbe690bda9a35e8329d4bb60f0098050f27746692eb16fc3ea4d8b6c8dfdd15216ec1ce49403e2c562697dc509b3f6264d28a982fc848149a8b67f607c2c1690f15557204421921cf07ed9d773714e7515098ce5f85a69879f970f3b56b311f95811db29d4730055dd767c94f480383241fd2f8d238b1d98df97aea2317d16f794aaff1bff91102a8ff0c5336f8dce376d6019639f415faff58e7d25e266d4cf891a86ded55211ff3159cf9c7aa19145a3cf416247f2d9dba0c950b97cc1df9d5bc793d1fb21b59fc068f86e857bab133821591eee8f7bf2c1d7d1834284bfb0606e33ac6c51dc9d5dd82bffb4e8eeadc66193e2e9b2212bfb84729facc55e25cb46e8c8a2d07e7e6df2cc66b542a09329b42cb5e5f57209c7983f8dfe7e9891eb1d49d8a740e7b3349d72773fb6f01a4207dd9f97a57aa5b4d43180436e73a49b9bf774ee71a225a2f121b3d3d460028e8fdad874c0110f4af5f490cce58fb3bba805170860a40972d464547522ef1313c41b034b6fe160a67aa32081716fecd47199d95df120e23623ea6185b0cc041d0a091ce90cd84e0382c72bc2d516acb227b0f60dc31370601672e093c4e4fef3466b225117e38c1a54106c71ba3bdd52ba962a5a634245fd39da6fc069d5c93e72a46cc5f20e5a0447c2f4e9c3e10436bf3539f3b0d8aa312f9011046c840f60ae5d980814815f215d82e0b02cf0d0807569972d8372ed9a68430162b6794d3e1b62618ac66229bbd7097dc2c4d87fe679cf1df3f23ec2ff74b96b022b5fe74eb942500c19e648881d34536df22f98a75cafa39dc3fc562b5479b4f58ac629e7b87b2d588b881fca1c012b3097ced3e1c2703be962e4d4f2a47d733c490d9f614910c2a365dd93dac055313d88ea441698a70c42403ae4fd810a31008930067e352ec610338c0e5d44f79bb8fd35e1c15d05bf89611fe350fced1a5c32257d98f4444b69d0286e4ad3addcc70e3073388d737803834987384235c87b539f03bc66153f6f418f1215bb98c3e5eff502df7a775bbe02f485cda80732f0f74ff9aa7cf59707758803eab80e72aa89077015fbeeb7cc5262a64aafb2868573826a7028c2949faffce93f20b1c64709e5a6f3c07fd49cf0b58389ff02d5bad87de08fc739539fdddc4e1d769475c687bf99def2d1911ae00c8c56ba044d4c186faba935c5641577df401ee2a649d1b610d21dcb45388320a536a850050d24d1fc9ba71f0b6f4cfe50eacb7405f13499178ae1594bfa7a81019e7d27f5f8128c7e40e1b164c2a35788cb0eccdffefe9b1478dcb801c23e111fe8f0d98a2f0e0350378b7a69718ed8c0d2b88256797f76d158915a795d9cd3df893f4c1b4389379150e01fdd8c10bdb1b61f1438c110be156e71978fcb70216e4462d7ade483c826f5f92379e5732b0ad73fde88f2b804cdd5dcc76b88c40888a372f7d3e643fc7ff99d3b0560e06dca1680a4edaf5fe9e827242465dfb8d0f8b0443623ef77a5539582fdc71a1b8a179d76646216cc36de47f8544b525502ce18416d808c7f636e8be7eae69cc8d7c981b1247198ae89cebfa12365c9e22143ca06906a68188ec98b8c2a2a58b4e96037e7bfb5baec90db9b72a2238c231035bb7634934a7896a93fa95ac22db46e591480d6e9f804cca1bc7ea2ae1c98523ce128cda3680c1403ade9ca4cb8e22dd7f4290cbd939860b27a4a570e7d7b198871dda9fdb8244320afdb10b89e386cd6bf0c77ae75d0676a6d011b71b644918dfc2a46172d18b023fdd368f89caf2d6318d8c8feaface74fd59be3460c86ab40fec6ec3d914da168cc4313250a40a1cbf58899e451839b9abd9d5a1a104bf978c9d820ee5065611df87552841ac9968682a7c926ee88a3ab222249a6484c60ef64fc22c228e383c523af050856c73abc8097707096bcceb4eec54946678440002892edeb531434ec50cf5434b1201e2ee4bbef5b582ea3b5a67647e0ad530130a242e1dabfd5bab6a00c2919a89b612a355c7b5661208e1e90b665b84046e95e0def0c76b02512aaefce5508a283938e5ea09e85bd40acf05fa436435d9f79308ed29d89daeca96a40a9c729b510bec72cee0c92f90fafd17ef049c0708060c84afa095ae8f62ea908f94e2a1bed50270ff339ae3e8e4698cc083c789d49099f044f12386067fca4112b56616650e73a8a8a5dbe7fe88b5d6042eec7a11018b7269566b069ba808e827363c4ea600069213024bb2e44311019cf6482c00ff83efa284a2334bc8129f014bfa811e7287d86dbe659867ea99011892e2b930a489de88d246ac1a86a69573eacfdda83246621a18f9fa7e922629df333005704791dbf4b420be331efee8d633d06a9193050bec0c9238736a8edb01339b6e07fda0896f65e4264f242790c8ea1b92797dbae1968d63a0c97460bee69824b515dd16c11c8b69a76811eaa3e2cd77a0ebfb04c790059a28448ba2492c0541e669db9aecaeb4e3717155418818dff8f8a4b2b5d1ca8927afa5e4ea8255b3947bca0b1c259a69e0fd6cf58041cf4f83b4f0425c24721c8785fb9ae18e14d928874dce15ca0e3d4da0dc0a1f6430d1027fdb4dcbc1066b15ac9c352d6aecce7185a1d50a0507bf9105dff3e9c2fa887a3b11e5a128ff666bc4f411297e7adafbbe3f19841142d46801762865d446b62d5972554f6a2a6ccb45ad5bbc81fd3133cd9d38bbc2f8f74322c78002f7a6be02757061060c48b3665c3d23dccb1fb2241c6668375e3e7879bfe4f5a0fb16e1683e790dfedf0a14abff6290f91bec539db0498930010025ab47c4a8b5bf056e98b348df4d9895f31c7446c69a5bb343e8985d0ab97e0390ce58c0f8c833c1f9d64ec4344fb9a3d9b594ade96e21d95a514dfae0ccba1a01003fe8e91d5cc9d7ef4f99458f35e9ac864df030b484a896c463ddfe82f1485d23083901b1bb330364df2fe5711c1427efd8d91946a3ab8e96c7b4c714fd16562100000000000000000100ae35d13e248bf5b4c177098c04765ec8426316f9117da25a2f55c3f395b50594704048761fa481ac88c562742037c38a19ce326a9805552cae5c01a778ed0c2321009aa99143bb05394cdcaa3c383b295dbcf4251ac0e1a66667c94386cae62d6e8201538a571fd7d618d45d0b2177ecf6e95cb378ad747e86a24a247f8b89dea23b8c02cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b00000000000000000831fe70c0679334d1a74dd348aa7c754a9b3b078283d9590dd62169b7933002521f3c26b84dd536603e05d95df327f851f2afa721ebf2db08c8d694d9284eccb51238458b0d0dd6d493ec964a7462117237ea214ab8bb54b5a7d9e005f5606865f5c7c04adb7149725e02ad0070000000000009156e03a0e30b4eeb3c4903909556fbde7e915d4a8084f8050df20ab1d981335244ecccbbc9d92d2de5f567a8c04f99e4b7977a61d97c99c51dbd0841474be80000100410054adefe35d6d946a45fadff8baffb45742a39c7c391bf4bca29ed0034ab8c5dc73365e7d494f3c5a50b64da633d4e6e4b23c38242158408c181df58711efb74a diff --git a/zebra-utils/CHANGELOG.md b/zebra-utils/CHANGELOG.md index 0531f82a425..d784b780c2d 100644 --- a/zebra-utils/CHANGELOG.md +++ b/zebra-utils/CHANGELOG.md @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.1] - 2026-06-18 + +### Changed + +- `zebra-rpc` dependency bumped to `10.0.1` +- `zebra-checkpoints` now backs off `MAX_BLOCK_REORG_HEIGHT` (1000) instead of the coinbase + maturity (100) when choosing the highest checkpoint. + +## [8.0.0] - 2026-06-10 + +### Changed + +- `zebra-chain` dependency bumped to `10.0.0`, `zebra-rpc` to `10.0.0`, and + `zebra-node-services` to `8.0.0`. No other changes to this crate. + +## [7.0.0] - 2026-06-02 + +### Changed + +- Update to `zebra-chain` 9.0.0, `zebra-rpc` 9.0.0, and `zebra-node-services` 7.0.0 + (NU6.2 support). No other changes to this crate. + +## [6.0.0] - 2026-05-01 + +### Changed + +- `zebra-chain` bumped to `7.0.0`. +- `zebra-rpc` bumped to `7.0.0`. +- `zebra-node-services` bumped to `5.0.0`. + ## [5.0.0] - 2026-03-12 ### Breaking Changes diff --git a/zebra-utils/Cargo.toml b/zebra-utils/Cargo.toml index 8a38921a6c1..3b8438a92ab 100644 --- a/zebra-utils/Cargo.toml +++ b/zebra-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zebra-utils" -version = "5.0.0" +version = "8.0.1" authors.workspace = true description = "Developer tools for Zebra maintenance and testing" license.workspace = true @@ -60,11 +60,11 @@ tracing-error = { workspace = true } tracing-subscriber = { workspace = true } thiserror = { workspace = true } -zebra-node-services = { path = "../zebra-node-services", version = "4.0.0" } -zebra-chain = { path = "../zebra-chain", version = "6.0.0" } +zebra-node-services = { path = "../zebra-node-services", version = "8.0.0" } +zebra-chain = { path = "../zebra-chain", version = "10.1.0" } # These crates are needed for the block-template-to-proposal binary -zebra-rpc = { path = "../zebra-rpc", version = "6.0.0" } +zebra-rpc = { path = "../zebra-rpc", version = "10.0.1" } # These crates are needed for the zebra-checkpoints binary itertools = { workspace = true, optional = true } diff --git a/zebra-utils/src/bin/zebra-checkpoints/main.rs b/zebra-utils/src/bin/zebra-checkpoints/main.rs index e4cd5a58f2d..39b090f9f70 100644 --- a/zebra-utils/src/bin/zebra-checkpoints/main.rs +++ b/zebra-utils/src/bin/zebra-checkpoints/main.rs @@ -23,6 +23,7 @@ use structopt::StructOpt; use zebra_chain::{ block::{self, Block, Height, HeightDiff, TryIntoHeight}, + parameters::constants::MAX_BLOCK_REORG_HEIGHT, serialization::ZcashDeserializeInto, transparent::MIN_TRANSPARENT_COINBASE_MATURITY, }; @@ -32,6 +33,14 @@ use zebra_node_services::{ }; use zebra_utils::init_tracing; +// Checkpoints are generated past Zebra's rollback window (`MAX_BLOCK_REORG_HEIGHT`), +// which must be at least the coinbase maturity so that checkpointed coinbase outputs +// are already settled. +const _: () = assert!( + MAX_BLOCK_REORG_HEIGHT >= MIN_TRANSPARENT_COINBASE_MATURITY, + "checkpoint settlement margin must be at least the coinbase maturity", +); + pub mod args; use args::{Args, Backend, Transport}; @@ -162,14 +171,15 @@ async fn main() -> Result<()> { .try_into_height() .expect("height: unexpected invalid value, missing field, or field type"); - // Checkpoints must be on the main chain, so we skip blocks that are within the - // Zcash reorg limit. - let height_limit = height_limit - HeightDiff::from(MIN_TRANSPARENT_COINBASE_MATURITY); + // Checkpoints must be on a settled part of the best chain, so we skip blocks + // within Zebra's rollback window (`MAX_BLOCK_REORG_HEIGHT`). A smaller margin + // could let a reorg that Zebra would still follow orphan a shipped checkpoint. + let height_limit = height_limit - HeightDiff::from(MAX_BLOCK_REORG_HEIGHT); let height_limit = height_limit .ok_or_else(|| { eyre!( "checkpoint generation needs at least {:?} blocks", - MIN_TRANSPARENT_COINBASE_MATURITY + MAX_BLOCK_REORG_HEIGHT ) }) .with_suggestion(|| "Hint: wait for the node to sync more blocks")?; diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 62db7d900ed..53b869e5636 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -1,7 +1,7 @@ [package] # Crate metadata name = "zebrad" -version = "4.2.0" +version = "5.2.0" authors.workspace = true description = "The Zcash Foundation's independent, consensus-compatible implementation of a Zcash node" license.workspace = true @@ -18,7 +18,7 @@ categories = ["command-line-utilities", "cryptography::cryptocurrencies"] edition.workspace = true # Zebra can have a higher MSRV than the other crates since it is a binary. -rust-version = "1.89" +rust-version = "1.91" # Settings that impact runtime behaviour @@ -29,9 +29,7 @@ default-run = "zebrad" # `cargo release` settings [package.metadata.release] pre-release-replacements = [ - {file="../book/src/user/install.md", search="git checkout [a-z0-9\\.-]+", replace="git checkout v{{version}}"}, - {file="../book/src/user/install.md", search="--tag [a-z0-9\\.-]+", replace="--tag v{{version}}"}, - {file="../book/src/user/docker.md", search="--branch [a-z0-9\\.-]+", replace="--branch v{{version}}"}, + {file="../README.md", search="--tag [a-z0-9\\.-]+", replace="--tag v{{version}}"}, ] [package.metadata.docs.rs] @@ -53,7 +51,7 @@ features = [ [features] # In release builds, don't compile debug logging code, to improve performance. -default-release-binaries = ["release_max_level_info", "progress-bar", "prometheus", "sentry"] +default-release-binaries = ["release_max_level_info", "progress-bar", "prometheus", "sentry", "opentelemetry"] # Default features for official ZF binary release builds @@ -150,20 +148,20 @@ tx_v6 = ["zebra-chain/tx_v6", "zebra-state/tx_v6", "zebra-consensus/tx_v6", "zeb comparison-interpreter = ["zebra-script/comparison-interpreter"] [dependencies] -zebra-chain = { path = "../zebra-chain", version = "6.0.0" } -zebra-consensus = { path = "../zebra-consensus", version = "5.0.0" } -zebra-network = { path = "../zebra-network", version = "5.0.0" } -zebra-node-services = { path = "../zebra-node-services", version = "4.0.0", features = ["rpc-client"] } -zebra-rpc = { path = "../zebra-rpc", version = "6.0.0" } -zebra-state = { path = "../zebra-state", version = "5.0.0" } +zebra-chain = { path = "../zebra-chain", version = "10.1.0" } +zebra-consensus = { path = "../zebra-consensus", version = "9.0.1" } +zebra-network = { path = "../zebra-network", version = "9.0.0" } +zebra-node-services = { path = "../zebra-node-services", version = "8.0.0", features = ["rpc-client"] } +zebra-rpc = { path = "../zebra-rpc", version = "10.0.1" } +zebra-state = { path = "../zebra-state", version = "9.0.1" } # zebra-script is not used directly, but we list it here to enable the # "comparison-interpreter" feature. (Feature unification will take care of # enabling it in the other imports of zcash-script.) -zebra-script = { path = "../zebra-script", version = "5.0.0" } +zebra-script = { path = "../zebra-script", version = "9.0.0" } zcash_script = { workspace = true } # Required for crates.io publishing, but it's only used in tests -zebra-utils = { path = "../zebra-utils", version = "5.0.0", optional = true } +zebra-utils = { path = "../zebra-utils", version = "8.0.1", optional = true } abscissa_core = { workspace = true } clap = { workspace = true, features = ["cargo"] } @@ -195,7 +193,6 @@ tracing = { workspace = true } metrics = { workspace = true } dirs = { workspace = true } -atty = { workspace = true } # Health check server dependencies hyper = { workspace = true, features = ["server", "http1"] } @@ -212,7 +209,7 @@ rand = { workspace = true } thread-priority = { workspace = true, optional = true } # prod feature sentry -sentry = { workspace = true, features = ["backtrace", "contexts", "reqwest", "rustls", "tracing"], optional = true } +sentry = { workspace = true, features = ["backtrace", "contexts", "logs", "rustls", "tracing", "ureq"], optional = true } # prod feature flamegraph tracing-flame = { workspace = true, optional = true } @@ -260,6 +257,7 @@ tonic-prost-build = { workspace = true, optional = true } [dev-dependencies] abscissa_core = { workspace = true, features = ["testing"] } +anyhow = { workspace = true } hex = { workspace = true } hex-literal = { workspace = true } jsonrpsee-types = { workspace = true } @@ -269,6 +267,8 @@ insta = { workspace = true, features = ["json"] } bytes = { workspace = true } http-body-util = { workspace = true } hyper-util = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } # zebra-rpc needs the preserve_order feature, it also makes test results more stable serde_json = { workspace = true, features = ["preserve_order"] } @@ -293,10 +293,10 @@ proptest-derive = { workspace = true } # enable span traces and track caller in tests color-eyre = { workspace = true } -zebra-chain = { path = "../zebra-chain", version = "6.0.0", features = ["proptest-impl"] } -zebra-consensus = { path = "../zebra-consensus", version = "5.0.0", features = ["proptest-impl"] } -zebra-network = { path = "../zebra-network", version = "5.0.0", features = ["proptest-impl"] } -zebra-state = { path = "../zebra-state", version = "5.0.0", features = ["proptest-impl"] } +zebra-chain = { path = "../zebra-chain", version = "10.1.0", features = ["proptest-impl"] } +zebra-consensus = { path = "../zebra-consensus", version = "9.0.1", features = ["proptest-impl"] } +zebra-network = { path = "../zebra-network", version = "9.0.0", features = ["proptest-impl"] } +zebra-state = { path = "../zebra-state", version = "9.0.1", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test", version = "3.0.0" } @@ -309,7 +309,7 @@ zebra-test = { path = "../zebra-test", version = "3.0.0" } # When `-Z bindeps` is stabilised, enable this binary dependency instead: # https://github.com/rust-lang/cargo/issues/9096 # zebra-utils { path = "../zebra-utils", artifact = "bin:zebra-checkpoints" } -zebra-utils = { path = "../zebra-utils", version = "5.0.0" } +zebra-utils = { path = "../zebra-utils", version = "8.0.1" } [package.metadata.cargo-udeps.ignore] # These dependencies are false positives - they are actually used diff --git a/zebrad/src/application.rs b/zebrad/src/application.rs index fa7703266fa..bdec5a40a3e 100644 --- a/zebrad/src/application.rs +++ b/zebrad/src/application.rs @@ -342,7 +342,7 @@ impl Application for ZebradApp { let mut metadata_section = "Diagnostic metadata:".to_string(); for (k, v) in panic_metadata { builder = builder.add_issue_metadata(k, v.clone()); - write!(&mut metadata_section, "\n{k}: {}", &v) + write!(&mut metadata_section, "\n{k}: {v}") .expect("unexpected failure writing to string"); } @@ -403,11 +403,7 @@ impl Application for ZebradApp { // environment variable. if env::var_os("SENTRY_DSN").is_some() { #[cfg(feature = "sentry")] - let guard = sentry::init(sentry::ClientOptions { - debug: true, - release: Some(build_version().to_string().into()), - ..Default::default() - }); + let guard = crate::sentry::init(); std::panic::set_hook(Box::new(move |panic_info| { let panic_report = panic_hook.panic_report(panic_info); diff --git a/zebrad/src/commands/start.rs b/zebrad/src/commands/start.rs index de1c550ac23..c76106c7533 100644 --- a/zebrad/src/commands/start.rs +++ b/zebrad/src/commands/start.rs @@ -111,8 +111,47 @@ pub struct StartCmd { filters: Vec, } +/// Warns if Linux TCP slow-start-after-idle is enabled, which significantly +/// reduces single-peer throughput for block propagation. +/// +/// See `book/src/user/troubleshooting.md`. +#[cfg(target_os = "linux")] +fn check_tcp_slow_start_after_idle() { + const PATH: &str = "/proc/sys/net/ipv4/tcp_slow_start_after_idle"; + + let raw = match std::fs::read_to_string(PATH) { + Ok(raw) => raw, + Err(error) => { + debug!( + ?error, + path = PATH, + "could not read TCP sysctl, skipping check" + ); + return; + } + }; + + if raw.trim() == "0" { + return; + } + + warn!( + setting = "net.ipv4.tcp_slow_start_after_idle", + "TCP slow-start-after-idle is enabled, which resets TCP's congestion window \ + between block requests and significantly reduces single-peer throughput for \ + block propagation. \ + Hint: set `net.ipv4.tcp_slow_start_after_idle=0` via sysctl. \ + See https://zebra.zfnd.org/user/troubleshooting.html#linux-tcp-tuning-for-block-propagation" + ); +} + +#[cfg(not(target_os = "linux"))] +fn check_tcp_slow_start_after_idle() {} + impl StartCmd { async fn start(&self) -> Result<(), Report> { + check_tcp_slow_start_after_idle(); + let config = APPLICATION.config(); let is_regtest = config.network.network.is_regtest(); @@ -370,28 +409,29 @@ impl StartCmd { ); info!("spawning syncer task"); - let syncer_task_handle = if is_regtest { - if !syncer + // In regtest, commit the genesis block directly (bypassing the syncer's genesis + // download, which requires a connected peer). Then run the syncer normally so + // that multi-hop block propagation works: gossiped blocks that arrive out of + // order (e.g. only the latest tip hash was gossiped) will be recovered by the + // syncer using block locators within REGTEST_SYNC_RESTART_DELAY (2 seconds). + if is_regtest + && !syncer .state_contains(config.network.network.genesis_hash()) .await? - { - let genesis_hash = block_verifier_router - .clone() - .oneshot(zebra_consensus::Request::Commit(regtest_genesis_block())) - .await - .expect("should validate Regtest genesis block"); - - assert_eq!( - genesis_hash, - config.network.network.genesis_hash(), - "validated block hash should match network genesis hash" - ) - } + { + let genesis_hash = block_verifier_router + .clone() + .oneshot(zebra_consensus::Request::Commit(regtest_genesis_block())) + .await + .expect("should validate Regtest genesis block"); - tokio::spawn(std::future::pending().in_current_span()) - } else { - tokio::spawn(syncer.sync().in_current_span()) - }; + assert_eq!( + genesis_hash, + config.network.network.genesis_hash(), + "validated block hash should match network genesis hash" + ) + } + let syncer_task_handle = tokio::spawn(syncer.sync().in_current_span()); // And finally, spawn the internal Zcash miner, if it is enabled. // diff --git a/zebrad/src/components.rs b/zebrad/src/components.rs index 864c669f678..ab1da50c647 100644 --- a/zebrad/src/components.rs +++ b/zebrad/src/components.rs @@ -22,3 +22,9 @@ pub mod miner; pub use inbound::Inbound; pub use sync::ChainSync; + +/// Consumes, updates, and returns `Self`. +pub trait With { + /// Consumes `self`, updates it, and returns the updated version. + fn with(self, _: T) -> Self; +} diff --git a/zebrad/src/components/inbound.rs b/zebrad/src/components/inbound.rs index 3d91cf76790..465bd9e6ced 100644 --- a/zebrad/src/components/inbound.rs +++ b/zebrad/src/components/inbound.rs @@ -113,6 +113,7 @@ pub struct InboundSetupData { } /// Tracks the internal state of the [`Inbound`] service during setup. +#[allow(clippy::large_enum_variant)] pub enum Setup { /// Waiting for service setup to complete. /// @@ -531,17 +532,28 @@ impl Service for Inbound { .map_ok(|_resp| zn::Response::Nil) .boxed() } - zn::Request::AdvertiseTransactionIds(transactions) => { - let transactions = transactions.into_iter().map(Into::into).collect(); + zn::Request::AdvertiseTransactionIds(transactions, advertiser) => { + // Tag the advertised txids with the announcing peer so the + // mempool downloader can enforce a per-peer queue cap. + // See `GHSA-4fc2-h7jh-287c`. + let request = match advertiser { + Some(peer_addr) => mempool::Request::QueueFromPeer { + txids: transactions, + source: *peer_addr, + }, + None => mempool::Request::Queue( + transactions.into_iter().map(Into::into).collect(), + ), + }; mempool .clone() - .oneshot(mempool::Request::Queue(transactions)) + .oneshot(request) // The response just indicates if processing was queued or not; ignore it .map_ok(|_resp| zn::Response::Nil) .boxed() } - zn::Request::AdvertiseBlock(hash) => { - block_downloads.download_and_verify(hash); + zn::Request::AdvertiseBlock(hash, advertiser) => { + block_downloads.download_and_verify(hash, advertiser); async { Ok(zn::Response::Nil) }.boxed() } // The size of this response is limited by the `Connection` state machine in the network layer diff --git a/zebrad/src/components/inbound/downloads.rs b/zebrad/src/components/inbound/downloads.rs index a57bd3b0067..df8743eb477 100644 --- a/zebrad/src/components/inbound/downloads.rs +++ b/zebrad/src/components/inbound/downloads.rs @@ -1,7 +1,8 @@ //! A download stream that handles gossiped blocks from peers. use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, + net::IpAddr, pin::Pin, task::{Context, Poll}, }; @@ -35,21 +36,17 @@ type BoxError = Box; /// /// ## Security /// -/// We use a small concurrency limit, to prevent memory denial-of-service -/// attacks. -/// /// The maximum block size is 2 million bytes. A deserialized malicious /// block with ~225_000 transparent outputs can take up 9MB of RAM. -/// So the maximum inbound queue usage is `MAX_INBOUND_CONCURRENCY * 9 MB`. +/// The total queue bound is `MAX_INBOUND_CONCURRENCY * 9 MB`. Each peer IP +/// is limited to one in-flight download (9 MB) by the per-IP cap enforced +/// in [`Downloads::download_and_verify`], so a sybil or IPv6-range attacker +/// still needs many distinct source IPs to approach the total bound. /// (See #1880 for more details.) /// /// Malicious blocks will eventually timeout or fail contextual validation. /// Once validation fails, the block is dropped, and its memory is deallocated. -/// -/// Since Zebra keeps an `inv` index, inbound downloads for malicious blocks -/// will be directed to the malicious node that originally gossiped the hash. -/// Therefore, this attack can be carried out by a single malicious node. -pub const MAX_INBOUND_CONCURRENCY: usize = 30; +pub const MAX_INBOUND_CONCURRENCY: usize = 200; /// The action taken in response to a peer's gossiped block hash. pub enum DownloadAction { @@ -66,6 +63,13 @@ pub enum DownloadAction { /// The sync service should discover this block later, when we are closer /// to the tip. The queue's capacity is [`Downloads::full_verify_concurrency_limit`]. FullQueue, + + /// The advertising peer's IP already has an in-flight download, so + /// this request was ignored. Zcash's post-Blossom target block spacing + /// is 75 seconds, so honest peers rarely gossip more than one block + /// before the first is verified; during reorgs or recovery the same + /// hash also arrives from other peers or via the syncer. + TooManyFromPeer, } /// Manages download and verification of blocks gossiped to this peer. @@ -111,9 +115,20 @@ where JoinHandle)>>, >, - /// A list of channels that can be used to cancel pending block download and - /// verify tasks. - cancel_handles: HashMap>, + /// Cancellation handles for tasks in [`Self::pending`], keyed by block + /// hash. The `Option` is the advertiser IP recorded in + /// [`Self::in_flight_ips`], so completion can remove it by hash lookup. + cancel_handles: HashMap, Option)>, + + /// Advertiser IPs with an in-flight download and verify task. + /// + /// Invariant: an IP is present iff some entry in [`Self::cancel_handles`] + /// has value `(_, Some(ip))`. Enforces the one-download-per-IP cap. + /// + /// Size-bounded by `full_verify_concurrency_limit` (≤ [`MAX_INBOUND_CONCURRENCY`]), + /// inherited from the [`DownloadAction::FullQueue`] check on + /// [`Self::pending`]. + in_flight_ips: HashSet, } impl Stream for Downloads @@ -142,16 +157,18 @@ where // // TODO: this would be cleaner with poll_map (#2693) if let Some(join_result) = ready!(this.pending.poll_next(cx)) { - match join_result.expect("block download and verify tasks must not panic") { - Ok(hash) => { - this.cancel_handles.remove(&hash); - Poll::Ready(Some(Ok(hash))) - } - Err((e, hash, advertiser_addr)) => { - this.cancel_handles.remove(&hash); - Poll::Ready(Some(Err((e, advertiser_addr)))) - } + let (result, hash) = + match join_result.expect("block download and verify tasks must not panic") { + Ok(hash) => (Ok(hash), hash), + Err((e, hash, advertiser_addr)) => (Err((e, advertiser_addr)), hash), + }; + if let Some((_, Some(ip))) = this.cancel_handles.remove(&hash) { + assert!( + this.in_flight_ips.remove(&ip), + "every tracked IP was inserted when its download was queued", + ); } + Poll::Ready(Some(result)) } else { Poll::Ready(None) } @@ -199,14 +216,22 @@ where latest_chain_tip, pending: FuturesUnordered::new(), cancel_handles: HashMap::new(), + in_flight_ips: HashSet::new(), } } /// Queue a block for download and verification. /// - /// Returns the action taken in response to the queue request. + /// When `advertiser` is `Some`, its IP is tracked in + /// [`Self::in_flight_ips`] and used to enforce the one-download-per-IP + /// cap; `None` bypasses per-IP accounting (for example when Zebra + /// triggers the download internally). #[instrument(skip(self, hash), fields(hash = %hash))] - pub fn download_and_verify(&mut self, hash: block::Hash) -> DownloadAction { + pub fn download_and_verify( + &mut self, + hash: block::Hash, + advertiser: Option, + ) -> DownloadAction { if self.cancel_handles.contains_key(&hash) { debug!( ?hash, @@ -235,6 +260,21 @@ where return DownloadAction::FullQueue; } + let advertiser_ip = advertiser.map(|addr| addr.ip()); + if let Some(ip) = advertiser_ip { + if self.in_flight_ips.contains(&ip) { + debug!( + ?hash, + ?advertiser, + "already have an in-flight inbound download from peer IP: ignored block", + ); + + metrics::counter!("gossip.peer.limit.dropped.block.hash.count").increment(1); + + return DownloadAction::TooManyFromPeer; + } + } + // This oneshot is used to signal cancellation to the download task. let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>(); @@ -362,8 +402,8 @@ where metrics::counter!("gossip.verified.block.count").increment(1); hash }) - // Tack the hash onto the error so we can remove the cancel handle - // on failure as well as on success. + // Tack the hash onto the error so poll_next can look up the cancel + // handle and advertising IP on failure as well as success. .map_err(move |(e, advertiser_addr)| (e, hash, advertiser_addr)) .in_current_span(); @@ -382,9 +422,17 @@ where self.pending.push(task); assert!( - self.cancel_handles.insert(hash, cancel_tx).is_none(), + self.cancel_handles + .insert(hash, (cancel_tx, advertiser_ip)) + .is_none(), "blocks are only queued once" ); + if let Some(ip) = advertiser_ip { + assert!( + self.in_flight_ips.insert(ip), + "the per-IP cap check above rejects any IP already in flight", + ); + } debug!( ?hash, diff --git a/zebrad/src/components/inbound/tests/fake_peer_set.rs b/zebrad/src/components/inbound/tests/fake_peer_set.rs index 53650a878f0..74d8492e48c 100644 --- a/zebrad/src/components/inbound/tests/fake_peer_set.rs +++ b/zebrad/src/components/inbound/tests/fake_peer_set.rs @@ -171,6 +171,7 @@ async fn mempool_push_transaction() -> Result<(), crate::BoxError> { transaction, Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -206,7 +207,7 @@ async fn mempool_push_transaction() -> Result<(), crate::BoxError> { let mut hs = HashSet::new(); hs.insert(tx.unmined_id()); peer_set - .expect_request(Request::AdvertiseTransactionIds(hs)) + .expect_request(Request::AdvertiseTransactionIds(hs, None)) .await .respond(Response::Nil); @@ -255,7 +256,7 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> { // Test `Request::AdvertiseTransactionIds` let request = inbound_service .clone() - .oneshot(Request::AdvertiseTransactionIds(txs.clone())); + .oneshot(Request::AdvertiseTransactionIds(txs.clone(), None)); // Ensure the mocked peer set responds let peer_set_responder = peer_set @@ -281,6 +282,7 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> { transaction, Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -316,7 +318,7 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> { let mut hs = HashSet::new(); hs.insert(test_transaction.unmined_id()); peer_set - .expect_request(Request::AdvertiseTransactionIds(hs)) + .expect_request(Request::AdvertiseTransactionIds(hs, None)) .await .respond(Response::Nil); @@ -385,6 +387,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { transaction, Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -434,8 +437,8 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { // Transaction and Block IDs are gossipped, in any order, after waiting for the gossip delay tokio::time::sleep(PEER_GOSSIP_DELAY).await; let possible_requests = &mut [ - Request::AdvertiseTransactionIds(hs), - Request::AdvertiseBlock(block_two.hash()), + Request::AdvertiseTransactionIds(hs, None), + Request::AdvertiseBlock(block_two.hash(), None), ] .to_vec(); @@ -503,7 +506,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { // Test the block is gossiped, after waiting for the multi-gossip delay tokio::time::sleep(PEER_GOSSIP_DELAY).await; peer_set - .expect_request(Request::AdvertiseBlock(block_three.hash())) + .expect_request(Request::AdvertiseBlock(block_three.hash(), None)) .await .respond(Response::Nil); @@ -526,6 +529,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { transaction, Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -585,7 +589,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { let mut hs = HashSet::new(); hs.insert(tx2_id); peer_set - .expect_request(Request::AdvertiseTransactionIds(hs)) + .expect_request(Request::AdvertiseTransactionIds(hs, None)) .await .respond(Response::Nil); @@ -613,7 +617,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> { // Test the block is gossiped, after waiting for the multi-gossip delay tokio::time::sleep(PEER_GOSSIP_DELAY).await; peer_set - .expect_request(Request::AdvertiseBlock(block.hash())) + .expect_request(Request::AdvertiseBlock(block.hash(), None)) .await .respond(Response::Nil); @@ -681,7 +685,7 @@ async fn inbound_block_height_lookahead_limit() -> Result<(), crate::BoxError> { // Push test block hash let _request = inbound_service .clone() - .oneshot(Request::AdvertiseBlock(block_hash)) + .oneshot(Request::AdvertiseBlock(block_hash, None)) .await?; // Block is fetched, and committed to the state @@ -717,7 +721,7 @@ async fn inbound_block_height_lookahead_limit() -> Result<(), crate::BoxError> { // Push test block hash let _request = inbound_service .clone() - .oneshot(Request::AdvertiseBlock(block_hash)) + .oneshot(Request::AdvertiseBlock(block_hash, None)) .await?; // Block is fetched, but the downloader drops it because it is too high @@ -1015,7 +1019,7 @@ async fn setup( tokio::time::sleep(PEER_GOSSIP_DELAY).await; peer_set - .expect_request(Request::AdvertiseBlock(block.hash())) + .expect_request(Request::AdvertiseBlock(block.hash(), None)) .await .respond(Response::Nil); } diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index 5ae1bb61baf..da0c804516d 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -576,7 +576,7 @@ impl Service for Mempool { for tx in tx_retries { // This is just an efficiency optimisation, so we don't care if queueing // transaction requests fails. - let _result = tx_downloads.download_if_needed_and_verify(tx, None); + let _result = tx_downloads.download_if_needed_and_verify(tx, None, None); } } @@ -634,8 +634,11 @@ impl Service for Mempool { tracing::trace!("chain grew during tx verification, retrying ..",); // We don't care if re-queueing the transaction request fails. - let _result = tx_downloads - .download_if_needed_and_verify(tx.transaction.into(), rsp_tx); + let _result = tx_downloads.download_if_needed_and_verify( + tx.transaction.into(), + None, + rsp_tx, + ); } } Ok(Err(boxed_err)) => { @@ -660,13 +663,13 @@ impl Service for Mempool { invalidated_ids.insert(tx_id); storage.reject_if_needed(tx_id, error); } - Err(_elapsed) => { - // A timeout happens when the stream hangs waiting for another service, - // so there is no specific transaction ID. + Err((tx_id, _elapsed)) => { + tracing::info!( + ?tx_id, + "mempool transaction failed to verify due to timeout" + ); - // TODO: Return the transaction id that timed out during verification so it can be - // included in the list of invalidated transactions and change `warn!` to `info!`. - tracing::warn!("mempool transaction failed to verify due to timeout"); + invalidated_ids.insert(tx_id); metrics::counter!("mempool.failed.verify.tasks.total", "reason" => "timeout").increment(1); } @@ -872,8 +875,11 @@ impl Service for Mempool { > { let (rsp_tx, rsp_rx) = oneshot::channel(); storage.should_download_or_verify(gossiped_tx.id())?; - tx_downloads - .download_if_needed_and_verify(gossiped_tx, Some(rsp_tx))?; + tx_downloads.download_if_needed_and_verify( + gossiped_tx, + None, + Some(rsp_tx), + )?; Ok(rsp_rx) }, @@ -887,6 +893,27 @@ impl Service for Mempool { async move { Ok(Response::Queued(rsp)) }.boxed() } + // Queue inv-advertised candidates from a specific peer. + // Per-peer accounting is enforced inside the downloader. + Request::QueueFromPeer { txids, source } => { + trace!(req_count = ?txids.len(), ?source, "got mempool QueueFromPeer request"); + + for txid in txids { + if storage.should_download_or_verify(txid).is_err() { + continue; + } + let _ = tx_downloads.download_if_needed_and_verify( + Gossip::Id(txid), + Some(source), + None, + ); + } + + self.update_metrics(); + + async move { Ok(Response::Queued(Vec::new())) }.boxed() + } + // Store successfully downloaded and verified transactions in the mempool Request::CheckForVerifiedTransactions => { trace!(?req, "got mempool request"); @@ -1007,6 +1034,9 @@ impl Service for Mempool { .collect(), ), + // Drop peer-advertised txids when the mempool is disabled. + Request::QueueFromPeer { .. } => Response::Queued(Vec::new()), + // Check if the mempool should be enabled. // This request makes sure mempools are debug-enabled in the acceptance tests. Request::CheckForVerifiedTransactions => { diff --git a/zebrad/src/components/mempool/downloads.rs b/zebrad/src/components/mempool/downloads.rs index 8d75aecd05c..741ceb6aa31 100644 --- a/zebrad/src/components/mempool/downloads.rs +++ b/zebrad/src/components/mempool/downloads.rs @@ -27,6 +27,7 @@ //! [`Mempool::poll_ready`]: super::Mempool::poll_ready use std::{ collections::{HashMap, HashSet}, + net::SocketAddr, pin::Pin, task::{Context, Poll}, time::Duration, @@ -102,7 +103,17 @@ pub(crate) const TRANSACTION_VERIFY_TIMEOUT: Duration = BLOCK_VERIFY_TIMEOUT; /// Therefore, this attack can be carried out by a single malicious node. // // TODO: replace with the configured value of network.peerset_initial_target_size -pub const MAX_INBOUND_CONCURRENCY: usize = 25; +pub const MAX_INBOUND_CONCURRENCY: usize = 500; + +/// The maximum number of concurrent inbound download tasks attributable to a +/// single advertising peer. +/// +/// Caps how many slots of [`MAX_INBOUND_CONCURRENCY`] one peer's `Inv` +/// advertisements can occupy, so a single peer cannot saturate the global +/// queue with fake txids and deny gossip-path mempool admission for honest +/// peers. See `GHSA-4fc2-h7jh-287c`. Crawler-driven and locally-pushed +/// transactions have no source peer and are not counted against the cap. +pub const MAX_INBOUND_CONCURRENCY_PER_PEER: usize = 5; /// A marker struct for the oneshot channels which cancel a pending download and verify. #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -169,14 +180,30 @@ where ), Box<(TransactionDownloadVerifyError, UnminedTxId)>, >, - tokio::time::error::Elapsed, + (UnminedTxId, tokio::time::error::Elapsed), >, >, >, - /// A list of channels that can be used to cancel pending transaction download and - /// verify tasks. Each channel also has the corresponding request. - cancel_handles: HashMap, Gossip)>, + /// A list of channels that can be used to cancel pending transaction + /// download and verify tasks. Each entry also stores the corresponding + /// gossip request and the announcing peer (when known), so completion can + /// release the per-peer slot by `UnminedTxId` lookup. + cancel_handles: HashMap< + UnminedTxId, + ( + oneshot::Sender, + Gossip, + Option, + ), + >, + + /// The number of currently in-flight download tasks per advertising peer. + /// + /// Invariant: a peer is present here iff some entry in [`Self::cancel_handles`] + /// has it as the third tuple element. Enforces + /// [`MAX_INBOUND_CONCURRENCY_PER_PEER`]. See `GHSA-4fc2-h7jh-287c`. + pending_per_peer: HashMap, } impl Stream for Downloads @@ -198,7 +225,7 @@ where ), Box<(UnminedTxId, TransactionDownloadVerifyError)>, >, - tokio::time::error::Elapsed, + (UnminedTxId, tokio::time::error::Elapsed), >; fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { @@ -214,19 +241,34 @@ where // TODO: this would be cleaner with poll_map (#2693) let item = if let Some(join_result) = ready!(this.pending.poll_next(cx)) { let result = join_result.expect("transaction download and verify tasks must not panic"); - let result = match result { + let (result, completed_txid) = match result { Ok(Ok((tx, spent_mempool_outpoints, tip_height, rsp_tx))) => { - this.cancel_handles.remove(&tx.transaction.id); - Ok(Ok((tx, spent_mempool_outpoints, tip_height, rsp_tx))) + let hash = tx.transaction.id; + ( + Ok(Ok((tx, spent_mempool_outpoints, tip_height, rsp_tx))), + Some(hash), + ) } Ok(Err(boxed_err)) => { let (e, hash) = *boxed_err; - this.cancel_handles.remove(&hash); - Ok(Err(Box::new((hash, e)))) + (Ok(Err(Box::new((hash, e)))), Some(hash)) + } + Err((txid, elapsed)) => { + // Remove the cancel handle so the spawned task's queued `Gossip` + // doesn't stay resident in `cancel_handles` after a verification + // timeout. Without this, a peer that gets each transaction to + // hit `RATE_LIMIT_DELAY` can leak ~2 MB per tx until OOM. + this.cancel_handles.remove(&txid); + (Err((txid, elapsed)), None) } - Err(elapsed) => Err(elapsed), }; + if let Some(hash) = completed_txid { + if let Some((_, _gossip, Some(source))) = this.cancel_handles.remove(&hash) { + Self::release_peer_slot(this.pending_per_peer, source); + } + } + Some(result) } else { None @@ -265,17 +307,23 @@ where state, pending: FuturesUnordered::new(), cancel_handles: HashMap::new(), + pending_per_peer: HashMap::new(), } } /// Queue a transaction for download (if needed) and verification. /// /// Returns the action taken in response to the queue request. + /// + /// When `source` is `Some`, the per-peer cap + /// [`MAX_INBOUND_CONCURRENCY_PER_PEER`] is enforced; crawler-driven and + /// locally-pushed transactions pass `None` and are not capped per peer. #[instrument(skip(self, gossiped_tx), fields(txid = %gossiped_tx.id()))] #[allow(clippy::unwrap_in_result)] pub fn download_if_needed_and_verify( &mut self, gossiped_tx: Gossip, + source: Option, mut rsp_tx: Option>>, ) -> Result<(), MempoolError> { let txid = gossiped_tx.id(); @@ -306,6 +354,22 @@ where return Err(MempoolError::FullQueue); } + // Per-peer cap: a single advertising peer cannot saturate the queue + // with attacker-supplied fake txids. See `GHSA-4fc2-h7jh-287c`. + if let Some(source) = source { + let count = self.pending_per_peer.get(&source).copied().unwrap_or(0); + if count >= MAX_INBOUND_CONCURRENCY_PER_PEER { + debug!( + ?txid, + peer_queue_len = count, + ?MAX_INBOUND_CONCURRENCY_PER_PEER, + "too many transactions queued for this peer: ignored transaction" + ); + metrics::counter!("mempool.full_queue.per_peer.total").increment(1); + return Err(MempoolError::FullQueue); + } + } + // This oneshot is used to signal cancellation to the download task. let (cancel_tx, mut cancel_rx) = oneshot::channel::(); @@ -431,6 +495,7 @@ where let _ = rsp_tx.send(Err("timeout waiting for verification result".into())); } }) + .map_err(|elapsed| (txid, elapsed)) .map(|inner_result| { match inner_result { Ok((transaction, spent_mempool_outpoints, tip_height)) => Ok((transaction, spent_mempool_outpoints, tip_height, rsp_tx)), @@ -456,10 +521,15 @@ where self.pending.push(task); assert!( self.cancel_handles - .insert(txid, (cancel_tx, gossiped_tx_req)) + .insert(txid, (cancel_tx, gossiped_tx_req, source)) .is_none(), "transactions are only queued once" ); + if let Some(source) = source { + // The per-peer cap check above ensures this can't exceed + // `MAX_INBOUND_CONCURRENCY_PER_PEER`. + *self.pending_per_peer.entry(source).or_insert(0) += 1; + } debug!( ?txid, @@ -486,8 +556,11 @@ where .collect(); for txid in removed_txids { - if let Some(handle) = self.cancel_handles.remove(&txid) { - let _ = handle.0.send(CancelDownloadAndVerify); + if let Some((cancel_tx, _gossip, source)) = self.cancel_handles.remove(&txid) { + let _ = cancel_tx.send(CancelDownloadAndVerify); + if let Some(source) = source { + Self::release_peer_slot(&mut self.pending_per_peer, source); + } } } } @@ -500,14 +573,26 @@ where // Signal cancellation to all running tasks. // Since we already dropped the JoinHandles above, they should // fail silently. - for (_hash, cancel) in self.cancel_handles.drain() { - let _ = cancel.0.send(CancelDownloadAndVerify); + for (_hash, (cancel_tx, _gossip, _source)) in self.cancel_handles.drain() { + let _ = cancel_tx.send(CancelDownloadAndVerify); } + self.pending_per_peer.clear(); assert!(self.pending.is_empty()); assert!(self.cancel_handles.is_empty()); metrics::gauge!("mempool.currently.queued.transactions",).set(self.pending.len() as f64); } + /// Decrement the per-peer pending count for `source`, removing the entry + /// when it reaches zero. + fn release_peer_slot(pending_per_peer: &mut HashMap, source: SocketAddr) { + if let Some(count) = pending_per_peer.get_mut(&source) { + *count = count.saturating_sub(1); + if *count == 0 { + pending_per_peer.remove(&source); + } + } + } + /// Get the number of currently in-flight download tasks. #[allow(dead_code)] pub fn in_flight(&self) -> usize { @@ -516,7 +601,9 @@ where /// Get a list of the currently pending transaction requests. pub fn transaction_requests(&self) -> impl Iterator { - self.cancel_handles.iter().map(|(_tx_id, (_handle, tx))| tx) + self.cancel_handles + .iter() + .map(|(_tx_id, (_handle, tx, _source))| tx) } /// Check if transaction is already in the best chain. diff --git a/zebrad/src/components/mempool/gossip.rs b/zebrad/src/components/mempool/gossip.rs index 61dc4cb0007..9a047d04475 100644 --- a/zebrad/src/components/mempool/gossip.rs +++ b/zebrad/src/components/mempool/gossip.rs @@ -95,7 +95,8 @@ where } let txs_len = txs.len(); - let request = zn::Request::AdvertiseTransactionIds(txs); + // Zebra is the originator of this advertisement, so no peer source. + let request = zn::Request::AdvertiseTransactionIds(txs, None); info!(%request, changes = %combined_changes, "sending mempool transaction broadcast"); debug!( diff --git a/zebrad/src/components/mempool/storage.rs b/zebrad/src/components/mempool/storage.rs index f72cc0456f9..edfd7e20bb5 100644 --- a/zebrad/src/components/mempool/storage.rs +++ b/zebrad/src/components/mempool/storage.rs @@ -58,7 +58,10 @@ pub(crate) const MAX_EVICTION_MEMORY_ENTRIES: usize = 40_000; #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[allow(dead_code)] pub enum ExactTipRejectionError { + /// Skip this variant in proptest because `TransactionError` is a large enum + /// that causes stack overflow during arbitrary value generation. #[error("transaction did not pass consensus validation: {0}")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] FailedVerification(#[from] zebra_consensus::error::TransactionError), #[error("transaction did not pass standard validation: {0}")] FailedStandard(#[from] NonStandardTransactionError), @@ -300,9 +303,7 @@ impl Storage { // Rule: per-transaction sigops (legacy + P2SH) must not exceed the limit. // zcashd sums GetLegacySigOpCount + GetP2SHSigOpCount for AcceptToMemoryPool: // https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L1819 - let total_sigops = tx - .legacy_sigop_count - .saturating_add(policy::p2sh_sigop_count(transaction, spent_outputs)); + let total_sigops = tx.block_sigop_count(); if total_sigops > policy::MAX_STANDARD_TX_SIGOPS { return self.reject_non_standard(tx, NonStandardTransactionError::TooManySigops); } diff --git a/zebrad/src/components/mempool/storage/policy.rs b/zebrad/src/components/mempool/storage/policy.rs index 1bfab3a5d47..9691e56dc1d 100644 --- a/zebrad/src/components/mempool/storage/policy.rs +++ b/zebrad/src/components/mempool/storage/policy.rs @@ -5,7 +5,6 @@ //! `AreInputsStandard()`. use zcash_script::opcode::PossiblyBad; -use zcash_script::script::Evaluable as _; use zcash_script::{script, solver, Opcode}; use zebra_chain::{transaction::Transaction, transparent}; @@ -85,51 +84,8 @@ fn script_sig_args_expected(kind: &solver::ScriptKind) -> Option { } } -/// Extracts the P2SH redeemed script's sigop count for a single input. -/// -/// Returns `Some(count)` for P2SH inputs where a redeemed script was found, -/// `None` for non-P2SH, coinbase, or P2SH inputs with an empty scriptSig. -fn p2sh_redeemed_script_sigop_count( - input: &transparent::Input, - spent_output: &transparent::Output, -) -> Option { - let unlock_script = match input { - transparent::Input::PrevOut { unlock_script, .. } => unlock_script, - transparent::Input::Coinbase { .. } => return None, - }; - - let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec()); - if !lock_code.is_pay_to_script_hash() { - return None; - } - - let redeemed_bytes = extract_p2sh_redeemed_script(unlock_script)?; - let redeemed = script::Code(redeemed_bytes); - Some(redeemed.sig_op_count(true)) -} - -/// Returns the total number of P2SH sigops across all inputs of the transaction. -/// -/// Mirrors zcashd's `GetP2SHSigOpCount()`: -/// -/// -/// # Correctness -/// -/// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs. -/// If the lengths differ, `zip()` silently truncates the longer iterator, which may -/// cause incorrect sigop counts. -pub(super) fn p2sh_sigop_count(tx: &Transaction, spent_outputs: &[transparent::Output]) -> u32 { - debug_assert_eq!( - tx.inputs().len(), - spent_outputs.len(), - "spent_outputs must align with transaction inputs" - ); - tx.inputs() - .iter() - .zip(spent_outputs.iter()) - .filter_map(|(input, spent_output)| p2sh_redeemed_script_sigop_count(input, spent_output)) - .sum() -} +#[cfg(test)] +pub(super) use zebra_script::p2sh_sigop_count; /// Returns `true` if all transparent inputs are standard. /// diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index 96695611063..0d902d49f2b 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -490,6 +490,7 @@ impl SpendConflictTestInput { // make sure miner fee is big enough for all cases Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -499,6 +500,7 @@ impl SpendConflictTestInput { // make sure miner fee is big enough for all cases Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -527,6 +529,7 @@ impl SpendConflictTestInput { // make sure miner fee is big enough for all cases Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -536,6 +539,7 @@ impl SpendConflictTestInput { // make sure miner fee is big enough for all cases Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) diff --git a/zebrad/src/components/mempool/storage/tests/vectors.rs b/zebrad/src/components/mempool/storage/tests/vectors.rs index a9ebcee590d..f5f47a23684 100644 --- a/zebrad/src/components/mempool/storage/tests/vectors.rs +++ b/zebrad/src/components/mempool/storage/tests/vectors.rs @@ -278,6 +278,7 @@ fn mempool_expired_basic_for_network(network: Network) -> Result<()> { tx.into(), Amount::try_from(1_000_000).expect("valid amount"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) diff --git a/zebrad/src/components/mempool/tests.rs b/zebrad/src/components/mempool/tests.rs index 197c659d85f..a99f3578a72 100644 --- a/zebrad/src/components/mempool/tests.rs +++ b/zebrad/src/components/mempool/tests.rs @@ -114,6 +114,7 @@ pub fn standard_verified_unmined_tx_strategy() -> BoxedStrategy Result<(), Report> { #[tokio::test(flavor = "multi_thread")] async fn mempool_cancel_downloads_after_network_upgrade() -> Result<(), Report> { - let block1: Arc = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + // Use a configured Testnet where a network upgrade activates at height 2. + // + // The mempool resets (and cancels pending downloads) when the chain tip reaches the block + // *before* a network upgrade activation height, because that next height is what the next + // block is verified against (see `ChainTipChange::action`). So committing the block at + // height 1, whose next height (2) is the Overwinter activation height, triggers a + // network-upgrade reset that must cancel all pending transaction downloads. + // + // We can't use Mainnet here: the only activation height reachable with the available block + // vectors is `BeforeOverwinter` (height 1), whose reset fires at the genesis block (height + // 0). That reset is consumed while enabling the mempool, so it can't cancel a later download. + let network = ParametersBuilder::default() + .with_activation_heights(ConfiguredActivationHeights { + before_overwinter: Some(1), + overwinter: Some(2), + sapling: Some(3), + blossom: Some(4), + heartwood: Some(5), + canopy: Some(6), + nu5: Some(7), + nu6: Some(8), + nu6_1: Some(9), + nu6_2: Some(10), + nu7: Some(11), + }) + .expect("activation heights are valid") + .extend_funding_streams() + .to_network() + .expect("configured network is valid"); + + let genesis_block: Arc = zebra_test::vectors::BLOCK_TESTNET_GENESIS_BYTES .zcash_deserialize_into() .unwrap(); - let block2: Arc = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + let block1: Arc = zebra_test::vectors::BLOCK_TESTNET_1_BYTES + .zcash_deserialize_into() + .unwrap(); + let block2: Arc = zebra_test::vectors::BLOCK_TESTNET_2_BYTES .zcash_deserialize_into() .unwrap(); - // Using the mainnet for now - let network = Network::Mainnet; - + // Don't commit the genesis block during setup: we commit it below so we control when the + // mempool first sees a chain tip (it can only be enabled once there is a tip). let ( mut mempool, mut peer_set, @@ -631,13 +666,29 @@ async fn mempool_cancel_downloads_after_network_upgrade() -> Result<(), Report> _tx_verifier, mut recent_syncs, _mempool_transaction_receiver, - ) = setup(&network, u64::MAX, true).await; + ) = setup(&network, u64::MAX, false).await; + + // Commit the genesis block so the mempool can be enabled (it requires a chain tip). + state_service + .ready() + .await + .unwrap() + .call(zebra_state::Request::CommitCheckpointVerifiedBlock( + genesis_block.clone().into(), + )) + .await + .unwrap(); + chain_tip_change + .wait_for_tip_change() + .await + .expect("unexpected chain tip update failure"); // Enable the mempool mempool.enable(&mut recent_syncs).await; assert!(mempool.is_enabled()); - // Queue transaction from block 2 for download + // Queue transaction from block 2 for download. Block 2 is never committed, so the + // transaction is never mined and the download can be retried after the reset. let txid = block2.transactions[0].unmined_id(); let response = mempool .ready() @@ -657,8 +708,8 @@ async fn mempool_cancel_downloads_after_network_upgrade() -> Result<(), Report> // Query the mempool to make it poll chain_tip_change mempool.dummy_call().await; - // Push block 1 to the state. This is considered a network upgrade, - // and thus must cancel all pending transaction downloads. + // Push block 1 to the state. Its next height (2) is the Overwinter activation height, so this + // triggers a network-upgrade reset, which must cancel all pending transaction downloads. state_service .ready() .await @@ -892,6 +943,9 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { let block3: Arc = zebra_test::vectors::BLOCK_MAINNET_3_BYTES .zcash_deserialize_into() .unwrap(); + let block4: Arc = zebra_test::vectors::BLOCK_MAINNET_4_BYTES + .zcash_deserialize_into() + .unwrap(); let ( mut mempool, @@ -907,9 +961,11 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { mempool.enable(&mut recent_syncs).await; assert!(mempool.is_enabled()); - // Queue transaction from block 3 for download - let tx = block3.transactions[0].clone(); - let txid = block3.transactions[0].unmined_id(); + // Queue transaction from block 4 for download. + // Blocks 1-3 are committed to the state below, so the transaction must come from a + // later block, or it would be cancelled as a mined transaction instead of re-verified. + let tx = block4.transactions[0].clone(); + let txid = block4.transactions[0].unmined_id(); let response = mempool .ready() .await @@ -951,6 +1007,7 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { transaction, Amount::try_from(1_000_000).expect("invalid value"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -959,24 +1016,28 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { }) .await; - // Push block 1 to the state. This is considered a network upgrade, - // and must cancel all pending transaction downloads with a `TipAction::Reset`. - state_service - .ready() - .await - .unwrap() - .call(zebra_state::Request::CommitCheckpointVerifiedBlock( - block1.clone().into(), - )) - .await - .unwrap(); + // Push blocks 1 and 2 to the state before the mempool polls its chain tip change + // receiver again. The mempool then sees the tip jump from the genesis block to + // block 2, skipping block 1, which must cancel all pending transaction downloads + // with a `TipAction::Reset`. + for block in [block1.clone(), block2.clone()] { + state_service + .ready() + .await + .unwrap() + .call(zebra_state::Request::CommitCheckpointVerifiedBlock( + block.into(), + )) + .await + .unwrap(); - // Wait for the chain tip update without a timeout - // (skipping the chain tip change here will fail the test) - chain_tip_change - .wait_for_tip_change() - .await - .expect("unexpected chain tip update failure"); + // Wait for the chain tip update without a timeout + // (skipping the chain tip change here will fail the test) + chain_tip_change + .wait_for_tip_change() + .await + .expect("unexpected chain tip update failure"); + } // Query the mempool to make it poll chain_tip_change and try reverifying its state for the `TipAction::Reset` mempool.dummy_call().await; @@ -1013,6 +1074,7 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { transaction, Amount::try_from(1_000_000).expect("invalid value"), 0, + 0, std::sync::Arc::new(vec![]), SigHash([0; 32]), ) @@ -1021,14 +1083,14 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> { }) .await; - // Push block 2 to the state. This will increase the tip height past the expected + // Push block 3 to the state. This will increase the tip height past the expected // tip height that the tx was verified at. state_service .ready() .await .unwrap() .call(zebra_state::Request::CommitCheckpointVerifiedBlock( - block2.clone().into(), + block3.clone().into(), )) .await .unwrap(); @@ -1889,3 +1951,87 @@ async fn setup_with_mempool_config( mempool_transaction_subscriber.subscribe(), ) } + +/// Regression test for GHSA-65jj-fmw8-468q. +/// +/// Before the fix, when the outer `tokio::time::timeout(RATE_LIMIT_DELAY, ..)` +/// wrapping mempool tx verification fired, `Downloads::poll_next` propagated +/// the `Elapsed` error without dropping the corresponding `cancel_handles` +/// entry. Each retained entry held a full `Gossip::Tx(UnminedTx)`, was never +/// garbage-collected (since `cancel(mined_ids)` only matches mined txids), +/// and the structure grew without bound under sustained adversarial input. +#[tokio::test(flavor = "current_thread", start_paused = true)] +async fn cancel_handles_drained_after_verification_timeout() { + use futures::stream::StreamExt; + use tower::timeout::Timeout; + use zebra_node_services::mempool::Gossip; + + use crate::components::mempool::{ + crawler::RATE_LIMIT_DELAY, + downloads::{Downloads, TRANSACTION_DOWNLOAD_TIMEOUT, TRANSACTION_VERIFY_TIMEOUT}, + }; + + let _init_guard = zebra_test::init(); + + let peer_set: MockPeerSet = MockService::build().for_unit_tests(); + let state: MockService = + MockService::build().for_unit_tests(); + let tx_verifier: MockTxVerifier = MockService::build().for_unit_tests(); + + let mut downloads = Box::pin(Downloads::new( + Timeout::new(peer_set, TRANSACTION_DOWNLOAD_TIMEOUT), + Timeout::new(tx_verifier, TRANSACTION_VERIFY_TIMEOUT), + state, + )); + + let mut iter = Network::Mainnet.unmined_transactions_in_blocks(1..=10); + let v1 = iter.next().expect("vector tx 1").transaction; + let v2 = iter.next().expect("vector tx 2").transaction; + let v3 = iter.next().expect("vector tx 3").transaction; + + downloads + .as_mut() + .download_if_needed_and_verify(Gossip::Tx(v1), None, None) + .expect("queue tx 1"); + downloads + .as_mut() + .download_if_needed_and_verify(Gossip::Tx(v2), None, None) + .expect("queue tx 2"); + downloads + .as_mut() + .download_if_needed_and_verify(Gossip::Tx(v3), None, None) + .expect("queue tx 3"); + + assert_eq!(downloads.in_flight(), 3); + assert_eq!(downloads.transaction_requests().count(), 3); + + // Advance past the RATE_LIMIT_DELAY so every spawned task hits the + // outer timeout. We don't service the mocked state/network/verifier, + // so verification never makes progress and the timeout deterministically + // fires. + time::advance(RATE_LIMIT_DELAY + Duration::from_secs(5)).await; + tokio::task::yield_now().await; + + let mut elapsed_count = 0usize; + for _ in 0..3 { + match downloads.as_mut().next().await { + Some(Err(_)) => elapsed_count += 1, + Some(Ok(other)) => panic!( + "expected Err(Elapsed); got Ok with inner is_ok={}", + other.is_ok() + ), + None => panic!("Downloads stream ended before all tasks resolved"), + } + } + assert_eq!( + elapsed_count, 3, + "all 3 tasks should have hit RATE_LIMIT_DELAY" + ); + assert_eq!(downloads.in_flight(), 0, "pending should be drained"); + + let leaked = downloads.transaction_requests().count(); + assert_eq!( + leaked, 0, + "regression GHSA-65jj-fmw8-468q: cancel_handles must be drained after timeout" + ); +} diff --git a/zebrad/src/components/sync.rs b/zebrad/src/components/sync.rs index 2d62f0713f5..f08940c2c7e 100644 --- a/zebrad/src/components/sync.rs +++ b/zebrad/src/components/sync.rs @@ -2,7 +2,14 @@ //! //! It is used when Zebra is a long way behind the current chain tip. -use std::{cmp::max, collections::HashSet, convert, pin::Pin, task::Poll, time::Duration}; +use std::{ + cmp::max, + collections::{HashMap, HashSet}, + convert, + pin::Pin, + task::Poll, + time::Duration, +}; use color_eyre::eyre::{eyre, Report}; use futures::stream::{FuturesUnordered, StreamExt}; @@ -60,6 +67,17 @@ const FANOUT: usize = 3; /// retries may be concurrent, inner retries are sequential. const BLOCK_DOWNLOAD_RETRY_LIMIT: usize = 3; +/// Controls how many times the syncer will re-request a block whose download +/// failed because no peer delivered it (a `NotFound`), before giving up and +/// letting the normal tip re-walk handle it. +/// +/// Without this re-request, a single missing block at the checkpoint frontier +/// is dropped and never re-fetched, wedging the whole verify pipeline until the +/// 8-minute `BLOCK_VERIFY_TIMEOUT` fires (#5709). Each attempt already goes +/// through the tower-level `BLOCK_DOWNLOAD_RETRY_LIMIT` (and hedging), so this +/// is a coarse, hash-scoped retry on top of an exhausted per-request retry. +const MAX_BLOCK_REOBTAIN_RETRIES: u8 = 3; + /// A lower bound on the user-specified checkpoint verification concurrency limit. /// /// Set to the maximum checkpoint interval, so the pipeline holds around a checkpoint's @@ -205,6 +223,11 @@ const FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT_LIMIT: HeightDiff = 100; /// previous sync runs. const SYNC_RESTART_DELAY: Duration = Duration::from_secs(67); +/// In regtest, use a much shorter restart delay so that downstream nodes pick up +/// newly-mined blocks quickly (e.g. after `generate(N)` in integration tests). +/// The default 67-second delay exceeds the typical `sync_all` timeout of 60 seconds. +const REGTEST_SYNC_RESTART_DELAY: Duration = Duration::from_secs(2); + /// Controls how long we wait to retry a failed attempt to download /// and verify the genesis block. /// @@ -342,6 +365,9 @@ where /// The configured full verification concurrency limit, after applying the minimum limit. full_verify_concurrency_limit: usize, + /// Whether the node is running on regtest. Used to apply a shorter sync restart delay. + is_regtest: bool, + // Services // /// A network service which is used to perform ObtainTips and ExtendTips @@ -382,6 +408,14 @@ where /// Sender for reporting peer addresses that advertised unexpectedly invalid transactions. misbehavior_sender: mpsc::Sender<(PeerSocketAddr, u32)>, + + /// Blocks whose download failed with `NotFound` and should be re-requested on + /// the next sync round, instead of being silently dropped (#5709). + reobtain_hashes: IndexSet, + + /// Per-hash count of how many times a `NotFound` block has been re-requested, + /// bounded by [`MAX_BLOCK_REOBTAIN_RETRIES`]. + block_reobtain_retries: HashMap, } /// Polls the network to determine whether further blocks are available and @@ -487,7 +521,7 @@ where // We apply a timeout to the verifier to avoid hangs due to missing earlier blocks. let verifier = Timeout::new(verifier, BLOCK_VERIFY_TIMEOUT); - let (sync_status, recent_syncs) = SyncStatus::new(); + let (sync_status, recent_syncs) = SyncStatus::new_for_network(&config.network.network); let (past_lookahead_limit_sender, past_lookahead_limit_receiver) = watch::channel(false); let past_lookahead_limit_receiver = zs::WatchReceiver::new(past_lookahead_limit_receiver); @@ -509,6 +543,7 @@ where max_checkpoint_height, checkpoint_verify_concurrency_limit, full_verify_concurrency_limit, + is_regtest: config.network.network.is_regtest(), tip_network, downloads, state, @@ -517,6 +552,8 @@ where recent_syncs, past_lookahead_limit_receiver, misbehavior_sender, + reobtain_hashes: IndexSet::new(), + block_reobtain_retries: HashMap::new(), }; (new_syncer, sync_status) @@ -536,12 +573,17 @@ where self.update_metrics(); + let restart_delay = if self.is_regtest { + REGTEST_SYNC_RESTART_DELAY + } else { + SYNC_RESTART_DELAY + }; info!( - timeout = ?SYNC_RESTART_DELAY, + timeout = ?restart_delay, state_tip = ?self.latest_chain_tip.best_tip_height(), "waiting to restart sync" ); - sleep(SYNC_RESTART_DELAY).await; + sleep(restart_delay).await; } } @@ -561,6 +603,9 @@ where async fn try_to_sync(&mut self) -> Result<(), Report> { self.prospective_tips = HashSet::new(); + self.reobtain_hashes.clear(); + self.block_reobtain_retries.clear(); + info!( state_tip = ?self.latest_chain_tip.best_tip_height(), "starting sync, obtaining new tips" @@ -609,6 +654,9 @@ where // the syncer will reset itself. self.handle_block_response(rsp)?; } + // Re-request any blocks that just failed with `NotFound`, before pausing + // on the lookahead limit (#5709). + self.reobtain_missing_blocks().await; self.update_metrics(); // Pause new downloads while the syncer or downloader are past their lookahead limits. @@ -631,6 +679,10 @@ where let response = self.downloads.next().await.expect("downloads is nonempty"); self.handle_block_response(response)?; + // A block that just failed with `NotFound` is what unblocks the + // verifier, so re-request it now rather than waiting for the pause + // loop to clear — which it cannot until this block arrives (#5709). + self.reobtain_missing_blocks().await; self.update_metrics(); } @@ -667,6 +719,28 @@ where Ok(extra_hashes) } + /// Re-issues downloads for blocks that failed with `NotFound` (#5709). + /// + /// These are re-requested even while the download pipeline is past its + /// lookahead limit, because a missing low block is exactly what stops the + /// checkpoint verifier from advancing. Waiting for the lookahead pause to + /// clear would deadlock — the pause cannot clear until this block arrives. + /// The per-hash retry count is bounded by [`MAX_BLOCK_REOBTAIN_RETRIES`]. + async fn reobtain_missing_blocks(&mut self) { + if self.reobtain_hashes.is_empty() { + return; + } + + for hash in std::mem::take(&mut self.reobtain_hashes) { + // The block was removed from the in-flight set when its download + // failed, so this re-queues it. A residual duplicate/queue error is + // benign — it means the block is already being handled. + if let Err(error) = self.downloads.download_and_verify(hash).await { + trace!(?hash, ?error, "re-download of missing block not queued"); + } + } + } + /// Given a block_locator list fan out request for subsequent hashes to /// multiple peers #[instrument(skip(self))] @@ -739,13 +813,28 @@ where // out-of-order first hashes. // We use the last hash for the tip, and we want to avoid bad - // tips. So we discard the last hash. (We don't need to worry - // about missed downloads, because we will pick them up again - // in ExtendTips.) - let hashes = match hashes.as_slice() { - [] => continue, - [rest @ .., _last] => rest, + // tips from zcashd's quirk of appending an unrelated hash. + // So we discard the last hash on mainnet/testnet. + // (We don't need to worry about missed downloads, because we + // will pick them up again in ExtendTips.) + // + // In regtest we only connect to Zebra nodes, not zcashd, + // so we trust all hashes in the response and keep them all. + // This is necessary when there are only a small number of + // blocks to sync (e.g. 2 new blocks), where stripping the + // last hash leaves only 1 unknown hash and rchunks_exact(2) + // would discard the entire response. + let hashes = if self.is_regtest { + hashes.as_slice() + } else { + match hashes.as_slice() { + [] => continue, + [rest @ .., _last] => rest, + } }; + if hashes.is_empty() { + continue; + } let mut first_unknown = None; for (i, &hash) in hashes.iter().enumerate() { @@ -1058,8 +1147,18 @@ where IndexSet::new() }; + // Dispatch blocks with duplicate-tolerant error handling. + // DuplicateBlockQueuedForDownload is caught and skipped instead of + // propagating — this prevents dropping unprocessed hashes from the + // batch, which would create frontier gaps and stalls (#5709). for hash in hashes.into_iter() { - self.downloads.download_and_verify(hash).await?; + match self.downloads.download_and_verify(hash).await { + Ok(()) => {} + Err(BlockDownloadVerifyError::DuplicateBlockQueuedForDownload { .. }) => { + debug!("block request was already queued, continuing"); + } + Err(error) => return Err(error), + } } Ok(extra_hashes) @@ -1108,6 +1207,9 @@ where Ok((height, hash)) => { trace!(?height, ?hash, "verified and committed block to state"); + // The block arrived, so forget any re-request bookkeeping for it. + self.block_reobtain_retries.remove(&hash); + return Ok(()); } @@ -1121,9 +1223,50 @@ where .try_send((advertiser_addr, error.misbehavior_score())); } + Err(BlockDownloadVerifyError::AboveLookaheadHeightLimit { + advertiser_addr: Some(advertiser_addr), + .. + }) => { + let _ = self.misbehavior_sender.try_send((advertiser_addr, 100)); + } + + Err(BlockDownloadVerifyError::InvalidHeight { + advertiser_addr: Some(advertiser_addr), + .. + }) => { + let _ = self.misbehavior_sender.try_send((advertiser_addr, 100)); + } + Err(_) => {} }; + // A block whose download failed because no peer delivered it (`NotFound`) + // is otherwise dropped here and never re-requested, which wedges the + // checkpoint frontier until the verify timeout (#5709). Re-queue it for + // the next sync round, bounded by `MAX_BLOCK_REOBTAIN_RETRIES`. Consensus + // failures (`Invalid`/`ValidationRequestError`) are deliberately excluded — + // re-downloading a block the network already rejected is pointless. + if let Err(BlockDownloadVerifyError::DownloadFailed { error, hash }) = &response { + if format!("{error:?}").contains("NotFound") { + let attempts = self.block_reobtain_retries.entry(*hash).or_insert(0); + if *attempts < MAX_BLOCK_REOBTAIN_RETRIES { + *attempts += 1; + self.reobtain_hashes.insert(*hash); + debug!( + ?hash, + attempts = *attempts, + "re-queueing missing block for re-download" + ); + } else { + debug!( + ?hash, + "missing block exceeded re-download retries, dropping" + ); + self.block_reobtain_retries.remove(hash); + } + } + } + Self::handle_response(response) } @@ -1209,6 +1352,22 @@ where ); false } + BlockDownloadVerifyError::AboveLookaheadHeightLimit { .. } => { + debug!( + error = ?e, + "block height is above the lookahead limit, \ + dropping the block and continuing sync" + ); + false + } + BlockDownloadVerifyError::InvalidHeight { .. } => { + debug!( + error = ?e, + "block has no valid height, \ + dropping the block and continuing sync" + ); + false + } BlockDownloadVerifyError::DuplicateBlockQueuedForDownload { .. } => { debug!( error = ?e, diff --git a/zebrad/src/components/sync/downloads.rs b/zebrad/src/components/sync/downloads.rs index 24fd9fd4602..71104322e62 100644 --- a/zebrad/src/components/sync/downloads.rs +++ b/zebrad/src/components/sync/downloads.rs @@ -108,6 +108,7 @@ pub enum BlockDownloadVerifyError { AboveLookaheadHeightLimit { height: block::Height, hash: block::Hash, + advertiser_addr: Option, }, #[error("downloaded block was too far behind the chain tip: {height:?} {hash:?}")] @@ -117,7 +118,10 @@ pub enum BlockDownloadVerifyError { }, #[error("downloaded block had an invalid height: {hash:?}")] - InvalidHeight { hash: block::Hash }, + InvalidHeight { + hash: block::Hash, + advertiser_addr: Option, + }, #[error("block failed consensus validation: {error:?} {height:?} {hash:?}")] Invalid { @@ -448,11 +452,11 @@ where ); metrics::counter!("sync.no.height.dropped.block.count").increment(1); - return Err(BlockDownloadVerifyError::InvalidHeight { hash }); + return Err(BlockDownloadVerifyError::InvalidHeight { hash, advertiser_addr }); }; if block_height > lookahead_drop_height { - Err(BlockDownloadVerifyError::AboveLookaheadHeightLimit { height: block_height, hash })?; + Err(BlockDownloadVerifyError::AboveLookaheadHeightLimit { height: block_height, hash, advertiser_addr })?; } else if block_height > lookahead_pause_height { // This log can be very verbose, usually hundreds of blocks are dropped. // So we only log at info level for the first above-height block. diff --git a/zebrad/src/components/sync/end_of_support.rs b/zebrad/src/components/sync/end_of_support.rs index cba4aa33eb5..05622802dae 100644 --- a/zebrad/src/components/sync/end_of_support.rs +++ b/zebrad/src/components/sync/end_of_support.rs @@ -13,7 +13,7 @@ use zebra_chain::{ use crate::application::release_version; /// The estimated height that this release will be published. -pub const ESTIMATED_RELEASE_HEIGHT: u32 = 3_271_000; +pub const ESTIMATED_RELEASE_HEIGHT: u32 = 3_382_189; /// The maximum number of days after `ESTIMATED_RELEASE_HEIGHT` where a Zebra server will run /// without halting. @@ -22,8 +22,8 @@ pub const ESTIMATED_RELEASE_HEIGHT: u32 = 3_271_000; /// /// - Zebra will exit with a panic if the current tip height is bigger than the /// `ESTIMATED_RELEASE_HEIGHT` plus this number of days. -/// - Currently set to 15 weeks. -pub const EOS_PANIC_AFTER: u32 = 105; +/// - Reduced to 37 days for the NU7 network upgrade expected at end of July 2026. +pub const EOS_PANIC_AFTER: u32 = 37; /// The number of days before the end of support where Zebra will display warnings. pub const EOS_WARN_AFTER: u32 = EOS_PANIC_AFTER - 14; diff --git a/zebrad/src/components/sync/gossip.rs b/zebrad/src/components/sync/gossip.rs index 8b5dd255189..7ad7017b20b 100644 --- a/zebrad/src/components/sync/gossip.rs +++ b/zebrad/src/components/sync/gossip.rs @@ -131,7 +131,7 @@ where let request = if is_block_submission { zn::Request::AdvertiseBlockToAll(hash) } else { - zn::Request::AdvertiseBlock(hash) + zn::Request::AdvertiseBlock(hash, None) }; info!(?height, ?request, log_msg); diff --git a/zebrad/src/components/sync/status.rs b/zebrad/src/components/sync/status.rs index 73cc112a3df..5ea4e7a2cf9 100644 --- a/zebrad/src/components/sync/status.rs +++ b/zebrad/src/components/sync/status.rs @@ -16,6 +16,7 @@ mod tests; #[derive(Clone, Debug)] pub struct SyncStatus { latest_sync_length: watch::Receiver>, + is_regtest: bool, } impl SyncStatus { @@ -26,13 +27,32 @@ impl SyncStatus { /// once Zebra reaches the tip. const MIN_DIST_FROM_TIP: usize = 20; + /// Create an instance of [`SyncStatus`] for a specific network. + /// + /// The status is determined based on the latest counts of synchronized blocks, observed + /// through `latest_sync_length`. In regtest, [`ChainSyncStatus::is_close_to_tip`] always returns `true`. + pub fn new_for_network( + network: &zebra_chain::parameters::Network, + ) -> (Self, RecentSyncLengths) { + let (recent_sync_lengths, latest_sync_length) = RecentSyncLengths::new(); + let status = SyncStatus { + latest_sync_length, + is_regtest: network.is_regtest(), + }; + + (status, recent_sync_lengths) + } + /// Create an instance of [`SyncStatus`]. /// /// The status is determined based on the latest counts of synchronized blocks, observed /// through `latest_sync_length`. pub fn new() -> (Self, RecentSyncLengths) { let (recent_sync_lengths, latest_sync_length) = RecentSyncLengths::new(); - let status = SyncStatus { latest_sync_length }; + let status = SyncStatus { + latest_sync_length, + is_regtest: false, + }; (status, recent_sync_lengths) } @@ -52,6 +72,10 @@ impl SyncStatus { impl ChainSyncStatus for SyncStatus { /// Check if the synchronization is likely close to the chain tip. fn is_close_to_tip(&self) -> bool { + if self.is_regtest { + return true; + } + let sync_lengths = self.latest_sync_length.borrow(); // Return early if sync_lengths is empty. diff --git a/zebrad/src/components/sync/tests/vectors.rs b/zebrad/src/components/sync/tests/vectors.rs index c9d8d535d1f..355caf7170a 100644 --- a/zebrad/src/components/sync/tests/vectors.rs +++ b/zebrad/src/components/sync/tests/vectors.rs @@ -13,7 +13,7 @@ use zebra_chain::{ serialization::ZcashDeserializeInto, }; use zebra_consensus::{Config as ConsensusConfig, RouterError, VerifyBlockError}; -use zebra_network::InventoryResponse; +use zebra_network::{InventoryResponse, PeerSocketAddr}; use zebra_state::Config as StateConfig; use zebra_test::mock_service::{MockService, PanicAssertion}; @@ -1020,6 +1020,127 @@ async fn should_restart_sync_returns_false() { ); } +/// Verifies fix for GHSA-gvjc-3w7c-92jx: `AboveLookaheadHeightLimit` now has +/// an explicit match arm in `should_restart_sync` that returns `false`. +#[tokio::test] +async fn above_lookahead_does_not_restart_sync() { + let err = BlockDownloadVerifyError::AboveLookaheadHeightLimit { + height: block::Height(60_000), + hash: block::Hash::from([0xBB; 32]), + advertiser_addr: None, + }; + + let restart = ChainSync::< + MockService, + MockService, + MockService, + MockChainTip, + >::should_restart_sync(&err); + + assert!( + !restart, + "AboveLookaheadHeightLimit should NOT trigger sync restart (GHSA-gvjc-3w7c-92jx fix)" + ); +} + +/// Verifies fix for GHSA-gvjc-3w7c-92jx: `AboveLookaheadHeightLimit` now +/// carries `advertiser_addr` so the offending peer can be scored. +#[tokio::test] +async fn above_lookahead_has_peer_attribution() { + let addr: PeerSocketAddr = "127.0.0.1:8233".parse().unwrap(); + let err = BlockDownloadVerifyError::AboveLookaheadHeightLimit { + height: block::Height(60_000), + hash: block::Hash::from([0xCC; 32]), + advertiser_addr: Some(addr), + }; + + let has_addr = match &err { + BlockDownloadVerifyError::AboveLookaheadHeightLimit { + advertiser_addr, .. + } => advertiser_addr.is_some(), + _ => false, + }; + + assert!( + has_addr, + "AboveLookaheadHeightLimit should carry advertiser_addr for peer scoring \ + (GHSA-gvjc-3w7c-92jx fix)" + ); +} + +/// Verifies fix for GHSA-gvjc-3w7c-92jx: both height-limit errors now +/// return `false` from `should_restart_sync` — symmetric handling. +#[tokio::test] +async fn both_height_limits_do_not_restart_sync() { + let below = BlockDownloadVerifyError::BehindTipHeightLimit { + height: block::Height(1), + hash: block::Hash::from([0xDD; 32]), + }; + + let above = BlockDownloadVerifyError::AboveLookaheadHeightLimit { + height: block::Height(60_000), + hash: block::Hash::from([0xEE; 32]), + advertiser_addr: None, + }; + + let restart_below = ChainSync::< + MockService, + MockService, + MockService, + MockChainTip, + >::should_restart_sync(&below); + + let restart_above = ChainSync::< + MockService, + MockService, + MockService, + MockChainTip, + >::should_restart_sync(&above); + + assert!( + !restart_below, + "BehindTipHeightLimit should NOT restart sync" + ); + assert!( + !restart_above, + "AboveLookaheadHeightLimit should NOT restart sync (GHSA-gvjc-3w7c-92jx fix)" + ); +} + +/// Verifies fix for GHSA-rj6c-83wx-jxf2: `InvalidHeight` does not trigger +/// sync restart and carries `advertiser_addr` for peer scoring. +#[tokio::test] +async fn invalid_height_does_not_restart_sync() { + let addr: PeerSocketAddr = "127.0.0.1:8233".parse().unwrap(); + let err = BlockDownloadVerifyError::InvalidHeight { + hash: block::Hash::from([0xFF; 32]), + advertiser_addr: Some(addr), + }; + + let restart = ChainSync::< + MockService, + MockService, + MockService, + MockChainTip, + >::should_restart_sync(&err); + + assert!( + !restart, + "InvalidHeight should NOT trigger sync restart (GHSA-rj6c-83wx-jxf2 fix)" + ); + + let has_addr = match &err { + BlockDownloadVerifyError::InvalidHeight { + advertiser_addr, .. + } => advertiser_addr.is_some(), + _ => false, + }; + assert!( + has_addr, + "InvalidHeight should carry advertiser_addr for peer scoring" + ); +} + fn setup() -> ( // ChainSync impl Future> + Send, diff --git a/zebrad/src/components/tracing.rs b/zebrad/src/components/tracing.rs index c6657e146bc..2e480f50447 100644 --- a/zebrad/src/components/tracing.rs +++ b/zebrad/src/components/tracing.rs @@ -1,6 +1,7 @@ //! Tracing and logging infrastructure for Zebra. use std::{ + io::IsTerminal, net::SocketAddr, ops::{Deref, DerefMut}, path::PathBuf, @@ -245,20 +246,22 @@ impl Config { /// Returns `true` if standard output should use color escapes. /// Automatically checks if Zebra is running in a terminal. pub fn use_color_stdout(&self) -> bool { - self.force_use_color || (self.use_color && atty::is(atty::Stream::Stdout)) + self.force_use_color || (self.use_color && std::io::stdout().is_terminal()) } /// Returns `true` if standard error should use color escapes. /// Automatically checks if Zebra is running in a terminal. pub fn use_color_stderr(&self) -> bool { - self.force_use_color || (self.use_color && atty::is(atty::Stream::Stderr)) + self.force_use_color || (self.use_color && std::io::stderr().is_terminal()) } /// Returns `true` if output that could go to standard output or standard error /// should use color escapes. Automatically checks if Zebra is running in a terminal. pub fn use_color_stdout_and_stderr(&self) -> bool { self.force_use_color - || (self.use_color && atty::is(atty::Stream::Stdout) && atty::is(atty::Stream::Stderr)) + || (self.use_color + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) } } diff --git a/zebrad/src/components/tracing/component.rs b/zebrad/src/components/tracing/component.rs index 009f5abcba8..92ee1cfd51f 100644 --- a/zebrad/src/components/tracing/component.rs +++ b/zebrad/src/components/tracing/component.rs @@ -269,7 +269,7 @@ impl Tracing { let subscriber = subscriber.with(journaldlayer); #[cfg(feature = "sentry")] - let subscriber = subscriber.with(sentry::integrations::tracing::layer()); + let subscriber = subscriber.with(crate::sentry::tracing_layer()); // OpenTelemetry layer - zero overhead when config.opentelemetry_endpoint is None #[cfg(feature = "opentelemetry")] diff --git a/zebrad/src/config.rs b/zebrad/src/config.rs index 01505caa3df..801e32977dd 100644 --- a/zebrad/src/config.rs +++ b/zebrad/src/config.rs @@ -7,6 +7,9 @@ use std::{collections::HashMap, path::PathBuf}; use serde::{Deserialize, Serialize}; +use zebra_rpc::config::mining::{default_miner_address, MinerAddressType}; + +use crate::components::With; /// Centralized, case-insensitive suffix-based deny-list to ban setting config fields with /// environment variables if those config field names end with any of these suffixes. @@ -178,3 +181,15 @@ impl ZebradConfig { config.try_deserialize() } } + +impl With for ZebradConfig { + fn with(mut self, miner_address_type: MinerAddressType) -> Self { + self.mining.miner_address = Some( + default_miner_address(self.network.network.kind(), &miner_address_type) + .parse() + .expect("valid hard-coded address"), + ); + + self + } +} diff --git a/zebrad/src/lib.rs b/zebrad/src/lib.rs index e18358f8955..fe7efa59bcf 100644 --- a/zebrad/src/lib.rs +++ b/zebrad/src/lib.rs @@ -151,4 +151,4 @@ pub mod config; pub mod prelude; #[cfg(feature = "sentry")] -pub mod sentry; +pub(crate) mod sentry; diff --git a/zebrad/src/sentry.rs b/zebrad/src/sentry.rs index da97ee259a9..15b4b41de45 100644 --- a/zebrad/src/sentry.rs +++ b/zebrad/src/sentry.rs @@ -1,15 +1,148 @@ //! Integration with sentry.io for event reporting. -//! -//! Currently handles panic reports. -#[allow(unused_imports)] +use std::{collections::BTreeMap, env, sync::Arc}; + use sentry::{ - integrations::backtrace::current_stacktrace, - protocol::{Event, Exception, Mechanism}, + integrations::tracing::EventFilter, + protocol::{Context, Event, Exception, Log, LogAttribute, Map, Mechanism, Value}, + ClientInitGuard, ClientOptions, Scope, }; +use tracing::Level; + +use crate::application::{build_version, ZebradApp}; + +/// Environment and CI metadata attached to all Sentry events. +#[derive(Debug, Default)] +struct Metadata { + environment: Option, + tags: BTreeMap<&'static str, String>, + ci_context: BTreeMap<&'static str, String>, +} + +impl Metadata { + fn from_env() -> Self { + Self::from_lookup(env_var) + } + + fn from_lookup(lookup: F) -> Self + where + F: Fn(&str) -> Option, + { + let mut metadata = Self { + environment: lookup_value(&lookup, "SENTRY_ENVIRONMENT"), + ..Default::default() + }; + + insert_lookup_values( + &lookup, + &mut metadata.ci_context, + &[ + ("GITHUB_RUN_ID", "run_id"), + ("GITHUB_RUN_ATTEMPT", "run_attempt"), + ("GITHUB_WORKFLOW", "workflow"), + ("GITHUB_JOB", "job"), + ], + ); + + insert_lookup_values( + &lookup, + &mut metadata.tags, + &[ + ("GITHUB_EVENT_NAME", "deploy.trigger"), + ("CI_PR_NUMBER", "pr.number"), + ("CI_TEST_ID", "test.id"), + ], + ); + + if let Some(git_ref) = slugged_git_ref(&lookup) { + metadata.tags.insert("git.ref", git_ref); + } + + let git_sha = lookup_value(&lookup, "GITHUB_SHA") + .or_else(|| ZebradApp::git_commit().map(ToOwned::to_owned)); + if let Some(git_sha) = git_sha { + metadata.tags.insert("git.sha", git_sha); + } + + if lookup_value(&lookup, "GITHUB_ACTIONS").is_some_and(|v| v.eq_ignore_ascii_case("true")) { + metadata + .tags + .insert("ci.provider", "github-actions".to_owned()); + } + + metadata + } + + fn client_options(&self) -> ClientOptions { + self.client_options_with_release(release_name()) + } + + fn client_options_with_release(&self, release: String) -> ClientOptions { + let log_attributes = self.log_attributes(); + ClientOptions { + release: Some(release.into()), + environment: self.environment.clone().map(Into::into), + enable_logs: true, + before_send_log: Some(Arc::new(move |mut log: Log| { + merge_log_attributes(&mut log, &log_attributes); + Some(log) + })), + ..Default::default() + } + } + + /// Build the static CI/git attributes to attach to every outgoing [`Log`]. + /// + /// `Scope::set_tag` only propagates to [`Event`]s, so Logs need their own + /// enrichment path. CI context is namespaced under `ci.*` to mirror the + /// grouping used for Event contexts. + fn log_attributes(&self) -> Vec<(String, LogAttribute)> { + let mut attrs = Vec::with_capacity(self.tags.len() + self.ci_context.len()); + for (key, value) in &self.tags { + attrs.push(((*key).to_owned(), LogAttribute::from(value.clone()))); + } + for (key, value) in &self.ci_context { + attrs.push((format!("ci.{key}"), LogAttribute::from(value.clone()))); + } + attrs + } + + fn apply_to_scope(&self, scope: &mut Scope) { + for (key, value) in &self.tags { + scope.set_tag(key, value.as_str()); + } + + if !self.ci_context.is_empty() { + scope.set_context("ci", Context::Other(context_map(&self.ci_context))); + } + } + + #[cfg(test)] + fn environment(&self) -> Option<&str> { + self.environment.as_deref() + } +} + +pub(crate) fn init() -> ClientInitGuard { + let metadata = Metadata::from_env(); + let guard = sentry::init(metadata.client_options()); + sentry::configure_scope(|scope| metadata.apply_to_scope(scope)); + guard +} -/// Send a panic `msg` to the sentry service. -pub fn panic_event_from(msg: T) -> Event<'static> +pub(crate) fn tracing_layer() -> impl tracing_subscriber::Layer +where + S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>, +{ + sentry::integrations::tracing::layer().event_filter(|metadata| match *metadata.level() { + Level::ERROR => EventFilter::Event | EventFilter::Log, + Level::WARN => EventFilter::Log | EventFilter::Breadcrumb, + Level::INFO => EventFilter::Breadcrumb, + _ => EventFilter::Ignore, + }) +} + +pub(crate) fn panic_event_from(msg: T) -> Event<'static> where T: ToString, { @@ -21,11 +154,6 @@ where ..Default::default() }), value: Some(msg.to_string()), - // Sentry does not handle panic = abort well yet, and when given this - // stacktrace, it consists only of this line, making Sentry dedupe - // events together by their stacktrace fingerprint incorrectly. - // - // stacktrace: current_stacktrace(), ..Default::default() }; @@ -35,3 +163,358 @@ where ..Default::default() } } + +/// Merge `attrs` into `log.attributes`, letting existing keys win. +/// +/// Tracing event fields are already present on the [`Log`] when the +/// `before_send_log` hook fires, so we must not overwrite them; this keeps +/// our CI/git metadata as a fallback that enriches (but never masks) the +/// log's own attributes. +fn merge_log_attributes(log: &mut Log, attrs: &[(String, LogAttribute)]) { + for (key, value) in attrs { + log.attributes + .entry(key.clone()) + .or_insert_with(|| value.clone()); + } +} + +fn env_var(key: &str) -> Option { + env::var(key).ok() +} + +fn lookup_value(lookup: &F, key: &str) -> Option +where + F: Fn(&str) -> Option, +{ + let value = lookup(key)?; + let value = value.trim(); + + (!value.is_empty()).then(|| value.to_owned()) +} + +fn insert_lookup_values( + lookup: &F, + target: &mut BTreeMap<&'static str, String>, + mappings: &[(&str, &'static str)], +) where + F: Fn(&str) -> Option, +{ + for (env_key, target_key) in mappings { + if let Some(value) = lookup_value(lookup, env_key) { + target.insert(*target_key, value); + } + } +} + +fn slugged_git_ref(lookup: &F) -> Option +where + F: Fn(&str) -> Option, +{ + [ + "GITHUB_REF_POINT_SLUG_URL", + "GITHUB_HEAD_REF_SLUG_URL", + "GITHUB_REF_NAME_SLUG_URL", + ] + .into_iter() + .find_map(|key| lookup_value(lookup, key)) +} + +fn context_map(values: &BTreeMap<&'static str, String>) -> Map { + values + .iter() + .map(|(key, value)| (key.to_string(), Value::String(value.clone()))) + .collect() +} + +fn release_name() -> String { + release_name_from(&env_var, ZebradApp::git_commit()) +} + +fn release_name_from(lookup: &F, git_commit: Option<&str>) -> String +where + F: Fn(&str) -> Option, +{ + if let Some(release) = lookup_value(lookup, "SENTRY_RELEASE") { + return release; + } + + let version = build_version(); + + if version.build.is_empty() { + if let Some(git_sha) = git_commit { + return format!("{version}+git.{git_sha}"); + } + } + + version.to_string() +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{BTreeMap, HashMap}, + time::SystemTime, + }; + + use sentry::protocol::{Log, LogAttribute, LogLevel}; + + use crate::application::build_version; + + use super::{merge_log_attributes, release_name_from, Metadata}; + + #[test] + fn metadata_ignores_empty_values() { + let env = HashMap::from([ + ("SENTRY_ENVIRONMENT", "".to_string()), + ("CI_TEST_ID", " ".to_string()), + ]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + assert_eq!(metadata.environment(), None); + assert_eq!(metadata.tags.get("test.id"), None); + assert!(metadata.ci_context.is_empty()); + } + + #[test] + fn metadata_reads_expected_tags_and_ci_context() { + let env = HashMap::from([ + ("SENTRY_ENVIRONMENT", "stage".to_string()), + ("GITHUB_ACTIONS", "true".to_string()), + ("GITHUB_EVENT_NAME", "push".to_string()), + ("GITHUB_REF_POINT_SLUG_URL", "main".to_string()), + ("GITHUB_SHA", "deadbeef".to_string()), + ("GITHUB_RUN_ID", "42".to_string()), + ("GITHUB_RUN_ATTEMPT", "2".to_string()), + ("GITHUB_WORKFLOW", "CI".to_string()), + ("GITHUB_JOB", "deploy".to_string()), + ("CI_TEST_ID", "sync-full-mainnet".to_string()), + ]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + assert_eq!(metadata.environment(), Some("stage")); + assert_eq!( + metadata.tags.get("deploy.trigger").map(String::as_str), + Some("push"), + ); + assert_eq!( + metadata.tags.get("git.ref").map(String::as_str), + Some("main"), + ); + assert_eq!( + metadata.tags.get("git.sha").map(String::as_str), + Some("deadbeef"), + ); + assert_eq!( + metadata.tags.get("ci.provider").map(String::as_str), + Some("github-actions"), + ); + assert_eq!( + metadata.tags.get("test.id").map(String::as_str), + Some("sync-full-mainnet"), + ); + assert_eq!( + metadata.ci_context.get("run_id").map(String::as_str), + Some("42"), + ); + assert_eq!( + metadata.ci_context.get("run_attempt").map(String::as_str), + Some("2"), + ); + assert_eq!( + metadata.ci_context.get("workflow").map(String::as_str), + Some("CI"), + ); + assert_eq!( + metadata.ci_context.get("job").map(String::as_str), + Some("deploy"), + ); + } + + #[test] + fn metadata_reads_pull_request_number_from_ci_input() { + let env = HashMap::from([ + ("GITHUB_ACTIONS", "true".to_string()), + ("GITHUB_REF_POINT_SLUG_URL", "fix-sentry-tags".to_string()), + ("CI_PR_NUMBER", "84".to_string()), + ]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + assert_eq!( + metadata.tags.get("git.ref").map(String::as_str), + Some("fix-sentry-tags"), + ); + assert_eq!( + metadata.tags.get("pr.number").map(String::as_str), + Some("84"), + ); + } + + #[test] + fn metadata_prefers_slugged_git_ref_when_available() { + let env = HashMap::from([ + ( + "GITHUB_REF_POINT_SLUG_URL", + "feature-use-sentry".to_string(), + ), + ( + "GITHUB_HEAD_REF_SLUG_URL", + "feature-use-plus-sentry".to_string(), + ), + ("GITHUB_REF_NAME_SLUG_URL", "84-merge".to_string()), + ]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + assert_eq!( + metadata.tags.get("git.ref").map(String::as_str), + Some("feature-use-sentry"), + ); + } + + #[test] + fn metadata_ignores_missing_slugged_git_ref() { + let env = HashMap::from([ + ("GITHUB_ACTIONS", "true".to_string()), + ("GITHUB_EVENT_NAME", "push".to_string()), + ]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + assert_eq!(metadata.tags.get("git.ref"), None); + } + + #[test] + fn release_name_prefers_sentry_release_override() { + let env = HashMap::from([("SENTRY_RELEASE", "zebrad@4.4.0-rc.1".to_string())]); + + let release = release_name_from(&|key| env.get(key).cloned(), Some("deadbeef")); + + assert_eq!(release, "zebrad@4.4.0-rc.1"); + } + + #[test] + fn release_name_appends_git_sha_when_build_metadata_is_empty() { + let env: HashMap<&str, String> = HashMap::new(); + + let release = release_name_from(&|key| env.get(key).cloned(), Some("deadbeef")); + + let version = build_version(); + if version.build.is_empty() { + assert_eq!(release, format!("{version}+git.deadbeef")); + } else { + // Tagged build already carries +build metadata; do not double-append. + assert_eq!(release, version.to_string()); + } + } + + #[test] + fn release_name_falls_back_to_version_without_git_sha() { + let env: HashMap<&str, String> = HashMap::new(); + + let release = release_name_from(&|key| env.get(key).cloned(), None); + + assert_eq!(release, build_version().to_string()); + } + + #[test] + fn client_options_enables_logs_and_roundtrips_environment() { + let env = HashMap::from([("SENTRY_ENVIRONMENT", "stage".to_string())]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + let options = metadata.client_options_with_release("zebrad@test".to_string()); + + assert!(options.enable_logs); + assert_eq!(options.environment.as_deref(), Some("stage")); + assert_eq!(options.release.as_deref(), Some("zebrad@test")); + } + + #[test] + fn client_options_omits_environment_when_unset() { + let metadata = Metadata::from_lookup(|_: &str| -> Option { None }); + + let options = metadata.client_options_with_release("zebrad@test".to_string()); + + assert!(options.environment.is_none()); + } + + #[test] + fn log_attributes_include_tags_and_ci_context() { + let env = HashMap::from([ + ("GITHUB_ACTIONS", "true".to_string()), + ("GITHUB_EVENT_NAME", "push".to_string()), + ("GITHUB_SHA", "deadbeef".to_string()), + ("GITHUB_REF_POINT_SLUG_URL", "main".to_string()), + ("GITHUB_RUN_ID", "42".to_string()), + ("GITHUB_RUN_ATTEMPT", "2".to_string()), + ("GITHUB_WORKFLOW", "CI".to_string()), + ("GITHUB_JOB", "deploy".to_string()), + ("CI_TEST_ID", "sync-full-mainnet".to_string()), + ("CI_PR_NUMBER", "84".to_string()), + ]); + let metadata = Metadata::from_lookup(|key| env.get(key).cloned()); + + let attrs: BTreeMap = metadata.log_attributes().into_iter().collect(); + + let expect = |key: &str, value: &str| { + assert_eq!( + attrs.get(key).and_then(|attr| attr.0.as_str()), + Some(value), + "missing or wrong log attribute {key}", + ); + }; + + expect("test.id", "sync-full-mainnet"); + expect("git.sha", "deadbeef"); + expect("git.ref", "main"); + expect("ci.provider", "github-actions"); + expect("pr.number", "84"); + expect("deploy.trigger", "push"); + expect("ci.run_id", "42"); + expect("ci.run_attempt", "2"); + expect("ci.workflow", "CI"); + expect("ci.job", "deploy"); + } + + #[test] + fn merge_log_attributes_preserves_existing() { + let mut log = Log { + level: LogLevel::Info, + body: "test".to_string(), + trace_id: None, + timestamp: SystemTime::UNIX_EPOCH, + severity_number: None, + attributes: BTreeMap::new(), + }; + + log.attributes.insert( + "git.sha".to_string(), + LogAttribute::from("event-sha".to_string()), + ); + + let attrs = vec![ + ( + "git.sha".to_string(), + LogAttribute::from("metadata-sha".to_string()), + ), + ( + "ci.run_id".to_string(), + LogAttribute::from("42".to_string()), + ), + ]; + + merge_log_attributes(&mut log, &attrs); + + assert_eq!( + log.attributes + .get("git.sha") + .and_then(|attr| attr.0.as_str()), + Some("event-sha"), + "pre-existing attribute must not be overwritten", + ); + assert_eq!( + log.attributes + .get("ci.run_id") + .and_then(|attr| attr.0.as_str()), + Some("42"), + "new attribute must be inserted", + ); + } +} diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index e996bcac2b3..a948a4e2c98 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -154,9 +154,8 @@ use semver::Version; use serde_json::Value; use tower::ServiceExt; -use zcash_keys::address::Address; - use zebra_chain::{ + amount::Amount, block::{self, genesis::regtest_genesis_block, ChainHistoryBlockTxAuthCommitmentHash, Height}, parameters::{ testnet::{ConfiguredActivationHeights, ConfiguredCheckpoints, RegtestParameters}, @@ -168,14 +167,15 @@ use zebra_chain::{ use zebra_node_services::rpc_client::RpcRequestClient; use zebra_rpc::{ client::{ - BlockTemplateResponse, GetBlockTemplateParameters, GetBlockTemplateRequestMode, - GetBlockTemplateResponse, SubmitBlockErrorResponse, SubmitBlockResponse, + BlockTemplateResponse, DefaultRoots, GetBlockTemplateParameters, + GetBlockTemplateRequestMode, GetBlockTemplateResponse, SubmitBlockErrorResponse, + SubmitBlockResponse, TransactionTemplate, }, - fetch_state_tip_and_local_time, generate_coinbase_and_roots, + fetch_chain_info, methods::{RpcImpl, RpcServer}, proposal_block_from_template, server::OPENED_RPC_ENDPOINT_MSG, - SubmitBlockChannel, + MinerParams, SubmitBlockChannel, }; use zebra_state::{constants::LOCK_FILE_ERROR, state_database_format_version_in_code}; use zebra_test::{ @@ -227,7 +227,7 @@ fn generate_no_args() -> Result<()> { let _init_guard = zebra_test::init(); let child = testdir()? - .with_config(&mut default_test_config(&Mainnet)?)? + .with_config(&mut default_test_config(&Mainnet))? .spawn_child(args!["generate"])?; let output = child.wait_with_output()?; @@ -289,7 +289,7 @@ fn generate_args() -> Result<()> { fn help_no_args() -> Result<()> { let _init_guard = zebra_test::init(); - let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; + let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet))?; let child = testdir.spawn_child(args!["help"])?; let output = child.wait_with_output()?; @@ -361,7 +361,7 @@ fn start_no_args() -> Result<()> { fn start_args() -> Result<()> { let _init_guard = zebra_test::init(); - let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; + let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet))?; let testdir = &testdir; let mut child = testdir.spawn_child(args!["start"])?; @@ -386,7 +386,7 @@ fn start_args() -> Result<()> { #[tokio::test] async fn db_init_outside_future_executor() -> Result<()> { let _init_guard = zebra_test::init(); - let config = default_test_config(&Mainnet)?; + let config = default_test_config(&Mainnet); let start = Instant::now(); @@ -487,7 +487,7 @@ fn ephemeral(cache_dir_config: EphemeralConfig, cache_dir_check: EphemeralCheck) let _init_guard = zebra_test::init(); - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); let run_dir = testdir()?; let ignored_cache_dir = run_dir.path().join("state"); @@ -590,7 +590,7 @@ fn ephemeral(cache_dir_config: EphemeralConfig, cache_dir_check: EphemeralCheck) fn version_no_args() -> Result<()> { let _init_guard = zebra_test::init(); - let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; + let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet))?; let child = testdir.spawn_child(args!["--version"])?; let output = child.wait_with_output()?; @@ -611,7 +611,7 @@ fn version_no_args() -> Result<()> { fn version_args() -> Result<()> { let _init_guard = zebra_test::init(); - let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; + let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet))?; let testdir = &testdir; // unrecognized option `-f` @@ -1396,8 +1396,8 @@ fn sync_full_testnet() -> Result<()> { ) } -#[cfg(all(feature = "prometheus", not(target_os = "windows")))] #[tokio::test] +#[cfg(all(feature = "prometheus", not(target_os = "windows")))] async fn metrics_endpoint() -> Result<()> { use bytes::Bytes; use http_body_util::BodyExt; @@ -1413,7 +1413,7 @@ async fn metrics_endpoint() -> Result<()> { let url = format!("http://{endpoint}"); // Write a configuration that has metrics endpoint_addr set - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); config.metrics.endpoint_addr = Some(endpoint.parse().unwrap()); let dir = testdir()?.with_config(&mut config)?; @@ -1482,7 +1482,7 @@ async fn tracing_endpoint() -> Result<()> { let url_filter = format!("{url_default}/filter"); // Write a configuration that has tracing endpoint_addr option set - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); config.tracing.endpoint_addr = Some(endpoint.parse().unwrap()); let dir = testdir()?.with_config(&mut config)?; @@ -2204,7 +2204,7 @@ fn zebra_zcash_listener_conflict() -> Result<()> { let listen_addr = format!("127.0.0.1:{port}"); // Write a configuration that has our created network listen_addr - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); config.network.listen_addr = listen_addr.parse().unwrap(); let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!("Opened Zcash protocol endpoint at {listen_addr}")); @@ -2233,7 +2233,7 @@ fn zebra_metrics_conflict() -> Result<()> { let listen_addr = format!("127.0.0.1:{port}"); // Write a configuration that has our created metrics endpoint_addr - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); config.metrics.endpoint_addr = Some(listen_addr.parse().unwrap()); let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!(r"Opened metrics endpoint at {listen_addr}")); @@ -2262,7 +2262,7 @@ fn zebra_tracing_conflict() -> Result<()> { let listen_addr = format!("127.0.0.1:{port}"); // Write a configuration that has our created tracing endpoint_addr - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); config.tracing.endpoint_addr = Some(listen_addr.parse().unwrap()); let dir1 = testdir()?.with_config(&mut config)?; let regex1 = regex::escape(&format!(r"Opened tracing endpoint at {listen_addr}")); @@ -2489,7 +2489,7 @@ fn delete_old_databases() -> Result<()> { return Ok(()); } - let mut config = default_test_config(&Mainnet)?; + let mut config = default_test_config(&Mainnet); let run_dir = testdir()?; let cache_dir = run_dir.path().join("state"); @@ -2602,7 +2602,7 @@ async fn rpc_submit_block() -> Result<()> { #[test] fn end_of_support_is_checked_at_start() -> Result<()> { let _init_guard = zebra_test::init(); - let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet)?)?; + let testdir = testdir()?.with_config(&mut default_test_config(&Mainnet))?; let mut child = testdir.spawn_child(args!["start"])?; // Give enough time to start up the eos task. @@ -3002,13 +3002,89 @@ fn external_address() -> Result<()> { /// Test successful `getblocktemplate` and `submitblock` RPC calls on Regtest on Canopy. /// /// See [`common::regtest::submit_blocks`] for more information. -// TODO: Test this with an NU5 activation height too once config can be serialized. #[tokio::test] async fn regtest_block_templates_are_valid_block_submissions() -> Result<()> { common::regtest::submit_blocks_test().await?; Ok(()) } +/// Regression test for . +/// +/// `getrawtransaction` must count confirmations against the full best-chain tip +/// (including non-finalized blocks), not just the finalized-database tip. +#[tokio::test] +async fn getrawtransaction_confirmations_include_non_finalized_blocks() -> Result<()> { + use zebra_state::constants::MAX_BLOCK_REORG_HEIGHT; + + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest( + ConfiguredActivationHeights { + nu5: Some(100), + ..Default::default() + } + .into(), + ); + let mut config = os_assigned_rpc_port_config(false, &network)?; + config.mempool.debug_enable_at_height = Some(0); + + let mut zebrad = testdir()? + .with_config(&mut config)? + .spawn_child(args!["start"])?; + let rpc_address = read_listen_addr_from_logs(&mut zebrad, OPENED_RPC_ENDPOINT_MSG)?; + + tokio::time::sleep(LAUNCH_DELAY).await; + + let client = RpcRequestClient::new(rpc_address); + + // Mine enough blocks to push the first few blocks into the finalized state. + // Block at height 2 is finalized once tip > 2 + MAX_BLOCK_REORG_HEIGHT (= 1002). + let blocks_to_mine = MAX_BLOCK_REORG_HEIGHT + 10; + client.generate(blocks_to_mine).await?; + + // Get the coinbase txid from block 2 (it will be in the finalized DB). + let block2 = client + .get_block(2) + .await + .map_err(|err| eyre::eyre!(err))? + .expect("block at height 2 should exist"); + let txid = block2.transactions[0].hash(); + + // Confirm the tip height and compute expected confirmations. + let info = client.blockchain_info().await?; + let tip_height = info.blocks().0; + let expected_confirmations = 1 + tip_height - 2; + + // getrawtransaction verbose=1 returns a JSON object that includes `confirmations`. + let response: Value = client + .json_result_from_call("getrawtransaction", format!(r#"["{txid}", 1]"#)) + .await + .map_err(|err| eyre::eyre!(err))?; + + let confirmations: u32 = response["confirmations"] + .as_u64() + .expect("confirmations should be a positive integer") + .try_into() + .expect("confirmations should fit in u32 because regtest block heights fit in u32"); + + assert_eq!( + confirmations, expected_confirmations, + "getrawtransaction must count confirmations against the full best-chain tip \ + (including non-finalized blocks), not just the finalized-DB tip" + ); + + zebrad.kill(false)?; + let output = zebrad.wait_with_output()?; + output.assert_failure()?.assert_was_killed()?; + + Ok(()) +} + +#[tokio::test] +async fn regtest_coinbase() -> Result<()> { + common::coinbase::regtest_coinbase().await +} + #[tokio::test(flavor = "multi_thread")] async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { use std::sync::Arc; @@ -3354,16 +3430,9 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { tracing::info!("built configured Testnet, starting state service and block verifier"); - let default_test_config = default_test_config(&network)?; - let mining_config = default_test_config.mining; - let miner_address = Address::try_from_zcash_address( - &network, - mining_config - .miner_address - .clone() - .expect("mining address should be configured"), - ) - .expect("configured mining address should be valid"); + let default_test_config = default_test_config(&network); + let mining_conf = default_test_config.mining; + let miner_params = MinerParams::new(&network, mining_conf.clone())?; let (state, read_state, latest_chain_tip, _chain_tip_change) = zebra_state::init_test_services(&network).await; @@ -3400,7 +3469,7 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { let (rpc, _) = RpcImpl::new( network.clone(), - mining_config, + mining_conf, false, "0.0.1", "Zebra tests", @@ -3514,9 +3583,9 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { let zebra_state::GetBlockTemplateChainInfo { chain_history_root, .. - } = fetch_state_tip_and_local_time(read_state.clone()).await?; + } = fetch_chain_info(read_state.clone()).await?; - let network = base_network_params + let net = base_network_params .clone() .with_funding_streams(vec![ConfiguredFundingStreams { height_range: Some(Height(1)..Height(100)), @@ -3525,18 +3594,24 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { .to_network() .expect("failed to build configured network"); - let (coinbase_txn, default_roots) = generate_coinbase_and_roots( - &network, + let coinbase_txn = TransactionTemplate::new_coinbase( + &net, Height(block_template.height()), - &miner_address, - &[], - chain_history_root, - vec![], + &miner_params, + Amount::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, ) .expect("coinbase transaction should be valid under the given parameters"); + let default_roots = DefaultRoots::from_coinbase( + &net, + Height(block_template.height()), + &coinbase_txn, + chain_history_root, + &[], + ); + let block_template = BlockTemplateResponse::new( block_template.capabilities().clone(), block_template.version(), @@ -3561,7 +3636,7 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { block_template.submit_old(), ); - let proposal_block = proposal_block_from_template(&block_template, None, &network)?; + let proposal_block = proposal_block_from_template(&block_template, None, &net)?; // Submit the invalid block with an excessive coinbase output value let submit_block_response = rpc @@ -3577,7 +3652,7 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { ); // Use an invalid coinbase transaction (with an output value less than the `block_subsidy + miner_fees - expected_lockbox_funding_stream`) - let network = base_network_params + let net = base_network_params .clone() .with_funding_streams(vec![ConfiguredFundingStreams { height_range: Some(Height(1)..Height(100)), @@ -3586,18 +3661,24 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { .to_network() .expect("failed to build configured network"); - let (coinbase_txn, default_roots) = generate_coinbase_and_roots( - &network, + let coinbase_txn = TransactionTemplate::new_coinbase( + &net, Height(block_template.height()), - &miner_address, - &[], - chain_history_root, - vec![], + &miner_params, + Amount::zero(), #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, ) .expect("coinbase transaction should be valid under the given parameters"); + let default_roots = DefaultRoots::from_coinbase( + &net, + Height(block_template.height()), + &coinbase_txn, + chain_history_root, + &[], + ); + let block_template = BlockTemplateResponse::new( block_template.capabilities().clone(), block_template.version(), @@ -3622,7 +3703,7 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { block_template.submit_old(), ); - let proposal_block = proposal_block_from_template(&block_template, None, &network)?; + let proposal_block = proposal_block_from_template(&block_template, None, &net)?; // Submit the invalid block with an excessive coinbase input value let submit_block_response = rpc @@ -3638,8 +3719,7 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { ); // Check that the original block template can be submitted successfully - let proposal_block = - proposal_block_from_template(&valid_original_block_template, None, &network)?; + let proposal_block = proposal_block_from_template(&valid_original_block_template, None, &net)?; let submit_block_response = rpc .submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None) @@ -3678,11 +3758,11 @@ async fn nu7_nsm_transactions() -> Result<()> { let base_network_params = testnet::Parameters::build() // Regtest genesis hash .with_genesis_hash("029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327") - .expect("failed to set genesis hash") + .unwrap() .with_checkpoints(false) - .expect("failed to verify checkpoints") + .unwrap() .with_target_difficulty_limit(U256::from_big_endian(&[0x0f; 32])) - .expect("failed to set target difficulty limit") + .unwrap() .with_disable_pow(true) .with_slow_start_interval(Height::MIN) .with_lockbox_disbursements(vec![]) @@ -3701,11 +3781,11 @@ async fn nu7_nsm_transactions() -> Result<()> { recipients: None, }]) .to_network() - .expect("failed to build configured network"); + .unwrap(); tracing::info!("built configured Testnet, starting state service and block verifier"); - let default_test_config = default_test_config(&network)?; + let default_test_config = default_test_config(&network); let mining_config = default_test_config.mining; let (state, read_state, latest_chain_tip, _chain_tip_change) = @@ -3878,8 +3958,8 @@ async fn has_spending_transaction_ids() -> Result<()> { tracing::info!("checking indexes of spending transaction ids"); - // Read the last 500 blocks - should be greater than the MAX_BLOCK_REORG_HEIGHT so that - // both the finalized and non-finalized state are checked. + // Read the last 500 blocks. This exceeds the 100 blocks committed to the non-finalized + // state above, so the loop reads into the finalized state too, checking both. let num_blocks_to_check = 500; let mut is_failure = false; for i in 0..num_blocks_to_check { diff --git a/zebrad/tests/common/checkpoints.rs b/zebrad/tests/common/checkpoints.rs index 6fe8975982e..008e32b7a90 100644 --- a/zebrad/tests/common/checkpoints.rs +++ b/zebrad/tests/common/checkpoints.rs @@ -15,8 +15,8 @@ use tempfile::TempDir; use zebra_chain::{ block::{Height, HeightDiff, TryIntoHeight}, + parameters::constants::MAX_BLOCK_REORG_HEIGHT, parameters::Network, - transparent::MIN_TRANSPARENT_COINBASE_MATURITY, }; use zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP; use zebra_node_services::rpc_client::RpcRequestClient; @@ -400,7 +400,7 @@ pub fn wait_for_zebra_checkpoints_generation< test_type: TestType, show_zebrad_logs: bool, ) -> Result<(TestChild, TestChild

)> { - let last_checkpoint_gap = HeightDiff::from(MIN_TRANSPARENT_COINBASE_MATURITY) + let last_checkpoint_gap = HeightDiff::from(MAX_BLOCK_REORG_HEIGHT) + HeightDiff::try_from(MAX_CHECKPOINT_HEIGHT_GAP).expect("constant fits in HeightDiff"); let expected_final_checkpoint_height = (zebra_tip_height - last_checkpoint_gap).expect("network tip is high enough"); diff --git a/zebrad/tests/common/coinbase.rs b/zebrad/tests/common/coinbase.rs new file mode 100644 index 00000000000..00fcaedf89d --- /dev/null +++ b/zebrad/tests/common/coinbase.rs @@ -0,0 +1,90 @@ +//! Tests for checking that Zebra produces valid coinbase transactions. + +use std::sync::Arc; + +use color_eyre::eyre::{self, Context}; +use futures::future::try_join_all; +use strum::IntoEnumIterator; + +use zebra_chain::{ + parameters::{testnet::ConfiguredActivationHeights, Network}, + primitives::byte_array::increment_big_endian, +}; +use zebra_consensus::difficulty_is_valid; +use zebra_node_services::rpc_client::RpcRequestClient; +use zebra_rpc::{config::mining::MinerAddressType, server::OPENED_RPC_ENDPOINT_MSG}; +use zebra_test::args; +use zebrad::components::With; + +use super::{ + config::{os_assigned_rpc_port_config, read_listen_addr_from_logs, testdir}, + launch::{ZebradTestDirExt, LAUNCH_DELAY}, + regtest::MiningRpcMethods, +}; + +/// Tests that Zebra can mine blocks with valid coinbase transactions on Regtest. +pub(crate) async fn regtest_coinbase() -> eyre::Result<()> { + async fn regtest_coinbase(addr_type: MinerAddressType) -> eyre::Result<()> { + let _init_guard = zebra_test::init(); + + // NU6.2 must be active for the blocks being mined: coinbase transactions for + // unified miner addresses contain an Orchard output, and the `orchard` crate + // only supports proving with the fixed (NU6.2+) Action circuit, so Orchard + // proofs in blocks mined at earlier heights would fail to verify against the + // historical pre-NU6.2 verifying key. + let net = Network::new_regtest( + ConfiguredActivationHeights { + nu6_2: Some(1), + ..Default::default() + } + .into(), + ); + + let mut config = os_assigned_rpc_port_config(false, &net)?.with(addr_type); + config.mempool.debug_enable_at_height = Some(0); + + let mut zebrad = testdir()? + .with_config(&mut config)? + .spawn_child(args!["start"])?; + + tokio::time::sleep(LAUNCH_DELAY).await; + + let client = RpcRequestClient::new(read_listen_addr_from_logs( + &mut zebrad, + OPENED_RPC_ENDPOINT_MSG, + )?); + + for _ in 0..2 { + let (mut block, height) = client.block_from_template(&net).await?; + + // If the network requires PoW, find a valid nonce. + if !net.disable_pow() { + let header = Arc::make_mut(&mut block.header); + + loop { + let hash = header.hash(); + + if difficulty_is_valid(header, &net, &height, &hash).is_ok() { + break; + } + + increment_big_endian(header.nonce.as_mut()); + } + } + + client.submit_block(block).await?; + } + + zebrad.kill(false)?; + + zebrad + .wait_with_output()? + .assert_failure()? + .assert_was_killed() + .wrap_err("possible port conflict with another zebrad instance") + } + + try_join_all(MinerAddressType::iter().map(regtest_coinbase)) + .await + .map(|_| ()) +} diff --git a/zebrad/tests/common/config.rs b/zebrad/tests/common/config.rs index 55a4774d25b..cdd24ed2cd7 100644 --- a/zebrad/tests/common/config.rs +++ b/zebrad/tests/common/config.rs @@ -15,10 +15,11 @@ use std::{ use color_eyre::eyre::Result; use tempfile::TempDir; -use zebra_chain::parameters::{Network, NetworkKind}; +use zebra_chain::parameters::Network; +use zebra_rpc::config::mining::MinerAddressType; use zebra_test::{command::TestChild, net::random_known_port}; use zebrad::{ - components::{mempool, sync, tracing}, + components::{mempool, sync, tracing, With}, config::ZebradConfig, }; @@ -29,13 +30,13 @@ use crate::common::cached_state::DATABASE_FORMAT_CHECK_INTERVAL; /// - an ephemeral state, /// - the minimum syncer lookahead limit, and /// - shorter task intervals, to improve test coverage. -pub fn default_test_config(net: &Network) -> Result { +pub fn default_test_config(net: &Network) -> ZebradConfig { const TEST_DURATION: Duration = Duration::from_secs(30); let network = zebra_network::Config { network: net.clone(), // The OS automatically chooses an unused port. - listen_addr: "127.0.0.1:0".parse()?, + listen_addr: "127.0.0.1:0".parse().unwrap(), crawl_new_peer_interval: TEST_DURATION, ..zebra_network::Config::default() }; @@ -61,54 +62,26 @@ pub fn default_test_config(net: &Network) -> Result { let mut state = zebra_state::Config::ephemeral(); state.debug_validity_check_interval = Some(DATABASE_FORMAT_CHECK_INTERVAL); - // Provide a default miner address matched to the effective network so - // mining-related tests have a valid configuration. Environment variables - // like ZEBRA_MINING__MINER_ADDRESS will override this file value. - let mut mining = zebra_rpc::config::mining::Config::default(); - - // Determine the effective network kind: prefer environment override if present. - let effective_kind: NetworkKind = match env::var("ZEBRA_NETWORK__NETWORK").ok().as_deref() { - Some("Mainnet") => NetworkKind::Mainnet, - Some("Regtest") => NetworkKind::Regtest, - Some("Testnet") => NetworkKind::Testnet, - _ => net.kind(), - }; - - let default_miner_address = match effective_kind { - // Mainnet UA - NetworkKind::Mainnet => "u1cymdny2u2vllkx7t5jnelp0kde0dgnwu0jzmggzguxvxj6fe7gpuqehywejndlrjwgk9snr6g69azs8jfet78s9zy60uepx6tltk7ee57jlax49dezkhkgvjy2puuue6dvaevt53nah7t2cc2k4p0h0jxmlu9sx58m2xdm5f9sy2n89jdf8llflvtml2ll43e334avu2fwytuna404a", - // Regtest UA - NetworkKind::Regtest => "uregtest1a2yn922nnxyvnj4qmax07lkr7kmnyxq3rw0paa2kes87h2rapehrzgy8xrq665sg6aatmpgzkngwlumzr40e5y4vc40a809rsyqcwq25xfj5r2sxu774xdt6dj5xckjkv5ll0c2tv6qtsl60mpccwd6m95upy2da0rheqmkmxr7fv9z5uve0kpkmssxcuvzasewwns986yud6aact4y", - // Testnet UA - NetworkKind::Testnet => "utest1quxrs9munape90f833rnse9s02xwkvrh2yzlvm56rsg2lccpr3kwmprxw4zq6ukkv5ht6uvmzasf9pwwfhpfqct4ghmkp7zka6ufurnc9vkwvzt4jved8hld2cram6x75qxs0dgg3eq8gef8kttpw4eqjywnxpuns0fpfz072whje4xmld6ahy9dezsvzmugemn8lerr47lhcx3rzl6", - }; - - mining.miner_address = Some( - default_miner_address - .parse() - .expect("hard-coded address is valid"), - ); - - Ok(ZebradConfig { + ZebradConfig { network, state, sync, mempool, consensus, tracing, - mining, ..ZebradConfig::default() - }) + } + .with(MinerAddressType::Transparent) } pub fn persistent_test_config(network: &Network) -> Result { - let mut config = default_test_config(network)?; + let mut config = default_test_config(network); config.state.ephemeral = false; Ok(config) } pub fn external_address_test_config(network: &Network) -> Result { - let mut config = default_test_config(network)?; + let mut config = default_test_config(network); config.network.external_addr = Some("127.0.0.1:0".parse()?); Ok(config) } @@ -166,7 +139,7 @@ pub fn rpc_port_config( // Write a configuration that has the rpc listen_addr option set // TODO: split this config into another function? - let mut config = default_test_config(network)?; + let mut config = default_test_config(network); config.rpc.listen_addr = Some(zebra_rpc_listener); if parallel_cpu_threads { // Auto-configure to the number of CPU cores: most users configure this diff --git a/zebrad/tests/common/configs/custom-conf.toml b/zebrad/tests/common/configs/custom-conf.toml index 0d43be1372b..fa2b3a05b55 100644 --- a/zebrad/tests/common/configs/custom-conf.toml +++ b/zebrad/tests/common/configs/custom-conf.toml @@ -8,7 +8,7 @@ tx_cost_limit = 80000000 [metrics] [mining] -extra_coinbase_data = "do you even shield?" +extra_coinbase_data = "Do you even shield?" [network] cache_dir = true diff --git a/zebrad/tests/common/configs/v1.9.0-internal-miner.toml b/zebrad/tests/common/configs/v1.9.0-internal-miner.toml index 55a1dbab641..0a593094706 100644 --- a/zebrad/tests/common/configs/v1.9.0-internal-miner.toml +++ b/zebrad/tests/common/configs/v1.9.0-internal-miner.toml @@ -40,7 +40,7 @@ debug_enable_at_height = 0 [metrics] [mining] -miner_address = 'u1cymdny2u2vllkx7t5jnelp0kde0dgnwu0jzmggzguxvxj6fe7gpuqehywejndlrjwgk9snr6g69azs8jfet78s9zy60uepx6tltk7ee57jlax49dezkhkgvjy2puuue6dvaevt53nah7t2cc2k4p0h0jxmlu9sx58m2xdm5f9sy2n89jdf8llflvtml2ll43e334avu2fwytuna404a' +miner_address = 'zs1xl84ekz6stprmvrp39s77mf9t953nqjndwlcjtzfrr3cgjjez87639xm4u9pfuvylrhec3uryy5' internal_miner = true [network] diff --git a/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs b/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs index 777571c5c75..a7a35316371 100644 --- a/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs +++ b/zebrad/tests/common/get_block_template_rpcs/get_block_template.rs @@ -258,6 +258,8 @@ async fn try_validate_block_template(client: &RpcRequestClient, net: &Network) - .expect("response should be success output with a serialized `BlockProposalResponse`"); if let BlockProposalResponse::Rejected(reject_reason) = proposal_result { + if reject_reason.contains("best-chain-tip") { continue; } + tracing::info!( ?reject_reason, ?template, diff --git a/zebrad/tests/common/mod.rs b/zebrad/tests/common/mod.rs index 24e474e9a2e..09a9f174c82 100644 --- a/zebrad/tests/common/mod.rs +++ b/zebrad/tests/common/mod.rs @@ -13,6 +13,7 @@ pub mod cached_state; pub mod check; #[cfg(feature = "zebra-checkpoints")] pub mod checkpoints; +pub mod coinbase; pub mod config; pub mod failure_messages; pub mod get_block_template_rpcs; diff --git a/zebrad/tests/common/regtest.rs b/zebrad/tests/common/regtest.rs index 82da1429745..fbeba80eec7 100644 --- a/zebrad/tests/common/regtest.rs +++ b/zebrad/tests/common/regtest.rs @@ -37,14 +37,14 @@ const NUM_BLOCKS_TO_SUBMIT: usize = 200; pub(crate) async fn submit_blocks_test() -> Result<()> { let _init_guard = zebra_test::init(); - let network = Network::new_regtest( + let net = Network::new_regtest( ConfiguredActivationHeights { nu5: Some(100), ..Default::default() } .into(), ); - let mut config = os_assigned_rpc_port_config(false, &network)?; + let mut config = os_assigned_rpc_port_config(false, &net)?; config.mempool.debug_enable_at_height = Some(0); let mut zebrad = testdir()? @@ -58,10 +58,10 @@ pub(crate) async fn submit_blocks_test() -> Result<()> { let client = RpcRequestClient::new(rpc_address); for _ in 1..=NUM_BLOCKS_TO_SUBMIT { - let (mut block, height) = client.block_from_template(&network).await?; + let (mut block, height) = client.block_from_template(&net).await?; - while !network.disable_pow() - && zebra_consensus::difficulty_is_valid(&block.header, &network, &height, &block.hash()) + while !net.disable_pow() + && zebra_consensus::difficulty_is_valid(&block.header, &net, &height, &block.hash()) .is_err() { increment_big_endian(Arc::make_mut(&mut block.header).nonce.as_mut()); diff --git a/zebrad/tests/common/sync.rs b/zebrad/tests/common/sync.rs index 2289d24cb42..8c134cbc875 100644 --- a/zebrad/tests/common/sync.rs +++ b/zebrad/tests/common/sync.rs @@ -58,15 +58,14 @@ pub const STOP_ON_LOAD_TIMEOUT: Duration = Duration::from_secs(10); /// /// Usually the small checkpoint is much shorter than this. // -// Tempoaraily increased to 4 minutes to get more diagnostic info in failed tests. +// Temporarily increased to 4 minutes to get more diagnostic info in failed tests. // TODO: reduce to 120 when #6506 is fixed pub const TINY_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(240); /// The maximum amount of time Zebra should take to sync a thousand blocks. // -// Tempoaraily increased to 4 minutes to get more diagnostic info in failed tests. -// TODO: reduce to 180 when #6506 is fixed -pub const LARGE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(240); +// Temporarily increased to 8 minutes due to sync performance issues. +pub const LARGE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(480); /// The maximum time to wait for Zebrad to synchronize up to the chain tip starting from a /// partially synchronized state. diff --git a/zebrad/tests/common/test_type.rs b/zebrad/tests/common/test_type.rs index 0975ecf5234..af1d5866e4e 100644 --- a/zebrad/tests/common/test_type.rs +++ b/zebrad/tests/common/test_type.rs @@ -225,7 +225,7 @@ impl TestType { // This is what we recommend our users configure. random_known_rpc_port_config(true, network) } else { - default_test_config(network) + Ok(default_test_config(network)) }; let mut config = match config {