Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
15cde0c
Add dynamic IVK traits to the backend
czarcas7ic Oct 8, 2026
72aefc3
Merge the dynamic-ivk fixes
czarcas7ic Oct 10, 2026
2c821b7
Require stores to select dynamic transaction keys
czarcas7ic Oct 10, 2026
ad4c13f
Stop promising the recovered memo is committed with the note
czarcas7ic Oct 10, 2026
4057ecf
Drop the receiver-client reference from the backend dynamic IVK docs
czarcas7ic Oct 10, 2026
e2f4a0b
Merge the dynamic-ivk changelog fix
czarcas7ic Oct 10, 2026
c3d14b2
Watch every swept key instead of closing on a recent-set miss
czarcas7ic Oct 10, 2026
c15cc16
Record a provider seen hit with the sweep attempt lease
czarcas7ic Oct 10, 2026
010c7d9
Merge the publishable dynamic-ivk crate into the backend
czarcas7ic Oct 10, 2026
06c9a2e
Look every restore sweep attempt up in the current publication
czarcas7ic Oct 10, 2026
cc33c49
Merge the exact-input refund fix
czarcas7ic Oct 10, 2026
4d2ae9c
Merge main into the backend dynamic IVK traits
czarcas7ic Oct 11, 2026
c886999
Merge the failed-swap direction fix
czarcas7ic Oct 11, 2026
f477106
Require pruning to keep the Ironwood retention floor
czarcas7ic Oct 11, 2026
359320b
Merge the zero exact-output refund fix
czarcas7ic Oct 11, 2026
a55867b
Merge the incomplete-deposit note
czarcas7ic Oct 11, 2026
8436eca
Merge the incomplete-deposit wording fix
czarcas7ic Oct 11, 2026
4f43fa2
Merge the graph root and PRF note
czarcas7ic Oct 11, 2026
34728d8
Carry the dynamic key snapshot on decrypted transactions
czarcas7ic Oct 11, 2026
daccd13
Merge the dynamic-key memo note
czarcas7ic Oct 11, 2026
d7547d1
Close non-recent swept keys and recheck them by the paid filter
czarcas7ic Oct 11, 2026
f181c49
Merge the dynamic-ivk simplifications
czarcas7ic Oct 11, 2026
4f9093b
Mark dynamic-key decryption with a flag again
czarcas7ic Oct 11, 2026
5dcb449
Drop PaymentApplication::BeforeBirthday from the public enum
czarcas7ic Oct 11, 2026
6de6cef
Let the paid-filter recheck cover every closed unpaid key
czarcas7ic Oct 11, 2026
364a555
Merge the one-time IVK crate rename
czarcas7ic Oct 12, 2026
72f9e48
Rename dynamic IVKs to one-time IVKs in the backend
czarcas7ic Oct 12, 2026
12f3252
Merge the one-time IVK crate restructure
czarcas7ic Oct 12, 2026
13bc108
Split the one-time IVK restore protocol into its own traits
czarcas7ic Oct 12, 2026
aa31617
Sort the one-time IVK entry in Cargo.lock
czarcas7ic Oct 12, 2026
d3ffed3
Gate one-time IVKs behind a `one-time-ivk` feature
czarcas7ic Oct 12, 2026
d3c3640
Add a `one-time-ivk` test lane for the backend
czarcas7ic Oct 12, 2026
2bf6050
Merge the NEAR Intents 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
1 change: 1 addition & 0 deletions Cargo.lock

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

