From 827e59675f33828cf8929004c5edd57363a84cda Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 29 May 2026 13:26:21 +1200 Subject: [PATCH 01/11] Initial lock free refactor --- Cargo.lock | 45 +- Cargo.toml | 23 +- crates/store/Cargo.toml | 1 + crates/store/src/accounts/mod.rs | 17 +- crates/store/src/db/mod.rs | 21 - crates/store/src/errors.rs | 8 +- .../samples/02-with-account-files/bridge.mac | Bin 37762 -> 37863 bytes crates/store/src/server/block_producer.rs | 2 +- crates/store/src/server/mod.rs | 2 +- crates/store/src/server/replica_sync.rs | 4 +- crates/store/src/server/rpc_api.rs | 16 +- crates/store/src/state/apply_block.rs | 320 +---------- crates/store/src/state/loader.rs | 6 + crates/store/src/state/mod.rs | 520 +++++++----------- crates/store/src/state/sync_state.rs | 11 +- crates/store/src/state/writer.rs | 330 +++++++++++ 16 files changed, 614 insertions(+), 712 deletions(-) create mode 100644 crates/store/src/state/writer.rs diff --git a/Cargo.lock b/Cargo.lock index 06a910230..4075f373d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -2944,9 +2953,8 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3e46490926f6084d6216067ef9c0e606e547361ba492b1ca311dcfd780b185" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "alloy-sol-types", "fs-err", @@ -3025,9 +3033,8 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1829e10ccf220c6052cdcc4181c820307f177612e070623213c8670766de64" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "miden-protocol", "thiserror 2.0.18", @@ -3472,6 +3479,7 @@ name = "miden-node-store" version = "0.15.0" dependencies = [ "anyhow", + "arc-swap", "assert_matches", "async-trait", "build-rs", @@ -3659,9 +3667,8 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b70acd2c57f7f9185c5174966dd47080389271410f25f6a3539970a4474fd440" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "bech32", "fs-err", @@ -3763,9 +3770,8 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc788da5d0dc43f93d6489a302d453cc6c67a12923208c9c8963e43073db0b3b" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "bon", "fs-err", @@ -3802,9 +3808,8 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f8cdaa93307907537ae95f3c30efbba94ed9d393bbc75c057c763cceff8599" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3823,9 +3828,8 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3cfee228a8b02efa9130bbe6ea086d1c8ffc1b4c93df10f880c9a9a3dfe634" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "miden-processor", "miden-protocol", @@ -3837,9 +3841,8 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ff829d060dc0cf38816fbe179fe22f0916d2a141974b2518606d58fe0bd1b37" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" dependencies = [ "miden-protocol", "miden-tx", diff --git a/Cargo.toml b/Cargo.toml index e86a77609..a8a7fce66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,13 +61,13 @@ miden-validator = { path = "bin/validator", version = "0.15" } miden-node-rocksdb-cxx-linkage-fix = { path = "crates/rocksdb-cxx-linkage-fix", version = "0.15" } # miden-protocol dependencies. These should be updated in sync. -miden-agglayer = { version = "0.15" } -miden-block-prover = { version = "0.15" } -miden-protocol = { default-features = false, version = "0.15" } -miden-standards = { version = "0.15" } -miden-testing = { version = "0.15" } -miden-tx = { default-features = false, version = "0.15" } -miden-tx-batch-prover = { version = "0.15" } +miden-agglayer = { version = "0.16" } +miden-block-prover = { version = "0.16" } +miden-protocol = { default-features = false, version = "0.16" } +miden-standards = { version = "0.16" } +miden-testing = { version = "0.16" } +miden-tx = { default-features = false, version = "0.16" } +miden-tx-batch-prover = { version = "0.16" } # Other miden dependencies. These should align with those expected by miden-protocol. miden-crypto = { version = "0.25" } @@ -162,3 +162,12 @@ files.extend-exclude = [ "*.min.js", # Minified JS bundles (vendored htmx etc.). "*.svg", # SVG files. ] + +[patch.crates-io] +miden-agglayer = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } +miden-block-prover = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } +miden-protocol = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } +miden-standards = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } +miden-testing = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } +miden-tx = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } +miden-tx-batch-prover = { branch = "sergerad-clone", git = "https://github.com/0xmiden/protocol" } diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 0ab2168ce..b3a631b37 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -19,6 +19,7 @@ doctest = false [dependencies] anyhow = { workspace = true } +arc-swap = "1" async-trait = { workspace = true } deadpool = { features = ["managed", "rt_tokio_1"], workspace = true } deadpool-diesel = { features = ["sqlite"], workspace = true } diff --git a/crates/store/src/accounts/mod.rs b/crates/store/src/accounts/mod.rs index c99488df0..985d06d55 100644 --- a/crates/store/src/accounts/mod.rs +++ b/crates/store/src/accounts/mod.rs @@ -15,6 +15,7 @@ use miden_protocol::crypto::merkle::smt::{ SMT_DEPTH, SmtLeaf, SmtStorage, + SmtStorageReader, }; use miden_protocol::crypto::merkle::{ EmptySubtreeRoots, @@ -118,7 +119,7 @@ impl HistoricalOverlay { /// reversion data (mutations that undo changes). Historical witnesses are reconstructed /// by starting from the latest state and applying reversion overlays backwards in time. #[derive(Debug)] -pub struct AccountTreeWithHistory { +pub struct AccountTreeWithHistory { /// The current block number (latest state). block_number: BlockNumber, /// The latest account tree state. @@ -127,7 +128,7 @@ pub struct AccountTreeWithHistory { overlays: BTreeMap, } -impl AccountTreeWithHistory { +impl AccountTreeWithHistory { /// Maximum number of historical blocks to maintain. pub const MAX_HISTORY: usize = 50; @@ -343,7 +344,9 @@ impl AccountTreeWithHistory { let path = SparseMerklePath::try_from(path).ok()?; Some((path, leaf)) } +} +impl AccountTreeWithHistory { // PUBLIC MUTATORS // -------------------------------------------------------------------------------------------- @@ -394,4 +397,14 @@ impl AccountTreeWithHistory { Ok(()) } + + /// Returns a read-only snapshot of this tree backed by a reader view of the storage. + pub fn reader(&self) -> AccountTreeWithHistory { + let latest = self.latest.reader().expect("snapshot creation should not fail"); + AccountTreeWithHistory { + block_number: self.block_number, + latest, + overlays: self.overlays.clone(), + } + } } diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 3a574f77a..dd4c9b481 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -27,7 +27,6 @@ use miden_protocol::note::{ }; use miden_protocol::transaction::TransactionHeader; use miden_protocol::utils::serde::{Deserializable, Serializable}; -use tokio::sync::oneshot; use tracing::{info, instrument}; use crate::COMPONENT; @@ -564,36 +563,16 @@ impl Db { } /// Inserts the data of a new block into the DB. - /// - /// `allow_acquire` and `acquire_done` are used to synchronize writes to the DB with writes to - /// the in-memory trees. Further details available on [`super::state::State::apply_block`]. // TODO: This span is logged in a root span, we should connect it to the parent one. #[instrument(target = COMPONENT, skip_all, err)] pub async fn apply_block( &self, - allow_acquire: oneshot::Sender<()>, - acquire_done: oneshot::Receiver<()>, signed_block: SignedBlock, notes: Vec<(NoteRecord, Option)>, ) -> Result<()> { self.transact("apply block", move |conn| -> Result<()> { models::queries::apply_block(conn, &signed_block, ¬es)?; - - // XXX FIXME TODO free floating mutex MUST NOT exist it doesn't bind it properly to the - // data locked! - { - let _span = tracing::info_span!(target: COMPONENT, "acquire_write_lock").entered(); - if allow_acquire.send(()).is_err() { - tracing::warn!(target: COMPONENT, "failed to send notification for successful block application, potential deadlock"); - } - } - models::queries::prune_history(conn, signed_block.header().block_num())?; - - let _span = - tracing::info_span!(target: COMPONENT, "acquire_done_lock").entered(); - acquire_done.blocking_recv()?; - Ok(()) }) .await diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index d16548804..730dea928 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -215,10 +215,10 @@ pub enum ApplyBlockError { // OTHER ERRORS // --------------------------------------------------------------------------------------------- - #[error("block applying was cancelled because of closed channel on database side")] - ClosedChannel(#[from] RecvError), - #[error("concurrent write detected")] - ConcurrentWrite, + #[error("failed to send block to writer task: {0}")] + WriterTaskSendFailed(String), + #[error("writer task dropped the result channel")] + WriterTaskRecvFailed(#[from] RecvError), #[error("database doesn't have any block header data")] DbBlockHeaderEmpty, #[error("database update failed: {0}")] diff --git a/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac b/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac index 8412cfe5cb431e2e9b554f126ab3e959d5d57864..7017fd1eff1691a140bfb5f18c14589824fa318b 100644 GIT binary patch delta 3505 zcmai$d2Ccw6vpp#plB6a5fOp5L#eXsv`A&=u?lDlNZAC-CR#uvP{4)4JTxLvS#&_5 z8f6jc0t#uMD59fcR3HRpQ9+Rwql8El#i$g)-#0y>jsN(Pd%yF2=R4=#``(+^K31CW zQ)$BH=E3NMpcl&6nAiK@Q8mJof-3X%t%~Wo^yWB*z87YkCYTII~!dPT%5YF zUT*z%&bc9sk&MZV@d4)&6P!C%*0#oX>@h#^s;72dpkw; z*OlPHlbrJv+ygcSPng~$QK}2aXL5zk#jc0snhy7yz^6Iq30pFnG9EyeZ;|Kgdvn*PSo(7pmESwK#Ay#s} z@Phdl5Qn5TxRQvSv7=aspri3YhBWk>qd&80peZN2f=;q~BeFxoISo4(QXP1H+&i~ic#4$E&w zZ>w8;#DonLw8N0sfQb!I;My~`Vw67J^m2UCx6=?kWrOMO!f!&~!O%kQ!F4Y`v3xh` zZiZJG??W8c!%V-1i1;H4!UP|)pp3iV!W_#J`&sb(8t$^;pW~N4)AT*~q`$!EV5ob( zkAC8I`TGg7S>L^6g$VvF@BqidnB@P)vy(e=Ce1}=Qx`qXI%;XvP# zPbSDPehjX+?0fp$Dg(OK4_6@Q4Xb2yVw}L31NOJVI}K^(cSbKVeiEPf0^_IX(*>ew*i*dNBB+ z#DwR;?%+}L5$f_UGI}ZsM4YGKq!|(d&hy~Gfb&)Or|Fk9fv~aoOmRK(y&%_`4)=P) zlLF2OTh!=IFItxItMG{@2V6|Pln8hOs7tdII_qJdA8mFgx8F>z#yJ!h1*R3*^rbjzZDA% zBp9wYu+|-FOZ^~(cGL??EPuf8r)rE`x}{m{><*0Wn=#R-0}aG)VS(Ghj&Naj%NH3U z>GC^MpGVL%HO?>f?MA)y?$irgTD~WIu-<>9^q* zA3*&Oa3EZFqTKRqICdxesB!L;Hw>OfA7Nq|6YGbEV3hEL1UM)0^9hDgApFjBPT~!R zpE6FU%O6F(^wHD_?_vSr7~9ZTkd35pVs9jR0jQ0R!xzyru9MDG>)B1jFbbj1fAq=d zUD4Num}>b8*}!OoDJ&%16k;Mj4X*z8Q>T~i;}8?Nz9VA@PKM&&pSa-0BFvzX67`xh zVJxD7^lOaIX2*BK=g?5ThsfUrp5fQxf$8RZlzahvE`DJ#sEy5|zYey=e1-h}W8)BR zz%U+MKtu71EtuFqD6<;bv75kPgPw39jmCo`EFaPb9{8k&6Rx3AVKsWfbr^;G;fWWnx55qhrl6O?^_ryF!r6xV4BsR_ zmHbkDZP?ga!%wR*66VijY#N!6{v=`8LgV`h3XCf+F8u*;D-Gc(+d$R*X1?tlK>2s@ zafROHV9M`+3mfS#8~t~}A0&96{8YVvA0TLl#b(&gGn@`Dso^p4T`VYkuSWN0z`0_= z&uA#DuzWzTJPY=*XFaxtdqpJnv2Kw^z%!0X*bmMG-!|iyG@J!L048zbgB0i)>)Fl* z53`|}(ZzWkBHxmE2;~?VVHQiuah#6g73k_*iBCs30qSk|f&2vULtBwhm(Pej!j#m7 zi6iKP@Mt`YPkyfHKU0q$ai`fzZ392L*j&8J&3J|di{YOc|DA^V7@wt1`X8YD-SBaF zmFD}41?C~7c8%-dbL2-5R#876%#Z8+UHBh_p$Hc+@J!uB8miDGILF3P0twF7iI!NS zAl!%7NpPO@y702IgENz(ji-B|9+lap#kt##oj(3`Wv^gG&sF=9mj-jUuPDwc-~II7 gDfwlc#ua7-GRoF0Ym^Y(`rNuU|5s3bEY=D93qzOxuK)l5 delta 3465 zcmai#dyvg#9LC>smS`__t4(%UdltFJVNF=KqjgCx(Zwy7m0XtGt=r=KYAO}Ra3YCH z7`HC6t|@cUOw=@6$|X&OrYWT|Yeh-)dCvKY@lU^*_kF(4^S!?3J?D3RTJL{yy?;wi zLZE7HmkqySoPk0ChgvHcl}TF~QqbkFo~B^{ ze1=3}ck@3BzXM)q{CVOM;PWY13tVIRLhRbCu_E@OgUr*;s z@C~5!RiRI?4v~zlGiizTfy-?8c4{v(2qo3_ya1;7SM-4mN!|_Fb$U z6r?~}#PnTo>AT5Oqj%xDgRSOoi`|rU%6L9;;>owM(JxATQ(fiLKCpI1xRbg z>UfQN;`@mg4mJH_?B=YcG`NcTtUslmxIJDee&s!9d2rW){_Y@wx$!8k1^6Wm#1Ekd z!NaTo>j=7j4@X(T%~q()@N42z(NojeIN>+&1dMONyTL5ek5flquoDavZi#UcK>@iI z(7{l}TC)m_|45wpc;lxiEN+kYo$043oQL?c@!yR9j@E{SKPt@5(C5#jWdz7L%hJ6y z&k%FL5rz{D*BV}kxn1B-^4ek7W3cw%MNs^J#l^#%_YXz~)@2$i?(kK=FYKan_K_( zdlB~_e6s#}=pW=H?jh_xB*^iJ9O#@RD*dSOn99lfQs7}oKk|hAX`r|PHqbzjhH0EQ z+82EgsH+`JobJ5rx_b0?UI799FoM}^*WKxG^gM(`0m;|ydL#o3Ko~_M;feqo{zAAK zjUi88x-9`VbiGF&VI2;z#knUw!RNvu8Rr6QI3`hGkgObSE;xmW#ivqmAXtQ7c!paG zfyov(1OH(7OyY&J!2Vz{g9=}kcgLwy3nSxxj8OP=bBFA7swhG&k&obWOD5*j}WZZiD^ zxby~`d<1v9j1$OTZvN%?(cMeblfHsFLh+H{O86*n6>;KVNId*1g=BzH;c5a$gUx89 zz%`b*mN@C_;MrhOJvL5~ZMes9BmOZsX6tQ(f05xf!==<6iz7vUJoFE~O<@V$<3dmY zkHe)4ODG7YZ37i|+~Rg{f^qPj=sba496*hClP65jH;@7Lz{g{}hrbYfA1*A6`NJ+) z${kKXm}d#_&_wt?3JW)0)7>OEPe}L?1?4X_|L=zRXu8sFt=Sj=9-WNwDUF3(acBxy z3Qi7TthB(-DJaK59EqIx3lemPy4$JXAqG+bVG($QxT45wy>i1`R!sR0Wjgk8GBn}| zD(Gk@!C~O{_=Rs-M@&5)D>Resg6Vo8h}+}k=`Fxfej$Gr#%U%H+7tGMP@ow0gaw|V zq4;*=Wfas)_biFC!E#W3UU7OtpIO{_8q7gRY!lOie^TdR%)iK&-qG~G;l1IP@XysB z+dmXiz-0s`PrCxw35qSj=Z_!L?|0D|=n3$WJ9^hi4eU>C8}aW*&Nx>*ZPD4~Nma^! zx%sQV3J=U{SAAgV-f!y9EO~k8=w|-R$|bifPVz?rixVR?BC8wzUsn8FH1hom&3@|u diff --git a/crates/store/src/server/block_producer.rs b/crates/store/src/server/block_producer.rs index 7f971b8ec..6cdaa3001 100644 --- a/crates/store/src/server/block_producer.rs +++ b/crates/store/src/server/block_producer.rs @@ -229,7 +229,7 @@ impl block_producer_server::BlockProducer for BlockProducerApi { .inspect_err(|err| tracing::Span::current().set_error(err)) .map_err(|err| tonic::Status::internal(err.as_report()))?; - let block_height = self.inner.state.chain_tip(Finality::Committed).await.as_u32(); + let block_height = self.inner.state.chain_tip(Finality::Committed).as_u32(); Ok(Response::new(proto::store::TransactionInputs { account_state: Some(proto::store::transaction_inputs::AccountTransactionInputRecord { diff --git a/crates/store/src/server/mod.rs b/crates/store/src/server/mod.rs index 98c21ef84..e207b3fd7 100644 --- a/crates/store/src/server/mod.rs +++ b/crates/store/src/server/mod.rs @@ -284,7 +284,7 @@ impl Store { Arc::new(BlockProver::local()) }; - let chain_tip = state.chain_tip(crate::state::Finality::Committed).await; + let chain_tip = state.chain_tip(crate::state::Finality::Committed); let (chain_tip_tx, chain_tip_rx) = watch::channel(chain_tip); let handle = proof_scheduler::spawn( diff --git a/crates/store/src/server/replica_sync.rs b/crates/store/src/server/replica_sync.rs index e60eaa156..9c5ff7869 100644 --- a/crates/store/src/server/replica_sync.rs +++ b/crates/store/src/server/replica_sync.rs @@ -96,7 +96,7 @@ impl ReplicaSync for BlockReplicaSync { } async fn subscribe(&self, mut client: StoreRpcClient) -> anyhow::Result<()> { - let block_from = self.state.chain_tip(Finality::Committed).await.child().as_u32(); + let block_from = self.state.chain_tip(Finality::Committed).child().as_u32(); info!(block_from, upstream_url = %self.upstream_url, "Connecting to upstream store for blocks"); let mut stream = client @@ -139,7 +139,7 @@ impl ReplicaSync for ProofReplicaSync { } async fn subscribe(&self, mut client: StoreRpcClient) -> anyhow::Result<()> { - let block_from = self.state.chain_tip(Finality::Proven).await.as_u32().saturating_add(1); + let block_from = self.state.chain_tip(Finality::Proven).as_u32().saturating_add(1); info!(block_from, upstream_url = %self.upstream_url, "Connecting to upstream store for proofs"); let mut stream = client diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 5512e3aac..35648d255 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -138,7 +138,7 @@ impl rpc_server::Rpc for StoreApi { }) .collect(); - let chain_tip = self.state.chain_tip(Finality::Committed).await; + let chain_tip = self.state.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncNullifiersResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -160,7 +160,7 @@ impl rpc_server::Rpc for StoreApi { read_block_range::(request.block_range, "SyncNotesRequest")? .into_inclusive_range::()?; - let chain_tip = self.state.chain_tip(Finality::Committed).await; + let chain_tip = self.state.chain_tip(Finality::Committed); if *block_range.end() > chain_tip { Err(NoteSyncError::FutureBlock { chain_tip, block_to: *block_range.end() })?; } @@ -201,9 +201,9 @@ impl rpc_server::Rpc for StoreApi { // Determine finality level of the tip to sync to or default to the committed tip. let sync_target = match request.finality_level() { proto::rpc::FinalityLevel::Committed | proto::rpc::FinalityLevel::Unspecified => { - self.state.chain_tip(Finality::Committed).await + self.state.chain_tip(Finality::Committed) }, - proto::rpc::FinalityLevel::Proven => self.state.chain_tip(Finality::Proven).await, + proto::rpc::FinalityLevel::Proven => self.state.chain_tip(Finality::Proven), }; if current_client_block_height > sync_target { @@ -332,7 +332,7 @@ impl rpc_server::Rpc for StoreApi { }) .collect(); - let chain_tip = self.state.chain_tip(Finality::Committed).await; + let chain_tip = self.state.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncAccountVaultResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -384,7 +384,7 @@ impl rpc_server::Rpc for StoreApi { }) .collect(); - let chain_tip = self.state.chain_tip(Finality::Committed).await; + let chain_tip = self.state.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncAccountStorageMapsResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -402,7 +402,7 @@ impl rpc_server::Rpc for StoreApi { Ok(Response::new(proto::rpc::StoreStatus { version: env!("CARGO_PKG_VERSION").to_string(), status: "connected".to_string(), - chain_tip: self.state.chain_tip(Finality::Committed).await.as_u32(), + chain_tip: self.state.chain_tip(Finality::Committed).as_u32(), })) } @@ -469,7 +469,7 @@ impl rpc_server::Rpc for StoreApi { .map(crate::db::TransactionRecord::into_proto) .collect(); - let chain_tip = self.state.chain_tip(Finality::Committed).await; + let chain_tip = self.state.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncTransactionsResponse { pagination_info: Some(proto::rpc::PaginationInfo { diff --git a/crates/store/src/state/apply_block.rs b/crates/store/src/state/apply_block.rs index 4c5f2ab73..a2a6ce7d9 100644 --- a/crates/store/src/state/apply_block.rs +++ b/crates/store/src/state/apply_block.rs @@ -1,163 +1,20 @@ -use std::sync::Arc; - use miden_node_proto::domain::proof_request::BlockProofRequest; -use miden_node_utils::ErrorReport; -use miden_protocol::Word; -use miden_protocol::account::delta::AccountUpdateDetails; -use miden_protocol::block::account_tree::AccountMutationSet; -use miden_protocol::block::nullifier_tree::NullifierMutationSet; -use miden_protocol::block::{BlockBody, BlockHeader, BlockNumber, SignedBlock}; -use miden_protocol::note::{NoteAttachments, NoteDetails, Nullifier}; -use miden_protocol::transaction::OutputNote; +use miden_protocol::block::{BlockNumber, SignedBlock}; use miden_protocol::utils::serde::Serializable; -use tokio::sync::oneshot; -use tracing::{Instrument, info, info_span, instrument}; -use crate::db::NoteRecord; -use crate::errors::{ApplyBlockError, InvalidBlockError}; -use crate::state::{BlockNotification, State}; -use crate::{COMPONENT, HistoricalError}; +use crate::errors::ApplyBlockError; +use crate::state::State; +use crate::COMPONENT; +use tracing::instrument; impl State { /// Apply changes of a new block to the DB and in-memory data structures. /// - /// ## Note on state consistency - /// - /// The server contains in-memory representations of the existing trees, the in-memory - /// representation must be kept consistent with the committed data, this is necessary so to - /// provide consistent results for all endpoints. In order to achieve consistency, the - /// following steps are used: - /// - /// - the request data is validated, prior to starting any modifications. - /// - block is being saved into the store in parallel with updating the DB, but before - /// committing. This block is considered as candidate and not yet available for reading - /// because the latest block pointer is not updated yet. - /// - a transaction is open in the DB and the writes are started. - /// - while the transaction is not committed, concurrent reads are allowed, both the DB and the - /// in-memory representations, which are consistent at this stage. - /// - prior to committing the changes to the DB, an exclusive lock to the in-memory data is - /// acquired, preventing concurrent reads to the in-memory data, since that will be - /// out-of-sync w.r.t. the DB. - /// - the DB transaction is committed, and requests that read only from the DB can proceed to - /// use the fresh data. - /// - the in-memory structures are updated, including the latest block pointer and the lock is - /// released. - // TODO: This span is logged in a root span, we should connect it to the parent span. + /// Forwards the block to the [`BlockWriter`](crate::state::writer::BlockWriter) task for + /// serialised processing. #[instrument(target = COMPONENT, skip_all, err)] pub async fn apply_block(&self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> { - let _lock = self.writer.try_lock().map_err(|_| ApplyBlockError::ConcurrentWrite)?; - - let header = signed_block.header(); - let body = signed_block.body(); - - let block_num = header.block_num(); - let block_commitment = header.commitment(); - - self.validate_block_header(header, body).await?; - - // Save the block to the block store. In a case of a rolled-back DB transaction, the - // in-memory state will be unchanged, but the file might still be written. Such blocks - // should be considered candidates, not finalized blocks. - let signed_block_bytes = signed_block.to_bytes(); - // Clone before moving into the block-save task so we can cache for replicas at commit. - let cache_bytes = signed_block_bytes.clone(); - let store = Arc::clone(&self.block_store); - let block_save_task = tokio::spawn( - async move { store.save_block(block_num, &signed_block_bytes).await }.in_current_span(), - ); - - let ( - nullifier_tree_old_root, - nullifier_tree_update, - account_tree_old_root, - account_tree_update, - ) = self.compute_tree_mutations(header, body).await?; - - let notes = Self::build_note_records(header, body)?; - - // Signals the transaction is ready to be committed, and the write lock can be acquired. - let (allow_acquire, acquired_allowed) = oneshot::channel::<()>(); - // Signals the write lock has been acquired, and the transaction can be committed. - let (inform_acquire_done, acquire_done) = oneshot::channel::<()>(); - - // Extract public account updates with deltas before block is moved into async task. Private - // accounts are filtered out since they don't expose their state changes. - let account_deltas = - Vec::from_iter(body.updated_accounts().iter().filter_map( - |update| match update.details() { - AccountUpdateDetails::Delta(delta) => Some(delta.clone()), - AccountUpdateDetails::Private => None, - }, - )); - - // The DB and in-memory state updates need to be synchronized and are partially overlapping. - // Namely, the DB transaction only proceeds after this task acquires the in-memory write - // lock. This requires the DB update to run concurrently, so a new task is spawned. - let db = Arc::clone(&self.db); - let db_update_task = tokio::spawn( - async move { db.apply_block(allow_acquire, acquire_done, signed_block, notes).await } - .in_current_span(), - ); - - // Wait for the message from the DB update task, that we ready to commit the DB transaction. - acquired_allowed - .instrument(info_span!(target: COMPONENT, "await_db_readiness")) - .await - .map_err(ApplyBlockError::ClosedChannel)?; - - // Awaiting the block saving task to complete without errors. - block_save_task.await??; - - self.with_inner_write_blocking(|inner| { - // We need to check that neither the nullifier tree nor the account tree have changed - // while we were waiting for the DB preparation task to complete. If either of them did - // change, we do not proceed with in-memory and database updates, since it may lead to - // an inconsistent state. - if inner.nullifier_tree.root() != nullifier_tree_old_root - || inner.account_tree.root_latest() != account_tree_old_root - { - return Err(ApplyBlockError::ConcurrentWrite); - } - - // Notify the DB update task that the write lock has been acquired, so it can commit the - // DB transaction. - inform_acquire_done - .send(()) - .map_err(|_| ApplyBlockError::DbUpdateTaskFailed("Receiver was dropped".into()))?; - - // TODO: shutdown #91 Await for successful commit of the DB transaction. If the commit - // fails, we mustn't change in-memory state, so we return a block applying error and - // don't proceed with in-memory updates. - tokio::runtime::Handle::current() - .block_on(db_update_task)? - .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?; - - // Update the in-memory data structures after successful commit of the DB transaction - inner - .nullifier_tree - .apply_mutations(nullifier_tree_update) - .expect("Unreachable: old nullifier tree root must be checked before this step"); - inner - .account_tree - .apply_mutations(account_tree_update) - .expect("Unreachable: old account tree root must be checked before this step"); - - inner.blockchain.push(block_commitment); - - Ok(()) - })?; - - self.with_forest_write_blocking(|forest| { - forest.apply_block_updates(block_num, account_deltas) - })?; - - // Push to cache and notify replica subscribers. - self.block_cache.push(block_num, BlockNotification::new(block_num, cache_bytes)); - let _ = self.committed_tip_tx.send(block_num); - - info!(%block_commitment, block_num = block_num.as_u32(), COMPONENT, "apply_block successful"); - - Ok(()) + self.write_handle.apply_block(signed_block).await } /// Saves the proving inputs for the given block to the block store. @@ -170,165 +27,4 @@ impl State { .save_proving_inputs(block_num, &proving_inputs.to_bytes()) .await } - - /// Validates that the block header is consistent with the block body and the current state. - #[instrument(target = COMPONENT, skip_all, err)] - async fn validate_block_header( - &self, - header: &BlockHeader, - body: &BlockBody, - ) -> Result<(), ApplyBlockError> { - // Validate that header and body match. - let tx_commitment = body.transactions().commitment(); - if header.tx_commitment() != tx_commitment { - return Err(InvalidBlockError::InvalidBlockTxCommitment { - expected: tx_commitment, - actual: header.tx_commitment(), - } - .into()); - } - - let block_num = header.block_num(); - - // Validate that the applied block is the next block in sequence. - let prev_block = self - .db - .select_block_header_by_block_num(None) - .await? - .ok_or(ApplyBlockError::DbBlockHeaderEmpty)?; - let expected_block_num = prev_block.block_num().child(); - if block_num != expected_block_num { - return Err(InvalidBlockError::NewBlockInvalidBlockNum { - expected: expected_block_num, - submitted: block_num, - } - .into()); - } - if header.prev_block_commitment() != prev_block.commitment() { - return Err(InvalidBlockError::NewBlockInvalidPrevCommitment.into()); - } - - Ok(()) - } - - /// Computes nullifier and account tree mutations, validating roots against the block header. - #[instrument(target = COMPONENT, skip_all, err)] - async fn compute_tree_mutations( - &self, - header: &BlockHeader, - body: &BlockBody, - ) -> Result<(Word, NullifierMutationSet, Word, AccountMutationSet), ApplyBlockError> { - self.with_inner_read_blocking(|inner| { - let block_num = header.block_num(); - - // nullifiers can be produced only once - let duplicate_nullifiers: Vec<_> = body - .created_nullifiers() - .iter() - .filter(|&nullifier| inner.nullifier_tree.get_block_num(nullifier).is_some()) - .copied() - .collect(); - if !duplicate_nullifiers.is_empty() { - return Err(InvalidBlockError::DuplicatedNullifiers(duplicate_nullifiers).into()); - } - - // new_block.chain_root must be equal to the chain MMR root prior to the update - let peaks = inner.blockchain.peaks(); - if peaks.hash_peaks() != header.chain_commitment() { - return Err(InvalidBlockError::NewBlockInvalidChainCommitment.into()); - } - - // compute update for nullifier tree - let nullifier_tree_update = inner - .nullifier_tree - .compute_mutations( - body.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)), - ) - .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?; - - if nullifier_tree_update.as_mutation_set().root() != header.nullifier_root() { - // We do our best here to notify the serve routine, if it doesn't care (dropped the - // receiver) we can't do much. - let _ = self.termination_ask.try_send(ApplyBlockError::InvalidBlockError( - InvalidBlockError::NewBlockInvalidNullifierRoot, - )); - return Err(InvalidBlockError::NewBlockInvalidNullifierRoot.into()); - } - - // compute update for account tree - let account_tree_update = inner - .account_tree - .compute_mutations( - body.updated_accounts() - .iter() - .map(|update| (update.account_id(), update.final_state_commitment())), - ) - .map_err(|e| match e { - HistoricalError::AccountTreeError(err) => { - InvalidBlockError::NewBlockDuplicateAccountIdPrefix(err) - }, - HistoricalError::MerkleError(_) => { - panic!("Unexpected MerkleError during account tree mutation computation") - }, - })?; - - if account_tree_update.as_mutation_set().root() != header.account_root() { - let _ = self.termination_ask.try_send(ApplyBlockError::InvalidBlockError( - InvalidBlockError::NewBlockInvalidAccountRoot, - )); - return Err(InvalidBlockError::NewBlockInvalidAccountRoot.into()); - } - - Ok(( - inner.nullifier_tree.root(), - nullifier_tree_update, - inner.account_tree.root_latest(), - account_tree_update, - )) - }) - } - - /// Builds note records with inclusion proofs from the block body. - #[instrument(target = COMPONENT, skip_all, err)] - fn build_note_records( - header: &BlockHeader, - body: &BlockBody, - ) -> Result)>, ApplyBlockError> { - let block_num = header.block_num(); - - let note_tree = body.compute_block_note_tree(); - if note_tree.root() != header.note_root() { - return Err(InvalidBlockError::NewBlockInvalidNoteRoot.into()); - } - - let notes = body - .output_notes() - .map(|(note_index, note)| { - let (details, attachments, nullifier) = match note { - OutputNote::Public(public) => ( - Some(NoteDetails::from(public.as_note())), - public.as_note().attachments().clone(), - Some(public.as_note().nullifier()), - ), - OutputNote::Private(_) => (None, NoteAttachments::empty(), None), - }; - - let inclusion_path = note_tree.open(note_index); - - let note_record = NoteRecord { - block_num, - note_index, - note_id: note.id().as_word(), - metadata: *note.metadata(), - details, - attachments, - inclusion_path, - }; - - Ok((note_record, nullifier)) - }) - .collect::, InvalidBlockError>>()?; - - Ok(notes) - } } diff --git a/crates/store/src/state/loader.rs b/crates/store/src/state/loader.rs index 9d06f00e4..01c6fe0d9 100644 --- a/crates/store/src/state/loader.rs +++ b/crates/store/src/state/loader.rs @@ -71,6 +71,12 @@ pub type TreeStorage = RocksDbStorage; #[cfg(not(feature = "rocksdb"))] pub type TreeStorage = MemoryStorage; +/// The read-only snapshot storage backend for trees (used in [`InMemoryState`]). +#[cfg(feature = "rocksdb")] +pub type SnapshotTreeStorage = miden_large_smt_backend_rocksdb::RocksDbSnapshotStorage; +#[cfg(not(feature = "rocksdb"))] +pub type SnapshotTreeStorage = miden_protocol::crypto::merkle::smt::MemoryStorageSnapshot; + // ERROR CONVERSION // ================================================================================================ diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index b5fa213b9..1daa3cf30 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -9,6 +9,7 @@ use std::ops::RangeInclusive; use std::path::Path; use std::sync::Arc; +use arc_swap::ArcSwap; use miden_node_proto::domain::account::{ AccountDetailRequest, AccountDetails, @@ -32,11 +33,11 @@ use miden_protocol::block::account_tree::AccountWitness; use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness}; use miden_protocol::block::{BlockHeader, BlockInputs, BlockNumber, Blockchain}; use miden_protocol::crypto::merkle::mmr::{MmrPeaks, MmrProof, PartialMmr}; -use miden_protocol::crypto::merkle::smt::{LargeSmt, SmtStorage}; +use miden_protocol::crypto::merkle::smt::LargeSmt; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_protocol::transaction::PartialBlockchain; -use tokio::sync::{Mutex, RwLock, watch}; -use tracing::{Instrument, Span, info, instrument}; +use tokio::sync::{RwLock, watch}; +use tracing::{Span, info, instrument}; use crate::account_state_forest::{ AccountStateForest, @@ -73,6 +74,7 @@ use loader::{ ACCOUNT_TREE_STORAGE_DIR, AccountForestLoader, NULLIFIER_TREE_STORAGE_DIR, + SnapshotTreeStorage, TreeStorage, TreeStorageLoader, load_mmr, @@ -86,6 +88,8 @@ pub use replica::{BlockCache, BlockNotification, ProofCache, ProofNotification}; mod apply_block; mod apply_proof; mod sync_state; +pub(crate) mod writer; +use writer::{BlockWriter, WriteHandle}; // FINALITY // ================================================================================================ @@ -117,23 +121,15 @@ type BlockInputWitnesses = ( PartialMmr, ); -/// Container for state that needs to be updated atomically. -struct InnerState -where - S: SmtStorage, -{ - nullifier_tree: NullifierTree>, - blockchain: Blockchain, - account_tree: AccountTreeWithHistory, -} - -impl InnerState { - /// Returns the latest block number. - fn latest_block_num(&self) -> BlockNumber { - self.blockchain - .chain_tip() - .expect("chain should always have at least the genesis block") - } +/// Immutable snapshot of in-memory tree state published after each committed block. +/// +/// Backed by read-only snapshot storage so that any number of readers can access the data +/// concurrently without holding a lock. +pub(crate) struct InMemoryState { + pub block_num: BlockNumber, + pub nullifier_tree: NullifierTree>, + pub account_tree: AccountTreeWithHistory, + pub blockchain: Blockchain, } // CHAIN STATE @@ -148,19 +144,21 @@ pub struct State { /// The block store which stores full block contents for all blocks. block_store: Arc, - /// Read-write lock used to prevent writing to a structure while it is being used. + /// Atomically swappable pointer to the latest in-memory snapshot. /// - /// The lock is writer-preferring, meaning the writer won't be starved. - inner: RwLock>, + /// Readers load the snapshot wait-free via `ArcSwap::load()`; the writer task atomically + /// replaces the pointer after each committed block. + in_memory: Arc>, - /// Forest-related state `(SmtForest, storage_map_roots, vault_roots)` with its own lock. - forest: RwLock>, + /// Handle for sending block-write requests to the [`BlockWriter`] task. + write_handle: WriteHandle, - /// To allow readers to access the tree data while an update in being performed, and prevent - /// TOCTOU issues, there must be no concurrent writers. This locks to serialize the writers. - writer: Mutex<()>, + /// Forest-related state `(SmtForest, storage_map_roots, vault_roots)` with its own lock. + forest: Arc>>, /// Request termination of the process due to a fatal internal state error. + /// Stored to keep the channel alive alongside the BlockWriter's copy of the sender. + #[allow(dead_code)] termination_ask: tokio::sync::mpsc::Sender, /// The latest proven-in-sequence block number, updated by the proof scheduler or `apply_proof`. @@ -168,14 +166,12 @@ pub struct State { /// Watch sender fired after each block is committed. Replicas subscribe via /// `subscribe_committed_tip()` to be woken when new blocks arrive. - committed_tip_tx: watch::Sender, + committed_tip_tx: Arc>, - /// FIFO cache of recent committed blocks for replica subscriptions. When a subscriber needs a - /// block that has been evicted, it falls back to loading from the block store. + /// FIFO cache of recent committed blocks for replica subscriptions. pub(crate) block_cache: BlockCache, - /// FIFO cache of recent block proofs for replica subscriptions. When a subscriber needs a proof - /// that has been evicted, it falls back to loading from the block store. + /// FIFO cache of recent block proofs for replica subscriptions. pub(crate) proof_cache: ProofCache, } @@ -186,8 +182,7 @@ impl State { /// Loads the state from the data directory. /// /// Returns `(Self, ProvenTipWriter)`. The `ProvenTipWriter` is used by the proof scheduler - /// (in block-producer mode) to advance the proven tip; callers can subscribe to tip changes - /// via the methods on `Self`. + /// (in block-producer mode) to advance the proven tip. #[instrument(target = COMPONENT, skip_all)] pub async fn load( data_path: &Path, @@ -206,8 +201,7 @@ impl State { /// Loads the state from the data directory using explicit database options. /// /// Returns `(Self, ProvenTipWriter)`. The `ProvenTipWriter` is used by the proof scheduler - /// (in block-producer mode) to advance the proven tip; callers can subscribe to tip changes - /// via the methods on `Self`. + /// (in block-producer mode) to advance the proven tip. #[instrument(target = COMPONENT, skip_all)] pub async fn load_with_database_options( data_path: &Path, @@ -253,9 +247,6 @@ impl State { TreeStorage::create(data_path, &nullifier_storage_config, NULLIFIER_TREE_STORAGE_DIR)?; let nullifier_tree = nullifier_storage.load_nullifier_tree(&mut db).await?; - // Verify that tree roots match the expected roots from the database. This catches any - // divergence between persistent storage and the database caused by corruption or incomplete - // shutdown. verify_tree_consistency(account_tree.root(), nullifier_tree.root(), &mut db).await?; let account_tree = AccountTreeWithHistory::new(account_tree, latest_block_num); @@ -268,33 +259,63 @@ impl State { let forest = forest_backend.load_account_state_forest(&mut db, latest_block_num).await?; verify_account_state_forest_consistency(&forest, &mut db).await?; - let inner = RwLock::new(InnerState { nullifier_tree, blockchain, account_tree }); - - let forest = RwLock::new(forest); - let writer = Mutex::new(()); let db = Arc::new(db); - // Initialize the proven tip from the block store. let proven_tip_init = block_store .load_proven_tip() .map_err(StateInitializationError::ProvenTipLoadError)?; let (proven_tip, _rx) = ProvenTipWriter::new(proven_tip_init); - // Committed-tip watch: fires after each successful apply_block. let (committed_tip_tx, _rx) = watch::channel(latest_block_num); + let committed_tip_tx = Arc::new(committed_tip_tx); + + let forest = Arc::new(RwLock::new(forest)); + let block_cache = BlockCache::new(BLOCK_CACHE_CAPACITY); + let proof_cache = ProofCache::new(PROOF_CACHE_CAPACITY); + + // Create the initial snapshot using reader views of the just-loaded trees. + let initial_snapshot = Arc::new(InMemoryState { + block_num: latest_block_num, + nullifier_tree: nullifier_tree + .reader() + .map_err(|e| StateInitializationError::NullifierTreeIoError(e.to_string()))?, + account_tree: account_tree.reader(), + blockchain: blockchain.clone(), + }); + let in_memory = Arc::new(ArcSwap::from(initial_snapshot)); + + // Channel for write requests from State to BlockWriter. + let (write_tx, write_rx) = tokio::sync::mpsc::channel(1); + let write_handle = WriteHandle::new(write_tx); + + // Spawn the BlockWriter task. + let block_writer = BlockWriter { + db: Arc::clone(&db), + block_store: Arc::clone(&block_store), + in_memory: Arc::clone(&in_memory), + forest: Arc::clone(&forest), + committed_tip_tx: Arc::clone(&committed_tip_tx), + block_cache: block_cache.clone(), + termination_ask: termination_ask.clone(), + rx: write_rx, + nullifier_tree, + account_tree, + blockchain, + }; + tokio::spawn(block_writer.run()); Ok(( Self { db, block_store, - inner, + in_memory, + write_handle, forest, - writer, termination_ask, proven_tip: proven_tip.clone(), committed_tip_tx, - block_cache: BlockCache::new(BLOCK_CACHE_CAPACITY), - proof_cache: ProofCache::new(PROOF_CACHE_CAPACITY), + block_cache, + proof_cache, }, proven_tip, )) @@ -315,42 +336,19 @@ impl State { self.proven_tip.subscribe() } - // HELPER FUNCTIONS TO AVOID BLOCKING CALLS IN ASYNC CONTEXT + // SNAPSHOT HELPER // -------------------------------------------------------------------------------------------- - /// Runs a synchronous read-only operation over the inner state on Tokio's blocking path. - /// - /// The account and nullifier trees may be backed by `RocksDB`, so tree access must not run on - /// an async worker thread directly. This helper preserves the current tracing span while - /// moving the blocking lock acquisition and closure body into `block_in_place`. - fn with_inner_read_blocking(&self, f: impl FnOnce(&InnerState) -> R) -> R { - let span = Span::current(); - tokio::task::block_in_place(|| { - span.in_scope(|| { - let inner = self.inner.blocking_read(); - f(&inner) - }) - }) + /// Returns the current in-memory snapshot (wait-free, no lock required). + fn snapshot(&self) -> Arc { + self.in_memory.load_full() } - /// Runs a synchronous mutable operation over the inner state on Tokio's blocking path. - /// - /// See [`Self::with_inner_read_blocking`] for why this uses `block_in_place`. - fn with_inner_write_blocking(&self, f: impl FnOnce(&mut InnerState) -> R) -> R { - let span = Span::current(); - tokio::task::block_in_place(|| { - span.in_scope(|| { - let mut inner = self.inner.blocking_write(); - f(&mut inner) - }) - }) - } + // HELPER FUNCTIONS TO AVOID BLOCKING CALLS IN ASYNC CONTEXT + // -------------------------------------------------------------------------------------------- /// Runs a synchronous read-only operation over the account state forest on Tokio's blocking /// path. - /// - /// The forest may be backed by `RocksDB`, so accesses to the underlying `LargeSmtForest` must - /// not run directly on an async worker thread. fn with_forest_read_blocking( &self, f: impl FnOnce(&AccountStateForest) -> R, @@ -364,22 +362,6 @@ impl State { }) } - /// Runs a synchronous mutable operation over the account state forest on Tokio's blocking path. - /// - /// See [`Self::with_forest_read_blocking`] for why this uses `block_in_place`. - fn with_forest_write_blocking( - &self, - f: impl FnOnce(&mut AccountStateForest) -> R, - ) -> R { - let span = Span::current(); - tokio::task::block_in_place(|| { - span.in_scope(|| { - let mut forest = self.forest.blocking_write(); - f(&mut forest) - }) - }) - } - // STATE ACCESSORS // -------------------------------------------------------------------------------------------- @@ -396,8 +378,8 @@ impl State { let block_header = self.db.select_block_header_by_block_num(block_num).await?; if let Some(header) = block_header { let mmr_proof = if include_mmr_proof { - let inner = self.inner.read().await; - let mmr_proof = inner.blockchain.open(header.block_num())?; + let snapshot = self.snapshot(); + let mmr_proof = snapshot.blockchain.open(header.block_num())?; Some(mmr_proof) } else { None @@ -409,9 +391,6 @@ impl State { } /// Queries a list of notes from the database. - /// - /// If the provided list of [`NoteId`] given is empty or no note matches the provided - /// [`NoteId`] an empty list is returned. pub async fn get_notes_by_id( &self, note_ids: Vec, @@ -426,7 +405,7 @@ impl State { block_num: Option, ) -> Result, GetCurrentBlockchainDataError> { if let Some(number) = block_num - && number == self.chain_tip(Finality::Committed).await + && number == self.chain_tip(Finality::Committed) { return Ok(None); } @@ -440,8 +419,9 @@ impl State { .map_err(GetCurrentBlockchainDataError::ErrorRetrievingBlockHeader)? .unwrap(); - let blockchain = &self.inner.read().await.blockchain; - let peaks = blockchain + let snapshot = self.snapshot(); + let peaks = snapshot + .blockchain .peaks_at(block_header.block_num()) .map_err(GetCurrentBlockchainDataError::InvalidPeaks)?; @@ -449,23 +429,6 @@ impl State { } /// Fetches the inputs for a transaction batch from the database. - /// - /// ## Inputs - /// - /// The function takes as input: - /// - The tx reference blocks are the set of blocks referenced by transactions in the batch. - /// - The unauthenticated note commitments are the set of commitments of unauthenticated notes - /// consumed by all transactions in the batch. For these notes, we attempt to find inclusion - /// proofs. Not all notes will exist in the DB necessarily, as some notes can be created and - /// consumed within the same batch. - /// - /// ## Outputs - /// - /// The function will return: - /// - A block inclusion proof for all tx reference blocks and for all blocks which are - /// referenced by a note inclusion proof. - /// - Note inclusion proofs for all notes that were found in the DB. - /// - The block header that the batch should reference, i.e. the latest known block. pub async fn get_batch_inputs( &self, tx_reference_blocks: BTreeSet, @@ -475,30 +438,20 @@ impl State { return Err(GetBatchInputsError::TransactionBlockReferencesEmpty); } - // First we grab note inclusion proofs for the known notes. These proofs only prove that the - // note was included in a given block. We then also need to prove that each of those blocks - // is included in the chain. let note_proofs = self .db .select_note_inclusion_proofs(unauthenticated_note_commitments) .await .map_err(GetBatchInputsError::SelectNoteInclusionProofError)?; - // The set of blocks that the notes are included in. let note_blocks = note_proofs.values().map(|proof| proof.location().block_num()); - // Collect all blocks we need to query without duplicates, which is: - // - all blocks for which we need to prove note inclusion. - // - all blocks referenced by transactions in the batch. let mut blocks: BTreeSet = tx_reference_blocks; blocks.extend(note_blocks); - // Scoped block to automatically drop the read lock guard as soon as we're done. We also - // avoid accessing the db in the block as this would delay dropping the guard. + let snapshot = self.snapshot(); let (batch_reference_block, partial_mmr) = { - let inner_state = self.inner.read().await; - - let latest_block_num = inner_state.latest_block_num(); + let latest_block_num = snapshot.block_num; let highest_block_num = *blocks.last().expect("we should have checked for empty block references"); @@ -509,19 +462,10 @@ impl State { }); } - // Remove the latest block from the to-be-tracked blocks as it will be the reference - // block for the batch itself and thus added to the MMR within the batch kernel, so - // there is no need to prove its inclusion. blocks.remove(&latest_block_num); - // SAFETY: - // - The latest block num was retrieved from the inner blockchain from which we will - // also retrieve the proofs, so it is guaranteed to exist in that chain. - // - We have checked that no block number in the blocks set is greater than latest block - // number *and* latest block num was removed from the set. Therefore only block - // numbers smaller than latest block num remain in the set. Therefore all the block - // numbers are guaranteed to exist in the chain state at latest block num. - let partial_mmr = inner_state + // SAFETY: as in original code - latest block num exists in chain, all blocks < latest. + let partial_mmr = snapshot .blockchain .partial_mmr_from_blocks(&blocks, latest_block_num) .expect("latest block num should exist and all blocks in set should be < than latest block"); @@ -529,15 +473,12 @@ impl State { (latest_block_num, partial_mmr) }; - // Fetch the reference block of the batch as part of this query, so we can avoid looking it - // up in a separate DB access. let mut headers = self .db .select_block_headers(blocks.into_iter().chain(std::iter::once(batch_reference_block))) .await .map_err(GetBatchInputsError::SelectBlockHeaderError)?; - // Find and remove the batch reference block as we don't want to add it to the chain MMR. let header_index = headers .iter() .enumerate() @@ -546,17 +487,8 @@ impl State { }) .expect("DB should have returned the header of the batch reference block"); - // The order doesn't matter for PartialBlockchain::new, so swap remove is fine. let batch_reference_block_header = headers.swap_remove(header_index); - // SAFETY: This should not error because: - // - we're passing exactly the block headers that we've added to the partial MMR, - // - so none of the block headers block numbers should exceed the chain length of the - // partial MMR, - // - and we've added blocks to a BTreeSet, so there can be no duplicates. - // - // We construct headers and partial MMR in concert, so they are consistent. This is why we - // can call the unchecked constructor. let partial_block_chain = PartialBlockchain::new_unchecked(partial_mmr, headers) .expect("partial mmr and block headers should be consistent"); @@ -575,36 +507,27 @@ impl State { unauthenticated_note_commitments: BTreeSet, reference_blocks: BTreeSet, ) -> Result { - // Get the note inclusion proofs from the DB. We do this first so we have to acquire the - // lock to the state just once. There we need the reference blocks of the note proofs to get - // their authentication paths in the chain MMR. let unauthenticated_note_proofs = self .db .select_note_inclusion_proofs(unauthenticated_note_commitments) .await .map_err(GetBlockInputsError::SelectNoteInclusionProofError)?; - // The set of blocks that the notes are included in. let note_proof_reference_blocks = unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()); - // Collect all blocks we need to prove inclusion for, without duplicates. let mut blocks = reference_blocks; blocks.extend(note_proof_reference_blocks); let (latest_block_number, account_witnesses, nullifier_witnesses, partial_mmr) = self.get_block_inputs_witnesses(&mut blocks, &account_ids, &nullifiers)?; - // Fetch the block headers for all blocks in the partial MMR plus the latest one which will - // be used as the previous block header of the block being built. let mut headers = self .db .select_block_headers(blocks.into_iter().chain(std::iter::once(latest_block_number))) .await .map_err(GetBlockInputsError::SelectBlockHeaderError)?; - // Find and remove the latest block as we must not add it to the chain MMR, since it is not - // yet in the chain. let latest_block_header_index = headers .iter() .enumerate() @@ -613,17 +536,8 @@ impl State { }) .expect("DB should have returned the header of the latest block header"); - // The order doesn't matter for PartialBlockchain::new, so swap remove is fine. let latest_block_header = headers.swap_remove(latest_block_header_index); - // SAFETY: This should not error because: - // - we're passing exactly the block headers that we've added to the partial MMR, - // - so none of the block header's block numbers should exceed the chain length of the - // partial MMR, - // - and we've added blocks to a BTreeSet, so there can be no duplicates. - // - // We construct headers and partial MMR in concert, so they are consistent. This is why we - // can call the unchecked constructor. let partial_block_chain = PartialBlockchain::new_unchecked(partial_mmr, headers) .expect("partial mmr and block headers should be consistent"); @@ -636,65 +550,48 @@ impl State { )) } - /// Get account and nullifier witnesses for the requested account IDs and nullifier as well as - /// the [`PartialMmr`] for the given blocks. The MMR won't contain the latest block and its - /// number is removed from `blocks` and returned separately. - /// - /// This method acquires the lock to the inner state and does not access the DB so we release - /// the lock asap. + /// Get account and nullifier witnesses and [`PartialMmr`] for the given blocks. fn get_block_inputs_witnesses( &self, blocks: &mut BTreeSet, account_ids: &[AccountId], nullifiers: &[Nullifier], ) -> Result { - self.with_inner_read_blocking(|inner| { - let latest_block_number = inner.latest_block_num(); - - // If `blocks` is empty, use the latest block number which will never trigger the error. - let highest_block_number = blocks.last().copied().unwrap_or(latest_block_number); - if highest_block_number > latest_block_number { - return Err(GetBlockInputsError::UnknownBatchBlockReference { - highest_block_number, - latest_block_number, - }); - } + let snapshot = self.snapshot(); + let span = Span::current(); + tokio::task::block_in_place(|| { + span.in_scope(|| { + let latest_block_number = snapshot.block_num; + + let highest_block_number = blocks.last().copied().unwrap_or(latest_block_number); + if highest_block_number > latest_block_number { + return Err(GetBlockInputsError::UnknownBatchBlockReference { + highest_block_number, + latest_block_number, + }); + } - // The latest block is not yet in the chain MMR, so we can't (and don't need to) prove - // its inclusion in the chain. - blocks.remove(&latest_block_number); - - // Fetch the partial MMR at the state of the latest block with authentication paths for - // the provided set of blocks. - // - // SAFETY: - // - The latest block num was retrieved from the inner blockchain from which we will - // also retrieve the proofs, so it is guaranteed to exist in that chain. - // - We have checked that no block number in the blocks set is greater than latest block - // number *and* latest block num was removed from the set. Therefore only block - // numbers smaller than latest block num remain in the set. Therefore all the block - // numbers are guaranteed to exist in the chain state at latest block num. - let partial_mmr = - inner.blockchain.partial_mmr_from_blocks(blocks, latest_block_number).expect( - "latest block num should exist and all blocks in set should be < than latest block", - ); - - // Fetch witnesses for all accounts. - let account_witnesses = account_ids - .iter() - .copied() - .map(|account_id| (account_id, inner.account_tree.open_latest(account_id))) - .collect::>(); - - // Fetch witnesses for all nullifiers. We don't check whether the nullifiers are spent - // or not as this is done as part of proposing the block. - let nullifier_witnesses: BTreeMap = nullifiers - .iter() - .copied() - .map(|nullifier| (nullifier, inner.nullifier_tree.open(&nullifier))) - .collect(); - - Ok((latest_block_number, account_witnesses, nullifier_witnesses, partial_mmr)) + blocks.remove(&latest_block_number); + + let partial_mmr = + snapshot.blockchain.partial_mmr_from_blocks(blocks, latest_block_number).expect( + "latest block num should exist and all blocks in set should be < than latest block", + ); + + let account_witnesses = account_ids + .iter() + .copied() + .map(|account_id| (account_id, snapshot.account_tree.open_latest(account_id))) + .collect::>(); + + let nullifier_witnesses: BTreeMap = nullifiers + .iter() + .copied() + .map(|nullifier| (nullifier, snapshot.nullifier_tree.open(&nullifier))) + .collect(); + + Ok((latest_block_number, account_witnesses, nullifier_witnesses, partial_mmr)) + }) }) } @@ -708,32 +605,42 @@ impl State { ) -> Result { info!(target: COMPONENT, account_id = %account_id.to_string(), nullifiers = %format_array(nullifiers)); - let tree_inputs = self.with_inner_read_blocking(|inner| { - let account_commitment = inner.account_tree.get_latest_commitment(account_id); - - let new_account_id_prefix_is_unique = if account_commitment.is_empty() { - Some(!inner.account_tree.contains_account_id_prefix_in_latest(account_id.prefix())) - } else { - None - }; - - // Non-unique account Id prefixes for new accounts are not allowed. - if let Some(false) = new_account_id_prefix_is_unique { - return Err(TransactionInputs { - new_account_id_prefix_is_unique, - ..Default::default() - }); - } - - let nullifiers = nullifiers - .iter() - .map(|nullifier| NullifierInfo { - nullifier: *nullifier, - block_num: inner.nullifier_tree.get_block_num(nullifier).unwrap_or_default(), - }) - .collect(); + let snapshot = self.snapshot(); + let span = Span::current(); + let tree_inputs = tokio::task::block_in_place(|| { + span.in_scope(|| { + let account_commitment = snapshot.account_tree.get_latest_commitment(account_id); + + let new_account_id_prefix_is_unique = if account_commitment.is_empty() { + Some( + !snapshot + .account_tree + .contains_account_id_prefix_in_latest(account_id.prefix()), + ) + } else { + None + }; + + if let Some(false) = new_account_id_prefix_is_unique { + return Err(TransactionInputs { + new_account_id_prefix_is_unique, + ..Default::default() + }); + } - Ok((account_commitment, nullifiers, new_account_id_prefix_is_unique)) + let nullifiers = nullifiers + .iter() + .map(|nullifier| NullifierInfo { + nullifier: *nullifier, + block_num: snapshot + .nullifier_tree + .get_block_num(nullifier) + .unwrap_or_default(), + }) + .collect(); + + Ok((account_commitment, nullifiers, new_account_id_prefix_is_unique)) + }) }); let (account_commitment, nullifiers, new_account_id_prefix_is_unique) = match tree_inputs { Ok(inputs) => inputs, @@ -774,14 +681,7 @@ impl State { self.db.select_network_accounts_subset(account_ids.to_vec()).await } - /// Returns network account IDs within the specified block range (based on account creation - /// block). - /// - /// The function may return fewer accounts than exist in the range if the result would exceed - /// `MAX_RESPONSE_PAYLOAD_BYTES / AccountId::SERIALIZED_SIZE` rows. In this case, the result is - /// truncated at a block boundary to ensure all accounts from included blocks are returned. - /// - /// The response includes the last block number that was fully included in the result. + /// Returns network account IDs within the specified block range. pub async fn get_all_network_accounts( &self, block_range: RangeInclusive, @@ -790,14 +690,6 @@ impl State { } /// Returns an account witness and optionally account details at a specific block. - /// - /// The witness is a Merkle proof of inclusion in the account tree, proving the account's - /// state commitment. If `details` is requested, the method also returns the account's code, - /// vault assets, and storage data. Account details are only available for public accounts. - /// - /// If `block_num` is provided, returns the state at that historical block; otherwise, returns - /// the latest state. Note that historical states are only available for recent blocks close - /// to the chain tip. #[instrument(target = COMPONENT, skip_all)] pub async fn get_account( &self, @@ -821,48 +713,41 @@ impl State { } /// Returns an account witness (Merkle proof of inclusion in the account tree). - /// - /// If `block_num` is provided, returns the witness at that historical block; - /// otherwise, returns the witness at the latest block. #[instrument(target = COMPONENT, skip_all)] async fn get_account_witness( &self, block_num: Option, account_id: AccountId, ) -> Result<(BlockNumber, AccountWitness), GetAccountError> { - self.with_inner_read_blocking(|inner_state| { - // Determine which block to query - let (block_num, witness) = if let Some(requested_block) = block_num { - // Historical query: use the account tree with history - let witness = inner_state - .account_tree - .open_at(account_id, requested_block) - .ok_or_else(|| { - let latest_block = inner_state.account_tree.block_number_latest(); - if requested_block > latest_block { - GetAccountError::UnknownBlock(requested_block) - } else { - GetAccountError::BlockPruned(requested_block) - } - })?; - (requested_block, witness) - } else { - // Latest query: use the latest state - let block_num = inner_state.account_tree.block_number_latest(); - let witness = inner_state.account_tree.open_latest(account_id); - (block_num, witness) - }; - - Ok((block_num, witness)) + let snapshot = self.snapshot(); + let span = Span::current(); + tokio::task::block_in_place(|| { + span.in_scope(|| { + let (block_num, witness) = if let Some(requested_block) = block_num { + let witness = snapshot + .account_tree + .open_at(account_id, requested_block) + .ok_or_else(|| { + let latest_block = snapshot.account_tree.block_number_latest(); + if requested_block > latest_block { + GetAccountError::UnknownBlock(requested_block) + } else { + GetAccountError::BlockPruned(requested_block) + } + })?; + (requested_block, witness) + } else { + let block_num = snapshot.account_tree.block_number_latest(); + let witness = snapshot.account_tree.open_latest(account_id); + (block_num, witness) + }; + + Ok((block_num, witness)) + }) }) } /// Returns storage map details from the forest for a specific account and storage slot. - /// - /// The forest can only be used if all hashed keys in the storage map are known in the - /// reverse-key LRU cache. If any hashed key is unknown, the method returns `Ok(None)` to signal - /// that the caller should fall back to reconstructing the storage map details from the - /// database. #[instrument(target = COMPONENT, skip_all)] fn get_storage_map_details_from_forest( &self, @@ -915,14 +800,6 @@ impl State { /// Fetches the account details (code, vault, storage) for a public account at the specified /// block. - /// - /// This method queries the database to fetch the account state and processes the detail - /// request to return only the requested information. - /// - /// For specific key queries (`SlotData::MapKeys`), the forest is used to provide SMT proofs. - /// Returns an error if the forest doesn't have data for the requested slot. - /// All-entries queries (`SlotData::All`) use the forest when all hashed keys are known in the - /// reverse-key LRU cache, otherwise they fall back to database reconstruction. #[expect(clippy::too_many_lines)] #[instrument(target = COMPONENT, skip_all)] async fn fetch_public_account_details( @@ -941,17 +818,14 @@ impl State { return Err(GetAccountError::AccountNotPublic(account_id)); } - // Validate block exists in the blockchain before querying the database + // Validate block exists in the blockchain before querying the database. { - let inner = self.inner.read().instrument(tracing::info_span!("acquire_inner")).await; - let latest_block_num = inner.latest_block_num(); - - if block_num > latest_block_num { + let snapshot = self.snapshot(); + if block_num > snapshot.block_num { return Err(GetAccountError::UnknownBlock(block_num)); } } - // Query account header and storage header together in a single DB call let (account_header, storage_header) = self .db .select_account_header_with_storage_header_at_block(account_id, block_num) @@ -1063,17 +937,13 @@ impl State { /// Returns the effective chain tip for the given finality level. /// - /// - [`Finality::Committed`]: returns the latest committed block number (from in-memory MMR). + /// - [`Finality::Committed`]: returns the latest committed block number (from in-memory + /// snapshot, wait-free). /// - [`Finality::Proven`]: returns the latest proven-in-sequence block number (cached via watch /// channel, updated by the proof scheduler). - pub async fn chain_tip(&self, finality: Finality) -> BlockNumber { + pub fn chain_tip(&self, finality: Finality) -> BlockNumber { match finality { - Finality::Committed => self - .inner - .read() - .instrument(tracing::info_span!("acquire_inner")) - .await - .latest_block_num(), + Finality::Committed => self.in_memory.load().block_num, Finality::Proven => self.proven_tip.read(), } } @@ -1083,7 +953,7 @@ impl State { &self, block_num: BlockNumber, ) -> Result>, DatabaseError> { - if block_num > self.chain_tip(Finality::Committed).await { + if block_num > self.chain_tip(Finality::Committed) { return Ok(None); } self.block_store.load_block(block_num).await.map_err(Into::into) @@ -1094,14 +964,13 @@ impl State { &self, block_num: BlockNumber, ) -> Result>, DatabaseError> { - if block_num > self.chain_tip(Finality::Proven).await { + if block_num > self.chain_tip(Finality::Proven) { return Ok(None); } self.block_store.load_proof(block_num).await.map_err(Into::into) } - /// Returns the network notes for an account that are unconsumed by a specified block number, - /// along with the next pagination token. + /// Returns the network notes for an account that are unconsumed by a specified block number. pub async fn get_unconsumed_network_notes_for_account( &self, account_id: AccountId, @@ -1133,9 +1002,6 @@ impl State { /// Returns a storage map witness for the specified account and storage entry at the block /// number. - /// - /// Note that the `raw_key` is the raw, user-provided key that needs to be hashed in order to - /// get the actual key into the storage map. pub fn get_storage_map_witness( &self, account_id: AccountId, diff --git a/crates/store/src/state/sync_state.rs b/crates/store/src/state/sync_state.rs index d2f8fc094..e63ccab27 100644 --- a/crates/store/src/state/sync_state.rs +++ b/crates/store/src/state/sync_state.rs @@ -67,9 +67,7 @@ impl State { let to_forest = block_to.as_usize(); let mmr_delta = self - .inner - .read() - .await + .snapshot() .blockchain .as_mmr() .get_delta( @@ -106,11 +104,12 @@ impl State { let mut results = Vec::new(); { - let inner = self.inner.read().await; + let snapshot = self.snapshot(); for note_sync in note_syncs { - let mmr_proof = - inner.blockchain.open_at(note_sync.block_header.block_num(), mmr_checkpoint)?; + let mmr_proof = snapshot + .blockchain + .open_at(note_sync.block_header.block_num(), mmr_checkpoint)?; results.push((note_sync, mmr_proof)); } } diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs new file mode 100644 index 000000000..6828f98c7 --- /dev/null +++ b/crates/store/src/state/writer.rs @@ -0,0 +1,330 @@ +//! Serialised block-write path for the store state. +//! +//! A single [`BlockWriter`] task owns the mutable trees and processes incoming [`WriteRequest`]s +//! one at a time via an mpsc channel. After each successful commit it publishes a new +//! [`InMemoryState`] snapshot via an [`ArcSwap`], making the updated trees immediately visible to +//! wait-free readers. + +use std::sync::Arc; + +use arc_swap::ArcSwap; +use miden_node_utils::ErrorReport; +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::block::account_tree::AccountMutationSet; +use miden_protocol::block::nullifier_tree::{NullifierMutationSet, NullifierTree}; +use miden_protocol::block::{BlockBody, BlockHeader, BlockNumber, Blockchain, SignedBlock}; +use miden_protocol::crypto::merkle::smt::LargeSmt; +use miden_protocol::note::{NoteAttachments, NoteDetails, Nullifier}; +use miden_protocol::transaction::OutputNote; +use miden_protocol::utils::serde::Serializable; +use tokio::sync::{RwLock, mpsc, oneshot, watch}; +use tracing::{Instrument, info, info_span, instrument}; + +use crate::account_state_forest::{AccountStateForest, AccountStateForestBackend}; +use crate::accounts::AccountTreeWithHistory; +use crate::blocks::BlockStore; +use crate::db::{Db, NoteRecord}; +use crate::errors::{ApplyBlockError, InvalidBlockError}; +use crate::state::loader::TreeStorage; +use crate::state::{BlockCache, BlockNotification, InMemoryState}; +use crate::{COMPONENT, HistoricalError}; + +// WRITE REQUEST +// ================================================================================================ + +/// A request to apply a block, paired with a one-shot channel for the result. +pub struct WriteRequest { + pub signed_block: SignedBlock, + pub result_tx: oneshot::Sender>, +} + +// WRITE HANDLE +// ================================================================================================ + +/// Cloneable handle for sending block-write requests to the [`BlockWriter`] task. +#[derive(Clone)] +pub struct WriteHandle { + tx: mpsc::Sender, +} + +impl WriteHandle { + pub(super) fn new(tx: mpsc::Sender) -> Self { + Self { tx } + } + + /// Sends a block to the writer task and awaits its result. + pub async fn apply_block(&self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> { + let (result_tx, result_rx) = oneshot::channel(); + self.tx + .send(WriteRequest { signed_block, result_tx }) + .await + .map_err(|e| ApplyBlockError::WriterTaskSendFailed(e.to_string()))?; + result_rx.await.map_err(ApplyBlockError::WriterTaskRecvFailed)? + } +} + +// BLOCK WRITER +// ================================================================================================ + +/// Single-task owner of the mutable trees. Processes [`WriteRequest`]s serially. +pub(super) struct BlockWriter { + pub db: Arc, + pub block_store: Arc, + pub in_memory: Arc>, + pub forest: Arc>>, + pub committed_tip_tx: Arc>, + pub block_cache: BlockCache, + pub termination_ask: tokio::sync::mpsc::Sender, + pub rx: mpsc::Receiver, + /// The mutable nullifier tree owned by this writer. + pub nullifier_tree: NullifierTree>, + /// The mutable account tree owned by this writer. + pub account_tree: AccountTreeWithHistory, + /// The blockchain MMR owned by this writer. + pub blockchain: Blockchain, +} + +impl BlockWriter { + /// Runs the writer loop, processing requests until the channel closes. + pub async fn run(mut self) { + while let Some(req) = self.rx.recv().await { + let result = self.process_request(req.signed_block).await; + let _ = req.result_tx.send(result); + } + } + + #[instrument(target = COMPONENT, skip_all, err)] + async fn process_request( + &mut self, + signed_block: SignedBlock, + ) -> Result<(), ApplyBlockError> { + let header = signed_block.header(); + let body = signed_block.body(); + + let block_num = header.block_num(); + let block_commitment = header.commitment(); + + self.validate_block_header(header, body).await?; + + // Save the block to the block store concurrently with computing mutations. + let signed_block_bytes = signed_block.to_bytes(); + let cache_bytes = signed_block_bytes.clone(); + let store = Arc::clone(&self.block_store); + let block_save_task = tokio::spawn( + async move { store.save_block(block_num, &signed_block_bytes).await } + .in_current_span(), + ); + + let (nullifier_tree_update, account_tree_update) = + self.compute_tree_mutations(header, body)?; + + let notes = Self::build_note_records(header, body)?; + + // Extract public account deltas before `signed_block` is moved. + let account_deltas = + Vec::from_iter(body.updated_accounts().iter().filter_map( + |update| match update.details() { + AccountUpdateDetails::Delta(delta) => Some(delta.clone()), + AccountUpdateDetails::Private => None, + }, + )); + + // Commit to the database. + let db = Arc::clone(&self.db); + db.apply_block(signed_block, notes) + .instrument(info_span!(target: COMPONENT, "db_apply_block")) + .await + .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?; + + // Wait for the block store save to complete. + block_save_task.await??; + + // Apply mutations to the owned mutable trees. + tokio::task::block_in_place(|| { + self.nullifier_tree + .apply_mutations(nullifier_tree_update) + .expect("nullifier tree mutation should succeed after validation"); + + self.account_tree + .apply_mutations(account_tree_update) + .expect("account tree mutation should succeed after validation"); + + self.blockchain.push(block_commitment); + + // Publish a new snapshot via ArcSwap. + let snapshot = Arc::new(InMemoryState { + block_num, + nullifier_tree: self + .nullifier_tree + .reader() + .expect("nullifier tree snapshot creation should not fail"), + account_tree: self.account_tree.reader(), + blockchain: self.blockchain.clone(), + }); + self.in_memory.store(snapshot); + + Ok::<(), ApplyBlockError>(()) + })?; + + // Update the forest. + tokio::task::block_in_place(|| { + let mut forest = self.forest.blocking_write(); + forest.apply_block_updates(block_num, account_deltas) + })?; + + // Notify replica subscribers. + self.block_cache.push(block_num, BlockNotification::new(block_num, cache_bytes)); + let _ = self.committed_tip_tx.send(block_num); + + info!(%block_commitment, block_num = block_num.as_u32(), COMPONENT, "apply_block successful"); + + Ok(()) + } + + /// Validates that the block header is consistent with the block body and the current state. + #[instrument(target = COMPONENT, skip_all, err)] + async fn validate_block_header( + &self, + header: &BlockHeader, + body: &BlockBody, + ) -> Result<(), ApplyBlockError> { + let tx_commitment = body.transactions().commitment(); + if header.tx_commitment() != tx_commitment { + return Err(InvalidBlockError::InvalidBlockTxCommitment { + expected: tx_commitment, + actual: header.tx_commitment(), + } + .into()); + } + + let block_num = header.block_num(); + + let prev_block = self + .db + .select_block_header_by_block_num(None) + .await? + .ok_or(ApplyBlockError::DbBlockHeaderEmpty)?; + let expected_block_num = prev_block.block_num().child(); + if block_num != expected_block_num { + return Err(InvalidBlockError::NewBlockInvalidBlockNum { + expected: expected_block_num, + submitted: block_num, + } + .into()); + } + if header.prev_block_commitment() != prev_block.commitment() { + return Err(InvalidBlockError::NewBlockInvalidPrevCommitment.into()); + } + + Ok(()) + } + + /// Computes nullifier and account tree mutations from the owned mutable trees. + #[instrument(target = COMPONENT, skip_all, err)] + fn compute_tree_mutations( + &self, + header: &BlockHeader, + body: &BlockBody, + ) -> Result<(NullifierMutationSet, AccountMutationSet), ApplyBlockError> { + let block_num = header.block_num(); + + let duplicate_nullifiers: Vec<_> = body + .created_nullifiers() + .iter() + .filter(|&nullifier| self.nullifier_tree.get_block_num(nullifier).is_some()) + .copied() + .collect(); + if !duplicate_nullifiers.is_empty() { + return Err(InvalidBlockError::DuplicatedNullifiers(duplicate_nullifiers).into()); + } + + let peaks = self.blockchain.peaks(); + if peaks.hash_peaks() != header.chain_commitment() { + return Err(InvalidBlockError::NewBlockInvalidChainCommitment.into()); + } + + let nullifier_tree_update = self + .nullifier_tree + .compute_mutations( + body.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)), + ) + .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?; + + if nullifier_tree_update.as_mutation_set().root() != header.nullifier_root() { + let _ = self.termination_ask.try_send(ApplyBlockError::InvalidBlockError( + InvalidBlockError::NewBlockInvalidNullifierRoot, + )); + return Err(InvalidBlockError::NewBlockInvalidNullifierRoot.into()); + } + + let account_tree_update = self + .account_tree + .compute_mutations( + body.updated_accounts() + .iter() + .map(|update| (update.account_id(), update.final_state_commitment())), + ) + .map_err(|e| match e { + HistoricalError::AccountTreeError(err) => { + InvalidBlockError::NewBlockDuplicateAccountIdPrefix(err) + }, + HistoricalError::MerkleError(_) => { + panic!("Unexpected MerkleError during account tree mutation computation") + }, + })?; + + if account_tree_update.as_mutation_set().root() != header.account_root() { + let _ = self.termination_ask.try_send(ApplyBlockError::InvalidBlockError( + InvalidBlockError::NewBlockInvalidAccountRoot, + )); + return Err(InvalidBlockError::NewBlockInvalidAccountRoot.into()); + } + + Ok((nullifier_tree_update, account_tree_update)) + } + + /// Builds note records with inclusion proofs from the block body. + #[instrument(target = COMPONENT, skip_all, err)] + fn build_note_records( + header: &BlockHeader, + body: &BlockBody, + ) -> Result)>, ApplyBlockError> { + let block_num = header.block_num(); + + let note_tree = body.compute_block_note_tree(); + if note_tree.root() != header.note_root() { + return Err(InvalidBlockError::NewBlockInvalidNoteRoot.into()); + } + + let notes = body + .output_notes() + .map(|(note_index, note)| { + let (details, attachments, nullifier) = match note { + OutputNote::Public(public) => ( + Some(NoteDetails::from(public.as_note())), + public.as_note().attachments().clone(), + Some(public.as_note().nullifier()), + ), + OutputNote::Private(_) => (None, NoteAttachments::empty(), None), + }; + + let inclusion_path = note_tree.open(note_index); + + let note_record = NoteRecord { + block_num, + note_index, + note_id: note.id().as_word(), + metadata: *note.metadata(), + details, + attachments, + inclusion_path, + }; + + Ok((note_record, nullifier)) + }) + .collect::, InvalidBlockError>>()?; + + Ok(notes) + } +} + From 1933f2d6e2cb8e7144abb0347c39c85df32c6517 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 10:20:56 +1200 Subject: [PATCH 02/11] Update protocol and fix --- Cargo.lock | 36 ++-- bin/genesis/src/main.rs | 4 +- bin/stress-test/src/seeding/mod.rs | 4 +- crates/store/src/account_state_forest/mod.rs | 6 +- .../store/src/account_state_forest/tests.rs | 186 +++++++++--------- .../src/db/models/queries/accounts/delta.rs | 4 +- .../db/models/queries/accounts/delta/tests.rs | 20 +- .../src/db/models/queries/accounts/tests.rs | 12 +- crates/store/src/db/tests.rs | 108 +++++----- crates/store/src/genesis/config/mod.rs | 6 +- .../samples/02-with-account-files/bridge.mac | Bin 37863 -> 37945 bytes crates/store/src/state/mod.rs | 6 - 12 files changed, 193 insertions(+), 199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4075f373d..8ffa36aaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -188,7 +188,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1693,7 +1693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2954,7 +2954,7 @@ dependencies = [ [[package]] name = "miden-agglayer" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "alloy-sol-types", "fs-err", @@ -3034,7 +3034,7 @@ dependencies = [ [[package]] name = "miden-block-prover" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "miden-protocol", "thiserror 2.0.18", @@ -3668,7 +3668,7 @@ dependencies = [ [[package]] name = "miden-protocol" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "bech32", "fs-err", @@ -3771,7 +3771,7 @@ dependencies = [ [[package]] name = "miden-standards" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "bon", "fs-err", @@ -3809,7 +3809,7 @@ dependencies = [ [[package]] name = "miden-testing" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3829,7 +3829,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "miden-processor", "miden-protocol", @@ -3842,7 +3842,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.16.0" -source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#3500acf62cee4eadce748e2d814a79d25c2d79de" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "miden-protocol", "miden-tx", @@ -4064,7 +4064,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5400,7 +5400,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5471,7 +5471,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5879,7 +5879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6100,7 +6100,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6109,7 +6109,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6128,7 +6128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7140,7 +7140,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 3155a726c..291ca1ea6 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -5,7 +5,7 @@ use anyhow::Context; use clap::Parser; use miden_agglayer::create_bridge_account; use miden_protocol::account::auth::{AuthScheme, AuthSecretKey}; -use miden_protocol::account::delta::{AccountStorageDelta, AccountVaultDelta}; +use miden_protocol::account::delta::{AccountStoragePatch, AccountVaultDelta}; use miden_protocol::account::{Account, AccountDelta, AccountFile, AccountType}; use miden_protocol::crypto::dsa::falcon512_poseidon2::{self, SecretKey as FalconSecretKey}; use miden_protocol::crypto::rand::RandomCoin; @@ -178,7 +178,7 @@ fn resolve_pubkey( fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { let delta = AccountDelta::new( account.id(), - AccountStorageDelta::default(), + AccountStoragePatch::new(), AccountVaultDelta::default(), ONE, )?; diff --git a/bin/stress-test/src/seeding/mod.rs b/bin/stress-test/src/seeding/mod.rs index 15ea556e1..4aa6954c4 100644 --- a/bin/stress-test/src/seeding/mod.rs +++ b/bin/stress-test/src/seeding/mod.rs @@ -19,7 +19,7 @@ use miden_protocol::account::{ AccountComponentMetadata, AccountDelta, AccountId, - AccountStorageDelta, + AccountStoragePatch, AccountType, AccountVaultDelta, StorageMap, @@ -714,7 +714,7 @@ fn create_existing_account_delta( vault_delta.add_asset(*asset).unwrap(); } - let mut storage_delta = AccountStorageDelta::new(); + let mut storage_delta = AccountStoragePatch::new(); if let Some((storage_update, tx_index)) = storage_update { if storage_update.storage_map_entries > 0 && account.storage().get(&benchmark_storage_map_slot()).is_some() diff --git a/crates/store/src/account_state_forest/mod.rs b/crates/store/src/account_state_forest/mod.rs index 3d793fa71..f3b4ee00b 100644 --- a/crates/store/src/account_state_forest/mod.rs +++ b/crates/store/src/account_state_forest/mod.rs @@ -8,7 +8,7 @@ use miden_crypto::merkle::smt::{Backend, ForestInMemoryBackend}; use miden_node_proto::domain::account::{AccountStorageMapDetails, AccountVaultDetails}; use miden_node_utils::ErrorReport; use miden_node_utils::lru_cache::LruCache; -use miden_protocol::account::delta::{AccountDelta, AccountStorageDelta, AccountVaultDelta}; +use miden_protocol::account::delta::{AccountDelta, AccountStoragePatch, AccountVaultDelta}; use miden_protocol::account::{ AccountId, NonFungibleDeltaAction, @@ -629,7 +629,7 @@ impl AccountStateForest { &mut self, block_num: BlockNumber, account_id: AccountId, - storage_delta: &AccountStorageDelta, + storage_delta: &AccountStoragePatch, ) { for (slot_name, map_delta) in storage_delta.maps() { // get the latest root for this map, and make sure the root is for an empty tree @@ -784,7 +784,7 @@ impl AccountStateForest { &mut self, block_num: BlockNumber, account_id: AccountId, - storage_delta: &AccountStorageDelta, + storage_delta: &AccountStoragePatch, ) { for (slot_name, map_delta) in storage_delta.maps() { // map delta shouldn't be empty, but if it is for some reason, there is nothing to do diff --git a/crates/store/src/account_state_forest/tests.rs b/crates/store/src/account_state_forest/tests.rs index a1f84e237..a42b99e7e 100644 --- a/crates/store/src/account_state_forest/tests.rs +++ b/crates/store/src/account_state_forest/tests.rs @@ -35,7 +35,7 @@ fn dummy_fungible_asset(faucet_id: AccountId, amount: u64) -> Asset { fn dummy_partial_delta( account_id: AccountId, vault_delta: AccountVaultDelta, - storage_delta: AccountStorageDelta, + storage_delta: AccountStoragePatch, ) -> AccountDelta { let nonce_delta = if vault_delta.is_empty() && storage_delta.is_empty() { Felt::ZERO @@ -86,7 +86,7 @@ fn update_account_with_empty_deltas() { let delta = dummy_partial_delta( account_id, AccountVaultDelta::default(), - AccountStorageDelta::default(), + AccountStoragePatch::default(), ); forest.update_account(block_num, &delta).unwrap(); @@ -110,7 +110,7 @@ fn vault_partial_vs_full_state_produces_same_root() { let mut vault_delta = AccountVaultDelta::default(); vault_delta.add_asset(asset).unwrap(); let partial_delta = - dummy_partial_delta(account_id, vault_delta, AccountStorageDelta::default()); + dummy_partial_delta(account_id, vault_delta, AccountStoragePatch::default()); forest_partial.update_account(block_num, &partial_delta).unwrap(); // Full-state delta (DB reconstruction) @@ -135,7 +135,7 @@ fn vault_incremental_updates_with_add_and_remove() { let block_1 = BlockNumber::GENESIS.child(); let mut vault_delta_1 = AccountVaultDelta::default(); vault_delta_1.add_asset(dummy_fungible_asset(faucet_id, 100)).unwrap(); - let delta_1 = dummy_partial_delta(account_id, vault_delta_1, AccountStorageDelta::default()); + let delta_1 = dummy_partial_delta(account_id, vault_delta_1, AccountStoragePatch::default()); forest.update_account(block_1, &delta_1).unwrap(); let root_after_100 = forest.get_vault_root(account_id, block_1).unwrap(); @@ -143,7 +143,7 @@ fn vault_incremental_updates_with_add_and_remove() { let block_2 = block_1.child(); let mut vault_delta_2 = AccountVaultDelta::default(); vault_delta_2.add_asset(dummy_fungible_asset(faucet_id, 50)).unwrap(); - let delta_2 = dummy_partial_delta(account_id, vault_delta_2, AccountStorageDelta::default()); + let delta_2 = dummy_partial_delta(account_id, vault_delta_2, AccountStoragePatch::default()); forest.update_account(block_2, &delta_2).unwrap(); let root_after_150 = forest.get_vault_root(account_id, block_2).unwrap(); @@ -153,7 +153,7 @@ fn vault_incremental_updates_with_add_and_remove() { let block_3 = block_2.child(); let mut vault_delta_3 = AccountVaultDelta::default(); vault_delta_3.remove_asset(dummy_fungible_asset(faucet_id, 30)).unwrap(); - let delta_3 = dummy_partial_delta(account_id, vault_delta_3, AccountStorageDelta::default()); + let delta_3 = dummy_partial_delta(account_id, vault_delta_3, AccountStoragePatch::default()); forest.update_account(block_3, &delta_3).unwrap(); let root_after_120 = forest.get_vault_root(account_id, block_3).unwrap(); @@ -182,7 +182,7 @@ fn vault_details_returns_latest_and_historical_assets() { let block_2 = block_1.child(); let mut vault_delta_2 = AccountVaultDelta::default(); vault_delta_2.add_asset(dummy_fungible_asset(faucet_id, 50)).unwrap(); - let delta_2 = dummy_partial_delta(account_id, vault_delta_2, AccountStorageDelta::default()); + let delta_2 = dummy_partial_delta(account_id, vault_delta_2, AccountStoragePatch::default()); forest.update_account(block_2, &delta_2).unwrap(); let historical = forest.get_vault_details(account_id, block_1).unwrap(); @@ -222,7 +222,7 @@ fn forest_versions_are_continuous_for_sequential_updates() { use std::collections::BTreeMap; use assert_matches::assert_matches; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -239,10 +239,10 @@ fn forest_versions_are_continuous_for_sequential_updates() { .add_asset(dummy_fungible_asset(faucet_id, u64::from(i) * 10)) .unwrap(); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(raw_key, Word::from([i, 0, 0, 0])); - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, vault_delta, storage_delta); forest.update_account(block_num, &delta).unwrap(); @@ -264,13 +264,13 @@ fn vault_state_is_not_available_for_block_gaps() { let block_1 = BlockNumber::GENESIS.child(); let mut vault_delta_1 = AccountVaultDelta::default(); vault_delta_1.add_asset(dummy_fungible_asset(faucet_id, 100)).unwrap(); - let delta_1 = dummy_partial_delta(account_id, vault_delta_1, AccountStorageDelta::default()); + let delta_1 = dummy_partial_delta(account_id, vault_delta_1, AccountStoragePatch::default()); forest.update_account(block_1, &delta_1).unwrap(); let block_6 = BlockNumber::from(6); let mut vault_delta_6 = AccountVaultDelta::default(); vault_delta_6.add_asset(dummy_fungible_asset(faucet_id, 150)).unwrap(); - let delta_6 = dummy_partial_delta(account_id, vault_delta_6, AccountStorageDelta::default()); + let delta_6 = dummy_partial_delta(account_id, vault_delta_6, AccountStoragePatch::default()); forest.update_account(block_6, &delta_6).unwrap(); assert!(forest.get_vault_root(account_id, BlockNumber::from(3)).is_some()); @@ -283,7 +283,7 @@ fn witness_queries_work_with_sparse_lineage_updates() { use std::collections::BTreeMap; use assert_matches::assert_matches; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -295,17 +295,17 @@ fn witness_queries_work_with_sparse_lineage_updates() { let block_1 = BlockNumber::GENESIS.child(); let mut vault_delta_1 = AccountVaultDelta::default(); vault_delta_1.add_asset(dummy_fungible_asset(faucet_id, 100)).unwrap(); - let mut map_delta_1 = StorageMapDelta::default(); + let mut map_delta_1 = StorageMapPatch::default(); map_delta_1.insert(raw_key, value); - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_1))]); - let storage_delta_1 = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_1))]); + let storage_delta_1 = AccountStoragePatch::from_raw(raw); let delta_1 = dummy_partial_delta(account_id, vault_delta_1, storage_delta_1); forest.update_account(block_1, &delta_1).unwrap(); let block_3 = block_1.child().child(); let mut vault_delta_3 = AccountVaultDelta::default(); vault_delta_3.add_asset(dummy_fungible_asset(faucet_id, 50)).unwrap(); - let delta_3 = dummy_partial_delta(account_id, vault_delta_3, AccountStorageDelta::default()); + let delta_3 = dummy_partial_delta(account_id, vault_delta_3, AccountStoragePatch::default()); forest.update_account(block_3, &delta_3).unwrap(); let block_2 = block_1.child(); @@ -384,12 +384,12 @@ fn vault_shared_root_retained_when_one_entry_pruned() { let mut vault_delta_1 = AccountVaultDelta::default(); vault_delta_1.add_asset(asset).unwrap(); - let delta_1 = dummy_partial_delta(account1, vault_delta_1, AccountStorageDelta::default()); + let delta_1 = dummy_partial_delta(account1, vault_delta_1, AccountStoragePatch::default()); forest.update_account(block_1, &delta_1).unwrap(); let mut vault_delta_2 = AccountVaultDelta::default(); vault_delta_2.add_asset(dummy_fungible_asset(faucet_id, asset_amount)).unwrap(); - let delta_2 = dummy_partial_delta(account2, vault_delta_2, AccountStorageDelta::default()); + let delta_2 = dummy_partial_delta(account2, vault_delta_2, AccountStoragePatch::default()); forest.update_account(block_1, &delta_2).unwrap(); let root1 = forest.get_vault_root(account1, block_1).unwrap(); @@ -402,7 +402,7 @@ fn vault_shared_root_retained_when_one_entry_pruned() { .add_asset(dummy_fungible_asset(faucet_id, amount_increment)) .unwrap(); let delta_2_update = - dummy_partial_delta(account2, vault_delta_2_update, AccountStorageDelta::default()); + dummy_partial_delta(account2, vault_delta_2_update, AccountStoragePatch::default()); forest.update_account(block_at_51, &delta_2_update).unwrap(); let block_at_52 = BlockNumber::from(HISTORICAL_BLOCK_RETENTION + 2); @@ -430,7 +430,7 @@ fn vault_shared_root_retained_when_one_entry_pruned() { fn storage_map_incremental_updates() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -444,30 +444,30 @@ fn storage_map_incremental_updates() { // Block 1: Insert key1 -> value1 let block_1 = BlockNumber::GENESIS.child(); - let mut map_delta_1 = StorageMapDelta::default(); + let mut map_delta_1 = StorageMapPatch::default(); map_delta_1.insert(key1, value1); - let raw_1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_1))]); - let storage_delta_1 = AccountStorageDelta::from_raw(raw_1); + let raw_1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_1))]); + let storage_delta_1 = AccountStoragePatch::from_raw(raw_1); let delta_1 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_1); forest.update_account(block_1, &delta_1).unwrap(); let root_1 = forest.get_storage_map_root(account_id, &slot_name, block_1).unwrap(); // Block 2: Insert key2 -> value2 let block_2 = block_1.child(); - let mut map_delta_2 = StorageMapDelta::default(); + let mut map_delta_2 = StorageMapPatch::default(); map_delta_2.insert(key2, value2); - let raw_2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_2))]); - let storage_delta_2 = AccountStorageDelta::from_raw(raw_2); + let raw_2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_2))]); + let storage_delta_2 = AccountStoragePatch::from_raw(raw_2); let delta_2 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_2); forest.update_account(block_2, &delta_2).unwrap(); let root_2 = forest.get_storage_map_root(account_id, &slot_name, block_2).unwrap(); // Block 3: Update key1 -> value3 let block_3 = block_2.child(); - let mut map_delta_3 = StorageMapDelta::default(); + let mut map_delta_3 = StorageMapPatch::default(); map_delta_3.insert(key1, value3); - let raw_3 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_3))]); - let storage_delta_3 = AccountStorageDelta::from_raw(raw_3); + let raw_3 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_3))]); + let storage_delta_3 = AccountStoragePatch::from_raw(raw_3); let delta_3 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_3); forest.update_account(block_3, &delta_3).unwrap(); let root_3 = forest.get_storage_map_root(account_id, &slot_name, block_3).unwrap(); @@ -481,7 +481,7 @@ fn storage_map_incremental_updates() { fn test_storage_map_removals() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; const SLOT_INDEX: usize = 3; const VALUE_1: [u32; 4] = [10, 0, 0, 0]; @@ -496,18 +496,18 @@ fn test_storage_map_removals() { let value_2 = Word::from(VALUE_2); let block_1 = BlockNumber::GENESIS.child(); - let mut map_delta_1 = StorageMapDelta::default(); + let mut map_delta_1 = StorageMapPatch::default(); map_delta_1.insert(key_1, value_1); map_delta_1.insert(key_2, value_2); - let raw_1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_1))]); - let storage_delta_1 = AccountStorageDelta::from_raw(raw_1); + let raw_1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_1))]); + let storage_delta_1 = AccountStoragePatch::from_raw(raw_1); let delta_1 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_1); forest.update_account(block_1, &delta_1).unwrap(); let block_2 = block_1.child(); - let map_delta_2 = StorageMapDelta::from_iters([key_1], []); - let raw_2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_2))]); - let storage_delta_2 = AccountStorageDelta::from_raw(raw_2); + let map_delta_2 = StorageMapPatch::from_iters([key_1], []); + let raw_2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_2))]); + let storage_delta_2 = AccountStoragePatch::from_raw(raw_2); let delta_2 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_2); forest.update_account(block_2, &delta_2).unwrap(); @@ -527,7 +527,7 @@ fn test_storage_map_removals() { fn storage_map_state_is_not_available_for_block_gaps() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; const BLOCK_FIRST: u32 = 1; const BLOCK_SECOND: u32 = 4; @@ -543,20 +543,20 @@ fn storage_map_state_is_not_available_for_block_gaps() { let raw_key = StorageMapKey::from_index(KEY_VALUE); let block_1 = BlockNumber::from(BLOCK_FIRST); - let mut map_delta_1 = StorageMapDelta::default(); + let mut map_delta_1 = StorageMapPatch::default(); let value_1 = Word::from([VALUE_FIRST, 0, 0, 0]); map_delta_1.insert(raw_key, value_1); - let raw_1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_1))]); - let storage_delta_1 = AccountStorageDelta::from_raw(raw_1); + let raw_1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_1))]); + let storage_delta_1 = AccountStoragePatch::from_raw(raw_1); let delta_1 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_1); forest.update_account(block_1, &delta_1).unwrap(); let block_4 = BlockNumber::from(BLOCK_SECOND); - let mut map_delta_4 = StorageMapDelta::default(); + let mut map_delta_4 = StorageMapPatch::default(); let value_2 = Word::from([VALUE_SECOND, 0, 0, 0]); map_delta_4.insert(raw_key, value_2); - let raw_4 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_4))]); - let storage_delta_4 = AccountStorageDelta::from_raw(raw_4); + let raw_4 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_4))]); + let storage_delta_4 = AccountStoragePatch::from_raw(raw_4); let delta_4 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_4); forest.update_account(block_4, &delta_4).unwrap(); @@ -629,21 +629,21 @@ fn storage_map_open_returns_proofs() { use std::collections::BTreeMap; use assert_matches::assert_matches; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); let slot_name = StorageSlotName::mock(3); let block_num = BlockNumber::GENESIS.child(); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); for i in 0..20u32 { let key = StorageMapKey::from_index(i); let value = Word::from([0, 0, 0, i]); map_delta.insert(key, value); } - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta); forest.update_account(block_num, &delta).unwrap(); @@ -661,7 +661,7 @@ fn storage_map_open_returns_proofs() { fn storage_map_all_entries_returns_raw_keys_after_update() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -670,10 +670,10 @@ fn storage_map_all_entries_returns_raw_keys_after_update() { let raw_key = StorageMapKey::from_index(42); let value = Word::from([42u32, 0, 0, 0]); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(raw_key, value); - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta); forest.update_account(block_num, &delta).unwrap(); @@ -694,7 +694,7 @@ fn storage_map_all_entries_returns_raw_keys_after_update() { fn storage_map_all_entries_returns_cache_miss_when_raw_key_is_not_cached() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -703,10 +703,10 @@ fn storage_map_all_entries_returns_cache_miss_when_raw_key_is_not_cached() { let raw_key = StorageMapKey::from_index(43); let value = Word::from([43u32, 0, 0, 0]); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(raw_key, value); - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta); forest.update_account(block_num, &delta).unwrap(); @@ -737,7 +737,7 @@ fn storage_map_all_entries_returns_cache_miss_when_raw_key_is_not_cached() { fn storage_map_key_hashing_and_raw_entries_are_consistent() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; const SLOT_INDEX: usize = 4; const KEY_VALUE: u32 = 11; @@ -750,10 +750,10 @@ fn storage_map_key_hashing_and_raw_entries_are_consistent() { let raw_key = StorageMapKey::from_index(KEY_VALUE); let value = Word::from([VALUE_VALUE, 0, 0, 0]); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(raw_key, value); - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta); forest.update_account(block_num, &delta).unwrap(); @@ -789,7 +789,7 @@ fn prune_handles_empty_forest() { #[test] fn prune_removes_smt_roots_from_forest() { - use miden_protocol::account::delta::StorageMapDelta; + use miden_protocol::account::delta::StorageMapPatch; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -804,15 +804,15 @@ fn prune_removes_smt_roots_from_forest() { .add_asset(dummy_fungible_asset(faucet_id, (i * TEST_AMOUNT_MULTIPLIER).into())) .unwrap(); let storage_delta = if i.is_multiple_of(3) { - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert( StorageMapKey::new(Word::from([1u32, 0, 0, 0])), Word::from([99u32, i, i * i, i * i * i]), ); - let asd = AccountStorageDelta::new(); + let asd = AccountStoragePatch::new(); asd.add_updated_maps([(slot_name.clone(), map_delta)]) } else { - AccountStorageDelta::default() + AccountStoragePatch::default() }; let delta = dummy_partial_delta(account_id, vault_delta, storage_delta); @@ -852,7 +852,7 @@ fn prune_respects_retention_boundary() { vault_delta .add_asset(dummy_fungible_asset(faucet_id, (i * TEST_AMOUNT_MULTIPLIER).into())) .unwrap(); - let delta = dummy_partial_delta(account_id, vault_delta, AccountStorageDelta::default()); + let delta = dummy_partial_delta(account_id, vault_delta, AccountStoragePatch::default()); forest.update_account(block_num, &delta).unwrap(); } @@ -864,7 +864,7 @@ fn prune_respects_retention_boundary() { #[test] fn prune_roots_removes_old_entries() { - use miden_protocol::account::delta::StorageMapDelta; + use miden_protocol::account::delta::StorageMapPatch; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -880,10 +880,10 @@ fn prune_roots_removes_old_entries() { let key = StorageMapKey::new(Word::from([i, i * i, 5, 4])); let value = Word::from([0, 0, i * i * i, 77]); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(key, value); let storage_delta = - AccountStorageDelta::new().add_updated_maps([(slot_name.clone(), map_delta)]); + AccountStoragePatch::new().add_updated_maps([(slot_name.clone(), map_delta)]); let delta = dummy_partial_delta(account_id, vault_delta, storage_delta); forest.update_account(block_num, &delta).unwrap(); @@ -911,12 +911,12 @@ fn prune_handles_multiple_accounts() { let mut vault_delta1 = AccountVaultDelta::default(); vault_delta1.add_asset(dummy_fungible_asset(faucet_id, amount)).unwrap(); - let delta1 = dummy_partial_delta(account1, vault_delta1, AccountStorageDelta::default()); + let delta1 = dummy_partial_delta(account1, vault_delta1, AccountStoragePatch::default()); forest.update_account(block_num, &delta1).unwrap(); let mut vault_delta2 = AccountVaultDelta::default(); vault_delta2.add_asset(dummy_fungible_asset(account2, amount * 2)).unwrap(); - let delta2 = dummy_partial_delta(account2, vault_delta2, AccountStorageDelta::default()); + let delta2 = dummy_partial_delta(account2, vault_delta2, AccountStoragePatch::default()); forest.update_account(block_num, &delta2).unwrap(); } @@ -935,7 +935,7 @@ fn prune_handles_multiple_accounts() { fn prune_handles_multiple_slots() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -944,15 +944,15 @@ fn prune_handles_multiple_slots() { for i in 1..=TEST_CHAIN_LENGTH { let block_num = BlockNumber::from(i); - let mut map_delta_a = StorageMapDelta::default(); + let mut map_delta_a = StorageMapPatch::default(); map_delta_a.insert(StorageMapKey::new(Word::from([i, 0, 0, 0])), Word::from([i, 0, 0, 1])); - let mut map_delta_b = StorageMapDelta::default(); + let mut map_delta_b = StorageMapPatch::default(); map_delta_b.insert(StorageMapKey::new(Word::from([i, 0, 0, 2])), Word::from([i, 0, 0, 3])); let raw = BTreeMap::from_iter([ - (slot_a.clone(), StorageSlotDelta::Map(map_delta_a)), - (slot_b.clone(), StorageSlotDelta::Map(map_delta_b)), + (slot_a.clone(), StorageSlotPatch::Map(map_delta_a)), + (slot_b.clone(), StorageSlotPatch::Map(map_delta_b)), ]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta); forest.update_account(block_num, &delta).unwrap(); } @@ -971,7 +971,7 @@ fn prune_handles_multiple_slots() { fn prune_preserves_most_recent_state_per_entity() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -984,31 +984,31 @@ fn prune_preserves_most_recent_state_per_entity() { let mut vault_delta_1 = AccountVaultDelta::default(); vault_delta_1.add_asset(dummy_fungible_asset(faucet_id, 1000)).unwrap(); - let mut map_delta_a = StorageMapDelta::default(); + let mut map_delta_a = StorageMapPatch::default(); map_delta_a .insert(StorageMapKey::new(Word::from([1u32, 0, 0, 0])), Word::from([100u32, 0, 0, 0])); - let mut map_delta_b = StorageMapDelta::default(); + let mut map_delta_b = StorageMapPatch::default(); map_delta_b .insert(StorageMapKey::new(Word::from([2u32, 0, 0, 0])), Word::from([200u32, 0, 0, 0])); let raw = BTreeMap::from_iter([ - (slot_map_a.clone(), StorageSlotDelta::Map(map_delta_a)), - (slot_map_b.clone(), StorageSlotDelta::Map(map_delta_b)), + (slot_map_a.clone(), StorageSlotPatch::Map(map_delta_a)), + (slot_map_b.clone(), StorageSlotPatch::Map(map_delta_b)), ]); - let storage_delta_1 = AccountStorageDelta::from_raw(raw); + let storage_delta_1 = AccountStoragePatch::from_raw(raw); let delta_1 = dummy_partial_delta(account_id, vault_delta_1, storage_delta_1); forest.update_account(block_1, &delta_1).unwrap(); // Block 51: Update only map_a let block_at_51 = BlockNumber::from(51); - let mut map_delta_a_new = StorageMapDelta::default(); + let mut map_delta_a_new = StorageMapPatch::default(); map_delta_a_new .insert(StorageMapKey::new(Word::from([1u32, 0, 0, 0])), Word::from([999u32, 0, 0, 0])); let raw_at_51 = - BTreeMap::from_iter([(slot_map_a.clone(), StorageSlotDelta::Map(map_delta_a_new))]); - let storage_delta_at_51 = AccountStorageDelta::from_raw(raw_at_51); + BTreeMap::from_iter([(slot_map_a.clone(), StorageSlotPatch::Map(map_delta_a_new))]); + let storage_delta_at_51 = AccountStoragePatch::from_raw(raw_at_51); let delta_at_51 = dummy_partial_delta(account_id, AccountVaultDelta::default(), storage_delta_at_51); forest.update_account(block_at_51, &delta_at_51).unwrap(); @@ -1028,7 +1028,7 @@ fn prune_preserves_most_recent_state_per_entity() { fn prune_preserves_entries_within_retention_window() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; let mut forest = AccountStateForest::new(); let account_id = dummy_account(); @@ -1045,12 +1045,12 @@ fn prune_preserves_entries_within_retention_window() { .add_asset(dummy_fungible_asset(faucet_id, u64::from(block_num) * 100)) .unwrap(); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta .insert(StorageMapKey::from_index(block_num), Word::from([block_num * 10, 0, 0, 0])); - let raw = BTreeMap::from_iter([(slot_map.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_map.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta = dummy_partial_delta(account_id, vault_delta, storage_delta); forest.update_account(block, &delta).unwrap(); } @@ -1087,14 +1087,14 @@ fn shared_vault_root_retained_when_one_account_changes() { let mut vault_delta_1 = AccountVaultDelta::default(); vault_delta_1.add_asset(asset).unwrap(); - let delta_1 = dummy_partial_delta(account1, vault_delta_1, AccountStorageDelta::default()); + let delta_1 = dummy_partial_delta(account1, vault_delta_1, AccountStoragePatch::default()); forest.update_account(block_1, &delta_1).unwrap(); let mut vault_delta_2 = AccountVaultDelta::default(); vault_delta_2 .add_asset(dummy_fungible_asset(faucet_id, initial_amount)) .unwrap(); - let delta_2 = dummy_partial_delta(account2, vault_delta_2, AccountStorageDelta::default()); + let delta_2 = dummy_partial_delta(account2, vault_delta_2, AccountStoragePatch::default()); forest.update_account(block_1, &delta_2).unwrap(); // Both accounts should have the same vault root (structural sharing in SmtForest) @@ -1107,7 +1107,7 @@ fn shared_vault_root_retained_when_one_account_changes() { let mut vault_delta_2_update = AccountVaultDelta::default(); vault_delta_2_update.add_asset(dummy_fungible_asset(faucet_id, 500)).unwrap(); let delta_2_update = - dummy_partial_delta(account2, vault_delta_2_update, AccountStorageDelta::default()); + dummy_partial_delta(account2, vault_delta_2_update, AccountStoragePatch::default()); forest.update_account(block_2, &delta_2_update).unwrap(); // Account2 now has a different root diff --git a/crates/store/src/db/models/queries/accounts/delta.rs b/crates/store/src/db/models/queries/accounts/delta.rs index 56b70fbfc..403531621 100644 --- a/crates/store/src/db/models/queries/accounts/delta.rs +++ b/crates/store/src/db/models/queries/accounts/delta.rs @@ -12,7 +12,7 @@ use std::collections::{BTreeMap, HashMap}; use diesel::query_dsl::methods::SelectDsl; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SqliteConnection}; -use miden_protocol::account::delta::AccountStorageDelta; +use miden_protocol::account::delta::AccountStoragePatch; use miden_protocol::account::{ Account, AccountId, @@ -228,7 +228,7 @@ pub(super) fn select_latest_vault_assets( /// For map slots, uses the precomputed roots for updated maps. pub(super) fn apply_storage_delta( header: &AccountStorageHeader, - delta: &AccountStorageDelta, + delta: &AccountStoragePatch, map_entries: &HashMap>, ) -> Result { let mut value_updates: HashMap<&StorageSlotName, Word> = HashMap::new(); diff --git a/crates/store/src/db/models/queries/accounts/delta/tests.rs b/crates/store/src/db/models/queries/accounts/delta/tests.rs index 4ec675932..30618fc10 100644 --- a/crates/store/src/db/models/queries/accounts/delta/tests.rs +++ b/crates/store/src/db/models/queries/accounts/delta/tests.rs @@ -9,11 +9,11 @@ use miden_node_utils::fee::test_fee_params; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::delta::{ - AccountStorageDelta, + AccountStoragePatch, AccountUpdateDetails, AccountVaultDelta, - StorageMapDelta, - StorageSlotDelta, + StorageMapPatch, + StorageSlotPatch, }; use miden_protocol::account::{ AccountBuilder, @@ -192,9 +192,9 @@ fn optimized_delta_matches_full_account_method() { let storage_delta = { let deltas = BTreeMap::from_iter([( value_slot_name.clone(), - StorageSlotDelta::Value(new_slot_value), + StorageSlotPatch::Value(new_slot_value), )]); - AccountStorageDelta::from_raw(deltas) + AccountStoragePatch::from_raw(deltas) }; // Build the vault delta (add 500 tokens to empty vault) @@ -379,7 +379,7 @@ fn optimized_delta_updates_non_empty_vault() { let partial_delta = AccountDelta::new( account.id(), - AccountStorageDelta::new(), + AccountStoragePatch::new(), vault_delta, Felt::new_unchecked(NONCE_DELTA), ) @@ -420,7 +420,7 @@ fn optimized_delta_updates_non_empty_vault() { let partial_delta_3 = AccountDelta::new( account.id(), - AccountStorageDelta::new(), + AccountStoragePatch::new(), vault_delta_3, Felt::new_unchecked(NONCE_DELTA), ) @@ -530,11 +530,11 @@ fn optimized_delta_updates_storage_map_header() { let full_account_before = select_full_account(&mut conn, account.id()).expect("Failed to load full account"); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(map_key, map_value_updated); - let storage_delta = AccountStorageDelta::from_raw(BTreeMap::from_iter([( + let storage_delta = AccountStoragePatch::from_raw(BTreeMap::from_iter([( StorageSlotName::mock(SLOT_INDEX_MAP), - StorageSlotDelta::Map(map_delta), + StorageSlotPatch::Map(map_delta), )])); let partial_delta = AccountDelta::new( diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index de74a8756..a95d9a4e2 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -16,16 +16,16 @@ use miden_protocol::account::{ AccountId, AccountIdVersion, AccountStorage, - AccountStorageDelta, + AccountStoragePatch, AccountStorageHeader, AccountType, AccountVaultDelta, StorageMap, - StorageMapDelta, + StorageMapPatch, StorageMapKey, StorageSlot, StorageSlotContent, - StorageSlotDelta, + StorageSlotPatch, StorageSlotName, StorageSlotType, }; @@ -863,12 +863,12 @@ fn test_select_latest_account_storage_slot_updates() { upsert_accounts(&mut conn, &[account_update], block_1).expect("upsert_accounts failed"); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(key_1, value_2); map_delta.insert(key_2, value_3); - let storage_delta = AccountStorageDelta::from_raw(BTreeMap::from_iter([( + let storage_delta = AccountStoragePatch::from_raw(BTreeMap::from_iter([( slot_name.clone(), - StorageSlotDelta::Map(map_delta), + StorageSlotPatch::Map(map_delta), )])); let partial_delta = AccountDelta::new( diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 468e38b64..c75e442db 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -16,13 +16,13 @@ use miden_protocol::account::{ AccountDelta, AccountId, AccountIdVersion, - AccountStorageDelta, + AccountStoragePatch, AccountType, AccountVaultDelta, StorageMapKey, StorageSlot, StorageSlotContent, - StorageSlotDelta, + StorageSlotPatch, StorageSlotName, }; use miden_protocol::asset::{Asset, FungibleAsset}; @@ -1112,7 +1112,7 @@ fn insert_account_delta( fn sql_account_storage_map_values_insertion() { use std::collections::BTreeMap; - use miden_protocol::account::StorageMapDelta; + use miden_protocol::account::StorageMapPatch; let mut conn = create_db(); let conn = &mut conn; @@ -1136,11 +1136,11 @@ fn sql_account_storage_map_values_insertion() { let value3 = Word::from([30u32, 31, 32, 33]); // Insert at block 1 - let mut map1 = StorageMapDelta::default(); + let mut map1 = StorageMapPatch::default(); map1.insert(key1, value1); map1.insert(key2, value2); - let delta1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map1))]); - let storage1 = AccountStorageDelta::from_raw(delta1); + let delta1 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map1))]); + let storage1 = AccountStoragePatch::from_raw(delta1); let delta1 = AccountDelta::new(account_id, storage1, AccountVaultDelta::default(), Felt::ONE).unwrap(); insert_account_delta(conn, account_id, block1, &delta1); @@ -1155,10 +1155,10 @@ fn sql_account_storage_map_values_insertion() { assert_eq!(storage_map_page.values.len(), 2, "expect 2 initial rows"); // Update key1 at block 2 - let mut map2 = StorageMapDelta::default(); + let mut map2 = StorageMapPatch::default(); map2.insert(key1, value3); - let delta2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map2))]); - let storage2 = AccountStorageDelta::from_raw(delta2); + let delta2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map2))]); + let storage2 = AccountStoragePatch::from_raw(delta2); let delta2 = AccountDelta::new( account_id, storage2, @@ -3047,7 +3047,7 @@ fn test_prune_history() { fn account_state_forest_matches_db_storage_map_roots_across_updates() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; use miden_protocol::crypto::merkle::smt::Smt; use crate::account_state_forest::AccountStateForest; @@ -3135,15 +3135,15 @@ fn account_state_forest_matches_db_storage_map_roots_across_updates() { let value3 = num_to_word(3000); // Block 1: Add storage map entries and a storage value - let mut map_delta_1 = StorageMapDelta::default(); + let mut map_delta_1 = StorageMapPatch::default(); map_delta_1.insert(key1, value1); map_delta_1.insert(key2, value2); let raw_1 = BTreeMap::from_iter([ - (slot_map.clone(), StorageSlotDelta::Map(map_delta_1)), - (slot_value.clone(), StorageSlotDelta::Value(value1)), + (slot_map.clone(), StorageSlotPatch::Map(map_delta_1)), + (slot_value.clone(), StorageSlotPatch::Value(value1)), ]); - let storage_1 = AccountStorageDelta::from_raw(raw_1); + let storage_1 = AccountStoragePatch::from_raw(raw_1); let delta_1 = AccountDelta::new(account_id, storage_1.clone(), AccountVaultDelta::default(), Felt::ONE) .unwrap(); @@ -3162,14 +3162,14 @@ fn account_state_forest_matches_db_storage_map_roots_across_updates() { ); // Block 2: Delete storage map entry (set to EMPTY_WORD) and delete storage value - let mut map_delta_2 = StorageMapDelta::default(); + let mut map_delta_2 = StorageMapPatch::default(); map_delta_2.insert(key1, EMPTY_WORD); let raw_2 = BTreeMap::from_iter([ - (slot_map.clone(), StorageSlotDelta::Map(map_delta_2)), - (slot_value.clone(), StorageSlotDelta::Value(EMPTY_WORD)), + (slot_map.clone(), StorageSlotPatch::Map(map_delta_2)), + (slot_value.clone(), StorageSlotPatch::Value(EMPTY_WORD)), ]); - let storage_2 = AccountStorageDelta::from_raw(raw_2); + let storage_2 = AccountStoragePatch::from_raw(raw_2); let delta_2 = AccountDelta::new( account_id, storage_2.clone(), @@ -3192,14 +3192,14 @@ fn account_state_forest_matches_db_storage_map_roots_across_updates() { ); // Block 3: Re-add same value as block 1 and add different map entry - let mut map_delta_3 = StorageMapDelta::default(); + let mut map_delta_3 = StorageMapPatch::default(); map_delta_3.insert(key2, value3); // Update existing key let raw_3 = BTreeMap::from_iter([ - (slot_map.clone(), StorageSlotDelta::Map(map_delta_3)), - (slot_value.clone(), StorageSlotDelta::Value(value1)), // Same as block 1 + (slot_map.clone(), StorageSlotPatch::Map(map_delta_3)), + (slot_value.clone(), StorageSlotPatch::Value(value1)), // Same as block 1 ]); - let storage_3 = AccountStorageDelta::from_raw(raw_3); + let storage_3 = AccountStoragePatch::from_raw(raw_3); let delta_3 = AccountDelta::new( account_id, storage_3.clone(), @@ -3241,7 +3241,7 @@ fn account_state_forest_matches_db_storage_map_roots_across_updates() { fn account_state_forest_shared_roots_not_deleted_prematurely() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, @@ -3268,13 +3268,13 @@ fn account_state_forest_shared_roots_not_deleted_prematurely() { let value2 = num_to_word(2000); // All three accounts add identical storage maps at block 1 - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(key1, value1); map_delta.insert(key2, value2); // Setups a single slot with a map and two key-value-pairs - let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta.clone()))]); - let storage = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta.clone()))]); + let storage = AccountStoragePatch::from_raw(raw); // Account 1 let delta1 = @@ -3328,11 +3328,11 @@ fn account_state_forest_shared_roots_not_deleted_prematurely() { assert_eq!(total_roots_removed, 0); // Update accounts 1,2,3 - let mut map_delta_update = StorageMapDelta::default(); + let mut map_delta_update = StorageMapPatch::default(); map_delta_update.insert(key1, num_to_word(1001)); // Slight change let raw_update = - BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map_delta_update))]); - let storage_update = AccountStorageDelta::from_raw(raw_update); + BTreeMap::from_iter([(slot_name.clone(), StorageSlotPatch::Map(map_delta_update))]); + let storage_update = AccountStoragePatch::from_raw(raw_update); let delta2_update = AccountDelta::new( account2, storage_update.clone(), @@ -3402,7 +3402,7 @@ fn account_state_forest_shared_roots_not_deleted_prematurely() { fn account_state_forest_retains_latest_after_100_blocks_and_pruning() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; use crate::account_state_forest::{AccountStateForest, HISTORICAL_BLOCK_RETENTION}; @@ -3421,12 +3421,12 @@ fn account_state_forest_retains_latest_after_100_blocks_and_pruning() { let block_1 = BlockNumber::from(1); // Create storage map with two entries - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(key1, value1); map_delta.insert(key2, value2); - let raw = BTreeMap::from_iter([(slot_map.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_map.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); // Create vault with one asset let asset = FungibleAsset::new(faucet_id, 100).unwrap(); @@ -3474,11 +3474,11 @@ fn account_state_forest_retains_latest_after_100_blocks_and_pruning() { // Update with new values let value1_new = num_to_word(3000); - let mut map_delta_51 = StorageMapDelta::default(); + let mut map_delta_51 = StorageMapPatch::default(); map_delta_51.insert(key1, value1_new); - let raw_51 = BTreeMap::from_iter([(slot_map.clone(), StorageSlotDelta::Map(map_delta_51))]); - let storage_delta_51 = AccountStorageDelta::from_raw(raw_51); + let raw_51 = BTreeMap::from_iter([(slot_map.clone(), StorageSlotPatch::Map(map_delta_51))]); + let storage_delta_51 = AccountStoragePatch::from_raw(raw_51); let asset_51 = FungibleAsset::new(faucet_id, 200).unwrap(); let mut vault_delta_51 = AccountVaultDelta::default(); @@ -3535,7 +3535,7 @@ fn account_state_forest_preserves_most_recent_vault_only() { vault_delta.add_asset(asset.into()).unwrap(); let delta_1 = - AccountDelta::new(account_id, AccountStorageDelta::default(), vault_delta, Felt::ONE) + AccountDelta::new(account_id, AccountStoragePatch::default(), vault_delta, Felt::ONE) .unwrap(); forest.update_account(block_1, &delta_1).unwrap(); @@ -3703,7 +3703,7 @@ fn db_roundtrip_transactions_filters_missing_output_note_sync_records() { fn account_state_forest_preserves_most_recent_storage_map_only() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; use crate::account_state_forest::AccountStateForest; @@ -3716,11 +3716,11 @@ fn account_state_forest_preserves_most_recent_storage_map_only() { // Block 1: Create storage map let block_1 = BlockNumber::from(1); - let mut map_delta = StorageMapDelta::default(); + let mut map_delta = StorageMapPatch::default(); map_delta.insert(key1, value1); - let raw = BTreeMap::from_iter([(slot_map.clone(), StorageSlotDelta::Map(map_delta))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_map.clone(), StorageSlotPatch::Map(map_delta))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta_1 = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), Felt::ONE) @@ -3765,7 +3765,7 @@ fn account_state_forest_preserves_most_recent_storage_map_only() { fn account_state_forest_preserves_most_recent_storage_value_slot() { use std::collections::BTreeMap; - use miden_protocol::account::delta::StorageSlotDelta; + use miden_protocol::account::delta::StorageSlotPatch; use crate::account_state_forest::AccountStateForest; @@ -3778,8 +3778,8 @@ fn account_state_forest_preserves_most_recent_storage_value_slot() { // Block 1: Create storage value slot let block_1 = BlockNumber::from(1); - let raw = BTreeMap::from_iter([(slot_value.clone(), StorageSlotDelta::Value(value1))]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let raw = BTreeMap::from_iter([(slot_value.clone(), StorageSlotPatch::Value(value1))]); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta_1 = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), Felt::ONE) @@ -3816,7 +3816,7 @@ fn account_state_forest_preserves_most_recent_storage_value_slot() { fn account_state_forest_preserves_mixed_slots_independently() { use std::collections::BTreeMap; - use miden_protocol::account::delta::{StorageMapDelta, StorageSlotDelta}; + use miden_protocol::account::delta::{StorageMapPatch, StorageSlotPatch}; use crate::account_state_forest::AccountStateForest; @@ -3839,18 +3839,18 @@ fn account_state_forest_preserves_mixed_slots_independently() { let mut vault_delta = AccountVaultDelta::default(); vault_delta.add_asset(asset.into()).unwrap(); - let mut map_delta_a = StorageMapDelta::default(); + let mut map_delta_a = StorageMapPatch::default(); map_delta_a.insert(key1, value1); - let mut map_delta_b = StorageMapDelta::default(); + let mut map_delta_b = StorageMapPatch::default(); map_delta_b.insert(key1, value1); let raw = BTreeMap::from_iter([ - (slot_map_a.clone(), StorageSlotDelta::Map(map_delta_a)), - (slot_map_b.clone(), StorageSlotDelta::Map(map_delta_b)), - (slot_value.clone(), StorageSlotDelta::Value(value_slot_data)), + (slot_map_a.clone(), StorageSlotPatch::Map(map_delta_a)), + (slot_map_b.clone(), StorageSlotPatch::Map(map_delta_b)), + (slot_value.clone(), StorageSlotPatch::Value(value_slot_data)), ]); - let storage_delta = AccountStorageDelta::from_raw(raw); + let storage_delta = AccountStoragePatch::from_raw(raw); let delta_1 = AccountDelta::new(account_id, storage_delta, vault_delta, Felt::ONE).unwrap(); @@ -3864,12 +3864,12 @@ fn account_state_forest_preserves_mixed_slots_independently() { let block_51 = BlockNumber::from(51); let value2 = num_to_word(2000); - let mut map_delta_a_update = StorageMapDelta::default(); + let mut map_delta_a_update = StorageMapPatch::default(); map_delta_a_update.insert(key1, value2); let raw_51 = - BTreeMap::from_iter([(slot_map_a.clone(), StorageSlotDelta::Map(map_delta_a_update))]); - let storage_delta_51 = AccountStorageDelta::from_raw(raw_51); + BTreeMap::from_iter([(slot_map_a.clone(), StorageSlotPatch::Map(map_delta_a_update))]); + let storage_delta_51 = AccountStoragePatch::from_raw(raw_51); let delta_51 = AccountDelta::new( account_id, diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index 59556fcdc..7db2c3a23 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -13,7 +13,7 @@ use miden_protocol::account::{ AccountDelta, AccountFile, AccountId, - AccountStorageDelta, + AccountStoragePatch, AccountType, AccountVaultDelta, FungibleAssetDelta, @@ -246,7 +246,7 @@ impl GenesisConfig { // therefore we need bump the nonce manually to uphold this invariant. let wallet_delta = AccountDelta::new( wallet_account.id(), - AccountStorageDelta::default(), + AccountStoragePatch::default(), AccountVaultDelta::new( wallet_fungible_asset_update, NonFungibleAssetDelta::default(), @@ -275,7 +275,7 @@ impl GenesisConfig { // `ONE`. let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default(); - let mut storage_delta = AccountStorageDelta::default(); + let mut storage_delta = AccountStoragePatch::default(); if total_issuance != 0 { let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?; diff --git a/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac b/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac index 7017fd1eff1691a140bfb5f18c14589824fa318b..6879144f0453312149c5484e60e9fe6b6fb6b760 100644 GIT binary patch delta 689 zcmaF9oN4C@rVS67CMz;KDC}JN?_H_rftg-x-@ZQG{Y-pz+v^}rVYZGroR;r1A6`wK z?8tnUv2QXzi#Q|4W_1=9@A@uA28J3228K8W1_uy5#R^C;Fc^GhWM}~KPeA!!85tb> zfc$Sj^|e6yJ5atFNZSDUK;ZBQs_qw1T^&$B)*38s@CPU#24wzaWHt8h1s#&)|7pc+}rW=-K&B( zugrR{t@!4B$WnDr+GOiK1%+GetDdhBSUF#c?>%qk(U#4<4_%yl98Q?|e3gG17$&)6 za&@1B!}Muoyz{nh^L?TFV^@)}z345?NrKwJeBnQzDt0c|HRIBriu--h=NGnqDP;38`}eG`CG6`bS5HvLV%aV5Q~iJJ6p4Rw|5CSI@owHy)W)#6 zj_G-`#hdip*B|&7J#LFwyJglPsS^{-^`5xPFt*?B5XxX!72Fx^*zqDsZvS%LX8t?T zPu_X+l}bKclV=_9okPN??S$@w@Z%{=5{yuw#j#m%A~(}yg^7HVWhN^s5Yjg}c5)X0 DNh|^k delta 612 zcmdnFg6a8krVS676b_m#H?qDg`TMDDN|@uPQ|u=)Gg}YMIzP?heEL!NCv6|H8=7022QS zq^p6n4Nx5rI6Q)?`@zU?pcW_~YYi4R_yv>?12TUD_16LE5UBWHAU_bu{|A(>2h#t6 z<^lC-F)%POFjO%x2rx1+G*kj5`k)$^f&2y_pM{CR0VK}4StUT-ZgO{@g2LZdQHp)3lTY_KIMjYJyY+KwncsxxXI9=<7MPyV zYrNaNSFCJSLRTm!`>yY`^DiEq<}hJiAFtr8`-x1-r@Pp9J!7(LahYVDxY_5@rOn>` z3XBPD+0mEZE;&8(ObzSDz?Ca9jTTF9w2^CD6EdMc?BtXQ+|OLDEj78b;-h}JXM)(X zb>U^t`rXy$MoU&INqv4{XI;0-e(A3h*QcE|3g%h6uJ_Ia@wBd)(v!b^`t!qt?N0ag zxsy*%P{?X8o+S6<^UY_U4@7t>zYe)0sL*M0WmSwNM~TVP8ygvKt4#j+mY3aVQb3-~ zip|`jGHcd#3-AW-QL%o#ETc){=Pmu8hRY|kPLq8E@zX@%oYj$_JRWjtKqO i`bhQ>BNQlcY)+iW%QU%QBG=@+$%+bu^i5trxeEZXHti+= diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 1daa3cf30..b0b8ea28a 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -156,11 +156,6 @@ pub struct State { /// Forest-related state `(SmtForest, storage_map_roots, vault_roots)` with its own lock. forest: Arc>>, - /// Request termination of the process due to a fatal internal state error. - /// Stored to keep the channel alive alongside the BlockWriter's copy of the sender. - #[allow(dead_code)] - termination_ask: tokio::sync::mpsc::Sender, - /// The latest proven-in-sequence block number, updated by the proof scheduler or `apply_proof`. proven_tip: ProvenTipWriter, @@ -311,7 +306,6 @@ impl State { in_memory, write_handle, forest, - termination_ask, proven_tip: proven_tip.clone(), committed_tip_tx, block_cache, From a15670a76f6c31046733cb2baeae30d8a9f0ed48 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 11:06:06 +1200 Subject: [PATCH 03/11] Refactor snapshot call in fetch pub accounts --- crates/store/src/state/account.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/store/src/state/account.rs b/crates/store/src/state/account.rs index 596688c7e..bdce7024e 100644 --- a/crates/store/src/state/account.rs +++ b/crates/store/src/state/account.rs @@ -189,11 +189,8 @@ impl State { } // Validate block exists in the blockchain before querying the database - { - let snapshot = self.snapshot(); - if block_num > snapshot.block_num { - return Err(GetAccountError::UnknownBlock(block_num)); - } + if block_num > self.snapshot().block_num { + return Err(GetAccountError::UnknownBlock(block_num)); } // Query account header and storage header together in a single DB call From a0d19153a352dbfa0a889ee0bfd9c0d5c6d73fcc Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 13:35:24 +1200 Subject: [PATCH 04/11] Fix test shutdown panic --- crates/rpc/src/tests.rs | 59 +++++++++++++++++++++++--------- crates/store/src/state/mod.rs | 41 +++++++++++++++------- crates/store/src/state/writer.rs | 40 ++++++++++++++++------ 3 files changed, 100 insertions(+), 40 deletions(-) diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 7bebc604b..e47e79a99 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -87,11 +87,34 @@ impl TestStore { genesis_commitment } + + /// Shuts down the [`BlockWriter`] task before releasing the temp directory. + /// + /// Call this at the end of any test that uses RocksDB-backed storage. Without an explicit + /// shutdown the task is cancelled by the runtime *after* the temp directory is deleted, + /// causing a panic in the `RocksDB` destructor. + /// + /// # Panics + /// + /// Panics if `Self::state` has more than one reference. + async fn shutdown(self) { + let Self { + state, + genesis_commitment: _, + data_directory, + } = self; + + Arc::try_unwrap(state) + .unwrap_or_else(|_| panic!("State should have a single Arc reference at shutdown")) + .shutdown() + .await; + + drop(data_directory); + } } async fn load_state(path: &std::path::Path) -> Arc { - let state = State::load(path, StorageOptions::default()).await.expect("state should load"); - Arc::new(state) + Arc::new(State::load(path, StorageOptions::default()).await.expect("state should load")) } /// Byte offset of the account delta commitment in serialized `ProvenTransaction`. Layout: @@ -428,21 +451,25 @@ async fn rpc_rejects_post_deployment_network_account_tx() { transaction_inputs: None, }; - let service = RpcService::new( - Arc::clone(&store.state), - RpcMode::full_node(source_rpc_client()), - None, - NonZeroUsize::new(1_000_000).unwrap(), - None, - ); + { + let service = RpcService::new( + Arc::clone(&store.state), + RpcMode::full_node(source_rpc_client()), + None, + NonZeroUsize::new(1_000_000).unwrap(), + None, + ); - let response = service.submit_proven_tx(Request::new(request)).await; - assert!(response.is_err()); - let err = response.as_ref().unwrap_err().message(); - assert!( - err.contains("Network transactions may not be submitted by users yet"), - "expected the network-tx gate error, got: {err}" - ); + let response = service.submit_proven_tx(Request::new(request)).await; + assert!(response.is_err()); + let err = response.as_ref().unwrap_err().message(); + assert!( + err.contains("Network transactions may not be submitted by users yet"), + "expected the network-tx gate error, got: {err}" + ); + } // service (and its Arc clone) dropped here, before calling shutdown. + + store.shutdown().await; } fn source_rpc_client() -> RpcClient { diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 909425346..8544fe488 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -24,7 +24,7 @@ use miden_protocol::crypto::merkle::mmr::{MmrPeaks, MmrProof, PartialMmr}; use miden_protocol::crypto::merkle::smt::LargeSmt; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_protocol::transaction::PartialBlockchain; -use tokio::sync::{RwLock, watch}; +use tokio::sync::{RwLock, oneshot, watch}; use tracing::{Span, info, instrument}; use crate::account_state_forest::{AccountStateForest, AccountStateForestBackend, WitnessError}; @@ -165,6 +165,10 @@ pub struct State { /// FIFO cache of recent block proofs for replica subscriptions. pub(crate) proof_cache: ProofCache, + + /// Resolves after the [`BlockWriter`] task has fully stopped and all on-disk resources + /// (including `RocksDB`) have been flushed and released. Consumed by [`Self::shutdown`]. + writer_done: oneshot::Receiver<()>, } impl State { @@ -172,9 +176,6 @@ impl State { // -------------------------------------------------------------------------------------------- /// Loads the state from the data directory. - /// - /// The loaded state owns all store data structures and exposes subscription methods for - /// sequencer and replica tasks. #[instrument(target = COMPONENT, skip_all)] pub async fn load( data_path: &Path, @@ -185,9 +186,6 @@ impl State { } /// Loads the state from the data directory using explicit database options. - /// - /// The loaded state owns all store data structures and exposes subscription methods for - /// sequencer and replica tasks. #[instrument(target = COMPONENT, skip_all)] pub async fn load_with_database_options( data_path: &Path, @@ -273,8 +271,9 @@ impl State { let (write_tx, write_rx) = tokio::sync::mpsc::channel(1); let write_handle = WriteHandle::new(write_tx); - // Channel used by BlockWriter to signal critical errors; receiver is held for future use. - let (termination_ask, _termination_rx) = tokio::sync::mpsc::channel(1); + // writer_done_tx is held by BlockWriter and dropped after its trees are flushed, waking + // writer_done on the caller side. + let (writer_done_tx, writer_done) = oneshot::channel(); // Spawn the BlockWriter task. let block_writer = BlockWriter { @@ -284,11 +283,11 @@ impl State { forest: Arc::clone(&forest), committed_tip_tx: Arc::clone(&committed_tip_tx), block_cache: block_cache.clone(), - termination_ask, rx: write_rx, - nullifier_tree, - account_tree, - blockchain, + nullifier_tree: std::mem::ManuallyDrop::new(nullifier_tree), + account_tree: std::mem::ManuallyDrop::new(account_tree), + blockchain: std::mem::ManuallyDrop::new(blockchain), + writer_done_tx: std::mem::ManuallyDrop::new(writer_done_tx), }; tokio::spawn(block_writer.run()); @@ -303,6 +302,7 @@ impl State { committed_tip_tx, block_cache, proof_cache, + writer_done, }) } @@ -311,6 +311,21 @@ impl State { self.committed_tip_tx.subscribe() } + /// Shuts down the state, waiting for the [`BlockWriter`] task to fully stop. + /// + /// Dropping `State` closes the write channel, which signals [`BlockWriter`] to stop. This + /// method waits for the writer to complete — including any pending `RocksDB` flushes — before + /// returning. Call this before releasing on-disk resources (e.g. removing a data directory). + pub async fn shutdown(self) { + // Drop everything except `writer_done` inside an inner scope so that `write_handle` is + // dropped — closing the write channel — before we await the writer's completion. + let writer_done = { + let Self { writer_done, .. } = self; + writer_done + }; + writer_done.await.ok(); + } + /// Loads serialized block proving inputs from the block store. pub async fn load_proving_inputs( &self, diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index 51ae4bca0..01c6b85ea 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -5,6 +5,7 @@ //! [`InMemoryState`] snapshot via an [`ArcSwap`], making the updated trees immediately visible to //! wait-free readers. +use std::mem::ManuallyDrop; use std::sync::Arc; use arc_swap::ArcSwap; @@ -67,6 +68,12 @@ impl WriteHandle { // ================================================================================================ /// Single-task owner of the mutable trees. Processes [`WriteRequest`]s serially. +/// +/// The RocksDB-backed tree fields are wrapped in [`ManuallyDrop`] so that the [`Drop`] impl can +/// flush them in an explicit order before `writer_done_tx` is dropped. Dropping `writer_done_tx` +/// wakes [`State::shutdown`], which then releases on-disk resources; the flush must complete first +/// or `RocksDB` would write to a deleted directory. Using [`ManuallyDrop`] keeps this invariant +/// independent of field declaration order. pub(super) struct BlockWriter { pub db: Arc, pub block_store: Arc, @@ -74,14 +81,31 @@ pub(super) struct BlockWriter { pub forest: Arc>>, pub committed_tip_tx: Arc>, pub block_cache: BlockCache, - pub termination_ask: tokio::sync::mpsc::Sender, pub rx: mpsc::Receiver, /// The mutable nullifier tree owned by this writer. - pub nullifier_tree: NullifierTree>, + pub nullifier_tree: ManuallyDrop>>, /// The mutable account tree owned by this writer. - pub account_tree: AccountTreeWithHistory, + pub account_tree: ManuallyDrop>, /// The blockchain MMR owned by this writer. - pub blockchain: Blockchain, + pub blockchain: ManuallyDrop, + /// Signals [`State::shutdown`] after the trees above have been flushed. + pub writer_done_tx: ManuallyDrop>, +} + +impl Drop for BlockWriter { + fn drop(&mut self) { + // Drop the trees first so their RocksDB destructors flush to disk, then explicitly + // signal `State::shutdown` that it is safe to release on-disk resources. + // + // SAFETY: each field is dropped exactly once here; the compiler's auto-drop pass + // afterwards treats ManuallyDrop fields as no-ops. + unsafe { + ManuallyDrop::drop(&mut self.nullifier_tree); + ManuallyDrop::drop(&mut self.account_tree); + ManuallyDrop::drop(&mut self.blockchain); + let _ = ManuallyDrop::take(&mut self.writer_done_tx).send(()); + } + } } impl BlockWriter { @@ -155,7 +179,7 @@ impl BlockWriter { .reader() .expect("nullifier tree snapshot creation should not fail"), account_tree: self.account_tree.reader(), - blockchain: self.blockchain.clone(), + blockchain: (*self.blockchain).clone(), // TODO(sergerad): snapshot of blockchain? }); self.in_memory.store(snapshot); @@ -247,9 +271,6 @@ impl BlockWriter { .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?; if nullifier_tree_update.as_mutation_set().root() != header.nullifier_root() { - let _ = self.termination_ask.try_send(ApplyBlockError::InvalidBlockError( - InvalidBlockError::NewBlockInvalidNullifierRoot, - )); return Err(InvalidBlockError::NewBlockInvalidNullifierRoot.into()); } @@ -270,9 +291,6 @@ impl BlockWriter { })?; if account_tree_update.as_mutation_set().root() != header.account_root() { - let _ = self.termination_ask.try_send(ApplyBlockError::InvalidBlockError( - InvalidBlockError::NewBlockInvalidAccountRoot, - )); return Err(InvalidBlockError::NewBlockInvalidAccountRoot.into()); } From f6601601ea0e275bdb141ecbc4fae3b5439d3864 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 14:35:20 +1200 Subject: [PATCH 05/11] Add todos for account state forest rwlock --- crates/store/src/state/mod.rs | 1 + crates/store/src/state/writer.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 8544fe488..a6ef19845 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -150,6 +150,7 @@ pub struct State { /// Handle for sending block-write requests to the [`BlockWriter`] task. write_handle: WriteHandle, + // TODO(sergerad): RM lock and move to in memory state when protocol updated with crypto v0.26.0. /// Forest-related state `(SmtForest, storage_map_roots, vault_roots)` with its own lock. forest: Arc>>, diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index 01c6b85ea..46dce3aa2 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -78,6 +78,7 @@ pub(super) struct BlockWriter { pub db: Arc, pub block_store: Arc, pub in_memory: Arc>, + // TODO(sergerad): RM lock and move to in memory state when protocol updated with crypto v0.26.0. pub forest: Arc>>, pub committed_tip_tx: Arc>, pub block_cache: BlockCache, From 860d744f70244df2a5050eaa8e52de34792e47c4 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 14:58:17 +1200 Subject: [PATCH 06/11] Update snapshot calls --- crates/store/src/state/account.rs | 3 ++- crates/store/src/state/mod.rs | 16 ++++++++-------- crates/store/src/state/sync_state.rs | 1 - crates/store/src/state/writer.rs | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/store/src/state/account.rs b/crates/store/src/state/account.rs index bdce7024e..c9b54c953 100644 --- a/crates/store/src/state/account.rs +++ b/crates/store/src/state/account.rs @@ -30,6 +30,7 @@ use super::State; use crate::COMPONENT; use crate::account_state_forest::AccountStorageMapResult; use crate::errors::{DatabaseError, GetAccountError}; +use crate::state::Finality; impl State { /// Returns an account witness and optionally account details at a specific block. @@ -189,7 +190,7 @@ impl State { } // Validate block exists in the blockchain before querying the database - if block_num > self.snapshot().block_num { + if block_num > self.chain_tip(Finality::Committed) { return Err(GetAccountError::UnknownBlock(block_num)); } diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index a6ef19845..379bc1e3a 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -150,7 +150,8 @@ pub struct State { /// Handle for sending block-write requests to the [`BlockWriter`] task. write_handle: WriteHandle, - // TODO(sergerad): RM lock and move to in memory state when protocol updated with crypto v0.26.0. + // TODO(sergerad): RM lock and move to in memory state when protocol updated with crypto + // v0.26.0. /// Forest-related state `(SmtForest, storage_map_roots, vault_roots)` with its own lock. forest: Arc>>, @@ -394,8 +395,7 @@ impl State { let block_header = self.db.select_block_header_by_block_num(block_num).await?; if let Some(header) = block_header { let mmr_proof = if include_mmr_proof { - let snapshot = self.snapshot(); - let mmr_proof = snapshot.blockchain.open(header.block_num())?; + let mmr_proof = self.snapshot().blockchain.open(header.block_num())?; Some(mmr_proof) } else { None @@ -435,8 +435,8 @@ impl State { .map_err(GetCurrentBlockchainDataError::ErrorRetrievingBlockHeader)? .unwrap(); - let snapshot = self.snapshot(); - let peaks = snapshot + let peaks = self + .snapshot() .blockchain .peaks_at(block_header.block_num()) .map_err(GetCurrentBlockchainDataError::InvalidPeaks)?; @@ -465,8 +465,8 @@ impl State { let mut blocks: BTreeSet = tx_reference_blocks; blocks.extend(note_blocks); - let snapshot = self.snapshot(); let (batch_reference_block, partial_mmr) = { + let snapshot = self.snapshot(); let latest_block_num = snapshot.block_num; let highest_block_num = @@ -573,10 +573,10 @@ impl State { account_ids: &[AccountId], nullifiers: &[Nullifier], ) -> Result { - let snapshot = self.snapshot(); let span = Span::current(); tokio::task::block_in_place(|| { span.in_scope(|| { + let snapshot = self.snapshot(); let latest_block_number = snapshot.block_num; let highest_block_number = blocks.last().copied().unwrap_or(latest_block_number); @@ -621,10 +621,10 @@ impl State { ) -> Result { info!(target: COMPONENT, account_id = %account_id.to_string(), nullifiers = %format_array(nullifiers)); - let snapshot = self.snapshot(); let span = Span::current(); let tree_inputs = tokio::task::block_in_place(|| { span.in_scope(|| { + let snapshot = self.snapshot(); let account_commitment = snapshot.account_tree.get_latest_commitment(account_id); let new_account_id_prefix_is_unique = if account_commitment.is_empty() { diff --git a/crates/store/src/state/sync_state.rs b/crates/store/src/state/sync_state.rs index e63ccab27..820549a5e 100644 --- a/crates/store/src/state/sync_state.rs +++ b/crates/store/src/state/sync_state.rs @@ -105,7 +105,6 @@ impl State { { let snapshot = self.snapshot(); - for note_sync in note_syncs { let mmr_proof = snapshot .blockchain diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index 46dce3aa2..bec50e9a1 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -78,7 +78,8 @@ pub(super) struct BlockWriter { pub db: Arc, pub block_store: Arc, pub in_memory: Arc>, - // TODO(sergerad): RM lock and move to in memory state when protocol updated with crypto v0.26.0. + // TODO(sergerad): RM lock and move to in memory state when protocol updated with crypto + // v0.26.0. pub forest: Arc>>, pub committed_tip_tx: Arc>, pub block_cache: BlockCache, From 942828385fe94c8720d8a160ee9feab93f72305b Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 15:43:45 +1200 Subject: [PATCH 07/11] Rename fn and add comments --- crates/store/src/db/mod.rs | 1 - crates/store/src/state/mod.rs | 2 +- crates/store/src/state/writer.rs | 10 ++++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 000b139ae..063c559e5 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -512,7 +512,6 @@ impl Db { } /// Inserts the data of a new block into the DB. - // TODO: This span is logged in a root span, we should connect it to the parent one. #[instrument(target = COMPONENT, skip_all, err)] pub async fn apply_block( &self, diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 379bc1e3a..64fc11fd3 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -713,7 +713,7 @@ impl State { /// channel, updated by the proof scheduler). pub fn chain_tip(&self, finality: Finality) -> BlockNumber { match finality { - Finality::Committed => self.in_memory.load().block_num, + Finality::Committed => self.snapshot().block_num, Finality::Proven => self.proven_tip.read(), } } diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index bec50e9a1..b4949059d 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -114,13 +114,19 @@ impl BlockWriter { /// Runs the writer loop, processing requests until the channel closes. pub async fn run(mut self) { while let Some(req) = self.rx.recv().await { - let result = self.process_request(req.signed_block).await; + let result = self.write_block(req.signed_block).await; let _ = req.result_tx.send(result); } } + /// Validates and commits a signed block to all persistent and in-memory stores. + /// + /// Validates the block header, concurrently saves the raw block bytes to the block store and + /// computes tree mutations, writes the block to the database, applies mutations to the owned + /// trees, then atomically publishes a new [`InMemoryState`] snapshot and updates the account + /// state forest. #[instrument(target = COMPONENT, skip_all, err)] - async fn process_request(&mut self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> { + async fn write_block(&mut self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> { let header = signed_block.header(); let body = signed_block.body(); From 92d9fce9df9ebcbc6e9aba03665f05d13e607bd6 Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 2 Jun 2026 16:10:30 +1200 Subject: [PATCH 08/11] Reorder write block ops --- crates/store/src/state/writer.rs | 53 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index b4949059d..e3cd3b9e2 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -138,37 +138,25 @@ impl BlockWriter { // Save the block to the block store concurrently with computing mutations. let signed_block_bytes = signed_block.to_bytes(); let cache_bytes = signed_block_bytes.clone(); - let store = Arc::clone(&self.block_store); - let block_save_task = tokio::spawn( - async move { store.save_block(block_num, &signed_block_bytes).await }.in_current_span(), - ); - - let (nullifier_tree_update, account_tree_update) = - self.compute_tree_mutations(header, body)?; - - let notes = Self::build_note_records(header, body)?; + // TODO(sergerad): move this when account forest integrated into in-memory state. // Extract public account deltas before `signed_block` is moved. - let account_deltas = + let account_deltas = tokio::task::block_in_place(|| { Vec::from_iter(body.updated_accounts().iter().filter_map( |update| match update.details() { AccountUpdateDetails::Delta(delta) => Some(delta.clone()), AccountUpdateDetails::Private => None, }, - )); + )) + }); - // Commit to the database. - let db = Arc::clone(&self.db); - db.apply_block(signed_block, notes) - .instrument(info_span!(target: COMPONENT, "db_apply_block")) - .await - .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?; + // Apply in-memory mutations. + let (snapshot, notes) = tokio::task::block_in_place(|| { + let notes = Self::build_note_records(header, body)?; - // Wait for the block store save to complete. - block_save_task.await??; + let (nullifier_tree_update, account_tree_update) = + self.compute_tree_mutations(header, body)?; - // Apply mutations to the owned mutable trees. - tokio::task::block_in_place(|| { self.nullifier_tree .apply_mutations(nullifier_tree_update) .expect("nullifier tree mutation should succeed after validation"); @@ -179,7 +167,6 @@ impl BlockWriter { self.blockchain.push(block_commitment); - // Publish a new snapshot via ArcSwap. let snapshot = Arc::new(InMemoryState { block_num, nullifier_tree: self @@ -189,17 +176,35 @@ impl BlockWriter { account_tree: self.account_tree.reader(), blockchain: (*self.blockchain).clone(), // TODO(sergerad): snapshot of blockchain? }); - self.in_memory.store(snapshot); - Ok::<(), ApplyBlockError>(()) + Ok::<(Arc, Vec<(NoteRecord, Option)>), ApplyBlockError>(( + snapshot, notes, + )) })?; + // Save the block to the block store. + self.block_store.save_block(block_num, &signed_block_bytes).await?; + + // Commit to DB. Readers continue to see the old in-memory state (via their Arc) while + // the DB commits. We ensure consistency by scoping all RPC queries that hit DB data by + // the block number that is Arc swapped at the end of this function. + self.db + .apply_block(signed_block, notes) + .instrument(info_span!(target: COMPONENT, "db_apply_block")) + .await + .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?; + + // TODO(sergerad): Move this into the same task as above once forest is integrated into in-memory state. // Update the forest. tokio::task::block_in_place(|| { let mut forest = self.forest.blocking_write(); forest.apply_block_updates(block_num, account_deltas) })?; + // Atomically publish the new state. Readers that call snapshot() after this point + // will see the updated state. Readers holding the old Arc continue unaffected. + self.in_memory.store(snapshot); + // Notify replica subscribers. self.block_cache.push(block_num, BlockNotification::new(block_num, cache_bytes)); let _ = self.committed_tip_tx.send(block_num); From 8dc3c6e6ed04e72401c52869f3dfb47f92c7bf54 Mon Sep 17 00:00:00 2001 From: sergerad Date: Thu, 4 Jun 2026 11:33:08 +1200 Subject: [PATCH 09/11] Handle block cache err --- crates/rpc/src/server/api.rs | 2 +- crates/store/src/errors.rs | 2 ++ crates/store/src/state/writer.rs | 42 +++++++++++++------------------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 2c6003d59..2750e1697 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -1005,7 +1005,7 @@ impl api_server::Api for RpcService { Ok(Response::new(proto::rpc::RpcStatus { version: env!("CARGO_PKG_VERSION").to_string(), - chain_tip: self.store.chain_tip(Finality::Committed).await.as_u32(), + chain_tip: self.store.chain_tip(Finality::Committed).as_u32(), block_producer: block_producer_status.or(Some(proto::rpc::BlockProducerStatus { status: "unreachable".to_string(), version: "-".to_string(), diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index 0b809e9e7..bac2eb70b 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -188,6 +188,8 @@ pub enum ApplyBlockError { // OTHER ERRORS // --------------------------------------------------------------------------------------------- + #[error("supplied block is not in order: {0}")] + BlockCacheOutOfOrder(BlockNumber), #[error("failed to send block to writer task: {0}")] WriterTaskSendFailed(String), #[error("writer task dropped the result channel")] diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index 17c41cf8f..9ac085771 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -58,15 +58,10 @@ impl WriteHandle { pub async fn apply_block(&self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> { let (result_tx, result_rx) = oneshot::channel(); self.tx - .send(WriteRequest { - signed_block, - result_tx, - }) + .send(WriteRequest { signed_block, result_tx }) .await .map_err(|e| ApplyBlockError::WriterTaskSendFailed(e.to_string()))?; - result_rx - .await - .map_err(ApplyBlockError::WriterTaskRecvFailed)? + result_rx.await.map_err(ApplyBlockError::WriterTaskRecvFailed)? } } @@ -145,8 +140,8 @@ impl BlockWriter { let signed_block_bytes = signed_block.to_bytes(); let cache_bytes = signed_block_bytes.clone(); - // TODO(sergerad): move this when account forest integrated into in-memory state. - // Extract public account deltas before `signed_block` is moved. + // TODO(sergerad): move this when account forest integrated into in-memory state. Extract + // public account deltas before `signed_block` is moved. let account_deltas = tokio::task::block_in_place(|| { Vec::from_iter(body.updated_accounts().iter().filter_map( |update| match update.details() { @@ -189,33 +184,32 @@ impl BlockWriter { })?; // Save the block to the block store. - self.block_store - .save_block(block_num, &signed_block_bytes) - .await?; + self.block_store.save_block(block_num, &signed_block_bytes).await?; - // Commit to DB. Readers continue to see the old in-memory state (via their Arc) while - // the DB commits. We ensure consistency by scoping all RPC queries that hit DB data by - // the block number that is Arc swapped at the end of this function. + // Commit to DB. Readers continue to see the old in-memory state (via their Arc) while the + // DB commits. We ensure consistency by scoping all RPC queries that hit DB data by the + // block number that is Arc swapped at the end of this function. self.db .apply_block(signed_block, notes) .instrument(info_span!(target: COMPONENT, "db_apply_block")) .await .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?; - // TODO(sergerad): Move this into the same task as above once forest is integrated into in-memory state. - // Update the forest. + // TODO(sergerad): Move this into the same task as above once forest is integrated into + // in-memory state. Update the forest. tokio::task::block_in_place(|| { let mut forest = self.forest.blocking_write(); forest.apply_block_updates(block_num, account_deltas) })?; - // Atomically publish the new state. Readers that call snapshot() after this point - // will see the updated state. Readers holding the old Arc continue unaffected. + // Atomically publish the new state. Readers that call snapshot() after this point will see + // the updated state. Readers holding the old Arc continue unaffected. self.in_memory.store(snapshot); // Notify replica subscribers. self.block_cache - .push(block_num, BlockNotification::new(block_num, cache_bytes)); + .push(block_num, BlockNotification::new(block_num, cache_bytes)) + .map_err(|n| ApplyBlockError::BlockCacheOutOfOrder(n.block_num()))?; let _ = self.committed_tip_tx.send(block_num); info!(%block_commitment, block_num = block_num.as_u32(), COMPONENT, "apply_block successful"); @@ -293,9 +287,7 @@ impl BlockWriter { let nullifier_tree_update = self .nullifier_tree .compute_mutations( - body.created_nullifiers() - .iter() - .map(|nullifier| (*nullifier, block_num)), + body.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)), ) .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?; @@ -314,10 +306,10 @@ impl BlockWriter { .map_err(|e| match e { HistoricalError::AccountTreeError(err) => { InvalidBlockError::NewBlockDuplicateAccountIdPrefix(err) - } + }, HistoricalError::MerkleError(_) => { panic!("Unexpected MerkleError during account tree mutation computation") - } + }, })?; if account_tree_update.as_mutation_set().root() != header.account_root() { From 9a865a63f09a87f36b7258be61e5dde7d334d68c Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 9 Jun 2026 14:06:42 +1200 Subject: [PATCH 10/11] Undo shutdown --- crates/rpc/src/tests.rs | 200 ++++++++++++++++++------------- crates/store/src/state/mod.rs | 115 ++++++++---------- crates/store/src/state/writer.rs | 48 +++----- 3 files changed, 186 insertions(+), 177 deletions(-) diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 1d252bc9b..a9216f41d 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -8,12 +8,7 @@ use http::header::{ACCEPT, CONTENT_TYPE}; use http::{HeaderMap, HeaderValue}; use miden_node_block_producer::{BlockProducerApi, BlockProducerApiConfig}; use miden_node_proto::clients::{ - Builder, - GrpcClient, - Interceptor, - NtxBuilderClient, - RpcClient, - ValidatorClient, + Builder, GrpcClient, Interceptor, NtxBuilderClient, RpcClient, ValidatorClient, }; use miden_node_proto::generated::ntx_builder::api_server::ApiServer as NtxBuilderApiServer; use miden_node_proto::generated::rpc::api_client::ApiClient as ProtoClient; @@ -24,21 +19,13 @@ use miden_node_store::state::State; use miden_node_utils::clap::{GrpcOptionsExternal, StorageOptions}; use miden_node_utils::fee::test_fee; use miden_node_utils::limiter::{ - QueryParamAccountIdLimit, - QueryParamLimiter, - QueryParamNoteIdLimit, - QueryParamNoteTagLimit, + QueryParamAccountIdLimit, QueryParamLimiter, QueryParamNoteIdLimit, QueryParamNoteTagLimit, QueryParamNullifierPrefixLimit, }; use miden_protocol::Word; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::account::{ - Account, - AccountBuilder, - AccountDelta, - AccountId, - AccountIdVersion, - AccountType, + Account, AccountBuilder, AccountDelta, AccountId, AccountIdVersion, AccountType, }; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::testing::noop_auth_component::NoopAuthComponent; @@ -97,34 +84,14 @@ impl TestStore { genesis_commitment } - - /// Shuts down the [`BlockWriter`] task before releasing the temp directory. - /// - /// Call this at the end of any test that uses RocksDB-backed storage. Without an explicit - /// shutdown the task is cancelled by the runtime *after* the temp directory is deleted, - /// causing a panic in the `RocksDB` destructor. - /// - /// # Panics - /// - /// Panics if `Self::state` has more than one reference. - async fn shutdown(self) { - let Self { - state, - genesis_commitment: _, - data_directory, - } = self; - - Arc::try_unwrap(state) - .unwrap_or_else(|_| panic!("State should have a single Arc reference at shutdown")) - .shutdown() - .await; - - drop(data_directory); - } } async fn load_state(path: &std::path::Path) -> Arc { - Arc::new(State::load(path, StorageOptions::default()).await.expect("state should load")) + Arc::new( + State::load(path, StorageOptions::default()) + .await + .expect("state should load"), + ) } /// Byte offset of the account delta commitment in serialized `ProvenTransaction`. Layout: @@ -299,8 +266,18 @@ async fn rpc_server_rejects_requests_with_accept_header_invalid_version() { // Assert the server does not reject our request on the basis of missing accept header. assert!(response.is_err()); - assert_eq!(response.as_ref().err().unwrap().code(), tonic::Code::InvalidArgument); - assert!(response.as_ref().err().unwrap().message().contains("server does not support"),); + assert_eq!( + response.as_ref().err().unwrap().code(), + tonic::Code::InvalidArgument + ); + assert!( + response + .as_ref() + .err() + .unwrap() + .message() + .contains("server does not support"), + ); } } @@ -321,7 +298,10 @@ async fn rpc_server_has_web_support() { let mut headers = HeaderMap::new(); let accept_header = concat!("application/vnd.miden; version=", env!("CARGO_PKG_VERSION")); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/grpc-web+proto")); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/grpc-web+proto"), + ); headers.insert(ACCEPT, HeaderValue::from_static(accept_header)); // An empty message with header format: @@ -461,25 +441,21 @@ async fn rpc_rejects_post_deployment_network_account_tx() { transaction_inputs: None, }; - { - let service = RpcService::new( - Arc::clone(&store.state), - RpcMode::full_node(source_rpc_client()), - None, - NonZeroUsize::new(1_000_000).unwrap(), - None, - ); - - let response = service.submit_proven_tx(Request::new(request)).await; - assert!(response.is_err()); - let err = response.as_ref().unwrap_err().message(); - assert!( - err.contains("Network transactions may not be submitted by users yet"), - "expected the network-tx gate error, got: {err}" - ); - } // service (and its Arc clone) dropped here, before calling shutdown. + let service = RpcService::new( + Arc::clone(&store.state), + RpcMode::full_node(source_rpc_client()), + None, + NonZeroUsize::new(1_000_000).unwrap(), + None, + ); - store.shutdown().await; + let response = service.submit_proven_tx(Request::new(request)).await; + assert!(response.is_err()); + let err = response.as_ref().unwrap_err().message(); + assert!( + err.contains("Network transactions may not be submitted by users yet"), + "expected the network-tx gate error, got: {err}" + ); } fn source_rpc_client() -> RpcClient { @@ -512,8 +488,12 @@ impl proto::ntx_builder::api_server::Api for FixedNtxBuilder { async fn start_ntx_builder( response: proto::rpc::GetNetworkNoteStatusResponse, ) -> (NtxBuilderClient, Arc) { - let listener = TcpListener::bind("127.0.0.1:0").await.expect("Failed to bind ntx-builder"); - let addr = listener.local_addr().expect("Failed to get ntx-builder address"); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind ntx-builder"); + let addr = listener + .local_addr() + .expect("Failed to get ntx-builder address"); let call_count = Arc::new(AtomicUsize::new(0)); let service = FixedNtxBuilder { response, @@ -547,8 +527,12 @@ async fn start_source_rpc(ntx_builder: NtxBuilderClient) -> (RpcClient, TestStor let block_producer_state = load_state(block_producer_data_directory.path()).await; let store_state = Arc::clone(&store.state); - let listener = TcpListener::bind("127.0.0.1:0").await.expect("Failed to bind source RPC"); - let addr = listener.local_addr().expect("Failed to get source RPC address"); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind source RPC"); + let addr = listener + .local_addr() + .expect("Failed to get source RPC address"); task::spawn(async move { let _block_producer_data_directory = block_producer_data_directory; @@ -704,8 +688,12 @@ async fn start_rpc_with_options( let store_state = Arc::clone(&store.state); // Start the rpc component. - let rpc_listener = TcpListener::bind("127.0.0.1:0").await.expect("Failed to bind rpc"); - let rpc_addr = rpc_listener.local_addr().expect("Failed to get rpc address"); + let rpc_listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind rpc"); + let rpc_addr = rpc_listener + .local_addr() + .expect("Failed to get rpc address"); task::spawn(async move { let _block_producer_data_directory = block_producer_data_directory; // SAFETY: Using dummy validator URL for test - not actually contacted in this test @@ -748,16 +736,26 @@ async fn get_limits_endpoint() { let (mut rpc_client, _rpc_addr, _store) = start_rpc().await; // Call the get_limits endpoint - let response = rpc_client.get_limits(()).await.expect("get_limits should succeed"); + let response = rpc_client + .get_limits(()) + .await + .expect("get_limits should succeed"); let limits = response.into_inner(); // Verify the response contains expected endpoints and limits - assert!(!limits.endpoints.is_empty(), "endpoints should not be empty"); + assert!( + !limits.endpoints.is_empty(), + "endpoints should not be empty" + ); - let sync_transactions = - limits.endpoints.get("SyncTransactions").expect("SyncTransactions should exist"); + let sync_transactions = limits + .endpoints + .get("SyncTransactions") + .expect("SyncTransactions should exist"); assert_eq!( - sync_transactions.parameters.get(QueryParamAccountIdLimit::PARAM_NAME), + sync_transactions + .parameters + .get(QueryParamAccountIdLimit::PARAM_NAME), Some(&(QueryParamAccountIdLimit::LIMIT as u32)), "SyncTransactions {} limit should be {}", QueryParamAccountIdLimit::PARAM_NAME, @@ -765,10 +763,14 @@ async fn get_limits_endpoint() { ); // Verify SyncNullifiers endpoint - let sync_nullifiers = - limits.endpoints.get("SyncNullifiers").expect("SyncNullifiers should exist"); + let sync_nullifiers = limits + .endpoints + .get("SyncNullifiers") + .expect("SyncNullifiers should exist"); assert_eq!( - sync_nullifiers.parameters.get(QueryParamNullifierPrefixLimit::PARAM_NAME), + sync_nullifiers + .parameters + .get(QueryParamNullifierPrefixLimit::PARAM_NAME), Some(&(QueryParamNullifierPrefixLimit::LIMIT as u32)), "SyncNullifiers {} limit should be {}", QueryParamNullifierPrefixLimit::PARAM_NAME, @@ -776,9 +778,14 @@ async fn get_limits_endpoint() { ); // Verify SyncNotes endpoint - let sync_notes = limits.endpoints.get("SyncNotes").expect("SyncNotes should exist"); + let sync_notes = limits + .endpoints + .get("SyncNotes") + .expect("SyncNotes should exist"); assert_eq!( - sync_notes.parameters.get(QueryParamNoteTagLimit::PARAM_NAME), + sync_notes + .parameters + .get(QueryParamNoteTagLimit::PARAM_NAME), Some(&(QueryParamNoteTagLimit::LIMIT as u32)), "SyncNotes {} limit should be {}", QueryParamNoteTagLimit::PARAM_NAME, @@ -797,9 +804,14 @@ async fn get_limits_endpoint() { ); // Verify GetNotesById endpoint - let get_notes_by_id = limits.endpoints.get("GetNotesById").expect("GetNotesById should exist"); + let get_notes_by_id = limits + .endpoints + .get("GetNotesById") + .expect("GetNotesById should exist"); assert_eq!( - get_notes_by_id.parameters.get(QueryParamNoteIdLimit::PARAM_NAME), + get_notes_by_id + .parameters + .get(QueryParamNoteIdLimit::PARAM_NAME), Some(&(QueryParamNoteIdLimit::LIMIT as u32)), "GetNotesById {} limit should be {}", QueryParamNoteIdLimit::PARAM_NAME, @@ -815,7 +827,10 @@ async fn sync_chain_mmr_returns_delta() { current_client_block_height: 0, finality_level: proto::rpc::FinalityLevel::Committed.into(), }; - let response = rpc_client.sync_chain_mmr(request).await.expect("sync_chain_mmr should succeed"); + let response = rpc_client + .sync_chain_mmr(request) + .await + .expect("sync_chain_mmr should succeed"); let response = response.into_inner(); let mmr_delta = response.mmr_delta.expect("mmr_delta should exist"); @@ -844,16 +859,29 @@ fn sync_chain_mmr_block_header_matches_chain_commitment() { client_mmr.add(headers[0].commitment(), false).unwrap(); // First delta: block_from=0, block_to=2, so from_forest=1, to_forest=2. - let delta = server_mmr.get_delta(Forest::new(1).unwrap(), Forest::new(2).unwrap()).unwrap(); + let delta = server_mmr + .get_delta(Forest::new(1).unwrap(), Forest::new(2).unwrap()) + .unwrap(); client_mmr.apply(delta).unwrap(); - assert_eq!(client_mmr.peaks().hash_peaks(), headers[2].chain_commitment()); + assert_eq!( + client_mmr.peaks().hash_peaks(), + headers[2].chain_commitment() + ); client_mmr.add(headers[2].commitment(), false).unwrap(); // Second delta: block_from=2, block_to=4, so from_forest=3, to_forest=4. - let delta = server_mmr.get_delta(Forest::new(3).unwrap(), Forest::new(4).unwrap()).unwrap(); + let delta = server_mmr + .get_delta(Forest::new(3).unwrap(), Forest::new(4).unwrap()) + .unwrap(); client_mmr.apply(delta).unwrap(); - assert_eq!(client_mmr.peaks().hash_peaks(), headers[4].chain_commitment()); + assert_eq!( + client_mmr.peaks().hash_peaks(), + headers[4].chain_commitment() + ); client_mmr.add(headers[4].commitment(), false).unwrap(); - assert_eq!(client_mmr.peaks().hash_peaks(), server_mmr.peaks().hash_peaks()); + assert_eq!( + client_mmr.peaks().hash_peaks(), + server_mmr.peaks().hash_peaks() + ); } diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 4d9d67f43..b43bc677b 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -21,7 +21,7 @@ use miden_protocol::crypto::merkle::mmr::{MmrProof, PartialMmr}; use miden_protocol::crypto::merkle::smt::LargeSmt; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_protocol::transaction::PartialBlockchain; -use tokio::sync::{RwLock, oneshot, watch}; +use tokio::sync::{RwLock, watch}; use tracing::{Span, info, instrument}; use crate::account_state_forest::{AccountStateForest, AccountStateForestBackend}; @@ -29,10 +29,7 @@ use crate::accounts::AccountTreeWithHistory; use crate::blocks::BlockStore; use crate::db::{Db, NoteRecord, NullifierInfo}; use crate::errors::{ - DatabaseError, - GetBatchInputsError, - GetBlockHeaderError, - GetBlockInputsError, + DatabaseError, GetBatchInputsError, GetBlockHeaderError, GetBlockInputsError, StateInitializationError, }; use crate::proven_tip::ProvenTipWriter; @@ -46,16 +43,9 @@ const PROOF_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap(); mod loader; use loader::{ - ACCOUNT_STATE_FOREST_STORAGE_DIR, - ACCOUNT_TREE_STORAGE_DIR, - AccountForestLoader, - NULLIFIER_TREE_STORAGE_DIR, - SnapshotTreeStorage, - TreeStorage, - TreeStorageLoader, - load_mmr, - verify_account_state_forest_consistency, - verify_tree_consistency, + ACCOUNT_STATE_FOREST_STORAGE_DIR, ACCOUNT_TREE_STORAGE_DIR, AccountForestLoader, + NULLIFIER_TREE_STORAGE_DIR, SnapshotTreeStorage, TreeStorage, TreeStorageLoader, load_mmr, + verify_account_state_forest_consistency, verify_tree_consistency, }; mod replica; @@ -65,11 +55,8 @@ mod account; mod subscription; pub use subscription::{ - BlockSubscriptionEvent, - BlockSubscriptionStream, - ProofSubscriptionEvent, - ProofSubscriptionStream, - StateSubscriptionError, + BlockSubscriptionEvent, BlockSubscriptionStream, ProofSubscriptionEvent, + ProofSubscriptionStream, StateSubscriptionError, }; mod apply_block; @@ -162,10 +149,6 @@ pub struct State { /// FIFO cache of recent block proofs for replica subscriptions. pub(crate) proof_cache: ProofCache, - - /// Resolves after the [`BlockWriter`] task has fully stopped and all on-disk resources - /// (including `RocksDB`) have been flushed and released. Consumed by [`Self::shutdown`]. - writer_done: oneshot::Receiver<()>, } impl State { @@ -223,8 +206,11 @@ impl State { TreeStorage::create(data_path, &account_storage_config, ACCOUNT_TREE_STORAGE_DIR)?; let account_tree = account_storage.load_account_tree(&mut db).await?; - let nullifier_storage = - TreeStorage::create(data_path, &nullifier_storage_config, NULLIFIER_TREE_STORAGE_DIR)?; + let nullifier_storage = TreeStorage::create( + data_path, + &nullifier_storage_config, + NULLIFIER_TREE_STORAGE_DIR, + )?; let nullifier_tree = nullifier_storage.load_nullifier_tree(&mut db).await?; verify_tree_consistency(account_tree.root(), nullifier_tree.root(), &mut db).await?; @@ -236,7 +222,9 @@ impl State { &forest_storage_config, ACCOUNT_STATE_FOREST_STORAGE_DIR, )?; - let forest = forest_backend.load_account_state_forest(&mut db, latest_block_num).await?; + let forest = forest_backend + .load_account_state_forest(&mut db, latest_block_num) + .await?; verify_account_state_forest_consistency(&forest, &mut db).await?; let db = Arc::new(db); @@ -268,10 +256,6 @@ impl State { let (write_tx, write_rx) = tokio::sync::mpsc::channel(1); let write_handle = WriteHandle::new(write_tx); - // writer_done_tx is held by BlockWriter and dropped after its trees are flushed, waking - // writer_done on the caller side. - let (writer_done_tx, writer_done) = oneshot::channel(); - // Spawn the BlockWriter task. let block_writer = BlockWriter { db: Arc::clone(&db), @@ -281,10 +265,9 @@ impl State { committed_tip_tx: Arc::clone(&committed_tip_tx), block_cache: block_cache.clone(), rx: write_rx, - nullifier_tree: std::mem::ManuallyDrop::new(nullifier_tree), - account_tree: std::mem::ManuallyDrop::new(account_tree), - blockchain: std::mem::ManuallyDrop::new(blockchain), - writer_done_tx: std::mem::ManuallyDrop::new(writer_done_tx), + nullifier_tree, + account_tree, + blockchain, }; tokio::spawn(block_writer.run()); @@ -299,7 +282,6 @@ impl State { committed_tip_tx, block_cache, proof_cache, - writer_done, }) } @@ -308,21 +290,6 @@ impl State { self.committed_tip_tx.subscribe() } - /// Shuts down the state, waiting for the [`BlockWriter`] task to fully stop. - /// - /// Dropping `State` closes the write channel, which signals [`BlockWriter`] to stop. This - /// method waits for the writer to complete — including any pending `RocksDB` flushes — before - /// returning. Call this before releasing on-disk resources (e.g. removing a data directory). - pub async fn shutdown(self) { - // Drop everything except `writer_done` inside an inner scope so that `write_handle` is - // dropped — closing the write channel — before we await the writer's completion. - let writer_done = { - let Self { writer_done, .. } = self; - writer_done - }; - writer_done.await.ok(); - } - /// Loads serialized block proving inputs from the block store. pub async fn load_proving_inputs( &self, @@ -425,7 +392,9 @@ impl State { .await .map_err(GetBatchInputsError::SelectNoteInclusionProofError)?; - let note_blocks = note_proofs.values().map(|proof| proof.location().block_num()); + let note_blocks = note_proofs + .values() + .map(|proof| proof.location().block_num()); let mut blocks: BTreeSet = tx_reference_blocks; blocks.extend(note_blocks); @@ -434,8 +403,9 @@ impl State { let snapshot = self.snapshot(); let latest_block_num = snapshot.block_num; - let highest_block_num = - *blocks.last().expect("we should have checked for empty block references"); + let highest_block_num = *blocks + .last() + .expect("we should have checked for empty block references"); if highest_block_num > latest_block_num { return Err(GetBatchInputsError::UnknownTransactionBlockReference { highest_block_num, @@ -456,7 +426,11 @@ impl State { let mut headers = self .db - .select_block_headers(blocks.into_iter().chain(std::iter::once(batch_reference_block))) + .select_block_headers( + blocks + .into_iter() + .chain(std::iter::once(batch_reference_block)), + ) .await .map_err(GetBatchInputsError::SelectBlockHeaderError)?; @@ -494,8 +468,9 @@ impl State { .await .map_err(GetBlockInputsError::SelectNoteInclusionProofError)?; - let note_proof_reference_blocks = - unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()); + let note_proof_reference_blocks = unauthenticated_note_proofs + .values() + .map(|proof| proof.location().block_num()); let mut blocks = reference_blocks; blocks.extend(note_proof_reference_blocks); @@ -505,7 +480,11 @@ impl State { let mut headers = self .db - .select_block_headers(blocks.into_iter().chain(std::iter::once(latest_block_number))) + .select_block_headers( + blocks + .into_iter() + .chain(std::iter::once(latest_block_number)), + ) .await .map_err(GetBlockInputsError::SelectBlockHeaderError)?; @@ -620,7 +599,11 @@ impl State { }) .collect(); - Ok((account_commitment, nullifiers, new_account_id_prefix_is_unique)) + Ok(( + account_commitment, + nullifiers, + new_account_id_prefix_is_unique, + )) }) }); let (account_commitment, nullifiers, new_account_id_prefix_is_unique) = match tree_inputs { @@ -646,7 +629,9 @@ impl State { &self, account_ids: &[AccountId], ) -> Result, DatabaseError> { - self.db.select_network_accounts_subset(account_ids.to_vec()).await + self.db + .select_network_accounts_subset(account_ids.to_vec()) + .await } /// Returns the effective chain tip for the given finality level. @@ -670,7 +655,10 @@ impl State { if block_num > self.chain_tip(Finality::Committed) { return Ok(None); } - self.block_store.load_block(block_num).await.map_err(Into::into) + self.block_store + .load_block(block_num) + .await + .map_err(Into::into) } /// Loads a block proof from the block store. Returns `Ok(None)` if the proof is not found. @@ -681,7 +669,10 @@ impl State { if block_num > self.chain_tip(Finality::Proven) { return Ok(None); } - self.block_store.load_proof(block_num).await.map_err(Into::into) + self.block_store + .load_proof(block_num) + .await + .map_err(Into::into) } /// Returns the script for a note by its root. diff --git a/crates/store/src/state/writer.rs b/crates/store/src/state/writer.rs index 9ac085771..4153ab720 100644 --- a/crates/store/src/state/writer.rs +++ b/crates/store/src/state/writer.rs @@ -5,7 +5,6 @@ //! [`InMemoryState`] snapshot via an [`ArcSwap`], making the updated trees immediately visible to //! wait-free readers. -use std::mem::ManuallyDrop; use std::sync::Arc; use arc_swap::ArcSwap; @@ -58,10 +57,15 @@ impl WriteHandle { pub async fn apply_block(&self, signed_block: SignedBlock) -> Result<(), ApplyBlockError> { let (result_tx, result_rx) = oneshot::channel(); self.tx - .send(WriteRequest { signed_block, result_tx }) + .send(WriteRequest { + signed_block, + result_tx, + }) .await .map_err(|e| ApplyBlockError::WriterTaskSendFailed(e.to_string()))?; - result_rx.await.map_err(ApplyBlockError::WriterTaskRecvFailed)? + result_rx + .await + .map_err(ApplyBlockError::WriterTaskRecvFailed)? } } @@ -86,29 +90,11 @@ pub(super) struct BlockWriter { pub block_cache: BlockCache, pub rx: mpsc::Receiver, /// The mutable nullifier tree owned by this writer. - pub nullifier_tree: ManuallyDrop>>, + pub nullifier_tree: NullifierTree>, /// The mutable account tree owned by this writer. - pub account_tree: ManuallyDrop>, + pub account_tree: AccountTreeWithHistory, /// The blockchain MMR owned by this writer. - pub blockchain: ManuallyDrop, - /// Signals [`State::shutdown`] after the trees above have been flushed. - pub writer_done_tx: ManuallyDrop>, -} - -impl Drop for BlockWriter { - fn drop(&mut self) { - // Drop the trees first so their RocksDB destructors flush to disk, then explicitly - // signal `State::shutdown` that it is safe to release on-disk resources. - // - // SAFETY: each field is dropped exactly once here; the compiler's auto-drop pass - // afterwards treats ManuallyDrop fields as no-ops. - unsafe { - ManuallyDrop::drop(&mut self.nullifier_tree); - ManuallyDrop::drop(&mut self.account_tree); - ManuallyDrop::drop(&mut self.blockchain); - let _ = ManuallyDrop::take(&mut self.writer_done_tx).send(()); - } - } + pub blockchain: Blockchain, } impl BlockWriter { @@ -175,7 +161,7 @@ impl BlockWriter { .reader() .expect("nullifier tree snapshot creation should not fail"), account_tree: self.account_tree.reader(), - blockchain: (*self.blockchain).clone(), // TODO(sergerad): snapshot of blockchain? + blockchain: self.blockchain.clone(), // TODO(sergerad): snapshot of blockchain? }); Ok::<(Arc, Vec<(NoteRecord, Option)>), ApplyBlockError>(( @@ -184,7 +170,9 @@ impl BlockWriter { })?; // Save the block to the block store. - self.block_store.save_block(block_num, &signed_block_bytes).await?; + self.block_store + .save_block(block_num, &signed_block_bytes) + .await?; // Commit to DB. Readers continue to see the old in-memory state (via their Arc) while the // DB commits. We ensure consistency by scoping all RPC queries that hit DB data by the @@ -287,7 +275,9 @@ impl BlockWriter { let nullifier_tree_update = self .nullifier_tree .compute_mutations( - body.created_nullifiers().iter().map(|nullifier| (*nullifier, block_num)), + body.created_nullifiers() + .iter() + .map(|nullifier| (*nullifier, block_num)), ) .map_err(InvalidBlockError::NewBlockNullifierAlreadySpent)?; @@ -306,10 +296,10 @@ impl BlockWriter { .map_err(|e| match e { HistoricalError::AccountTreeError(err) => { InvalidBlockError::NewBlockDuplicateAccountIdPrefix(err) - }, + } HistoricalError::MerkleError(_) => { panic!("Unexpected MerkleError during account tree mutation computation") - }, + } })?; if account_tree_update.as_mutation_set().root() != header.account_root() { From abb08c239e7bcb7c7414035cfef17324be6d671f Mon Sep 17 00:00:00 2001 From: sergerad Date: Tue, 9 Jun 2026 13:11:19 +1200 Subject: [PATCH 11/11] Static temp dirs --- crates/rpc/src/tests.rs | 42 ++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index a9216f41d..9a3778335 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -43,11 +43,27 @@ use url::Url; use crate::server::api::RpcService; use crate::{Rpc, RpcMode}; +/// Global registry of temp directories. Held for the lifetime of the test binary so that `RocksDB` +/// can always flush on drop regardless of test outcome or drop ordering. +static TEMP_DIRS: std::sync::OnceLock>> = std::sync::OnceLock::new(); + +/// Creates a temp directory, registers it in the global registry, and returns its path. +fn new_tempdir() -> std::path::PathBuf { + let dir = tempfile::tempdir().expect("tempdir should be created"); + let path = dir.path().to_path_buf(); + TEMP_DIRS + .get_or_init(|| std::sync::Mutex::new(Vec::new())) + .lock() + .unwrap() + .push(dir); + path +} + /// A wrapper around the loaded store state and its backing data directory. struct TestStore { state: Arc, genesis_commitment: Word, - data_directory: TempDir, + data_directory: std::path::PathBuf, } impl TestStore { @@ -56,13 +72,13 @@ impl TestStore { } fn data_directory_path(&self) -> &std::path::Path { - self.data_directory.path() + &self.data_directory } async fn start() -> Self { - let data_directory = tempfile::tempdir().expect("tempdir should be created"); - let genesis_commitment = Self::bootstrap(data_directory.path()); - let state = load_state(data_directory.path()).await; + let data_directory = new_tempdir(); + let genesis_commitment = Self::bootstrap(&data_directory); + let state = load_state(&data_directory).await; Self { state, genesis_commitment, @@ -521,10 +537,9 @@ async fn start_ntx_builder( async fn start_source_rpc(ntx_builder: NtxBuilderClient) -> (RpcClient, TestStore) { let store = TestStore::start().await; - let block_producer_data_directory = - tempfile::tempdir().expect("block producer state tempdir should be created"); - TestStore::bootstrap(block_producer_data_directory.path()); - let block_producer_state = load_state(block_producer_data_directory.path()).await; + let block_producer_dir = new_tempdir(); + TestStore::bootstrap(&block_producer_dir); + let block_producer_state = load_state(&block_producer_dir).await; let store_state = Arc::clone(&store.state); let listener = TcpListener::bind("127.0.0.1:0") @@ -535,7 +550,6 @@ async fn start_source_rpc(ntx_builder: NtxBuilderClient) -> (RpcClient, TestStor .expect("Failed to get source RPC address"); task::spawn(async move { - let _block_producer_data_directory = block_producer_data_directory; let validator_url = Url::parse("http://127.0.0.1:0").unwrap(); let block_producer = BlockProducerApi::new( block_producer_state, @@ -681,10 +695,9 @@ async fn start_rpc_with_options( grpc_options: GrpcOptionsExternal, ) -> (RpcClient, std::net::SocketAddr, TestStore) { let store = TestStore::start().await; - let block_producer_data_directory = - tempfile::tempdir().expect("block producer state tempdir should be created"); - TestStore::bootstrap(block_producer_data_directory.path()); - let block_producer_state = load_state(block_producer_data_directory.path()).await; + let block_producer_dir = new_tempdir(); + TestStore::bootstrap(&block_producer_dir); + let block_producer_state = load_state(&block_producer_dir).await; let store_state = Arc::clone(&store.state); // Start the rpc component. @@ -695,7 +708,6 @@ async fn start_rpc_with_options( .local_addr() .expect("Failed to get rpc address"); task::spawn(async move { - let _block_producer_data_directory = block_producer_data_directory; // SAFETY: Using dummy validator URL for test - not actually contacted in this test let validator_url = Url::parse("http://127.0.0.1:0").unwrap(); let block_producer = BlockProducerApi::new(