diff --git a/.changelog/unreleased/improvements/4785-bridge-tree-shielded-wallet.md b/.changelog/unreleased/improvements/4785-bridge-tree-shielded-wallet.md new file mode 100644 index 00000000000..7ff4599820a --- /dev/null +++ b/.changelog/unreleased/improvements/4785-bridge-tree-shielded-wallet.md @@ -0,0 +1,2 @@ +- Optimize shielded sync and shielded wallet layout. + ([\#4785](https://github.com/anoma/namada/pull/4785)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a9edbf123d5..43cfc0c607e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -796,9 +796,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ "borsh-derive", "cfg_aliases", @@ -806,9 +806,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" dependencies = [ "once_cell", "proc-macro-crate", @@ -977,7 +977,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2130,7 +2130,7 @@ dependencies = [ "chrono", "rust_decimal", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "winnow 0.6.26", ] @@ -2327,7 +2327,7 @@ dependencies = [ "byteorder", "heapless 0.8.0", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -3058,7 +3058,7 @@ dependencies = [ "rand_core", "serde", "serdect", - "thiserror 2.0.11", + "thiserror 2.0.12", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -4671,15 +4671,6 @@ dependencies = [ "serde", ] -[[package]] -name = "init-once" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0863329819ed5ecf33446da6cb9104d2f8943ff8530d2b6c51adbc6be4f0632" -dependencies = [ - "portable-atomic", -] - [[package]] name = "inout" version = "0.1.3" @@ -4935,6 +4926,18 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leanbridgetree" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c255f8e570ab4acef90db79d99171bd5882615fb54d24591040fa69d80943e09" +dependencies = [ + "borsh", + "incrementalmerkletree", + "slab", + "thiserror 2.0.12", +] + [[package]] name = "leb128" version = "0.2.5" @@ -5504,7 +5507,7 @@ dependencies = [ "nam-ledger-proto", "once_cell", "strum 0.26.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "tracing-subscriber", @@ -5539,7 +5542,7 @@ dependencies = [ "displaydoc", "encdec", "num_enum", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5566,7 +5569,7 @@ dependencies = [ "pasta_curves", "rand_core", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -5674,6 +5677,7 @@ dependencies = [ "futures", "itertools 0.14.0", "kdam", + "konst", "lazy_static", "ledger-transport", "ledger-transport-hid", @@ -5703,7 +5707,7 @@ dependencies = [ "tendermint-config", "tendermint-rpc", "textwrap-macros", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "toml", "tracing", @@ -5742,7 +5746,7 @@ version = "0.251.0" dependencies = [ "namada_core", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5788,7 +5792,7 @@ dependencies = [ "smooth-operator", "tendermint 0.40.3", "tendermint-proto 0.40.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-keccak", "tokio", "toml", @@ -5849,7 +5853,7 @@ dependencies = [ "rand", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "toml", "tracing", ] @@ -5865,7 +5869,7 @@ dependencies = [ "namada_migrations", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5924,7 +5928,7 @@ dependencies = [ "namada_migrations", "proptest", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5956,7 +5960,7 @@ dependencies = [ "serde", "serde_json", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6001,7 +6005,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6013,7 +6017,7 @@ dependencies = [ "kdam", "namada_core", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -6058,7 +6062,7 @@ dependencies = [ "namada_migrations", "proptest", "prost 0.13.5", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6126,7 +6130,7 @@ dependencies = [ "tar", "tempfile", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-test", "toml", @@ -6149,7 +6153,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6182,7 +6186,7 @@ dependencies = [ "serde", "smooth-operator", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "tracing-subscriber", "yansi 1.0.1", @@ -6216,7 +6220,6 @@ dependencies = [ "fd-lock", "futures", "getrandom 0.3.1", - "init-once", "itertools 0.14.0", "lazy_static", "linkme", @@ -6262,7 +6265,7 @@ dependencies = [ "smooth-operator", "tempfile", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "tokio", "toml", @@ -6280,8 +6283,10 @@ dependencies = [ "eyre", "flume 0.11.1", "futures", + "group", "itertools 0.14.0", "lazy_static", + "leanbridgetree", "linkme", "masp_primitives", "masp_proofs", @@ -6316,7 +6321,7 @@ dependencies = [ "tempfile", "tendermint-rpc", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "typed-builder", @@ -6349,7 +6354,7 @@ dependencies = [ "proptest", "smooth-operator", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6369,7 +6374,7 @@ dependencies = [ "regex", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6495,7 +6500,7 @@ dependencies = [ "namada_vp", "namada_vp_env", "proptest", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6529,7 +6534,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "tonic-build", ] @@ -6590,7 +6595,7 @@ dependencies = [ "smooth-operator", "tempfile", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "wasmer", "wasmer-cache", @@ -6633,7 +6638,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6697,7 +6702,7 @@ dependencies = [ "serde", "slip10_ed25519", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "toml", "zeroize", @@ -7237,7 +7242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] @@ -7434,12 +7439,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" - [[package]] name = "postcard" version = "1.1.1" @@ -7855,7 +7854,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -8793,7 +8792,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -8805,12 +8804,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "slice-group-by" @@ -9476,11 +9472,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -9496,9 +9492,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 711a654af34..83d9a86a063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ bimap = {version = "0.6", features = ["serde"]} bit-set = "0.8" bitflags = { version = "2.5", features = ["serde"] } blake2b-rs = "0.2" +bridgetree = { package = "leanbridgetree", version = "0.3.0", features = ["borsh"] } byte-unit = "5.1" byteorder = "1.4" bytes = "1.1" @@ -160,6 +161,7 @@ fs_extra = "1.2" futures = "0.3" getrandom = "0.3" git2 = { version = "0.20", default-features = false } +group = { version = "0.13.0", default-features = false } hyper = { version = "1.6", features = ["full"] } ibc = { version = "0.57.0", features = ["serde"] } ibc-derive = "0.10.1" @@ -172,7 +174,6 @@ ics23 = "0.12" usize-set = { version = "0.10", features = ["serialize-borsh", "serialize-serde"] } impl-num-traits = "0.2" indexmap = { package = "nam-indexmap", version = "2.7.1-nam.0", features = ["borsh-schema", "serde"] } -init-once = "0.6" itertools = "0.14" jubjub = { package = "nam-jubjub", version = "1.10.1-nam.1" } k256 = { version = "0.13", default-features = false, features = ["ecdsa", "pkcs8", "precomputed-tables", "serde", "std"]} diff --git a/Makefile b/Makefile index 7ff90eefaa5..34172f912c5 100644 --- a/Makefile +++ b/Makefile @@ -124,9 +124,15 @@ check-crates: clippy-wasm = $(cargo) +$(nightly) clippy --manifest-path $(wasm)/Cargo.toml --all-targets -- -D warnings -# Need a separate command for benchmarks to prevent the "testing" feature flag from being activated +# Need a separate command for benchmarks to prevent the "testing" +# feature flag from being activated. Another special case is the +# "historic" feature flag present in `namada_shielded_token`. clippy: $(cargo) +$(nightly) clippy $(jobs) --all-targets --workspace --exclude namada_benchmarks -- -D warnings && \ + $(cargo) +$(nightly) clippy $(jobs) \ + --package namada_shielded_token \ + --features historic \ + -- -D warnings && \ $(cargo) +$(nightly) clippy $(jobs) --all-targets --package namada_benchmarks -- -D warnings && \ make -C $(wasms) clippy && \ make -C $(wasms_for_tests) clippy diff --git a/crates/apps_lib/Cargo.toml b/crates/apps_lib/Cargo.toml index 4d7e6385c82..f34e6711693 100644 --- a/crates/apps_lib/Cargo.toml +++ b/crates/apps_lib/Cargo.toml @@ -50,6 +50,7 @@ futures.workspace = true itertools.workspace = true jubjub.workspace = true kdam.workspace = true +konst.workspace = true lazy_static = { workspace = true, optional = true } linkme = { workspace = true, optional = true } ledger-lib.workspace = true diff --git a/crates/apps_lib/src/client/masp.rs b/crates/apps_lib/src/client/masp.rs index d2aba4c9f78..6b46b9546f2 100644 --- a/crates/apps_lib/src/client/masp.rs +++ b/crates/apps_lib/src/client/masp.rs @@ -14,6 +14,12 @@ use namada_sdk::masp::{ MaspLocalTaskEnv, ShieldedContext, ShieldedSyncConfig, ShieldedUtils, }; +const MASP_INDEXER_CLIENT_USER_AGENT: &str = { + const TOKENS: &[&str] = + &["Namada Masp Indexer Client/", env!("CARGO_PKG_VERSION")]; + konst::string::str_concat!(TOKENS) +}; + #[allow(clippy::too_many_arguments)] pub async fn syncing< U: ShieldedUtils + MaybeSend + MaybeSync, @@ -142,6 +148,7 @@ pub async fn syncing< let client = reqwest::Client::builder() .connect_timeout(Duration::from_secs(60)) + .user_agent(MASP_INDEXER_CLIENT_USER_AGENT) .build() .map_err(|err| { Error::Other(format!("Failed to build http client: {err}")) diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 08f01a5ec51..2fe47bddc47 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -108,7 +108,6 @@ ethers.workspace = true eyre.workspace = true fd-lock = { workspace = true, optional = true } futures.workspace = true -init-once.workspace = true itertools.workspace = true jubjub = { workspace = true, optional = true } lazy_static.workspace = true diff --git a/crates/sdk/src/masp/utilities.rs b/crates/sdk/src/masp/utilities.rs index 648c159b56c..7f4303cbf07 100644 --- a/crates/sdk/src/masp/utilities.rs +++ b/crates/sdk/src/masp/utilities.rs @@ -21,10 +21,12 @@ use namada_token::masp::utils::{ }; use namada_tx::event::MaspEvent; use namada_tx::{IndexedTx, Tx}; -use tokio::sync::Semaphore; +use tokio::sync::{OnceCell, Semaphore}; use crate::error::{Error, QueryError}; -use crate::masp::{extract_masp_tx, get_indexed_masp_events_at_height}; +use crate::masp::{ + NotePosition, extract_masp_tx, get_indexed_masp_events_at_height, +}; /// Middleware MASP client implementation that introduces /// linear backoff sleeps between failed requests. @@ -69,6 +71,11 @@ impl Clone for LinearBackoffSleepMaspClient { impl MaspClient for LinearBackoffSleepMaspClient { type Error = ::Error; + #[inline] + fn hint(&mut self, from: BlockHeight, to: BlockHeight) { + self.middleware_client.hint(from, to); + } + async fn last_block_height( &self, ) -> Result, Self::Error> { @@ -112,7 +119,7 @@ impl MaspClient for LinearBackoffSleepMaspClient { async fn fetch_note_index( &self, height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { with_linear_backoff( &self.shared.backoff, &self.shared.sleep, @@ -124,7 +131,8 @@ impl MaspClient for LinearBackoffSleepMaspClient { async fn fetch_witness_map( &self, height: BlockHeight, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> + { with_linear_backoff( &self.shared.backoff, &self.shared.sleep, @@ -181,6 +189,8 @@ impl LedgerMaspClient { impl MaspClient for LedgerMaspClient { type Error = Error; + fn hint(&mut self, _from: BlockHeight, _to: BlockHeight) {} + async fn last_block_height(&self) -> Result, Error> { let maybe_block = crate::rpc::query_block(&self.inner.client).await?; Ok(maybe_block.map(|b| b.height)) @@ -261,7 +271,7 @@ impl MaspClient for LedgerMaspClient { #[inline(always)] fn capabilities(&self) -> MaspClientCapabilities { - MaspClientCapabilities::OnlyTransfers + MaspClientCapabilities::NONE } async fn fetch_commitment_tree( @@ -277,7 +287,7 @@ impl MaspClient for LedgerMaspClient { async fn fetch_note_index( &self, _: BlockHeight, - ) -> Result, Error> { + ) -> Result, Error> { Err(Error::Other( "Transaction notes map fetching is not implemented by this client" .to_string(), @@ -287,7 +297,7 @@ impl MaspClient for LedgerMaspClient { async fn fetch_witness_map( &self, _: BlockHeight, - ) -> Result>, Error> { + ) -> Result>, Error> { Err(Error::Other( "Witness map fetching is not implemented by this client" .to_string(), @@ -313,7 +323,7 @@ struct IndexerMaspClientShared { indexer_api: reqwest::Url, /// Bloom filter to help avoid fetching block heights /// with no MASP notes. - block_index: init_once::InitOnce>, + block_index: OnceCell>, /// Maximum number of concurrent fetches. max_concurrent_fetches: usize, } @@ -352,13 +362,9 @@ impl IndexerMaspClient { indexer_api, max_concurrent_fetches, semaphore: Semaphore::new(max_concurrent_fetches), - block_index: { - let mut index = init_once::InitOnce::new(); - if !using_block_index { - index.init(|| None); - } - index - }, + block_index: OnceCell::new_with( + (!using_block_index).then_some(None), + ), }); Self { client, shared } } @@ -430,6 +436,12 @@ impl IndexerMaspClient { impl MaspClient for IndexerMaspClient { type Error = Error; + fn hint(&mut self, from: BlockHeight, to: BlockHeight) { + if to.0 - from.0 + 1 < self.shared.max_concurrent_fetches as _ { + _ = self.shared.block_index.set(None); + } + } + async fn last_block_height(&self) -> Result, Error> { use serde::Deserialize; @@ -507,15 +519,11 @@ impl MaspClient for IndexerMaspClient { ))); } - let maybe_block_index = self + let maybe_block_index: &Option<_> = self .shared .block_index - .try_init_async(async { - let _permit = self.shared.semaphore.acquire().await.unwrap(); - self.last_block_index().await.ok() - }) - .await - .and_then(Option::as_ref); + .get_or_init(|| async { self.last_block_index().await.ok() }) + .await; let mut fetches = vec![]; loop { @@ -654,7 +662,11 @@ impl MaspClient for IndexerMaspClient { #[inline(always)] fn capabilities(&self) -> MaspClientCapabilities { - MaspClientCapabilities::AllData + const { + MaspClientCapabilities::MAY_FETCH_PRE_BUILT_TREE + .plus(MaspClientCapabilities::MAY_FETCH_PRE_BUILT_NOTE_INDEX) + .plus(MaspClientCapabilities::MAY_FETCH_PRE_BUILT_WITNESS_MAP) + } } async fn fetch_commitment_tree( @@ -708,12 +720,12 @@ impl MaspClient for IndexerMaspClient { async fn fetch_note_index( &self, BlockHeight(height): BlockHeight, - ) -> Result, Error> { + ) -> Result, Error> { use serde::Deserialize; #[derive(Deserialize)] struct Note { - note_position: usize, + note_position: NotePosition, #[serde(rename = "masp_tx_index")] batch_index: u32, block_index: u32, @@ -796,13 +808,13 @@ impl MaspClient for IndexerMaspClient { async fn fetch_witness_map( &self, BlockHeight(height): BlockHeight, - ) -> Result>, Error> { + ) -> Result>, Error> { use serde::Deserialize; #[derive(Deserialize)] struct Witness { bytes: Vec, - index: usize, + index: NotePosition, } #[derive(Deserialize)] diff --git a/crates/shielded_token/Cargo.toml b/crates/shielded_token/Cargo.toml index 9454643c532..32e7e61f5c3 100644 --- a/crates/shielded_token/Cargo.toml +++ b/crates/shielded_token/Cargo.toml @@ -55,21 +55,23 @@ namada_wallet = { workspace = true, optional = true } async-trait.workspace = true borsh.workspace = true +bridgetree.workspace = true eyre.workspace = true -futures.workspace = true flume = { workspace = true, optional = true } +futures.workspace = true +group.workspace = true itertools.workspace = true lazy_static.workspace = true linkme = { workspace = true, optional = true } masp_primitives.workspace = true masp_proofs.workspace = true proptest = { workspace = true, optional = true } -rand.workspace = true rand_core.workspace = true +rand.workspace = true rayon = { workspace = true, optional = true } ripemd.workspace = true -serde.workspace = true serde_json.workspace = true +serde.workspace = true sha2.workspace = true smooth-operator.workspace = true tempfile.workspace = true @@ -78,7 +80,6 @@ tracing.workspace = true typed-builder.workspace = true xorf.workspace = true - [dev-dependencies] namada_gas.path = "../gas" namada_governance = { path = "../governance", features = ["testing"] } diff --git a/crates/shielded_token/src/masp.rs b/crates/shielded_token/src/masp.rs index 47555165553..56dd3a243a1 100644 --- a/crates/shielded_token/src/masp.rs +++ b/crates/shielded_token/src/masp.rs @@ -2,6 +2,7 @@ #![allow(clippy::arithmetic_side_effects)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_sign_loss)] +pub mod bridge_tree; mod shielded_sync; pub mod shielded_wallet; #[cfg(test)] @@ -42,7 +43,7 @@ use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; use rand_core::{CryptoRng, RngCore}; -pub use shielded_wallet::ShieldedWallet; +pub use shielded_wallet::{NotePosition, ShieldedWallet}; use thiserror::Error; use self::utils::MaspIndexedTx; @@ -286,7 +287,7 @@ pub type MaspAmount = ValueSum<(Option, Address), token::Change>; /// A type tracking the notes used to construct a shielded transfer. Used to /// avoid reusing the same notes multiple times which would lead to an invalid /// transaction -pub type SpentNotesTracker = HashMap>; +pub type SpentNotesTracker = HashMap>; /// Represents the amount used of different conversions pub type Conversions = @@ -299,15 +300,16 @@ pub type TransferDelta = HashMap; pub type TransactionDelta = HashMap; /// Maps a shielded tx to the index of its first output note. -pub type NoteIndex = BTreeMap; +pub type NoteIndex = BTreeMap; /// Maps the note index (in the commitment tree) to a witness -pub type WitnessMap = HashMap>; +pub type WitnessMap = HashMap>; -#[derive(Copy, Clone, BorshSerialize, BorshDeserialize, Debug)] +#[derive(Copy, Clone, BorshSerialize, BorshDeserialize, Debug, Default)] /// The possible sync states of the shielded context pub enum ContextSyncStatus { /// The context contains data that has been confirmed by the protocol + #[default] Confirmed, /// The context possibly contains data that has not yet been confirmed by /// the protocol and could be incomplete or invalid @@ -1264,7 +1266,7 @@ pub mod fs { .load(&mut shielded, true) .await .expect("Test failed"); - assert_eq!(shielded.spents, HashSet::from([42])); + assert_eq!(shielded.spents, HashSet::from([NotePosition(42)])); } #[tokio::test] @@ -1283,11 +1285,11 @@ pub mod fs { let mut bytes: Vec = Vec::new(); let shielded = ShieldedWallet { utils, - spents: HashSet::from([42]), + spents: HashSet::from([NotePosition(42)]), ..Default::default() }; BorshSerialize::serialize( - &VersionedWalletRef::V1(&shielded), + &VersionedWalletRef::V2(&shielded), &mut bytes, ) .expect("Test failed"); @@ -1302,7 +1304,7 @@ pub mod fs { .load(&mut shielded, true) .await .expect("Test failed"); - assert_eq!(shielded.spents, HashSet::from([42])); + assert_eq!(shielded.spents, HashSet::from([NotePosition(42)])); } /// Check that we error out if the file cannot be loaded and migrated diff --git a/crates/shielded_token/src/masp/bridge_tree.rs b/crates/shielded_token/src/masp/bridge_tree.rs new file mode 100644 index 00000000000..6e833e36790 --- /dev/null +++ b/crates/shielded_token/src/masp/bridge_tree.rs @@ -0,0 +1,235 @@ +//! Bridge tree wrapper types to be used by the MASP. + +use std::collections::{BTreeMap, BTreeSet}; + +pub use bridgetree as pkg; // Re-export the bridgetree crate as `pkg`. +use eyre::{Context, ContextCompat}; +use masp_primitives::merkle_tree::{CommitmentTree, MerklePath}; +use masp_primitives::sapling::{Node, SAPLING_COMMITMENT_TREE_DEPTH}; +use namada_core::borsh::*; +use namada_core::collections::HashMap; + +use self::pkg::{Address, Position}; +use crate::masp::WitnessMap; + +/// Inner type wrapped in [`BridgeTree`]. +pub type InnerBridgeTree = + bridgetree::BridgeTree; + +/// Wrapper around a [`bridgetree::BridgeTree`]. +#[derive(Debug, Eq, PartialEq, Clone, BorshSerialize, BorshDeserialize)] +pub struct BridgeTree(InnerBridgeTree); + +impl AsRef for BridgeTree { + fn as_ref(&self) -> &InnerBridgeTree { + &self.0 + } +} + +impl AsMut for BridgeTree { + fn as_mut(&mut self) -> &mut InnerBridgeTree { + &mut self.0 + } +} + +impl Default for BridgeTree { + fn default() -> Self { + Self::empty() + } +} + +impl From for BridgeTree { + fn from(inner: InnerBridgeTree) -> Self { + Self(inner) + } +} + +impl BridgeTree { + /// Create an empty [`BridgeTree`]. + pub const fn empty() -> Self { + Self(bridgetree::BridgeTree::new()) + } + + /// Instantiate a [`BridgeTree`] from a [`CommitmentTree`]. + pub fn from_commitment_tree( + tree: CommitmentTree, + ) -> eyre::Result { + if let Some(frontier) = + tree.into_incrementalmerkletree().to_frontier().take() + { + Ok(InnerBridgeTree::from_frontier(frontier) + .context( + "Failed to create InnerBridgeTree from the provided \ + CommitmentTree", + )? + .into()) + } else { + Ok(Self::empty()) + } + } + + /// Instantiate a [`BridgeTree`] from a [`CommitmentTree`] and + /// [`WitnessMap`]. + /// + /// Does not verify that each witness in the [`WitnessMap`] has the same + /// root, nor does it check that these roots match that of the provided + /// [`CommitmentTree`]. + pub fn from_tree_and_witness_map( + tree: CommitmentTree, + witness_map: WitnessMap, + ) -> eyre::Result { + let witness_map = { + let mut map: HashMap = witness_map + .into_iter() + .map(|(pos, wit)| { + (pos.into(), wit.into_incrementalmerkletree()) + }) + .collect(); + map.sort_unstable_keys(); + map + }; + + let frontier = tree.into_incrementalmerkletree().to_frontier().take(); + let mut tracking = BTreeSet::new(); + let mut ommers = BTreeMap::new(); + let mut prior_bridges = Vec::with_capacity(witness_map.len()); + + for (position, inc_witness) in witness_map { + let mut next_incomplete_parent = + Address::from(position).current_incomplete(); + + for ommer in inc_witness.filled().iter().copied() { + let ommer_addr = { + let next = next_incomplete_parent; + + next_incomplete_parent = + next_incomplete_parent.next_incomplete_parent(); + + next.sibling() + }; + ommers.insert(ommer_addr, ommer); + } + + if next_incomplete_parent.level() + < (SAPLING_COMMITMENT_TREE_DEPTH as u8).into() + { + tracking.insert(next_incomplete_parent); + } + + prior_bridges.push( + inc_witness + .tree() + .to_frontier() + .take() + .context("IncrementalWitness with empty commitment tree")?, + ); + } + + Ok(InnerBridgeTree::from_parts( + frontier, + prior_bridges, + tracking, + ommers, + ) + .context("Failed to create InnerBridgeTree from constituent parts")? + .into()) + } + + /// Witness the node at `node_pos`. + /// + /// Returns a proof targeting the latest anchor in this tree. + pub fn witness( + &self, + node_pos: impl TryInto, + ) -> Option> { + let position = bridgetree::Position::from(node_pos.try_into().ok()?); + + Some( + self.as_ref() + .witness(position) + .ok()? + .into_iter() + .fold( + ( + MerklePath::from_path( + Vec::with_capacity(SAPLING_COMMITMENT_TREE_DEPTH), + position.into(), + ), + bridgetree::Address::from(position), + ), + |(mut merkle_path, addr), node| { + merkle_path + .auth_path + .push((node, addr.is_right_child())); + (merkle_path, addr.parent()) + }, + ) + .0, + ) + } +} + +#[cfg(test)] +mod tests { + use masp_primitives::merkle_tree::IncrementalWitness; + + use super::*; + + #[test] + fn test_bridge_tree_same_anchor_as_cmt_tree() { + let mut legacy_witnesses: BTreeMap> = + BTreeMap::new(); + let mut legacy_tree = CommitmentTree::empty(); + let mut tree = BridgeTree::empty(); + + let nodes_to_witness = BTreeSet::from([0u64, 3u64, 5u64]); + + // build witnesses + for node_pos in 0u64..10 { + let node = Node::from_scalar(node_pos.into()); + + tree.as_mut().append(node).unwrap(); + assert!(legacy_tree.append(node).is_ok()); + + for wit in legacy_witnesses.values_mut() { + assert!(wit.append(node).is_ok()); + } + + if nodes_to_witness.contains(&node_pos) { + assert!(tree.as_mut().mark().is_some()); + legacy_witnesses.insert( + node_pos, + IncrementalWitness::from_tree(&legacy_tree), + ); + } + } + + // compare roots + { + let root = legacy_tree.root(); + + assert_eq!(root, tree.as_ref().root()); + + for wit in legacy_witnesses.values() { + assert_eq!(root, wit.root()); + } + } + + // compare witnesses + let merkle_proofs_from_bridge_tree: BTreeMap<_, _> = nodes_to_witness + .iter() + .copied() + .map(|node_pos| (node_pos, tree.witness(node_pos).unwrap())) + .collect(); + let merkle_proofs_from_legacy_witnesses: BTreeMap<_, _> = + legacy_witnesses + .into_iter() + .map(|(node_pos, witness)| (node_pos, witness.path().unwrap())) + .collect(); + + assert_eq!( + merkle_proofs_from_bridge_tree, + merkle_proofs_from_legacy_witnesses + ); + } +} diff --git a/crates/shielded_token/src/masp/shielded_sync/dispatcher.rs b/crates/shielded_token/src/masp/shielded_sync/dispatcher.rs index f0ce7e8ba70..a9bfde07d79 100644 --- a/crates/shielded_token/src/masp/shielded_sync/dispatcher.rs +++ b/crates/shielded_token/src/masp/shielded_sync/dispatcher.rs @@ -8,30 +8,32 @@ use std::sync::atomic::{self, AtomicBool, AtomicUsize}; use std::task::{Context, Poll}; use borsh::{BorshDeserialize, BorshSerialize}; -use eyre::{WrapErr, eyre}; +use eyre::{ContextCompat, WrapErr, eyre}; use futures::future::{Either, select}; use futures::task::AtomicWaker; use masp_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; use masp_primitives::sapling::{Node, ViewingKey}; use masp_primitives::transaction::Transaction; use namada_core::chain::BlockHeight; -use namada_core::collections::HashMap; +use namada_core::collections::{HashMap, HashSet}; use namada_core::control_flow::ShutdownSignal; use namada_core::control_flow::time::{Duration, LinearBackoff, Sleep}; use namada_core::hints; use namada_core::task_env::TaskSpawner; use namada_io::{MaybeSend, MaybeSync, ProgressBar}; -use namada_tx::IndexedTx; use namada_wallet::{DatedKeypair, DatedSpendingKey}; -use super::utils::{IndexedNoteEntry, MaspClient, MaspIndexedTx, MaspTxKind}; +use super::utils::{ + IndexedNoteEntry, MaspClient, MaspClientCapabilities, MaspIndexedTx, +}; +use crate::masp::bridge_tree::BridgeTree; use crate::masp::shielded_sync::trial_decrypt; use crate::masp::utils::{ DecryptedData, Fetched, RetryStrategy, TrialDecrypted, blocks_left_to_fetch, }; use crate::masp::{ - MaspExtendedSpendingKey, NoteIndex, ShieldedUtils, ShieldedWallet, - WitnessMap, to_viewing_key, + MaspExtendedSpendingKey, NoteIndex, NotePosition, ShieldedUtils, + ShieldedWallet, WitnessMap, to_viewing_key, }; struct AsyncCounterInner { @@ -150,15 +152,23 @@ struct TaskError { context: C, } -#[allow(clippy::large_enum_variant)] +#[allow(clippy::large_enum_variant, clippy::type_complexity)] enum Message { - UpdateCommitmentTree(Result, TaskError>), - UpdateNotesMap( - Result, TaskError>, + FirstCommitmentTree( + Result<(BlockHeight, CommitmentTree), TaskError>, + ), + UpdateCommitmentTree( + Result<(BlockHeight, CommitmentTree), TaskError>, + ), + UpdateNoteIndex( + Result< + (BlockHeight, BTreeMap), + TaskError, + >, ), UpdateWitnessMap( Result< - HashMap>, + (BlockHeight, HashMap>), TaskError, >, ), @@ -203,11 +213,18 @@ impl DispatcherTasks { self.message_receiver.try_recv().ok() } } + + #[inline(always)] + fn try_recv_orphaned_message(&self) -> Option { + self.message_receiver.try_recv().ok() + } } /// Shielded sync cache. #[derive(Default, BorshSerialize, BorshDeserialize)] pub struct DispatcherCache { + pub(crate) first_commitment_tree: + Option<(BlockHeight, CommitmentTree)>, pub(crate) commitment_tree: Option<(BlockHeight, CommitmentTree)>, pub(crate) witness_map: Option<(BlockHeight, WitnessMap)>, pub(crate) note_index: Option<(BlockHeight, NoteIndex)>, @@ -224,7 +241,7 @@ enum DispatcherState { #[derive(Default, Debug)] struct InitialState { - last_witnessed_tx: Option, + synced: bool, start_height: BlockHeight, last_query_height: BlockHeight, } @@ -240,20 +257,32 @@ pub struct Config { pub shutdown_signal: I, } +/// State of different fetch jobs. +#[derive(Default)] +enum FetchState { + #[default] + NothingToFetch, + Fetching { + left: usize, + }, + Complete, +} + /// Shielded sync message dispatcher. pub struct Dispatcher where U: ShieldedUtils, { client: M, + birthdays: HashMap, state: DispatcherState, tasks: DispatcherTasks, ctx: ShieldedWallet, config: Config, cache: DispatcherCache, - /// We are syncing up to this height - height_to_sync: BlockHeight, interrupt_flag: AtomicFlag, + orphaned_flag: AtomicFlag, + fetch_state: FetchState, } /// Create a new dispatcher in the initial state. @@ -297,7 +326,7 @@ where let cache = ctx.utils.cache_load().await.unwrap_or_default(); Dispatcher { - height_to_sync: BlockHeight(0), + birthdays: HashMap::new(), state, ctx, tasks, @@ -305,6 +334,8 @@ where config, cache, interrupt_flag: Default::default(), + orphaned_flag: Default::default(), + fetch_state: Default::default(), } } @@ -319,20 +350,19 @@ where /// Run the dispatcher pub async fn run( mut self, - start_query_height: Option, last_query_height: Option, sks: &[DatedSpendingKey], fvks: &[DatedKeypair], ) -> Result>, eyre::Error> { let initial_state = self - .perform_initial_setup( - start_query_height, - last_query_height, - sks, - fvks, - ) + .perform_initial_setup(last_query_height, sks, fvks) .await?; + if initial_state.synced { + self.finish_progress_bars(); + return Ok(Some(self.ctx)); + } + self.check_exit_conditions(); while let Some(message) = self.tasks.get_next_message().await { @@ -352,7 +382,7 @@ where Ok(None) } DispatcherState::Normal => { - self.apply_cache_to_shielded_context(&initial_state).await?; + self.apply_cache_to_shielded_context(&initial_state)?; self.finish_progress_bars(); self.ctx.save().await.map_err(|err| { eyre!("Failed to save the shielded context: {err}") @@ -383,61 +413,76 @@ where } } - async fn apply_cache_to_shielded_context( + fn apply_cache_to_shielded_context( &mut self, InitialState { - last_witnessed_tx, - last_query_height, - .. + last_query_height, .. }: &InitialState, ) -> Result<(), eyre::Error> { - if let Some((_, cmt)) = self.cache.commitment_tree.take() { - self.ctx.tree = cmt; - } - if let Some((_, wm)) = self.cache.witness_map.take() { - self.ctx.witness_map = wm; - } - if let Some((_, nm)) = self.cache.note_index.take() { - self.ctx.note_index = nm; + // NB: Load the first commitment tree from cache + if let Some((_, ct)) = self.cache.first_commitment_tree.take() { + self.ctx.tree = BridgeTree::from_commitment_tree(ct)?; } for (masp_indexed_tx, stx_batch) in self.cache.fetched.take() { - let needs_witness_map_update = - self.client.capabilities().needs_witness_map_update(); - self.ctx.save_shielded_spends( - &stx_batch, - needs_witness_map_update, - #[cfg(feature = "historic")] - Some(masp_indexed_tx.indexed_tx), - )?; - if needs_witness_map_update - && Some(&masp_indexed_tx) > last_witnessed_tx.as_ref() + self.try_get_optional_masp_data(); + + if masp_indexed_tx.indexed_tx.block_height > self.ctx.synced_height { - self.ctx.update_witness_map( - masp_indexed_tx, + let cmt_tree_still_fetching = !self.has_optional_masp_data(); + + if cmt_tree_still_fetching { + self.ctx.update_witnesses( + masp_indexed_tx, + &stx_batch, + &self.cache.trial_decrypted, + )?; + } + + self.ctx.save_shielded_spends( &stx_batch, - &self.cache.trial_decrypted, + cmt_tree_still_fetching, + #[cfg(feature = "historic")] + Some(masp_indexed_tx.indexed_tx), )?; } - let first_note_pos = self.ctx.note_index[&masp_indexed_tx]; - let mut vk_heights = BTreeMap::new(); - std::mem::swap(&mut vk_heights, &mut self.ctx.vk_heights); - for (vk, _) in vk_heights - .iter() - // NB: skip keys that are synced past the given `indexed_tx` - .filter(|(_vk, h)| h.as_ref() < Some(&masp_indexed_tx)) - { + + let first_note_pos = self + .ctx + .note_index + .get(&masp_indexed_tx) + .copied() + .with_context(|| { + format!( + "Could not locate the first note position of the MASP \ + tx at {masp_indexed_tx:?}" + ) + })?; + + for vk_index in 0..self.ctx.pos_map.len() { + let vk = self.ctx.pos_map.get_index(vk_index).unwrap().0; + + if !self.vk_is_outdated(vk, &masp_indexed_tx) { + // NB: skip keys that are synced past the given + // `masp_indexed_tx` + continue; + } + + // NB: copy the viewing key onto the stack, to remove + // the borrow and allow mutating through `self.ctx` + let vk = *vk; + for (note_pos_offset, (note, pa, memo)) in self .cache .trial_decrypted - .take(&masp_indexed_tx, vk) + .take(&masp_indexed_tx, &vk) .unwrap_or_default() { self.ctx.save_decrypted_shielded_outputs( #[cfg(feature = "historic")] masp_indexed_tx.indexed_tx, - vk, - first_note_pos + note_pos_offset, + &vk, + first_note_pos.checked_add(note_pos_offset).unwrap(), note, pa, memo, @@ -445,43 +490,30 @@ where self.config.applied_tracker.increment_by(1); } } - std::mem::swap(&mut vk_heights, &mut self.ctx.vk_heights); } - for (_, h) in self - .ctx - .vk_heights - .iter_mut() - // NB: skip keys that are synced past the last input height - .filter(|(_vk, h)| { - h.as_ref().map(|itx| &itx.indexed_tx.block_height) - < Some(last_query_height) - }) - { - // NB: the entire block is synced - *h = Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(*last_query_height), - kind: MaspTxKind::Transfer, - }); - } + self.abandon_orphaned(); + self.try_load_optional_masp_data()?; + + // NB: at this point, the wallet has been synced + self.ctx.synced_height = *last_query_height; Ok(()) } + fn abandon_orphaned(&self) { + self.orphaned_flag.set(); + } + async fn perform_initial_setup( &mut self, - start_query_height: Option, last_query_height: Option, sks: &[DatedSpendingKey], fvks: &[DatedKeypair], ) -> Result { - if start_query_height > last_query_height { - return Err(eyre!( - "The start height {start_query_height:?} cannot be higher \ - than the ending height {last_query_height:?} in the shielded \ - sync" - )); - } + debug_assert!(self.birthdays.is_empty()); + + let mut min_birthday = BlockHeight(u64::MAX); for vk in sks .iter() @@ -492,25 +524,43 @@ where }) .chain(fvks.iter().copied()) { - if let Some(h) = self.ctx.vk_heights.entry(vk.key).or_default() { - let birthday = IndexedTx::entire_block(vk.birthday); - if birthday > h.indexed_tx { - h.indexed_tx = birthday; - } - } else if vk.birthday >= BlockHeight::first() { - self.ctx.vk_heights.insert( - vk.key, - Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(vk.birthday), - kind: MaspTxKind::Transfer, - }), + // NB: store the viewing keys in the wallet + let decrypted_notes = self.ctx.pos_map.entry(vk.key).or_default(); + + // NB: sanity check to confirm we haven't decrypted notes + // before the supplied birthday + if vk.birthday > self.ctx.synced_height + && !decrypted_notes.is_empty() + { + eyre::bail!( + "Invalid viewing key birthday, set after notes have \ + already been decrypted" ); } + + // NB: store the birthday in order to potentially + // save some work during trial decryptions + self.birthdays.insert(vk.key, vk.birthday); + + // NB: the min birthday will allow skipping a bunch + // of blocks if we are syncing from scratch + if vk.birthday < min_birthday { + min_birthday = vk.birthday + } } - // the latest block height which has been added to the witness Merkle - // tree - let last_witnessed_tx = self.ctx.note_index.keys().max().cloned(); + // NB: Optimize syncing from scratch when the + // user supplied key birthdays + const THRESHOLD_MIN_BIRTHDAY_LOCK: u64 = 1000; + + if self.ctx.synced_height == BlockHeight(0) + && min_birthday.0 > THRESHOLD_MIN_BIRTHDAY_LOCK + && self.client.capabilities().may_fetch_pre_built_tree() + && !self.birthdays.is_empty() + { + self.ctx.synced_height = min_birthday; + self.spawn_first_commitment_tree(min_birthday); + } let shutdown_signal = RefCell::new(&mut self.config.shutdown_signal); @@ -533,7 +583,7 @@ where .client .last_block_height() .await - .wrap_err("Failed to fetch last block height") + .wrap_err("Failed to fetch last block height") { Ok(Some(last_block_height)) => last_block_height, Ok(None) => { @@ -563,27 +613,38 @@ where // NB: limit fetching until the last committed height .min(last_block_height); - let start_height = start_query_height - .map_or_else(|| self.ctx.min_height_to_sync_from(), Ok)? - // NB: the start height cannot be greater than - // `last_query_height` - .min(last_query_height); + let start_height = self.ctx.synced_height.clamp( + // NB: the wallet is initialized with height 0 + // if it hasn't synced any note, so we must clamp + // the first height to 1 explicitly + BlockHeight::first(), + last_query_height, + ); + + let synced = self.ctx.synced_height == start_height + && start_height == last_query_height; let initial_state = InitialState { - last_witnessed_tx, + synced, last_query_height, start_height, }; - self.height_to_sync = initial_state.last_query_height; - self.spawn_initial_set_of_tasks(&initial_state); + if !synced { + self.spawn_initial_set_of_tasks(&initial_state); - self.config - .scanned_tracker - .set_upper_limit(self.cache.fetched.len() as u64); - self.config.applied_tracker.set_upper_limit( - self.cache.trial_decrypted.successful_decryptions() as u64, - ); + self.client.hint( + initial_state.start_height, + initial_state.last_query_height, + ); + + self.config + .scanned_tracker + .set_upper_limit(self.cache.fetched.len() as u64); + self.config.applied_tracker.set_upper_limit( + self.cache.trial_decrypted.successful_decryptions() as u64, + ); + } self.force_redraw_progress_bars(); @@ -612,16 +673,26 @@ where } fn spawn_initial_set_of_tasks(&mut self, initial_state: &InitialState) { - if self.client.capabilities().may_fetch_pre_built_notes_index() { - self.spawn_update_note_index(initial_state.last_query_height); - } + let needed_caps = const { + MaspClientCapabilities::MAY_FETCH_PRE_BUILT_TREE + .plus(MaspClientCapabilities::MAY_FETCH_PRE_BUILT_NOTE_INDEX) + .plus(MaspClientCapabilities::MAY_FETCH_PRE_BUILT_WITNESS_MAP) + }; - if self.client.capabilities().may_fetch_pre_built_tree() { - self.spawn_update_commitment_tree(initial_state.last_query_height); - } + const THRESHOLD_START_OPT_MASP_DATA_FETCHES: u64 = 100; + + let meets_opt_masp_data_block_threshold = + initial_state.last_query_height.0 - initial_state.start_height.0 + > THRESHOLD_START_OPT_MASP_DATA_FETCHES; - if self.client.capabilities().may_fetch_pre_built_witness_map() { + if self.client.capabilities().contains(needed_caps) + && meets_opt_masp_data_block_threshold + { + self.spawn_update_commitment_tree(initial_state.last_query_height); + self.spawn_update_note_index(initial_state.last_query_height); self.spawn_update_witness_map(initial_state.last_query_height); + + self.fetch_state = FetchState::Fetching { left: 3usize }; } let mut number_of_fetches = 0; @@ -647,11 +718,21 @@ where fn handle_incoming_message(&mut self, message: Message) { match message { - Message::UpdateCommitmentTree(Ok(ct)) => { - _ = self - .cache - .commitment_tree - .insert((self.height_to_sync, ct)); + Message::FirstCommitmentTree(Ok((h, ct))) => { + _ = self.cache.first_commitment_tree.insert((h, ct)); + } + Message::FirstCommitmentTree(Err(TaskError { + error, + context: height, + })) => { + if self.can_launch_new_fetch_retry(error) { + self.spawn_first_commitment_tree(height); + } + } + Message::UpdateCommitmentTree(Ok((h, ct))) => { + _ = self.cache.commitment_tree.insert((h, ct)); + + self.must_decrement_left_fetching(); } Message::UpdateCommitmentTree(Err(TaskError { error, @@ -661,10 +742,12 @@ where self.spawn_update_commitment_tree(height); } } - Message::UpdateNotesMap(Ok(nm)) => { - _ = self.cache.note_index.insert((self.height_to_sync, nm)); + Message::UpdateNoteIndex(Ok((h, nm))) => { + _ = self.cache.note_index.insert((h, nm)); + + self.must_decrement_left_fetching(); } - Message::UpdateNotesMap(Err(TaskError { + Message::UpdateNoteIndex(Err(TaskError { error, context: height, })) => { @@ -672,8 +755,10 @@ where self.spawn_update_note_index(height); } } - Message::UpdateWitnessMap(Ok(wm)) => { - _ = self.cache.witness_map.insert((self.height_to_sync, wm)); + Message::UpdateWitnessMap(Ok((h, wm))) => { + _ = self.cache.witness_map.insert((h, wm)); + + self.must_decrement_left_fetching(); } Message::UpdateWitnessMap(Err(TaskError { error, @@ -733,17 +818,122 @@ where } } + fn vk_synced_height(&self, vk: &ViewingKey) -> BlockHeight { + self.birthdays + .get(vk) + .copied() + .unwrap_or(self.ctx.synced_height) + } + + fn vk_is_outdated(&self, vk: &ViewingKey, itx: &MaspIndexedTx) -> bool { + self.vk_synced_height(vk) < itx.indexed_tx.block_height + } + + fn try_load_optional_masp_data(&mut self) -> Result<(), eyre::Error> { + if !self.has_optional_masp_data() { + self.ctx.tree.as_mut().garbage_collect_ommers(); + return Ok(()); + } + + let mut witness_map = self.cache.witness_map.take().unwrap().1; + let commitment_tree = self.cache.commitment_tree.take().unwrap().1; + + let set_of_unspent_note_pos: HashSet<_> = self + .ctx + .pos_map + .values() + .flat_map(|vk_owned_note_set| { + vk_owned_note_set.iter().filter_map(|owned_note| { + if !self.ctx.spents.contains(owned_note) { + Some(*owned_note) + } else { + None + } + }) + }) + .collect(); + + // NB: Prune witnesses we don't care about + witness_map + .retain(|note_pos, _| set_of_unspent_note_pos.contains(note_pos)); + + self.ctx.tree = BridgeTree::from_tree_and_witness_map( + commitment_tree, + witness_map, + )?; + + Ok(()) + } + + #[inline(always)] + fn has_optional_masp_data(&self) -> bool { + matches!(&self.fetch_state, FetchState::Complete) + } + + fn try_get_optional_masp_data(&mut self) { + if !matches!(&self.fetch_state, FetchState::Fetching { .. }) { + return; + } + while let Some(message) = self.tasks.try_recv_orphaned_message() { + self.handle_incoming_message(message); + std::hint::spin_loop(); + } + // NB: Load note index as soon as we can, to avoid running + // into errors related with unknown note positions + if matches!(&self.fetch_state, FetchState::Fetching { left: 0usize }) { + self.fetch_state = FetchState::Complete; + self.ctx.note_index = self.cache.note_index.take().unwrap().1; + } + } + + fn must_decrement_left_fetching(&mut self) { + let left = self.must_get_left_fetching(); + *left = left + .checked_sub(1) + .expect("Should not have underflowed in `must_get_left_fetching`"); + } + + fn must_get_left_fetching(&mut self) -> &mut usize { + if let FetchState::Fetching { left } = &mut self.fetch_state { + left + } else { + unreachable!("Must get `left_fetching` as `FetchState::Fetching`") + } + } + fn spawn_update_witness_map(&mut self, height: BlockHeight) { if pre_built_in_cache(self.cache.witness_map.as_ref(), height) { return; } let client = self.client.clone(); - self.spawn_async(Box::pin(async move { + self.spawn_async_orphaned(Box::pin(async move { Message::UpdateWitnessMap( client .fetch_witness_map(height) .await .wrap_err("Failed to fetch witness map") + .map(|wm| (height, wm)) + .map_err(|error| TaskError { + error, + context: height, + }), + ) + })); + } + + fn spawn_first_commitment_tree(&mut self, height: BlockHeight) { + if pre_built_in_cache(self.cache.first_commitment_tree.as_ref(), height) + { + return; + } + let client = self.client.clone(); + self.spawn_async(Box::pin(async move { + Message::FirstCommitmentTree( + client + .fetch_commitment_tree(height) + .await + .wrap_err("Failed to fetch first commitment tree") + .map(|ct| (height, ct)) .map_err(|error| TaskError { error, context: height, @@ -757,12 +947,13 @@ where return; } let client = self.client.clone(); - self.spawn_async(Box::pin(async move { + self.spawn_async_orphaned(Box::pin(async move { Message::UpdateCommitmentTree( client .fetch_commitment_tree(height) .await .wrap_err("Failed to fetch commitment tree") + .map(|ct| (height, ct)) .map_err(|error| TaskError { error, context: height, @@ -776,12 +967,13 @@ where return; } let client = self.client.clone(); - self.spawn_async(Box::pin(async move { - Message::UpdateNotesMap( + self.spawn_async_orphaned(Box::pin(async move { + Message::UpdateNoteIndex( client .fetch_note_index(height) .await .wrap_err("Failed to fetch note index") + .map(|ni| (height, ni)) .map_err(|error| TaskError { error, context: height, @@ -815,11 +1007,12 @@ where } fn spawn_trial_decryptions(&self, itx: MaspIndexedTx, tx: &Transaction) { - for (vk, vk_height) in self.ctx.vk_heights.iter() { - let key_is_outdated = vk_height.as_ref() < Some(&itx); - let cached = self.cache.trial_decrypted.get(&itx, vk).is_some(); + for vk in self.ctx.pos_map.keys() { + let vk_is_outdated = self.vk_is_outdated(vk, &itx); + let tx_decrypted_in_cache = + self.cache.trial_decrypted.get(&itx, vk).is_some(); - if key_is_outdated && !cached { + if vk_is_outdated && !tx_decrypted_in_cache { let tx = tx.clone(); let vk = *vk; @@ -834,6 +1027,31 @@ where } } + fn spawn_async_orphaned(&self, mut fut: F) + where + F: Future + Unpin + 'static, + { + let sender = self.tasks.message_sender.clone(); + let guard = ( + self.tasks.panic_flag.clone(), + self.orphaned_flag.clone(), + self.interrupt_flag.clone(), + ); + self.tasks.spawner.spawn_async(async move { + let (_panic_flag, orphaned, interrupt) = guard; + let wrapped_fut = std::future::poll_fn(move |cx| { + if interrupt.get() || orphaned.get() { + Poll::Ready(None) + } else { + Pin::new(&mut fut).poll(cx).map(Some) + } + }); + if let Some(msg) = wrapped_fut.await { + sender.send_async(msg).await.unwrap() + } + }); + } + fn spawn_async(&self, mut fut: F) where F: Future + Unpin + 'static, @@ -896,9 +1114,9 @@ mod dispatcher_tests { use namada_core::task_env::TaskEnvironment; use namada_io::DevNullProgressBar; use namada_tx::IndexedTx; - use namada_wallet::StoredKeypair; use tempfile::tempdir; + use super::super::utils::MaspTxKind; use super::*; use crate::masp::fs::FsShieldedUtils; use crate::masp::test_utils::{ @@ -928,10 +1146,12 @@ mod dispatcher_tests { .expect("Test failed") .run(|s| async { let mut dispatcher = config.dispatcher(s, &utils).await; - dispatcher.ctx.vk_heights = - BTreeMap::from([(arbitrary_vk(), None)]); + dispatcher + .ctx + .pos_map + .insert(arbitrary_vk(), BTreeSet::new()); // fill up the dispatcher's cache - for h in 0u64..10 { + for h in 1u64..10 { let itx = MaspIndexedTx { indexed_tx: IndexedTx { block_height: h.into(), @@ -941,7 +1161,7 @@ mod dispatcher_tests { kind: MaspTxKind::Transfer, }; dispatcher.cache.fetched.insert((itx, arbitrary_masp_tx())); - dispatcher.ctx.note_index.insert(itx, h as usize); + dispatcher.ctx.note_index.insert(itx, NotePosition(h)); dispatcher.cache.trial_decrypted.insert( itx, arbitrary_vk(), @@ -951,22 +1171,18 @@ mod dispatcher_tests { dispatcher .apply_cache_to_shielded_context(&InitialState { - last_witnessed_tx: None, - start_height: Default::default(), + synced: false, + start_height: BlockHeight::first(), last_query_height: 9.into(), }) - .await .expect("Test failed"); assert!(dispatcher.cache.fetched.is_empty()); assert!(dispatcher.cache.trial_decrypted.is_empty()); - let expected = BTreeMap::from([( - arbitrary_vk(), - Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(9.into()), - kind: MaspTxKind::Transfer, - }), - )]); - assert_eq!(expected, dispatcher.ctx.vk_heights); + assert_eq!( + HashMap::from([(arbitrary_vk(), BTreeSet::new())]), + dispatcher.ctx.pos_map + ); + assert_eq!(BlockHeight(9), dispatcher.ctx.synced_height); }) .await; } @@ -1052,9 +1268,12 @@ mod dispatcher_tests { let barrier = barrier.clone(); dispatcher.spawn_async(Box::pin(async move { barrier.wait().await; - Message::UpdateWitnessMap(Err(TaskError { + Message::FetchTxs(Err(TaskError { error: eyre!("Test"), - context: BlockHeight::first(), + context: [ + BlockHeight::first(), + BlockHeight::first(), + ], })) })); } @@ -1069,12 +1288,12 @@ mod dispatcher_tests { // run the dispatcher let flag = dispatcher.tasks.panic_flag.clone(); - let dispatcher_fut = dispatcher.run( - Some(BlockHeight::first()), - Some(BlockHeight(10)), - &[], - &[], - ); + let esks = [DatedSpendingKey::new( + MaspExtendedSpendingKey::master(b"bing bong").into(), + None, + )]; + let dispatcher_fut = + dispatcher.run(Some(BlockHeight(10)), &esks, &[]); // we poll the dispatcher future until the panic thread has // panicked. @@ -1100,17 +1319,17 @@ mod dispatcher_tests { // we assert that the panic thread panicked and retrieve the // dispatcher future - let Either::Right((_, fut)) = - select(Box::pin(dispatcher_fut), Box::pin(panicked_fut)) - .await - else { - panic!("Test failed") + let fut = match select( + Box::pin(dispatcher_fut), + Box::pin(panicked_fut), + ) + .await + { + Either::Right((_, fut)) => fut, + Either::Left((res, _)) => panic!("Test failed: {res:?}"), }; - let (_, res) = join! { - barrier.wait(), - fut, - }; + let (_, res) = join!(barrier.wait(), fut); let Err(msg) = res else { panic!("Test failed") }; @@ -1122,43 +1341,6 @@ mod dispatcher_tests { .await; } - /// Test that upon each retry, we either resume from the - /// latest height that had been previously stored in the - /// `vk_heights`. - #[test] - fn test_min_height_to_sync_from() { - let temp_dir = tempdir().unwrap(); - let mut shielded_ctx = - FsShieldedUtils::new(temp_dir.path().to_path_buf()); - - let vk = arbitrary_vk(); - - // Test that this function errors if not keys are - // present in the shielded context - assert!(shielded_ctx.min_height_to_sync_from().is_err()); - - // the min height here should be 1, since - // this vk hasn't decrypted any note yet - shielded_ctx.vk_heights.insert(vk, None); - - let height = shielded_ctx.min_height_to_sync_from().unwrap(); - assert_eq!(height, BlockHeight(1)); - - // let's bump the vk height - *shielded_ctx.vk_heights.get_mut(&vk).unwrap() = Some(MaspIndexedTx { - indexed_tx: IndexedTx { - block_height: 6.into(), - block_index: TxIndex(0), - batch_index: None, - }, - kind: MaspTxKind::Transfer, - }); - - // the min height should now be 6 - let height = shielded_ctx.min_height_to_sync_from().unwrap(); - assert_eq!(height, BlockHeight(6)); - } - /// We test that if a masp transaction is only partially trial-decrypted /// before the process is interrupted, we discard the partial results. #[test] @@ -1206,7 +1388,7 @@ mod dispatcher_tests { masp_tx_sender.send(None).expect("Test failed"); let dispatcher = config.clone().dispatcher(s, &utils).await; - let result = dispatcher.run(None, None, &[], &[vk]).await; + let result = dispatcher.run(None, &[], &[vk]).await; match result { Err(msg) => assert_eq!( msg.to_string(), @@ -1256,7 +1438,7 @@ mod dispatcher_tests { let dispatcher = config.dispatcher(s, &utils).await; // This should complete successfully let ctx = dispatcher - .run(None, None, &[], &[vk]) + .run(None, &[], &[vk]) .await .expect("Test failed") .expect("Test failed"); @@ -1282,13 +1464,7 @@ mod dispatcher_tests { ]); assert_eq!(keys, expected); - assert_eq!( - *ctx.vk_heights[&vk.key].as_ref().unwrap(), - MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(2.into(),), - kind: MaspTxKind::Transfer - } - ); + assert_eq!(ctx.synced_height, BlockHeight(2)); assert_eq!(ctx.note_map.len(), 2); }) .await; @@ -1348,7 +1524,7 @@ mod dispatcher_tests { ))) .expect("Test failed"); masp_tx_sender.send(None).expect("Test failed"); - let result = dispatcher.run(None, None, &[], &[vk]).await; + let result = dispatcher.run(None, &[], &[vk]).await; match result { Err(msg) => assert_eq!( msg.to_string(), @@ -1431,18 +1607,20 @@ mod dispatcher_tests { send.send_replace(true); let res = dispatcher - .run(None, None, &[], &[dated_arbitrary_vk()]) + .run(None, &[], &[dated_arbitrary_vk()]) .await .expect("Test failed"); assert!(res.is_none()); let DispatcherCache { + first_commitment_tree, commitment_tree, witness_map, note_index, fetched, trial_decrypted, } = utils.cache_load().await.expect("Test failed"); + assert!(first_commitment_tree.is_none()); assert!(commitment_tree.is_none()); assert!(witness_map.is_none()); assert!(note_index.is_none()); @@ -1451,116 +1629,100 @@ mod dispatcher_tests { }) .await; } - /// Test the the birthdays of keys are properly reflected in the key - /// sync heights when starting shielded sync. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_key_birthdays() { let temp_dir = tempdir().unwrap(); - let mut shielded_ctx = - FsShieldedUtils::new(temp_dir.path().to_path_buf()); - let (client, masp_tx_sender) = TestingMaspClient::new(2.into()); - // First test the case where no keys have been seen yet - let mut vk = DatedKeypair::new(arbitrary_vk(), Some(10.into())); - let StoredKeypair::Raw(mut sk) = serde_json::from_str::<'_, StoredKeypair::>(r#""unencrypted:zsknam1q02rgh4mqqqqpqqm68m2lmd0xe9k5vf4fscmdxuvewqhdhwl0h492fj40tzl5f6gwfk6kgnaxpgct7mx9cw2he4724858jdfhrzdh3e4hu3us463gphqyl6k5hvkjwkv9r7rx3jtcueurgflgj6dx9qn4rg0caf0t9zawfcdwt3ramxlrs4jyan4wyp4nh9hj8s806ru0smk3437ejy56ewtw9ljz8rc3vkyznxdf3l5c70skcw6aatpv5de9zhxuxs5k6l6jz6zktgg0udvl<<30""#).expect("Test failed") else { - panic!("Test failed") + let utils = FsShieldedUtils { + context_dir: temp_dir.path().to_path_buf(), }; - let masp_tx = arbitrary_masp_tx(); - masp_tx_sender - .send(Some(( - MaspIndexedTx { - indexed_tx: IndexedTx { - block_height: 1.into(), - block_index: TxIndex(1), - batch_index: None, - }, - kind: MaspTxKind::Transfer, - }, - masp_tx.clone(), - ))) - .expect("Test failed"); - let (_shutdown_send, shutdown_sig) = shutdown_signal(); + let (client, _masp_tx_sender) = TestingMaspClient::new(3.into()); + let (_send, shutdown_sig) = shutdown_signal(); let config = ShieldedSyncConfig::builder() - .client(client) .fetched_tracker(DevNullProgressBar) .scanned_tracker(DevNullProgressBar) .applied_tracker(DevNullProgressBar) + .shutdown_signal(shutdown_sig) + .client(client) .retry_strategy(RetryStrategy::Times(0)) + .block_batch_size(1) + .build(); + + MaspLocalTaskEnv::new(4) + .expect("Test failed") + .run(|s| async { + let mut dispatcher = config.clone().dispatcher(s, &utils).await; + + let vk = arbitrary_vk(); + + fn itx(block_height: BlockHeight) -> MaspIndexedTx { + MaspIndexedTx { + kind: MaspTxKind::Transfer, + indexed_tx: IndexedTx { + block_height, + block_index: TxIndex(0), + batch_index: None, + }, + } + } + + dispatcher.ctx.synced_height = BlockHeight(123); + dispatcher.birthdays.insert(vk, BlockHeight(456)); + assert!( + !dispatcher.vk_is_outdated(&vk, &itx(BlockHeight(300))) + ); + + dispatcher.birthdays.insert(vk, BlockHeight(6)); + assert!(dispatcher.vk_is_outdated(&vk, &itx(BlockHeight(300)))); + + dispatcher.birthdays.swap_remove(&vk); + assert!(dispatcher.vk_is_outdated(&vk, &itx(BlockHeight(300)))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_no_blocks_to_sync() { + let temp_dir = tempdir().unwrap(); + let utils = FsShieldedUtils { + context_dir: temp_dir.path().to_path_buf(), + }; + let (client, _masp_tx_sender) = TestingMaspClient::new(3.into()); + let (_send, shutdown_sig) = shutdown_signal(); + let config = ShieldedSyncConfig::builder() + .fetched_tracker(DevNullProgressBar) + .scanned_tracker(DevNullProgressBar) + .applied_tracker(DevNullProgressBar) .shutdown_signal(shutdown_sig) + .client(client) + .retry_strategy(RetryStrategy::Times(0)) + .block_batch_size(1) .build(); - shielded_ctx - .sync( - MaspLocalTaskEnv::new(4).unwrap(), - config.clone(), - None, - &[sk], - &[vk], - ) - .await - .expect("Test failed"); - let birthdays = shielded_ctx - .vk_heights - .values() - .cloned() - .collect::>(); - assert_eq!( - birthdays, - vec![ - Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(BlockHeight(30)), - kind: MaspTxKind::Transfer - }), - Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(BlockHeight(10)), - kind: MaspTxKind::Transfer - }) - ] - ); - // Test two cases: - // * A birthday is less than the synced height of key - // * A birthday is greater than the synced height of key - vk.birthday = 5.into(); - sk.birthday = 60.into(); - masp_tx_sender - .send(Some(( - MaspIndexedTx { - indexed_tx: IndexedTx { - block_height: 1.into(), - block_index: TxIndex(1), - batch_index: None, - }, - kind: MaspTxKind::Transfer, - }, - masp_tx.clone(), - ))) - .expect("Test failed"); - shielded_ctx - .sync( - MaspLocalTaskEnv::new(4).unwrap(), - config, - None, - &[sk], - &[vk], - ) - .await - .expect("Test failed"); - let birthdays = shielded_ctx - .vk_heights - .values() - .cloned() - .collect::>(); - assert_eq!( - birthdays, - vec![ - Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(BlockHeight(60)), - kind: MaspTxKind::Transfer - }), - Some(MaspIndexedTx { - indexed_tx: IndexedTx::entire_block(BlockHeight(10)), - kind: MaspTxKind::Transfer - }) - ] - ) + MaspLocalTaskEnv::new(2) + .expect("Test failed") + .run(|s| async { + let mut dispatcher = config.clone().dispatcher(s, &utils).await; + dispatcher.ctx.synced_height = BlockHeight(3); + let initial_state = dispatcher + .perform_initial_setup(None, &[], &[dated_arbitrary_vk()]) + .await + .unwrap(); + assert!(initial_state.synced); + }) + .await; + + MaspLocalTaskEnv::new(2) + .expect("Test failed") + .run(|s| async { + let mut dispatcher = config.clone().dispatcher(s, &utils).await; + dispatcher.ctx.synced_height = BlockHeight(2); + let initial_state = dispatcher + .perform_initial_setup(None, &[], &[dated_arbitrary_vk()]) + .await + .unwrap(); + assert!(!initial_state.synced); + }) + .await; } } diff --git a/crates/shielded_token/src/masp/shielded_sync/mod.rs b/crates/shielded_token/src/masp/shielded_sync/mod.rs index aa4e62dcb4b..5b11b84efd7 100644 --- a/crates/shielded_token/src/masp/shielded_sync/mod.rs +++ b/crates/shielded_token/src/masp/shielded_sync/mod.rs @@ -4,6 +4,7 @@ use std::future::Future; use std::ops::ControlFlow; #[cfg(not(target_family = "wasm"))] +#[cfg(feature = "std")] use eyre::eyre; use masp_primitives::sapling::ViewingKey; use masp_primitives::sapling::note_encryption::{ diff --git a/crates/shielded_token/src/masp/shielded_sync/utils.rs b/crates/shielded_token/src/masp/shielded_sync/utils.rs index 31f95ae3816..d23a662d46a 100644 --- a/crates/shielded_token/src/masp/shielded_sync/utils.rs +++ b/crates/shielded_token/src/masp/shielded_sync/utils.rs @@ -15,6 +15,8 @@ use namada_tx::IndexedTx; use namada_tx::event::MaspEventKind; use serde::{Deserialize, Serialize}; +use crate::masp::NotePosition; + /// The type of a MASP transaction #[derive( Debug, @@ -235,8 +237,12 @@ impl TrialDecrypted { } /// Check if the tx with [`MaspIndexedTx`] was successfully decrypted - pub fn has_indexed_tx(&self, ix: &MaspIndexedTx) -> bool { - self.inner.contains_key(ix) + pub fn decrypted_by_any_vk(&self, ix: &MaspIndexedTx) -> bool { + self.inner.get(ix).is_some_and(|viewing_keys_to_notes| { + viewing_keys_to_notes + .values() + .any(|decrypted_notes| !decrypted_notes.is_empty()) + }) } } @@ -270,7 +276,7 @@ impl Fetched { /// Iterates over the fetched transactions in the order /// they appear in blocks, whilst taking ownership of /// the returned data. - pub fn take(&mut self) -> impl IntoIterator { + pub fn take(&mut self) -> IndexedNoteData { std::mem::take(&mut self.txs) } @@ -338,40 +344,55 @@ impl RetryStrategy { /// Enumerates the capabilities of a [`MaspClient`] implementation. #[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum MaspClientCapabilities { - /// The masp client implementation is only capable of fetching shielded - /// transfers. - OnlyTransfers, - /// The masp client implementation is capable of not only fetching shielded - /// transfers, but also of fetching commitment trees, witness maps, and - /// note maps. - AllData, -} +pub struct MaspClientCapabilities(u8); impl MaspClientCapabilities { - /// Check if the lack of one or more capabilities in the - /// masp client implementation warrants a manual update - /// of the witnesses map. - pub const fn needs_witness_map_update(&self) -> bool { - matches!(self, Self::OnlyTransfers) + #[allow(missing_docs)] + pub const MAY_FETCH_PRE_BUILT_NOTE_INDEX: Self = Self(0b00000010); + #[allow(missing_docs)] + pub const MAY_FETCH_PRE_BUILT_TREE: Self = Self(0b00000001); + #[allow(missing_docs)] + pub const MAY_FETCH_PRE_BUILT_WITNESS_MAP: Self = Self(0b00000100); + #[allow(missing_docs)] + pub const NONE: Self = Self(0); + + /// Add `other` to the current set of capabilities. + pub const fn plus(self, other: Self) -> Self { + Self(self.0 | other.0) + } + + /// Subtract `other` from the current set of capabilities. + pub const fn minus(self, other: Self) -> Self { + Self(self.0 & !other.0) + } + + /// Check if `other` has a subset of the [`MaspClientCapabilities`] of + /// `self`. + pub const fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } + + /// Check whether there are no [`MaspClientCapabilities`]. + pub const fn are_none(self) -> bool { + self.0 == 0 } /// Check if the masp client is able to fetch a pre-built /// commitment tree. pub const fn may_fetch_pre_built_tree(&self) -> bool { - matches!(self, Self::AllData) + self.contains(Self::MAY_FETCH_PRE_BUILT_TREE) } /// Check if the masp client is able to fetch a pre-built - /// notes index. - pub const fn may_fetch_pre_built_notes_index(&self) -> bool { - matches!(self, Self::AllData) + /// note index. + pub const fn may_fetch_pre_built_note_index(&self) -> bool { + self.contains(Self::MAY_FETCH_PRE_BUILT_NOTE_INDEX) } /// Check if the masp client is able to fetch a pre-built /// witness map. pub const fn may_fetch_pre_built_witness_map(&self) -> bool { - matches!(self, Self::AllData) + self.contains(Self::MAY_FETCH_PRE_BUILT_WITNESS_MAP) } } @@ -382,6 +403,16 @@ pub trait MaspClient: Clone { /// Error type returned by the methods of this trait type Error: std::error::Error + Send + Sync + 'static; + /// Hint to this [`MaspClient`] implementation the block range + /// that will be fetched. + /// + /// ## Use-case + /// + /// This function is primarily used to decide when it is optimal + /// (latency wise) to fetch a block index (i.e. bloom filter) with + /// the block heights that contain MASP txs. + fn hint(&mut self, from: BlockHeight, to: BlockHeight); + /// Return the last block height we can retrieve data from. #[allow(async_fn_in_trait)] async fn last_block_height( @@ -413,14 +444,14 @@ pub trait MaspClient: Clone { async fn fetch_note_index( &self, height: BlockHeight, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; /// Fetch the witness map of height `height`. #[allow(async_fn_in_trait)] async fn fetch_witness_map( &self, height: BlockHeight, - ) -> Result>, Self::Error>; + ) -> Result>, Self::Error>; /// Check whether the given commitment anchor exists #[allow(async_fn_in_trait)] diff --git a/crates/shielded_token/src/masp/shielded_wallet.rs b/crates/shielded_token/src/masp/shielded_wallet.rs index 45da616002d..bca12d9e090 100644 --- a/crates/shielded_token/src/masp/shielded_wallet.rs +++ b/crates/shielded_token/src/masp/shielded_wallet.rs @@ -1,7 +1,10 @@ //! The shielded wallet implementation + use std::collections::{BTreeMap, BTreeSet, btree_map}; +use std::fmt; +use std::io::{Read, Write}; -use eyre::{Context, eyre}; +use eyre::{ContextCompat, WrapErr, eyre}; use masp_primitives::asset_type::AssetType; #[cfg(feature = "mainnet")] use masp_primitives::consensus::MainNetwork as Network; @@ -10,9 +13,7 @@ use masp_primitives::consensus::TestNetwork as Network; use masp_primitives::convert::AllowedConversion; use masp_primitives::ff::PrimeField; use masp_primitives::memo::MemoBytes; -use masp_primitives::merkle_tree::{ - CommitmentTree, IncrementalWitness, MerklePath, -}; +use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::{ Diversifier, Node, Note, Nullifier, ViewingKey, }; @@ -49,15 +50,17 @@ use namada_tx::IndexedTx; use namada_wallet::{DatedKeypair, DatedSpendingKey}; use rand::prelude::StdRng; use rand_core::{OsRng, SeedableRng}; +use serde::{Deserialize, Serialize}; use super::utils::{MaspIndexedTx, TrialDecrypted}; +use crate::masp::bridge_tree::BridgeTree; use crate::masp::utils::MaspClient; use crate::masp::wallet_migrations::VersionedWalletRef; use crate::masp::{ ContextSyncStatus, Conversions, MaspAmount, MaspDataLogEntry, MaspFeeData, MaspTransferData, MaspTxCombinedData, NETWORK, NoteIndex, ShieldedSyncConfig, ShieldedTransfer, ShieldedUtils, SpentNotesTracker, - TransferErr, WalletMap, WitnessMap, + TransferErr, WalletMap, }; #[cfg(any(test, feature = "testing"))] use crate::masp::{ENV_VAR_MASP_TEST_SEED, testing}; @@ -114,48 +117,211 @@ pub struct EpochedConversions { pub epoch: MaspEpoch, } +/// Position of a note in the commitment tree. +/// +/// This value corresponds to the number of leaf nodes in the +/// tree before the current note. +#[derive( + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + Debug, + Default, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, +)] +#[repr(transparent)] +pub struct NotePosition(pub u64); + +impl From for NotePosition { + #[inline] + fn from(pos: u64) -> Self { + Self(pos) + } +} + +impl From for u64 { + #[inline] + fn from(NotePosition(pos): NotePosition) -> u64 { + pos + } +} + +impl From for crate::masp::bridge_tree::pkg::Position { + #[inline] + fn from(NotePosition(pos): NotePosition) -> Self { + Self::from(pos) + } +} + +impl NotePosition { + #[allow(missing_docs)] + pub fn checked_add>(self, other: T) -> Option { + let other = other.try_into().ok()?; + Some(Self(self.0.checked_add(other)?)) + } + + #[allow(missing_docs)] + pub fn checked_sub>(self, other: T) -> Option { + let other = other.try_into().ok()?; + Some(Self(self.0.checked_sub(other)?)) + } +} + +impl fmt::Display for NotePosition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Compact representation of a [`Note`]. +#[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] +pub struct CompactNote { + pub asset_type: AssetType, + pub value: u64, + pub diversifier: Diversifier, + pub pk_d: masp_primitives::jubjub::SubgroupPoint, + pub rseed: masp_primitives::sapling::Rseed, +} + +impl BorshSerialize for CompactNote { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + use group::GroupEncoding; + + BorshSerialize::serialize(&self.asset_type, writer)?; + BorshSerialize::serialize(&self.value, writer)?; + BorshSerialize::serialize(&self.diversifier, writer)?; + BorshSerialize::serialize(&self.pk_d.to_bytes(), writer)?; + BorshSerialize::serialize(&self.rseed, writer) + } +} + +impl BorshDeserialize for CompactNote { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + use group::GroupEncoding; + + let asset_type = + ::deserialize_reader(reader)?; + let value = ::deserialize_reader(reader)?; + let diversifier = + ::deserialize_reader(reader)?; + let pk_d = masp_primitives::jubjub::SubgroupPoint::from_bytes( + &<[u8; 32] as BorshDeserialize>::deserialize_reader(reader)?, + ) + .into_option() + .ok_or_else(|| std::io::Error::other("Invalid pk_d in CompactNote"))?; + let rseed = ::deserialize_reader(reader)?; + + Ok(Self { + asset_type, + value, + diversifier, + pk_d, + rseed, + }) + } +} + +impl CompactNote { + /// Create a compact version of a [`Note`]. + pub fn new( + note: Note, + pa: masp_primitives::sapling::PaymentAddress, + ) -> Option { + let g_d = pa.g_d()?; + let pk_d = *pa.pk_d(); + + if g_d != note.g_d || pk_d != note.pk_d { + return None; + } + + let diversifier = *pa.diversifier(); + let Note { + asset_type, + value, + pk_d, + rseed, + .. + } = note; + + Some(Self { + asset_type, + value, + diversifier, + pk_d, + rseed, + }) + } + + /// Convert this [`CompactNote`] back into a [`Note`]. + pub fn into_note(self) -> Option { + let g_d = self.diversifier.g_d()?; + + let Self { + asset_type, + value, + pk_d, + rseed, + .. + } = self; + + Some(Note { + asset_type, + value, + g_d, + pk_d, + rseed, + }) + } +} + /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. -#[derive(BorshSerialize, BorshDeserialize, Debug)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Default)] pub struct ShieldedWallet { /// Location where this shielded context is saved #[borsh(skip)] pub utils: U, - /// The commitment tree produced by scanning all transactions up to tx_pos - pub tree: CommitmentTree, - /// Maps viewing keys to the block height to which they are synced. - /// In particular, the height given by the value *has been scanned*. - pub vk_heights: BTreeMap>, + /// The commitment tree produced by scanning all transactions in the MASP + pub tree: BridgeTree, + /// Block height to which the wallet has been synced to + pub synced_height: BlockHeight, + /// The set of note positions that have been spent + pub spents: HashSet, /// Maps viewing keys to applicable note positions - pub pos_map: HashMap>, + /// + /// The inner set includes notes that have already been spent + pub pos_map: HashMap>, /// Maps a nullifier to the note position to which it applies - pub nf_map: HashMap, + pub nf_map: HashMap, /// Maps note positions to their corresponding notes - pub note_map: HashMap, + pub note_map: HashMap, /// Maps note positions to their corresponding memos - pub memo_map: HashMap, - /// Maps note positions to the diversifier of their payment address - pub div_map: HashMap, - /// Maps note positions to their witness (used to make merkle paths) - pub witness_map: WitnessMap, - /// The set of note positions that have been spent - pub spents: HashSet, + pub memo_map: HashMap, /// Maps asset types to their decodings pub asset_types: HashMap, /// A conversions cache pub conversions: EpochedConversions, - /// Maps note positions to their corresponding viewing keys - pub vk_map: HashMap, /// Maps a shielded tx to the index of its first output note. pub note_index: NoteIndex, + /// The sync state of the context + pub sync_status: ContextSyncStatus, + /// Maps note positions to their corresponding viewing keys + #[cfg(feature = "historic")] + pub vk_map: HashMap, /// The history of the applied shielded transactions (the failed ones won't /// show up in here). Only the sapling bundle data is cached here, for the /// transparent bundle data one should rely on querying a node or an /// indexer #[cfg(feature = "historic")] pub history: HashMap>, - /// The sync state of the context - pub sync_status: ContextSyncStatus, } /// The data for an indexed masp transaction @@ -169,32 +335,6 @@ pub struct TxHistoryEntry { pub conversions: bool, } -/// Default implementation to ease construction of TxContexts. Derive cannot be -/// used here due to CommitmentTree not implementing Default. -impl Default for ShieldedWallet { - fn default() -> ShieldedWallet { - ShieldedWallet:: { - utils: U::default(), - vk_heights: BTreeMap::new(), - note_index: BTreeMap::default(), - tree: CommitmentTree::empty(), - pos_map: HashMap::default(), - nf_map: HashMap::default(), - note_map: HashMap::default(), - memo_map: HashMap::default(), - div_map: HashMap::default(), - witness_map: HashMap::default(), - spents: HashSet::default(), - conversions: Default::default(), - asset_types: HashMap::default(), - vk_map: HashMap::default(), - #[cfg(feature = "historic")] - history: Default::default(), - sync_status: ContextSyncStatus::Confirmed, - } - } -} - impl ShieldedWallet { /// Try to load the last saved shielded wallet from the given context /// directory. If the file is missing, an empty wallet is created. @@ -230,19 +370,20 @@ impl ShieldedWallet { /// available) pub async fn save(&self) -> std::io::Result<()> { self.utils - .save(VersionedWalletRef::V1(self), self.sync_status) + .save(VersionedWalletRef::V2(self), self.sync_status) .await } /// Update the merkle tree of witnesses the first time we /// scan new MASP transactions. - pub(crate) fn update_witness_map( + pub(crate) fn update_witnesses( &mut self, masp_indexed_tx: MaspIndexedTx, shielded: &Transaction, trial_decrypted: &TrialDecrypted, ) -> Result<(), eyre::Error> { - let mut note_pos = self.tree.size(); + let mut note_pos = NotePosition(self.tree.as_ref().tree_size()); + self.note_index.insert(masp_indexed_tx, note_pos); for so in shielded @@ -251,25 +392,25 @@ impl ShieldedWallet { { // Create merkle tree leaf node from note commitment let node = Node::new(so.cmu.to_repr()); - // Update each merkle tree in the witness map with the latest - // addition - for (_, witness) in self.witness_map.iter_mut() { - witness.append(node).map_err(|()| { - eyre!("note commitment tree is full".to_string()) - })?; - } - self.tree.append(node).map_err(|()| { - eyre!("note commitment tree is full".to_string()) - })?; - if trial_decrypted.has_indexed_tx(&masp_indexed_tx) { + // Append the node to the tree + self.tree + .as_mut() + .append(node) + .wrap_err("failed to append to note commitment tree")?; + + if trial_decrypted.decrypted_by_any_vk(&masp_indexed_tx) { // Finally, make it easier to construct merkle paths to this new - // notes that we own - let witness = IncrementalWitness::::from_tree(&self.tree); - self.witness_map.insert(note_pos, witness); + // note that we own + eyre::ensure!( + self.tree.as_mut().mark().is_some(), + "could not mark node as a witness" + ); } - note_pos = checked!(note_pos + 1).unwrap(); + + checked!(note_pos += 1u64).unwrap(); } + Ok(()) } @@ -292,7 +433,7 @@ impl ShieldedWallet { let dispatcher = config.dispatcher(spawner, &self.utils).await; if let Some(updated_ctx) = - dispatcher.run(None, last_query_height, sks, fvks).await? + dispatcher.run(last_query_height, sks, fvks).await? { *self = updated_ctx; } @@ -302,28 +443,12 @@ impl ShieldedWallet { .await } - pub(crate) fn min_height_to_sync_from( - &self, - ) -> Result { - let Some(maybe_least_synced_vk_height) = - self.vk_heights.values().min().cloned() - else { - return Err(eyre!( - "No viewing keys are available in the shielded context to \ - decrypt notes with" - .to_string(), - )); - }; - Ok(maybe_least_synced_vk_height - .map_or_else(BlockHeight::first, |itx| itx.indexed_tx.block_height)) - } - #[allow(missing_docs)] pub fn save_decrypted_shielded_outputs( &mut self, #[cfg(feature = "historic")] indexed_tx: IndexedTx, vk: &ViewingKey, - note_pos: usize, + note_pos: NotePosition, note: Note, pa: masp_primitives::sapling::PaymentAddress, memo: MemoBytes, @@ -332,44 +457,55 @@ impl ShieldedWallet { // viewing key self.pos_map.entry(*vk).or_default().insert(note_pos); // Compute the nullifier now to quickly recognize when spent - let nf = note.nf( - &vk.nk, - note_pos - .try_into() - .map_err(|_| eyre!("Can not get nullifier".to_string()))?, - ); - self.note_map.insert(note_pos, note); - self.memo_map.insert(note_pos, memo); - // The payment address' diversifier is required to spend - // note - self.div_map.insert(note_pos, *pa.diversifier()); + let nf = note.nf(&vk.nk, note_pos.into()); + let compact_note = CompactNote::new(note, pa) + .context("Invalid diversifier in decrypted note")?; + self.note_map.insert(note_pos, compact_note); + if !is_empty_memo(&memo) { + self.memo_map.insert(note_pos, memo); + } self.nf_map.insert(nf, note_pos); - self.vk_map.insert(note_pos, *vk); + #[cfg(feature = "historic")] - { - // Update the history - let asset_data = self - .asset_types - .get(¬e.asset_type) - .ok_or_else(|| eyre!("Can not get the asset data"))? - .to_owned(); - let output_entry = self - .history - .entry(vk.to_owned()) - .or_default() - .entry(indexed_tx) - .or_default() - .outputs - .entry(asset_data.token) - .or_insert(Amount::zero()); - // No need to take care of the denomination as that should already - // be the default one for the given token - let note_amount = - Amount::from_masp_denominated(note.value, asset_data.position); - - *output_entry = checked!(output_entry + note_amount) - .wrap_err("Overflow in shielded history outputs")?; - } + self.save_decrypted_shielded_outputs_history( + indexed_tx, vk, note_pos, ¬e, + )?; + + Ok(()) + } + + #[cfg(feature = "historic")] + fn save_decrypted_shielded_outputs_history( + &mut self, + indexed_tx: IndexedTx, + vk: &ViewingKey, + note_pos: NotePosition, + note: &Note, + ) -> Result<(), eyre::Error> { + self.vk_map.insert(note_pos, *vk); + + // Update the history + let asset_data = self + .asset_types + .get(¬e.asset_type) + .ok_or_else(|| eyre!("Can not get the asset data"))? + .to_owned(); + let output_entry = self + .history + .entry(vk.to_owned()) + .or_default() + .entry(indexed_tx) + .or_default() + .outputs + .entry(asset_data.token) + .or_insert(Amount::zero()); + // No need to take care of the denomination as that should already + // be the default one for the given token + let note_amount = + Amount::from_masp_denominated(note.value, asset_data.position); + + *output_entry = checked!(output_entry + note_amount) + .wrap_err("Overflow in shielded history outputs")?; Ok(()) } @@ -378,13 +514,13 @@ impl ShieldedWallet { pub fn save_shielded_spends( &mut self, transaction: &Transaction, - update_witness_map: bool, + update_tree: bool, #[cfg(feature = "historic")] update_history: Option, ) -> Result<(), eyre::Error> { #[cfg(feature = "historic")] let used_conversions = transaction .sapling_bundle() - .map_or(false, |bundle| !bundle.shielded_converts.is_empty()); + .is_some_and(|bundle| !bundle.shielded_converts.is_empty()); for ss in transaction .sapling_bundle() @@ -392,57 +528,24 @@ impl ShieldedWallet { { // If the shielded spend's nullifier is in our map, then target // note is rendered unusable - if let Some(note_pos) = self.nf_map.get(&ss.nullifier) { - self.spents.insert(*note_pos); - if update_witness_map { - self.witness_map.swap_remove(note_pos); - } + if let Some(note_pos) = self.nf_map.swap_remove(&ss.nullifier) { #[cfg(feature = "historic")] - { - // Update the history if required - if let Some(indexed_tx) = update_history { - let vk = - self.vk_map.get(note_pos).ok_or_else(|| { - eyre!( - "Missing viewing key for the provided \ - note position" - ) - })?; - let note = - self.note_map.get(note_pos).ok_or_else(|| { - eyre!( - "Missing note for the provided note \ - position" - ) - })?; - let asset_data = self - .asset_types - .get(¬e.asset_type) - .ok_or_else(|| eyre!("Can not get the asset data"))? - .to_owned(); - - let history_entry = self - .history - .entry(vk.to_owned()) - .or_default() - .entry(indexed_tx) - .or_default(); - history_entry.conversions = used_conversions; - - let input_entry = history_entry - .inputs - .entry(asset_data.token) - .or_insert(Amount::zero()); - // No need to take care of the denomination as that - // should already be the default - // one for the given token - let note_amount = Amount::from_masp_denominated( - note.value, - asset_data.position, - ); - *input_entry = checked!(input_entry + note_amount) - .wrap_err("Overflow in shielded history inputs")?; - } + self.save_shielded_spends_history( + ¬e_pos, + used_conversions, + update_history, + )?; + + self.note_map.swap_remove(¬e_pos); + self.spents.insert(note_pos); + + if update_tree { + self.tree + .as_mut() + .remove_mark(note_pos.into()) + .unwrap_or_else(|err| { + panic!("Failed to remove marked leaf: {err}") + }); } } } @@ -450,6 +553,54 @@ impl ShieldedWallet { Ok(()) } + #[cfg(feature = "historic")] + fn save_shielded_spends_history( + &mut self, + note_pos: &NotePosition, + used_conversions: bool, + update_history: Option, + ) -> Result<(), eyre::Error> { + // Update the history if required + let Some(indexed_tx) = update_history else { + return Ok(()); + }; + + let vk = self.vk_map.get(note_pos).ok_or_else(|| { + eyre!("Missing viewing key for the provided note position") + })?; + let note = self.note_map.get(note_pos).ok_or_else(|| { + eyre!("Missing note for the provided note position") + })?; + let asset_data = self + .asset_types + .get(¬e.asset_type) + .ok_or_else(|| eyre!("Can not get the asset data"))? + .to_owned(); + + let history_entry = self + .history + .entry(vk.to_owned()) + .or_default() + .entry(indexed_tx) + .or_default(); + history_entry.conversions = used_conversions; + + let input_entry = history_entry + .inputs + .entry(asset_data.token) + .or_insert(Amount::zero()); + // No need to take care of the denomination as that + // should already be the default + // one for the given token + let note_amount = + Amount::from_masp_denominated(note.value, asset_data.position); + + *input_entry = checked!(input_entry + note_amount) + .wrap_err("Overflow in shielded history inputs")?; + + Ok(()) + } + /// Compute the total unspent notes associated with the viewing key in the /// context. If the key is not in the context, then we do not know the /// balance and hence we return None. @@ -558,7 +709,7 @@ impl ShieldedWallet { ) -> Result<(), eyre::Error> { self.save_shielded_spends( masp_tx, - false, + true, #[cfg(feature = "historic")] None, )?; @@ -1087,7 +1238,7 @@ pub trait ShieldedApi: #[allow(clippy::type_complexity)] fn select_note_naive( exchanged_notes: &BTreeMap< - usize, + NotePosition, ( Note, I128Sum, @@ -1097,7 +1248,7 @@ pub trait ShieldedApi: >, namada_acc: &ValueSum<(MaspDigitPos, Address), i128>, target: ValueSum<(MaspDigitPos, Address), i128>, - ) -> Option { + ) -> Option { // How much do we still need in order to arrive at target? let gap = ValueSum::zero().sup(&(target - namada_acc)); @@ -1121,9 +1272,9 @@ pub trait ShieldedApi: #[allow(clippy::type_complexity)] fn select_note_greedy( exchanged_notes: &BTreeMap< - usize, + NotePosition, ( - Note, + CompactNote, I128Sum, I128Sum, ValueSum<(MaspDigitPos, Address), i128>, @@ -1131,7 +1282,7 @@ pub trait ShieldedApi: >, namada_acc: &ValueSum<(MaspDigitPos, Address), i128>, target: ValueSum<(MaspDigitPos, Address), i128>, - ) -> Option { + ) -> Option { let mut max_coverage = I256::zero(); let mut min_projection = ValueSum::<(MaspDigitPos, Address), i128>::zero(); @@ -1197,9 +1348,9 @@ pub trait ShieldedApi: conversions: &mut Conversions, ) -> Result< BTreeMap< - usize, + NotePosition, ( - Note, + CompactNote, I128Sum, I128Sum, ValueSum<(MaspDigitPos, Address), i128>, @@ -1279,10 +1430,8 @@ pub trait ShieldedApi: target: ValueSum<(MaspDigitPos, Address), i128>, conversions: &mut Conversions, usages: &mut I128Sum, - ) -> Result< - (I128Sum, Vec<(Diversifier, Note, MerklePath)>), - eyre::Error, - > { + ) -> Result<(I128Sum, Vec<(CompactNote, MerklePath)>), eyre::Error> + { let vk = &sk.to_viewing_key().fvk.vk; let mut namada_acc = ValueSum::zero(); let mut masp_acc = I128Sum::zero(); @@ -1313,18 +1462,12 @@ pub trait ShieldedApi: // Commit the conversions that were used to exchange *usages += proposed_usages; - let merkle_path = self - .witness_map - .get(¬e_idx) - .ok_or_else(|| eyre!("Unable to get note {note_idx}"))? - .path() - .ok_or_else(|| eyre!("Unable to get path: {}", line!()))?; - let diversifier = self - .div_map - .get(¬e_idx) - .ok_or_else(|| eyre!("Unable to get note {note_idx}"))?; + let merkle_path = + self.tree.witness(note_idx).wrap_err_with(|| { + format!("Unable to get merkle path to note {note_idx}") + })?; // Commit this note to our transaction - notes.push((*diversifier, note, merkle_path)); + notes.push((note, merkle_path)); // Append the note the list of used ones spent_notes .entry(vk.to_owned()) @@ -1941,7 +2084,16 @@ pub trait ShieldedApi: } // Commit the notes found to our transaction - for (diversifier, note, merkle_path) in unspent_notes { + for (compact_note, merkle_path) in unspent_notes { + let diversifier = compact_note.diversifier; + let note = compact_note.into_note().ok_or_else(|| { + TransferErr::General( + "Invalid diversifier in decrypted note was stored in \ + shielded wallet" + .into(), + ) + })?; + builder .add_sapling_spend(sk, diversifier, note, merkle_path) .map_err(|e| TransferErr::Build { @@ -2167,6 +2319,14 @@ impl> { } +/// Check whether the given utxo memo is empty (i.e. equal to +/// [`MemoBytes::empty`]). +#[inline] +pub fn is_empty_memo(memo: &MemoBytes) -> bool { + let memo_bytes = memo.as_array(); + memo_bytes[0] == 0xf6 && memo_bytes[1..].iter().all(|&byte| byte == 0) +} + #[cfg(test)] mod test_shielded_wallet { use namada_core::address::InternalAddress; diff --git a/crates/shielded_token/src/masp/test_utils.rs b/crates/shielded_token/src/masp/test_utils.rs index 26a7c55a542..810130397e7 100644 --- a/crates/shielded_token/src/masp/test_utils.rs +++ b/crates/shielded_token/src/masp/test_utils.rs @@ -8,7 +8,7 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::{ CommitmentTree, IncrementalWitness, MerklePath, }; -use masp_primitives::sapling::{Node, Note, Rseed, ViewingKey}; +use masp_primitives::sapling::{Node, Rseed, ViewingKey}; use masp_primitives::transaction::Transaction; use masp_primitives::transaction::components::I128Sum; use masp_primitives::zip32::ExtendedFullViewingKey; @@ -30,11 +30,11 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use super::utils::MaspIndexedTx; use crate::ShieldedWallet; -use crate::masp::ShieldedUtils; -use crate::masp::shielded_wallet::ShieldedQueries; +use crate::masp::shielded_wallet::{CompactNote, ShieldedQueries}; use crate::masp::utils::{ IndexedNoteEntry, MaspClient, MaspClientCapabilities, }; +use crate::masp::{NotePosition, ShieldedUtils}; /// A viewing key derived from A_SPENDING_KEY pub const AA_VIEWING_KEY: &str = "zvknam1qqqqqqqqqqqqqq9v0sls5r5de7njx8ehu49pqgmqr9ygelg87l5x8y4s9r0pjlvu6x74w9gjpw856zcu826qesdre628y6tjc26uhgj6d9zqur9l5u3p99d9ggc74ald6s8y3sdtka74qmheyqvdrasqpwyv2fsmxlz57lj4grm2pthzj3sflxc0jx0edrakx3vdcngrfjmru8ywkguru8mxss2uuqxdlglaz6undx5h8w7g70t2es850g48xzdkqay5qs0yw06rtxcpjdve6"; @@ -386,6 +386,8 @@ pub enum TestError { impl MaspClient for TestingMaspClient { type Error = TestError; + fn hint(&mut self, _from: BlockHeight, _to: BlockHeight) {} + async fn last_block_height( &self, ) -> Result, Self::Error> { @@ -412,7 +414,7 @@ impl MaspClient for TestingMaspClient { #[inline(always)] fn capabilities(&self) -> MaspClientCapabilities { - MaspClientCapabilities::OnlyTransfers + MaspClientCapabilities::NONE } async fn fetch_commitment_tree( @@ -427,7 +429,7 @@ impl MaspClient for TestingMaspClient { async fn fetch_note_index( &self, _: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { unimplemented!( "Transaction notes map fetching is not implemented by this client" ) @@ -436,7 +438,8 @@ impl MaspClient for TestingMaspClient { async fn fetch_witness_map( &self, _: BlockHeight, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> + { unimplemented!("Witness map fetching is not implemented by this client") } @@ -465,25 +468,28 @@ impl TestingContext { } /// Add a note to a given viewing key - pub fn add_note(&mut self, note: Note, vk: ViewingKey) { + pub fn add_note(&mut self, note: CompactNote, vk: ViewingKey) { let next_note_idx = self .wallet .note_map .keys() .max() - .map(|ix| ix + 1) + .map(|ix| ix.checked_add(1).unwrap()) .unwrap_or_default(); self.wallet.note_map.insert(next_note_idx, note); let avail_notes = self.wallet.pos_map.entry(vk).or_default(); avail_notes.insert(next_note_idx); } - pub fn spend_note(&mut self, note: &Note) { + pub fn spend_note(&mut self, note: &CompactNote) { + let other = note.into_note().unwrap(); let idx = self .wallet .note_map .iter() - .find(|(_, v)| *v == note) + .find(|(_, compact_note)| { + compact_note.into_note().unwrap() == other + }) .map(|(idx, _)| idx) .expect("Could find the note to spend in the note map"); self.wallet.spents.insert(*idx); @@ -675,11 +681,11 @@ pub fn create_note( asset_data: AssetData, value: u64, pa: PaymentAddress, -) -> Note { +) -> CompactNote { let payment_addr: masp_primitives::sapling::PaymentAddress = pa.into(); - Note { + CompactNote { value, - g_d: payment_addr.g_d().unwrap(), + diversifier: *payment_addr.diversifier(), pk_d: *payment_addr.pk_d(), asset_type: asset_data.encode().unwrap(), rseed: Rseed::AfterZip212([0; 32]), diff --git a/crates/shielded_token/src/masp/wallet_migrations.rs b/crates/shielded_token/src/masp/wallet_migrations.rs index 3d0f2c36bd8..2f9d700beca 100644 --- a/crates/shielded_token/src/masp/wallet_migrations.rs +++ b/crates/shielded_token/src/masp/wallet_migrations.rs @@ -15,7 +15,9 @@ pub enum VersionedWallet { /// Version 0 V0(v0::ShieldedWallet), /// Version 1 - V1(ShieldedWallet), + V1(v1::ShieldedWallet), + /// Version 2 + V2(ShieldedWallet), } impl VersionedWallet { @@ -24,7 +26,8 @@ impl VersionedWallet { pub fn migrate(self) -> eyre::Result> { match self { VersionedWallet::V0(w) => Ok(w.into()), - VersionedWallet::V1(w) => Ok(w), + VersionedWallet::V1(w) => Ok(w.into()), + VersionedWallet::V2(w) => Ok(w), } } } @@ -35,11 +38,135 @@ pub enum VersionedWalletRef<'w, U: ShieldedUtils> { /// Version 0 V0(&'w v0::ShieldedWallet), /// Version 1 - V1(&'w ShieldedWallet), + V1(&'w v1::ShieldedWallet), + /// Version 2 + V2(&'w ShieldedWallet), +} + +mod migrations { + use masp_primitives::merkle_tree::CommitmentTree; + use masp_primitives::sapling::{Diversifier, Node, Note}; + use namada_core::collections::HashMap; + + use crate::masp::bridge_tree::BridgeTree; + use crate::masp::shielded_wallet::CompactNote; + use crate::masp::{NotePosition, WitnessMap}; + + #[allow(missing_docs, dead_code)] + pub fn migrate_note_map( + note_map: HashMap, + mut div_map: HashMap, + ) -> HashMap { + let mut migrated = HashMap::new(); + + for (pos, note) in note_map { + let diversifier = div_map + .swap_remove(&pos) + .expect("Missing diversifier in shielded wallet"); + + let Note { + asset_type, + value, + pk_d, + rseed, + .. + } = note; + + migrated.insert( + NotePosition(pos.try_into().unwrap()), + CompactNote { + asset_type, + value, + diversifier, + pk_d, + rseed, + }, + ); + } + + migrated + } + + #[allow(missing_docs, dead_code)] + pub fn migrate_bridge_tree( + tree: &CommitmentTree, + witness_map: &WitnessMap, + ) -> BridgeTree { + BridgeTree::from_tree_and_witness_map(tree.clone(), witness_map.clone()) + .unwrap() + } + + #[cfg(test)] + #[test] + fn test_bridge_tree_migrations() { + use masp_primitives::merkle_tree::IncrementalWitness; + + use crate::masp::NotePosition; + + let mut tree: CommitmentTree = CommitmentTree::empty(); + let mut witness_map = WitnessMap::new(); + + // build commitment tree and witness map incrementally + for i in 0u64..10 { + let node = Node::from_scalar(i.into()); + + tree.append(node).unwrap(); + + for wit in witness_map.values_mut() { + wit.append(node).unwrap(); + } + + if i % 2 == 0 { + witness_map + .insert(i.into(), IncrementalWitness::from_tree(&tree)); + } + } + + // convert to bridge tree + let mut bridge_tree = migrate_bridge_tree(&tree, &witness_map); + + for i in (0u64..10).filter(|&i| i % 2 == 0) { + assert_eq!( + witness_map[&NotePosition::from(i)].path(), + bridge_tree.witness(i), + "Position {i} not equal,\n{witness_map:#?}\n{bridge_tree:#?}" + ); + } + + // add new incremental witnesses + for i in 10u64..20 { + let node = Node::from_scalar(i.into()); + + tree.append(node).unwrap(); + bridge_tree.as_mut().append(node).unwrap(); + + for wit in witness_map.values_mut() { + wit.append(node).unwrap(); + } + + if i % 2 == 0 { + witness_map + .insert(i.into(), IncrementalWitness::from_tree(&tree)); + bridge_tree.as_mut().mark().unwrap(); + } + } + + // check if roots and merkle proofs match + assert_eq!(tree.root(), bridge_tree.as_ref().root()); + + for i in (0u64..20).filter(|&i| i % 2 == 0) { + assert_eq!( + witness_map[&NotePosition::from(i)].path(), + bridge_tree.witness(i), + "Position {i} not equal,\n{witness_map:#?}\n{bridge_tree:#?}" + ); + } + } } -/// Version 0 of the shielded wallet, which is used for migration purposes. pub mod v0 { + //! Version 0 of the shielded wallet, which is used for migration purposes. + use std::collections::{BTreeMap, BTreeSet}; use masp_primitives::asset_type::AssetType; @@ -51,11 +178,7 @@ pub mod v0 { use namada_core::borsh::{BorshDeserialize, BorshSerialize}; use namada_core::collections::{HashMap, HashSet}; use namada_core::masp::AssetData; - #[cfg(feature = "historic")] - use namada_tx::IndexedTx; - #[cfg(feature = "historic")] - use crate::masp::shielded_wallet::TxHistoryEntry; use crate::masp::utils::MaspIndexedTx; use crate::masp::{ ContextSyncStatus, NoteIndex, ShieldedUtils, WitnessMap, @@ -93,15 +216,10 @@ pub mod v0 { pub vk_map: HashMap, /// Maps a shielded tx to the index of its first output note. pub note_index: NoteIndex, - /// The history of the applied shielded transactions (the failed ones - /// won't show up in here). Only the sapling bundle data is - /// cached here, for the transparent bundle data one should - /// rely on querying a node or an indexer - #[cfg(feature = "historic")] - pub history: HashMap>, /// The sync state of the context pub sync_status: ContextSyncStatus, } + impl Default for ShieldedWallet { fn default() -> ShieldedWallet { ShieldedWallet:: { @@ -118,8 +236,6 @@ pub mod v0 { spents: HashSet::default(), asset_types: HashMap::default(), vk_map: HashMap::default(), - #[cfg(feature = "historic")] - history: Default::default(), sync_status: ContextSyncStatus::Confirmed, } } @@ -127,24 +243,214 @@ pub mod v0 { impl From> for super::ShieldedWallet { fn from(wallet: ShieldedWallet) -> Self { - Self { - utils: wallet.utils, - tree: wallet.tree, - vk_heights: wallet.vk_heights, - pos_map: wallet.pos_map, - nf_map: wallet.nf_map, - note_map: wallet.note_map, - memo_map: wallet.memo_map, - div_map: wallet.div_map, - witness_map: wallet.witness_map, - spents: wallet.spents, - asset_types: wallet.asset_types, + #[cfg(not(feature = "historic"))] + { + use super::migrations; + use crate::masp::NotePosition; + + Self { + utils: wallet.utils, + tree: migrations::migrate_bridge_tree( + &wallet.tree, + &wallet.witness_map, + ), + synced_height: wallet + .vk_heights + .into_values() + .filter_map(|itx| Some(itx?.indexed_tx.block_height)) + .max() + .unwrap_or_default(), + pos_map: wallet + .pos_map + .into_iter() + .map(|(vk, positions)| { + ( + vk, + positions + .into_iter() + .map(|pos| { + NotePosition(pos.try_into().unwrap()) + }) + .collect(), + ) + }) + .collect(), + nf_map: wallet + .nf_map + .into_iter() + .map(|(nf, pos)| { + (nf, NotePosition(pos.try_into().unwrap())) + }) + .collect(), + note_map: migrations::migrate_note_map( + wallet.note_map, + wallet.div_map, + ), + memo_map: wallet + .memo_map + .into_iter() + .map(|(pos, memo)| { + (NotePosition(pos.try_into().unwrap()), memo) + }) + .collect(), + spents: wallet + .spents + .into_iter() + .map(|pos| NotePosition(pos.try_into().unwrap())) + .collect(), + asset_types: wallet.asset_types, + conversions: Default::default(), + note_index: wallet.note_index, + sync_status: wallet.sync_status, + } + } + #[cfg(feature = "historic")] + { + drop(wallet); + + // NB: Need to return an empty wallet because + // we can not rebuild the shielded history. + Default::default() + } + } + } +} + +pub mod v1 { + //! Version 1 of the shielded wallet, which is used for migration purposes. + + #![allow(missing_docs)] + + use std::collections::{BTreeMap, BTreeSet}; + + use masp_primitives::asset_type::AssetType; + use masp_primitives::memo::MemoBytes; + use masp_primitives::merkle_tree::CommitmentTree; + use masp_primitives::sapling::{ + Diversifier, Node, Note, Nullifier, ViewingKey, + }; + use namada_core::borsh::{BorshDeserialize, BorshSerialize}; + use namada_core::collections::{HashMap, HashSet}; + use namada_core::masp::AssetData; + + use crate::masp::shielded_wallet::EpochedConversions; + use crate::masp::utils::MaspIndexedTx; + use crate::masp::{ + ContextSyncStatus, NoteIndex, ShieldedUtils, WitnessMap, + }; + + #[derive(BorshSerialize, BorshDeserialize, Debug)] + pub struct ShieldedWallet { + #[borsh(skip)] + pub utils: U, + pub tree: CommitmentTree, + pub vk_heights: BTreeMap>, + pub pos_map: HashMap>, + pub nf_map: HashMap, + pub note_map: HashMap, + pub memo_map: HashMap, + pub div_map: HashMap, + pub witness_map: WitnessMap, + pub spents: HashSet, + pub asset_types: HashMap, + pub conversions: EpochedConversions, + pub vk_map: HashMap, + pub note_index: NoteIndex, + pub sync_status: ContextSyncStatus, + } + + impl Default for ShieldedWallet { + fn default() -> ShieldedWallet { + ShieldedWallet:: { + utils: U::default(), + vk_heights: BTreeMap::new(), + note_index: BTreeMap::default(), + tree: CommitmentTree::empty(), + pos_map: HashMap::default(), + nf_map: HashMap::default(), + note_map: HashMap::default(), + memo_map: HashMap::default(), + div_map: HashMap::default(), + witness_map: HashMap::default(), + spents: HashSet::default(), conversions: Default::default(), - vk_map: wallet.vk_map, - note_index: wallet.note_index, - sync_status: wallet.sync_status, - #[cfg(feature = "historic")] - history: wallet.history, + asset_types: HashMap::default(), + vk_map: HashMap::default(), + sync_status: ContextSyncStatus::Confirmed, + } + } + } + + impl From> for super::ShieldedWallet { + fn from(wallet: ShieldedWallet) -> Self { + #[cfg(not(feature = "historic"))] + { + use super::migrations; + use crate::masp::NotePosition; + + Self { + utils: wallet.utils, + tree: migrations::migrate_bridge_tree( + &wallet.tree, + &wallet.witness_map, + ), + synced_height: wallet + .vk_heights + .into_values() + .filter_map(|itx| Some(itx?.indexed_tx.block_height)) + .max() + .unwrap_or_default(), + pos_map: wallet + .pos_map + .into_iter() + .map(|(vk, positions)| { + ( + vk, + positions + .into_iter() + .map(|pos| { + NotePosition(pos.try_into().unwrap()) + }) + .collect(), + ) + }) + .collect(), + nf_map: wallet + .nf_map + .into_iter() + .map(|(nf, pos)| { + (nf, NotePosition(pos.try_into().unwrap())) + }) + .collect(), + note_map: migrations::migrate_note_map( + wallet.note_map, + wallet.div_map, + ), + memo_map: wallet + .memo_map + .into_iter() + .map(|(pos, memo)| { + (NotePosition(pos.try_into().unwrap()), memo) + }) + .collect(), + spents: wallet + .spents + .into_iter() + .map(|pos| NotePosition(pos.try_into().unwrap())) + .collect(), + asset_types: wallet.asset_types, + conversions: wallet.conversions, + note_index: wallet.note_index, + sync_status: wallet.sync_status, + } + } + #[cfg(feature = "historic")] + { + drop(wallet); + + // NB: Need to return an empty wallet because + // we can not rebuild the shielded history. + Default::default() } } } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index e8c971b4015..dbf4b22ea70 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -673,9 +673,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ "borsh-derive", "cfg_aliases", @@ -683,9 +683,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" dependencies = [ "once_cell", "proc-macro-crate", @@ -1575,7 +1575,7 @@ dependencies = [ "chrono", "rust_decimal", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "winnow 0.6.26", ] @@ -2408,7 +2408,7 @@ dependencies = [ "rand_core", "serde", "serdect", - "thiserror 2.0.11", + "thiserror 2.0.12", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -3911,15 +3911,6 @@ dependencies = [ "serde", ] -[[package]] -name = "init-once" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0863329819ed5ecf33446da6cb9104d2f8943ff8530d2b6c51adbc6be4f0632" -dependencies = [ - "portable-atomic", -] - [[package]] name = "inout" version = "0.1.3" @@ -4123,6 +4114,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leanbridgetree" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c255f8e570ab4acef90db79d99171bd5882615fb54d24591040fa69d80943e09" +dependencies = [ + "borsh", + "incrementalmerkletree", + "slab", + "thiserror 2.0.12", +] + [[package]] name = "leb128" version = "0.2.5" @@ -4518,7 +4521,7 @@ dependencies = [ "pasta_curves", "rand_core", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -4588,7 +4591,7 @@ version = "0.251.0" dependencies = [ "namada_core", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4630,7 +4633,7 @@ dependencies = [ "smooth-operator", "tendermint 0.40.3", "tendermint-proto 0.40.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-keccak", "tokio", "tracing", @@ -4665,7 +4668,7 @@ dependencies = [ "namada_vp_env", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4678,7 +4681,7 @@ dependencies = [ "namada_macros", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4691,7 +4694,7 @@ dependencies = [ "namada_events", "namada_macros", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4713,7 +4716,7 @@ dependencies = [ "serde", "serde_json", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4749,7 +4752,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4761,7 +4764,7 @@ dependencies = [ "kdam", "namada_core", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -4787,7 +4790,7 @@ dependencies = [ "namada_core", "namada_macros", "prost", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4801,7 +4804,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4824,7 +4827,7 @@ dependencies = [ "proptest", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4854,7 +4857,6 @@ dependencies = [ "fd-lock", "futures", "getrandom 0.3.1", - "init-once", "itertools 0.14.0", "lazy_static", "masp_primitives", @@ -4898,7 +4900,7 @@ dependencies = [ "smooth-operator", "tempfile", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "tokio", "toml", @@ -4916,8 +4918,10 @@ dependencies = [ "eyre", "flume 0.11.1", "futures", + "group", "itertools 0.14.0", "lazy_static", + "leanbridgetree", "masp_primitives", "masp_proofs", "namada_account", @@ -4942,7 +4946,7 @@ dependencies = [ "sha2 0.10.8", "smooth-operator", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "typed-builder", "xorf", @@ -4967,7 +4971,7 @@ dependencies = [ "patricia_tree", "proptest", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4985,7 +4989,7 @@ dependencies = [ "regex", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5074,7 +5078,7 @@ dependencies = [ "namada_tx", "namada_tx_env", "namada_vp_env", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5103,7 +5107,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "tonic-build", ] @@ -5159,7 +5163,7 @@ dependencies = [ "rayon", "smooth-operator", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "wasmer", "wasmer-cache", @@ -5197,7 +5201,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5258,7 +5262,7 @@ dependencies = [ "serde", "slip10_ed25519", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "toml", "zeroize", @@ -5850,12 +5854,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" - [[package]] name = "postcard" version = "1.1.1" @@ -7003,7 +7001,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -7015,12 +7013,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "slice-group-by" @@ -7573,11 +7568,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -7593,9 +7588,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index 6efbd8887cb..bce90e82176 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -423,9 +423,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ "borsh-derive", "cfg_aliases", @@ -433,9 +433,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" dependencies = [ "once_cell", "proc-macro-crate", @@ -1256,7 +1256,7 @@ dependencies = [ "rand_core", "serde", "serdect", - "thiserror 2.0.11", + "thiserror 2.0.12", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -2333,6 +2333,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leanbridgetree" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c255f8e570ab4acef90db79d99171bd5882615fb54d24591040fa69d80943e09" +dependencies = [ + "borsh", + "incrementalmerkletree", + "slab", + "thiserror 2.0.12", +] + [[package]] name = "libc" version = "0.2.169" @@ -2599,7 +2611,7 @@ dependencies = [ "pasta_curves", "rand_core", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -2646,7 +2658,7 @@ version = "0.251.0" dependencies = [ "namada_core", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2684,7 +2696,7 @@ dependencies = [ "smooth-operator", "tendermint", "tendermint-proto", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-keccak", "tracing", "uint 0.10.0", @@ -2701,7 +2713,7 @@ dependencies = [ "namada_macros", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2714,7 +2726,7 @@ dependencies = [ "namada_events", "namada_macros", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2735,7 +2747,7 @@ dependencies = [ "serde", "serde_json", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2769,7 +2781,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2795,7 +2807,7 @@ dependencies = [ "namada_core", "namada_macros", "prost", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2809,7 +2821,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2831,7 +2843,7 @@ dependencies = [ "once_cell", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2850,8 +2862,10 @@ dependencies = [ "borsh", "eyre", "futures", + "group", "itertools 0.14.0", "lazy_static", + "leanbridgetree", "masp_primitives", "masp_proofs", "namada_account", @@ -2872,7 +2886,7 @@ dependencies = [ "sha2 0.10.8", "smooth-operator", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "typed-builder", "xorf", @@ -2896,7 +2910,7 @@ dependencies = [ "namada_tx", "patricia_tree", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2914,7 +2928,7 @@ dependencies = [ "regex", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2967,7 +2981,7 @@ dependencies = [ "namada_tx", "namada_tx_env", "namada_vp_env", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2995,7 +3009,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "tonic-build", ] @@ -3047,7 +3061,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -3978,12 +3992,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smooth-operator" @@ -4207,11 +4218,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -4227,9 +4238,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote",