Skip to content

fix: validate Zcash unified addresses per ZIP-316 (#749) - #999

Open
Anirudh-712 wants to merge 3 commits into
defuse-protocol:mainfrom
Anirudh-712:fix/zcash-ua-zip316
Open

Anirudh-712 wants to merge 3 commits into
defuse-protocol:mainfrom
Anirudh-712:fix/zcash-ua-zip316

Conversation

@Anirudh-712

Copy link
Copy Markdown

Closes #749.

validateZcashAddress accepts any u1… string that decodes as Bech32m with HRP u (the 128-character rule from #748 was reverted in #756). That lets through strings that are not valid unified addresses. Each of the first eight below is a correct Bech32m string with one rule broken, from ZIP-316 or the CompactSize encoding it specifies, and main accepts all eight:

Invalid address main This PR
transparent receiver only (69 characters) accepted rejected
wrong HRP padding accepted rejected
both P2PKH and P2SH accepted rejected
items out of order accepted rejected
wrong Orchard receiver length accepted rejected
MUST-understand metadata item (0xE0), which Revision 0 forbids accepted rejected
typecode in non-canonical CompactSize form accepted rejected
typecode above 0x02000000 accepted rejected
testnet HRP (utest) rejected rejected
corrupted checksum rejected rejected

This PR decodes the whole address in addressValidation/zcash.ts, next to the Cardano validator: Bech32m without the 90-character limit, F4Jumble⁻¹, the 16-byte HRP padding check, then the typecode-length items. Typecodes and lengths are read as canonical CompactSize of at most 0x02000000, as librustzcash does. Typecodes must strictly increase, known receivers must have their exact lengths, and an address can't hold both P2PKH and P2SH. As ZIP-316 Revision 0 requires, it needs a Sapling or Orchard receiver and rejects MUST-understand metadata (0xE0–0xFC); other unknown typecodes are ignored, as the spec says. No new dependencies: it uses @scure/base and @noble/hashes, already in package.json.

Tests:

  • addressValidation/zcash.test.ts:
    • five official addresses from zcash-test-vectors (unified_address.json: Orchard only, Sapling only, P2PKH + Orchard, all three, Orchard + an unknown typecode);
    • one valid address with an ignorable metadata item (0xC0);
    • the eight invalid addresses above, and five strings that are not mainnet unified addresses (testnet, bad checksum, the 78-character example already in validateAddress.test.ts, a t1 address, the empty string);
    • the constructed addresses were built with an independent F4Jumble and Bech32m implementation that rebuilds all 60 official mainnet vectors byte for byte from their raw receivers; the bad-checksum one is vector Staging #27 with its last character changed.
  • Two new cases in validateAddress.test.ts: an official Orchard-only address passes, and the 69-character transparent-only one fails.
  • Run locally with vitest 3.2.4, @scure/base 1.2.6 and @noble/hashes 1.8.0 (the versions in bun.lock): all pass, and all 60 official mainnet vectors are accepted. Strict tsc is clean on the new module and its tests, and biome check (1.9.3) is clean on all four changed files. I could not run the whole app's suite locally, so CI is the check for validateAddress.test.ts.

One note for later: a valid unified address is not always payable by every route. A Sapling-only address has no Orchard or transparent receiver, for example. That is a route check, not part of this PR.

🤖 Generated with Claude Code

Anirudh-712 and others added 2 commits October 18, 2026 21:52
A u1 address passed when it merely decoded as Bech32m with HRP "u", so a
well-formed string that is not a valid unified address was accepted
(transparent-only, wrong padding, P2PKH with P2SH, items out of order,
wrong receiver length, MUST-understand metadata, a non-canonical or
oversized CompactSize). Decode it fully instead: Bech32m without the
90-character limit, F4Jumble⁻¹, the 16-byte HRP padding, then the
typecode-length items, read as canonical CompactSize of at most
0x02000000 as librustzcash does. Require a Sapling or Orchard receiver,
as ZIP-316 Revision 0 does, and reject MUST-understand metadata
(0xE0 to 0xFC), which Revision 0 forbids; other unknown typecodes are
ignored.

No new dependencies: @scure/base and @noble/hashes are already used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 18, 2026

Copy link
Copy Markdown

@Anirudh-712 is attempting to deploy a commit to the Defuse Labs Limed Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: be7abf55-1adc-43ff-88ff-ab015b262f55
📥 Commits

Reviewing files that changed from the base of the PR and between cf32c04 and c7915cd.

📒 Files selected for processing: 1
  • src/components/DefuseSDK/utils/addressValidation/zcash.ts
🚧 Files skipped from review as they are similar to previous changes: 1
  • src/components/DefuseSDK/utils/addressValidation/zcash.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The change adds a ZIP-316 Revision 0 validator for mainnet Zcash unified addresses. The existing Zcash address validation path now delegates u1 checks to it. Tests cover valid addresses and invalid formats and receiver combinations.

Changes

Zcash Unified Address Validation

Layer / File(s) Summary
ZIP-316 decoding primitives
src/components/DefuseSDK/utils/addressValidation/zcash.ts
Adds format constants, F4Jumble inverse support, and input size checks for decoding unified-address payloads.
Unified-address receiver validation
src/components/DefuseSDK/utils/addressValidation/zcash.ts, src/components/DefuseSDK/utils/addressValidation/zcash.test.ts
Validates Bech32m decoding, HRP padding, CompactSize values, receiver ordering and lengths, metadata, and required receiver types. Tests cover valid vectors and invalid addresses.
Zcash address-check integration
src/components/DefuseSDK/utils/validateAddress.ts, src/components/DefuseSDK/utils/validateAddress.test.ts
Routes u1 checks through the new validator, documents Unified addresses as supported, and tests Orchard-only acceptance and transparent-only rejection.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant validateAddress
  participant validateZcashUnifiedAddress
  participant Bech32m
  participant F4JumbleInverse
  validateAddress->>validateZcashUnifiedAddress: validate u1 address
  validateZcashUnifiedAddress->>Bech32m: decode address
  Bech32m-->>validateZcashUnifiedAddress: HRP and payload
  validateZcashUnifiedAddress->>F4JumbleInverse: reverse payload jumbling
  F4JumbleInverse-->>validateZcashUnifiedAddress: decoded bytes
  validateZcashUnifiedAddress-->>validateAddress: validation result
Loading
Merge Risk: ⚪ Minimal · up to c7915

No remaining issue is identified that would block merging the unified-address validation change after normal checks.

Security architecture risk: 🔵 Low · up to cf32c

The change strengthens Zcash address checks without adding transaction authority or a new service path. Residual uncertainty concerns the largest inputs handled by the new client-side decoder, rather than an identified validation bypass.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is Zcash recipient validation in the existing client withdrawal flow. Validated recipients continue through existing quote and withdrawal fields; this PR does not introduce a new service endpoint, privilege, tenant scope, or destination class.

Trust Boundaries and Controls

  • observed — Recipient text reaches the existing chain-specific client validator. The added checks establish address structure, not recipient ownership or route-specific receiver support. Those downstream guarantees are not established by a true result from this utility.

Resilience and Maintainability Implications

  • observed — The new inverse-F4Jumble work is synchronous and bounded to decoded inputs of 38–4,194,368 bytes. The size check occurs after Bech32m decoding, and hashing occurs before padding or receiver rejection. This adds local processing cost, but the inspected evidence does not establish a remotely exploitable availability failure.
Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: validating Zcash unified addresses according to ZIP-316.
Description check Passed The description is directly related to the changeset and explains the validation rules, tests, and scope.
Linked Issues check Passed The PR satisfies the coding requirement in #749. validateZcashAddress now delegates u1 addresses to validateZcashUnifiedAddress instead of applying the old broad Bech32m acceptance. The new vali…
Out of Scope Changes check Passed The changes stay within #749. Full ZIP-316 validation is directly related because removing the old length restriction must not accept arbitrary u1 Bech32m strings. The decoder, validation rules, int…
Docstring Coverage Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 4 files.
Full details: Linked Issues check

Explanation

The PR satisfies the coding requirement in #749. validateZcashAddress now delegates u1 addresses to validateZcashUnifiedAddress instead of applying the old broad Bech32m acceptance. The new validator supports the ZIP-316 payload range, including valid 69-character unified addresses, and checks the required mainnet Unified Address rules. Tests cover an official Orchard-only address that passes and the 69-character transparent-only address that fails. The tests also cover malformed and non-mainnet inputs.

Full details: Out of Scope Changes check

Explanation

The changes stay within #749. Full ZIP-316 validation is directly related because removing the old length restriction must not accept arbitrary u1 Bech32m strings. The decoder, validation rules, integration change, and tests support that objective. The PR does not change route-specific payability.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 Biome (2.5.14)
src/components/DefuseSDK/utils/addressValidation/zcash.ts

Biome could not lint this file: configuration resulted in errors. Check the repository's Biome configuration and plugins.

A rabbit checks the letters in a row,
For Zcash paths where unified addresses go.
It hops through jumble, bytes, and type,
Rejecting forms that fail the ZIP-316 type.
Valid receivers pass the test,
And settle in their burrow, safe to rest.

Comment @coderabbitai help to get the list of available commands.

Docstrings for xor, f4H and f4G, worded after ZIP-316's definitions of H_i and G_i (CodeRabbit's docstring-coverage check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Fix Zcash unified address validation minimum length

1 participant