18 changes: 18 additions & 0 deletions librustzcash/zcash_client_backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ workspace.
alone contributed, so a publication's facts do not outlive the policy that admitted them.
This is a new required trait method: external implementors of `TransparentLedgerWrite` must
add it.
- Dynamic IVKs (with `orchard`): Ironwood receiving keys that keep an account's spending
authority with their own incoming viewing keys (see `zakura-dynamic-ivk`), which the
wallet trial-decrypts as a changing set. `scanning::dynamic_ivk::DynamicScanningKey` is
one such key, and received notes and outputs report theirs through `dynamic_key_id`. A
store implements `data_api::dynamic_ivk::{DynamicIvkRead, DynamicIvkWrite}`.
`chain::scan_cached_blocks_with_dynamic_ivks` also trial-decrypts every key from
`get_dynamic_scanning_keys` and passes that snapshot to `put_blocks_with_dynamic_ivks`,
so a store can rescan blocks that a key activated mid-batch missed, and
`wallet::decrypt_and_store_transaction_with_dynamic_ivks` also decrypts with
`get_dynamic_transaction_keys`, which `DecryptedTransaction::dynamic_ivks_applied`
records, so a store can refuse transactions decrypted without them. The traits' restore
sweep steps look keys recovered from the seed up in a receiver directory; a step that
must wait returns a `SweepDeferral`. Transaction construction spends a dynamic-key note
with its derived viewing key and sends change to the ordinary account key.
- `transparent_ledger::{TransparentDetailRead, TransparentDetailWrite}` (prototype): durable
transparent txid enhancement work (`transparent_detail_work`, returning the requests with the
mode and policy generation of the same snapshot as `TransparentDetailWork`;
Expand Down Expand Up @@ -50,6 +64,10 @@ workspace.
- `TransactionHistoryDetails::inferred_outgoing` documents the Activity default that the account
funded a recovered send and its fee unless recovered evidence identifies another contributor.
This display assumption does not establish payment or fee attribution.
- `ll::wallet::put_blocks` and `ll::wallet::put_blocks_rows` take
`ironwood_nullifier_retention` (with `orchard`): a height from which Ironwood nullifiers
stay tracked, for a store that finds Ironwood notes after scanning past them. Pass `None`
to keep the previous behaviour.
- `enhance_pir::storage::validate_and_apply_records` captures the stored-metadata snapshot for
has-transparent records too, so storage can compare it before keeping such a record's
authenticated memo and agreeing fee without public authority. A `PrivateDetailsUnsupported`
Expand Down
6 changes: 4 additions & 2 deletions librustzcash/zcash_client_backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
zakura-pir-enhance-types = { version = "=0.0.1-rc0", path = "../../zakura/pir-enhance-types" }
zakura-dynamic-ivk = { version = "0.0.0", path = "../../zakura/dynamic-ivk", optional = true }

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 Publish the dynamic-IVK dependency before adding it

When zakura-client-backend is packaged or published, Cargo replaces this path dependency with the registry requirement zakura-dynamic-ivk = "0.0.0"; however, that crate's manifest declares publish = false, so the required registry package cannot exist. This blocks the next backend release and would leave published consumers unable to resolve the orchard feature. Publish the dependency at a real version before the backend, or avoid exposing it from this publishable crate.

Useful? React with 👍 / 👎.

zcash_address.workspace = true
zcash_encoding.workspace = true
zcash_keys = { workspace = true, features = ["std", "sapling"] }
Expand Down Expand Up @@ -206,8 +207,9 @@ spend-index = ["transparent-inputs"]
## The `orchard` crate itself is an unconditional dependency (via
## `zcash_primitives`, which always requires it because every v5+ transaction
## carries an Orchard bundle type); this feature gates the wallet's
## Orchard note-handling code, not the crate linkage.
orchard = ["dep:pasta_curves", "zcash_keys/orchard"]
## Orchard note-handling code, not the crate linkage. It also enables dynamic
## IVKs, derived Ironwood receiving keys (see `data_api::dynamic_ivk`).
orchard = ["dep:pasta_curves", "dep:zakura-dynamic-ivk", "zcash_keys/orchard"]

## Enables compatibility with legacy zcashd wallet data
zcashd-compat = ["zcash_keys/zcashd-compat"]
Expand Down
39 changes: 39 additions & 0 deletions librustzcash/zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
//! [`propose_shielding`]: crate::data_api::wallet::propose_shielding
//! [`pczt`]: https://docs.rs/pczt

#[cfg(feature = "orchard")]
use crate::scanning::dynamic_ivk::DynamicScanningKey;

use nonempty::NonEmpty;
use secrecy::SecretVec;
use std::{
Expand Down Expand Up @@ -106,6 +109,8 @@ use crate::{
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
};

#[cfg(feature = "orchard")]
pub mod dynamic_ivk;
pub mod enhance_pir;
pub mod status;
pub mod transparent_ledger;
Expand Down Expand Up @@ -2976,6 +2981,30 @@ pub struct DecryptedTransaction<'a, Tx: DecryptableTransaction<AccountId>, Accou
orchard_outputs: Vec<Tx::DecryptedOrchardOutput>,
#[cfg(feature = "orchard")]
ironwood_outputs: Vec<Tx::DecryptedOrchardOutput>,
#[cfg(feature = "orchard")]
dynamic_ivks_applied: bool,
}

#[cfg(feature = "orchard")]
impl<AccountId: Copy + Eq> DecryptedTransaction<'_, Transaction, AccountId> {
/// Adds authenticated incoming outputs from the wallet's dynamic keys.
/// A self-payment may already have been recovered with the account OVK.
/// In that case, retain one incoming record carrying its receiving-key identity.
pub fn with_dynamic_ivks(
mut self,
keys: impl IntoIterator<Item = DynamicScanningKey<AccountId>>,
) -> Self {
for key in keys {
for output in key.decrypt_outputs(self.tx) {
self.ironwood_outputs.retain(|existing| {
existing.index() != output.index() || existing.account() != output.account()
});
self.ironwood_outputs.push(output);
}
}
self.dynamic_ivks_applied = true;
self
}
}

