From de0c67803b34f4857c4b6b1e56f5f86776cd88d0 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Mon, 18 May 2026 14:53:58 -0300 Subject: [PATCH 1/3] chore: update miden-protocol --- CHANGELOG.md | 1 + Cargo.lock | 330 ++++---- Cargo.toml | 2 +- bin/genesis/src/main.rs | 8 +- bin/network-monitor/src/counter.rs | 10 +- bin/network-monitor/src/deploy/counter.rs | 2 +- bin/ntx-builder/src/actor/mod.rs | 4 +- bin/ntx-builder/src/builder.rs | 21 +- bin/ntx-builder/src/clients/store.rs | 20 +- bin/ntx-builder/src/coordinator.rs | 16 +- .../src/db/models/account_effect.rs | 36 +- bin/ntx-builder/src/db/models/conv.rs | 3 +- bin/ntx-builder/src/db/models/queries/mod.rs | 8 +- .../src/db/models/queries/notes.rs | 7 +- bin/ntx-builder/src/test_utils.rs | 12 +- bin/stress-test/src/seeding/mod.rs | 37 +- bin/stress-test/src/seeding/tests.rs | 12 +- bin/stress-test/src/store/mod.rs | 2 +- bin/validator/src/commands/mod.rs | 6 +- bin/validator/src/db/models.rs | 2 +- bin/validator/src/signers/mod.rs | 6 +- .../block-producer/src/block_builder/mod.rs | 3 +- .../block-producer/src/domain/transaction.rs | 9 +- .../src/mempool/subscription.rs | 12 +- crates/block-producer/src/store/mod.rs | 2 +- .../src/test_utils/proven_tx.rs | 2 +- crates/db/src/conv.rs | 2 +- .../large-smt-backend-rocksdb/src/helpers.rs | 65 +- crates/large-smt-backend-rocksdb/src/lib.rs | 2 + .../large-smt-backend-rocksdb/src/rocksdb.rs | 722 +++++++++++------- crates/proto/src/domain/account.rs | 21 +- crates/proto/src/domain/digest.rs | 8 +- crates/proto/src/domain/merkle.rs | 2 +- crates/proto/src/domain/note.rs | 3 +- crates/rpc/src/server/api.rs | 60 +- crates/rpc/src/tests.rs | 106 ++- crates/store/build.rs | 18 +- crates/store/src/account_state_forest/mod.rs | 19 +- crates/store/src/db/mod.rs | 12 + crates/store/src/db/models/conv.rs | 2 +- .../store/src/db/models/queries/accounts.rs | 107 ++- .../src/db/models/queries/accounts/delta.rs | 2 +- .../db/models/queries/accounts/delta/tests.rs | 79 +- .../src/db/models/queries/accounts/tests.rs | 143 +++- .../src/db/models/queries/transactions.rs | 9 +- crates/store/src/db/tests.rs | 149 ++-- crates/store/src/genesis/config/errors.rs | 3 + crates/store/src/genesis/config/mod.rs | 23 +- .../agglayer_faucet_eth.mac | Bin 14803 -> 21004 bytes .../agglayer_faucet_usdc.mac | Bin 14803 -> 21004 bytes .../samples/02-with-account-files/bridge.mac | Bin 31421 -> 34376 bytes crates/store/src/genesis/config/tests.rs | 36 +- crates/store/src/genesis/mod.rs | 4 +- crates/store/src/lib.rs | 43 ++ crates/store/src/server/api.rs | 2 +- crates/store/src/server/rpc_api.rs | 14 +- crates/store/src/state/apply_block.rs | 2 +- crates/store/src/state/loader.rs | 9 +- crates/store/src/state/mod.rs | 12 +- crates/store/src/state/sync_state.rs | 7 +- crates/utils/src/crypto.rs | 2 +- proto/proto/internal/store.proto | 19 + proto/proto/types/account.proto | 6 + 63 files changed, 1434 insertions(+), 852 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bc8f0b3d..edca7c29eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - [BREAKING] Changed `SyncChainMmr` endpoint: the upper end of the block range we're syncing is now the chain tip with the requested finality level. Validator signature is also returned ([#2075](https://github.com/0xMiden/node/pull/2075)). - [BREAKING] Renamed `SubmitProvenTransaction` RPC endpoint to `SubmitProvenTx` ([#2094](https://github.com/0xMiden/node/pull/2094)). - [BREAKING] Renamed `SubmitProvenBatch` RPC endpoint to `SubmitProvenTxBatch` ([#2094](https://github.com/0xMiden/node/pull/2094)). +- Updated `miden-protocol` and bumped `miden-crypto` to `v0.25`. `AccountId::is_network()` was removed upstream, so `SubmitProvenTx` and `SubmitProvenTxBatch` now consult the store to classify post-deployment public-account transactions as network accounts. ## v0.14.10 (2026-05-29) diff --git a/Cargo.lock b/Cargo.lock index e6818092a4..96df84f824 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]] @@ -1225,6 +1225,12 @@ dependencies = [ "itertools 0.13.0", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1678,7 +1684,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]] @@ -2913,10 +2919,21 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "miden-ace-codegen" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c5e3d08008b5f9dd3e6145e55e4d3c1a4e47a74383dd8d0d64003455ee94510" +dependencies = [ + "miden-core", + "miden-crypto", + "thiserror 2.0.18", +] + [[package]] name = "miden-agglayer" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "alloy-sol-types", "fs-err", @@ -2936,22 +2953,25 @@ dependencies = [ [[package]] name = "miden-air" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15646ebc95906b2a7cb66711d1e184f53fd6edc2605730bbcf0c2a129f792cf" +checksum = "d980fa23010e53c43077cf0182e6e88acd1a099abbf8a98cd298aa69b86688dc" dependencies = [ + "miden-ace-codegen", "miden-core", "miden-crypto", + "miden-lifted-stark", "miden-utils-indexing", + "proptest", "thiserror 2.0.18", "tracing", ] [[package]] name = "miden-assembly" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6013b3a390e0dcb29242f4480a7727965887bbf0903466c88f362b4cb20c0e" +checksum = "24eb53abd723b7dd8ff1210b7d126f0d9e1d9127ea0e7f6a46471845d060cc43" dependencies = [ "env_logger", "log", @@ -2960,15 +2980,16 @@ dependencies = [ "miden-mast-package", "miden-package-registry", "miden-project", + "proptest", "smallvec", "thiserror 2.0.18", ] [[package]] name = "miden-assembly-syntax" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "996156b8f7c5fe6be17dea71089c6d7985c2dec1e3a4fec068b1dfc690e25df5" +checksum = "02ead435c8dccfd3b9bd97779cfa0d74cc14c767a9740f162bf7cd49a0da26bd" dependencies = [ "aho-corasick", "env_logger", @@ -2992,7 +3013,7 @@ dependencies = [ [[package]] name = "miden-block-prover" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "miden-protocol", "thiserror 2.0.18", @@ -3000,12 +3021,12 @@ dependencies = [ [[package]] name = "miden-core" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdec54a321cdf3d23e9ef615e91cb858038c6b4d4202507bdec048fc6d7763e4" +checksum = "9a7c7eecda385bcc66aea99ee3279ae7c78f38e3541f4c1d67d309ac32314ff4" dependencies = [ "derive_more", - "itertools 0.14.0", + "log", "miden-crypto", "miden-debug-types", "miden-formatting", @@ -3022,9 +3043,9 @@ dependencies = [ [[package]] name = "miden-core-lib" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "621e8fa911a790bcf3cd3aedce80bc10922a19d6181f08ff3ca078f955cff70b" +checksum = "99bc06c94dcb75e3e44220fe7623ab99585e7828f19b80534c48a9971d31bfd8" dependencies = [ "env_logger", "fs-err", @@ -3039,24 +3060,26 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.23.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0a034a460e27723dcfdf25effffab84331c3b46b13e7a1bd674197cc71bfe" +checksum = "72ae084a6d15d5d862760bcbdbb5caad41415ed455cd7ab2b53a012d21a431ed" dependencies = [ "blake3", "cc", "chacha20poly1305", "curve25519-dalek", + "der", "ed25519-dalek", "flume", - "glob", "hkdf", "k256", "miden-crypto-derive", "miden-field", + "miden-lifted-stark", "miden-serde-utils", "num", "num-complex", + "once_cell", "p3-blake3", "p3-challenger", "p3-dft", @@ -3064,7 +3087,6 @@ dependencies = [ "p3-keccak", "p3-matrix", "p3-maybe-rayon", - "p3-miden-lifted-stark", "p3-symmetric", "p3-util", "rand 0.9.2", @@ -3083,9 +3105,9 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.23.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8bf6ebde028e79bcc61a3632d2f375a5cc64caa17d014459f75015238cb1e08" +checksum = "e8523f6ac9b28782ca759920e536164e7eab7dbfaf9132721ca3e325c060df73" dependencies = [ "quote", "syn 2.0.117", @@ -3093,9 +3115,9 @@ dependencies = [ [[package]] name = "miden-debug-types" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e50274d11c80b901cf6c90362de8c98c8c8ad6030c80624d683b63d899a0fb" +checksum = "206f5346bef744da1ffae9874a22170a2d0c27dd20947d89182d16f5d86348f7" dependencies = [ "memchr", "miden-crypto", @@ -3104,6 +3126,7 @@ dependencies = [ "miden-utils-indexing", "miden-utils-sync", "paste", + "proptest", "serde", "serde_spanned", "thiserror 2.0.18", @@ -3111,15 +3134,16 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.23.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38011348f4fb4c9e5ce1f471203d024721c00e3b60a91aa91aaefe6738d8b5ea" +checksum = "7b8a48ba526c353bf583bba18ddf62ad4846945e85fcaf44614633276416b5d9" dependencies = [ "miden-serde-utils", "num-bigint", "p3-challenger", "p3-field", "p3-goldilocks", + "p3-util", "paste", "rand 0.10.0", "serde", @@ -3165,13 +3189,48 @@ dependencies = [ "rocksdb", ] +[[package]] +name = "miden-lifted-air" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00d97ada3bfd70d6cc4f1a13ced8cb509f72fb16b1b28c292366aee846d3220" +dependencies = [ + "p3-air", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror 2.0.18", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3601a30d39e08a31ecc5b5215c87b11623942d9610aebccda89a4a5bf757c4" +dependencies = [ + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-goldilocks", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.0", + "serde", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "miden-mast-package" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8b2e3447fcde1f0e6b76e5219f129517639772cb02ca543177f0584e315288" +checksum = "3ae69cd4dc25b0993eb5fafcc99987abd3502d8a7a902248af4a597537dce600" dependencies = [ - "derive_more", "miden-assembly-syntax", "miden-core", "miden-debug-types", @@ -3537,13 +3596,14 @@ dependencies = [ [[package]] name = "miden-package-registry" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969ba3942052e52b3968e34dbd1c52c707e75777ee42ebdae2c8f57af56cf6cf" +checksum = "8c1d77c6f5942f3111fc56a19d638076ea77df3628efa97c7955259c609ca8dd" dependencies = [ "miden-assembly-syntax", "miden-core", "miden-mast-package", + "proptest", "pubgrub", "serde", "smallvec", @@ -3552,9 +3612,9 @@ dependencies = [ [[package]] name = "miden-processor" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ec6cecbf22bd92b73a931ee80b424e46b8b7cdf4f2f3c364c25c5c15d2840da" +checksum = "70508a4a75989a47f03a0681df412b80040c76230c3d850f8f2b80f7a986d4aa" dependencies = [ "itertools 0.14.0", "miden-air", @@ -3565,20 +3625,20 @@ dependencies = [ "paste", "rayon", "thiserror 2.0.18", - "tokio", "tracing", ] [[package]] name = "miden-project" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3840520c01881534fbbceb6b3687ec1c407fbaf310a35ce415fd3510abc52fdb" +checksum = "6338d92713845f57b137e4305db11ee8614b0cec9b1516470233ce64d53f6376" dependencies = [ "miden-assembly-syntax", "miden-core", "miden-mast-package", "miden-package-registry", + "proptest", "serde", "serde-untagged", "thiserror 2.0.18", @@ -3588,7 +3648,7 @@ dependencies = [ [[package]] name = "miden-protocol" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "bech32", "fs-err", @@ -3616,19 +3676,16 @@ dependencies = [ [[package]] name = "miden-prover" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb2c94e36f57684d7fa0cd382adeedc1728d502dbbe69ad1c12f4a931f45511" +checksum = "c59b5dbdac3a8596fc201de028c17ed5060ad554901259222420afd186f2b467" dependencies = [ "bincode", "miden-air", "miden-core", "miden-crypto", - "miden-debug-types", "miden-processor", "serde", - "thiserror 2.0.18", - "tokio", "tracing", ] @@ -3683,9 +3740,9 @@ dependencies = [ [[package]] name = "miden-serde-utils" -version = "0.23.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff78082e9b4ca89863e68da01b35f8a4029ee6fd912e39fa41fde4273a7debab" +checksum = "d899afcfbf85c851522f2dff73db1064116486fb8e0ebce0ade44ce72dadc075" dependencies = [ "p3-field", "p3-goldilocks", @@ -3694,7 +3751,7 @@ dependencies = [ [[package]] name = "miden-standards" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "bon", "fs-err", @@ -3708,10 +3765,32 @@ dependencies = [ "walkdir", ] +[[package]] +name = "miden-stark-transcript" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0e1f55bdff89f12ec6fc3881660a0d51d4e77f66026ab0e6ddcbd098a16f2f" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0069bab01139a5660c7189589c95dfedf8449514d03641fd3f9d049e1b031f9" +dependencies = [ + "p3-field", + "p3-symmetric", +] + [[package]] name = "miden-testing" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3731,7 +3810,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "miden-processor", "miden-protocol", @@ -3744,7 +3823,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#9160eee3eea8b79bbff9ff1113c535743d6c693c" +source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" dependencies = [ "miden-protocol", "miden-tx", @@ -3752,9 +3831,9 @@ dependencies = [ [[package]] name = "miden-utils-core-derive" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3846c8674ccec0c37005f99c1a599a24790ba2a5e5f4e1c7aec5f456821df835" +checksum = "9f74ef56bd44d23c805f662cd4f4639b04df6e0204d78806ede0e6f93d9695a5" dependencies = [ "proc-macro2", "quote", @@ -3763,33 +3842,33 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397f5d1e8679cf17cf7713ffd9654840791a6ed5818b025bbc2fbfdce846579a" +checksum = "c7b3aa61e0ee0f8a5b3f44250b73879437f9e10b6061c3ae743fd1cdb1f56321" dependencies = [ "miden-crypto", "miden-debug-types", "miden-miette", - "paste", "tracing", ] [[package]] name = "miden-utils-indexing" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8834e76299686bcce3de1685158aa4cff49b7fa5e0e00a6cc811e8f2cf5775f" +checksum = "57419a0557e580e04125542f4383910fb997660633e15ad4570ce1ff2ae80557" dependencies = [ "miden-crypto", + "proptest", "serde", "thiserror 2.0.18", ] [[package]] name = "miden-utils-sync" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9e9747e9664c1a0997bb040ae291306ea0a1c74a572141ec66cec855c1b0e8" +checksum = "5a8a579c0c7cf44c123129045339f53b1f26f0aa2dc508494e97360d3ccab80e" dependencies = [ "lock_api", "loom", @@ -3829,9 +3908,9 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4580df640d889c9f3c349cd2268968e44a99a8cf0df6c36ae5b1fb273712b00" +checksum = "49cd4ca9a8b90c48d5fc6f5707c46cc44b17285f2f705cbe44e5b56c430a85c8" dependencies = [ "bincode", "miden-air", @@ -3844,9 +3923,9 @@ dependencies = [ [[package]] name = "midenc-hir-type" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb29d7c049fb69373c7e775e3d4411e63e4ee608bc43826282ba62c6ec9f891" +checksum = "1ff0511aa2201f7098995e38a3c97a319d379c3b2d26fb83677b21b71f61a7b4" dependencies = [ "miden-formatting", "miden-serde-utils", @@ -3988,7 +4067,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]] @@ -4106,6 +4185,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -4239,19 +4322,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "p3-commit" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916ae7989d5c3b49f887f5c55b2f9826bdbb81aaebf834503c4145d8b267c829" -dependencies = [ - "itertools 0.14.0", - "p3-field", - "p3-matrix", - "p3-util", - "serde", -] - [[package]] name = "p3-dft" version = "0.5.2" @@ -4351,102 +4421,6 @@ dependencies = [ "rand 0.10.0", ] -[[package]] -name = "p3-miden-lifted-air" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c31c65fdc88952d7b301546add9670676e5b878aa0066dd929f107c203b006" -dependencies = [ - "p3-air", - "p3-field", - "p3-matrix", - "p3-util", - "thiserror 2.0.18", -] - -[[package]] -name = "p3-miden-lifted-fri" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9932f1b0a16609a45cd4ee10a4d35412728bc4b38837c7979d7c85d8dcc9fc" -dependencies = [ - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-miden-lmcs", - "p3-miden-transcript", - "p3-util", - "rand 0.10.0", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "p3-miden-lifted-stark" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3956ab7270c3cdd53ca9796d39ae1821984eb977415b0672110f9666bff5d8" -dependencies = [ - "p3-challenger", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-miden-lifted-air", - "p3-miden-lifted-fri", - "p3-miden-lmcs", - "p3-miden-stateful-hasher", - "p3-miden-transcript", - "p3-util", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "p3-miden-lmcs" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c46791c983e772136db3d48f102431457451447abb9087deb6c8ce3c1efc86" -dependencies = [ - "p3-commit", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-miden-stateful-hasher", - "p3-miden-transcript", - "p3-symmetric", - "p3-util", - "rand 0.10.0", - "serde", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "p3-miden-stateful-hasher" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec47a9d9615eb3d9d2a59b00d19751d9ad85384b55886827913d680d912eac6a" -dependencies = [ - "p3-field", - "p3-symmetric", -] - -[[package]] -name = "p3-miden-transcript" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c565647487e4a949f67e6f115b0391d6cb82ac8e561165789939bab23d0ae7" -dependencies = [ - "p3-challenger", - "p3-field", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "p3-monty-31" version = "0.5.2" @@ -5429,7 +5403,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5500,7 +5474,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5908,7 +5882,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]] @@ -6129,7 +6103,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6138,7 +6112,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]] @@ -6157,7 +6131,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]] @@ -7191,7 +7165,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 91c51bc386..39fb69a9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ miden-tx = { branch = "next", default-features = false, git = "http miden-tx-batch-prover = { branch = "next", git = "https://github.com/0xMiden/protocol" } # Other miden dependencies. These should align with those expected by miden-protocol. -miden-crypto = { version = "0.23" } +miden-crypto = { version = "0.25" } # External dependencies anyhow = { version = "1.0" } diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 9ce02cf5f9..09a8661753 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -94,7 +94,7 @@ fn run( // Create bridge account (NoAuth, nonce=0), then bump nonce to 1 for genesis. let mut rng = ChaCha20Rng::from_seed(rand::random()); let bridge_seed: [u64; 4] = rng.random(); - let bridge_seed = Word::from(bridge_seed.map(Felt::new)); + let bridge_seed = Word::from(bridge_seed.map(Felt::new_unchecked)); let bridge = create_bridge_account(bridge_seed, bridge_admin_id, ger_manager_id); // Bump bridge nonce to 1 (required for genesis accounts). File-loaded accounts via [[account]] @@ -158,7 +158,7 @@ path = "bridge.mac" fn generate_falcon_keypair() -> (falcon512_poseidon2::PublicKey, FalconSecretKey) { let mut rng = ChaCha20Rng::from_seed(rand::random()); let auth_seed: [u64; 4] = rng.random(); - let mut coin = RandomCoin::new(Word::from(auth_seed.map(Felt::new))); + let mut coin = RandomCoin::new(Word::from(auth_seed.map(Felt::new_unchecked))); let secret_key = FalconSecretKey::with_rng(&mut coin); let public_key = secret_key.public_key(); (public_key, secret_key) @@ -197,7 +197,7 @@ fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { #[cfg(test)] mod tests { use miden_node_store::genesis::config::GenesisConfig; - use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; + use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::utils::serde::Serializable; use super::*; @@ -208,7 +208,7 @@ mod tests { let bridge_id = AccountFile::read(dir.join("bridge.mac")).unwrap().account.id(); let config = GenesisConfig::read_toml_file(&dir.join("genesis.toml")).unwrap(); - let signer = SecretKey::read_from_bytes(&[0x01; 32]).unwrap(); + let signer = SigningKey::read_from_bytes(&[0x01; 32]).unwrap(); let (state, _) = config.into_state(signer.public_key()).unwrap(); let bridge = state.accounts.iter().find(|a| a.id() == bridge_id).unwrap(); diff --git a/bin/network-monitor/src/counter.rs b/bin/network-monitor/src/counter.rs index 2d829235ba..58b18ce2df 100644 --- a/bin/network-monitor/src/counter.rs +++ b/bin/network-monitor/src/counter.rs @@ -834,7 +834,7 @@ async fn fetch_wallet_account( let storage_details = details.storage_details.context("missing storage details")?; let storage = build_account_storage(storage_details)?; - let account = Account::new(account_id, vault, storage, code, Felt::new(nonce), None) + let account = Account::new(account_id, vault, storage, code, Felt::new_unchecked(nonce), None) .context("failed to create account")?; // Sanity check: verify reconstructed account matches header commitments @@ -952,10 +952,10 @@ fn create_network_note( let partial_metadata = PartialNoteMetadata::new(wallet_account.id(), NoteType::Public); let serial_num = Word::new([ - Felt::new(rng.random()), - Felt::new(rng.random()), - Felt::new(rng.random()), - Felt::new(rng.random()), + Felt::new_unchecked(rng.random()), + Felt::new_unchecked(rng.random()), + Felt::new_unchecked(rng.random()), + Felt::new_unchecked(rng.random()), ]); let recipient = NoteRecipient::new(serial_num, script, NoteStorage::new(vec![])?); diff --git a/bin/network-monitor/src/deploy/counter.rs b/bin/network-monitor/src/deploy/counter.rs index 5479f15898..68635c8bc1 100644 --- a/bin/network-monitor/src/deploy/counter.rs +++ b/bin/network-monitor/src/deploy/counter.rs @@ -64,7 +64,7 @@ pub fn create_counter_account(owner_account_id: AccountId) -> Result { let init_seed: [u8; 32] = rand::random(); let counter_account = AccountBuilder::new(init_seed) .account_type(AccountType::RegularAccountUpdatableCode) - .storage_mode(AccountStorageMode::Network) + .storage_mode(AccountStorageMode::Public) .with_component(account_code) .with_auth_component(incr_nonce_auth) .build()?; diff --git a/bin/ntx-builder/src/actor/mod.rs b/bin/ntx-builder/src/actor/mod.rs index f53815f484..c4474b5bd6 100644 --- a/bin/ntx-builder/src/actor/mod.rs +++ b/bin/ntx-builder/src/actor/mod.rs @@ -112,7 +112,9 @@ impl AccountActorContext { let url = Url::parse("http://127.0.0.1:1").unwrap(); let block_header = mock_block_header(0_u32.into()); - let chain_mmr = PartialMmr::from_peaks(MmrPeaks::new(Forest::new(0), vec![]).unwrap()); + let chain_mmr = PartialMmr::from_peaks( + MmrPeaks::new(Forest::new(0).expect("forest 0 is valid"), vec![]).unwrap(), + ); let chain_state = Arc::new(SharedChainState::new(block_header, chain_mmr)); let (request_tx, _request_rx) = mpsc::channel(1); diff --git a/bin/ntx-builder/src/builder.rs b/bin/ntx-builder/src/builder.rs index a051249601..de94689fb6 100644 --- a/bin/ntx-builder/src/builder.rs +++ b/bin/ntx-builder/src/builder.rs @@ -5,8 +5,10 @@ use anyhow::Context; use futures::Stream; use miden_node_proto::domain::account::NetworkAccountId; use miden_node_proto::domain::mempool::MempoolEvent; +use miden_protocol::account::Account; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::block::BlockHeader; +use miden_standards::account::auth::NetworkAccountNoteAllowlist; use tokio::net::TcpListener; use tokio::sync::mpsc; use tokio::task::JoinSet; @@ -223,13 +225,18 @@ impl NetworkTransactionBuilder { .await .context("failed to write TransactionAdded to DB")?; - // Spawn new actors for newly created network accounts. - if let Some(AccountUpdateDetails::Delta(delta)) = account_delta { - if delta.is_full_state() { - if let Ok(network_id) = NetworkAccountId::try_from(delta.id()) { - self.coordinator.spawn_actor(network_id, &self.actor_context); - } - } + // Spawn new actors for newly created network accounts. A delta carrying full state + // lets us reconstruct the Account and look for the standardized + // `NetworkAccountNoteAllowlist` slot in storage; that is the new protocol-level + // definition of a network account. + if let Some(AccountUpdateDetails::Delta(delta)) = account_delta + && delta.is_full_state() + && let Ok(account) = Account::try_from(delta) + && account.is_public() + && NetworkAccountNoteAllowlist::try_from(account.storage()).is_ok() + { + let network_id = NetworkAccountId::new_trusted(account.id()); + self.coordinator.spawn_actor(network_id, &self.actor_context); } let inactive_targets = self.coordinator.send_targeted(&event); for account_id in inactive_targets { diff --git a/bin/ntx-builder/src/clients/store.rs b/bin/ntx-builder/src/clients/store.rs index c901fc69ab..ae6fad1eae 100644 --- a/bin/ntx-builder/src/clients/store.rs +++ b/bin/ntx-builder/src/clients/store.rs @@ -109,12 +109,14 @@ impl StoreClient { let header = BlockHeader::try_from(block).map_err(StoreError::DeserializationError)?; - let peaks = MmrPeaks::new(Forest::new(header.block_num().as_usize()), peaks) - .map_err(|_| { - StoreError::MalformedResponse( - "returned peaks are not valid for the sent request".into(), - ) - })?; + let forest = Forest::new(header.block_num().as_usize()).map_err(|err| { + StoreError::DeserializationError(ConversionError::from(err).context("forest")) + })?; + let peaks = MmrPeaks::new(forest, peaks).map_err(|_| { + StoreError::MalformedResponse( + "returned peaks are not valid for the sent request".into(), + ) + })?; let partial_mmr = PartialMmr::from_peaks(peaks); @@ -327,11 +329,7 @@ impl StoreClient { ConversionError::from(err).context("account_id"), ) })?; - NetworkAccountId::try_from(account_id).map_err(|_| { - StoreError::MalformedResponse( - "account id is not a valid network account".into(), - ) - }) + Ok(NetworkAccountId::new_trusted(account_id)) }) .collect::, StoreError>>()?; diff --git a/bin/ntx-builder/src/coordinator.rs b/bin/ntx-builder/src/coordinator.rs index d4243cc979..6fac9148cc 100644 --- a/bin/ntx-builder/src/coordinator.rs +++ b/bin/ntx-builder/src/coordinator.rs @@ -274,21 +274,19 @@ impl Coordinator { // transaction has been applied, and in the future also resolves race conditions with // external network transactions (once these are allowed). if let Some(AccountUpdateDetails::Delta(delta)) = account_delta { - let account_id = delta.id(); - if account_id.is_network() { - let network_account_id = - account_id.try_into().expect("account is network account"); - if self.actor_registry.contains_key(&network_account_id) { - target_account_ids.insert(network_account_id); - } + // The actor registry only contains accounts the builder has already classified as + // network. Wrap the id unconditionally and let the registry lookup filter for us; + // unknown accounts simply won't match. + let network_account_id = NetworkAccountId::new_trusted(delta.id()); + if self.actor_registry.contains_key(&network_account_id) { + target_account_ids.insert(network_account_id); } } // Determine target actors for each note. for note in network_notes { let account = note.target_account_id(); - let account = NetworkAccountId::try_from(account) - .expect("network note target account should be a network account"); + let account = NetworkAccountId::new_trusted(account); if self.actor_registry.contains_key(&account) { target_account_ids.insert(account); diff --git a/bin/ntx-builder/src/db/models/account_effect.rs b/bin/ntx-builder/src/db/models/account_effect.rs index 7a6acf0058..5e483035c6 100644 --- a/bin/ntx-builder/src/db/models/account_effect.rs +++ b/bin/ntx-builder/src/db/models/account_effect.rs @@ -1,6 +1,7 @@ use miden_node_proto::domain::account::NetworkAccountId; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::account::{Account, AccountDelta, AccountId}; +use miden_standards::account::auth::NetworkAccountNoteAllowlist; // NETWORK ACCOUNT EFFECT // ================================================================================================ @@ -14,23 +15,34 @@ pub enum NetworkAccountEffect { impl NetworkAccountEffect { pub fn from_protocol(update: &AccountUpdateDetails) -> Option { - let update = match update { - AccountUpdateDetails::Private => return None, + match update { + AccountUpdateDetails::Private => None, AccountUpdateDetails::Delta(update) if update.is_full_state() => { - NetworkAccountEffect::Created( - Account::try_from(update) - .expect("Account should be derivable by full state AccountDelta"), - ) + // Only treat full-state creations as network if the storage carries the + // standardized `NetworkAccountNoteAllowlist` slot. + let account = Account::try_from(update) + .expect("Account should be derivable by full state AccountDelta"); + if account.is_public() + && NetworkAccountNoteAllowlist::try_from(account.storage()).is_ok() + { + Some(NetworkAccountEffect::Created(account)) + } else { + None + } }, - AccountUpdateDetails::Delta(update) => NetworkAccountEffect::Updated(update.clone()), - }; - - update.protocol_account_id().is_network().then_some(update) + AccountUpdateDetails::Delta(update) => { + // Partial updates carry no storage we can inspect here. Forward them as updates and + // let the coordinator's actor registry filter to known network accounts. + Some(NetworkAccountEffect::Updated(update.clone())) + }, + } } pub fn network_account_id(&self) -> NetworkAccountId { - // SAFETY: This is a network account by construction. - self.protocol_account_id().try_into().unwrap() + // Trusted: constructors only produce this enum for accounts already classified as network + // (via the allowlist check above) or for updates that the caller filters through the actor + // registry. + NetworkAccountId::new_trusted(self.protocol_account_id()) } fn protocol_account_id(&self) -> AccountId { diff --git a/bin/ntx-builder/src/db/models/conv.rs b/bin/ntx-builder/src/db/models/conv.rs index 0fed2b9593..61cd9cd772 100644 --- a/bin/ntx-builder/src/db/models/conv.rs +++ b/bin/ntx-builder/src/db/models/conv.rs @@ -58,8 +58,7 @@ pub fn account_id_from_bytes(bytes: &[u8]) -> Result { pub fn network_account_id_from_bytes(bytes: &[u8]) -> Result { let account_id = account_id_from_bytes(bytes)?; - NetworkAccountId::try_from(account_id) - .map_err(|e| DatabaseError::deserialization("network account id", e)) + Ok(NetworkAccountId::new_trusted(account_id)) } pub fn word_to_bytes(word: &Word) -> Vec { diff --git a/bin/ntx-builder/src/db/models/queries/mod.rs b/bin/ntx-builder/src/db/models/queries/mod.rs index f2f97eaf3e..5760f8d675 100644 --- a/bin/ntx-builder/src/db/models/queries/mod.rs +++ b/bin/ntx-builder/src/db/models/queries/mod.rs @@ -146,11 +146,9 @@ pub fn add_transaction( for note in notes { let insert = NoteInsert { nullifier: conversions::nullifier_to_bytes(¬e.as_note().nullifier()), - account_id: conversions::network_account_id_to_bytes( - note.target_account_id() - .try_into() - .expect("network note's target account must be a network account"), - ), + account_id: conversions::network_account_id_to_bytes(NetworkAccountId::new_trusted( + note.target_account_id(), + )), note_data: note.as_note().to_bytes(), note_id: Some(conversions::note_id_to_bytes(¬e.as_note().id())), attempt_count: 0, diff --git a/bin/ntx-builder/src/db/models/queries/notes.rs b/bin/ntx-builder/src/db/models/queries/notes.rs index bb846d7a5e..bb3672feee 100644 --- a/bin/ntx-builder/src/db/models/queries/notes.rs +++ b/bin/ntx-builder/src/db/models/queries/notes.rs @@ -77,10 +77,9 @@ pub fn insert_committed_notes( for note in notes { let row = NoteInsert { nullifier: conversions::nullifier_to_bytes(¬e.as_note().nullifier()), - account_id: conversions::network_account_id_to_bytes( - NetworkAccountId::try_from(note.target_account_id()) - .expect("account ID of a network note should be a network account"), - ), + account_id: conversions::network_account_id_to_bytes(NetworkAccountId::new_trusted( + note.target_account_id(), + )), note_data: note.as_note().to_bytes(), note_id: Some(conversions::note_id_to_bytes(¬e.as_note().id())), attempt_count: 0, diff --git a/bin/ntx-builder/src/test_utils.rs b/bin/ntx-builder/src/test_utils.rs index 1c2c743106..8751ea2bb5 100644 --- a/bin/ntx-builder/src/test_utils.rs +++ b/bin/ntx-builder/src/test_utils.rs @@ -5,7 +5,7 @@ use miden_protocol::Word; use miden_protocol::account::{AccountId, AccountStorageMode, AccountType}; use miden_protocol::block::BlockNumber; use miden_protocol::testing::account_id::{ - ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, AccountIdBuilder, }; use miden_protocol::transaction::TransactionId; @@ -17,17 +17,17 @@ use rand_chacha::rand_core::SeedableRng; /// Creates a network account ID from a test constant. pub fn mock_network_account_id() -> NetworkAccountId { let account_id: AccountId = - ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(); - NetworkAccountId::try_from(account_id).unwrap() + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(); + NetworkAccountId::new_trusted(account_id) } /// Creates a distinct network account ID using a seeded RNG. pub fn mock_network_account_id_seeded(seed: u8) -> NetworkAccountId { let account_id = AccountIdBuilder::new() .account_type(AccountType::RegularAccountImmutableCode) - .storage_mode(AccountStorageMode::Network) + .storage_mode(AccountStorageMode::Public) .build_with_seed([seed; 32]); - NetworkAccountId::try_from(account_id).unwrap() + NetworkAccountId::new_trusted(account_id) } /// Creates a unique `TransactionId` from a seed value. @@ -69,7 +69,7 @@ pub fn mock_account(_account_id: NetworkAccountId) -> miden_protocol::account::A AccountBuilder::new([0u8; 32]) .account_type(AccountType::RegularAccountImmutableCode) - .storage_mode(AccountStorageMode::Network) + .storage_mode(AccountStorageMode::Public) .with_component(MockAccountComponent::with_slots(vec![])) .with_auth_component(NoopAuthComponent) .build_existing() diff --git a/bin/stress-test/src/seeding/mod.rs b/bin/stress-test/src/seeding/mod.rs index bed3d5d34c..6d576a9ca3 100644 --- a/bin/stress-test/src/seeding/mod.rs +++ b/bin/stress-test/src/seeding/mod.rs @@ -28,7 +28,7 @@ use miden_protocol::account::{ StorageSlot, StorageSlotName, }; -use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset, TokenSymbol}; +use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol}; use miden_protocol::batch::{BatchAccountUpdate, BatchId, ProvenBatch}; use miden_protocol::block::{ BlockHeader, @@ -38,11 +38,11 @@ use miden_protocol::block::{ ProposedBlock, SignedBlock, }; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey as EcdsaSecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as EcdsaSecretKey; use miden_protocol::crypto::dsa::falcon512_poseidon2::{PublicKey, SecretKey}; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::errors::AssetError; -use miden_protocol::note::{Note, NoteAssets, NoteHeader, NoteId, NoteInclusionProof}; +use miden_protocol::note::{Note, NoteAssets, NoteId, NoteInclusionProof}; use miden_protocol::transaction::{ InputNote, InputNoteCommitment, @@ -62,7 +62,7 @@ use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ BurnPolicyConfig, MintPolicyConfig, - PolicyAuthority, + PolicyRegistration, TokenPolicyManager, }; use miden_standards::account::wallets::BasicWallet; @@ -205,7 +205,7 @@ async fn generate_blocks( // share random coin seed and key pair for all accounts to avoid key generation overhead let coin_seed: [u64; 4] = rand::rng().random(); - let rng = Arc::new(Mutex::new(RandomCoin::new(coin_seed.map(Felt::new).into()))); + let rng = Arc::new(Mutex::new(RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()))); let key_pair = { let mut rng = rng.lock().unwrap(); SecretKey::with_rng(&mut *rng) @@ -420,7 +420,7 @@ fn create_accounts_and_notes( AccountStorageMode::Public => { asset_faucet_ids.iter().take(vault_entries).copied().collect() }, - AccountStorageMode::Private | AccountStorageMode::Network => vec![asset_faucet_ids[0]], + AccountStorageMode::Private => vec![asset_faucet_ids[0]], }; (0..num_accounts) @@ -573,7 +573,7 @@ fn create_benchmark_faucets(vault_entries: usize) -> Vec { fn create_faucet_with_seed(index: u64) -> Account { let coin_seed: [u64; 4] = rand::rng().random(); - let mut rng = RandomCoin::new(coin_seed.map(Felt::new).into()); + let mut rng = RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()); let key_pair = SecretKey::with_rng(&mut rng); let init_seed: Vec<_> = index.to_be_bytes().into_iter().chain([0u8; 24]).collect(); @@ -582,7 +582,7 @@ fn create_faucet_with_seed(index: u64) -> Account { .name(TokenName::new("TEST").unwrap()) .symbol(token_symbol) .decimals(2) - .max_supply(AssetAmount::new(FungibleAsset::MAX_AMOUNT).unwrap()) + .max_supply(FungibleAsset::MAX_AMOUNT) .build() .unwrap(); @@ -590,11 +590,13 @@ fn create_faucet_with_seed(index: u64) -> Account { .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Private) .with_component(faucet) - .with_components(TokenPolicyManager::new( - PolicyAuthority::AuthControlled, - MintPolicyConfig::AllowAll, - BurnPolicyConfig::AllowAll, - )) + .with_components( + TokenPolicyManager::new() + .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active) + .unwrap() + .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active) + .unwrap(), + ) .with_auth_component(AuthSingleSig::new( key_pair.public_key().into(), AuthScheme::Falcon512Poseidon2, @@ -757,7 +759,10 @@ fn create_emit_note_tx( let slot = faucet.storage().get_item(token_config_slot).unwrap(); faucet .storage_mut() - .set_item(token_config_slot, [slot[0] + Felt::new(10), slot[1], slot[2], slot[3]].into()) + .set_item( + token_config_slot, + [slot[0] + Felt::new_unchecked(10), slot[1], slot[2], slot[3]].into(), + ) .unwrap(); faucet.increment_nonce(ONE).unwrap(); @@ -803,7 +808,7 @@ async fn get_batch_inputs( let batch_inputs = store_client .get_batch_inputs( vec![(block_ref.block_num(), block_ref.commitment())].into_iter(), - notes.iter().map(Note::commitment), + notes.iter().map(|n| n.id().as_word()), ) .await .unwrap(); @@ -826,7 +831,7 @@ async fn get_block_inputs( batch .input_notes() .into_iter() - .filter_map(|note| note.header().map(NoteHeader::to_commitment)) + .filter_map(|note| note.header().map(|h| h.id().as_word())) }), batches.iter().map(ProvenBatch::reference_block_num), ) diff --git a/bin/stress-test/src/seeding/tests.rs b/bin/stress-test/src/seeding/tests.rs index 3084d2a8be..ed4522eb52 100644 --- a/bin/stress-test/src/seeding/tests.rs +++ b/bin/stress-test/src/seeding/tests.rs @@ -11,7 +11,7 @@ fn benchmark_fungible_faucet_ids(vault_entries: usize) -> Vec { #[test] fn public_account_can_be_created_with_large_storage_map() { - let coin_seed = [1, 2, 3, 4].map(Felt::new); + let coin_seed = [1, 2, 3, 4].map(Felt::new_unchecked); let mut rng = RandomCoin::new(coin_seed.into()); let key_pair = SecretKey::with_rng(&mut rng); @@ -33,7 +33,7 @@ fn public_account_can_be_created_with_large_storage_map() { #[test] fn private_account_ignores_large_storage_map_entries() { - let coin_seed = [1, 2, 3, 4].map(Felt::new); + let coin_seed = [1, 2, 3, 4].map(Felt::new_unchecked); let mut rng = RandomCoin::new(coin_seed.into()); let key_pair = SecretKey::with_rng(&mut rng); @@ -50,7 +50,7 @@ fn private_account_ignores_large_storage_map_entries() { #[test] fn public_account_note_contains_requested_distinct_vault_assets() { - let coin_seed = [1, 2, 3, 4].map(Felt::new); + let coin_seed = [1, 2, 3, 4].map(Felt::new_unchecked); let rng = Arc::new(Mutex::new(RandomCoin::new(coin_seed.into()))); let mut key_rng = rng.lock().unwrap(); let key_pair = SecretKey::with_rng(&mut *key_rng); @@ -78,7 +78,7 @@ fn public_account_note_contains_requested_distinct_vault_assets() { #[test] fn private_account_note_keeps_single_vault_asset() { - let coin_seed = [1, 2, 3, 4].map(Felt::new); + let coin_seed = [1, 2, 3, 4].map(Felt::new_unchecked); let rng = Arc::new(Mutex::new(RandomCoin::new(coin_seed.into()))); let mut key_rng = rng.lock().unwrap(); let key_pair = SecretKey::with_rng(&mut *key_rng); @@ -101,7 +101,7 @@ fn private_account_note_keeps_single_vault_asset() { #[test] fn public_account_storage_map_entry_can_be_updated_for_benchmark_blocks() { - let coin_seed = [1, 2, 3, 4].map(Felt::new); + let coin_seed = [1, 2, 3, 4].map(Felt::new_unchecked); let mut rng = RandomCoin::new(coin_seed.into()); let key_pair = SecretKey::with_rng(&mut rng); let mut account = create_account(key_pair.public_key(), 42, AccountStorageMode::Public, 4); @@ -125,7 +125,7 @@ fn public_account_storage_map_entry_can_be_updated_for_benchmark_blocks() { #[test] fn private_account_storage_map_update_is_skipped() { - let coin_seed = [1, 2, 3, 4].map(Felt::new); + let coin_seed = [1, 2, 3, 4].map(Felt::new_unchecked); let mut rng = RandomCoin::new(coin_seed.into()); let key_pair = SecretKey::with_rng(&mut rng); let mut account = create_account(key_pair.public_key(), 42, AccountStorageMode::Private, 4); diff --git a/bin/stress-test/src/store/mod.rs b/bin/stress-test/src/store/mod.rs index 7dfef90b35..98b81e93e2 100644 --- a/bin/stress-test/src/store/mod.rs +++ b/bin/stress-test/src/store/mod.rs @@ -55,7 +55,7 @@ pub async fn bench_get_account( let mut account_ids: Vec = accounts .lines() .map(|a| AccountId::from_hex(a).expect("invalid account id")) - .filter(AccountId::has_public_state) + .filter(AccountId::is_public) .collect(); assert!( diff --git a/bin/validator/src/commands/mod.rs b/bin/validator/src/commands/mod.rs index 8ad6073ac1..96fda9abfa 100644 --- a/bin/validator/src/commands/mod.rs +++ b/bin/validator/src/commands/mod.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use clap::Parser; use miden_node_utils::clap::GrpcOptionsInternal; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::utils::serde::Deserializable; use miden_validator::ValidatorSigner; @@ -158,7 +158,7 @@ impl ValidatorCommand { ) .await } else { - let signer = SecretKey::read_from_bytes(hex::decode(validator_key)?.as_ref())?; + let signer = SigningKey::read_from_bytes(hex::decode(validator_key)?.as_ref())?; let signer = ValidatorSigner::new_local(signer); start::start( address, @@ -216,7 +216,7 @@ impl ValidatorKey { if let Some(kms_key_id) = self.validator_kms_key_id { Ok(ValidatorSigner::new_kms(kms_key_id).await?) } else { - let signer = SecretKey::read_from_bytes(hex::decode(self.validator_key)?.as_ref())?; + let signer = SigningKey::read_from_bytes(hex::decode(self.validator_key)?.as_ref())?; Ok(ValidatorSigner::new_local(signer)) } } diff --git a/bin/validator/src/db/models.rs b/bin/validator/src/db/models.rs index fd10445338..85f1e7354d 100644 --- a/bin/validator/src/db/models.rs +++ b/bin/validator/src/db/models.rs @@ -39,7 +39,7 @@ impl ValidatedTransactionRowInsert { output_notes: tx.output_notes().to_bytes(), initial_account_hash: tx.initial_account_hash().to_bytes(), final_account_hash: tx.final_account_hash().to_bytes(), - fee: tx.fee().amount().to_le_bytes().to_vec(), + fee: tx.fee().amount().as_u64().to_le_bytes().to_vec(), } } } diff --git a/bin/validator/src/signers/mod.rs b/bin/validator/src/signers/mod.rs index 2c50b2dbb2..59999339d7 100644 --- a/bin/validator/src/signers/mod.rs +++ b/bin/validator/src/signers/mod.rs @@ -2,7 +2,7 @@ mod kms; pub use kms::KmsSigner; use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_protocol::block::BlockHeader; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, SecretKey, Signature}; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature, SigningKey}; // VALIDATOR SIGNER // ================================================================================================= @@ -10,7 +10,7 @@ use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, SecretKey, Signa /// Signer that the Validator uses to sign blocks. pub enum ValidatorSigner { Kms(KmsSigner), - Local(SecretKey), + Local(SigningKey), } impl ValidatorSigner { @@ -24,7 +24,7 @@ impl ValidatorSigner { } /// Constructs a signer which uses a local secret key for signing. - pub fn new_local(secret_key: SecretKey) -> Self { + pub fn new_local(secret_key: SigningKey) -> Self { Self::Local(secret_key) } diff --git a/crates/block-producer/src/block_builder/mod.rs b/crates/block-producer/src/block_builder/mod.rs index 08ebd38f5d..1a45c9dafa 100644 --- a/crates/block-producer/src/block_builder/mod.rs +++ b/crates/block-producer/src/block_builder/mod.rs @@ -7,7 +7,6 @@ use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_protocol::batch::{OrderedBatches, ProvenBatch}; use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock, ProvenBlock, SignedBlock}; -use miden_protocol::note::NoteHeader; use miden_protocol::transaction::TransactionHeader; use tokio::time::Duration; use tracing::{Span, instrument}; @@ -168,7 +167,7 @@ impl BlockBuilder { .input_notes() .iter() .cloned() - .filter_map(|note| note.header().map(NoteHeader::to_commitment)) + .filter_map(|note| note.header().map(|h| h.id().as_word())) }); let block_references_iter = batch_iter.clone().map(Deref::deref).map(ProvenBatch::reference_block_num); diff --git a/crates/block-producer/src/domain/transaction.rs b/crates/block-producer/src/domain/transaction.rs index 4292db804c..62cb9bde1b 100644 --- a/crates/block-producer/src/domain/transaction.rs +++ b/crates/block-producer/src/domain/transaction.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use miden_protocol::Word; use miden_protocol::account::AccountId; use miden_protocol::block::BlockNumber; -use miden_protocol::note::{NoteHeader, Nullifier}; +use miden_protocol::note::Nullifier; use miden_protocol::transaction::{OutputNote, ProvenTransaction, TransactionId, TxAccountUpdate}; use crate::errors::StateConflict; @@ -90,10 +90,7 @@ impl AuthenticatedTransaction { } pub fn output_note_commitments(&self) -> impl Iterator + '_ { - self.inner - .output_notes() - .iter() - .map(miden_protocol::transaction::OutputNote::to_commitment) + self.inner.output_notes().iter().map(|n| n.id().as_word()) } pub fn output_notes(&self) -> impl Iterator + '_ { @@ -117,7 +114,7 @@ impl AuthenticatedTransaction { pub fn unauthenticated_note_commitments(&self) -> impl Iterator + '_ { self.inner .unauthenticated_notes() - .map(NoteHeader::to_commitment) + .map(|h| h.id().as_word()) .filter(|commitment| !self.notes_authenticated_by_store.contains(commitment)) } diff --git a/crates/block-producer/src/mempool/subscription.rs b/crates/block-producer/src/mempool/subscription.rs index d38ea0fe0f..7b42245ea9 100644 --- a/crates/block-producer/src/mempool/subscription.rs +++ b/crates/block-producer/src/mempool/subscription.rs @@ -87,8 +87,16 @@ impl SubscriptionProvider { OutputNote::Private(_) => None, }) .collect(); - let account_delta = - tx.account_id().is_network().then(|| tx.account_update().details().clone()); + // The classifier `is_network()` is gone from the protocol; network-ness now lives in + // account storage and cannot be determined from an AccountId alone. We send the delta for + // every non-private update and let the subscriber (which keeps its own list of network + // accounts) filter. Private accounts carry no payload, so the extra envelopes are cheap. + let account_delta = match tx.account_update().details() { + miden_protocol::account::delta::AccountUpdateDetails::Private => None, + details @ miden_protocol::account::delta::AccountUpdateDetails::Delta(_) => { + Some(details.clone()) + }, + }; let event = MempoolEvent::TransactionAdded { id, nullifiers, diff --git a/crates/block-producer/src/store/mod.rs b/crates/block-producer/src/store/mod.rs index 92c78cfe4d..043e54f259 100644 --- a/crates/block-producer/src/store/mod.rs +++ b/crates/block-producer/src/store/mod.rs @@ -162,7 +162,7 @@ impl StoreClient { nullifiers: proven_tx.nullifiers().map(Into::into).collect(), unauthenticated_notes: proven_tx .unauthenticated_notes() - .map(|note| note.to_commitment().into()) + .map(|note| note.id().as_word().into()) .collect(), }; diff --git a/crates/block-producer/src/test_utils/proven_tx.rs b/crates/block-producer/src/test_utils/proven_tx.rs index e434dd52c9..aa7eccf16d 100644 --- a/crates/block-producer/src/test_utils/proven_tx.rs +++ b/crates/block-producer/src/test_utils/proven_tx.rs @@ -110,7 +110,7 @@ impl MockProvenTxBuilder { pub fn nullifiers_range(self, range: Range) -> Self { let nullifiers = range .map(|index| { - let nullifier = Word::from([ONE, ONE, ONE, Felt::new(index)]); + let nullifier = Word::from([ONE, ONE, ONE, Felt::new_unchecked(index)]); Nullifier::from_raw(nullifier) }) diff --git a/crates/db/src/conv.rs b/crates/db/src/conv.rs index 8da9b50d38..5f1b4d0903 100644 --- a/crates/db/src/conv.rs +++ b/crates/db/src/conv.rs @@ -147,7 +147,7 @@ pub(crate) fn nullifier_prefix_to_raw_sql(prefix: u16) -> i32 { #[inline(always)] pub(crate) fn raw_sql_to_nonce(raw: i64) -> Felt { debug_assert!(raw >= 0); - Felt::new(raw as u64) + Felt::new_unchecked(raw as u64) } #[inline(always)] pub(crate) fn nonce_to_raw_sql(nonce: Felt) -> i64 { diff --git a/crates/large-smt-backend-rocksdb/src/helpers.rs b/crates/large-smt-backend-rocksdb/src/helpers.rs index 23f3c8d88f..bcc1611e92 100644 --- a/crates/large-smt-backend-rocksdb/src/helpers.rs +++ b/crates/large-smt-backend-rocksdb/src/helpers.rs @@ -1,5 +1,4 @@ use miden_crypto::merkle::smt::{MAX_LEAF_ENTRIES, SmtLeaf, SmtLeafError}; -use miden_crypto::word::LexicographicWord; use rocksdb::Error as RocksDbError; use crate::{StorageError, Word}; @@ -25,30 +24,28 @@ pub(crate) fn insert_into_leaf( Ok(Some(old_value)) } else { let mut pairs = vec![*kv_pair, (key, value)]; - pairs.sort_by(|(key_1, _), (key_2, _)| { - LexicographicWord::from(*key_1).cmp(&LexicographicWord::from(*key_2)) - }); + pairs.sort_by(|(key_1, _), (key_2, _)| key_1.cmp(key_2)); *leaf = SmtLeaf::Multiple(pairs); Ok(None) } }, - SmtLeaf::Multiple(kv_pairs) => match kv_pairs.binary_search_by(|kv_pair| { - LexicographicWord::from(kv_pair.0).cmp(&LexicographicWord::from(key)) - }) { - Ok(pos) => { - let old_value = kv_pairs[pos].1; - kv_pairs[pos].1 = value; - Ok(Some(old_value)) - }, - Err(pos) => { - if kv_pairs.len() >= MAX_LEAF_ENTRIES { - return Err(StorageError::Leaf(SmtLeafError::TooManyLeafEntries { - actual: kv_pairs.len() + 1, - })); - } - kv_pairs.insert(pos, (key, value)); - Ok(None) - }, + SmtLeaf::Multiple(kv_pairs) => { + match kv_pairs.binary_search_by(|kv_pair| kv_pair.0.cmp(&key)) { + Ok(pos) => { + let old_value = kv_pairs[pos].1; + kv_pairs[pos].1 = value; + Ok(Some(old_value)) + }, + Err(pos) => { + if kv_pairs.len() >= MAX_LEAF_ENTRIES { + return Err(StorageError::Leaf(SmtLeafError::TooManyLeafEntries { + actual: kv_pairs.len() + 1, + })); + } + kv_pairs.insert(pos, (key, value)); + Ok(None) + }, + } }, } } @@ -65,19 +62,19 @@ pub(crate) fn remove_from_leaf(leaf: &mut SmtLeaf, key: Word) -> (Option, (None, false) } }, - SmtLeaf::Multiple(kv_pairs) => match kv_pairs.binary_search_by(|kv_pair| { - LexicographicWord::from(kv_pair.0).cmp(&LexicographicWord::from(key)) - }) { - Ok(pos) => { - let old_value = kv_pairs[pos].1; - kv_pairs.remove(pos); - debug_assert!(!kv_pairs.is_empty()); - if kv_pairs.len() == 1 { - *leaf = SmtLeaf::Single(kv_pairs[0]); - } - (Some(old_value), false) - }, - Err(_) => (None, false), + SmtLeaf::Multiple(kv_pairs) => { + match kv_pairs.binary_search_by(|kv_pair| kv_pair.0.cmp(&key)) { + Ok(pos) => { + let old_value = kv_pairs[pos].1; + kv_pairs.remove(pos); + debug_assert!(!kv_pairs.is_empty()); + if kv_pairs.len() == 1 { + *leaf = SmtLeaf::Single(kv_pairs[0]); + } + (Some(old_value), false) + }, + Err(_) => (None, false), + } }, } } diff --git a/crates/large-smt-backend-rocksdb/src/lib.rs b/crates/large-smt-backend-rocksdb/src/lib.rs index eaca341fdc..97ccceff99 100644 --- a/crates/large-smt-backend-rocksdb/src/lib.rs +++ b/crates/large-smt-backend-rocksdb/src/lib.rs @@ -39,6 +39,7 @@ pub use miden_protocol::crypto::merkle::smt::{ SmtLeafError, SmtProof, SmtStorage, + SmtStorageReader, StorageError, StorageUpdateParts, StorageUpdates, @@ -61,6 +62,7 @@ pub use rocksdb::{ RocksDbConfig, RocksDbDurabilityMode, RocksDbMemoryBudget, + RocksDbSnapshotStorage, RocksDbStorage, RocksDbTuningOptions, RocksDbWriteBufferManagerBudget, diff --git a/crates/large-smt-backend-rocksdb/src/rocksdb.rs b/crates/large-smt-backend-rocksdb/src/rocksdb.rs index 1310ae6dff..048116ef4e 100644 --- a/crates/large-smt-backend-rocksdb/src/rocksdb.rs +++ b/crates/large-smt-backend-rocksdb/src/rocksdb.rs @@ -1,5 +1,6 @@ use alloc::boxed::Box; use alloc::vec::Vec; +use std::mem::ManuallyDrop; use std::path::PathBuf; use std::sync::Arc; @@ -24,7 +25,14 @@ use rocksdb::{ WriteOptions, }; -use super::{SmtStorage, StorageError, StorageUpdateParts, StorageUpdates, SubtreeUpdate}; +use super::{ + SmtStorage, + SmtStorageReader, + StorageError, + StorageUpdateParts, + StorageUpdates, + SubtreeUpdate, +}; use crate::helpers::{insert_into_leaf, map_rocksdb_err, remove_from_leaf}; use crate::{EMPTY_WORD, Word}; @@ -317,71 +325,297 @@ impl RocksDbStorage { }; KeyBytes::new(index.position(), keep) } +} + +// READ-SOURCE TRAIT +// -------------------------------------------------------------------------------------------- + +/// Internal abstraction over reading from either the live `DB` ([`RocksDbStorage`]) or a +/// point-in-time `Snapshot` ([`RocksDbSnapshotStorage`]). +/// +/// The two storage types each implement this trait by providing the few low-level operations +/// (`db`, `get_cf_bytes`, `multi_get_cf_bytes`, optional `apply_snapshot`); the shared +/// `SmtStorageReader` logic lives in the default methods below so it is written once. +trait RocksReadSource: Send + Sync + 'static { + fn db(&self) -> &DB; + + fn get_cf_bytes( + &self, + cf: &rocksdb::ColumnFamily, + key: &[u8], + ) -> Result>, rocksdb::Error>; + + fn multi_get_cf_bytes<'b, K, I, W>( + &self, + keys_cf: I, + ) -> Vec>, rocksdb::Error>> + where + K: AsRef<[u8]>, + I: IntoIterator, + W: rocksdb::AsColumnFamilyRef + 'b; + + fn apply_snapshot(&self, _read_opts: &mut ReadOptions) {} - /// Retrieves a handle to a `RocksDB` column family by its name. - /// - /// # Errors - /// Returns `StorageError::Backend` if the column family with the given `name` does not - /// exist. fn cf_handle(&self, name: &str) -> Result<&rocksdb::ColumnFamily, StorageError> { - self.db + self.db() .cf_handle(name) .ok_or_else(|| StorageError::Unsupported(format!("unknown column family `{name}`"))) } - /* helper: CF handle from NodeIndex ------------------------------------- */ #[inline(always)] fn subtree_cf(&self, index: NodeIndex) -> &rocksdb::ColumnFamily { - let name = cf_for_depth(index.depth()); - self.cf_handle(name).expect("CF handle missing") + self.cf_handle(cf_for_depth(index.depth())).expect("CF handle missing") } -} -impl SmtStorage for RocksDbStorage { - /// Retrieves the total count of non-empty leaves from the `METADATA_CF` column family. - /// Returns 0 if the count is not found. - /// - /// # Errors - /// - `StorageError::Backend`: If the metadata column family is missing or a RocksDB error - /// occurs. - /// - `StorageError::BadValueLen`: If the retrieved count bytes are invalid. - fn leaf_count(&self) -> Result { + fn read_count_impl(&self, what: &'static str, key: &[u8]) -> Result { let cf = self.cf_handle(METADATA_CF)?; - self.db - .get_cf(cf, LEAF_COUNT_KEY) - .map_err(map_rocksdb_err)? - .map_or(Ok(0), |bytes| { - let arr: [u8; 8] = - bytes.as_slice().try_into().map_err(|_| StorageError::BadValueLen { - what: "leaf count", - expected: 8, - found: bytes.len(), - })?; - Ok(usize::from_be_bytes(arr)) + self.get_cf_bytes(cf, key).map_err(map_rocksdb_err)?.map_or(Ok(0), |bytes| { + let arr: [u8; 8] = bytes + .as_slice() + .try_into() + .map_err(|_| StorageError::BadValueLen { what, expected: 8, found: bytes.len() })?; + Ok(usize::from_be_bytes(arr)) + }) + } + + fn leaf_count_impl(&self) -> Result { + self.read_count_impl("leaf count", LEAF_COUNT_KEY) + } + + fn entry_count_impl(&self) -> Result { + self.read_count_impl("entry count", ENTRY_COUNT_KEY) + } + + fn get_leaf_impl(&self, index: u64) -> Result, StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let key = RocksDbStorage::index_db_key(index); + match self.get_cf_bytes(cf, &key).map_err(map_rocksdb_err)? { + Some(bytes) => Ok(Some(SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?)), + None => Ok(None), + } + } + + fn get_leaves_impl(&self, indices: &[u64]) -> Result>, StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let db_keys: Vec<[u8; 8]> = + indices.iter().map(|&idx| RocksDbStorage::index_db_key(idx)).collect(); + let results = self.multi_get_cf_bytes(db_keys.iter().map(|k| (cf, k.as_ref()))); + results + .into_iter() + .map(|result| match result { + Ok(Some(bytes)) => { + Ok(Some(SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?)) + }, + Ok(None) => Ok(None), + Err(e) => Err(map_rocksdb_err(e)), }) + .collect() } - /// Retrieves the total count of key-value entries from the `METADATA_CF` column family. - /// Returns 0 if the count is not found. + fn has_leaves_impl(&self) -> Result { + Ok(self.leaf_count_impl()? > 0) + } + + fn get_subtree_impl(&self, index: NodeIndex) -> Result, StorageError> { + let cf = self.subtree_cf(index); + let key = RocksDbStorage::subtree_db_key(index); + match self.get_cf_bytes(cf, key.as_ref()).map_err(map_rocksdb_err)? { + Some(bytes) => Ok(Some(Subtree::from_vec(index, &bytes)?)), + None => Ok(None), + } + } + + /// Batch-retrieves subtrees grouped by depth via parallel `multi_get` calls, one per depth + /// bucket. /// - /// # Errors - /// - `StorageError::Backend`: If the metadata column family is missing or a RocksDB error - /// occurs. - /// - `StorageError::BadValueLen`: If the retrieved count bytes are invalid. + /// Note: when multiple buckets error, only the first one observed by `collect` is propagated. + fn get_subtrees_impl( + &self, + indices: &[NodeIndex], + ) -> Result>, StorageError> { + use rayon::prelude::*; + + let mut depth_buckets: [Vec<(usize, NodeIndex)>; 5] = Default::default(); + + for (original_index, &node_index) in indices.iter().enumerate() { + let depth = node_index.depth(); + let bucket_index = match depth { + 56 => 0, + 48 => 1, + 40 => 2, + 32 => 3, + 24 => 4, + _ => { + return Err(StorageError::Unsupported(format!( + "unsupported subtree depth {depth}" + ))); + }, + }; + depth_buckets[bucket_index].push((original_index, node_index)); + } + let mut results = vec![None; indices.len()]; + + let bucket_results: Result, StorageError> = depth_buckets + .into_par_iter() + .enumerate() + .filter(|(_, bucket)| !bucket.is_empty()) + .map( + |(bucket_index, bucket)| -> Result)>, StorageError> { + let depth = SUBTREE_DEPTHS[bucket_index]; + let cf = self.cf_handle(cf_for_depth(depth))?; + let keys: Vec<_> = bucket + .iter() + .map(|(_, idx)| RocksDbStorage::subtree_db_key(*idx)) + .collect(); + + let db_results = self.multi_get_cf_bytes(keys.iter().map(|k| (cf, k.as_ref()))); + + bucket + .into_iter() + .zip(db_results) + .map(|((original_index, node_index), db_result)| { + let subtree = match db_result { + Ok(Some(bytes)) => Some(Subtree::from_vec(node_index, &bytes)?), + Ok(None) => None, + Err(e) => return Err(map_rocksdb_err(e)), + }; + Ok((original_index, subtree)) + }) + .collect() + }, + ) + .collect(); + + for bucket_result in bucket_results? { + for (original_index, subtree) in bucket_result { + results[original_index] = subtree; + } + } + + Ok(results) + } + + fn get_inner_node_impl(&self, index: NodeIndex) -> Result, StorageError> { + if index.depth() < IN_MEMORY_DEPTH { + return Err(StorageError::Unsupported( + "Cannot get inner node from upper part of the tree".into(), + )); + } + let subtree_root_index = Subtree::find_subtree_root(index); + Ok(self + .get_subtree_impl(subtree_root_index)? + .and_then(|subtree| subtree.get_inner_node(index))) + } + + fn iter_leaves_impl( + &self, + ) -> Result + '_>, StorageError> { + let cf = self.cf_handle(LEAVES_CF)?; + let mut read_opts = ReadOptions::default(); + read_opts.set_total_order_seek(true); + self.apply_snapshot(&mut read_opts); + let db_iter = self.db().iterator_cf_opt(cf, read_opts, IteratorMode::Start); + Ok(Box::new(RocksDbDirectLeafIterator { iter: db_iter })) + } + + fn iter_subtrees_impl(&self) -> Result + '_>, StorageError> { + const SUBTREE_CFS: [&str; 5] = + [SUBTREE_24_CF, SUBTREE_32_CF, SUBTREE_40_CF, SUBTREE_48_CF, SUBTREE_56_CF]; + + let mut cf_handles = Vec::new(); + for cf_name in SUBTREE_CFS { + cf_handles.push(self.cf_handle(cf_name)?); + } + let configure = move |opts: &mut ReadOptions| self.apply_snapshot(opts); + Ok(Box::new(RocksDbSubtreeIterator::new(self.db(), cf_handles, configure))) + } + + fn get_depth24_impl(&self) -> Result, StorageError> { + let cf = self.cf_handle(DEPTH_24_CF)?; + let mut read_opts = ReadOptions::default(); + self.apply_snapshot(&mut read_opts); + let iter = self.db().iterator_cf_opt(cf, read_opts, IteratorMode::Start); + let mut hashes = Vec::new(); + for item in iter { + let (key_bytes, value_bytes) = item.map_err(map_rocksdb_err)?; + let index = index_from_key_bytes(&key_bytes)?; + let hash = Word::read_from_bytes_with_budget(&value_bytes, value_bytes.len())?; + hashes.push((index, hash)); + } + Ok(hashes) + } +} + +impl RocksReadSource for RocksDbStorage { + fn db(&self) -> &DB { + &self.db + } + + fn get_cf_bytes( + &self, + cf: &rocksdb::ColumnFamily, + key: &[u8], + ) -> Result>, rocksdb::Error> { + self.db.get_cf(cf, key) + } + + fn multi_get_cf_bytes<'b, K, I, W>( + &self, + keys_cf: I, + ) -> Vec>, rocksdb::Error>> + where + K: AsRef<[u8]>, + I: IntoIterator, + W: rocksdb::AsColumnFamilyRef + 'b, + { + self.db.multi_get_cf(keys_cf) + } +} + +impl SmtStorageReader for RocksDbStorage { + fn leaf_count(&self) -> Result { + self.leaf_count_impl() + } fn entry_count(&self) -> Result { - let cf = self.cf_handle(METADATA_CF)?; - self.db - .get_cf(cf, ENTRY_COUNT_KEY) - .map_err(map_rocksdb_err)? - .map_or(Ok(0), |bytes| { - let arr: [u8; 8] = - bytes.as_slice().try_into().map_err(|_| StorageError::BadValueLen { - what: "entry count", - expected: 8, - found: bytes.len(), - })?; - Ok(usize::from_be_bytes(arr)) - }) + self.entry_count_impl() + } + fn get_leaf(&self, index: u64) -> Result, StorageError> { + self.get_leaf_impl(index) + } + fn get_leaves(&self, indices: &[u64]) -> Result>, StorageError> { + self.get_leaves_impl(indices) + } + fn has_leaves(&self) -> Result { + self.has_leaves_impl() + } + fn get_subtree(&self, index: NodeIndex) -> Result, StorageError> { + self.get_subtree_impl(index) + } + fn get_subtrees(&self, indices: &[NodeIndex]) -> Result>, StorageError> { + self.get_subtrees_impl(indices) + } + fn get_inner_node(&self, index: NodeIndex) -> Result, StorageError> { + self.get_inner_node_impl(index) + } + fn iter_leaves(&self) -> Result + '_>, StorageError> { + self.iter_leaves_impl() + } + fn iter_subtrees(&self) -> Result + '_>, StorageError> { + self.iter_subtrees_impl() + } + fn get_depth24(&self) -> Result, StorageError> { + self.get_depth24_impl() + } +} + +impl SmtStorage for RocksDbStorage { + type Reader = RocksDbSnapshotStorage; + + /// Returns a read-only point-in-time snapshot of the underlying RocksDB. + /// + /// Subsequent writes through `self` do not affect the returned reader. + fn reader(&self) -> Result { + Ok(RocksDbSnapshotStorage::new(self.db.clone())) } /// Inserts a key-value pair into the SMT leaf at the specified logical `index`. @@ -503,23 +737,6 @@ impl SmtStorage for RocksDbStorage { Ok(current_value) } - /// Retrieves a single SMT leaf node by its logical `index` from the `LEAVES_CF` column family. - /// - /// # Errors - /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs. - /// - `StorageError::DeserializationError`: If the retrieved leaf data is corrupt. - fn get_leaf(&self, index: u64) -> Result, StorageError> { - let cf = self.cf_handle(LEAVES_CF)?; - let key = Self::index_db_key(index); - match self.db.get_cf(cf, key).map_err(map_rocksdb_err)? { - Some(bytes) => { - let leaf = SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?; - Ok(Some(leaf)) - }, - None => Ok(None), - } - } - /// Sets or updates multiple SMT leaf nodes in the `LEAVES_CF` column family. /// /// This method performs a batch write to RocksDB. It also updates the global @@ -575,144 +792,6 @@ impl SmtStorage for RocksDbStorage { })) } - /// Retrieves multiple SMT leaf nodes by their logical `indices` using RocksDB's `multi_get_cf`. - /// - /// # Errors - /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs. - /// - `StorageError::DeserializationError`: If any retrieved leaf data is corrupt. - fn get_leaves(&self, indices: &[u64]) -> Result>, StorageError> { - let cf = self.cf_handle(LEAVES_CF)?; - let db_keys: Vec<[u8; 8]> = indices.iter().map(|&idx| Self::index_db_key(idx)).collect(); - let results = self.db.multi_get_cf(db_keys.iter().map(|k| (cf, k.as_ref()))); - - results - .into_iter() - .map(|result| match result { - Ok(Some(bytes)) => { - Ok(Some(SmtLeaf::read_from_bytes_with_budget(&bytes, bytes.len())?)) - }, - Ok(None) => Ok(None), - Err(e) => Err(map_rocksdb_err(e)), - }) - .collect() - } - - /// Returns true if the storage has any leaves. - /// - /// # Errors - /// Returns `StorageError` if the storage read operation fails. - fn has_leaves(&self) -> Result { - Ok(self.leaf_count()? > 0) - } - - /// Batch-retrieves multiple subtrees from RocksDB by their node indices. - /// - /// This method groups requests by subtree depth into column family buckets, - /// then performs parallel `multi_get` operations to efficiently retrieve - /// all subtrees. Results are deserialized and placed in the same order as - /// the input indices. - /// - /// Note: Retrieval is performed in parallel. If multiple errors occur (e.g., - /// deserialization or backend errors), only the first one encountered is returned. - /// Other errors will be discarded. - /// - /// # Parameters - /// - `indices`: A slice of subtree root indices to retrieve. - /// - /// # Returns - /// - A `Vec>` where each index corresponds to the original input. - /// - `Ok(...)` if all fetches succeed. - /// - `Err(StorageError)` if any RocksDB access or deserialization fails. - fn get_subtree(&self, index: NodeIndex) -> Result, StorageError> { - let cf = self.subtree_cf(index); - let key = Self::subtree_db_key(index); - match self.db.get_cf(cf, key).map_err(map_rocksdb_err)? { - Some(bytes) => { - let subtree = Subtree::from_vec(index, &bytes)?; - Ok(Some(subtree)) - }, - None => Ok(None), - } - } - - /// Batch-retrieves multiple subtrees from RocksDB by their node indices. - /// - /// This method groups requests by subtree depth into column family buckets, - /// then performs parallel `multi_get` operations to efficiently retrieve - /// all subtrees. Results are deserialized and placed in the same order as - /// the input indices. - /// - /// # Parameters - /// - `indices`: A slice of subtree root indices to retrieve. - /// - /// # Returns - /// - A `Vec>` where each index corresponds to the original input. - /// - `Ok(...)` if all fetches succeed. - /// - `Err(StorageError)` if any RocksDB access or deserialization fails. - fn get_subtrees(&self, indices: &[NodeIndex]) -> Result>, StorageError> { - use rayon::prelude::*; - - let mut depth_buckets: [Vec<(usize, NodeIndex)>; 5] = Default::default(); - - for (original_index, &node_index) in indices.iter().enumerate() { - let depth = node_index.depth(); - let bucket_index = match depth { - 56 => 0, - 48 => 1, - 40 => 2, - 32 => 3, - 24 => 4, - _ => { - return Err(StorageError::Unsupported(format!( - "unsupported subtree depth {depth}" - ))); - }, - }; - depth_buckets[bucket_index].push((original_index, node_index)); - } - let mut results = vec![None; indices.len()]; - - // Process depth buckets in parallel - let bucket_results: Result, StorageError> = depth_buckets - .into_par_iter() - .enumerate() - .filter(|(_, bucket)| !bucket.is_empty()) - .map( - |(bucket_index, bucket)| -> Result)>, StorageError> { - let depth = SUBTREE_DEPTHS[bucket_index]; - let cf = self.cf_handle(cf_for_depth(depth))?; - let keys: Vec<_> = - bucket.iter().map(|(_, idx)| Self::subtree_db_key(*idx)).collect(); - - let db_results = self.db.multi_get_cf(keys.iter().map(|k| (cf, k.as_ref()))); - - // Process results for this bucket - bucket - .into_iter() - .zip(db_results) - .map(|((original_index, node_index), db_result)| { - let subtree = match db_result { - Ok(Some(bytes)) => Some(Subtree::from_vec(node_index, &bytes)?), - Ok(None) => None, - Err(e) => return Err(map_rocksdb_err(e)), - }; - Ok((original_index, subtree)) - }) - .collect() - }, - ) - .collect(); - - // Flatten results and place them in correct positions - for bucket_result in bucket_results? { - for (original_index, subtree) in bucket_result { - results[original_index] = subtree; - } - } - - Ok(results) - } - /// Stores a single subtree in RocksDB and optionally updates the depth-24 root cache. /// /// The subtree is serialized and written to its corresponding column family. @@ -808,27 +887,6 @@ impl SmtStorage for RocksDbStorage { Ok(()) } - /// Retrieves a single inner node (non-leaf node) from within a Subtree. - /// - /// This method is intended for accessing nodes at depths greater than or equal to - /// `IN_MEMORY_DEPTH`. It first finds the appropriate Subtree containing the `index`, then - /// delegates to `Subtree::get_inner_node()`. - /// - /// # Errors - /// - `StorageError::Backend`: If `index.depth() < IN_MEMORY_DEPTH`, or if RocksDB errors occur. - /// - `StorageError::Value`: If the containing Subtree data is corrupt. - fn get_inner_node(&self, index: NodeIndex) -> Result, StorageError> { - if index.depth() < IN_MEMORY_DEPTH { - return Err(StorageError::Unsupported( - "Cannot get inner node from upper part of the tree".into(), - )); - } - let subtree_root_index = Subtree::find_subtree_root(index); - Ok(self - .get_subtree(subtree_root_index)? - .and_then(|subtree| subtree.get_inner_node(index))) - } - /// Sets or updates a single inner node (non-leaf node) within a Subtree. /// /// This method is intended for `index.depth() >= IN_MEMORY_DEPTH`. @@ -995,70 +1053,6 @@ impl SmtStorage for RocksDbStorage { Ok(()) } - - /// Returns an iterator over all (logical u64 index, `SmtLeaf`) pairs in the `LEAVES_CF`. - /// - /// The iterator uses a RocksDB snapshot for consistency and iterates in lexicographical - /// order of the keys (leaf indices). Errors during iteration (e.g., deserialization issues) - /// cause the iterator to skip the problematic item and attempt to continue. - /// - /// # Errors - /// - `StorageError::Backend`: If the leaves column family is missing or a RocksDB error occurs - /// during iterator creation. - fn iter_leaves(&self) -> Result + '_>, StorageError> { - let cf = self.cf_handle(LEAVES_CF)?; - let mut read_opts = ReadOptions::default(); - read_opts.set_total_order_seek(true); - let db_iter = self.db.iterator_cf_opt(cf, read_opts, IteratorMode::Start); - - Ok(Box::new(RocksDbDirectLeafIterator { iter: db_iter })) - } - - /// Returns an iterator over all `Subtree` instances across all subtree column families. - /// - /// The iterator uses a RocksDB snapshot and iterates in lexicographical order of keys - /// (subtree root NodeIndex) across all depth column families (24, 32, 40, 48, 56). - /// Errors during iteration (e.g., deserialization issues) cause the iterator to skip - /// the problematic item and attempt to continue. - /// - /// # Errors - /// - `StorageError::Backend`: If any subtree column family is missing or a RocksDB error occurs - /// during iterator creation. - fn iter_subtrees(&self) -> Result + '_>, StorageError> { - // All subtree column family names in order - const SUBTREE_CFS: [&str; 5] = - [SUBTREE_24_CF, SUBTREE_32_CF, SUBTREE_40_CF, SUBTREE_48_CF, SUBTREE_56_CF]; - - let mut cf_handles = Vec::new(); - for cf_name in SUBTREE_CFS { - cf_handles.push(self.cf_handle(cf_name)?); - } - - Ok(Box::new(RocksDbSubtreeIterator::new(&self.db, cf_handles))) - } - - /// Retrieves all depth 24 hashes for fast tree rebuilding. - /// - /// # Errors - /// - `StorageError::Backend`: If the depth24 column family is missing or a RocksDB error - /// occurs. - /// - `StorageError::Value`: If any hash bytes are corrupt. - fn get_depth24(&self) -> Result, StorageError> { - let cf = self.cf_handle(DEPTH_24_CF)?; - let iter = self.db.iterator_cf(cf, IteratorMode::Start); - let mut hashes = Vec::new(); - - for item in iter { - let (key_bytes, value_bytes) = item.map_err(map_rocksdb_err)?; - - let index = index_from_key_bytes(&key_bytes)?; - let hash = Word::read_from_bytes_with_budget(&value_bytes, value_bytes.len())?; - - hashes.push((index, hash)); - } - - Ok(hashes) - } } /// Syncs the RocksDB database to disk before dropping the storage. @@ -1105,18 +1099,28 @@ impl Iterator for RocksDbDirectLeafIterator<'_> { /// /// Iterates through all subtree column families (24, 32, 40, 48, 56) sequentially. /// When one column family is exhausted, it moves to the next one. +/// +/// The `configure` hook is called whenever a new per-CF iterator is built; it is used by the +/// snapshot-backed source to attach its `Snapshot` to the per-CF `ReadOptions`, and is a no-op for +/// the live-DB source. struct RocksDbSubtreeIterator<'a> { db: &'a DB, cf_handles: Vec<&'a rocksdb::ColumnFamily>, + configure: Box, current_cf_index: usize, current_iter: Option>, } impl<'a> RocksDbSubtreeIterator<'a> { - fn new(db: &'a DB, cf_handles: Vec<&'a rocksdb::ColumnFamily>) -> Self { + fn new( + db: &'a DB, + cf_handles: Vec<&'a rocksdb::ColumnFamily>, + configure: F, + ) -> Self { let mut iterator = Self { db, cf_handles, + configure: Box::new(configure), current_cf_index: 0, current_iter: None, }; @@ -1129,6 +1133,7 @@ impl<'a> RocksDbSubtreeIterator<'a> { let cf = self.cf_handles[self.current_cf_index]; let mut read_opts = ReadOptions::default(); read_opts.set_total_order_seek(true); + (self.configure)(&mut read_opts); self.current_iter = Some(self.db.iterator_cf_opt(cf, read_opts, IteratorMode::Start)); } else { self.current_iter = None; @@ -1596,3 +1601,138 @@ fn cf_for_depth(depth: u8) -> &'static str { _ => panic!("unsupported subtree depth: {depth}"), } } + +// SNAPSHOT STORAGE +// -------------------------------------------------------------------------------------------- + +/// Read-only point-in-time view over [`RocksDbStorage`]. +/// +/// Wraps a `rocksdb::Snapshot` together with the `Arc` it borrows from so the snapshot +/// stays valid for the lifetime of this value. Used as [`SmtStorage::Reader`]. +pub struct RocksDbSnapshotStorage { + inner: Arc, +} + +impl std::fmt::Debug for RocksDbSnapshotStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RocksDbSnapshotStorage").finish_non_exhaustive() + } +} + +/// Owns a RocksDB snapshot alongside the database it borrows from. +/// +/// `rocksdb::Snapshot<'a>` borrows the DB used to create it, but `SmtStorage::Reader` must be +/// `'static`, so we store the `Arc` next to the snapshot and rely on field-drop ordering +/// (snapshot first via `ManuallyDrop`, then the `Arc`) to keep the borrow valid. +/// +/// This mirrors the `RocksDbSnapshotInner` defined upstream in +/// `miden_crypto::merkle::smt::large::storage::rocksdb` (≥0.25); see that module for the same +/// SAFETY rationale. +struct RocksDbSnapshotInner { + snapshot: ManuallyDrop>, + db: Arc, +} + +impl RocksDbSnapshotInner { + fn new(db: Arc) -> Self { + let snapshot = db.snapshot(); + // SAFETY: The snapshot internally borrows the `DB` allocation owned by `db`. This struct + // keeps that `Arc` alive and its `Drop` impl releases the snapshot before the `Arc` is + // dropped by Rust's normal field cleanup, so the borrow stays valid for the snapshot's + // entire lifetime. Mirrors the identical pattern used upstream in + // `miden_crypto::merkle::smt::large::storage::rocksdb::RocksDbSnapshotInner`. + let snapshot = unsafe { + core::mem::transmute::, rocksdb::Snapshot<'static>>(snapshot) + }; + Self { + snapshot: ManuallyDrop::new(snapshot), + db, + } + } +} + +impl Drop for RocksDbSnapshotInner { + fn drop(&mut self) { + // SAFETY: `snapshot` is only wrapped in `ManuallyDrop` to control field-drop order. We drop + // it exactly once here, before `db` is dropped by Rust's normal field cleanup. Mirrors the + // upstream `RocksDbSnapshotInner::drop` in `miden_crypto`. + unsafe { + ManuallyDrop::drop(&mut self.snapshot); + } + } +} + +impl RocksDbSnapshotStorage { + /// Builds a snapshot-backed reader from a shared RocksDB handle. + pub fn new(db: Arc) -> Self { + Self { + inner: Arc::new(RocksDbSnapshotInner::new(db)), + } + } +} + +impl RocksReadSource for RocksDbSnapshotStorage { + fn db(&self) -> &DB { + &self.inner.db + } + + fn get_cf_bytes( + &self, + cf: &rocksdb::ColumnFamily, + key: &[u8], + ) -> Result>, rocksdb::Error> { + self.inner.snapshot.get_cf(cf, key) + } + + fn multi_get_cf_bytes<'b, K, I, W>( + &self, + keys_cf: I, + ) -> Vec>, rocksdb::Error>> + where + K: AsRef<[u8]>, + I: IntoIterator, + W: rocksdb::AsColumnFamilyRef + 'b, + { + self.inner.snapshot.multi_get_cf(keys_cf) + } + + fn apply_snapshot(&self, read_opts: &mut ReadOptions) { + read_opts.set_snapshot(&self.inner.snapshot); + } +} + +impl SmtStorageReader for RocksDbSnapshotStorage { + fn leaf_count(&self) -> Result { + self.leaf_count_impl() + } + fn entry_count(&self) -> Result { + self.entry_count_impl() + } + fn get_leaf(&self, index: u64) -> Result, StorageError> { + self.get_leaf_impl(index) + } + fn get_leaves(&self, indices: &[u64]) -> Result>, StorageError> { + self.get_leaves_impl(indices) + } + fn has_leaves(&self) -> Result { + self.has_leaves_impl() + } + fn get_subtree(&self, index: NodeIndex) -> Result, StorageError> { + self.get_subtree_impl(index) + } + fn get_subtrees(&self, indices: &[NodeIndex]) -> Result>, StorageError> { + self.get_subtrees_impl(indices) + } + fn get_inner_node(&self, index: NodeIndex) -> Result, StorageError> { + self.get_inner_node_impl(index) + } + fn iter_leaves(&self) -> Result + '_>, StorageError> { + self.iter_leaves_impl() + } + fn iter_subtrees(&self) -> Result + '_>, StorageError> { + self.iter_subtrees_impl() + } + fn get_depth24(&self) -> Result, StorageError> { + self.get_depth24_impl() + } +} diff --git a/crates/proto/src/domain/account.rs b/crates/proto/src/domain/account.rs index 33e31b8526..3e266af849 100644 --- a/crates/proto/src/domain/account.rs +++ b/crates/proto/src/domain/account.rs @@ -992,6 +992,16 @@ impl std::fmt::Display for NetworkAccountId { } impl NetworkAccountId { + /// Wraps an `AccountId` known by the caller to belong to a network account. + /// + /// Network-ness is no longer encoded in the `AccountId` itself; it is determined by the + /// presence of the standardized `NetworkAccountNoteAllowlist` slot in the account's storage. + /// Callers must therefore verify that classification themselves (e.g. via the store's + /// `network_account_type` column or the protocol's `NetworkAccount` type) before wrapping. + pub fn new_trusted(id: AccountId) -> Self { + Self(id) + } + /// Returns the inner `AccountId`. pub fn inner(&self) -> AccountId { self.0 @@ -1003,17 +1013,6 @@ impl NetworkAccountId { } } -impl TryFrom for NetworkAccountId { - type Error = NetworkAccountError; - - fn try_from(id: AccountId) -> Result { - if !id.is_network() { - return Err(NetworkAccountError::NotNetworkAccount(id)); - } - Ok(NetworkAccountId(id)) - } -} - impl TryFrom<&NoteAttachment> for NetworkAccountId { type Error = NetworkAccountError; diff --git a/crates/proto/src/domain/digest.rs b/crates/proto/src/domain/digest.rs index fd7640ae5b..043d458f61 100644 --- a/crates/proto/src/domain/digest.rs +++ b/crates/proto/src/domain/digest.rs @@ -179,10 +179,10 @@ impl TryFrom for [Felt; 4] { } Ok([ - Felt::new(value.d0), - Felt::new(value.d1), - Felt::new(value.d2), - Felt::new(value.d3), + Felt::new_unchecked(value.d0), + Felt::new_unchecked(value.d1), + Felt::new_unchecked(value.d2), + Felt::new_unchecked(value.d3), ]) } } diff --git a/crates/proto/src/domain/merkle.rs b/crates/proto/src/domain/merkle.rs index a4d3119769..1dbbd6e9ff 100644 --- a/crates/proto/src/domain/merkle.rs +++ b/crates/proto/src/domain/merkle.rs @@ -94,7 +94,7 @@ impl TryFrom for MmrDelta { .context("data")?; Ok(MmrDelta { - forest: Forest::new(value.forest as usize), + forest: Forest::new(value.forest as usize).context("forest")?, data, }) } diff --git a/crates/proto/src/domain/note.rs b/crates/proto/src/domain/note.rs index 5d8a51f0cf..351c053485 100644 --- a/crates/proto/src/domain/note.rs +++ b/crates/proto/src/domain/note.rs @@ -7,6 +7,7 @@ use miden_protocol::note::{ NoteAttachmentScheme, NoteAttachments, NoteDetails, + NoteDetailsCommitment, NoteHeader, NoteId, NoteInclusionProof, @@ -272,7 +273,7 @@ impl TryFrom for NoteHeader { let note_id_word: Word = decode!(decoder, value.note_id)?; let metadata: NoteMetadata = decode!(decoder, value.metadata)?; - Ok(NoteHeader::new(NoteId::from_raw(note_id_word), metadata)) + Ok(NoteHeader::new(NoteDetailsCommitment::from_raw(note_id_word), metadata)) } } diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index ed2d5b7b2f..8b2f95c469 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -490,13 +490,30 @@ impl api_server::Api for RpcService { let mut request = request; request.transaction = rebuilt_tx.to_bytes(); - // Only allow deployment transactions for new network accounts - if tx.account_id().is_network() - && !tx.account_update().initial_state_commitment().is_empty() + // Block post-deployment network-account transactions from user RPC. First-deployment txs + // are allowed because the protocol-level allowlist only kicks in once the account exists. + // For non-deployment txs, ask the store whether the account is classified as a network + // account; the store is the source of truth because network-ness now lives in account + // storage and isn't derivable from an AccountId alone. Network accounts must be public, so + // private-account txs short-circuit and skip the store roundtrip. + if !tx.account_update().initial_state_commitment().is_empty() + && tx.account_id().is_public() { - return Err(Status::invalid_argument( - "Network transactions may not be submitted by users yet", - )); + let response = self + .store + .clone() + .are_network_accounts(tonic::Request::new(proto::account::AccountIdList { + account_ids: vec![tx.account_id().into()], + })) + .await + .map_err(|err| { + Status::internal(format!("network-account classification failed: {err}")) + })?; + if !response.into_inner().network_account_ids.is_empty() { + return Err(Status::invalid_argument( + "Network transactions may not be submitted by users yet", + )); + } } let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); @@ -571,11 +588,32 @@ impl api_server::Api for RpcService { ))); } - // Only allow deployment transactions for new network accounts. - for tx in proposed_batch.transactions() { - if tx.account_id().is_network() - && !tx.account_update().initial_state_commitment().is_empty() - { + // Same gate as `submit_proven_transaction`, applied to every post-deployment tx in the + // batch. One store round-trip filters the non-deployment account ids; any match fails the + // entire batch (matches the original loop semantics). Network accounts must be public, so + // private-account txs are excluded up front to skip the store roundtrip when possible. + let non_deployment_ids: Vec<_> = proposed_batch + .transactions() + .iter() + .filter(|tx| { + !tx.account_update().initial_state_commitment().is_empty() + && tx.account_id().is_public() + }) + .map(|tx| proto::account::AccountId::from(tx.account_id())) + .collect(); + + if !non_deployment_ids.is_empty() { + let response = self + .store + .clone() + .are_network_accounts(tonic::Request::new(proto::account::AccountIdList { + account_ids: non_deployment_ids, + })) + .await + .map_err(|err| { + Status::internal(format!("network-account classification failed: {err}")) + })?; + if !response.into_inner().network_account_ids.is_empty() { return Err(Status::invalid_argument( "Network transactions may not be submitted by users yet", )); diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 362980f81b..e8eb6fa3a2 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -30,7 +30,7 @@ use miden_protocol::account::{ AccountStorageMode, AccountType, }; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::testing::noop_auth_component::NoopAuthComponent; use miden_protocol::transaction::{ProvenTransaction, TxAccountUpdate}; use miden_protocol::utils::serde::Serializable; @@ -103,6 +103,36 @@ fn build_test_proven_tx( .unwrap() } +/// Same as `build_test_proven_tx` but lets the caller supply the `AccountId`. Uses a non-empty +/// `initial_state_commitment` so the result is a post-deployment tx. +fn build_test_proven_tx_with_id( + account_id: AccountId, + account: &Account, + delta: &AccountDelta, + genesis: Word, +) -> ProvenTransaction { + let account_update = TxAccountUpdate::new( + account_id, + [8; 32].try_into().unwrap(), + account.to_commitment(), + delta.to_commitment(), + AccountUpdateDetails::Delta(delta.clone()), + ) + .unwrap(); + + ProvenTransaction::new( + account_update, + Vec::::new(), + Vec::::new(), + 0.into(), + genesis, + test_fee(), + u32::MAX.into(), + ExecutionProof::new_dummy(), + ) + .unwrap() +} + #[tokio::test] async fn rpc_server_accepts_requests_without_accept_header() { // Start the RPC. @@ -390,6 +420,63 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_reference_block() { shutdown_store(store_runtime).await; } +#[tokio::test] +async fn rpc_rejects_post_deployment_network_account_tx() { + let (_, rpc_addr, store_listener) = start_rpc().await; + let (store_runtime, data_directory, genesis, _store_addr) = start_store(store_listener).await; + + // Wait for the store to be ready before sending requests. + tokio::time::sleep(Duration::from_millis(100)).await; + + // Build a client that advertises the right `application/vnd.miden` content type so + // tx-submission requests reach the handler. + let mut rpc_client = + miden_node_proto::clients::Builder::new(Url::parse(&format!("http://{rpc_addr}")).unwrap()) + .without_tls() + .with_timeout(Duration::from_secs(5)) + .without_metadata_version() + .with_metadata_genesis(genesis.to_hex()) + .without_otel_context_injection() + .connect_lazy::(); + + // Seed a row marking a known AccountId as a network account directly in the store's SQLite DB. + // The store uses WAL mode so a secondary connection is safe. + let network_account_id = AccountId::dummy( + [7u8; 15], + AccountIdVersion::Version1, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + miden_node_store::test_support::seed_network_account( + &data_directory.path().join("miden-store.sqlite3"), + network_account_id, + ); + + // Build a non-deployment tx for that account. + let (account, account_delta) = build_test_account([0; 32]); + let tx = build_test_proven_tx_with_id(network_account_id, &account, &account_delta, genesis); + let request = proto::transaction::ProvenTransaction { + transaction: tx.to_bytes(), + transaction_inputs: None, + }; + + let response = rpc_client.submit_proven_tx(request).await; + assert!(response.is_err()); + let err = response.as_ref().unwrap_err().message(); + assert!( + err.contains("Network transactions may not be submitted by users yet"), + "expected the network-tx gate error, got: {err}" + ); + + shutdown_store(store_runtime).await; +} + +// Batch-path coverage for the network-account gate is provided manually. Building a valid +// `ProposedBatch` + `ProvenBatch` in this test harness would require duplicating LocalBatchProver +// setup. The query layer is covered by the unit test in store::db::tests, and the gRPC wiring is +// the same as `submit_proven_transaction` (covered by +// `rpc_rejects_post_deployment_network_account_tx`). + #[tokio::test] async fn rpc_server_rejects_tx_submissions_without_genesis() { // Start the RPC. @@ -527,7 +614,7 @@ async fn start_store(store_listener: TcpListener) -> (Runtime, TempDir, Word, So let data_directory = tempfile::tempdir().expect("tempdir should be created"); let config = GenesisConfig::default(); - let signer = SecretKey::new(); + let signer = SigningKey::new(); let (genesis_state, _) = config.into_state(signer.public_key()).unwrap(); let genesis_block = genesis_state .clone() @@ -710,25 +797,26 @@ fn sync_chain_mmr_block_header_matches_chain_commitment() { for i in 0..5u32 { let chain_commitment = server_mmr.peaks().hash_peaks(); let header = BlockHeader::mock(i, Some(chain_commitment), None, &[], Word::default()); - server_mmr.add(header.commitment()); + server_mmr.add(header.commitment()).unwrap(); headers.push(header); } // Client bootstraps with genesis. - let mut client_mmr = PartialMmr::from_peaks(MmrPeaks::new(Forest::new(0), vec![]).unwrap()); - client_mmr.add(headers[0].commitment(), false); + let mut client_mmr = + PartialMmr::from_peaks(MmrPeaks::new(Forest::new(0).unwrap(), vec![]).unwrap()); + 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), Forest::new(2)).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()); - client_mmr.add(headers[2].commitment(), false); + 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), Forest::new(4)).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()); - client_mmr.add(headers[4].commitment(), false); + client_mmr.add(headers[4].commitment(), false).unwrap(); assert_eq!(client_mmr.peaks().hash_peaks(), server_mmr.peaks().hash_peaks()); } diff --git a/crates/store/build.rs b/crates/store/build.rs index 09bf745d32..634a9826b8 100644 --- a/crates/store/build.rs +++ b/crates/store/build.rs @@ -45,16 +45,16 @@ fn generate_agglayer_sample_accounts() { fs_err::create_dir_all(&samples_dir).expect("Failed to create samples directory"); // Use deterministic seeds for reproducible builds. WARNING: DO NOT USE THESE IN PRODUCTION - let bridge_seed: Word = Word::new([Felt::new(1u64); 4]); - let eth_faucet_seed: Word = Word::new([Felt::new(2u64); 4]); - let usdc_faucet_seed: Word = Word::new([Felt::new(3u64); 4]); + let bridge_seed: Word = Word::new([Felt::new_unchecked(1u64); 4]); + let eth_faucet_seed: Word = Word::new([Felt::new_unchecked(2u64); 4]); + let usdc_faucet_seed: Word = Word::new([Felt::new_unchecked(3u64); 4]); // Create bridge admin and GER manager as proper wallet accounts. WARNING: DO NOT USE THESE IN // PRODUCTION let bridge_admin_key = - SecretKey::with_rng(&mut RandomCoin::new(Word::new([Felt::new(4u64); 4]))); + SecretKey::with_rng(&mut RandomCoin::new(Word::new([Felt::new_unchecked(4u64); 4]))); let ger_manager_key = - SecretKey::with_rng(&mut RandomCoin::new(Word::new([Felt::new(5u64); 4]))); + SecretKey::with_rng(&mut RandomCoin::new(Word::new([Felt::new_unchecked(5u64); 4]))); let bridge_admin = create_basic_wallet( [4u8; 32], @@ -96,8 +96,8 @@ fn generate_agglayer_sample_accounts() { eth_faucet_seed, "ETH", 8, - Felt::new(1_000_000_000), - Felt::new(0), + Felt::new_unchecked(1_000_000_000), + Felt::new_unchecked(0), bridge_account_id, ð_origin_address, 0u32, @@ -110,8 +110,8 @@ fn generate_agglayer_sample_accounts() { usdc_faucet_seed, "USDC", 6, - Felt::new(10_000_000_000), - Felt::new(0), + Felt::new_unchecked(10_000_000_000), + Felt::new_unchecked(0), bridge_account_id, &usdc_origin_address, 0u32, diff --git a/crates/store/src/account_state_forest/mod.rs b/crates/store/src/account_state_forest/mod.rs index fdd63f682c..3d793fa71a 100644 --- a/crates/store/src/account_state_forest/mod.rs +++ b/crates/store/src/account_state_forest/mod.rs @@ -356,7 +356,10 @@ impl AccountStateForest { let entries = self.forest.entries(tree).map_err(Self::map_forest_error_to_witness)?; let assets = entries .take(AccountVaultDetails::MAX_RETURN_ENTRIES + 1) - .map(|entry| Asset::from_key_value_words(entry.key, entry.value)) + .map(|entry| { + let entry = entry.map_err(Self::map_forest_error_to_witness)?; + Asset::from_key_value_words(entry.key, entry.value).map_err(WitnessError::from) + }) .collect::, _>>()?; Ok(AccountVaultDetails::from_assets(assets)) @@ -402,12 +405,12 @@ impl AccountStateForest { let lineage = Self::storage_lineage_id(account_id, slot_name); let tree = self.get_tree_id(lineage, block_num)?; - Some( - self.forest - .entries(tree) - .map_err(Self::map_forest_error) - .map(|entries| entries.take(limit).map(|entry| (entry.key, entry.value)).collect()), - ) + Some(self.forest.entries(tree).map_err(Self::map_forest_error).and_then(|entries| { + entries + .take(limit) + .map(|entry| entry.map(|e| (e.key, e.value)).map_err(Self::map_forest_error)) + .collect() + })) } /// Returns all storage map entries when the forest and reverse-key cache contain enough data. @@ -725,7 +728,7 @@ impl AccountStateForest { asset.add(delta)? }; - let value = if updated.amount() == 0 { + let value = if updated.amount().as_u64() == 0 { EMPTY_WORD } else { updated.to_value_word() diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 965ef439d9..b1348d7dc5 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -470,6 +470,18 @@ impl Db { .await } + /// Returns the subset of the provided account IDs that currently classify as network accounts. + #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] + pub async fn select_network_accounts_subset( + &self, + account_ids: Vec, + ) -> Result> { + self.transact("Filter network accounts subset", move |conn| { + queries::select_network_accounts_subset(conn, &account_ids) + }) + .await + } + /// Returns network account IDs within the specified block range (based on account creation /// block). /// diff --git a/crates/store/src/db/models/conv.rs b/crates/store/src/db/models/conv.rs index 25b6047f9e..66524b307b 100644 --- a/crates/store/src/db/models/conv.rs +++ b/crates/store/src/db/models/conv.rs @@ -200,7 +200,7 @@ pub(crate) fn nullifier_prefix_to_raw_sql(prefix: u16) -> i32 { #[inline(always)] pub(crate) fn raw_sql_to_nonce(raw: i64) -> Felt { debug_assert!(raw >= 0); - Felt::new(raw as u64) + Felt::new_unchecked(raw as u64) } #[inline(always)] pub(crate) fn nonce_to_raw_sql(nonce: Felt) -> i64 { diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 53dacbeab3..4f93af0906 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::num::NonZeroUsize; use std::ops::RangeInclusive; @@ -18,7 +18,11 @@ use diesel::{ SqliteConnection, }; use miden_node_proto::domain::account::{AccountInfo, AccountSummary}; -use miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES; +use miden_node_utils::limiter::{ + MAX_RESPONSE_PAYLOAD_BYTES, + QueryParamAccountIdLimit, + QueryParamLimiter, +}; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::account::{ Account, @@ -38,6 +42,7 @@ use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; use miden_protocol::block::{BlockAccountUpdate, BlockNumber}; use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_protocol::{Felt, Word}; +use miden_standards::account::auth::NetworkAccountNoteAllowlist; use crate::COMPONENT; use crate::db::models::conv::{SqlTypeConvert, nonce_to_raw_sql, raw_sql_to_nonce}; @@ -147,7 +152,7 @@ pub(crate) fn select_account( // Backfill account details from database For private accounts, we don't store full details in // the database - let details = if account_id.has_public_state() { + let details = if account_id.is_public() { Some(select_full_account(conn, account_id)?) } else { None @@ -543,7 +548,7 @@ pub(crate) fn select_account_vault_assets( const ROW_OVERHEAD_BYTES: usize = 2 * size_of::() + size_of::(); // key + asset + block_num const MAX_ROWS: usize = MAX_RESPONSE_PAYLOAD_BYTES / ROW_OVERHEAD_BYTES; - if !account_id.has_public_state() { + if !account_id.is_public() { return Err(DatabaseError::AccountNotPublic(account_id)); } @@ -790,7 +795,7 @@ pub(crate) fn select_account_storage_map_values_paged( ) -> Result { use schema::account_storage_map_values as t; - if !account_id.has_public_state() { + if !account_id.is_public() { return Err(DatabaseError::AccountNotPublic(account_id)); } @@ -1150,7 +1155,7 @@ fn prepare_full_account_update( for asset in account.vault().assets() { // Only insert assets with non-zero values for fungible assets let should_insert = match asset { - Asset::Fungible(fungible) => fungible.amount() > 0, + Asset::Fungible(fungible) => fungible.amount().as_u64() > 0, Asset::NonFungible(_) => true, }; if should_insert { @@ -1194,7 +1199,7 @@ fn prepare_partial_account_update( } else { prev_asset.add(delta)? }; - let update_or_remove = if new_balance.amount() == 0 { + let update_or_remove = if new_balance.amount().as_u64() == 0 { None } else { Some(Asset::from(new_balance)) @@ -1251,7 +1256,7 @@ fn prepare_partial_account_update( .ok_or_else(|| { DatabaseError::DataCorrupted(format!("Nonce overflow for account {account_id}")) })?; - let new_nonce = Felt::new(new_nonce_value); + let new_nonce = Felt::new_unchecked(new_nonce_value); // Create minimal account state data for the row insert. let account_state = PartialAccountState { @@ -1279,12 +1284,69 @@ fn prepare_partial_account_update( Ok((AccountStateForInsert::PartialState(account_state), storage, assets)) } +/// Reads the network-account classification of the latest row for `account_id_bytes`. +/// +/// Returns `Ok(None)` if no row exists for this account. +fn select_latest_network_account_type( + conn: &mut SqliteConnection, + account_id_bytes: &[u8], +) -> Result, DatabaseError> { + let raw: Option = QueryDsl::select( + schema::accounts::table.filter( + schema::accounts::account_id + .eq(account_id_bytes) + .and(schema::accounts::is_latest.eq(true)), + ), + schema::accounts::network_account_type, + ) + .first::(conn) + .optional() + .map_err(DatabaseError::Diesel)?; + + raw.map(NetworkAccountType::from_raw_sql) + .transpose() + .map_err(DatabaseError::from) +} + +/// Returns the subset of `account_ids` whose latest committed state is a network account. +/// +/// Unknown ids and non-network accounts are silently omitted. +pub(crate) fn select_network_accounts_subset( + conn: &mut SqliteConnection, + account_ids: &[AccountId], +) -> Result, DatabaseError> { + QueryParamAccountIdLimit::check(account_ids.len())?; + let id_bytes: Vec> = + account_ids.iter().map(miden_crypto::utils::Serializable::to_bytes).collect(); + + let rows: Vec> = + SelectDsl::select(schema::accounts::table, schema::accounts::account_id) + .filter( + schema::accounts::account_id + .eq_any(&id_bytes) + .and( + schema::accounts::network_account_type + .eq(NetworkAccountType::Network.to_raw_sql()), + ) + .and(schema::accounts::is_latest.eq(true)), + ) + .load::>(conn) + .map_err(DatabaseError::Diesel)?; + + rows.into_iter() + .map(|bytes| { + AccountId::read_from_bytes(&bytes).map_err(DatabaseError::DeserializationError) + }) + .collect() +} + /// Attention: Assumes the account details are NOT null! The schema explicitly allows this though! #[tracing::instrument( target = COMPONENT, skip_all, err, )] +#[expect(clippy::too_many_lines)] pub(crate) fn upsert_accounts( conn: &mut SqliteConnection, accounts: &[BlockAccountUpdate], @@ -1296,12 +1358,6 @@ pub(crate) fn upsert_accounts( let account_id_bytes = account_id.to_bytes(); let block_num_raw = block_num.to_raw_sql(); - let network_account_type = if account_id.is_network() { - NetworkAccountType::Network - } else { - NetworkAccountType::None - }; - // Preserve the original creation block when updating existing accounts. let created_at_block_raw = QueryDsl::select( schema::accounts::table.filter( @@ -1340,6 +1396,27 @@ pub(crate) fn upsert_accounts( }, }; + // Classify the account as network or not by looking for the standardized + // `NetworkAccountNoteAllowlist` slot in storage. Only full account states let us inspect + // storage directly; for partial updates we inherit the latest classification from the DB. + let network_account_type = match &account_state { + AccountStateForInsert::Private => NetworkAccountType::None, + AccountStateForInsert::FullAccount(account) => { + if account.is_public() + && NetworkAccountNoteAllowlist::try_from(account.storage()).is_ok() + { + NetworkAccountType::Network + } else { + NetworkAccountType::None + } + }, + AccountStateForInsert::PartialState(_) => { + // We do not have full storage here; carry the previous classification forward. + select_latest_network_account_type(conn, &account_id_bytes)? + .unwrap_or(NetworkAccountType::None) + }, + }; + // Insert account _code_ for full accounts (new account creation) if let AccountStateForInsert::FullAccount(ref account) = account_state { let code = account.code(); @@ -1436,7 +1513,7 @@ pub(crate) struct AccountRowInsert { impl AccountRowInsert { /// Creates an insert row for a private account (no public state). - fn new_private( + pub(crate) fn new_private( account_id: AccountId, network_account_type: NetworkAccountType, account_commitment: Word, diff --git a/crates/store/src/db/models/queries/accounts/delta.rs b/crates/store/src/db/models/queries/accounts/delta.rs index 88e5f8d68b..56b70fbfcc 100644 --- a/crates/store/src/db/models/queries/accounts/delta.rs +++ b/crates/store/src/db/models/queries/accounts/delta.rs @@ -181,7 +181,7 @@ pub(super) fn select_vault_balances_by_faucet_ids( if let Some(asset_bytes) = maybe_asset_bytes { let asset = Asset::read_from_bytes(&asset_bytes)?; if let Asset::Fungible(fungible) = asset { - balances.insert(fungible.faucet_id(), fungible.amount()); + balances.insert(fungible.faucet_id(), fungible.amount().as_u64()); } } } 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 3cacb46b0d..ef7190a1fc 100644 --- a/crates/store/src/db/models/queries/accounts/delta/tests.rs +++ b/crates/store/src/db/models/queries/accounts/delta/tests.rs @@ -30,7 +30,7 @@ use miden_protocol::account::{ }; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::block::{BlockAccountUpdate, BlockHeader, BlockNumber}; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, @@ -61,7 +61,7 @@ fn setup_test_db() -> SqliteConnection { fn insert_block_header(conn: &mut SqliteConnection, block_num: BlockNumber) { use crate::db::schema::block_headers; - let secret_key = SecretKey::new(); + let secret_key = SigningKey::new(); let block_header = BlockHeader::new( 1_u8.into(), Word::default(), @@ -124,10 +124,10 @@ fn optimized_delta_matches_full_account_method() { // Create an account with value slots only (no map slots to avoid SmtForest complexity) let slot_value_initial = Word::from([ - Felt::new(INITIAL_SLOT_VALUES[0]), - Felt::new(INITIAL_SLOT_VALUES[1]), - Felt::new(INITIAL_SLOT_VALUES[2]), - Felt::new(INITIAL_SLOT_VALUES[3]), + Felt::new_unchecked(INITIAL_SLOT_VALUES[0]), + Felt::new_unchecked(INITIAL_SLOT_VALUES[1]), + Felt::new_unchecked(INITIAL_SLOT_VALUES[2]), + Felt::new_unchecked(INITIAL_SLOT_VALUES[3]), ]); let component_storage = vec![ @@ -186,10 +186,10 @@ fn optimized_delta_matches_full_account_method() { // - Add 500 tokens to the vault (starting from empty) let new_slot_value = Word::from([ - Felt::new(UPDATED_SLOT_VALUES[0]), - Felt::new(UPDATED_SLOT_VALUES[1]), - Felt::new(UPDATED_SLOT_VALUES[2]), - Felt::new(UPDATED_SLOT_VALUES[3]), + Felt::new_unchecked(UPDATED_SLOT_VALUES[0]), + Felt::new_unchecked(UPDATED_SLOT_VALUES[1]), + Felt::new_unchecked(UPDATED_SLOT_VALUES[2]), + Felt::new_unchecked(UPDATED_SLOT_VALUES[3]), ]); let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); @@ -215,7 +215,7 @@ fn optimized_delta_matches_full_account_method() { }; // Create a partial delta - let nonce_delta = Felt::new(NONCE_DELTA); + let nonce_delta = Felt::new_unchecked(NONCE_DELTA); let partial_delta = AccountDelta::new( full_account_before.id(), storage_delta.clone(), @@ -226,8 +226,9 @@ fn optimized_delta_matches_full_account_method() { assert!(!partial_delta.is_full_state(), "Delta should be partial, not full state"); // Construct the expected final account by applying the delta - let expected_nonce = - Felt::new(full_account_before.nonce().as_canonical_u64() + nonce_delta.as_canonical_u64()); + let expected_nonce = Felt::new_unchecked( + full_account_before.nonce().as_canonical_u64() + nonce_delta.as_canonical_u64(), + ); let expected_code_commitment = full_account_before.code().commitment(); let mut expected_account = full_account_before.clone(); @@ -283,7 +284,7 @@ fn optimized_delta_matches_full_account_method() { assert_eq!(vault_assets_after.len(), 1, "Should have 1 vault asset"); assert_matches!(&vault_assets_after[0], Asset::Fungible(f) => { assert_eq!(f.faucet_id(), faucet_id, "Faucet ID should match"); - assert_eq!(f.amount(), VAULT_AMOUNT, "Amount should be 500"); + assert_eq!(f.amount().as_u64(), VAULT_AMOUNT, "Amount should be 500"); }); // Verify the account commitment matches @@ -390,7 +391,7 @@ fn optimized_delta_updates_non_empty_vault() { account.id(), AccountStorageDelta::new(), vault_delta, - Felt::new(NONCE_DELTA), + Felt::new_unchecked(NONCE_DELTA), ) .unwrap(); @@ -412,7 +413,7 @@ fn optimized_delta_updates_non_empty_vault() { assert_eq!(vault_assets_after.len(), 1, "Should have 1 vault asset"); assert_matches!(&vault_assets_after[0], Asset::Fungible(f) => { assert_eq!(f.faucet_id(), faucet_id_1, "Faucet ID should match"); - assert_eq!(f.amount(), ADDED_AMOUNT_BLOCK_2, "Amount should match"); + assert_eq!(f.amount().as_u64(), ADDED_AMOUNT_BLOCK_2, "Amount should match"); }); let full_account_after = select_full_account(&mut conn, account.id()) @@ -431,7 +432,7 @@ fn optimized_delta_updates_non_empty_vault() { account.id(), AccountStorageDelta::new(), vault_delta_3, - Felt::new(NONCE_DELTA), + Felt::new_unchecked(NONCE_DELTA), ) .unwrap(); @@ -454,7 +455,7 @@ fn optimized_delta_updates_non_empty_vault() { assert_eq!(final_assets.len(), 1, "Should have exactly 1 vault asset"); assert_matches!(&final_assets[0], Asset::Fungible(f) => { assert_eq!(f.faucet_id(), faucet_id_1); - assert_eq!(f.amount(), ADDED_AMOUNT_BLOCK_2 + ADDED_AMOUNT_BLOCK_3, "Expected total of 400"); + assert_eq!(f.amount().as_u64(), ADDED_AMOUNT_BLOCK_2 + ADDED_AMOUNT_BLOCK_3, "Expected total of 400"); }); assert_eq!(full_account_final.vault().root(), expected_vault_root_3); @@ -480,22 +481,22 @@ fn optimized_delta_updates_storage_map_header() { let mut conn = setup_test_db(); let map_key = StorageMapKey::new(Word::from([ - Felt::new(MAP_KEY_VALUES[0]), - Felt::new(MAP_KEY_VALUES[1]), - Felt::new(MAP_KEY_VALUES[2]), - Felt::new(MAP_KEY_VALUES[3]), + Felt::new_unchecked(MAP_KEY_VALUES[0]), + Felt::new_unchecked(MAP_KEY_VALUES[1]), + Felt::new_unchecked(MAP_KEY_VALUES[2]), + Felt::new_unchecked(MAP_KEY_VALUES[3]), ])); let map_value_initial = Word::from([ - Felt::new(MAP_VALUE_INITIAL[0]), - Felt::new(MAP_VALUE_INITIAL[1]), - Felt::new(MAP_VALUE_INITIAL[2]), - Felt::new(MAP_VALUE_INITIAL[3]), + Felt::new_unchecked(MAP_VALUE_INITIAL[0]), + Felt::new_unchecked(MAP_VALUE_INITIAL[1]), + Felt::new_unchecked(MAP_VALUE_INITIAL[2]), + Felt::new_unchecked(MAP_VALUE_INITIAL[3]), ]); let map_value_updated = Word::from([ - Felt::new(MAP_VALUE_UPDATED[0]), - Felt::new(MAP_VALUE_UPDATED[1]), - Felt::new(MAP_VALUE_UPDATED[2]), - Felt::new(MAP_VALUE_UPDATED[3]), + Felt::new_unchecked(MAP_VALUE_UPDATED[0]), + Felt::new_unchecked(MAP_VALUE_UPDATED[1]), + Felt::new_unchecked(MAP_VALUE_UPDATED[2]), + Felt::new_unchecked(MAP_VALUE_UPDATED[3]), ]); let storage_map = StorageMap::with_entries(vec![(map_key, map_value_initial)]).unwrap(); @@ -551,7 +552,7 @@ fn optimized_delta_updates_storage_map_header() { account.id(), storage_delta, AccountVaultDelta::default(), - Felt::new(NONCE_DELTA), + Felt::new_unchecked(NONCE_DELTA), ) .unwrap(); @@ -612,10 +613,10 @@ fn upsert_private_account() { ); let account_commitment = Word::from([ - Felt::new(COMMITMENT_WORDS[0]), - Felt::new(COMMITMENT_WORDS[1]), - Felt::new(COMMITMENT_WORDS[2]), - Felt::new(COMMITMENT_WORDS[3]), + Felt::new_unchecked(COMMITMENT_WORDS[0]), + Felt::new_unchecked(COMMITMENT_WORDS[1]), + Felt::new_unchecked(COMMITMENT_WORDS[2]), + Felt::new_unchecked(COMMITMENT_WORDS[3]), ]); // Insert as private account @@ -667,10 +668,10 @@ fn upsert_full_state_delta() { // Create an account with storage let slot_value = Word::from([ - Felt::new(SLOT_VALUES[0]), - Felt::new(SLOT_VALUES[1]), - Felt::new(SLOT_VALUES[2]), - Felt::new(SLOT_VALUES[3]), + Felt::new_unchecked(SLOT_VALUES[0]), + Felt::new_unchecked(SLOT_VALUES[1]), + Felt::new_unchecked(SLOT_VALUES[2]), + Felt::new_unchecked(SLOT_VALUES[3]), ]); let component_storage = vec![StorageSlot::with_value(StorageSlotName::mock(SLOT_INDEX), slot_value)]; diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index eee9f69795..c4e56a25df 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -39,7 +39,7 @@ use miden_protocol::account::{ StorageSlotType, }; use miden_protocol::block::{BlockAccountUpdate, BlockHeader, BlockNumber}; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_protocol::{EMPTY_WORD, Felt, Word}; use miden_standards::account::auth::AuthSingleSig; @@ -144,7 +144,12 @@ fn create_test_account_with_storage() -> (Account, AccountId) { AccountStorageMode::Public, ); - let storage_value = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + let storage_value = Word::from([ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]); let component_storage = vec![StorageSlot::with_value(StorageSlotName::mock(0), storage_value)]; let account_component_code = CodeBuilder::default() @@ -175,7 +180,7 @@ fn create_test_account_with_storage() -> (Account, AccountId) { fn insert_block_header(conn: &mut SqliteConnection, block_num: BlockNumber) { use crate::db::schema::block_headers; - let secret_key = SecretKey::new(); + let secret_key = SigningKey::new(); let block_header = BlockHeader::new( 1_u8.into(), Word::default(), @@ -520,8 +525,12 @@ fn test_upsert_accounts_updates_is_latest_flag() { upsert_accounts(&mut conn, &[account_update_1], block_num_1).expect("First upsert failed"); // Create modified account with different storage value - let storage_value_modified = - Word::from([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]); + let storage_value_modified = Word::from([ + Felt::new_unchecked(10), + Felt::new_unchecked(20), + Felt::new_unchecked(30), + Felt::new_unchecked(40), + ]); let component_storage_modified = vec![StorageSlot::with_value(StorageSlotName::mock(0), storage_value_modified)]; @@ -614,9 +623,24 @@ fn test_upsert_accounts_with_multiple_storage_slots() { AccountStorageMode::Public, ); - let slot_value_1 = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); - let slot_value_2 = Word::from([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]); - let slot_value_3 = Word::from([Felt::new(9), Felt::new(10), Felt::new(11), Felt::new(12)]); + let slot_value_1 = Word::from([ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]); + let slot_value_2 = Word::from([ + Felt::new_unchecked(5), + Felt::new_unchecked(6), + Felt::new_unchecked(7), + Felt::new_unchecked(8), + ]); + let slot_value_3 = Word::from([ + Felt::new_unchecked(9), + Felt::new_unchecked(10), + Felt::new_unchecked(11), + Felt::new_unchecked(12), + ]); let component_storage = vec![ StorageSlot::with_value(StorageSlotName::mock(0), slot_value_1), @@ -773,9 +797,9 @@ fn test_select_latest_account_storage_ordering_semantics() { let key_2 = StorageMapKey::from_index(2); let key_3 = StorageMapKey::from_index(3); - let value_1 = Word::from([Felt::new(10), Felt::ZERO, Felt::ZERO, Felt::ZERO]); - let value_2 = Word::from([Felt::new(20), Felt::ZERO, Felt::ZERO, Felt::ZERO]); - let value_3 = Word::from([Felt::new(30), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_1 = Word::from([Felt::new_unchecked(10), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_2 = Word::from([Felt::new_unchecked(20), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_3 = Word::from([Felt::new_unchecked(30), Felt::ZERO, Felt::ZERO, Felt::ZERO]); let mut entries = vec![(key_2, value_2), (key_1, value_1), (key_3, value_3)]; entries.reverse(); @@ -818,8 +842,8 @@ fn test_select_latest_account_storage_multiple_slots() { let key_a = StorageMapKey::from_index(1); let key_b = StorageMapKey::from_index(2); - let value_a = Word::from([Felt::new(11), Felt::ZERO, Felt::ZERO, Felt::ZERO]); - let value_b = Word::from([Felt::new(22), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_a = Word::from([Felt::new_unchecked(11), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_b = Word::from([Felt::new_unchecked(22), Felt::ZERO, Felt::ZERO, Felt::ZERO]); let map_a = StorageMap::with_entries(vec![(key_a, value_a)]).unwrap(); let map_b = StorageMap::with_entries(vec![(key_b, value_b)]).unwrap(); @@ -881,9 +905,9 @@ fn test_select_latest_account_storage_slot_updates() { let key_1 = StorageMapKey::from_index(1); let key_2 = StorageMapKey::from_index(2); - let value_1 = Word::from([Felt::new(10), Felt::ZERO, Felt::ZERO, Felt::ZERO]); - let value_2 = Word::from([Felt::new(20), Felt::ZERO, Felt::ZERO, Felt::ZERO]); - let value_3 = Word::from([Felt::new(30), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_1 = Word::from([Felt::new_unchecked(10), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_2 = Word::from([Felt::new_unchecked(20), Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value_3 = Word::from([Felt::new_unchecked(30), Felt::ZERO, Felt::ZERO, Felt::ZERO]); let account = create_account_with_map_storage(slot_name.clone(), vec![(key_1, value_1)]); let account_id = account.id(); @@ -903,9 +927,13 @@ fn test_select_latest_account_storage_slot_updates() { StorageSlotDelta::Map(map_delta), )])); - let partial_delta = - AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), Felt::new(1)) - .unwrap(); + let partial_delta = AccountDelta::new( + account_id, + storage_delta, + AccountVaultDelta::default(), + Felt::new_unchecked(1), + ) + .unwrap(); let mut expected_account = account.clone(); expected_account.apply_delta(&partial_delta).unwrap(); @@ -1000,7 +1028,7 @@ fn test_select_account_vault_at_block_historical_with_updates() { .expect("Query at block 1 should succeed"); assert_eq!(assets_at_block_1.len(), 1, "Should have 1 asset at block 1"); - assert_matches!(&assets_at_block_1[0], Asset::Fungible(f) if f.amount() == 1000); + assert_matches!(&assets_at_block_1[0], Asset::Fungible(f) if f.amount().as_u64() == 1000); // Query at block 2: should see vault_key_1 with 2000 tokens AND vault_key_2 with 500 tokens let assets_at_block_2 = select_account_vault_at_block(&mut conn, account_id, block_2) @@ -1011,7 +1039,7 @@ fn test_select_account_vault_at_block_historical_with_updates() { // Find the amounts (order may vary) let amounts: Vec = assets_at_block_2 .iter() - .map(|a| assert_matches!(a, Asset::Fungible(f) => f.amount())) + .map(|a| assert_matches!(a, Asset::Fungible(f) => f.amount().as_u64())) .collect(); assert!(amounts.contains(&2000), "Block 2 should have vault_key_1 with 2000 tokens"); @@ -1025,7 +1053,7 @@ fn test_select_account_vault_at_block_historical_with_updates() { let amounts: Vec = assets_at_block_3 .iter() - .map(|a| assert_matches!(a, Asset::Fungible(f) => f.amount())) + .map(|a| assert_matches!(a, Asset::Fungible(f) => f.amount().as_u64())) .collect(); assert!(amounts.contains(&3000), "Block 3 should have vault_key_1 with 3000 tokens"); @@ -1082,7 +1110,7 @@ fn test_select_account_vault_at_block_exponential_updates() { let expected_amount = 1u64 << index; assert_matches!( &assets_at_block[0], - Asset::Fungible(f) if f.amount() == expected_amount + Asset::Fungible(f) if f.amount().as_u64() == expected_amount ); } } @@ -1153,7 +1181,7 @@ fn test_select_account_vault_at_block_with_deletion() { let assets_at_block_3 = select_account_vault_at_block(&mut conn, account_id, block_3) .expect("Query at block 3 should succeed"); assert_eq!(assets_at_block_3.len(), 1, "Should have 1 asset at block 3"); - assert_matches!(&assets_at_block_3[0], Asset::Fungible(f) if f.amount() == 2000); + assert_matches!(&assets_at_block_3[0], Asset::Fungible(f) if f.amount().as_u64() == 2000); } // ACCOUNT CODE PRUNING TESTS @@ -1208,7 +1236,7 @@ fn build_account_with_code(push_value: u32) -> Account { component_code, vec![StorageSlot::with_value( StorageSlotName::mock(0), - Word::from([Felt::new(1), Felt::ZERO, Felt::ZERO, Felt::ZERO]), + Word::from([Felt::new_unchecked(1), Felt::ZERO, Felt::ZERO, Felt::ZERO]), )], AccountComponentMetadata::new( "code_prune_test", @@ -1371,3 +1399,68 @@ fn test_prune_account_code_retains_revisited_code() { "latest account must reference code A" ); } + +#[test] +#[miden_node_test_macro::enable_logging] +fn network_accounts_subset_classifies_correctly() { + use crate::db::models::queries::accounts::{ + AccountRowInsert, + NetworkAccountType, + select_network_accounts_subset, + }; + + let mut conn = setup_test_db(); + let block_num = BlockNumber::from(1); + insert_block_header(&mut conn, block_num); + + // Three accounts with distinct classifications. AccountIds are dummies — the queries only care + // about the (account_id, network_account_type, is_latest) tuple, not protocol-level validity. + let network_id = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version1, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + let public_id = AccountId::dummy( + [2u8; 15], + AccountIdVersion::Version1, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + let private_id = AccountId::dummy( + [3u8; 15], + AccountIdVersion::Version1, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let unknown_id = AccountId::dummy( + [4u8; 15], + AccountIdVersion::Version1, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + + for (id, ty) in [ + (network_id, NetworkAccountType::Network), + (public_id, NetworkAccountType::None), + (private_id, NetworkAccountType::None), + ] { + let row = AccountRowInsert::new_private(id, ty, Word::default(), block_num, block_num); + diesel::insert_into(crate::db::schema::accounts::table) + .values(&row) + .execute(&mut conn) + .unwrap(); + } + + // Batched lookup returns only the network-classified id; public, private, and unknown ids are + // all omitted. + let subset = + select_network_accounts_subset(&mut conn, &[network_id, public_id, private_id, unknown_id]) + .unwrap(); + assert_eq!(subset.len(), 1); + assert!(subset.contains(&network_id)); + + // Empty input slice short-circuits to an empty result. + let empty = select_network_accounts_subset(&mut conn, &[]).unwrap(); + assert!(empty.is_empty()); +} diff --git a/crates/store/src/db/models/queries/transactions.rs b/crates/store/src/db/models/queries/transactions.rs index 4551219068..0f8e8f67d9 100644 --- a/crates/store/src/db/models/queries/transactions.rs +++ b/crates/store/src/db/models/queries/transactions.rs @@ -20,7 +20,7 @@ use miden_node_utils::limiter::{ }; use miden_protocol::account::AccountId; use miden_protocol::block::BlockNumber; -use miden_protocol::note::NoteHeader; +use miden_protocol::note::{NoteHeader, NoteId}; use miden_protocol::transaction::{ InputNoteCommitment, InputNotes, @@ -313,7 +313,7 @@ fn with_output_note_proofs( let mut all_note_commitments = Vec::new(); for raw in &raw_transactions { let notes: Vec = Deserializable::read_from_bytes(&raw.output_notes)?; - all_note_commitments.extend(notes.iter().map(NoteHeader::to_commitment)); + all_note_commitments.extend(notes.iter().map(|h| h.id().as_word())); tx_output_notes.push(notes); } @@ -332,7 +332,10 @@ fn with_output_note_proofs( // table were erased (created and consumed in the same batch). let output_note_proofs = output_notes .iter() - .filter_map(|note| output_notes_by_id.get(¬e.id()).cloned()) + .filter_map(|note| { + let key = NoteId::from_raw(note.details_commitment().as_word()); + output_notes_by_id.get(&key).cloned() + }) .collect(); let header = TransactionHeader::new_unchecked( diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 5a8b4df197..cf4e8c1c9f 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -34,7 +34,7 @@ use miden_protocol::block::{ BlockNoteTree, BlockNumber, }; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::crypto::merkle::SparseMerklePath; use miden_protocol::crypto::merkle::mmr::{Forest, Mmr}; use miden_protocol::crypto::merkle::smt::SmtProof; @@ -44,6 +44,7 @@ use miden_protocol::note::{ NoteAttachment, NoteAttachments, NoteDetails, + NoteDetailsCommitment, NoteHeader, NoteId, NoteMetadata, @@ -102,12 +103,12 @@ fn create_block(conn: &mut SqliteConnection, block_num: BlockNumber) { num_to_word(7), num_to_word(8), num_to_word(9), - SecretKey::new().public_key(), + SigningKey::new().public_key(), test_fee_params(), 11_u8.into(), ); - let dummy_signature = SecretKey::new().sign(block_header.commitment()); + let dummy_signature = SigningKey::new().sign(block_header.commitment()); conn.transaction(|conn| { queries::insert_block_header(conn, &block_header, &dummy_signature)?; @@ -201,7 +202,7 @@ fn sql_select_nullifiers() { pub fn create_note(account_id: AccountId) -> Note { let coin_seed: [u64; 4] = rand::rng().random(); - let rng = Arc::new(Mutex::new(RandomCoin::new(coin_seed.map(Felt::new).into()))); + let rng = Arc::new(Mutex::new(RandomCoin::new(coin_seed.map(Felt::new_unchecked).into()))); let mut rng = rng.lock().unwrap(); P2idNote::create( account_id, @@ -346,7 +347,7 @@ fn sql_unconsumed_network_notes() { // Create account. let account_note = - make_account_and_note(&mut conn, 0.into(), [1u8; 32], AccountStorageMode::Network); + make_account_and_note(&mut conn, 0.into(), [1u8; 32], AccountStorageMode::Public); // Create 2 blocks. create_block(&mut conn, 0.into()); @@ -751,13 +752,13 @@ fn db_block_header() { num_to_word(7), num_to_word(8), num_to_word(9), - SecretKey::new().public_key(), + SigningKey::new().public_key(), test_fee_params(), 11_u8.into(), ); // test insertion - let dummy_signature = SecretKey::new().sign(block_header.commitment()); + let dummy_signature = SigningKey::new().sign(block_header.commitment()); queries::insert_block_header(conn, &block_header, &dummy_signature).unwrap(); // test fetch unknown block header @@ -784,12 +785,12 @@ fn db_block_header() { num_to_word(17), num_to_word(18), num_to_word(19), - SecretKey::new().public_key(), + SigningKey::new().public_key(), test_fee_params(), 21_u8.into(), ); - let dummy_signature = SecretKey::new().sign(block_header2.commitment()); + let dummy_signature = SigningKey::new().sign(block_header2.commitment()); queries::insert_block_header(conn, &block_header2, &dummy_signature).unwrap(); let res = queries::select_block_header_by_block_num(conn, None).unwrap(); @@ -831,7 +832,7 @@ fn notes() { &NoteAttachments::default(), ); - let note_header = NoteHeader::new(new_note.id(), note_metadata); + let note_header = NoteHeader::new(new_note.details_commitment(), note_metadata); let values = [(note_index, ¬e_header)]; let notes_db = BlockNoteTree::with_entries(values).unwrap(); let inclusion_path = notes_db.open(note_index); @@ -840,7 +841,7 @@ fn notes() { block_num: block_num_1, note_index, note_id: new_note.id().as_word(), - note_commitment: new_note.commitment(), + note_commitment: new_note.id().as_word(), metadata: note_metadata, details: Some(NoteDetails::from(&new_note)), attachments: NoteAttachments::default(), @@ -871,7 +872,7 @@ fn notes() { block_num: block_num_2, note_index: note.note_index, note_id: new_note.id().as_word(), - note_commitment: new_note.commitment(), + note_commitment: new_note.id().as_word(), metadata: note.metadata, details: None, attachments: NoteAttachments::default(), @@ -937,7 +938,7 @@ fn note_sync_across_multiple_blocks() { PartialNoteMetadata::new(sender, NoteType::Public).with_tag(tag.into()), &NoteAttachments::default(), ); - let note_header = NoteHeader::new(new_note.id(), note_metadata); + let note_header = NoteHeader::new(new_note.details_commitment(), note_metadata); let values = [(note_index, ¬e_header)]; let notes_db = BlockNoteTree::with_entries(values).unwrap(); let inclusion_path = notes_db.open(note_index); @@ -946,7 +947,7 @@ fn note_sync_across_multiple_blocks() { block_num, note_index, note_id: new_note.id().as_word(), - note_commitment: new_note.commitment(), + note_commitment: new_note.id().as_word(), metadata: note_metadata, details: Some(NoteDetails::from(&new_note)), attachments: NoteAttachments::default(), @@ -959,10 +960,10 @@ fn note_sync_across_multiple_blocks() { // Build an MMR with enough leaves to cover all blocks (0..=3). let mut mmr = Mmr::default(); for _ in 0..=3u32 { - mmr.add(Word::default()); + mmr.add(Word::default()).unwrap(); } // Use block_end + 1 as the MMR forest, same as State::sync_notes. - let mmr_forest = Forest::new(4); + let mmr_forest = Forest::new(4).unwrap(); // A single call to get_note_sync_multi should return all 3 blocks. let block_range = BlockNumber::GENESIS..=BlockNumber::from(3); @@ -1019,7 +1020,7 @@ fn note_sync_multi_respects_payload_limit() { PartialNoteMetadata::new(sender, NoteType::Public).with_tag(tag.into()), &NoteAttachments::default(), ); - let note_header = NoteHeader::new(new_note.id(), note_metadata); + let note_header = NoteHeader::new(new_note.details_commitment(), note_metadata); let values = [(note_index, ¬e_header)]; let notes_db = BlockNoteTree::with_entries(values).unwrap(); let inclusion_path = notes_db.open(note_index); @@ -1028,7 +1029,7 @@ fn note_sync_multi_respects_payload_limit() { block_num, note_index, note_id: new_note.id().as_word(), - note_commitment: new_note.commitment(), + note_commitment: new_note.id().as_word(), metadata: note_metadata, details: Some(NoteDetails::from(&new_note)), attachments: NoteAttachments::default(), @@ -1077,7 +1078,7 @@ fn note_sync_no_matching_tags() { PartialNoteMetadata::new(sender, NoteType::Public).with_tag(10u32.into()), &NoteAttachments::default(), ); - let note_header = NoteHeader::new(new_note.id(), note_metadata); + let note_header = NoteHeader::new(new_note.details_commitment(), note_metadata); let values = [(note_index, ¬e_header)]; let notes_db = BlockNoteTree::with_entries(values).unwrap(); let inclusion_path = notes_db.open(note_index); @@ -1086,7 +1087,7 @@ fn note_sync_no_matching_tags() { block_num, note_index, note_id: new_note.id().as_word(), - note_commitment: new_note.commitment(), + note_commitment: new_note.id().as_word(), metadata: note_metadata, details: Some(NoteDetails::from(&new_note)), attachments: NoteAttachments::default(), @@ -1180,9 +1181,13 @@ fn sql_account_storage_map_values_insertion() { map2.insert(key1, value3); let delta2 = BTreeMap::from_iter([(slot_name.clone(), StorageSlotDelta::Map(map2))]); let storage2 = AccountStorageDelta::from_raw(delta2); - let delta2 = - AccountDelta::new(account_id, storage2, AccountVaultDelta::default(), Felt::new(2)) - .unwrap(); + let delta2 = AccountDelta::new( + account_id, + storage2, + AccountVaultDelta::default(), + Felt::new_unchecked(2), + ) + .unwrap(); insert_account_delta(conn, account_id, block2, &delta2); let storage_map_values = queries::select_account_storage_map_values_paged( @@ -1327,7 +1332,7 @@ fn select_storage_map_sync_values_for_network_account() { create_block(&mut conn, block_num); let (account_id, _) = - make_account_and_note(&mut conn, block_num, [42u8; 32], AccountStorageMode::Network); + make_account_and_note(&mut conn, block_num, [42u8; 32], AccountStorageMode::Public); let slot_name = StorageSlotName::mock(7); let key = StorageMapKey::from_index(1); let value = num_to_word(10); @@ -1687,11 +1692,11 @@ async fn reconstruct_storage_map_from_db_returns_limit_exceeded_for_single_block // UTILITIES // ------------------------------------------------------------------------------------------- fn num_to_word(n: u64) -> Word { - [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)].into() + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new_unchecked(n)].into() } fn num_to_storage_map_key(n: u64) -> StorageMapKey { - StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)])) + StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new_unchecked(n)])) } fn num_to_nullifier(n: u64) -> Nullifier { @@ -1742,10 +1747,7 @@ fn mock_block_transaction(account_id: AccountId, num: u64) -> TransactionHeader let input_notes = InputNotes::new_unchecked(notes); let output_notes = vec![NoteHeader::new( - NoteId::new( - Word::try_from([num, num, 0, 0]).unwrap(), - Word::try_from([0, 0, num, num]).unwrap(), - ), + NoteDetailsCommitment::from_raw(Word::try_from([num, num, 0, 0]).unwrap()), NoteMetadata::new( PartialNoteMetadata::new(account_id, NoteType::Public) .with_tag(NoteTag::new(num as u32)), @@ -2024,11 +2026,21 @@ async fn genesis_with_account_storage_map() { let storage_map = StorageMap::with_entries(vec![ ( StorageMapKey::from_index(1u32), - Word::from([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), + Word::from([ + Felt::new_unchecked(10), + Felt::new_unchecked(20), + Felt::new_unchecked(30), + Felt::new_unchecked(40), + ]), ), ( StorageMapKey::from_index(2u32), - Word::from([Felt::new(50), Felt::new(60), Felt::new(70), Felt::new(80)]), + Word::from([ + Felt::new_unchecked(50), + Felt::new_unchecked(60), + Felt::new_unchecked(70), + Felt::new_unchecked(80), + ]), ), ]) .unwrap(); @@ -2082,7 +2094,12 @@ async fn genesis_with_account_assets_and_storage() { let storage_map = StorageMap::with_entries(vec![( StorageMapKey::from_index(100u32), - Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + Word::from([ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]), )]) .unwrap(); @@ -2180,7 +2197,12 @@ async fn genesis_with_multiple_accounts() { let storage_map = StorageMap::with_entries(vec![( StorageMapKey::from_index(5u32), - Word::from([Felt::new(15), Felt::new(25), Felt::new(35), Felt::new(45)]), + Word::from([ + Felt::new_unchecked(15), + Felt::new_unchecked(25), + Felt::new_unchecked(35), + Felt::new_unchecked(45), + ]), )]) .unwrap(); @@ -2305,7 +2327,7 @@ fn serialization_symmetry_core_types() { assert_eq!(tx_id, restored, "TransactionId serialization must be symmetric"); // NoteId - let note_id = NoteId::new(num_to_word(1), num_to_word(2)); + let note_id = NoteId::from_raw(num_to_word(1)); let bytes = note_id.to_bytes(); let restored = NoteId::read_from_bytes(&bytes).unwrap(); assert_eq!(note_id, restored, "NoteId serialization must be symmetric"); @@ -2323,7 +2345,7 @@ fn serialization_symmetry_block_header() { num_to_word(7), num_to_word(8), num_to_word(9), - SecretKey::new().public_key(), + SigningKey::new().public_key(), test_fee_params(), 11_u8.into(), ); @@ -2394,8 +2416,7 @@ fn serialization_symmetry_nullifier_vec() { #[test] fn serialization_symmetry_note_id_vec() { - let note_ids: Vec = - (0..5).map(|i| NoteId::new(num_to_word(i), num_to_word(i + 100))).collect(); + let note_ids: Vec = (0..5).map(|i| NoteId::from_raw(num_to_word(i))).collect(); let bytes = note_ids.to_bytes(); let restored: Vec = Deserializable::read_from_bytes(&bytes).unwrap(); assert_eq!(note_ids, restored, "Vec serialization must be symmetric"); @@ -2416,13 +2437,13 @@ fn db_roundtrip_block_header() { num_to_word(7), num_to_word(8), num_to_word(9), - SecretKey::new().public_key(), + SigningKey::new().public_key(), test_fee_params(), 11_u8.into(), ); // Insert - let dummy_signature = SecretKey::new().sign(block_header.commitment()); + let dummy_signature = SigningKey::new().sign(block_header.commitment()); queries::insert_block_header(&mut conn, &block_header, &dummy_signature).unwrap(); // Retrieve @@ -2515,7 +2536,7 @@ fn db_roundtrip_notes() { block_num, note_index, note_id: new_note.id().as_word(), - note_commitment: new_note.commitment(), + note_commitment: new_note.id().as_word(), metadata: *new_note.metadata(), details: Some(NoteDetails::from(&new_note)), attachments: new_note.attachments().clone(), @@ -2649,11 +2670,21 @@ fn db_roundtrip_account_storage_with_maps() { let storage_map = StorageMap::with_entries(vec![ ( StorageMapKey::from_index(1u32), - Word::from([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), + Word::from([ + Felt::new_unchecked(10), + Felt::new_unchecked(20), + Felt::new_unchecked(30), + Felt::new_unchecked(40), + ]), ), ( StorageMapKey::from_index(2u32), - Word::from([Felt::new(50), Felt::new(60), Felt::new(70), Felt::new(80)]), + Word::from([ + Felt::new_unchecked(50), + Felt::new_unchecked(60), + Felt::new_unchecked(70), + Felt::new_unchecked(80), + ]), ), ]) .unwrap(); @@ -2759,7 +2790,7 @@ fn db_roundtrip_note_metadata_attachment() { create_block(&mut conn, block_num); let (account_id, _) = - make_account_and_note(&mut conn, block_num, [1u8; 32], AccountStorageMode::Network); + make_account_and_note(&mut conn, block_num, [1u8; 32], AccountStorageMode::Public); let target = NetworkAccountTarget::new(account_id, NoteExecutionHint::Always) .expect("NetworkAccountTarget creation should succeed for network account"); @@ -3191,7 +3222,7 @@ fn account_state_forest_matches_db_storage_map_roots_across_updates() { account_id, storage_2.clone(), AccountVaultDelta::default(), - Felt::new(2), + Felt::new_unchecked(2), ) .unwrap(); @@ -3221,7 +3252,7 @@ fn account_state_forest_matches_db_storage_map_roots_across_updates() { account_id, storage_3.clone(), AccountVaultDelta::default(), - Felt::new(3), + Felt::new_unchecked(3), ) .unwrap(); @@ -3354,7 +3385,7 @@ fn account_state_forest_shared_roots_not_deleted_prematurely() { account2, storage_update.clone(), AccountVaultDelta::default(), - Felt::new(2), + Felt::new_unchecked(2), ) .unwrap(); forest.update_account(block51, &delta2_update).unwrap(); @@ -3363,7 +3394,7 @@ fn account_state_forest_shared_roots_not_deleted_prematurely() { account3, storage_update.clone(), AccountVaultDelta::default(), - Felt::new(2), + Felt::new_unchecked(2), ) .unwrap(); forest.update_account(block52, &delta3_update).unwrap(); @@ -3376,9 +3407,13 @@ fn account_state_forest_shared_roots_not_deleted_prematurely() { let account1_root_after_prune = forest.get_storage_map_root(account1, &slot_name, block01); assert!(account1_root_after_prune.is_some()); - let delta1_update = - AccountDelta::new(account1, storage_update, AccountVaultDelta::default(), Felt::new(2)) - .unwrap(); + let delta1_update = AccountDelta::new( + account1, + storage_update, + AccountVaultDelta::default(), + Felt::new_unchecked(2), + ) + .unwrap(); forest.update_account(block53, &delta1_update).unwrap(); // Prune at block 53 @@ -3498,7 +3533,8 @@ fn account_state_forest_retains_latest_after_100_blocks_and_pruning() { vault_delta_51.add_asset(asset_51.into()).unwrap(); let delta_51 = - AccountDelta::new(account_id, storage_delta_51, vault_delta_51, Felt::new(51)).unwrap(); + AccountDelta::new(account_id, storage_delta_51, vault_delta_51, Felt::new_unchecked(51)) + .unwrap(); forest.update_account(block_51, &delta_51).unwrap(); @@ -3610,8 +3646,8 @@ fn db_roundtrip_transactions() { NoteRecord { block_num, note_index: BlockNoteIndex::new(0, idx).unwrap(), - note_id: note.id().as_word(), - note_commitment: note.to_commitment(), + note_id: note.details_commitment().as_word(), + note_commitment: note.id().as_word(), metadata: *note.metadata(), details: None, attachments: NoteAttachments::default(), @@ -3636,7 +3672,8 @@ fn db_roundtrip_transactions() { .map(|(idx, note)| NoteSyncRecord { block_num, note_index: BlockNoteIndex::new(0, idx).unwrap(), - note_id: note.id().as_word(), + // The `notes.note_id` column stores the details-only commitment (see `apply_block`). + note_id: note.details_commitment().as_word(), metadata: *note.metadata(), inclusion_path: SparseMerklePath::default(), }) @@ -3888,7 +3925,7 @@ fn account_state_forest_preserves_mixed_slots_independently() { account_id, storage_delta_51, AccountVaultDelta::default(), - Felt::new(51), + Felt::new_unchecked(51), ) .unwrap(); diff --git a/crates/store/src/genesis/config/errors.rs b/crates/store/src/genesis/config/errors.rs index 3a9721c8b4..8e2f520fdd 100644 --- a/crates/store/src/genesis/config/errors.rs +++ b/crates/store/src/genesis/config/errors.rs @@ -10,6 +10,7 @@ use miden_protocol::errors::{ }; use miden_protocol::utils::serde::DeserializationError; use miden_standards::account::faucets::FungibleFaucetError; +use miden_standards::account::policies::TokenPolicyManagerError; use miden_standards::account::wallets::BasicWalletError; use crate::genesis::config::TokenSymbolStr; @@ -74,4 +75,6 @@ pub enum GenesisConfigError { InvalidSecretKey(#[from] DeserializationError), #[error("provided signer config is not supported")] UnsupportedSignerConfig, + #[error("token policy manager error")] + TokenPolicyManager(#[from] TokenPolicyManagerError), } diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index 782bfe39c6..fb6f15f755 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -32,7 +32,7 @@ use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ BurnPolicyConfig, MintPolicyConfig, - PolicyAuthority, + PolicyRegistration, TokenPolicyManager, }; use miden_standards::account::wallets::create_basic_wallet; @@ -289,7 +289,7 @@ impl GenesisConfig { if total_issuance != 0 { let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?; let new_token_supply = AssetAmount::new(total_issuance)?; - let max_supply = current_faucet.max_supply().as_canonical_u64(); + let max_supply = current_faucet.max_supply().as_u64(); if max_supply < total_issuance { return Err(GenesisConfigError::MaxIssuanceExceeded { max_supply, @@ -299,7 +299,7 @@ impl GenesisConfig { } let new_token_config = Word::new([ Felt::from(new_token_supply), - current_faucet.max_supply(), + Felt::from(current_faucet.max_supply()), Felt::from(current_faucet.decimals()), Felt::from(current_faucet.symbol()), ]); @@ -330,7 +330,7 @@ impl GenesisConfig { // sanity check the total issuance against let faucet = FungibleFaucet::try_from(faucet_account.storage())?; - let max_supply = faucet.max_supply().as_canonical_u64(); + let max_supply = faucet.max_supply().as_u64(); if max_supply < total_issuance { return Err(GenesisConfigError::MaxIssuanceExceeded { max_supply, @@ -470,11 +470,11 @@ impl FungibleFaucetConfig { .storage_mode(storage_mode.into()) .with_auth_component(auth) .with_component(faucet) - .with_components(TokenPolicyManager::new( - PolicyAuthority::AuthControlled, - MintPolicyConfig::AllowAll, - BurnPolicyConfig::AllowAll, - )) + .with_components( + TokenPolicyManager::new() + .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? + .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)?, + ) .build()?; debug_assert_eq!(faucet_account.nonce(), Felt::ZERO); @@ -526,9 +526,10 @@ pub enum StorageMode { impl From for AccountStorageMode { fn from(mode: StorageMode) -> AccountStorageMode { match mode { - StorageMode::Network => AccountStorageMode::Network, + // Network accounts must be public in the new protocol model; the network-ness is + // determined by the standardized `NetworkAccountNoteAllowlist` slot in storage. + StorageMode::Network | StorageMode::Public => AccountStorageMode::Public, StorageMode::Private => AccountStorageMode::Private, - StorageMode::Public => AccountStorageMode::Public, } } } 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 6ee9d158a342091e27aa0d4b4f6c92bd69080e7e..5de6c22de45db136e92e17727ed5e2c65d55c8e6 100644 GIT binary patch literal 21004 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 literal 14803 zcmeHtby$?!+V>10EsX_8!$=RIgmfd_jxY>8bV`UiprA;H0uquc0@9KyX@G)+A|;}P zAW8^Q0`GcY^t^kYeKwx&JKsOwb?)n0JoQ_@JJ!8&z}VXQ62Nu@gTKN#IWdlou9!f3 zZ)s@boFrbK>K-P(PIB$vqbjf6aQzkFt)b#KA<%{{M|7&uJ%YDUwhA; zCwp%+$QK8!<4;z&!S3B35(awpTh8vC8wUSu+-~9m>pVSNv9?(IpLc<1H;g;R5z1|h ztuOYhJ=zWH?u*_*1)@E@J#5k59v;51Ng0*#+4^q3Z%A<(PTh&KZx|B0wWDv*-puOC z%P+_sin&!{teN;_8;#v#?RNu!yD@x2>Ykz`IFM$?>2kq=Wn(A-n~!YZ#;MN z^TpU;U4dtRO2)0BIhiZ_7O9ew$CT9L^=qexDKz=`(?#$FwwEaSGXiJj(@y+eAT^>^`vbX@H1aeh_1t9??q z4gr&?z3Tr~C)j&z{{58OdnoxWllG?h_tI$Ze*POv6475vlAlYHKVL=vCW##U+#RuC z7W=t4`QrozgSFq3ffAGypQT}{yKVdL`T3uY+ur@=x0-YBMal1H*ncux_jd0$XHXFs zp{lXQ-wiPTcjL=nE+Ks2gnKO__8K2_6pf65)dX^caBdoKqXdcrC~ly5fRX@8GAK7e zxep4o+NcNR87K^(yZ|Ktlq^s>L3s&^E`pF4(z~OZ7|sn{cUJy{h+nS>cfRf*s8K`! z010YqdwXZ5BjNaUE4WjK1o}*mvSw>%*6JxULitDEk~_xm6KAsg~?Ou*;Rx9YzvV1xiDb=8}yYx|C@p^lDY(7j9NwIDQvA%mhp z3}+MYUbQoC57L0%?KmNb+5}cYmj1GlghL1c@Uz~O;^#O=cVh}#9K;cJp2 z4*Xa5e`EgNTm!xCk9`Npkb<6xd-Ffs%v`u_)4Xw3P8P@~vI;ykrrFH2C* zs`XQ0D0jtdOj=w!J#^22Z53FsO}i~KHbJfPZaTMN48Dn z!S~R2AQ0r|giqaksJqaxqWMn~eyMZfknudy@|P966Fx#QFNdLtB^I@`#KUPQr%?Hm z?&pO6fcnfD`q126sNRmH#1cn--RnvG)wEg|_jc~K?LZO@0mr|b^&mC7cBhiX(c#E` z#%*VgqDIB35fwbGN|8pbFAXiA5y*nt!8hI}{7Y>~}x)AVxBJN$B# z0trXLxuD0L35ZNYgp%?fb&*Kuey1+=E|Bfc4~Ot)`g@xET4WbK;ks_=KAhgve5CD}+AW)`^I2yxzD;kk%b zn9;EsNa&%pk{+^K)7FYP^>Uxqh!Ktr*l!N8=GUlGYEDg6q_o~6_`%Yy7T2;;&9zl z64~_+O^l^$6Bp&xAKN@h=^EIjgW*^V9JIf&yxxnk77cphk5dRV8RY3_T~p8xj_7GAh9JKyZ8T>Njl_$c z>odjUmUBo-n9Lb^2`6PKuA6Wv$i}&-9c?}BbQUgxP;mOzx(kEh9=58bw^Iidi=NX` zx`d1SJ;@T1h#U8P_2CezK-TFa|y}XGOIMzC!z+Iq-b(H~IthcVvd~wIClYD+%*4d&- zm^;_cd|@gcXwi}j1bKji29B)siG{c3HAObGS>e^ux26fpA2Z)Q&-?P)@gsC@jLM-u z^^P!K`*H8aHC!>#=TWU@6ARnSEzsuN*mlUbtQ%0fJ z-;FqKAn8X+=OdrazbDVE|176#d>Hr(Z2`I6≺0V6`E}eim;rI1p#SL{@V&Gev~C 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 EAJMl-W&i*H 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 bb22ae3745580b000a0613dabe6f575007222c58..20e573a0e352fbc0f3f7f636a6e0c6394ff5c851 100644 GIT binary patch delta 21848 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 B2$TQ- delta 19155 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�vhDLsIiKN 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 diff --git a/crates/store/src/genesis/config/tests.rs b/crates/store/src/genesis/config/tests.rs index 743523186e..ae111f3640 100644 --- a/crates/store/src/genesis/config/tests.rs +++ b/crates/store/src/genesis/config/tests.rs @@ -4,7 +4,7 @@ use std::path::Path; use assert_matches::assert_matches; use miden_protocol::ONE; use miden_protocol::account::delta::AccountUpdateDetails; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use super::*; @@ -27,7 +27,7 @@ fn parsing_yields_expected_default_values() -> TestResult { let config_path = write_toml_file(temp_dir.path(), sample_content); let gcfg = GenesisConfig::read_toml_file(&config_path)?; - let signer = SecretKey::new(); + let signer = SigningKey::new(); let (state, _secrets) = gcfg.into_state(signer.public_key())?; let _ = state; // faucets always precede wallet accounts @@ -47,22 +47,22 @@ fn parsing_yields_expected_default_values() -> TestResult { { let faucet = FungibleFaucet::try_from(native_faucet.storage()).unwrap(); - assert_eq!(faucet.max_supply(), Felt::new(100_000_000_000_000_000)); + assert_eq!(faucet.max_supply().as_u64(), 100_000_000_000_000_000); assert_eq!(faucet.decimals(), 6); assert_eq!(*faucet.symbol(), TokenSymbol::new("MIDEN").unwrap()); } // check account balance, and ensure ordering is retained assert_matches!(wallet1.vault().get_balance(native_faucet.id()), Ok(val) => { - assert_eq!(val, 999_000); + assert_eq!(val.as_u64(), 999_000); }); assert_matches!(wallet2.vault().get_balance(native_faucet.id()), Ok(val) => { - assert_eq!(val, 777); + assert_eq!(val.as_u64(), 777); }); // check total issuance of the faucet let faucet = FungibleFaucet::try_from(native_faucet.storage()).unwrap(); - assert_eq!(faucet.token_supply(), Felt::new(999_777), "Issuance mismatch"); + assert_eq!(faucet.token_supply().as_u64(), 999_777, "Issuance mismatch"); Ok(()) } @@ -71,7 +71,7 @@ fn parsing_yields_expected_default_values() -> TestResult { #[miden_node_test_macro::enable_logging] async fn genesis_accounts_have_nonce_one() -> TestResult { let gcfg = GenesisConfig::default(); - let signer = SecretKey::new(); + let signer = SigningKey::new(); let (state, secrets) = gcfg.into_state(signer.public_key()).unwrap(); let mut iter = secrets.as_account_files(&state); let AccountFileWithName { account_file: status_quo, .. } = iter.next().unwrap().unwrap(); @@ -136,7 +136,7 @@ path = "test_account.mac" let gcfg = GenesisConfig::read_toml_file(&config_path)?; // Convert to state and verify the account is included - let signer = SecretKey::new(); + let signer = SigningKey::new(); let (state, _secrets) = gcfg.into_state(signer.public_key())?; assert!(state.accounts.iter().any(|a| a.id() == account_id)); @@ -152,7 +152,7 @@ fn parsing_native_faucet_from_file() -> TestResult { use miden_standards::account::policies::{ BurnPolicyConfig, MintPolicyConfig, - PolicyAuthority, + PolicyRegistration, TokenPolicyManager, }; use tempfile::tempdir; @@ -181,11 +181,11 @@ fn parsing_native_faucet_from_file() -> TestResult { .storage_mode(AccountStorageMode::Public) .with_auth_component(auth) .with_component(faucet) - .with_components(TokenPolicyManager::new( - PolicyAuthority::AuthControlled, - MintPolicyConfig::AllowAll, - BurnPolicyConfig::AllowAll, - )) + .with_components( + TokenPolicyManager::new() + .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)? + .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)?, + ) .build()?; let faucet_id = faucet_account.id(); @@ -211,7 +211,7 @@ verification_base_fee = 0 let gcfg = GenesisConfig::read_toml_file(&config_path)?; // Convert to state and verify the native faucet is included - let signer = SecretKey::new(); + let signer = SigningKey::new(); let (state, secrets) = gcfg.into_state(signer.public_key())?; assert!(state.accounts.iter().any(|a| a.id() == faucet_id)); @@ -271,7 +271,7 @@ verification_base_fee = 0 let gcfg = GenesisConfig::read_toml_file(&config_path)?; // into_state should fail with NativeFaucetNotFungible error when loading the file - let result = gcfg.into_state(SecretKey::new().public_key()); + let result = gcfg.into_state(SigningKey::new().public_key()); assert!(result.is_err()); let err = result.unwrap_err(); assert!( @@ -304,7 +304,7 @@ path = "does_not_exist.mac" let gcfg = GenesisConfig::read_toml_file(&config_path).unwrap(); // into_state should fail with AccountFileRead error when loading the file - let result = gcfg.into_state(SecretKey::new().public_key()); + let result = gcfg.into_state(SigningKey::new().public_key()); assert!(result.is_err()); let err = result.unwrap_err(); assert!( @@ -323,7 +323,7 @@ async fn parsing_agglayer_sample_with_account_files() -> TestResult { .join("src/genesis/config/samples/02-with-account-files.toml"); let gcfg = GenesisConfig::read_toml_file(&sample_path)?; - let signer = SecretKey::new(); + let signer = SigningKey::new(); let (state, secrets) = gcfg.into_state(signer.public_key())?; // Should have 4 accounts: diff --git a/crates/store/src/genesis/mod.rs b/crates/store/src/genesis/mod.rs index 9de141eade..a9c2ace5e4 100644 --- a/crates/store/src/genesis/mod.rs +++ b/crates/store/src/genesis/mod.rs @@ -11,7 +11,7 @@ use miden_protocol::block::{ FeeParameters, SignedBlock, }; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, SecretKey, Signature}; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature, SigningKey}; use miden_protocol::crypto::merkle::mmr::{Forest, MmrPeaks}; use miden_protocol::crypto::merkle::smt::{LargeSmt, MemoryStorage, Smt}; use miden_protocol::errors::AccountError; @@ -174,7 +174,7 @@ impl GenesisState { } /// Builds and signs the genesis block with a local secret key. - pub fn into_block(self, signer: &SecretKey) -> anyhow::Result { + pub fn into_block(self, signer: &SigningKey) -> anyhow::Result { let unsigned_block = self.into_unsigned_block()?; let signature = signer.sign(unsigned_block.header().commitment()); unsigned_block.into_block(signature) diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index a2a30d4c84..5ceb432353 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -24,6 +24,49 @@ pub fn default_sqlite_connection_pool_size() -> std::num::NonZeroUsize { DatabaseOptions::default().connection_pool_size } +/// Test-only helpers exposed for downstream integration tests. +/// +/// This module is hidden from public docs and not part of the stable API. It exists so +/// integration tests in sibling crates (e.g. `miden-node-rpc`) can seed network-account +/// rows directly into the store's SQLite database without us widening the visibility of +/// internal diesel types. +#[doc(hidden)] +pub mod test_support { + use std::path::Path; + + use diesel::prelude::*; + use miden_protocol::Word; + use miden_protocol::account::AccountId; + use miden_protocol::block::BlockNumber; + + use crate::db::models::queries::{AccountRowInsert, NetworkAccountType}; + use crate::db::schema; + + /// Opens a fresh connection to the store's SQLite database and inserts a private + /// network-account row for `account_id`, marking it as a network account in the + /// latest state at block 0. + /// + /// Intended for integration tests that need to exercise the network-account gate + /// without running a transaction through the block producer. The store's WAL mode + /// makes a secondary connection safe. + pub fn seed_network_account(db_path: &Path, account_id: AccountId) { + let mut conn = SqliteConnection::establish(db_path.to_str().expect("db path is utf-8")) + .expect("connect to store sqlite"); + + let row = AccountRowInsert::new_private( + account_id, + NetworkAccountType::Network, + Word::default(), + BlockNumber::from(0), + BlockNumber::from(0), + ); + diesel::insert_into(schema::accounts::table) + .values(&row) + .execute(&mut conn) + .expect("insert network account row"); + } +} + // CONSTANTS // ================================================================================================= const COMPONENT: &str = "miden-store"; diff --git a/crates/store/src/server/api.rs b/crates/store/src/server/api.rs index c5d393eb65..406df76631 100644 --- a/crates/store/src/server/api.rs +++ b/crates/store/src/server/api.rs @@ -96,7 +96,7 @@ impl StoreApi { for note in batch.input_notes().iter() { if let Some(header) = note.header() { - unauthenticated_note_commitments.insert(header.to_commitment()); + unauthenticated_note_commitments.insert(header.id().as_word()); } } } diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 99158a1ece..c854c61330 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -252,7 +252,7 @@ impl rpc_server::Rpc for StoreApi { SyncAccountVaultError, >(request.account_id)?; - if !account_id.has_public_state() { + if !account_id.is_public() { return Err(SyncAccountVaultError::AccountNotPublic(account_id).into()); } @@ -305,7 +305,7 @@ impl rpc_server::Rpc for StoreApi { SyncAccountStorageMapsError, >(request.account_id)?; - if !account_id.has_public_state() { + if !account_id.is_public() { Err(SyncAccountStorageMapsError::AccountNotPublic(account_id))?; } @@ -374,6 +374,16 @@ impl rpc_server::Rpc for StoreApi { })) } + async fn are_network_accounts( + &self, + request: Request, + ) -> Result, Status> { + let ids = read_account_ids::(request.into_inner().account_ids)?; + let subset = self.state.network_accounts_subset(&ids).await?; + let network_account_ids = subset.into_iter().map(proto::account::AccountId::from).collect(); + Ok(Response::new(proto::store::NetworkAccountIdSubset { network_account_ids })) + } + async fn sync_transactions( &self, request: Request, diff --git a/crates/store/src/state/apply_block.rs b/crates/store/src/state/apply_block.rs index 2cf12a1b10..b6acf01895 100644 --- a/crates/store/src/state/apply_block.rs +++ b/crates/store/src/state/apply_block.rs @@ -319,7 +319,7 @@ impl State { block_num, note_index, note_id: note.id().as_word(), - note_commitment: note.to_commitment(), + note_commitment: note.id().as_word(), metadata: *note.metadata(), details, attachments, diff --git a/crates/store/src/state/loader.rs b/crates/store/src/state/loader.rs index 72df4fff34..6c64dc1d8b 100644 --- a/crates/store/src/state/loader.rs +++ b/crates/store/src/state/loader.rs @@ -17,7 +17,7 @@ use miden_crypto::merkle::smt::{Backend, ForestInMemoryBackend}; #[cfg(feature = "rocksdb")] use miden_crypto::merkle::smt::{ForestPersistentBackend, PersistentBackendConfig}; #[cfg(feature = "rocksdb")] -use miden_large_smt_backend_rocksdb::RocksDbStorage; +use miden_large_smt_backend_rocksdb::{RocksDbStorage, SmtStorageReader}; #[cfg(feature = "rocksdb")] use miden_node_utils::clap::RocksDbOptions; use miden_protocol::account::{AccountId, AccountStorageHeader, StorageSlotType}; @@ -465,9 +465,10 @@ pub async fn load_mmr(db: &mut Db) -> Result Result, DatabaseError> { + self.db.select_network_accounts_subset(account_ids.to_vec()).await + } + /// Returns network account IDs within the specified block range (based on account creation /// block). /// @@ -797,7 +805,7 @@ impl State { ) -> Result { let AccountRequest { block_num, account_id, details } = account_request; - if details.is_some() && !account_id.has_public_state() { + if details.is_some() && !account_id.is_public() { return Err(GetAccountError::AccountNotPublic(account_id)); } @@ -929,7 +937,7 @@ impl State { storage_requests, } = detail_request; - if !account_id.has_public_state() { + if !account_id.is_public() { return Err(GetAccountError::AccountNotPublic(account_id)); } diff --git a/crates/store/src/state/sync_state.rs b/crates/store/src/state/sync_state.rs index dc890571a4..d2f8fc0940 100644 --- a/crates/store/src/state/sync_state.rs +++ b/crates/store/src/state/sync_state.rs @@ -46,7 +46,7 @@ impl State { if block_from == block_to { return Ok(( MmrDelta { - forest: Forest::new(block_from.as_usize()), + forest: Forest::new(block_from.as_usize()).expect("block index fits in u32"), data: vec![], }, block_header, @@ -72,7 +72,10 @@ impl State { .await .blockchain .as_mmr() - .get_delta(Forest::new(from_forest), Forest::new(to_forest)) + .get_delta( + Forest::new(from_forest).expect("from_forest fits in u32"), + Forest::new(to_forest).expect("to_forest fits in u32"), + ) .map_err(StateSyncError::FailedToBuildMmrDelta)?; Ok((mmr_delta, block_header, signature)) diff --git a/crates/utils/src/crypto.rs b/crates/utils/src/crypto.rs index d203ff75a0..baa2229dc2 100644 --- a/crates/utils/src/crypto.rs +++ b/crates/utils/src/crypto.rs @@ -5,7 +5,7 @@ use rand::Rng; /// Creates a new RPO Random Coin with random seed pub fn get_rpo_random_coin(rng: &mut T) -> RandomCoin { let auth_seed: [u64; 4] = rng.random(); - let rng_seed = Word::from(auth_seed.map(Felt::new)); + let rng_seed = Word::from(auth_seed.map(Felt::new_unchecked)); RandomCoin::new(rng_seed) } diff --git a/proto/proto/internal/store.proto b/proto/proto/internal/store.proto index 04fb41bb25..c34c06e1ff 100644 --- a/proto/proto/internal/store.proto +++ b/proto/proto/internal/store.proto @@ -21,6 +21,15 @@ service Rpc { // Returns the latest details the specified account. rpc GetAccount(rpc.AccountRequest) returns (rpc.AccountResponse) {} + // Filters the provided account ids down to the subset that are currently + // classified as network accounts. Ids that don't exist in the store, or that + // aren't network accounts, are omitted. The order of `network_account_ids` + // is unspecified. + // + // Callers checking a single id should pass a one-element list and check whether + // the response is non-empty. + rpc AreNetworkAccounts(account.AccountIdList) returns (NetworkAccountIdSubset) {} + // Returns raw block data for the specified block number, optionally including the block proof. rpc GetBlockByNumber(blockchain.BlockRequest) returns (blockchain.MaybeBlock) {} @@ -327,6 +336,16 @@ message MaybeAccountDetails { optional account.AccountDetails details = 1; } +// ARE NETWORK ACCOUNTS +// ================================================================================================ + +// Represents the subset of the provided account ids that are currently classified +// as network accounts. The order of `network_account_ids` is unspecified. +message NetworkAccountIdSubset { + // The network-account subset of the requested ids. + repeated account.AccountId network_account_ids = 1; +} + // GET UNCONSUMED NETWORK NOTES // ================================================================================================ diff --git a/proto/proto/types/account.proto b/proto/proto/types/account.proto index 8fd9729a51..6df8725658 100644 --- a/proto/proto/types/account.proto +++ b/proto/proto/types/account.proto @@ -16,6 +16,12 @@ message AccountId { bytes id = 1; } +// A list of account IDs. +message AccountIdList { + // The list of account IDs. + repeated AccountId account_ids = 1; +} + // The state of an account at a specific block height. message AccountSummary { // The account ID. From fbd49605860b362600a158ff3f54c76c06d1da48 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Tue, 19 May 2026 12:10:10 -0300 Subject: [PATCH 2/3] chore: simplify genesis --- Cargo.lock | 14 +++++++------- crates/rpc/src/server/api.rs | 3 +-- crates/store/src/genesis/config/mod.rs | 13 ++++--------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96df84f824..8f87e5bc01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "miden-agglayer" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "alloy-sol-types", "fs-err", @@ -3013,7 +3013,7 @@ dependencies = [ [[package]] name = "miden-block-prover" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "miden-protocol", "thiserror 2.0.18", @@ -3648,7 +3648,7 @@ dependencies = [ [[package]] name = "miden-protocol" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "bech32", "fs-err", @@ -3751,7 +3751,7 @@ dependencies = [ [[package]] name = "miden-standards" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "bon", "fs-err", @@ -3790,7 +3790,7 @@ dependencies = [ [[package]] name = "miden-testing" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3810,7 +3810,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "miden-processor", "miden-protocol", @@ -3823,7 +3823,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol?branch=next#01077765ca06fb49fc6e90e8ee9e181ad76ff7e2" +source = "git+https://github.com/0xMiden/protocol?branch=next#c2d8c615b90eb7b3993864eaa9c22c4bb4061ee5" dependencies = [ "miden-protocol", "miden-tx", diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 8b2f95c469..65fb4fb66a 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -496,8 +496,7 @@ impl api_server::Api for RpcService { // account; the store is the source of truth because network-ness now lives in account // storage and isn't derivable from an AccountId alone. Network accounts must be public, so // private-account txs short-circuit and skip the store roundtrip. - if !tx.account_update().initial_state_commitment().is_empty() - && tx.account_id().is_public() + if !tx.account_update().initial_state_commitment().is_empty() && tx.account_id().is_public() { let response = self .store diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index fb6f15f755..7613233ff3 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -25,7 +25,7 @@ use miden_protocol::block::FeeParameters; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey; use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey; use miden_protocol::errors::TokenSymbolError; -use miden_protocol::{Felt, ONE, Word}; +use miden_protocol::{Felt, ONE}; use miden_standards::AuthMethod; use miden_standards::account::auth::AuthSingleSig; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; @@ -297,14 +297,9 @@ impl GenesisConfig { total_issuance, }); } - let new_token_config = Word::new([ - Felt::from(new_token_supply), - Felt::from(current_faucet.max_supply()), - Felt::from(current_faucet.decimals()), - Felt::from(current_faucet.symbol()), - ]); - storage_delta - .set_item(FungibleFaucet::token_config_slot().clone(), new_token_config)?; + let updated_faucet = current_faucet.with_token_supply(new_token_supply)?; + let slot = updated_faucet.token_config_slot_value(); + storage_delta.set_item(slot.name().clone(), slot.value())?; tracing::debug!( "Reducing faucet account {faucet} for {symbol} by {amount}", faucet = faucet_id.to_hex(), From 8fbbb39c7544a03c0b7485f6f0aa6a86f9d108f2 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Thu, 21 May 2026 10:16:39 -0300 Subject: [PATCH 3/3] chore: address PR comments --- bin/ntx-builder/src/builder.rs | 7 +++---- .../src/db/models/account_effect.rs | 12 ++++-------- crates/store/src/db/models/queries/accounts.rs | 18 ++++++++---------- crates/store/src/genesis/config/mod.rs | 9 ++------- crates/store/src/genesis/config/tests.rs | 2 +- 5 files changed, 18 insertions(+), 30 deletions(-) diff --git a/bin/ntx-builder/src/builder.rs b/bin/ntx-builder/src/builder.rs index de94689fb6..fc05ff6a54 100644 --- a/bin/ntx-builder/src/builder.rs +++ b/bin/ntx-builder/src/builder.rs @@ -8,7 +8,7 @@ use miden_node_proto::domain::mempool::MempoolEvent; use miden_protocol::account::Account; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::block::BlockHeader; -use miden_standards::account::auth::NetworkAccountNoteAllowlist; +use miden_standards::account::auth::NetworkAccount; use tokio::net::TcpListener; use tokio::sync::mpsc; use tokio::task::JoinSet; @@ -232,10 +232,9 @@ impl NetworkTransactionBuilder { if let Some(AccountUpdateDetails::Delta(delta)) = account_delta && delta.is_full_state() && let Ok(account) = Account::try_from(delta) - && account.is_public() - && NetworkAccountNoteAllowlist::try_from(account.storage()).is_ok() + && let Ok(network_account) = NetworkAccount::new(account) { - let network_id = NetworkAccountId::new_trusted(account.id()); + let network_id = NetworkAccountId::new_trusted(network_account.id()); self.coordinator.spawn_actor(network_id, &self.actor_context); } let inactive_targets = self.coordinator.send_targeted(&event); diff --git a/bin/ntx-builder/src/db/models/account_effect.rs b/bin/ntx-builder/src/db/models/account_effect.rs index 5e483035c6..6d03795461 100644 --- a/bin/ntx-builder/src/db/models/account_effect.rs +++ b/bin/ntx-builder/src/db/models/account_effect.rs @@ -1,7 +1,7 @@ use miden_node_proto::domain::account::NetworkAccountId; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::account::{Account, AccountDelta, AccountId}; -use miden_standards::account::auth::NetworkAccountNoteAllowlist; +use miden_standards::account::auth::NetworkAccount; // NETWORK ACCOUNT EFFECT // ================================================================================================ @@ -22,13 +22,9 @@ impl NetworkAccountEffect { // standardized `NetworkAccountNoteAllowlist` slot. let account = Account::try_from(update) .expect("Account should be derivable by full state AccountDelta"); - if account.is_public() - && NetworkAccountNoteAllowlist::try_from(account.storage()).is_ok() - { - Some(NetworkAccountEffect::Created(account)) - } else { - None - } + NetworkAccount::new(account) + .ok() + .map(|na| NetworkAccountEffect::Created(na.into_account())) }, AccountUpdateDetails::Delta(update) => { // Partial updates carry no storage we can inspect here. Forward them as updates and diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 4f93af0906..ddf96d6dcf 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -42,7 +42,7 @@ use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; use miden_protocol::block::{BlockAccountUpdate, BlockNumber}; use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_protocol::{Felt, Word}; -use miden_standards::account::auth::NetworkAccountNoteAllowlist; +use miden_standards::account::auth::NetworkAccount; use crate::COMPONENT; use crate::db::models::conv::{SqlTypeConvert, nonce_to_raw_sql, raw_sql_to_nonce}; @@ -367,9 +367,9 @@ pub struct PublicAccountStateRootsPage { /// Returns up to `page_size` public account IDs, starting after `after_account_id` if provided. /// Results are ordered by `account_id` for stable pagination. /// -/// Public accounts are those with `AccountStorageMode::Public` or `AccountStorageMode::Network`. -/// We identify them by checking `code_commitment IS NOT NULL` - public accounts store their full -/// state (including `code_commitment`), while private accounts only store the `account_commitment`. +/// Public accounts are those with `AccountStorageMode::Public`. We identify them by checking +/// against the store. Public accounts store their `code_commitment`, while private accounts only +/// store the `account_commitment`. /// /// # Raw SQL /// @@ -427,9 +427,9 @@ pub(crate) fn select_public_account_ids_paged( /// Returns up to `page_size` public account states, starting after `after_account_id` if provided. /// Results are ordered by `account_id` for stable pagination. /// -/// Public accounts are those with `AccountStorageMode::Public` or `AccountStorageMode::Network`. -/// We identify them by checking `code_commitment IS NOT NULL` - public accounts store their full -/// state (including `code_commitment`), while private accounts only store the `account_commitment`. +/// Public accounts are those with `AccountStorageMode::Public`. We identify them by checking +/// against the store. Public accounts store their `code_commitment`, while private accounts only +/// store the `account_commitment`. /// /// # Raw SQL /// @@ -1402,9 +1402,7 @@ pub(crate) fn upsert_accounts( let network_account_type = match &account_state { AccountStateForInsert::Private => NetworkAccountType::None, AccountStateForInsert::FullAccount(account) => { - if account.is_public() - && NetworkAccountNoteAllowlist::try_from(account.storage()).is_ok() - { + if NetworkAccount::new(account.clone()).is_ok() { NetworkAccountType::Network } else { NetworkAccountType::None diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index 7613233ff3..40717d5a92 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -506,12 +506,9 @@ struct AssetEntry { /// for details #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)] pub enum StorageMode { - /// Monitor for `Notes` related to the account, in addition to being `Public`. - #[serde(alias = "network")] - #[default] - Network, /// A publicly stored account, lives on-chain. #[serde(alias = "public")] + #[default] Public, /// A private account, which must be known by interactors. #[serde(alias = "private")] @@ -521,9 +518,7 @@ pub enum StorageMode { impl From for AccountStorageMode { fn from(mode: StorageMode) -> AccountStorageMode { match mode { - // Network accounts must be public in the new protocol model; the network-ness is - // determined by the standardized `NetworkAccountNoteAllowlist` slot in storage. - StorageMode::Network | StorageMode::Public => AccountStorageMode::Public, + StorageMode::Public => AccountStorageMode::Public, StorageMode::Private => AccountStorageMode::Private, } } diff --git a/crates/store/src/genesis/config/tests.rs b/crates/store/src/genesis/config/tests.rs index ae111f3640..d15fa9a647 100644 --- a/crates/store/src/genesis/config/tests.rs +++ b/crates/store/src/genesis/config/tests.rs @@ -378,7 +378,7 @@ async fn parsing_agglayer_sample_with_account_files() -> TestResult { // Verify the genesis state can be converted to a block let block = state.into_block(&signer)?; - // Verify that non-private accounts (Public and Network) get full Delta details. + // Verify that non-private (Public) accounts get full Delta details. for update in block.inner().body().updated_accounts() { let is_private = update.account_id().is_private(); match update.details() {