Skip to content

docs: point NU7 join page at the kresko release; drop source-build wrappers#1

Closed
evan-forbes wants to merge 2 commits into
evan/nu7/testnetfrom
evan/nu7/join-docs-binaries
Closed

docs: point NU7 join page at the kresko release; drop source-build wrappers#1
evan-forbes wants to merge 2 commits into
evan/nu7/testnetfrom
evan/nu7/join-docs-binaries

Conversation

@evan-forbes

Copy link
Copy Markdown

The NU7 join flow moved to a single script published as a kresko release asset (valargroup/kreskoscripts/join-nu7-testnet.sh, see valargroup/kresko#4) that downloads the prebuilt zebrad/kresko binaries instead of building from source on the host.

This cleans up the zebra-fork side that's now obsolete:

  • Repoint book/src/dev/nu7-testnet-join.md at the released join script and the binary-download flow. Drops the old source-build instructions and the in-page live-script display (it relied on a same-origin served copy that no longer exists). Keeps the interactive command builder and the bundle download.
  • Remove the two obsolete downloader wrappers — scripts/join-nu7-testnet.sh and the served book copy book/src/dev/nu7-testnet/join-nu7-testnet.sh — both replaced by the kresko release script.

No code changes; docs/scripts only.

🤖 Generated with Claude Code

evan-forbes and others added 2 commits May 21, 2026 09:17
…appers

The NU7 join flow moved to a single script published as a kresko release asset
(valargroup/kresko: scripts/join-nu7-testnet.sh) that downloads prebuilt
zebrad/kresko binaries instead of building from source.

- Repoint book/src/dev/nu7-testnet-join.md at the released join script and the
  binary-download flow; drop the obsolete in-page source-build instructions and
  the live-script display that relied on a same-origin served copy.
- Remove the now-obsolete downloader wrappers (scripts/join-nu7-testnet.sh and
  the served book copy) — both are replaced by the kresko release script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@evan-forbes evan-forbes reopened this May 23, 2026
@evan-forbes

Copy link
Copy Markdown
Author

Closing: this PR targets the evan/nu7/testnet reference branch, not main, so it's outside the main-stack CI effort. Its two commits (05f7fa9 docs kresko join page, a6e99f8 expose the nsm pool via the rpc) are already present on the nu7 working line; the NSM RPC exposure is tracked within the NSM-LTS PR (#7). No work is lost. Reopen if the testnet branch specifically needs these as a standalone PR.

evan-forbes pushed a commit that referenced this pull request Jun 10, 2026
…5709) (ZcashFoundation#10679)

* fix(network): stop poisoning inventory registry on transient errors

Only mark inventory as missing for explicit notfound responses.
Transport failures (timeouts, drops) no longer poison the routing
registry. Combined with #131 FindBlocks registration + re-request.

Co-Authored-By: Gustavo Valverde <g.valverde02@gmail.com>

* feat(sync): source-aware block routing with contiguous-prefix guard

Track which peer announced each block hash from FindBlocks/ExtendTips
responses. Route block requests to the announcing peer via
BlocksByHashFrom, falling back to inventory routing if the source is
not ready. Force the first CONTIGUOUS_PREFIX_P2C hashes to reliable
P2C routing so the checkpoint verifier always gets its next-needed block.

Based on v5.0.0 + no-poison fix. No #131 (re-request is superseded by
proper routing).

Co-Authored-By: Gustavo Valverde <g.valverde02@gmail.com>

* feat(sync): add block re-request for failed downloads (ZcashFoundation#5709)

Re-queue blocks whose download failed with NotFound instead of
silently dropping them. Bounded by MAX_BLOCK_REOBTAIN_RETRIES.
Combined with source-aware routing, this handles the case where
the source peer is busy and the fallback misroutes — the block
gets another chance via normal routing.

Co-Authored-By: Zaki Manian <zaki@manian.org>

* feat(sync): match #129 behavior — prefix=2000, batch by source peer

Three fixes to match #129's proven 0-error genesis-to-tip result:
1. CONTIGUOUS_PREFIX_P2C: 32 → 2000 (most blocks use reliable P2C)
2. Batch blocks by source peer (SOURCE_BLOCK_BATCH_LIMIT=8)
3. Unsourced blocks dispatched individually via P2C

Co-Authored-By: Gustavo Valverde <g.valverde02@gmail.com>

* chore: remove unused download_and_verify_from and dead variable

* fix(ci): allow large_enum_variant on pre-existing Setup enum

The Setup enum in inbound.rs has a 224-byte Initialized variant vs 16-byte
Pending variant. This is pre-existing and unrelated to the sync fix, but
clippy with --all-features -D warnings now flags it.

* docs: clarify re-request intent and batch dispatch semantics

Address review feedback:
- Clarify that re-requests go through P2C to avoid re-asking the
  NotFound peer (Zaki point C)
- Fix misleading doc comment on download_and_verify_batch — each block
  gets its own readiness check, not a single shared one (Copilot #1)

* refactor: remove dead source-aware routing code, keep behavioral fix

Remove BlocksByHashFrom, source_by_hash tracking, route_source(),
CONTIGUOUS_PREFIX_P2C, SOURCE_BLOCK_BATCH_LIMIT, and download_and_verify_batch.
These were unreachable under any default config (prefix 2000 > max batch size
in all modes).

Keep the three behavioral changes that produce the 0-error result:
- No-poison: stop registry poisoning on transient errors (client.rs)
- Re-request: re-queue NotFound blocks instead of dropping (sync.rs)
- Duplicate-tolerant dispatch: catch DuplicateBlockQueuedForDownload and
  continue instead of dropping the batch (sync.rs)

3 files, +128/-31 vs main.

---------

Co-authored-by: Gustavo Valverde <g.valverde02@gmail.com>
Co-authored-by: Zaki Manian <zaki@manian.org>
p0mvn added a commit that referenced this pull request Jun 19, 2026
PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.
p0mvn added a commit that referenced this pull request Jun 19, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jun 21, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jun 22, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jun 22, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jun 23, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jun 23, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jun 23, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
p0mvn added a commit that referenced this pull request Jul 2, 2026
…tic path (#136)

* proto(chain): defer Sapling value-commitment point decompression

PROTOTYPE for benchmarking lever #1. After the native-digest and dropped-reparse
changes, a flamegraph of the checkpoint heavy region attributes about 60% of CPU
to Sapling Jubjub point decompression (a field square root in
jubjub::AffinePoint::from_bytes), almost entirely the value commitment cv on
every spend and output. Checkpoint sync never uses cv as a point: it verifies no
signatures or proofs, and the note-commitment tree uses cm_u, not cv.

Store cv as its canonical 32-byte encoding and decompress lazily, only when a
consumer needs the point. Deserialization just copies the bytes, serialization
and the txid digest use them directly, and the binding-signature verification in
the semantic verifier decompresses on demand via ValueCommitment::commitment.
This mirrors what Orchard already does for rk, which is why Orchard decompression
is negligible in the profile.

Prototype caveat: ValueCommitment::commitment panics on a non-canonical encoding
rather than returning an error, and the not-small-order check now happens at the
point of use instead of at parse. Correct for checkpoint sync (block hashes are
trusted) and exercised by the unit tests, but the production version must make
the accessor fallible so the semantic and mempool paths reject a malformed point
cleanly instead of panicking.

* proto(chain): also defer Sapling ephemeral_key point decompression

Extends the lazy value-commitment prototype. With cv deferred, the profile showed
the remaining ~50% of heavy-region CPU is the other per-output Jubjub point, the
ephemeral_key, decompressed at parse. The validator only needs its bytes (txid
digest and serialization); the point is needed only for wallet trial-decryption.

Store ephemeral_key as its canonical 32-byte encoding and skip decompression at
deserialization, like cv. Same prototype caveat: the not-small-order consensus
check is deferred and must be re-added on the semantic and mempool paths in a
production version.

* proto(chain): validate lazy Sapling cv/epk consensus safety

The deferred not-small-order checks for cv and ephemeral_key are not actually
missing on the consensus path: librustzcash enforces them for every untrusted
transaction, which all go through to_librustzcash (CachedFfiTransaction::new) on
the semantic and mempool paths. cv is rejected at read (zcash_primitives
read_value_commitment uses from_bytes_not_small_order); epk is rejected at verify
(sapling-crypto verifier check_output uses epk.is_small_order). The checkpoint
verifier trusts block hashes and does not need them.

Add a regression test that constructs a v5 transaction with a small-order cv and
epk and asserts both the deferral (Zebra now deserializes it) and the safety net
(to_librustzcash rejects it), plus that the exact library detection functions
flag the point. Correct the type docs accordingly.

* perf(chain): enforce deferred Sapling cv/epk check on the semantic path

Hardens the lazy Sapling cv/ephemeral_key prototype into a safer design. The lazy
types keep point decompression off the checkpoint-sync hot path (the measured
~2.5x win), but the not-small-order consensus check is now re-enforced explicitly
by Zebra on the untrusted boundary instead of relying solely on librustzcash.

Add `Transaction::sapling_point_encodings_are_valid` (and the underlying
`ShieldedData::point_encodings_are_valid`, `ValueCommitment::is_valid_not_small_order`,
`EphemeralPublicKey::is_valid_not_small_order`), and call it from
`verify_v4_transaction` / `verify_v5_transaction`, returning
`TransactionError::SmallOrder` for a small-order or off-curve cv or epk. This runs
on the semantic verification path and the mempool, which process untrusted
transactions; the checkpoint verifier never calls it (it trusts block hashes), so
the checkpoint throughput is unchanged.

This restores a Zebra-side, auditable enforcement of the rule and makes the epk
check isolatedly testable (it runs independently of proof verification). Spend rk
is still validated at deserialization. Validated by
`sapling_point_encodings_check_rejects_bad_points` and the existing lazy-cv/epk
tests.

* fix(consensus): run the deferred Sapling cv/epk check before to_librustzcash

Adversarial review of the lazy Sapling change found one non-consensus issue: a
small-order or off-curve cv failed inside CachedFfiTransaction::new (mapped to
UnsupportedByNetworkUpgrade, mempool misbehavior score 0) before the explicit
SmallOrder check ran, so a peer spamming bad-cv transactions received a lighter
penalty than before the change (when it was a deserialization error).

Move the sapling_point_encodings_are_valid check into the verifier's early quick
checks, before the state lookups and the librustzcash conversion. Now a bad cv or
epk fails fast with TransactionError::SmallOrder (score 100), restoring the peer
penalty and making the check the primary, version-agnostic enforcer for v4, v5,
and v6. Remove the now-redundant per-version copies.

No consensus behavior change: the same transactions are accepted and rejected.
The review confirmed no path commits or relays a transaction with a bad point
without this check or checkpoint hash-trust, the commitment() panic is not
reachable in release (no non-test caller), and there is no DoS amplification.

* refactor(chain): make ValueCommitment::commitment fallible

Removes the latent panic in `ValueCommitment::commitment`, which is the only
caller-facing point that could decompress a deferred (unvalidated) value
commitment. It now returns `Option`, so a future caller must handle an invalid
encoding instead of getting a hidden panic, eliminating a possible DoS if the
helper were ever moved onto a production path.

`ShieldedData::binding_verification_key` (its only caller, used in tests) now
propagates the `Option`. No production code calls either; the consensus encoding
check happens on the semantic path via `sapling_point_encodings_are_valid`.

* test(consensus): end-to-end reject of a Sapling output with an invalid epk

Adds the missing end-to-end test for the deferred Sapling cv/epk check: it takes
a real Sapling-output transaction, corrupts the first output's ephemeral key to
an off-curve point, and runs it through the full transaction Verifier, asserting
TransactionError::SmallOrder. The state service is unreachable!, proving the
check fires in the early quick checks before any state lookup, and that the
rejection is the explicit SmallOrder error rather than a later proof failure.

This closes the last gap from the security review: the epk rejection is now
confirmed by execution through the live verifier, not only by the isolated check
and the librustzcash backstop.

* consensus equivalence tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant