Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
691d27c
Add the zakura-dynamic-ivk crate
czarcas7ic Oct 8, 2026
68efa4e
Drop later-PR integration prose from the dynamic-ivk README
czarcas7ic Oct 10, 2026
908e174
Treat a missing or unknown swap type as an unknown receipt
czarcas7ic Oct 10, 2026
129fd30
Zeroize the derived rivk before copying it into the FVK
czarcas7ic Oct 10, 2026
e9edece
Stop exposing the unbound memo from a recovered note
czarcas7ic Oct 10, 2026
6144c65
Drop the SQLite claim from the dynamic-ivk changelog
czarcas7ic Oct 10, 2026
b7b3f47
Reject publishable packages that depend on unpublishable ones
czarcas7ic Oct 10, 2026
f42480f
Make zakura-dynamic-ivk publishable
czarcas7ic Oct 10, 2026
c4fa00a
Keep exact-input refunds unknown without a refunded amount
czarcas7ic Oct 10, 2026
7cba471
Merge main into the dynamic-ivk crate
czarcas7ic Oct 11, 2026
d67a472
Normalize failed swaps by refund direction
czarcas7ic Oct 11, 2026
2808186
Expect no refund after an exact-output success reporting zero
czarcas7ic Oct 11, 2026
cfeafcc
Note that an incomplete deposit settles through REFUNDED
czarcas7ic Oct 11, 2026
c36ab7c
Avoid asserting an incomplete deposit always becomes REFUNDED
czarcas7ic Oct 11, 2026
f623926
Root the graph check at the hand-written members too
czarcas7ic Oct 11, 2026
7ffd40e
Note that the rivk PRF state is not wiped
czarcas7ic Oct 11, 2026
6b259b0
Say dynamic-key notes carry no memo the wallet reads
czarcas7ic Oct 11, 2026
4d1f81e
Ignore refund memos that do not decode as v1 records
czarcas7ic Oct 11, 2026
95c1559
Rename dynamic IVKs to one-time IVKs
czarcas7ic Oct 12, 2026
032a42c
Move NEAR status normalization out of the one-time IVK crate
czarcas7ic Oct 12, 2026
f0f1b14
Move swap conventions into a `swap` module
czarcas7ic Oct 12, 2026
ceb231a
Add the `zakura-near-intents` provider adapter crate
czarcas7ic Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ members = [
"zakura/pir-status",
"zakura/transaction-status",
"zakura/pir-transparent",
"zakura/dynamic-ivk",
]

resolver = "2"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ or `zakura/` and are listed in `layout.extra_members` in
`manifests/sources.toml`. Checked-in protobuf bindings have an explicit
regeneration command, separate from ordinary builds.

The [dynamic IVK](zakura/dynamic-ivk/README.md) crate provides shared
derivation of receiving keys that keep an account's spending authority with their
own incoming viewing keys, and refund memo helpers, with an Ironwood proof test for
mixed ordinary, refund, and incoming inputs.

## How the rewiring works

Nothing is patched at the source level. `manifests/sources.toml` holds a
Expand Down
2 changes: 1 addition & 1 deletion manifests/sources.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ vendored_directory = "librustzcash"
vendor_branch = "vendor/librustzcash"
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 👍 / 👎.

# The backend selector. Named separately because the verification scripts have
# to build it one backend at a time, unlike every other member.
facade = "wallet-lib"
Expand Down
26 changes: 25 additions & 1 deletion scripts/verify-zakura-graph.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,32 @@ missing = expected_packages - versions.keys()
for name in sorted(missing):
problems.append(f"{name}: vendored crate is not in the resolved graph")

# Packaging rewrites a path dependency to a registry requirement, so a
# publishable package cannot depend (other than for development) on a local
# `publish = false` package.
local = {
package["name"]: package
for package in packages.values()
if package["source"] is None
}
for package in local.values():
if package["publish"] == []:
continue
for dependency in package["dependencies"]:
target = local.get(dependency["name"])
if (
dependency.get("path")
and dependency["kind"] != "dev"
and target is not None
and target["publish"] == []
):
problems.append(
f"{package['name']}: publishable but depends on unpublishable "
f"{dependency['name']}"
)

if problems:
print("the resolved graph is not Zakura-only:", file=sys.stderr)
print("the resolved graph failed verification:", file=sys.stderr)
for problem in problems:
print(f" {problem}", file=sys.stderr)
raise SystemExit(1)
Expand Down
16 changes: 16 additions & 0 deletions zakura/dynamic-ivk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

## [Unreleased]

- Add dynamic IVK derivation (`KeyId::derive`): each key keeps the
account's `ak` and `nk` with its own `rivk`, identified by purpose and index.
Add the `RefundMemo` funding record, which carries only the refund index, and
its `REFUND_MEMO_MAGIC` prefix, with independent KDF vectors and an Ironwood
receive/reconstruct/spend proof test.
- Add the shared completion limits, `RESTORE_WATCH_SECS` (24 hours for restored
keys) and `COMPLETION_LIMIT_SECS` (30 days), and direction-aware NEAR
status normalization, `lifecycle::near_observation`, including refunded amounts
and exact-output leftovers. Scheduling and receipt accounting are left to the
wallet's store.
- Add transport-independent incoming-note authentication and commitment-path
validation for privately discovered payments.
30 changes: 30 additions & 0 deletions zakura/dynamic-ivk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "zakura-dynamic-ivk"
version = "0.0.1-rc0"
description = "Experimental dynamic IVKs: derived Ironwood receiving keys and refund memos"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
readme = "README.md"
categories.workspace = true
keywords = ["zcash", "wallet", "privacy"]

[dependencies]
blake2b_simd.workspace = true
ff.workspace = true
orchard.workspace = true
pasta_curves.workspace = true
zcash_protocol.workspace = true
zcash_note_encryption.workspace = true
zeroize.workspace = true

[dev-dependencies]
hex.workspace = true
incrementalmerkletree.workspace = true
orchard = { workspace = true, features = ["circuit"] }
rand = { workspace = true, features = ["thread_rng"] }
shardtree.workspace = true

[lints]
workspace = true
94 changes: 94 additions & 0 deletions zakura/dynamic-ivk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Dynamic IVKs

Implementation of the draft v1 dynamic IVK and refund-memo
conventions. A dynamic key keeps the account's spending authority (`ak` and `nk`)
with its 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. The derivation still requires cryptographic review before
issuing live addresses.

## Wallet integration

1. Reserve a purpose-specific index durably before exposing an address.
2. Call `KeyId::new(purpose, index).derive(account_external_fvk)` and select
`address_at(0u32, Scope::External)` for the receiver.
3. Register that FVK for scanning and retain its purpose/index with received notes.
4. For a refund, encode `RefundMemo` in an ordinary internal note in the funding
transaction, whose only transparent output pays the P2PKH or P2SH deposit.
Decode only after note authentication and verification that the transaction
was the wallet's own send, including zero-value and spent notes.
5. Reconstruct inputs with their derived FVKs. Use the account's spending key to
sign, and its ordinary internal key for change.

`has_same_spending_authority` compares the `ak` and `nk` of valid FVKs. It does not
replace recipient, nullifier, signature, value, or change validation. The wallet
owns reservation, persistence, coverage, lifecycle, and note selection. There is
no alternate balance store in this crate.

Software PCZT signing of dynamic-key notes works through the same builder.
Hardware signers need firmware qualification before dynamic keys are enabled for
hardware accounts.

## Derivation

A dynamic key keeps the account's `ak` and `nk` and replaces its external `rivk`:

```text
rivk' = ToScalar(PRF^expand_rivk([0x85] || ak || nk || [purpose] || I2LEOSP_64(index)))
```

`PRF^expand` is BLAKE2b-512 personalized with `Zcash_ExpandSeed` over
`rivk || input`, and `ToScalar` reduces its little-endian output modulo the Pallas
scalar order. `purpose` is 0 for refund and 1 for incoming. This is ZIP 32's
internal-key derivation (first byte `0x83`, no suffix) with a different first byte
and a 9-byte suffix. The input fits one BLAKE2b block, as the internal key's does,
so a ZIP 2005 recovery circuit could accept dynamic keys as one more `rivk` case
with purpose and index as private inputs. Network and pool identify stored keys
but are not derivation inputs.

`0x85` was unused by the protocol specification and ZIPs when chosen. Reserve it
with the ZIP editors before issuing live addresses. Parsing the result validates
both external and internal incoming viewing keys; with negligible probability an
index has no valid key, and derivation returns an error.

## Refund memo

| Offset | Bytes | Field |
|---|---:|---|
| 0 | 5 | `FF 5A 53 57 50` (`0xFF`, `ZSWP`) |
| 5 | 1 | Version `1` |
| 6 | 1 | Refund purpose `0` |
| 7 | 8 | Index, little-endian |
| 15 | 497 | Reserved: written as zero, ignored on decode |

The memo does not store the deposit address. A restored refund key needs none: it
is swept once and, if the provider was given its address in the last day, scans for
another 24 hours, with no provider lookups. Incoming indices are
recovered through lookahead, not this memo. `RefundMemo::decode` returns `None`
for any memo that is not a v1 record. A wallet that selects records by their
`REFUND_MEMO_MAGIC` prefix keeps one it cannot decode pending, without failing,
and refund issuance waits for it, since it may hold a refund index.

## Validation

From the workspace root:

```sh
python3 scripts/dev.py test --config default -p zakura-dynamic-ivk
```

Regenerate the KDF vectors from this directory with
`python3 tests/vectors/generate.py > tests/vectors/rivk.csv`.

## Protocol baseline

Zcash Protocol Specification **v2026.7.0-202-gafa086, NU6.3 proposal**, commit
`afa086bd976e316612a5c06fb139429958d07d84`:

- [§4.2.3, Key Components, pp.40–41](https://github.com/zcash/zips/blob/afa086bd976e316612a5c06fb139429958d07d84/protocol/protocol.tex#L5753-L5909),
PDF anchor `orchardkeycomponents`.
- [§5.6.4.4, Raw Full Viewing Keys, p.123](https://github.com/zcash/zips/blob/afa086bd976e316612a5c06fb139429958d07d84/protocol/protocol.tex#L13077-L13104),
PDF anchor `orchardfullviewingkeyencoding`.

The dynamic IVK KDF and memo format are proposed wallet conventions, separate from
these protocol requirements. Passing these tests is not a cryptographic review.
Loading
Loading