Skip to content

Add the zakura-dynamic-ivk crate - #120

Open
czarcas7ic wants to merge 16 commits into
mainfrom
adam/swap-receiving-keys
Open

czarcas7ic wants to merge 16 commits into
mainfrom
adam/swap-receiving-keys

Conversation

@czarcas7ic

@czarcas7ic czarcas7ic commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Dynamic IVKs are Ironwood receiving keys that keep an account's spending authority (ak and nk) with their own rivk, so each has its own incoming viewing key and address, and a wallet trial-decrypts with a changing set of them. Wallets use them for swap refund and incoming addresses that cannot be linked to the wallet or to each other.

The unpublished crate derives a key's rivk as ToScalar(PRF^expand_rivk([0x85] || ak || nk || [purpose] || index)), ZIP 32's internal-key construction with another domain byte and a suffix, so a ZIP 2005 recovery circuit could check it the same way. 0x85 must be reserved with the ZIP editors before live addresses.

It also holds the refund memo, REFUND_MEMO_MAGIC followed by the refund index, that a funding transaction carries on its internal Ironwood change; the shared completion limits and NEAR status normalization; and authentication of privately discovered notes. Tests include independent KDF vectors and an Ironwood receive/restore/spend proof test.

Tests: the library CI matrix (fmt, every scripts/dev.py lane and the three verify scripts) passes at the top of the stack, and the lanes this PR affects pass at this PR.

🤖 Generated with Claude Code

@czarcas7ic
czarcas7ic added this pull request to stack #125 October 7, 2026 21:59
@czarcas7ic
czarcas7ic force-pushed the adam/swap-receiving-keys branch 3 times, most recently from b9f6e9f to 8941c41 Compare October 8, 2026 21:58
@czarcas7ic czarcas7ic changed the title Add the zakura-swap-receiving crate Add the zakura-dynamic-ivk crate Oct 8, 2026
Dynamic IVKs are Ironwood receiving keys that keep an account's
spending authority (ak and nk) with their own rivk, so each has its
own incoming viewing key and address, and a wallet trial-decrypts
with a changing set of them. Wallets use them for swap refund and
incoming addresses that cannot be linked to the wallet or to each
other.

The unpublished crate derives a key's rivk as
ToScalar(PRF^expand_rivk([0x85] || ak || nk || [purpose] || index)),
ZIP 32's internal-key construction with another domain byte and a
suffix, so a ZIP 2005 recovery circuit could check it the same way.
0x85 must be reserved with the ZIP editors before live addresses.

It also holds the refund memo, REFUND_MEMO_MAGIC followed by the
refund index, that a funding transaction carries on its internal
Ironwood change; the shared completion limits and NEAR status
normalization; and authentication of privately discovered notes.
Tests include independent KDF vectors and an Ironwood
receive/restore/spend proof test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@czarcas7ic
czarcas7ic force-pushed the adam/swap-receiving-keys branch from 8941c41 to 691d27c Compare October 10, 2026 06:07
@czarcas7ic
czarcas7ic marked this pull request as ready for review October 10, 2026 06:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T07:16:14.308677Z 7ffd40e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 691d27c7ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zakura/dynamic-ivk/README.md Outdated
Comment on lines +28 to +31
The SQLite backend implements this contract with its `orchard` feature: durable
key registration, scanning, completion, and the restore sweeps that
`zakura-pir-receiver` runs. Its rustdoc, starting at
`zcash_client_sqlite::wallet::dynamic_ivk`, documents the calls a wallet makes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the nonexistent SQLite integration claim

At this commit, a repository-wide search finds no dynamic_ivk module, tests, or dependency under zakura-client-sqlite, and zakura-pir-receiver is also absent. Integrators therefore cannot resolve the documented rustdoc path, and the advertised durable registration, scanning, and restore flow does not exist; either include that integration or describe it as future work.

Useful? React with 👍 / 👎.

Comment thread zakura/dynamic-ivk/README.md Outdated

```sh
cargo test -p zakura-dynamic-ivk --locked
cargo test -p zakura-client-sqlite --features orchard,test-dependencies --locked dynamic_ivk

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace the empty SQLite test filter

At this commit, zakura-client-sqlite contains no test whose name matches dynamic_ivk, so this Cargo command selects zero tests and still exits successfully. It therefore gives a false green for the claimed SQLite integration; point it at an existing test or add the missing test and invoke it through the repository runner, which explicitly treats an empty filtered selection as failure.

AGENTS.md reference: AGENTS.md:L34-L36

Useful? React with 👍 / 👎.

Comment on lines +91 to +95
Some(RecoveredNote {
nullifier: note.nullifier(&key),
note,
memo,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the recovered memo to the chain ciphertext

For outputs using the documented public zero OVK, any discovery provider can recover the complete Note, re-encrypt that same committed note with an arbitrary memo, and return the forged ciphertext here. The commitment, action nullifier, ephemeral key, 52-byte compact prefix, Merkle path, and accepted root remain valid, so decrypt, matches_compact, and verify_position all accept while this field exposes the substituted memo as authenticated; require an independently obtained full on-chain ciphertext comparison or keep the memo explicitly untrusted.

Useful? React with 👍 / 👎.

Comment thread zakura/dynamic-ivk/src/lifecycle.rs Outdated
Comment on lines +82 to +85
("SUCCESS", Purpose::Refund) => Terminal(match refund {
Some(value) => Positive(Some(value)),
None if status.swap_type == Some("EXACT_OUTPUT") => Positive(None),
None => ReceiptExpectation::None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep missing swap types inconclusive

When a refund-key operation reaches SUCCESS with no positive refunded_amount, an absent, malformed, or newly introduced swap_type falls through to ReceiptExpectation::None. If the original quote was exact-output, unused ZEC can still be sent to this key, so treating missing type data as proof that no receipt exists can end scanning and leave the refund undiscovered; only a recognized non-exact-output type should select None, while unavailable or unrecognized types should remain Unknown.

Useful? React with 👍 / 👎.

Comment thread zakura/dynamic-ivk/src/keys.rs Outdated
Comment on lines +66 to +67
let rivk = expand_rivk(&bytes, RIVK_DOMAIN, &suffix);
bytes[64..].copy_from_slice(&rivk);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Zeroize the derived rivk copy

expand_rivk returns the derived viewing-key material into this ordinary stack array, which is not wiped after it is copied into the zeroizing FVK byte buffer. Thus each derivation leaves an avoidable extra rivk copy available to later stack reuse or process-memory inspection, despite the surrounding code explicitly zeroizing the account bytes and wide PRF output; wrap this result in Zeroizing or write it directly into zeroizing storage.

Useful? React with 👍 / 👎.

czarcas7ic and others added 4 commits October 10, 2026 00:48
The README described the SQLite integration and the receiver client,
neither of which exists in this crate's PR, and its validation section
named a SQLite test filter that selects nothing here. Keep only the
crate's own test command, run through the repository runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refund key's SUCCESS without a positive refunded amount mapped every
swap type other than EXACT_OUTPUT, including an absent or unrecognized
one, to "no Zcash receipt expected", which lets storage settle the key
early. Only a recognized EXACT_INPUT now means no receipt; a missing,
empty or unrecognized type is Unknown. A positive refunded amount stays
authoritative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The derived rivk was left in an ordinary array between the zeroizing
FVK and hash buffers around it. Wrap it in Zeroizing so this local is
cleared on drop, as its neighbours are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RecoveredNote returned the decrypted memo as authenticated, but nothing
binds it to the chain. Anyone who recovers the note, for example
through the public zero OVK, can re-encrypt the same note with another
memo; the action nullifier, cmx, ephemeral key and 52-byte compact
prefix are unchanged and decryption still succeeds. Drop the memo field
and accessor, keep authenticating the note itself, and correct the docs.

Add a regression test that recovers a public-zero-OVK output as a
provider would, substitutes the memo, and shows decrypt accepts both.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9edeceb22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zakura/dynamic-ivk/CHANGELOG.md Outdated
keys) and `COMPLETION_LIMIT_SECS` (30 days), and direction-aware NEAR
status normalization, `lifecycle::near_observation`, including refunded amounts
and exact-output leftovers. Durable scheduling and receipt accounting live in
SQLite.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the remaining nonexistent SQLite integration claim

Although the earlier README assertion was removed, this new changelog entry still tells consumers that durable scheduling and receipt accounting live in SQLite. A repo-wide search of this squashed commit finds no dynamic_ivk module, dependency, schema, or tests under librustzcash/zcash_client_sqlite, so the release note still advertises integration that cannot be used; either land that integration or describe it as future work.

AGENTS.md reference: AGENTS.md:L9-L12

Useful? React with 👍 / 👎.

czarcas7ic and others added 3 commits October 10, 2026 01:13
The changelog said durable scheduling and receipt accounting live in
SQLite, which this crate's PR does not contain. Say they are left to
the wallet's store instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Packaging rewrites a path dependency to a registry requirement, so a
publishable workspace package whose normal, build or optional dependency
is a local `publish = false` package cannot be released. Check that edge
in the graph verification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend depends on this crate, so it must be releasable before the
next backend release. Give it the zakura prerelease version and registry
metadata, and drop the "unpublished" wording from its docs. Nothing is
published here.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f42480f974

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zakura/dynamic-ivk/src/lifecycle.rs Outdated
("SUCCESS", Purpose::Refund) => Terminal(match (refund, status.swap_type) {
(Some(value), _) => Positive(Some(value)),
(None, Some("EXACT_OUTPUT")) => Positive(None),
(None, Some("EXACT_INPUT")) => ReceiptExpectation::None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep exact-input refunds inconclusive until receipt is known

When an exact-input swap was over-deposited but a SUCCESS response omits or cannot parse refundedAmount, this branch returns ReceiptExpectation::None. The 1Click quote contract states that excess tokens are refunded even for exact-input swaps, so wallet receipt accounting may stop scanning and miss a later ZEC refund. Keep this case Unknown unless the adapter can independently prove that no excess was deposited.

Useful? React with 👍 / 👎.

czarcas7ic and others added 2 commits October 10, 2026 12:47
1Click refunds excess deposits for exact-input swaps too, so a refund
key's exact-input SUCCESS proves no ZEC receipt only when the response
reports a zero refunded amount. When the amount is absent, the
expectation is now Unknown and the key keeps scanning. A positive
amount stays authoritative and exact-output handling is unchanged.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cba47147f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zakura/dynamic-ivk/src/lifecycle.rs Outdated
}),
("REFUNDED", Purpose::Refund) => Terminal(Positive(refund)),
("REFUNDED", Purpose::Receive) => Terminal(ReceiptExpectation::None),
("FAILED", _) => Terminal(Unknown),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize failed swaps according to refund direction

When a deposited operation reaches FAILED, 1Click specifies that the origin funds are automatically returned to the refund address (1Click API documentation). For Purpose::Refund this means a ZEC receipt is expected (and any positive refunded_amount should be preserved), while for Purpose::Receive the refund is on the non-ZEC origin chain and no ZEC receipt is expected. Collapsing both cases to Unknown discards this direction and amount information, causing completion logic to keep already-resolved operations pending until its timeout instead of reconciling the correct receipt.

Useful? React with 👍 / 👎.

1Click returns a FAILED swap's deposit to the refund address on the
origin chain, as it does for REFUNDED. near_observation reported FAILED
as Unknown for both purposes, which kept resolved operations pending
until the completion limit.

A refund key now expects a ZEC receipt after FAILED, carrying a
positive refunded amount when reported, and an incoming key expects
none, since its refund lands on the source chain.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d67a472920

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
("SUCCESS", Purpose::Refund) => Terminal(match (refund, status.swap_type) {
(Some(value), _) => Positive(Some(value)),
(None, Some("EXACT_OUTPUT")) => Positive(None),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor an explicit zero exact-output refund

When an EXACT_OUTPUT operation succeeds with refunded_amount == Some(Zatoshis::ZERO), this arm still returns Positive(None), so completion waits for a ZEC receipt that the provider explicitly reported as zero and can remain pending until the 30-day cap. This is distinct from the earlier missing-amount case: the fresh evidence is the parsed zero value, and the 1Click quote contract says exact-output excess is refunded only when the input exceeds maxAmountIn; distinguish None from Some(0) here and cover both cases.

AGENTS.md reference: AGENTS.md:L43-L46

Useful? React with 👍 / 👎.

An exact-output SUCCESS that reports a zero refunded amount was still
normalized to Positive(None), so completion waited for a ZEC refund the
provider had explicitly reported as zero and could stay pending until
the 30-day cap. Treat a reported zero like the exact-input case: no
receipt is expected. An absent amount still expects a refund of unknown
amount, and a positive amount stays authoritative.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 280818699b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let refund_reported = status.refunded_amount.is_some();
let outcome = match (status.status, purpose) {
("PENDING_DEPOSIT", _) => AwaitingDeposit,
("KNOWN_DEPOSIT_TX" | "INCOMPLETE_DEPOSIT" | "PROCESSING", _) => Active,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat incomplete deposits as terminal refunds

When an underpayment reaches INCOMPLETE_DEPOSIT, 1Click treats it as a terminal state and refunds the deposit; the Defuse settlement specification explicitly includes it among terminal states and says every non-success terminal case is refunded. Mapping it to Active here bypasses the direction-specific terminal handling: a ZEC-origin Refund key never records its expected refund, while a Receive key never records that its source-chain refund produces no ZEC receipt, leaving either operation pending and scanning until the completion cap. Normalize this status by purpose like the other refunded failure states.

Useful? React with 👍 / 👎.

czarcas7ic and others added 2 commits October 10, 2026 22:14
1Click reports INCOMPLETE_DEPOSIT for an underpaid deposit and later
moves the operation to REFUNDED once the deadline passes. That refund
status is already normalized by direction, so INCOMPLETE_DEPOSIT stays
Active until then (or until the completion limit). Say so at the arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1Click documents that an underpayment is refunded around the deadline,
but its status reference does not promise an INCOMPLETE_DEPOSIT to
REFUNDED transition. Say only that the status is not final and that the
completion limit bounds the wait.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c36ab7c97a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread manifests/sources.toml
upstream_manifest = "librustzcash/upstream-workspace.toml"
# `zakura/` members are added here as they are written.
extra_members = ["wallet-lib", "zakura/pir-enhance", "zakura/pir-enhance-types", "zakura/pir-native", "zakura/pir-status", "zakura/transaction-status", "zakura/pir-transparent"]
extra_members = ["wallet-lib", "zakura/pir-enhance", "zakura/pir-enhance-types", "zakura/pir-native", "zakura/pir-status", "zakura/transaction-status", "zakura/pir-transparent", "zakura/dynamic-ivk"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include extra members in graph verification

Adding this crate only to layout.extra_members leaves it outside the graph verifier's reachability roots: verify-zakura-graph.sh seeds its queue solely from manifest["crate"], and a repo-wide dependency search confirms no vendored crate reaches zakura-dynamic-ivk. Consequently, forbidden crates or duplicate Zakura versions introduced through this new dependency-family boundary would still pass the repository's advertised graph check; include applicable extra members as verification roots while continuing to exclude the dual-family facade.

AGENTS.md reference: AGENTS.md:L43-L46

Useful? React with 👍 / 👎.

Comment on lines +103 to +107
.to_state()
.update(&fvk[64..])
.update(&[domain])
.update(&fvk[..64])
.update(suffix)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wipe the PRF state that copies the account FVK

Whenever a key is derived, these updates total 106 bytes, below BLAKE2b's 128-byte block size, so the temporary blake2b_simd::State retains rivk || domain || ak || nk || suffix in its internal buffer and drops it without wiping; wrapping only the copied digest in Zeroizing does not clear that account-FVK copy. The fresh evidence after the earlier returned-rivk fix is this separate non-zeroizing hasher buffer (upstream State layout); use a PRF implementation or explicitly controlled state that guarantees zeroization so later stack reuse or process-memory inspection cannot recover the viewing material.

Useful? React with 👍 / 👎.

The graph verifier seeded its walk from the vendored crates alone, so a
dependency reachable only from a `layout.extra_members` crate such as
`zakura-dynamic-ivk` escaped the forbidden-crate and duplicate-version
checks. Seed it from those members as well, still leaving out the
dual-backend facade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`blake2b_simd::State` offers no zeroization, so wrapping the PRF output
in `Zeroizing` is hygiene for the values this code owns rather than
erasure of every temporary. Say so at the call site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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