impl<'a, Tx: DecryptableTransaction<AccountId>, AccountId> DecryptedTransaction<'a, Tx, AccountId> {
Expand All @@ -2998,6 +3027,8 @@ impl<'a, Tx: DecryptableTransaction<AccountId>, AccountId> DecryptedTransaction<
orchard_outputs,
#[cfg(feature = "orchard")]
ironwood_outputs,
#[cfg(feature = "orchard")]
dynamic_ivks_applied: false,
}
}

Expand Down Expand Up @@ -3027,6 +3058,14 @@ impl<'a, Tx: DecryptableTransaction<AccountId>, AccountId> DecryptedTransaction<
&self.ironwood_outputs
}

/// Whether the wallet's dynamic keys were tried, through
/// [`DecryptedTransaction::with_dynamic_ivks`]. While a dynamic key is open, a
/// store may refuse a transaction decrypted without them.
#[cfg(feature = "orchard")]
pub fn dynamic_ivks_applied(&self) -> bool {
self.dynamic_ivks_applied
}

/// Returns whether the transaction has decrypted outputs
pub fn has_decrypted_outputs(&self) -> bool {
let has_sapling = !self.sapling_outputs.is_empty();
Expand Down
104 changes: 97 additions & 7 deletions librustzcash/zcash_client_backend/src/data_api/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ use zcash_primitives::block::BlockHash;
use zcash_protocol::consensus::{self, BlockHeight};

use crate::{
data_api::WalletWrite,
data_api::{ScannedBlock, WalletWrite},
proto::compact_formats::CompactBlock,
scanning::{
Nullifiers, ScanningKeys,
Expand All @@ -172,6 +172,9 @@ use {
super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait,
};

#[cfg(feature = "orchard")]
use super::dynamic_ivk::DynamicIvkWrite;

pub mod error;
use error::Error;

Expand Down Expand Up @@ -639,7 +642,97 @@ where
.get_unified_full_viewing_keys()
.map_err(Error::Wallet)?;
let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks);
let mut runners = BatchRunners::<_, (), (), ()>::for_keys(100, &scanning_keys);
let (scan_summary, scanned_blocks) = scan_blocks(
params,
block_source,
data_db,
from_height,
limit,
&scanning_keys,
)?;

data_db
.put_blocks(from_state, scanned_blocks)
.map_err(Error::Wallet)?;
Ok(scan_summary)
}

/// [`scan_cached_blocks`], also trial-decrypting with the wallet's dynamic keys.
///
/// The keys come from [`DynamicIvkRead::get_dynamic_scanning_keys`], and the blocks are
/// stored with [`DynamicIvkWrite::put_blocks_with_dynamic_ivks`]. A wallet with open
/// dynamic keys must be scanned with this function rather than [`scan_cached_blocks`].
///
/// [`DynamicIvkRead::get_dynamic_scanning_keys`]: super::dynamic_ivk::DynamicIvkRead::get_dynamic_scanning_keys
#[cfg(feature = "orchard")]
#[tracing::instrument(skip(params, block_source, data_db, from_state))]
#[allow(clippy::type_complexity)]
pub fn scan_cached_blocks_with_dynamic_ivks<ParamsT, DbT, BlockSourceT>(
params: &ParamsT,
block_source: &BlockSourceT,
data_db: &mut DbT,
from_height: BlockHeight,
from_state: &ChainState,
limit: usize,
) -> Result<ScanSummary, Error<<DbT as WalletRead>::Error, BlockSourceT::Error>>
where
ParamsT: consensus::Parameters + Send + 'static,
BlockSourceT: BlockSource,
DbT: DynamicIvkWrite,
<DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
{
assert_eq!(from_height, from_state.block_height + 1);

let account_ufvks = data_db
.get_unified_full_viewing_keys()
.map_err(Error::Wallet)?;
let dynamic_keys = data_db.get_dynamic_scanning_keys().map_err(Error::Wallet)?;
let dynamic_key_ids = dynamic_keys
.iter()
.map(|key| (*key.account_id(), key.key_id()))
.collect::<Vec<_>>();
let scanning_keys =
ScanningKeys::from_account_ufvks(account_ufvks).with_dynamic_ivks(dynamic_keys);
let (scan_summary, scanned_blocks) = scan_blocks(

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 Report dynamic Ironwood receipts in the scan summary

When this new scanner detects a dynamic-IVK payment, the output necessarily belongs to the Ironwood pool, but scan_blocks only increments the Sapling and Orchard counters and ScanSummary exposes no Ironwood counters. Consequently a batch can successfully persist one or more dynamic payments while the returned summary reports zero received notes, so callers using it for receipt notifications or sync metrics miss every dynamic payment. Extend the summary and accumulator to count Ironwood spends and outputs.

Useful? React with 👍 / 👎.

params,
block_source,
data_db,
from_height,
limit,
&scanning_keys,
)?;

data_db
.put_blocks_with_dynamic_ivks(from_state, scanned_blocks, &dynamic_key_ids)
.map_err(Error::Wallet)?;
Ok(scan_summary)
}

/// Scans at most `limit` blocks from `from_height` with `scanning_keys`, for
/// [`scan_cached_blocks`] and its dynamic IVK twin, returning the blocks to store.
#[allow(clippy::type_complexity)]
fn scan_blocks<ParamsT, DbT, BlockSourceT, IvkTag>(
params: &ParamsT,
block_source: &BlockSourceT,
data_db: &DbT,
from_height: BlockHeight,
limit: usize,
scanning_keys: &ScanningKeys<<DbT as WalletRead>::AccountId, IvkTag>,
) -> Result<
(
ScanSummary,
Vec<ScannedBlock<<DbT as WalletRead>::AccountId>>,
),
Error<<DbT as WalletRead>::Error, BlockSourceT::Error>,
>
where
ParamsT: consensus::Parameters + Send + 'static,
BlockSourceT: BlockSource,
DbT: WalletRead,
<DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
{
let mut runners = BatchRunners::<_, (), (), ()>::for_keys(100, scanning_keys);

block_source.with_blocks::<_, <DbT as WalletRead>::Error>(
Some(from_height),
Expand Down Expand Up @@ -669,7 +762,7 @@ where
let scanned_block = scan_block_with_runners::<_, _, _, (), (), ()>(
params,
block,
&scanning_keys,
scanning_keys,
&nullifiers,
prior_block_metadata.as_ref(),
Some(&mut runners),
Expand All @@ -694,10 +787,7 @@ where
},
)?;

data_db
.put_blocks(from_state, scanned_blocks)
.map_err(Error::Wallet)?;
Ok(scan_summary)
Ok((scan_summary, scanned_blocks))
}

#[cfg(feature = "test-dependencies")]
Expand Down
Loading
Loading