diff --git a/Cargo.lock b/Cargo.lock index 7380a60ed..bca372477 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]] @@ -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" @@ -1684,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]] @@ -2944,9 +2953,8 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a52fa470d035f75ec7d48e37cc297a3988858abfffe4b0c514c2561a7a92b1d1" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "alloy-sol-types", "fs-err", @@ -3025,9 +3033,8 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf9952b224950d826230146f513b45ebf9eb9dcc6066fb7c442afe83532dc4c" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "miden-protocol", "thiserror 2.0.18", @@ -3176,7 +3183,7 @@ dependencies = [ [[package]] name = "miden-genesis" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "clap", @@ -3194,7 +3201,7 @@ dependencies = [ [[package]] name = "miden-large-smt-backend-rocksdb" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "miden-crypto", "miden-node-rocksdb-cxx-linkage-fix", @@ -3290,7 +3297,7 @@ dependencies = [ [[package]] name = "miden-network-monitor" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "axum", @@ -3321,7 +3328,7 @@ dependencies = [ [[package]] name = "miden-node" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "clap", @@ -3339,7 +3346,7 @@ dependencies = [ [[package]] name = "miden-node-block-producer" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "assert_matches", @@ -3368,7 +3375,7 @@ dependencies = [ [[package]] name = "miden-node-db" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "build-rs", @@ -3387,7 +3394,7 @@ dependencies = [ [[package]] name = "miden-node-grpc-error-macro" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "quote", "syn 2.0.117", @@ -3395,7 +3402,7 @@ dependencies = [ [[package]] name = "miden-node-proto" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "build-rs", @@ -3421,7 +3428,7 @@ dependencies = [ [[package]] name = "miden-node-proto-build" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "build-rs", "codegen", @@ -3433,11 +3440,11 @@ dependencies = [ [[package]] name = "miden-node-rocksdb-cxx-linkage-fix" -version = "0.15.0-rc.3" +version = "0.15.0" [[package]] name = "miden-node-rpc" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "futures", @@ -3470,9 +3477,10 @@ dependencies = [ [[package]] name = "miden-node-store" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", + "arc-swap", "assert_matches", "build-rs", "criterion", @@ -3506,7 +3514,7 @@ dependencies = [ [[package]] name = "miden-node-stress-test" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "clap", "fs-err", @@ -3531,7 +3539,7 @@ dependencies = [ [[package]] name = "miden-node-utils" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "backon", @@ -3566,7 +3574,7 @@ dependencies = [ [[package]] name = "miden-ntx-builder" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "backon", @@ -3650,9 +3658,8 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1759044f21d139e6f82ecd47c55271b23f76999e87aa78869d862b4fd77ae3" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "bech32", "fs-err", @@ -3695,7 +3702,7 @@ dependencies = [ [[package]] name = "miden-remote-prover" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "assert_matches", @@ -3724,7 +3731,7 @@ dependencies = [ [[package]] name = "miden-remote-prover-client" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "build-rs", "fs-err", @@ -3754,9 +3761,8 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e80fd9b9e508cbdfae19dac06e19ec28f01635f67c76e593f99de0701ab7a2" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "bon", "fs-err", @@ -3793,9 +3799,8 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f589ec7d6bb05e96cbdb8cdc0847467b0fe650854047abe6ca85896d98a49fe5" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3814,9 +3819,8 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c529e50c553269c8a349e1379daade04025b1e9f0c9b32b969d1f31e2e1eb54d" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "miden-processor", "miden-protocol", @@ -3828,9 +3832,8 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935aaeeb1ee2fd8a1ac5df281a76fd553204221e3d574d6ae8a43906066bfc90" +version = "0.16.0" +source = "git+https://github.com/0xmiden/protocol?branch=sergerad-clone#e0446caead045ebb4ecba1be9d12dd82f623f937" dependencies = [ "miden-protocol", "miden-tx", @@ -3885,7 +3888,7 @@ dependencies = [ [[package]] name = "miden-validator" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "aws-config", @@ -4052,7 +4055,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]] @@ -4295,9 +4298,9 @@ dependencies = [ [[package]] name = "p3-challenger" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8972ccd1d5dc90e46cdb1f2ab4ee2bae49b3917e5e98aa533f0c2b779c010445" +checksum = "4a0b490c745a7d2adeeafff06411814c8078c432740162332b3cd71be0158a76" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -4309,9 +4312,9 @@ dependencies = [ [[package]] name = "p3-dft" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17771aca44632f9cc11f2718d7ea7ec06794946c4190ef3a985bfc893f14c18a" +checksum = "55301e91544440254977108b85c32c09d7ea05f2f0dd61092a2825339906a4a7" dependencies = [ "itertools 0.14.0", "p3-field", @@ -4324,9 +4327,9 @@ dependencies = [ [[package]] name = "p3-field" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3eb24d0591fd4d282d89cbe4e4efba5571c699375006f80b2cbf53ce83461c" +checksum = "85affca7fc983889f260655c4cf74163eebb94605f702e4b6809ead707cba54f" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -4340,9 +4343,9 @@ dependencies = [ [[package]] name = "p3-goldilocks" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5751c6591a0d2397d726620c2c29a7436ec6c5e19d2ed74ca5d078d4fbb18eb5" +checksum = "0ca1081f5c47b940f2d75a11c04f62ea1cc58a5d480dd465fef3861c045c63cd" dependencies = [ "num-bigint", "p3-challenger", @@ -4371,9 +4374,9 @@ dependencies = [ [[package]] name = "p3-matrix" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9c94c0714944e7b8a9a62e6340b1e3e1d3f8ecfd3e35c08798360200e73eff" +checksum = "53428126b009071563d1d07305a9de8be0d21de00b57d2475289ee32ffca6577" dependencies = [ "itertools 0.14.0", "p3-field", @@ -4386,18 +4389,18 @@ dependencies = [ [[package]] name = "p3-maybe-rayon" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebc233a34b1ab0273f35b4052fa2eeb3114b22ba4575bd7da00716e878ffb77" +checksum = "082bf467011c06c768c579ec6eb9accb5e1e62108891634cc770396e917f978a" dependencies = [ "rayon", ] [[package]] name = "p3-mds" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5441fa8116246ec9e6c835f15273cb27777ca572960ec87476b67fef13e01e" +checksum = "35209e6214102ea6ec6b8cb1b9c15a9b8e597a39f9173597c957f123bced81b3" dependencies = [ "p3-dft", "p3-field", @@ -4408,9 +4411,9 @@ dependencies = [ [[package]] name = "p3-monty-31" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8724f330ea6d19dd4f2436aa0f88b5fcbf88f0f55ca7fccd3fea8b736dbcddad" +checksum = "ffa8c99ec50c035020bbf5457c6a729ba6a975719c1a8dd3f16421081e4f650c" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -4432,9 +4435,9 @@ dependencies = [ [[package]] name = "p3-poseidon1" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e2a562fea210baae390a32f9ecf0dd8724ae3f4352d1c8e413077b6f00a162" +checksum = "6a018b618e3fa0aec8be933b1d8e404edd23f46991f6bf3f5c2f3f95e9413fe9" dependencies = [ "p3-field", "p3-symmetric", @@ -4443,9 +4446,9 @@ dependencies = [ [[package]] name = "p3-poseidon2" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06394851c161d17e4aa4ad2aad5557d32f14cadd1dc838f965d8e1821a63b8c5" +checksum = "256a668a9ba916f8767552f13d0ba50d18968bc74a623bfdafa41e2970c944d0" dependencies = [ "p3-field", "p3-mds", @@ -4456,9 +4459,9 @@ dependencies = [ [[package]] name = "p3-symmetric" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac1a276d421f8ef3361bb7d8c39a02c93c6b3f10eeaa559cc4c50222f9a5b82" +checksum = "6c60a71a1507c13611b0f2b0b6e83669fd5b76f8e3115bcbced5ccfdf3ca7807" dependencies = [ "itertools 0.14.0", "p3-field", @@ -4468,9 +4471,9 @@ dependencies = [ [[package]] name = "p3-util" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08a58162a4c264269ef454f0b28dcda89939490eecacb2b2cf5b00f719b80f6" +checksum = "f8b766b9e9254bf3fa98d76e42cf8a5b30628c182dfd5272d270076ee12f0fc0" dependencies = [ "rayon", "serde", @@ -4824,7 +4827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", - "itertools 0.13.0", + "itertools 0.14.0", "log", "multimap", "petgraph 0.8.3", @@ -4845,7 +4848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5388,7 +5391,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5459,7 +5462,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5867,7 +5870,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]] @@ -6088,7 +6091,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6097,7 +6100,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]] @@ -6116,7 +6119,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]] @@ -7128,7 +7131,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]] @@ -7550,7 +7553,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "0.15.0-rc.3" +version = "0.15.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index c2adcdffc..1e77d63a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ license = "MIT" readme = "README.md" repository = "https://github.com/0xMiden/node" rust-version = "1.93" -version = "0.15.0-rc.3" +version = "0.15.0" # Optimize the cryptography for faster tests involving account creation. [profile.test.package.miden-crypto] @@ -44,31 +44,29 @@ debug = true [workspace.dependencies] # Workspace crates. -miden-large-smt-backend-rocksdb = { path = "crates/large-smt-backend-rocksdb", version = "0.15.0-rc.3" } -miden-node-block-producer = { path = "crates/block-producer", version = "0.15.0-rc.3" } -miden-node-db = { path = "crates/db", version = "0.15.0-rc.3" } -miden-node-grpc-error-macro = { path = "crates/grpc-error-macro", version = "0.15.0-rc.3" } -miden-node-proto = { path = "crates/proto", version = "0.15.0-rc.3" } -miden-node-proto-build = { path = "proto", version = "0.15.0-rc.3" } -miden-node-rpc = { path = "crates/rpc", version = "0.15.0-rc.3" } -miden-node-store = { path = "crates/store", version = "0.15.0-rc.3" } +miden-large-smt-backend-rocksdb = { path = "crates/large-smt-backend-rocksdb", version = "0.15" } +miden-node-block-producer = { path = "crates/block-producer", version = "0.15" } +miden-node-db = { path = "crates/db", version = "0.15" } +miden-node-grpc-error-macro = { path = "crates/grpc-error-macro", version = "0.15" } +miden-node-proto = { path = "crates/proto", version = "0.15" } +miden-node-proto-build = { path = "proto", version = "0.15" } +miden-node-rpc = { path = "crates/rpc", version = "0.15" } +miden-node-store = { path = "crates/store", version = "0.15" } miden-node-test-macro = { path = "crates/test-macro" } -miden-node-utils = { path = "crates/utils", version = "0.15.0-rc.3" } -miden-remote-prover-client = { path = "crates/remote-prover-client", version = "0.15.0-rc.3" } +miden-node-utils = { path = "crates/utils", version = "0.15" } +miden-remote-prover-client = { path = "crates/remote-prover-client", version = "0.15" } # Temporary workaround until # is part of `rocksdb-rust` release -miden-node-rocksdb-cxx-linkage-fix = { path = "crates/rocksdb-cxx-linkage-fix", version = "0.15.0-rc.3" } +miden-node-rocksdb-cxx-linkage-fix = { path = "crates/rocksdb-cxx-linkage-fix", version = "0.15" } # miden-protocol dependencies. These should be updated in sync. -# Requires the v0.15.2 patch release, which carries the network-account tx-script -# allowlist (0xMiden/protocol#3028) and the `with_allowed_notes` rename (#3049). -miden-agglayer = { version = "0.15.2" } -miden-block-prover = { version = "0.15.2" } -miden-protocol = { default-features = false, version = "0.15.2" } -miden-standards = { version = "0.15.2" } -miden-testing = { version = "0.15.2" } -miden-tx = { default-features = false, version = "0.15.2" } -miden-tx-batch-prover = { version = "0.15.2" } +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" } @@ -134,6 +132,7 @@ url = { features = ["serde"], version = "2.5" } [workspace.metadata.cargo-shear] # libsqlite3-sys is kept to control the bundled SQLite linkage. # tonic-prost is used by generated gRPC code rather than handwritten Rust. +# ignored = ["libsqlite3-sys", "tonic-prost"] # Lints are set to warn for development, which are promoted to errors in CI. @@ -161,3 +160,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/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/network-monitor/src/deploy/counter.rs b/bin/network-monitor/src/deploy/counter.rs index 886fa787b..77c6d9b2a 100644 --- a/bin/network-monitor/src/deploy/counter.rs +++ b/bin/network-monitor/src/deploy/counter.rs @@ -69,7 +69,7 @@ pub fn create_counter_account(owner_account_id: AccountId) -> Result { // tx-script allowlist. This waits on the canonical, frozen expiration script (#3050) so the // root can be pinned here without duplicating the ntx-builder's script/delta. let network_account_auth: AccountComponent = - AuthNetworkAccount::with_allowed_notes(allowed_scripts) + AuthNetworkAccount::with_allowlist(allowed_scripts) .expect("list is not empty") .into(); diff --git a/bin/node/src/commands/modes.rs b/bin/node/src/commands/modes.rs index 27c2273ec..9eb671572 100644 --- a/bin/node/src/commands/modes.rs +++ b/bin/node/src/commands/modes.rs @@ -53,7 +53,6 @@ impl SequencerCommand { mempool_tx_capacity: self.block_producer.mempool.tx_capacity, } .spawn() - .await .context("failed to spawn sequencer")?; let block_producer = sequencer.api(); diff --git a/bin/ntx-builder/src/actor/allowlist.rs b/bin/ntx-builder/src/actor/allowlist.rs index 3b68cfc56..e586d9983 100644 --- a/bin/ntx-builder/src/actor/allowlist.rs +++ b/bin/ntx-builder/src/actor/allowlist.rs @@ -90,7 +90,7 @@ end"; let note = mock_single_target_note(account_id, 10); let root = note.as_note().script().root(); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([root])) + AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([root])) .expect("non-empty allowlist should construct"), ); @@ -113,7 +113,7 @@ end"; assert_ne!(allowed_root, rejected_root); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([allowed_root])) + AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([allowed_root])) .expect("non-empty allowlist should construct"), ); @@ -138,7 +138,7 @@ end"; assert_ne!(allowed_root, rejected_root); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([allowed_root])) + AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([allowed_root])) .expect("non-empty allowlist should construct"), ); diff --git a/bin/ntx-builder/src/test_utils.rs b/bin/ntx-builder/src/test_utils.rs index 203f0a2f9..d6a11de93 100644 --- a/bin/ntx-builder/src/test_utils.rs +++ b/bin/ntx-builder/src/test_utils.rs @@ -131,7 +131,7 @@ pub fn mock_network_account_update() // The allowlist content is irrelevant here; any non-empty set yields a valid network account. let root = mock_single_target_note(mock_network_account_id(), 1).as_note().script().root(); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([root])) + AuthNetworkAccount::with_allowlist(BTreeSet::from_iter([root])) .expect("non-empty allowlist should construct"), ); let details = AccountUpdateDetails::Delta( diff --git a/bin/stress-test/src/seeding/mod.rs b/bin/stress-test/src/seeding/mod.rs index cfe56b3bf..c64334761 100644 --- a/bin/stress-test/src/seeding/mod.rs +++ b/bin/stress-test/src/seeding/mod.rs @@ -16,7 +16,7 @@ use miden_protocol::account::{ AccountComponentMetadata, AccountDelta, AccountId, - AccountStorageDelta, + AccountStoragePatch, AccountType, AccountVaultDelta, StorageMap, @@ -708,7 +708,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/bin/stress-test/src/store/mod.rs b/bin/stress-test/src/store/mod.rs index a028d8ff0..02a091566 100644 --- a/bin/stress-test/src/store/mod.rs +++ b/bin/stress-test/src/store/mod.rs @@ -233,7 +233,7 @@ pub async fn bench_sync_notes(data_directory: PathBuf, iterations: usize, concur let store_state = start_store(data_directory).await; // Get the latest block number to determine the range - let chain_tip = store_state.chain_tip(Finality::Committed).await.as_u32(); + let chain_tip = store_state.chain_tip(Finality::Committed).as_u32(); // each request will have `ACCOUNTS_PER_SYNC_NOTES` note tags and will be sent with block number // 0. @@ -305,7 +305,7 @@ pub async fn bench_sync_nullifiers( .collect(); // Get the latest block number to determine the range - let chain_tip = store_state.chain_tip(Finality::Committed).await.as_u32(); + let chain_tip = store_state.chain_tip(Finality::Committed).as_u32(); // Get all nullifier prefixes from the store using sync_notes let mut nullifier_prefixes: Vec = vec![]; @@ -439,7 +439,7 @@ pub async fn bench_sync_transactions( let store_state = start_store(data_directory).await; // Get the latest block number to determine the range - let chain_tip = store_state.chain_tip(Finality::Committed).await.as_u32(); + let chain_tip = store_state.chain_tip(Finality::Committed).as_u32(); // each request will have `accounts_per_request` account ids and will query a range of blocks let request = |_| { @@ -517,7 +517,7 @@ pub async fn sync_transactions( .sync_transactions(account_ids, BlockNumber::from(block_from)..=BlockNumber::from(block_to)) .await .unwrap(); - let chain_tip = state.chain_tip(Finality::Committed).await; + let chain_tip = state.chain_tip(Finality::Committed); let response = proto::rpc::SyncTransactionsResponse { pagination_info: Some(proto::rpc::PaginationInfo { chain_tip: chain_tip.as_u32(), @@ -600,7 +600,7 @@ async fn sync_transactions_paginated( pub async fn bench_sync_chain_mmr(data_directory: PathBuf, iterations: usize, concurrency: usize) { let store_state = start_store(data_directory).await; - let chain_tip = store_state.chain_tip(Finality::Committed).await.as_u32(); + let chain_tip = store_state.chain_tip(Finality::Committed).as_u32(); let request = |_| { let state = Arc::clone(&store_state); @@ -629,7 +629,7 @@ pub async fn bench_sync_chain_mmr(data_directory: PathBuf, iterations: usize, co /// - the response. async fn sync_chain_mmr(state: &Arc, current_client_block_height: u32) -> SyncChainMmrRun { let start = Instant::now(); - let chain_tip = state.chain_tip(Finality::Committed).await; + let chain_tip = state.chain_tip(Finality::Committed); state .sync_chain_mmr(BlockNumber::from(current_client_block_height)..=chain_tip) .await diff --git a/crates/block-producer/src/proof_scheduler.rs b/crates/block-producer/src/proof_scheduler.rs index 81c410546..56dcf79a1 100644 --- a/crates/block-producer/src/proof_scheduler.rs +++ b/crates/block-producer/src/proof_scheduler.rs @@ -113,7 +113,7 @@ pub(crate) async fn run( // Next block number to schedule. Initialized from the proven tip's child so we skip // already-proven blocks on restart. - let mut next_to_prove = state.chain_tip(Finality::Proven).await.child(); + let mut next_to_prove = state.chain_tip(Finality::Proven).child(); // Completed proofs waiting to be committed in order. let mut pending: BTreeMap> = BTreeMap::new(); @@ -135,7 +135,7 @@ pub(crate) async fn run( // Drain completed proofs in ascending order so the proven tip advances without // gaps. - let mut next = state.chain_tip(Finality::Proven).await.child(); + let mut next = state.chain_tip(Finality::Proven).child(); while let Some(proof_bytes) = pending.remove(&next) { state.apply_proof(next, proof_bytes).await?; next = next.child(); diff --git a/crates/block-producer/src/rpc_sync.rs b/crates/block-producer/src/rpc_sync.rs index 4f69cf11b..813a52fe2 100644 --- a/crates/block-producer/src/rpc_sync.rs +++ b/crates/block-producer/src/rpc_sync.rs @@ -75,7 +75,7 @@ impl BlockSync { } async fn sync(&self) -> 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, "Connecting to upstream RPC for blocks"); let mut client = self.source_rpc.clone(); @@ -121,7 +121,7 @@ impl ProofSync { async fn sync(&self) -> anyhow::Result<()> { // Subscribe from next proven tip. - let starting_block = self.state.chain_tip(Finality::Proven).await.child().as_u32(); + let starting_block = self.state.chain_tip(Finality::Proven).child().as_u32(); info!(starting_block, "Connecting to upstream RPC for proofs"); let mut client = self.source_rpc.clone(); let mut stream = client diff --git a/crates/block-producer/src/server/mod.rs b/crates/block-producer/src/server/mod.rs index 7859fd4f6..220aeb28b 100644 --- a/crates/block-producer/src/server/mod.rs +++ b/crates/block-producer/src/server/mod.rs @@ -98,11 +98,11 @@ pub struct Sequencer { impl Sequencer { /// Spawns the sequencer tasks and returns its in-process API. - pub async fn spawn(self) -> Result { + pub fn spawn(self) -> Result { info!(target: COMPONENT, "Initializing sequencer"); let store = self.store; let validator = BlockProducerValidatorClient::new(self.validator_url.clone()); - let chain_tip = store.chain_tip(Finality::Committed).await; + let chain_tip = store.chain_tip(Finality::Committed); info!(target: COMPONENT, "Sequencer initialized"); @@ -159,7 +159,7 @@ impl Sequencer { /// Executes in place (i.e. not spawned) and will run indefinitely until a fatal error is /// encountered. pub async fn serve(self) -> anyhow::Result<()> { - self.spawn().await?.wait().await + self.spawn()?.wait().await } } diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index da9a50db1..3afd41779 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -36,7 +36,6 @@ async fn block_producer_starts_with_store_state() { mempool_tx_capacity: NonZeroUsize::new(100).unwrap(), } .spawn() - .await .unwrap(); let status = block_producer.api().status().await; diff --git a/crates/block-producer/src/store/mod.rs b/crates/block-producer/src/store/mod.rs index dbd896e8e..55b57ba8c 100644 --- a/crates/block-producer/src/store/mod.rs +++ b/crates/block-producer/src/store/mod.rs @@ -119,7 +119,7 @@ pub async fn get_tx_inputs( return Err(StoreError::DuplicateAccountIdPrefix(proven_tx.account_id())); } - let current_block_height = state.chain_tip(Finality::Committed).await; + let current_block_height = state.chain_tip(Finality::Committed); let tx_inputs = TransactionInputs::from_store_inputs( proven_tx.account_id(), store_inputs, diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 45dc2a79c..2750e1697 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -317,7 +317,7 @@ impl api_server::Api for RpcService { block_num: nullifier_info.block_num.as_u32(), }) .collect(); - let chain_tip = self.store.chain_tip(Finality::Committed).await; + let chain_tip = self.store.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncNullifiersResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -397,9 +397,9 @@ impl api_server::Api for RpcService { let current_client_block_height = BlockNumber::from(request.current_client_block_height); let sync_target = match request.finality_level() { proto::rpc::FinalityLevel::Committed | proto::rpc::FinalityLevel::Unspecified => { - self.store.chain_tip(Finality::Committed).await + self.store.chain_tip(Finality::Committed) }, - proto::rpc::FinalityLevel::Proven => self.store.chain_tip(Finality::Proven).await, + proto::rpc::FinalityLevel::Proven => self.store.chain_tip(Finality::Proven), }; if current_client_block_height > sync_target { @@ -500,7 +500,7 @@ impl api_server::Api for RpcService { let block_range = range .into_inclusive_range::() .map_err(invalid_block_range_to_status)?; - let chain_tip = self.store.chain_tip(Finality::Committed).await; + let chain_tip = self.store.chain_tip(Finality::Committed); if *block_range.end() > chain_tip { return Err(Status::invalid_argument(format!( "block_to ({}) is greater than chain tip ({chain_tip})", @@ -612,7 +612,7 @@ impl api_server::Api for RpcService { block_num: map_value.block_num.as_u32(), }) .collect(); - let chain_tip = self.store.chain_tip(Finality::Committed).await; + let chain_tip = self.store.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncAccountStorageMapsResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -663,7 +663,7 @@ impl api_server::Api for RpcService { } }) .collect(); - let chain_tip = self.store.chain_tip(Finality::Committed).await; + let chain_tip = self.store.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncAccountVaultResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -973,7 +973,7 @@ impl api_server::Api for RpcService { .map_err(|err| database_error_to_status(&err))?; let transactions = transaction_records_db.into_iter().map(transaction_record_to_proto).collect(); - let chain_tip = self.store.chain_tip(Finality::Committed).await; + let chain_tip = self.store.chain_tip(Finality::Committed); Ok(Response::new(proto::rpc::SyncTransactionsResponse { pagination_info: Some(proto::rpc::PaginationInfo { @@ -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/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 4fc8af544..9a3778335 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; @@ -56,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 { @@ -69,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, @@ -100,8 +103,11 @@ impl TestStore { } 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: @@ -276,8 +282,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"), + ); } } @@ -298,7 +314,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: @@ -485,8 +504,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, @@ -514,17 +537,19 @@ 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").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; let validator_url = Url::parse("http://127.0.0.1:0").unwrap(); let block_producer = BlockProducerApi::new( block_producer_state, @@ -670,17 +695,19 @@ 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. - 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 let validator_url = Url::parse("http://127.0.0.1:0").unwrap(); let block_producer = BlockProducerApi::new( @@ -721,16 +748,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, @@ -738,10 +775,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, @@ -749,9 +790,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, @@ -770,9 +816,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, @@ -788,7 +839,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"); @@ -817,16 +871,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/Cargo.toml b/crates/store/Cargo.toml index 30111a1e3..464fa6b68 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -19,6 +19,7 @@ doctest = false [dependencies] anyhow = { workspace = true } +arc-swap = "1" deadpool = { features = ["managed", "rt_tokio_1"], workspace = true } deadpool-diesel = { features = ["sqlite"], workspace = true } diesel = { features = ["numeric", "sqlite"], workspace = true } diff --git a/crates/store/src/account_state_forest/mod.rs b/crates/store/src/account_state_forest/mod.rs index 592518d6b..1809c1ad1 100644 --- a/crates/store/src/account_state_forest/mod.rs +++ b/crates/store/src/account_state_forest/mod.rs @@ -7,7 +7,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, @@ -586,7 +586,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 @@ -741,7 +741,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 042bb71f5..bf6518a9c 100644 --- a/crates/store/src/account_state_forest/tests.rs +++ b/crates/store/src/account_state_forest/tests.rs @@ -34,7 +34,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 @@ -85,7 +85,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(); @@ -109,7 +109,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) @@ -134,7 +134,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(); @@ -142,7 +142,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(); @@ -152,7 +152,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(); @@ -181,7 +181,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(); @@ -221,7 +221,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(); @@ -238,10 +238,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(); @@ -263,13 +263,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()); @@ -314,12 +314,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(); @@ -332,7 +332,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); @@ -353,7 +353,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(); @@ -367,30 +367,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(); @@ -404,7 +404,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]; @@ -419,18 +419,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(); @@ -450,7 +450,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; @@ -466,20 +466,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(); @@ -552,21 +552,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(); @@ -584,7 +584,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(); @@ -593,10 +593,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(); @@ -617,7 +617,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(); @@ -626,10 +626,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(); @@ -674,7 +674,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(); @@ -689,15 +689,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); @@ -737,7 +737,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(); } @@ -749,7 +749,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(); @@ -765,10 +765,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(); @@ -796,12 +796,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(); } @@ -820,7 +820,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(); @@ -829,15 +829,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(); } @@ -856,7 +856,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(); @@ -869,31 +869,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(); @@ -913,7 +913,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(); @@ -930,12 +930,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(); } @@ -971,14 +971,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) @@ -991,7 +991,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/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 7fe96137f..04f27244f 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -26,7 +26,6 @@ use miden_protocol::note::{ }; use miden_protocol::transaction::TransactionHeader; use miden_protocol::utils::serde::Deserializable; -use tokio::sync::oneshot; use tracing::{info, instrument}; use crate::COMPONENT; @@ -472,36 +471,15 @@ 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/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..2c18a7bdf 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -16,17 +16,17 @@ use miden_protocol::account::{ AccountId, AccountIdVersion, AccountStorage, - AccountStorageDelta, AccountStorageHeader, + AccountStoragePatch, AccountType, AccountVaultDelta, StorageMap, - StorageMapDelta, StorageMapKey, + StorageMapPatch, StorageSlot, StorageSlotContent, - StorageSlotDelta, StorageSlotName, + StorageSlotPatch, StorageSlotType, }; use miden_protocol::block::{BlockAccountUpdate, BlockHeader, BlockNumber}; @@ -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 9f10b233d..96b2c6ac1 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -15,14 +15,14 @@ use miden_protocol::account::{ AccountDelta, AccountId, AccountIdVersion, - AccountStorageDelta, + AccountStoragePatch, AccountType, AccountVaultDelta, StorageMapKey, StorageSlot, StorageSlotContent, - StorageSlotDelta, StorageSlotName, + StorageSlotPatch, }; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::block::{ @@ -978,7 +978,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; @@ -1002,11 +1002,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); @@ -1021,10 +1021,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, @@ -2915,7 +2915,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; @@ -3003,15 +3003,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(); @@ -3030,14 +3030,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(), @@ -3060,14 +3060,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(), @@ -3109,7 +3109,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, @@ -3136,13 +3136,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 = @@ -3175,11 +3175,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(), @@ -3230,7 +3230,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}; @@ -3249,12 +3249,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(); @@ -3299,11 +3299,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(); @@ -3349,7 +3349,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(); @@ -3473,7 +3473,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; @@ -3486,11 +3486,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) @@ -3527,7 +3527,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; @@ -3540,8 +3540,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) @@ -3578,7 +3578,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; @@ -3601,18 +3601,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(); @@ -3626,12 +3626,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/errors.rs b/crates/store/src/errors.rs index 9fcb7e03b..b24edb189 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -188,10 +188,12 @@ 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("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")] + 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/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/agglayer_faucet_eth.mac b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_eth.mac index 391dd0e15..3a3049f6d 100644 Binary files a/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_eth.mac and b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_eth.mac differ diff --git a/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac index b4c9b0c99..dee7fa660 100644 Binary files a/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac and b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac differ 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 75fd37e32..6879144f0 100644 Binary files a/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac and b/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac differ diff --git a/crates/store/src/state/account.rs b/crates/store/src/state/account.rs index 19663dcc7..c9b54c953 100644 --- a/crates/store/src/state/account.rs +++ b/crates/store/src/state/account.rs @@ -24,12 +24,13 @@ use miden_protocol::account::{ }; use miden_protocol::block::BlockNumber; use miden_protocol::block::account_tree::AccountWitness; -use tracing::{Instrument, instrument}; +use tracing::instrument; 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,13 +190,8 @@ impl State { } // 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 { - return Err(GetAccountError::UnknownBlock(block_num)); - } + if block_num > self.chain_tip(Finality::Committed) { + return Err(GetAccountError::UnknownBlock(block_num)); } // Query account header and storage header together in a single DB call diff --git a/crates/store/src/state/apply_block.rs b/crates/store/src/state/apply_block.rs index 2992573bc..1b798b35f 100644 --- a/crates/store/src/state/apply_block.rs +++ b/crates/store/src/state/apply_block.rs @@ -1,23 +1,12 @@ -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::batch::OrderedBatches; -use miden_protocol::block::account_tree::AccountMutationSet; -use miden_protocol::block::nullifier_tree::NullifierMutationSet; -use miden_protocol::block::{BlockBody, BlockHeader, BlockInputs, BlockNumber, SignedBlock}; -use miden_protocol::note::{NoteDetails, Nullifier}; -use miden_protocol::transaction::OutputNote; +use miden_protocol::block::{BlockInputs, BlockNumber, SignedBlock}; use miden_protocol::utils::serde::Serializable; -use tokio::sync::oneshot; -use tracing::{Instrument, info, info_span, instrument}; +use tracing::instrument; -use crate::db::NoteRecord; -use crate::errors::{ApplyBlockError, ApplyBlockWithProvingInputsError, InvalidBlockError}; -use crate::state::{BlockNotification, State}; -use crate::{COMPONENT, HistoricalError}; +use crate::COMPONENT; +use crate::errors::{ApplyBlockError, ApplyBlockWithProvingInputsError}; +use crate::state::State; impl State { /// Saves proving inputs for a signed block and applies it to the state. @@ -50,145 +39,11 @@ 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)) - .expect("block cache receives sequential block numbers"); - 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. @@ -201,157 +56,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() { - 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() { - 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(private) => (None, private.attachments().clone(), 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 87b2fdc18..b43bc677b 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -8,6 +8,7 @@ use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::Arc; +use arc_swap::ArcSwap; use miden_node_proto::domain::batch::BatchInputs; use miden_node_utils::clap::StorageOptions; use miden_node_utils::formatting::format_array; @@ -17,21 +18,18 @@ 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::{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, AccountStateForestBackend}; 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; @@ -45,15 +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, - 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; @@ -63,11 +55,8 @@ mod account; mod subscription; pub use subscription::{ - BlockSubscriptionEvent, - BlockSubscriptionStream, - ProofSubscriptionEvent, - ProofSubscriptionStream, - StateSubscriptionError, + BlockSubscriptionEvent, BlockSubscriptionStream, ProofSubscriptionEvent, + ProofSubscriptionStream, StateSubscriptionError, }; mod apply_block; @@ -75,6 +64,8 @@ mod apply_proof; mod bootstrap; mod disk_monitor; mod sync_state; +pub(crate) mod writer; +use writer::{BlockWriter, WriteHandle}; // FINALITY // ================================================================================================ @@ -106,23 +97,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 @@ -140,31 +123,31 @@ 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<()>, + // 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>>, /// The latest proven-in-sequence block number, updated by the proof scheduler or `apply_proof`. proven_tip: ProvenTipWriter, /// 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, } @@ -173,9 +156,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, @@ -186,9 +166,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, @@ -229,13 +206,13 @@ 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 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); @@ -245,35 +222,66 @@ 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 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(), + rx: write_rx, + nullifier_tree, + account_tree, + blockchain, + }; + tokio::spawn(block_writer.run()); Ok(Self { data_directory: data_path.to_path_buf(), db, block_store, - inner, + in_memory, + write_handle, forest, - writer, proven_tip, committed_tip_tx, - block_cache: BlockCache::new(BLOCK_CACHE_CAPACITY), - proof_cache: ProofCache::new(PROOF_CACHE_CAPACITY), + block_cache, + proof_cache, }) } @@ -295,42 +303,31 @@ 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 { + // HELPER FUNCTIONS TO AVOID BLOCKING CALLS IN ASYNC CONTEXT + // -------------------------------------------------------------------------------------------- + + /// Runs a synchronous operation over the current in-memory state snapshot on Tokio's blocking + /// path. + pub(crate) fn with_inner_read_blocking(&self, f: impl FnOnce(&InMemoryState) -> R) -> R { let span = Span::current(); tokio::task::block_in_place(|| { span.in_scope(|| { - let mut inner = self.inner.blocking_write(); - f(&mut inner) + let snapshot = self.snapshot(); + f(&snapshot) }) }) } /// 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, @@ -344,22 +341,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 // -------------------------------------------------------------------------------------------- @@ -376,8 +357,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 inner = self.inner.read().await; - let mmr_proof = inner.blockchain.open(header.block_num())?; + let mmr_proof = self.snapshot().blockchain.open(header.block_num())?; Some(mmr_proof) } else { None @@ -389,9 +369,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, @@ -400,23 +377,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, @@ -426,33 +386,26 @@ 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()); + 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 (batch_reference_block, partial_mmr) = { - let inner_state = self.inner.read().await; + let snapshot = self.snapshot(); + let latest_block_num = snapshot.block_num; - let latest_block_num = inner_state.latest_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, @@ -460,19 +413,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"); @@ -480,15 +424,16 @@ 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))) + .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() @@ -497,17 +442,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"); @@ -526,36 +462,32 @@ 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()); + 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))) + .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() @@ -564,17 +496,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"); @@ -587,65 +510,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, - }); - } - - // 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)) + 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); + if highest_block_number > latest_block_number { + return Err(GetBlockInputsError::UnknownBatchBlockReference { + highest_block_number, + latest_block_number, + }); + } + + 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)) + }) }) } @@ -659,32 +565,46 @@ 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 { + 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() { + 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() + }); + } + + 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, - ..Default::default() - }); - } - - let nullifiers = nullifiers - .iter() - .map(|nullifier| NullifierInfo { - nullifier: *nullifier, - block_num: inner.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, @@ -709,22 +629,20 @@ 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. /// - /// - [`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.snapshot().block_num, Finality::Proven => self.proven_tip.read(), } } @@ -734,10 +652,13 @@ 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) + 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. @@ -745,10 +666,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) + 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/sync_state.rs b/crates/store/src/state/sync_state.rs index d2f8fc094..820549a5e 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,11 @@ 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..4153ab720 --- /dev/null +++ b/crates/store/src/state/writer.rs @@ -0,0 +1,360 @@ +//! 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_crypto::Word; +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::{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. +/// +/// 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, + 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, + 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.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 write_block(&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(); + + // 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() { + AccountUpdateDetails::Delta(delta) => Some(delta.clone()), + AccountUpdateDetails::Private => None, + }, + )) + }); + + // Apply in-memory mutations. + let (snapshot, notes) = tokio::task::block_in_place(|| { + let notes = Self::build_note_records(header, body)?; + + let (_, nullifier_tree_update, _, account_tree_update) = + self.compute_tree_mutations(header, body)?; + + 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); + + 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(), // TODO(sergerad): snapshot of blockchain? + }); + + 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)) + .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"); + + 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> { + // 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)] + fn compute_tree_mutations( + &self, + header: &BlockHeader, + body: &BlockBody, + ) -> Result<(Word, NullifierMutationSet, Word, AccountMutationSet), ApplyBlockError> { + let block_num = header.block_num(); + + // nullifiers can be produced only once + 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()); + } + + // new_block.chain_root must be equal to the chain MMR root prior to the update + let peaks = self.blockchain.peaks(); + if peaks.hash_peaks() != header.chain_commitment() { + return Err(InvalidBlockError::NewBlockInvalidChainCommitment.into()); + } + + // compute update for nullifier tree + 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() { + return Err(InvalidBlockError::NewBlockInvalidNullifierRoot.into()); + } + + // compute update for account tree + 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() { + return Err(InvalidBlockError::NewBlockInvalidAccountRoot.into()); + } + + Ok(( + self.nullifier_tree.root(), + nullifier_tree_update, + self.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(private) => (None, private.attachments().clone(), 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) + } +}