diff --git a/CHANGELOG.md b/CHANGELOG.md index e02eaf5e48..f2bc47e1c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## v0.15.0 (TBD) + +### Changes + +- Added validation of leaf type on CLAIM note processing to prevent message leaves from being processed as asset claims ([#2730](https://github.com/0xMiden/protocol/pull/2730)). +- [BREAKING] Reduced `MAX_ASSETS_PER_NOTE` from 255 to 64 and `NOTE_MEM_SIZE` from 3072 to 1024 ([#2741](https://github.com/0xMiden/protocol/issues/2741)). +- Added `metadata_into_note_type` procedure to `note.masm` for extracting note type from metadata header ([#2738](https://github.com/0xMiden/protocol/pull/2738)). +- [BREAKING] Renamed `extract_sender_from_metadata` to `metadata_into_sender` and `extract_attachment_info_from_metadata` to `metadata_into_attachment_info` in `note.masm` ([#2758](https://github.com/0xMiden/protocol/pull/2758)). +- Updated `SwapNote::build_tag` to use 1-bit `NoteType` encoding, increasing script root bits from 14 to 15 ([#2758](https://github.com/0xMiden/protocol/pull/2758)). +- Added `AssetAmount` wrapper type for validated fungible asset amounts ([#2721](https://github.com/0xMiden/protocol/pull/2721)). +- [BREAKING] Renamed `ProvenBatch::new` to `new_unchecked` ([#2687](https://github.com/0xMiden/miden-base/issues/2687)). +- Added `ShortCapitalString` type and related `TokenSymbol` and `RoleSymbol` types ([#2690](https://github.com/0xMiden/protocol/pull/2690)). +- [BREAKING] Renamed the guarded multisig component-facing APIs from `multisig_guardian` / `AuthMultisigGuardian` to `guarded_multisig` / `AuthGuardedMultisig`, while retaining the `guardian` auth namespace and guardian-specific procedures. +- Added shared `ProcedurePolicy` for AuthMultisig ([#2670](https://github.com/0xMiden/protocol/pull/2670)). +- [BREAKING] Changed `NoteType` encoding from 2 bits to 1 and makes `NoteType::Private` the default ([#2691](https://github.com/0xMiden/miden-base/issues/2691)). +- Added `BlockNumber::saturating_sub()` ([#2660](https://github.com/0xMiden/protocol/issues/2660)). +- [BREAKING] Added cycle counts to notes returned by `NoteConsumptionInfo` and removed public fields from related types ([#2772](https://github.com/0xMiden/miden-base/issues/2772)). +- Automatically enable `concurrent` feature in `miden-tx` for `std` context ([#2791](https://github.com/0xMiden/protocol/pull/2791)). + +### Fixes + +- Made deserialization of `AccountCode` more robust ([#2788](https://github.com/0xMiden/protocol/pull/2788)). + ## 0.14.3 (2026-04-07) - [BREAKING] Updated for compatibility with miden-vm v0.22.1 (`Arc` return types, `MastArtifact`/`PackageKind` removal) ([#2742](https://github.com/0xMiden/protocol/pull/2742)). diff --git a/Cargo.lock b/Cargo.lock index 60f048bd64..c4c74dc476 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,9 +323,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake3" @@ -382,9 +382,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -462,9 +462,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] @@ -1201,9 +1201,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1255,12 +1255,12 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1378,9 +1378,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -1453,9 +1453,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" @@ -1534,7 +1534,7 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.14.3" +version = "0.15.0" dependencies = [ "alloy-sol-types", "fs-err", @@ -1611,7 +1611,7 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.14.3" +version = "0.15.0" dependencies = [ "miden-protocol", "thiserror", @@ -1686,7 +1686,7 @@ dependencies = [ "p3-miden-lifted-stark", "p3-symmetric", "p3-util", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha", "rand_core 0.9.5", "rand_hc", @@ -1739,7 +1739,7 @@ dependencies = [ "p3-field", "p3-goldilocks", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "subtle", "thiserror", @@ -1856,7 +1856,7 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.14.3" +version = "0.15.0" dependencies = [ "anyhow", "assert_matches", @@ -1870,14 +1870,14 @@ dependencies = [ "miden-core", "miden-core-lib", "miden-crypto", + "miden-crypto-derive", "miden-mast-package", "miden-processor", "miden-protocol", - "miden-protocol-macros", "miden-utils-sync", "miden-verifier", "pprof", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha", "rand_xoshiro", "regex", @@ -1890,16 +1890,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "miden-protocol-macros" -version = "0.14.3" -dependencies = [ - "miden-protocol", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "miden-prover" version = "0.22.1" @@ -1930,7 +1920,7 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.14.3" +version = "0.15.0" dependencies = [ "anyhow", "assert_matches", @@ -1941,7 +1931,7 @@ dependencies = [ "miden-processor", "miden-protocol", "miden-standards", - "rand 0.9.2", + "rand 0.9.4", "regex", "thiserror", "walkdir", @@ -1949,7 +1939,7 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.14.3" +version = "0.15.0" dependencies = [ "anyhow", "assert_matches", @@ -1966,7 +1956,7 @@ dependencies = [ "miden-tx", "miden-tx-batch-prover", "primitive-types", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha", "rstest", "serde", @@ -1977,7 +1967,7 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.14.3" +version = "0.15.0" dependencies = [ "anyhow", "assert_matches", @@ -1994,7 +1984,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" -version = "0.14.3" +version = "0.15.0" dependencies = [ "miden-protocol", "miden-tx", @@ -2335,7 +2325,7 @@ dependencies = [ "p3-maybe-rayon", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "tracing", ] @@ -2356,7 +2346,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", ] @@ -2381,7 +2371,7 @@ dependencies = [ "p3-field", "p3-maybe-rayon", "p3-util", - "rand 0.10.0", + "rand 0.10.1", "serde", "tracing", ] @@ -2405,7 +2395,7 @@ dependencies = [ "p3-field", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] @@ -2436,7 +2426,7 @@ dependencies = [ "p3-miden-lmcs", "p3-miden-transcript", "p3-util", - "rand 0.10.0", + "rand 0.10.1", "thiserror", "tracing", ] @@ -2476,7 +2466,7 @@ dependencies = [ "p3-miden-transcript", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", "serde", "thiserror", "tracing", @@ -2522,7 +2512,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand 0.10.0", + "rand 0.10.1", "serde", "spin 0.10.0", "tracing", @@ -2536,7 +2526,7 @@ checksum = "6a018b618e3fa0aec8be933b1d8e404edd23f46991f6bf3f5c2f3f95e9413fe9" dependencies = [ "p3-field", "p3-symmetric", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] @@ -2549,7 +2539,7 @@ dependencies = [ "p3-mds", "p3-symmetric", "p3-util", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] @@ -2686,9 +2676,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -2808,9 +2798,9 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -2874,18 +2864,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -2893,11 +2883,11 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -2930,9 +2920,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_hc" @@ -2963,9 +2953,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2987,7 +2977,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -3080,8 +3070,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "proptest", - "rand 0.8.5", - "rand 0.9.2", + "rand 0.8.6", + "rand 0.9.4", "ruint-macro", "serde_core", "valuable", @@ -3130,7 +3120,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -3433,9 +3423,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.17.3" +version = "12.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ca086c1eb5c7ee74b151ba83c6487d5d33f8c08ad991b86f3f58f6629e68d5" +checksum = "b1f3cdeaae6779ecba2567f20bf7716718b8c4ce6717c9def4ced18786bb11ea" dependencies = [ "debugid", "memmap2", @@ -3445,9 +3435,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.3" +version = "12.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa911a28a62823aaf2cc2e074212492a3ee69d0d926cc8f5b12b4a108ff5c0c" +checksum = "672c6ad9cb8fce6a1283cc9df9070073cccad00ae241b80e3686328a64e3523b" dependencies = [ "rustc-demangle", "symbolic-common", @@ -3585,9 +3575,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.51.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "pin-project-lite", "tokio-macros", @@ -3652,9 +3642,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.10+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", @@ -3869,9 +3859,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "js-sys", "wasm-bindgen", @@ -3885,9 +3875,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-ranges" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3595ffe225639f1e0fd8d7269dcc05d2fbfea93cfac2fea367daf1adb60aae91" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" dependencies = [ "smallvec", ] @@ -3943,9 +3933,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -3956,9 +3946,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3966,9 +3956,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -3979,9 +3969,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -4014,7 +4004,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver 1.0.28", @@ -4022,9 +4012,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4152,7 +4142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5fc25f6118..32f6f9cb42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "crates/miden-agglayer", "crates/miden-block-prover", "crates/miden-protocol", - "crates/miden-protocol-macros", "crates/miden-standards", "crates/miden-testing", "crates/miden-tx", @@ -21,7 +20,7 @@ homepage = "https://miden.xyz" license = "MIT" repository = "https://github.com/0xMiden/protocol" rust-version = "1.90" -version = "0.14.3" +version = "0.15.0" [profile.release] codegen-units = 1 @@ -37,14 +36,13 @@ lto = true [workspace.dependencies] # Workspace crates -miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.14" } -miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.14" } -miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.14" } -miden-protocol-macros = { default-features = false, path = "crates/miden-protocol-macros", version = "0.14" } -miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.14" } -miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.14" } -miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.14" } -miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.14" } +miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.15" } +miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.15" } +miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.15" } +miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.15" } +miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.15" } +miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.15" } +miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.15" } # Miden dependencies miden-assembly = { default-features = false, version = "0.22" } @@ -52,6 +50,7 @@ miden-assembly-syntax = { default-features = false, version = "0.22" } miden-core = { default-features = false, version = "0.22" } miden-core-lib = { default-features = false, version = "0.22" } miden-crypto = { default-features = false, version = "0.23" } +miden-crypto-derive = { default-features = false, version = "0.23" } miden-mast-package = { default-features = false, version = "0.22" } miden-processor = { default-features = false, version = "0.22" } miden-prover = { default-features = false, version = "0.22" } diff --git a/bin/bench-note-checker/src/lib.rs b/bin/bench-note-checker/src/lib.rs index bb2c4cd546..44af7b5d5f 100644 --- a/bin/bench-note-checker/src/lib.rs +++ b/bin/bench-note-checker/src/lib.rs @@ -81,8 +81,6 @@ pub fn setup_mixed_notes_benchmark(config: MixedNotesConfig) -> anyhow::Result anyhow::Result<() // Validate that we got the expected number of successful notes. assert_eq!( setup.expected_successful_count, - result.successful.len(), + result.successful().len(), "Expected {} successful notes, got {}", setup.expected_successful_count, - result.successful.len() + result.successful().len() ); // Validate that we have some failed notes (all the failing ones). - assert!(!result.failed.is_empty(), "Expected some failed notes"); + assert!(!result.failed().is_empty(), "Expected some failed notes"); Ok(()) } diff --git a/bin/bench-transaction/Cargo.toml b/bin/bench-transaction/Cargo.toml index e932ad28fa..376076ff73 100644 --- a/bin/bench-transaction/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -20,7 +20,7 @@ path = "src/time_counting_benchmarks/prove.rs" miden-protocol = { features = ["testing"], workspace = true } miden-standards = { workspace = true } miden-testing = { workspace = true } -miden-tx = { workspace = true } +miden-tx = { features = ["concurrent"], workspace = true } # External dependencies anyhow = { workspace = true } diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index cddf6461fd..40a8ecbf60 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -402,7 +402,7 @@ Keccak preimage format directly — the felt value does **not** equal the numeri | Field | Value | |-------|-------| | `serial_num` | Random (`rng.draw_word()`) | -| `script` | `B2AGG.masb` | +| `script` | `b2agg.masm` | | `storage` | 6 felts -- see layout below | **Storage layout (6 felts):** @@ -461,7 +461,7 @@ token registry, and creates a MINT note targeting the faucet. | Field | Value | |-------|-------| | `serial_num` | Random (`rng.draw_word()`) | -| `script` | `CLAIM.masb` | +| `script` | `claim.masm` | | `storage` | 569 felts -- see layout below | **Storage layout (569 felts):** @@ -530,7 +530,7 @@ The storage is divided into three logical regions: proof data (felts 0-535), lea | Field | Value | |-------|-------| | `serial_num` | Random (`rng.draw_word()`) | -| `script` | `CONFIG_AGG_BRIDGE.masb` | +| `script` | `config_agg_bridge.masm` | | `storage` | 7 felts -- see layout below | **Storage layout (7 felts):** @@ -577,7 +577,7 @@ CLAIM notes can be verified against it. | Field | Value | |-------|-------| | `serial_num` | Random (`rng.draw_word()`) | -| `script` | `UPDATE_GER.masb` | +| `script` | `update_ger.masm` | | `storage` | 8 felts -- see layout below | **Storage layout (8 felts):** diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index f101c90b40..0364f4a3f0 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -9,6 +9,7 @@ use miden::core::crypto::hashes::poseidon2 use miden::core::mem use miden::core::word use miden::protocol::note +use miden::protocol::note::NOTE_TYPE_PUBLIC use miden::protocol::output_note use miden::protocol::output_note::ATTACHMENT_KIND_NONE use miden::protocol::active_account @@ -32,6 +33,7 @@ const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet depos const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: provided SMT root does not match the computed root" const ERR_CLAIM_ALREADY_SPENT = "claim note has already been spent" const ERR_SOURCE_BRIDGE_NETWORK_OVERFLOW = "source bridge network overflowed u32" +const ERR_INVALID_LEAF_TYPE = "invalid leaf type: only asset claims (leafType=0) are supported" # CONSTANTS # ================================================================================================= @@ -70,9 +72,6 @@ const CLAIM_LEAF_DATA_WORD_LEN = 8 # - account_id_prefix [17] : 1 felt const MINT_NOTE_NUM_STORAGE_ITEMS = 18 -# P2ID output note constants -const OUTPUT_NOTE_TYPE_PUBLIC = 1 - # P2ID attachment constants (the P2ID note created by the faucet has no attachment) const P2ID_ATTACHMENT_SCHEME_NONE = 0 @@ -114,6 +113,7 @@ const CLAIM_LEAF_INDEX_MEM_ADDR = 900 const CLAIM_SOURCE_BRIDGE_NETWORK_MEM_ADDR = 901 # Memory addresses for leaf data fields (derived from leaf data layout at CLAIM_LEAF_DATA_START_PTR=536) +const LEAF_TYPE_ADDRESS = 536 const ORIGIN_TOKEN_ADDRESS_0 = 538 const ORIGIN_TOKEN_ADDRESS_1 = 539 const ORIGIN_TOKEN_ADDRESS_2 = 540 @@ -192,6 +192,7 @@ const CLAIM_DEST_ID_SUFFIX_LOCAL = 1 #! } #! #! Panics if: +#! - the leaf type is not 0 (not an asset claim). #! - the Merkle proof validation fails. #! - the origin token address is not registered in the bridge's token registry. #! @@ -216,6 +217,9 @@ pub proc claim # => [PROOF_DATA_KEY, pad(12)] swapw mem_loadw_be.CLAIM_LEAF_DATA_KEY_MEM_ADDR + + # Validate that this is an asset claim (leafType == 0) + exec.validate_leaf_type # => [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)] exec.verify_leaf_bridge @@ -246,6 +250,20 @@ end # HELPER PROCEDURES # ================================================================================================= +#! Validates that the leaf type is an asset claim (leafType == 0), not a message. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the leaf type is not 0 (not an asset claim). +#! +#! Invocation: exec +proc validate_leaf_type + mem_load.LEAF_TYPE_ADDRESS + assertz.err=ERR_INVALID_LEAF_TYPE +end + #! Computes the leaf value and verifies it against the AggLayer bridge state. #! #! Verification is delegated to `verify_leaf` to mimic the AggLayer Solidity contracts. @@ -936,7 +954,7 @@ end #! Invocation: exec proc create_mint_note_with_attachment # Create the MINT output note targeting the faucet - push.OUTPUT_NOTE_TYPE_PUBLIC + push.NOTE_TYPE_PUBLIC # => [note_type, MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] # Set tag to DEFAULT diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 824ad3da19..db367d415f 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -76,7 +76,7 @@ const ATTACHMENT_KIND_LOC=13 # ------------------------------------------------------------------------------------------------- const LEAF_TYPE_ASSET=0 -const PUBLIC_NOTE=1 +use miden::protocol::note::NOTE_TYPE_PUBLIC const BURN_NOTE_NUM_STORAGE_ITEMS=0 # PUBLIC INTERFACE @@ -528,7 +528,7 @@ proc create_burn_note exec.note::build_recipient # => [RECIPIENT] - push.PUBLIC_NOTE + push.NOTE_TYPE_PUBLIC push.DEFAULT_TAG # => [tag, note_type, RECIPIENT] diff --git a/crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm b/crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm index 8ebc0ba1d8..11ceaae5da 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm @@ -1,8 +1,8 @@ -# This file is generated by build.rs, do not modify +# This file contains deterministic values. Do not modify manually. -# This file contains the canonical zeros for the Keccak hash function. +# This file contains the canonical zeros for the Keccak hash function. # Zero of height `n` (ZERO_N) is the root of the binary tree of height `n` with leaves equal zero. -# +# # Since the Keccak hash is represented by eight u32 values, each constant consists of two Words. const ZERO_0_L = [0, 0, 0, 0] @@ -102,7 +102,8 @@ const ZERO_31_L = [2340505732, 1648733876, 2660540036, 3759582231] const ZERO_31_R = [2389186238, 4049365781, 1653344606, 2840985724] use ::agglayer::common::utils::mem_store_double_word - + + #! Inputs: [zeros_ptr] #! Outputs: [] pub proc load_zeros_to_memory diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/b2agg.masm similarity index 100% rename from crates/miden-agglayer/asm/note_scripts/B2AGG.masm rename to crates/miden-agglayer/asm/note_scripts/b2agg.masm diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/claim.masm similarity index 100% rename from crates/miden-agglayer/asm/note_scripts/CLAIM.masm rename to crates/miden-agglayer/asm/note_scripts/claim.masm diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/config_agg_bridge.masm similarity index 100% rename from crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm rename to crates/miden-agglayer/asm/note_scripts/config_agg_bridge.masm diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/update_ger.masm similarity index 100% rename from crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm rename to crates/miden-agglayer/asm/note_scripts/update_ger.masm diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index 4449d6d849..af7d0c2768 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -21,11 +21,6 @@ use miden_standards::account::mint_policies::OwnerControlled; // CONSTANTS // ================================================================================================ -/// Defines whether the build script should generate files in `/src`. -/// The docs.rs build pipeline has a read-only filesystem, so we have to avoid writing to `src`, -/// otherwise the docs will fail to build there. Note that writing to `OUT_DIR` is fine. -const BUILD_GENERATED_FILES_IN_SRC: bool = option_env!("BUILD_GENERATED_FILES_IN_SRC").is_some(); - const ASSETS_DIR: &str = "assets"; const ASM_DIR: &str = "asm"; const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts"; @@ -33,7 +28,7 @@ const ASM_AGGLAYER_DIR: &str = "agglayer"; const ASM_AGGLAYER_BRIDGE_DIR: &str = "agglayer/bridge"; const ASM_COMPONENTS_DIR: &str = "components"; -const AGGLAYER_ERRORS_FILE: &str = "src/errors/agglayer.rs"; +const AGGLAYER_ERRORS_RS_FILE: &str = "agglayer_errors.rs"; const AGGLAYER_ERRORS_ARRAY_NAME: &str = "AGGLAYER_ERRORS"; const AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME: &str = "agglayer_constants.rs"; @@ -47,21 +42,17 @@ const AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME: &str = "agglayer_constants.rs"; fn main() -> Result<()> { // re-build when the MASM code changes println!("cargo::rerun-if-changed={ASM_DIR}/"); - println!("cargo::rerun-if-env-changed=BUILD_GENERATED_FILES_IN_SRC"); + println!("cargo::rerun-if-env-changed=REGENERATE_CANONICAL_ZEROS"); - // Copies the MASM code to the build directory let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let build_dir = env::var("OUT_DIR").unwrap(); - let src = Path::new(&crate_dir).join(ASM_DIR); - - // generate canonical zeros in `asm/agglayer/bridge/canonical_zeros.masm` - generate_canonical_zeros(&src.join(ASM_AGGLAYER_BRIDGE_DIR))?; - let dst = Path::new(&build_dir).to_path_buf(); - shared::copy_directory(src, &dst, ASM_DIR)?; + // validate (or regenerate) canonical zeros in `asm/agglayer/bridge/canonical_zeros.masm` + let crate_path = Path::new(&crate_dir); + ensure_canonical_zeros(&crate_path.join(ASM_DIR).join(ASM_AGGLAYER_BRIDGE_DIR))?; - // set source directory to {OUT_DIR}/asm - let source_dir = dst.join(ASM_DIR); + // Read MASM sources directly from the crate's asm/ directory. + let source_dir = crate_path.join(ASM_DIR); // set target directory to {OUT_DIR}/assets let target_dir = Path::new(&build_dir).join(ASSETS_DIR); @@ -91,7 +82,7 @@ fn main() -> Result<()> { let constants_out_path = Path::new(&build_dir).join(AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME); generate_agglayer_constants(constants_out_path, component_libraries)?; - generate_error_constants(&source_dir)?; + generate_error_constants(&source_dir, &build_dir)?; Ok(()) } @@ -320,15 +311,7 @@ fn generate_agglayer_constants( /// /// The function ensures that a constant is not defined twice, except if their error message is the /// same. This can happen across multiple files. -/// -/// Because the error files will be written to ./src/errors, this should be a no-op if ./src is -/// read-only. To enable writing to ./src, set the `BUILD_GENERATED_FILES_IN_SRC` environment -/// variable. -fn generate_error_constants(asm_source_dir: &Path) -> Result<()> { - if !BUILD_GENERATED_FILES_IN_SRC { - return Ok(()); - } - +fn generate_error_constants(asm_source_dir: &Path, build_dir: &str) -> Result<()> { // Miden agglayer errors // ------------------------------------------ @@ -336,7 +319,7 @@ fn generate_error_constants(asm_source_dir: &Path) -> Result<()> { .context("failed to extract all masm errors")?; shared::generate_error_file( shared::ErrorModule { - file_name: AGGLAYER_ERRORS_FILE, + file_path: Path::new(build_dir).join(AGGLAYER_ERRORS_RS_FILE), array_name: AGGLAYER_ERRORS_ARRAY_NAME, is_crate_local: false, }, @@ -346,14 +329,13 @@ fn generate_error_constants(asm_source_dir: &Path) -> Result<()> { Ok(()) } -// CANONICAL ZEROS FILE GENERATION +// CANONICAL ZEROS VALIDATION // ================================================================================================ -fn generate_canonical_zeros(target_dir: &Path) -> Result<()> { - if !BUILD_GENERATED_FILES_IN_SRC { - return Ok(()); - } - +/// Validates that the committed `canonical_zeros.masm` matches the expected content computed from +/// Keccak256 canonical zeros. If the `REGENERATE_CANONICAL_ZEROS` environment variable is set, +/// the file is regenerated instead. +fn ensure_canonical_zeros(target_dir: &Path) -> Result<()> { const TREE_HEIGHT: u8 = 32; let mut zeros_by_height = Vec::with_capacity(TREE_HEIGHT as usize); @@ -373,10 +355,10 @@ fn generate_canonical_zeros(target_dir: &Path) -> Result<()> { // convert the keccak digest into the sequence of u32 values and create two word constants from // them to represent the hash let mut zero_constants = String::from( - "# This file is generated by build.rs, do not modify\n -# This file contains the canonical zeros for the Keccak hash function. + "# This file contains deterministic values. Do not modify manually.\n +# This file contains the canonical zeros for the Keccak hash function. # Zero of height `n` (ZERO_N) is the root of the binary tree of height `n` with leaves equal zero. -# +# # Since the Keccak hash is represented by eight u32 values, each constant consists of two Words.\n", ); @@ -398,7 +380,8 @@ fn generate_canonical_zeros(target_dir: &Path) -> Result<()> { zero_constants.push_str( " use ::agglayer::common::utils::mem_store_double_word - + + #! Inputs: [zeros_ptr] #! Outputs: [] pub proc load_zeros_to_memory\n", @@ -410,9 +393,23 @@ pub proc load_zeros_to_memory\n", zero_constants.push_str("\tdrop\nend\n"); - // write the resulting masm content into the file only if it changed to avoid - // invalidating the cargo fingerprint for the `asm/` directory - shared::write_if_changed(target_dir.join("canonical_zeros.masm"), zero_constants)?; + let file_path = target_dir.join("canonical_zeros.masm"); + + if option_env!("REGENERATE_CANONICAL_ZEROS").is_some() { + // Regeneration mode: write the file + shared::write_if_changed(&file_path, &zero_constants)?; + } else { + // Validation mode: ensure the committed file matches + let committed = fs::read_to_string(&file_path) + .into_diagnostic() + .wrap_err("canonical_zeros.masm not found - it should be committed in the repo")?; + if committed != zero_constants { + return Err(Report::msg( + "canonical_zeros.masm is out of date. \ + Run with REGENERATE_CANONICAL_ZEROS=1 to regenerate and commit the result.", + )); + } + } Ok(()) } @@ -431,54 +428,6 @@ mod shared { use regex::Regex; use walkdir::WalkDir; - /// Recursively copies `src` into `dst`. - /// - /// This function will overwrite the existing files if re-executed. - pub fn copy_directory, R: AsRef>( - src: T, - dst: R, - asm_dir: &str, - ) -> Result<()> { - let mut prefix = src.as_ref().canonicalize().unwrap(); - // keep all the files inside the `asm` folder - prefix.pop(); - - let target_dir = dst.as_ref().join(asm_dir); - if target_dir.exists() { - // Clear existing asm files that were copied earlier which may no longer exist. - fs::remove_dir_all(&target_dir) - .into_diagnostic() - .wrap_err("failed to remove ASM directory")?; - } - - // Recreate the directory structure. - fs::create_dir_all(&target_dir) - .into_diagnostic() - .wrap_err("failed to create ASM directory")?; - - let dst = dst.as_ref(); - let mut todo = vec![src.as_ref().to_path_buf()]; - - while let Some(goal) = todo.pop() { - for entry in fs::read_dir(goal).unwrap() { - let path = entry.unwrap().path(); - if path.is_dir() { - let src_dir = path.canonicalize().unwrap(); - let dst_dir = dst.join(src_dir.strip_prefix(&prefix).unwrap()); - if !dst_dir.exists() { - fs::create_dir_all(&dst_dir).unwrap(); - } - todo.push(src_dir); - } else { - let dst_file = dst.join(path.strip_prefix(&prefix).unwrap()); - fs::copy(&path, dst_file).unwrap(); - } - } - } - - Ok(()) - } - /// Returns a vector with paths to all MASM files in the specified directory. /// /// All non-MASM files are skipped. @@ -657,7 +606,7 @@ mod shared { .into_diagnostic()?; } - write_if_changed(module.file_name, output)?; + fs::write(module.file_path, output).into_diagnostic()?; Ok(()) } @@ -690,9 +639,9 @@ mod shared { pub message: String, } - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + #[derive(Debug, Clone)] pub struct ErrorModule { - pub file_name: &'static str, + pub file_path: PathBuf, pub array_name: &'static str, pub is_crate_local: bool, } diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs index 5309449d93..b407cc6991 100644 --- a/crates/miden-agglayer/src/b2agg_note.rs +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -32,7 +32,7 @@ use crate::EthAddress; // Initialize the B2AGG note script only once static B2AGG_SCRIPT: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/B2AGG.masb")); + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/b2agg.masb")); let program = Program::read_from_bytes(bytes).expect("shipped B2AGG script is well-formed"); NoteScript::new(program) }); diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index efdd9f6663..1ee5d31d2e 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -36,7 +36,7 @@ use crate::EthAddress; // Initialize the CONFIG_AGG_BRIDGE note script only once static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock = LazyLock::new(|| { let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CONFIG_AGG_BRIDGE.masb")); + include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/config_agg_bridge.masb")); let program = Program::read_from_bytes(bytes).expect("shipped CONFIG_AGG_BRIDGE script is well-formed"); NoteScript::new(program) diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs deleted file mode 100644 index 045c3226d6..0000000000 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ /dev/null @@ -1,93 +0,0 @@ -use miden_protocol::errors::MasmError; - -// This file is generated by build.rs, do not modify manually. -// It is generated by extracting errors from the MASM files in the `./asm` directory. -// -// To add a new error, define a constant in MASM of the pattern `const ERR__...`. -// Try to fit the error into a pre-existing category if possible (e.g. Account, Note, ...). - -// AGGLAYER ERRORS -// ================================================================================================ - -/// Error Message: "B2AGG note attachment target account does not match consuming account" -pub const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("B2AGG note attachment target account does not match consuming account"); -/// Error Message: "B2AGG script expects exactly 6 note storage items" -pub const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("B2AGG script expects exactly 6 note storage items"); -/// Error Message: "B2AGG script requires exactly 1 note asset" -pub const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("B2AGG script requires exactly 1 note asset"); - -/// Error Message: "mainnet flag must be 1 for a mainnet deposit" -pub const ERR_BRIDGE_NOT_MAINNET: MasmError = MasmError::from_static_str("mainnet flag must be 1 for a mainnet deposit"); -/// Error Message: "mainnet flag must be 0 for a rollup deposit" -pub const ERR_BRIDGE_NOT_ROLLUP: MasmError = MasmError::from_static_str("mainnet flag must be 0 for a rollup deposit"); - -/// Error Message: "claim note has already been spent" -pub const ERR_CLAIM_ALREADY_SPENT: MasmError = MasmError::from_static_str("claim note has already been spent"); -/// Error Message: "CLAIM note attachment target account does not match consuming account" -pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM note attachment target account does not match consuming account"); - -/// Error Message: "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" -pub const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE note attachment target account does not match consuming account"); -/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 7 note storage items" -pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 7 note storage items"); - -/// Error Message: "faucet is not registered in the bridge's faucet registry" -pub const ERR_FAUCET_NOT_REGISTERED: MasmError = MasmError::from_static_str("faucet is not registered in the bridge's faucet registry"); - -/// Error Message: "combined u64 doesn't fit in field" -pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combined u64 doesn't fit in field"); - -/// Error Message: "GER not found in storage" -pub const ERR_GER_NOT_FOUND: MasmError = MasmError::from_static_str("GER not found in storage"); - -/// Error Message: "leading bits of global index must be zero" -pub const ERR_LEADING_BITS_NON_ZERO: MasmError = MasmError::from_static_str("leading bits of global index must be zero"); - -/// Error Message: "mainnet flag must be 0 or 1" -pub const ERR_MAINNET_FLAG_INVALID: MasmError = MasmError::from_static_str("mainnet flag must be 0 or 1"); - -/// Error Message: "most-significant 4 bytes must be zero for AccountId" -pub const ERR_MSB_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes must be zero for AccountId"); - -/// Error Message: "number of leaves in the MTF would exceed 4294967295 (2^32 - 1)" -pub const ERR_MTF_LEAVES_NUM_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of leaves in the MTF would exceed 4294967295 (2^32 - 1)"); - -/// Error Message: "address limb is not u32" -pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); - -/// Error Message: "remainder z must be < 10^s" -pub const ERR_REMAINDER_TOO_LARGE: MasmError = MasmError::from_static_str("remainder z must be < 10^s"); - -/// Error Message: "rollup index must be zero for a mainnet deposit" -pub const ERR_ROLLUP_INDEX_NON_ZERO: MasmError = MasmError::from_static_str("rollup index must be zero for a mainnet deposit"); - -/// Error Message: "maximum scaling factor is 18" -pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("maximum scaling factor is 18"); - -/// Error Message: "note sender is not the bridge admin" -pub const ERR_SENDER_NOT_BRIDGE_ADMIN: MasmError = MasmError::from_static_str("note sender is not the bridge admin"); -/// Error Message: "note sender is not the global exit root manager" -pub const ERR_SENDER_NOT_GER_MANAGER: MasmError = MasmError::from_static_str("note sender is not the global exit root manager"); - -/// Error Message: "merkle proof verification failed: provided SMT root does not match the computed root" -pub const ERR_SMT_ROOT_VERIFICATION_FAILED: MasmError = MasmError::from_static_str("merkle proof verification failed: provided SMT root does not match the computed root"); - -/// Error Message: "source bridge network overflowed u32" -pub const ERR_SOURCE_BRIDGE_NETWORK_OVERFLOW: MasmError = MasmError::from_static_str("source bridge network overflowed u32"); - -/// Error Message: "token address is not registered in the bridge's token registry" -pub const ERR_TOKEN_NOT_REGISTERED: MasmError = MasmError::from_static_str("token address is not registered in the bridge's token registry"); - -/// Error Message: "x < y*10^s (underflow detected)" -pub const ERR_UNDERFLOW: MasmError = MasmError::from_static_str("x < y*10^s (underflow detected)"); - -/// Error Message: "UPDATE_GER note attachment target account does not match consuming account" -pub const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("UPDATE_GER note attachment target account does not match consuming account"); -/// Error Message: "UPDATE_GER script expects exactly 8 note storage items" -pub const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("UPDATE_GER script expects exactly 8 note storage items"); - -/// Error Message: "the agglayer bridge in u256 value is larger than 2**128 and cannot be verifiably scaled to u64" -pub const ERR_X_TOO_LARGE: MasmError = MasmError::from_static_str("the agglayer bridge in u256 value is larger than 2**128 and cannot be verifiably scaled to u64"); - -/// Error Message: "y exceeds max fungible token amount" -pub const ERR_Y_TOO_LARGE: MasmError = MasmError::from_static_str("y exceeds max fungible token amount"); diff --git a/crates/miden-agglayer/src/errors/mod.rs b/crates/miden-agglayer/src/errors/mod.rs index 2a86602e6f..3156528f2b 100644 --- a/crates/miden-agglayer/src/errors/mod.rs +++ b/crates/miden-agglayer/src/errors/mod.rs @@ -1,3 +1,3 @@ // Include generated error constants #[cfg(any(feature = "testing", test))] -include!("agglayer.rs"); +include!(concat!(env!("OUT_DIR"), "/agglayer_errors.rs")); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 97ceb05276..21ba7c40be 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -64,7 +64,7 @@ pub use utils::Keccak256Output; // Initialize the CLAIM note script only once static CLAIM_SCRIPT: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CLAIM.masb")); + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/claim.masb")); let program = Program::read_from_bytes(bytes).expect("shipped CLAIM script is well-formed"); NoteScript::new(program) }); diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index 07246db9f6..c965c5348b 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -34,7 +34,7 @@ use crate::ExitRoot; // Initialize the UPDATE_GER note script only once static UPDATE_GER_SCRIPT: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/UPDATE_GER.masb")); + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/update_ger.masb")); let program = Program::read_from_bytes(bytes).expect("shipped UPDATE_GER script is well-formed"); NoteScript::new(program) diff --git a/crates/miden-protocol-macros/Cargo.toml b/crates/miden-protocol-macros/Cargo.toml deleted file mode 100644 index 0ec8930785..0000000000 --- a/crates/miden-protocol-macros/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -authors.workspace = true -categories = ["development-tools::procedural-macro-helpers"] -description = "Procedural macros for Miden protocol" -edition.workspace = true -homepage.workspace = true -keywords = ["macros", "miden", "protocol"] -license.workspace = true -name = "miden-protocol-macros" -readme = "README.md" -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -syn = { features = ["extra-traits", "full"], version = "2.0" } - -[dev-dependencies] -miden-protocol = { path = "../miden-protocol" } - -[package.metadata.cargo-machete] -ignored = ["proc-macro2"] diff --git a/crates/miden-protocol-macros/README.md b/crates/miden-protocol-macros/README.md deleted file mode 100644 index 1b7676ec70..0000000000 --- a/crates/miden-protocol-macros/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# miden-protocol-macros - -A collection of procedural macros for the Miden protocol. - -## WordWrapper - -The `WordWrapper` derive macro automatically implements helpful accessor methods and conversions for tuple structs that wrap a `Word` type. - -### Usage - -Add the derive macro to any tuple struct with a single `Word` field: - -```rust -use miden_protocol_macros::WordWrapper; -use miden_crypto::word::Word; - -#[derive(WordWrapper)] -pub struct NoteId(Word); -``` - -### Generated Methods - -The macro automatically generates the following methods: - -#### Accessor Methods - -- **`new_unchecked(Word) -> Self`** - Construct without any checks -- **`as_elements(&self) -> &[Felt]`** - Returns the elements representation of the wrapped Word -- **`as_bytes(&self) -> [u8; 32]`** - Returns the byte representation -- **`to_hex(&self) -> String`** - Returns a big-endian, hex-encoded string -- **`as_word(&self) -> Word`** - Returns the underlying Word value - -### Example - -```rust -use miden_protocol_macros::WordWrapper; -use miden_crypto::word::Word; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, WordWrapper)] -pub struct NoteId(Word); - -// Create using new_unchecked (generated by the macro) -let word = Word::from([Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO]); -let note_id = NoteId::from_raw(word); - -// Use accessor methods -let elements = note_id.as_elements(); -let bytes = note_id.as_bytes(); -let hex = note_id.to_hex(); -let word_back = note_id.as_word(); -``` - -### Requirements - -The macro can only be applied to: -- Tuple structs (e.g., `struct Foo(Word)`) -- With exactly one field -- Where that field is of type `Word` - -### Benefits - -Using this macro eliminates boilerplate code. Instead of manually writing ~50 lines of implementation code for each Word wrapper type, you can simply add `#[derive(WordWrapper)]` to your struct definition. - -This is particularly useful in the Miden codebase where many types like `NoteId`, `TransactionId`, `Nullifier`, `BatchId`, etc. all follow the same pattern of wrapping a `Word` and providing similar accessor methods. - -### Important Notes - -- The macro generates the `new_unchecked` constructor. You should not manually implement this method. -- Previously, the macro also generated `From` and `From<&T>` trait implementations for `Word` and `[u8; 32]`. These have been **removed** to give types more control over their conversions. If you need these conversions, implement them manually for your specific type. diff --git a/crates/miden-protocol-macros/src/lib.rs b/crates/miden-protocol-macros/src/lib.rs deleted file mode 100644 index 100ccac410..0000000000 --- a/crates/miden-protocol-macros/src/lib.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Procedural macros for the Miden project. -//! -//! Provides derive macros and other procedural macros to reduce boilerplate -//! and ensure consistency across the Miden codebase. -//! -//! ## Available Macros -//! -//! ### `WordWrapper` -//! -//! A derive macro for tuple structs wrapping a `Word` type. Automatically generates -//! accessor methods and `From` trait implementations. - -use proc_macro::TokenStream; -use quote::quote; -use syn::{Data, DeriveInput, Fields, Type, parse_macro_input}; - -/// Generates accessor methods for tuple structs wrapping a `Word` type. -/// -/// Automatically implements: -/// - `new_unchecked(Word) -> Self` - Construct without further checks -/// - `as_elements(&self) -> &[Felt]` - Returns the elements representation -/// - `as_bytes(&self) -> [u8; 32]` - Returns the byte representation -/// - `to_hex(&self) -> String` - Returns a big-endian, hex-encoded string -/// - `as_word(&self) -> Word` - Returns the underlying Word -/// -/// Note: This macro does NOT generate `From` trait implementations. If you need conversions -/// to/from `Word` or `[u8; 32]`, implement them manually for your type. -/// -/// # Example -/// -/// ```ignore -/// use miden_protocol_macros::WordWrapper; -/// use miden_crypto::word::Word; -/// -/// #[derive(WordWrapper)] -/// pub struct NoteId(Word); -/// ``` -/// -/// This will generate implementations equivalent to: -/// -/// ```ignore -/// impl NoteId { -/// /// Construct without further checks from a given `Word` -/// /// -/// /// # Warning -/// /// -/// /// This requires the caller to uphold the guarantees/invariants of this type (if any). -/// /// Check the type-level documentation for guarantees/invariants. -/// pub fn new_unchecked(word: Word) -> Self { -/// Self(word) -/// } -/// -/// pub fn as_elements(&self) -> &[Felt] { -/// self.0.as_elements() -/// } -/// -/// pub fn as_bytes(&self) -> [u8; 32] { -/// self.0.as_bytes() -/// } -/// -/// pub fn to_hex(&self) -> String { -/// self.0.to_hex() -/// } -/// -/// pub fn as_word(&self) -> Word { -/// self.0 -/// } -/// } -/// ``` -#[proc_macro_derive(WordWrapper)] -pub fn word_wrapper_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = &input.ident; - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - - // Validate that this is a tuple struct with a single field - let field_type = match &input.data { - Data::Struct(data_struct) => match &data_struct.fields { - Fields::Unnamed(fields) if fields.unnamed.len() == 1 => match fields.unnamed.first() { - Some(field) => &field.ty, - None => { - return syn::Error::new_spanned( - &input, - "WordWrapper requires exactly one field", - ) - .to_compile_error() - .into(); - }, - }, - _ => { - return syn::Error::new_spanned( - &input, - "WordWrapper can only be derived for tuple structs with exactly one field", - ) - .to_compile_error() - .into(); - }, - }, - _ => { - return syn::Error::new_spanned(&input, "WordWrapper can only be derived for structs") - .to_compile_error() - .into(); - }, - }; - - // Verify that the field type is 'Word' (or a path ending in 'Word') - if let Type::Path(type_path) = field_type { - let last_segment = type_path.path.segments.last(); - if let Some(segment) = last_segment { - if segment.ident != "Word" { - return syn::Error::new_spanned( - field_type, - "WordWrapper can only be derived for types wrapping a 'Word' field", - ) - .to_compile_error() - .into(); - } - } else { - return syn::Error::new_spanned( - field_type, - "WordWrapper can only be derived for types wrapping a 'Word' field", - ) - .to_compile_error() - .into(); - } - } else { - return syn::Error::new_spanned( - field_type, - "WordWrapper can only be derived for types wrapping a 'Word' field", - ) - .to_compile_error() - .into(); - } - - let expanded = quote! { - impl #impl_generics #name #ty_generics #where_clause { - /// Construct without further checks from a given `Word` - /// - /// # Warning - /// - /// This requires the caller to uphold the guarantees/invariants of this type (if any). - /// Check the type-level documentation for guarantees/invariants. - pub fn from_raw(word: Word) -> Self { - Self(word) - } - - /// Returns the elements representation of this value. - pub fn as_elements(&self) -> &[Felt] { - self.0.as_elements() - } - - /// Returns the byte representation of this value. - pub fn as_bytes(&self) -> [u8; 32] { - self.0.as_bytes() - } - - /// Returns a big-endian, hex-encoded string. - pub fn to_hex(&self) -> String { - self.0.to_hex() - } - - /// Returns the underlying word of this value. - pub fn as_word(&self) -> Word { - self.0 - } - } - }; - - TokenStream::from(expanded) -} diff --git a/crates/miden-protocol-macros/tests/integration_test.rs b/crates/miden-protocol-macros/tests/integration_test.rs deleted file mode 100644 index 46f807852f..0000000000 --- a/crates/miden-protocol-macros/tests/integration_test.rs +++ /dev/null @@ -1,42 +0,0 @@ -#[cfg(test)] -mod tests { - use miden_protocol::{Felt, Word}; - use miden_protocol_macros::WordWrapper; - - #[derive(Debug, Clone, Copy, PartialEq, Eq, WordWrapper)] - pub struct TestId(Word); - - #[test] - fn test_word_wrapper_accessors() { - // Create a test Word - let word = Word::from([Felt::ONE, Felt::ONE, Felt::ZERO, Felt::ZERO]); - // Use the new_unchecked method generated by the macro - let test_id = TestId::from_raw(word); - - // Test as_elements - let elements = test_id.as_elements(); - assert_eq!(elements.len(), 4); - assert_eq!(elements[0], Felt::ONE); - assert_eq!(elements[1], Felt::ONE); - - // Test as_bytes - let bytes = test_id.as_bytes(); - assert_eq!(bytes.len(), 32); - - // Test to_hex - let hex = test_id.to_hex(); - assert!(!hex.is_empty()); - - // Test as_word - let retrieved_word = test_id.as_word(); - assert_eq!(retrieved_word, word); - } - - #[test] - fn test_new_unchecked_is_generated() { - // This test verifies that new_unchecked is generated by the macro - let word = Word::from([Felt::ONE, Felt::ONE, Felt::ZERO, Felt::ZERO]); - let test_id = TestId::from_raw(word); - assert_eq!(test_id.as_word(), word); - } -} diff --git a/crates/miden-protocol/Cargo.toml b/crates/miden-protocol/Cargo.toml index 74f08d132b..ffc69867f5 100644 --- a/crates/miden-protocol/Cargo.toml +++ b/crates/miden-protocol/Cargo.toml @@ -40,9 +40,9 @@ miden-assembly-syntax = { workspace = true } miden-core = { workspace = true } miden-core-lib = { workspace = true } miden-crypto = { workspace = true } +miden-crypto-derive = { workspace = true } miden-mast-package = { workspace = true } miden-processor = { workspace = true } -miden-protocol-macros = { workspace = true } miden-utils-sync = { workspace = true } miden-verifier = { workspace = true } diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm b/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm index 2d64bb61ad..9c4423f9d7 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm @@ -8,13 +8,13 @@ pub const WORD_SIZE = 4 pub const MAX_NOTE_STORAGE_ITEMS = 1024 # The maximum number of assets that can be stored in a single note. -pub const MAX_ASSETS_PER_NOTE = 256 +pub const MAX_ASSETS_PER_NOTE = 64 # The maximum number of notes that can be consumed in a single transaction. pub const MAX_INPUT_NOTES_PER_TX = 1024 # The size of the memory segment allocated to each note. -pub const NOTE_MEM_SIZE = 3072 +pub const NOTE_MEM_SIZE = 1024 # The depth of the Merkle tree used to commit to notes produced in a block. pub const NOTE_TREE_DEPTH = 16 diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 4efa51d1e9..25f533bddd 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -9,7 +9,7 @@ pub type AccountId = struct { prefix: felt, suffix: felt } # ERRORS # ================================================================================================= -const ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceed 255" +const ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceeds 64" const ERR_ACCOUNT_IS_NOT_NATIVE="the active account is not native" @@ -2012,7 +2012,7 @@ pub proc set_output_note_num_assets # => [note_ptr + offset, num_assets] # check note number of assets limit - dup.1 push.MAX_ASSETS_PER_NOTE lt assert.err=ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT + dup.1 lte.MAX_ASSETS_PER_NOTE assert.err=ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT mem_store end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm index a2fa70b19b..6733303dda 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -4,17 +4,20 @@ use $kernel::asset::ASSET_SIZE use $kernel::constants::NOTE_MEM_SIZE use $kernel::memory +pub use $kernel::util::note::NOTE_TYPE_PUBLIC +pub use $kernel::util::note::NOTE_TYPE_PRIVATE + # ERRORS # ================================================================================================= -const ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceed 255" +const ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceeds 64" # CONSTANTS # ================================================================================================= # The diff between the memory address after first mem_stream operation and the next target when # generating the output notes commitment. Must be NOTE_MEM_SIZE - 8; -const OUTPUT_NOTE_HASHING_MEM_DIFF=2040 +const OUTPUT_NOTE_HASHING_MEM_DIFF=1016 # ACTIVE NOTE PROCEDURES # ================================================================================================= diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index c8a80d5f06..0d44b877a5 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -4,6 +4,7 @@ use $kernel::callbacks use $kernel::fungible_asset use $kernel::memory use $kernel::note +use $kernel::note::NOTE_TYPE_PUBLIC use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX use $kernel::util::note::ATTACHMENT_KIND_NONE use $kernel::util::note::ATTACHMENT_KIND_ARRAY @@ -14,10 +15,6 @@ use miden::core::word # CONSTANTS # ================================================================================================= -# Constants for different note types -const PUBLIC_NOTE=1 # 0b01 -const PRIVATE_NOTE=2 # 0b10 - # The default value of the felt at index 3 in the note metadata header when a new note is created. # All zeros sets the attachment kind to None and the user-defined attachment scheme to "none". const ATTACHMENT_DEFAULT_KIND_AND_SCHEME=0 @@ -192,7 +189,7 @@ end #! - the asset key or value are malformed (e.g., invalid faucet ID). #! - the max amount of fungible assets is exceeded. #! - the non-fungible asset already exists in the note. -#! - the total number of ASSETs exceeds the maximum of 256. +#! - the total number of ASSETs exceeds the maximum of 64. pub proc add_asset # check if the note exists, it must be within [0, num_of_notes] dup.8 exec.memory::get_num_output_notes lte assert.err=ERR_NOTE_INVALID_INDEX @@ -318,10 +315,12 @@ end #! or off-chain). #! - NOTE_METADATA_HEADER is the metadata associated with a note. pub proc build_metadata_header - # Validate that note type is private or public. + # Validate that note type is private (0) or public (1). # -------------------------------------------------------------------------------------------- - dup.1 eq.PRIVATE_NOTE dup.2 eq.PUBLIC_NOTE or assert.err=ERR_NOTE_INVALID_TYPE + dup.1 + u32assert.err=ERR_NOTE_INVALID_TYPE u32lte.NOTE_TYPE_PUBLIC + assert.err=ERR_NOTE_INVALID_TYPE # => [tag, note_type] # Validate the note tag fits into a u32. @@ -330,22 +329,23 @@ pub proc build_metadata_header u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 # => [tag, note_type] - # Merge note type and sender ID suffix. + # Merge note type, version, and sender ID suffix. # -------------------------------------------------------------------------------------------- exec.account::get_id # => [sender_id_suffix, sender_id_prefix, tag, note_type] - # the lower bits of an account ID suffix are guaranteed to be zero, so we can safely use that - # space to encode the note type - movup.3 add - # => [sender_id_suffix_and_note_type, sender_id_prefix, tag] + # The lower 8 bits of the account ID suffix are guaranteed to be zero by construction. + # Encode note_type at bit 4, leaving version at 0 (in bits 0..=3). + # Shifting note_type left by 4 is equivalent to multiplying by 16. + movup.3 mul.16 add + # => [sender_id_suffix_type_version, sender_id_prefix, tag] # Build metadata header. # -------------------------------------------------------------------------------------------- push.ATTACHMENT_DEFAULT_KIND_AND_SCHEME movdn.3 - # => [sender_id_suffix_and_note_type, sender_id_prefix, tag, attachment_kind_scheme] + # => [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme] # => [NOTE_METADATA_HEADER] end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm index 9381fb6359..01d808d09c 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm @@ -48,7 +48,7 @@ const ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDE const ERR_PROLOGUE_MISMATCH_OF_REFERENCE_BLOCK_MMR_AND_NOTE_AUTHENTICATION_MMR="reference block MMR and note's authentication MMR must match" -const ERR_PROLOGUE_NUMBER_OF_NOTE_ASSETS_EXCEEDS_LIMIT="number of note assets exceeds the maximum limit of 256" +const ERR_PROLOGUE_NUMBER_OF_NOTE_ASSETS_EXCEEDS_LIMIT="number of note assets exceeds the maximum limit of 64" const ERR_PROLOGUE_PROVIDED_INPUT_ASSETS_INFO_DOES_NOT_MATCH_ITS_COMMITMENT="provided info about assets of an input does not match its commitment" diff --git a/crates/miden-protocol/asm/protocol/active_note.masm b/crates/miden-protocol/asm/protocol/active_note.masm index cf47a36aa1..adfc446466 100644 --- a/crates/miden-protocol/asm/protocol/active_note.masm +++ b/crates/miden-protocol/asm/protocol/active_note.masm @@ -184,7 +184,7 @@ pub proc get_sender # => [METADATA_HEADER] # extract the sender ID from the metadata header - exec.note::extract_sender_from_metadata + exec.note::metadata_into_sender # => [sender_id_suffix, sender_id_prefix] end diff --git a/crates/miden-protocol/asm/protocol/input_note.masm b/crates/miden-protocol/asm/protocol/input_note.masm index be9ae33d03..19696cc880 100644 --- a/crates/miden-protocol/asm/protocol/input_note.masm +++ b/crates/miden-protocol/asm/protocol/input_note.masm @@ -182,7 +182,7 @@ pub proc get_sender # => [METADATA_HEADER] # extract the sender ID from the metadata header - exec.note::extract_sender_from_metadata + exec.note::metadata_into_sender # => [sender_id_suffix, sender_id_prefix] end diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 8962a86263..37c7e6081a 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -4,6 +4,8 @@ use miden::core::mem # Re-export the max inputs per note constant. pub use miden::protocol::util::note::MAX_NOTE_STORAGE_ITEMS +pub use miden::protocol::util::note::NOTE_TYPE_PUBLIC +pub use miden::protocol::util::note::NOTE_TYPE_PRIVATE # ERRORS # ================================================================================================= @@ -82,9 +84,6 @@ end #! #! Inputs: #! Operand stack: [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] -#! Advice map: { -#! STORAGE_COMMITMENT: [INPUTS], -#! } #! Outputs: #! Operand stack: [RECIPIENT] #! Advice map: { @@ -183,12 +182,12 @@ end #! - METADATA_HEADER is the metadata of a note. #! - sender_{suffix,prefix} are the suffix and prefix felts of the sender ID of the note which #! metadata was provided. -pub proc extract_sender_from_metadata - # => [sender_id_suffix_and_note_type, sender_id_prefix, tag, attachment_kind_scheme] +pub proc metadata_into_sender + # => [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme] # drop tag and attachment_kind_scheme movup.3 drop movup.2 drop - # => [sender_id_suffix_and_note_type, sender_id_prefix] + # => [sender_id_suffix_type_version, sender_id_prefix] # extract suffix of sender from merged layout, which means clearing the least significant byte exec.account_id::shape_suffix @@ -206,8 +205,8 @@ end #! - attachment_scheme is the attachment scheme of the note. #! #! Invocation: exec -pub proc extract_attachment_info_from_metadata - # => [sender_id_suffix_and_note_type, sender_id_prefix, tag, attachment_kind_scheme] +pub proc metadata_into_attachment_info + # => [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme] drop drop drop # => [attachment_kind_scheme] @@ -217,3 +216,33 @@ pub proc extract_attachment_info_from_metadata u32split swap # => [attachment_kind, attachment_scheme] end + +#! Extracts the note type from the provided metadata header. +#! +#! The note type is encoded as a single bit at the 4th position from the right side (LSB) of the +#! first felt of the metadata header, where 0 = Private and 1 = Public. +#! +#! Inputs: [METADATA_HEADER] +#! Outputs: [note_type] +#! +#! Where: +#! - METADATA_HEADER is the metadata of a note, laid out on the stack as +#! [sender_id_suffix_type_version, sender_id_prefix, tag, attachment_kind_scheme]. +#! The first felt (sender_id_suffix_type_version) has the following bit layout: +#! [sender_id_suffix (56 bits) | reserved (3 bits) | note_type (1 bit) | version (4 bits)] +#! - note_type is the type of the note (0 for private, 1 for public). +#! +#! Invocation: exec +pub proc metadata_into_note_type + movdn.3 drop drop drop + # => [sender_id_suffix_type_version] + + u32split swap drop + # => [lo32] + + u32shr.4 + # => [shifted] + + u32and.1 + # => [note_type] +end diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm index 066dfcd2fb..d0f474b692 100644 --- a/crates/miden-protocol/asm/shared_utils/util/note.masm +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -10,3 +10,12 @@ pub const ATTACHMENT_KIND_NONE=0 pub const ATTACHMENT_KIND_WORD=1 #! A note attachment consisting of the commitment to a set of felts. pub const ATTACHMENT_KIND_ARRAY=2 + +# Note type constants. These encode the note type in the lower byte of the metadata header. +# See NoteType in the Rust protocol crate for details. + +#! The note type of private notes. +pub const NOTE_TYPE_PRIVATE=0 + +#! The note type of public notes. +pub const NOTE_TYPE_PUBLIC=1 diff --git a/crates/miden-protocol/src/account/access.rs b/crates/miden-protocol/src/account/access.rs new file mode 100644 index 0000000000..1f0241bdd6 --- /dev/null +++ b/crates/miden-protocol/src/account/access.rs @@ -0,0 +1,131 @@ +use alloc::fmt; + +use crate::Felt; +use crate::errors::RoleSymbolError; +use crate::utils::ShortCapitalString; + +/// Represents a role symbol for role-based access control. +/// +/// Role symbols can consist of up to 12 uppercase Latin characters and underscores, e.g. +/// "MINTER", "BURNER", "MINTER_ADMIN". +/// +/// The label is stored internally as a validated short string (`A`–`Z` and `_`) and can be +/// converted to a [`Felt`] encoding via [`as_element()`](Self::as_element). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoleSymbol(ShortCapitalString); + +impl RoleSymbol { + /// Alphabet used for role symbols (`A-Z` and `_`). + pub const ALPHABET: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_"; + + /// The minimum integer value of an encoded [`RoleSymbol`]. + /// + /// This value encodes the "A" role symbol. + pub const MIN_ENCODED_VALUE: u64 = 1; + + /// The maximum integer value of an encoded [`RoleSymbol`]. + /// + /// This value encodes the "____________" role symbol (12 underscores). + pub const MAX_ENCODED_VALUE: u64 = 4052555153018976252; + + /// Constructs a new [`RoleSymbol`] from a string, panicking on invalid input. + /// + /// # Panics + /// + /// Panics if: + /// - The length of the provided string is less than 1 or greater than 12. + /// - The provided role symbol contains characters outside `A-Z` and `_`. + pub fn new_unchecked(role_symbol: &str) -> Self { + Self::new(role_symbol).expect("invalid role symbol") + } + + /// Creates a new [`RoleSymbol`] from the provided role symbol string. + /// + /// # Errors + /// Returns an error if: + /// - The length of the provided string is less than 1 or greater than 12. + /// - The provided role symbol contains characters outside `A-Z` and `_`. + pub fn new(role_symbol: &str) -> Result { + ShortCapitalString::from_ascii_uppercase_and_underscore(role_symbol) + .map(Self) + .map_err(Into::into) + } + + /// Returns the [`Felt`] encoding of this role symbol. + pub fn as_element(&self) -> Felt { + self.0.as_element(Self::ALPHABET).expect("RoleSymbol alphabet is always valid") + } +} + +impl fmt::Display for RoleSymbol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for Felt { + fn from(role_symbol: RoleSymbol) -> Self { + role_symbol.as_element() + } +} + +impl From<&RoleSymbol> for Felt { + fn from(role_symbol: &RoleSymbol) -> Self { + role_symbol.as_element() + } +} + +impl TryFrom<&str> for RoleSymbol { + type Error = RoleSymbolError; + + fn try_from(role_symbol: &str) -> Result { + Self::new(role_symbol) + } +} + +impl TryFrom for RoleSymbol { + type Error = RoleSymbolError; + + /// Decodes a [`Felt`] representation of the role symbol into a [`RoleSymbol`]. + fn try_from(felt: Felt) -> Result { + ShortCapitalString::try_from_encoded_felt( + felt, + Self::ALPHABET, + Self::MIN_ENCODED_VALUE, + Self::MAX_ENCODED_VALUE, + ) + .map(Self) + .map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use assert_matches::assert_matches; + + use super::{Felt, RoleSymbol}; + use crate::errors::RoleSymbolError; + + #[test] + fn test_role_symbol_roundtrip_and_validation() { + let role_symbols = ["MINTER", "BURNER", "MINTER_ADMIN", "A", "A_B_C"]; + for role_symbol in role_symbols { + let encoded: Felt = RoleSymbol::new(role_symbol).unwrap().into(); + let decoded = RoleSymbol::try_from(encoded).unwrap(); + assert_eq!(decoded.to_string(), role_symbol); + } + + assert_matches!(RoleSymbol::new("").unwrap_err(), RoleSymbolError::InvalidLength(0)); + assert_matches!( + RoleSymbol::new("ABCDEFGHIJKLM").unwrap_err(), + RoleSymbolError::InvalidLength(13) + ); + assert_matches!( + RoleSymbol::new("MINTER-ADMIN").unwrap_err(), + RoleSymbolError::InvalidCharacter + ); + assert_matches!(RoleSymbol::new("mINTER").unwrap_err(), RoleSymbolError::InvalidCharacter); + } +} diff --git a/crates/miden-protocol/src/account/auth.rs b/crates/miden-protocol/src/account/auth.rs index e4947095db..ec3160d8f9 100644 --- a/crates/miden-protocol/src/account/auth.rs +++ b/crates/miden-protocol/src/account/auth.rs @@ -240,6 +240,12 @@ impl From for PublicKeyCommitment { } } +impl From for PublicKeyCommitment { + fn from(value: ecdsa_k256_keccak::PublicKey) -> Self { + Self(value.to_commitment()) + } +} + impl From for Word { fn from(value: PublicKeyCommitment) -> Self { value.0 diff --git a/crates/miden-protocol/src/account/builder/mod.rs b/crates/miden-protocol/src/account/builder/mod.rs index 2d1280f295..68472df64d 100644 --- a/crates/miden-protocol/src/account/builder/mod.rs +++ b/crates/miden-protocol/src/account/builder/mod.rs @@ -37,10 +37,10 @@ use crate::{Felt, Word}; /// - [`AccountBuilder::with_component`], which must be called at least once. /// /// Under the `testing` feature, it is possible to: -/// - Build an existing account using [`AccountBuilder::build_existing`] which will set the -/// account's nonce to `1` by default, or to the configured value. -/// - Add assets to the account's vault, however this will only succeed when using -/// [`AccountBuilder::build_existing`]. +/// - Build an existing account using `AccountBuilder::build_existing`, which will set the account's +/// nonce to `1` by default, or to the configured value. +/// - Add assets to the account's vault; this only succeeds when using +/// `AccountBuilder::build_existing`. /// /// **Storage Slot Order** /// diff --git a/crates/miden-protocol/src/account/code/mod.rs b/crates/miden-protocol/src/account/code/mod.rs index d5c8516eb0..09e7558501 100644 --- a/crates/miden-protocol/src/account/code/mod.rs +++ b/crates/miden-protocol/src/account/code/mod.rs @@ -61,6 +61,24 @@ impl AccountCode { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- + /// Returns a new [`AccountCode`] instantiated from the provided [`MastForest`] and a list of + /// [`AccountProcedureRoot`]s. + /// + /// # Panics + /// + /// Panics if: + /// - The number of procedures is smaller than 2 or greater than 256. + pub fn from_parts(mast: Arc, procedures: Vec) -> Self { + assert!(procedures.len() >= Self::MIN_NUM_PROCEDURES, "not enough account procedures"); + assert!(procedures.len() <= Self::MAX_NUM_PROCEDURES, "too many account procedures"); + + Self { + commitment: build_procedure_commitment(&procedures), + procedures, + mast, + } + } + /// Creates a new [`AccountCode`] from the provided components' libraries. /// /// For testing use only. @@ -117,32 +135,6 @@ impl AccountCode { }) } - /// Returns a new [AccountCode] deserialized from the provided bytes. - /// - /// # Errors - /// Returns an error if account code deserialization fails. - pub fn from_bytes(bytes: &[u8]) -> Result { - Self::read_from_bytes(bytes).map_err(AccountError::AccountCodeDeserializationError) - } - - /// Returns a new [`AccountCode`] instantiated from the provided [`MastForest`] and a list of - /// [`AccountProcedureRoot`]s. - /// - /// # Panics - /// - /// Panics if: - /// - The number of procedures is smaller than 2 or greater than 256. - pub fn from_parts(mast: Arc, procedures: Vec) -> Self { - assert!(procedures.len() >= Self::MIN_NUM_PROCEDURES, "not enough account procedures"); - assert!(procedures.len() <= Self::MAX_NUM_PROCEDURES, "too many account procedures"); - - Self { - commitment: build_procedure_commitment(&procedures), - procedures, - mast, - } - } - // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -190,7 +182,7 @@ impl AccountCode { /// ``` /// /// And then concatenating the resulting elements into a single vector. - pub fn as_elements(&self) -> Vec { + pub fn to_elements(&self) -> Vec { procedures_as_elements(self.procedures()) } @@ -281,13 +273,33 @@ impl Serializable for AccountCode { impl Deserializable for AccountCode { fn read_from(source: &mut R) -> Result { - let module = Arc::new(MastForest::read_from(source)?); + let mast = Arc::new(MastForest::read_from(source)?); let num_procedures = (source.read_u8()? as usize) + 1; + + // make sure the number of procedures is valid; we only check the minimum because + // u8::MAX + 1 is guaranteed to be less than or equal to 256 + if num_procedures < Self::MIN_NUM_PROCEDURES { + return Err(DeserializationError::InvalidValue(format!( + "account code must contain at least {} procedures, but has only {num_procedures} procedures", + Self::MIN_NUM_PROCEDURES + ))); + } + let procedures = source .read_many_iter(num_procedures)? .collect::, _>>()?; - Ok(Self::from_parts(module, procedures)) + // make sure that all account procedures are in the MAST forest + for procedure in procedures.iter() { + if mast.find_procedure_root(procedure.as_word()).is_none() { + return Err(DeserializationError::InvalidValue(format!( + "procedure with root {} is missing from account code's MAST forest", + procedure.as_word() + ))); + } + } + + Ok(Self::from_parts(mast, procedures)) } } @@ -391,13 +403,13 @@ impl AccountProcedureBuilder { // ================================================================================================ /// Computes the commitment to the given procedures -pub(crate) fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word { +fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word { let elements = procedures_as_elements(procedures); Hasher::hash_elements(&elements) } /// Converts given procedures into field elements -pub(crate) fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec { +fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec { procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect() } diff --git a/crates/miden-protocol/src/account/code/procedure.rs b/crates/miden-protocol/src/account/code/procedure.rs index a88fde5b0d..2c27f9fd20 100644 --- a/crates/miden-protocol/src/account/code/procedure.rs +++ b/crates/miden-protocol/src/account/code/procedure.rs @@ -3,8 +3,8 @@ use alloc::sync::Arc; use miden_core::mast::MastForest; use miden_core::prettier::PrettyPrint; +use miden_crypto_derive::WordWrapper; use miden_processor::mast::{MastNode, MastNodeExt, MastNodeId}; -use miden_protocol_macros::WordWrapper; use super::Felt; use crate::Word; diff --git a/crates/miden-protocol/src/account/component/storage/type_registry.rs b/crates/miden-protocol/src/account/component/storage/type_registry.rs index 88c884e9ec..2eddcb0766 100644 --- a/crates/miden-protocol/src/account/component/storage/type_registry.rs +++ b/crates/miden-protocol/src/account/component/storage/type_registry.rs @@ -8,6 +8,7 @@ use core::str::FromStr; use miden_core::{Felt, Word}; use thiserror::Error; +use crate::account::RoleSymbol; use crate::account::auth::{AuthScheme, PublicKey}; use crate::asset::TokenSymbol; use crate::utils::serde::{ @@ -32,6 +33,7 @@ pub static SCHEMA_TYPE_REGISTRY: LazyLock = LazyLock::new(|| registry.register_felt_type::(); registry.register_felt_type::(); registry.register_felt_type::(); + registry.register_felt_type::(); registry.register_felt_type::(); registry.register_word_type::(); registry.register_word_type::(); @@ -185,6 +187,11 @@ impl SchemaType { .expect("type is well formed") } + /// Returns the schema type for RBAC role symbols. + pub fn role_symbol() -> SchemaType { + SchemaType::new("miden::standards::access::role_symbol").expect("type is well formed") + } + /// Returns a reference to the inner string. pub fn as_str(&self) -> &str { &self.0 @@ -488,6 +495,29 @@ impl FeltType for TokenSymbol { } } +impl FeltType for RoleSymbol { + fn type_name() -> SchemaType { + SchemaType::role_symbol() + } + + fn parse_str(input: &str) -> Result { + let role_symbol = RoleSymbol::new(input).map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Ok(Felt::from(role_symbol)) + } + + fn display_felt(value: Felt) -> Result { + let role_symbol = RoleSymbol::try_from(value).map_err(|err| { + SchemaTypeError::ConversionError(format!( + "invalid role_symbol value `{}`: {err}", + value.as_canonical_u64() + )) + })?; + Ok(role_symbol.to_string()) + } +} + // WORD IMPLS FOR NATIVE TYPES // ================================================================================================ @@ -818,5 +848,14 @@ mod tests { assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&bool_type, "yes").is_err()); assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&bool_type, "2").is_err()); assert!(SCHEMA_TYPE_REGISTRY.validate_felt_value(&bool_type, Felt::new(2)).is_err()); + + let role_symbol_type = SchemaType::role_symbol(); + let role_symbol = + SCHEMA_TYPE_REGISTRY.try_parse_felt(&role_symbol_type, "MINTER_ADMIN").unwrap(); + assert_eq!( + SCHEMA_TYPE_REGISTRY.display_felt(&role_symbol_type, role_symbol), + "MINTER_ADMIN" + ); + assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&role_symbol_type, "minter").is_err()); } } diff --git a/crates/miden-protocol/src/account/mod.rs b/crates/miden-protocol/src/account/mod.rs index 778f36a688..5cd9c905c4 100644 --- a/crates/miden-protocol/src/account/mod.rs +++ b/crates/miden-protocol/src/account/mod.rs @@ -26,6 +26,9 @@ pub use account_id::{ pub mod auth; +mod access; +pub use access::RoleSymbol; + mod builder; pub use builder::AccountBuilder; diff --git a/crates/miden-protocol/src/account/storage/map/key.rs b/crates/miden-protocol/src/account/storage/map/key.rs index dda1a09c36..2a26c8f5d0 100644 --- a/crates/miden-protocol/src/account/storage/map/key.rs +++ b/crates/miden-protocol/src/account/storage/map/key.rs @@ -1,7 +1,7 @@ use alloc::string::String; use miden_crypto::merkle::smt::{LeafIndex, SMT_DEPTH}; -use miden_protocol_macros::WordWrapper; +use miden_crypto_derive::WordWrapper; use crate::utils::serde::{ ByteReader, diff --git a/crates/miden-protocol/src/asset/asset_amount.rs b/crates/miden-protocol/src/asset/asset_amount.rs new file mode 100644 index 0000000000..89276de2dc --- /dev/null +++ b/crates/miden-protocol/src/asset/asset_amount.rs @@ -0,0 +1,219 @@ +use alloc::string::ToString; +use core::fmt; +use core::ops::{Add, Sub}; + +use super::super::errors::AssetError; +use super::super::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; + +// ASSET AMOUNT +// ================================================================================================ + +/// A validated fungible asset amount. +/// +/// Wraps a `u64` that is guaranteed to be at most [`AssetAmount::MAX`]. This type is used in +/// [`FungibleAsset`](super::FungibleAsset) to ensure the amount is always valid. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssetAmount(u64); + +impl AssetAmount { + /// The maximum value an asset amount can represent. + /// + /// Equal to 2^63 - 2^31. This was chosen so that the amount fits as both a positive and + /// negative value in a field element. + pub const MAX: u64 = 2u64.pow(63) - 2u64.pow(31); + + /// Returns a new `AssetAmount` if `amount` does not exceed [`Self::MAX`]. + /// + /// # Errors + /// + /// Returns an error if `amount` is greater than [`Self::MAX`]. + pub fn new(amount: u64) -> Result { + if amount > Self::MAX { + return Err(AssetError::FungibleAssetAmountTooBig(amount)); + } + Ok(Self(amount)) + } +} + +impl Add for AssetAmount { + type Output = Result; + + fn add(self, other: Self) -> Self::Output { + let raw = u64::from(self) + .checked_add(u64::from(other)) + .expect("even MAX + MAX should not overflow u64"); + Self::new(raw) + } +} + +impl Sub for AssetAmount { + type Output = Result; + + fn sub(self, other: Self) -> Self::Output { + let raw = u64::from(self).checked_sub(u64::from(other)).ok_or( + AssetError::FungibleAssetAmountNotSufficient { + minuend: u64::from(self), + subtrahend: u64::from(other), + }, + )?; + Ok(Self(raw)) + } +} + +// CONVERSIONS +// ================================================================================================ + +impl From for AssetAmount { + fn from(value: u8) -> Self { + Self(value as u64) + } +} + +impl From for AssetAmount { + fn from(value: u16) -> Self { + Self(value as u64) + } +} + +impl From for AssetAmount { + fn from(value: u32) -> Self { + Self(value as u64) + } +} + +impl TryFrom for AssetAmount { + type Error = AssetError; + + fn try_from(value: u64) -> Result { + Self::new(value) + } +} + +impl From for u64 { + fn from(amount: AssetAmount) -> Self { + amount.0 + } +} + +// DISPLAY +// ================================================================================================ + +impl fmt::Display for AssetAmount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AssetAmount { + fn write_into(&self, target: &mut W) { + target.write(self.0); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + } +} + +impl Deserializable for AssetAmount { + fn read_from(source: &mut R) -> Result { + let amount: u64 = source.read()?; + Self::new(amount).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_amounts() { + let val: u64 = AssetAmount::new(0).unwrap().into(); + assert_eq!(val, 0); + let val: u64 = AssetAmount::new(1000).unwrap().into(); + assert_eq!(val, 1000); + let val: u64 = AssetAmount::new(AssetAmount::MAX).unwrap().into(); + assert_eq!(val, AssetAmount::MAX); + } + + #[test] + fn exceeds_max() { + assert!(AssetAmount::new(AssetAmount::MAX + 1).is_err()); + assert!(AssetAmount::new(u64::MAX).is_err()); + } + + #[test] + fn from_small_types() { + let a: AssetAmount = 42u8.into(); + let val: u64 = a.into(); + assert_eq!(val, 42); + + let b: AssetAmount = 1000u16.into(); + let val: u64 = b.into(); + assert_eq!(val, 1000); + + let c: AssetAmount = 100_000u32.into(); + let val: u64 = c.into(); + assert_eq!(val, 100_000); + } + + #[test] + fn try_from_u64() { + assert!(AssetAmount::try_from(0u64).is_ok()); + assert!(AssetAmount::try_from(AssetAmount::MAX).is_ok()); + assert!(AssetAmount::try_from(AssetAmount::MAX + 1).is_err()); + } + + #[test] + fn display() { + assert_eq!(AssetAmount::new(12345).unwrap().to_string(), "12345"); + } + + #[test] + fn into_u64() { + let amount = AssetAmount::new(500).unwrap(); + let raw: u64 = amount.into(); + assert_eq!(raw, 500); + } + + #[test] + fn add_amounts() { + let a = AssetAmount::new(100).unwrap(); + let b = AssetAmount::new(200).unwrap(); + let val: u64 = (a + b).unwrap().into(); + assert_eq!(val, 300); + } + + #[test] + fn add_overflow() { + let max = AssetAmount::new(AssetAmount::MAX).unwrap(); + let one = AssetAmount::new(1).unwrap(); + assert!((max + one).is_err()); + } + + #[test] + fn sub_amounts() { + let a = AssetAmount::new(300).unwrap(); + let b = AssetAmount::new(100).unwrap(); + let val: u64 = (a - b).unwrap().into(); + assert_eq!(val, 200); + } + + #[test] + fn sub_underflow() { + let a = AssetAmount::new(50).unwrap(); + let b = AssetAmount::new(100).unwrap(); + assert!((a - b).is_err()); + } +} diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index 58b5754663..7ba16ae136 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -2,7 +2,7 @@ use alloc::string::ToString; use core::fmt; use super::vault::AssetVaultKey; -use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word}; +use super::{AccountType, Asset, AssetAmount, AssetCallbackFlag, AssetError, Word}; use crate::Felt; use crate::account::AccountId; use crate::asset::AssetId; @@ -26,7 +26,7 @@ use crate::utils::serde::{ #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct FungibleAsset { faucet_id: AccountId, - amount: u64, + amount: AssetAmount, callbacks: AssetCallbackFlag, } @@ -37,7 +37,7 @@ impl FungibleAsset { /// /// This number was chosen so that it can be represented as a positive and negative number in a /// field element. See `account_delta.masm` for more details on how this number was chosen. - pub const MAX_AMOUNT: u64 = 2u64.pow(63) - 2u64.pow(31); + pub const MAX_AMOUNT: u64 = AssetAmount::MAX; /// The serialized size of a [`FungibleAsset`] in bytes. /// @@ -61,9 +61,7 @@ impl FungibleAsset { return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id)); } - if amount > Self::MAX_AMOUNT { - return Err(AssetError::FungibleAssetAmountTooBig(amount)); - } + let amount = AssetAmount::new(amount)?; Ok(Self { faucet_id, @@ -127,7 +125,7 @@ impl FungibleAsset { /// Returns the amount of this asset. pub fn amount(&self) -> u64 { - self.amount + self.amount.into() } /// Returns true if this and the other asset were issued from the same faucet. @@ -154,7 +152,7 @@ impl FungibleAsset { /// Returns the asset's value encoded to a [`Word`]. pub fn to_value_word(&self) -> Word { Word::new([ - Felt::try_from(self.amount) + Felt::try_from(u64::from(self.amount)) .expect("fungible asset should only allow amounts that fit into a felt"), Felt::ZERO, Felt::ZERO, @@ -180,13 +178,7 @@ impl FungibleAsset { }); } - let amount = self - .amount - .checked_add(other.amount) - .expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64"); - if amount > Self::MAX_AMOUNT { - return Err(AssetError::FungibleAssetAmountTooBig(amount)); - } + let amount = (self.amount + other.amount)?; Ok(Self { faucet_id: self.faucet_id, @@ -210,12 +202,7 @@ impl FungibleAsset { }); } - let amount = self.amount.checked_sub(other.amount).ok_or( - AssetError::FungibleAssetAmountNotSufficient { - minuend: self.amount, - subtrahend: other.amount, - }, - )?; + let amount = (self.amount - other.amount)?; Ok(FungibleAsset { faucet_id: self.faucet_id, @@ -246,13 +233,13 @@ impl Serializable for FungibleAsset { // All assets should serialize their faucet ID at the first position to allow them to be // distinguishable during deserialization. target.write(self.faucet_id); - target.write(self.amount); + target.write(u64::from(self.amount)); target.write(self.callbacks); } fn get_size_hint(&self) -> usize { self.faucet_id.get_size_hint() - + self.amount.get_size_hint() + + u64::from(self.amount).get_size_hint() + self.callbacks.get_size_hint() } } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 4bdec21c38..3f6cde41f1 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -10,6 +10,9 @@ use super::utils::serde::{ use super::{Felt, Word}; use crate::account::AccountId; +mod asset_amount; +pub use asset_amount::AssetAmount; + mod fungible; pub use fungible::FungibleAsset; diff --git a/crates/miden-protocol/src/asset/token_symbol.rs b/crates/miden-protocol/src/asset/token_symbol.rs index 7189d6805b..6f63421dd0 100644 --- a/crates/miden-protocol/src/asset/token_symbol.rs +++ b/crates/miden-protocol/src/asset/token_symbol.rs @@ -1,24 +1,24 @@ use alloc::fmt; -use alloc::string::String; use super::{Felt, TokenSymbolError}; +use crate::utils::ShortCapitalString; /// Represents a token symbol (e.g. "POL", "ETH"). /// /// Token Symbols can consist of up to 12 capital Latin characters, e.g. "C", "ETH", "MIDEN". /// -/// The symbol is stored as a [`String`] and can be converted to a [`Felt`] encoding via -/// [`as_element()`](Self::as_element). +/// The label is stored internally as a validated short uppercase string and can be converted to a +/// [`Felt`] encoding via [`as_element()`](Self::as_element). #[derive(Clone, Debug, PartialEq, Eq)] -pub struct TokenSymbol(String); +pub struct TokenSymbol(ShortCapitalString); impl TokenSymbol { + /// Alphabet used for token symbols (`A`–`Z`). + pub const ALPHABET: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + /// Maximum allowed length of the token string. pub const MAX_SYMBOL_LENGTH: usize = 12; - /// The length of the set of characters that can be used in a token's name. - pub const ALPHABET_LENGTH: u64 = 26; - /// The minimum integer value of an encoded [`TokenSymbol`]. /// /// This value encodes the "A" token symbol. @@ -47,19 +47,7 @@ impl TokenSymbol { /// - The length of the provided string is less than 1 or greater than 12. /// - The provided token string contains characters that are not uppercase ASCII. pub fn new(symbol: &str) -> Result { - let len = symbol.len(); - - if len == 0 || len > Self::MAX_SYMBOL_LENGTH { - return Err(TokenSymbolError::InvalidLength(len)); - } - - for byte in symbol.as_bytes() { - if !byte.is_ascii_uppercase() { - return Err(TokenSymbolError::InvalidCharacter); - } - } - - Ok(Self(String::from(symbol))) + ShortCapitalString::from_ascii_uppercase(symbol).map(Self).map_err(Into::into) } /// Returns the [`Felt`] encoding of this token symbol. @@ -75,29 +63,13 @@ impl TokenSymbol { /// from the index of the currently processing character, e.g., `A = 65 - 65 = 0`, /// `B = 66 - 65 = 1`, `...` , `Z = 90 - 65 = 25`. pub fn as_element(&self) -> Felt { - let bytes = self.0.as_bytes(); - let len = bytes.len(); - - let mut encoded_value: u64 = 0; - let mut idx = 0; - - while idx < len { - let digit = (bytes[idx] - b'A') as u64; - encoded_value = encoded_value * Self::ALPHABET_LENGTH + digit; - idx += 1; - } - - // add token length to the encoded value to be able to decode the exact number of - // characters - encoded_value = encoded_value * Self::ALPHABET_LENGTH + len as u64; - - Felt::new(encoded_value) + self.0.as_element(Self::ALPHABET).expect("TokenSymbol alphabet is always valid") } } impl fmt::Display for TokenSymbol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) + self.0.fmt(f) } } @@ -140,38 +112,14 @@ impl TryFrom for TokenSymbol { type Error = TokenSymbolError; fn try_from(felt: Felt) -> Result { - let encoded_value = felt.as_canonical_u64(); - if encoded_value < Self::MIN_ENCODED_VALUE { - return Err(TokenSymbolError::ValueTooSmall(encoded_value)); - } - if encoded_value > Self::MAX_ENCODED_VALUE { - return Err(TokenSymbolError::ValueTooLarge(encoded_value)); - } - - let mut decoded_string = String::new(); - let mut remaining_value = encoded_value; - - // get the token symbol length - let token_len = (remaining_value % Self::ALPHABET_LENGTH) as usize; - if token_len == 0 || token_len > Self::MAX_SYMBOL_LENGTH { - return Err(TokenSymbolError::InvalidLength(token_len)); - } - remaining_value /= Self::ALPHABET_LENGTH; - - for _ in 0..token_len { - let digit = (remaining_value % Self::ALPHABET_LENGTH) as u8; - let char = (digit + b'A') as char; - decoded_string.insert(0, char); - remaining_value /= Self::ALPHABET_LENGTH; - } - - // return an error if some data still remains after specified number of characters have - // been decoded. - if remaining_value != 0 { - return Err(TokenSymbolError::DataNotFullyDecoded); - } - - Ok(TokenSymbol(decoded_string)) + ShortCapitalString::try_from_encoded_felt( + felt, + Self::ALPHABET, + Self::MIN_ENCODED_VALUE, + Self::MAX_ENCODED_VALUE, + ) + .map(Self) + .map_err(Into::into) } } diff --git a/crates/miden-protocol/src/batch/batch_id.rs b/crates/miden-protocol/src/batch/batch_id.rs index b84769cbc8..39cfe42255 100644 --- a/crates/miden-protocol/src/batch/batch_id.rs +++ b/crates/miden-protocol/src/batch/batch_id.rs @@ -1,7 +1,7 @@ use alloc::string::String; use alloc::vec::Vec; -use miden_protocol_macros::WordWrapper; +use miden_crypto_derive::WordWrapper; use crate::account::AccountId; use crate::transaction::{ProvenTransaction, TransactionId}; diff --git a/crates/miden-protocol/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs index eb8aae5495..32a0bf9d18 100644 --- a/crates/miden-protocol/src/batch/proven_batch.rs +++ b/crates/miden-protocol/src/batch/proven_batch.rs @@ -36,13 +36,16 @@ impl ProvenBatch { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`ProvenBatch`] from the provided parts. + /// Creates a new [`ProvenBatch`] from the provided parts without checking any constraints + /// except the ones listed in the errors section below. + /// + /// This should essentially never be called by users. /// /// # Errors /// /// Returns an error if the batch expiration block number is not greater than the reference /// block number. - pub fn new( + pub fn new_unchecked( id: BatchId, reference_block_commitment: Word, reference_block_num: BlockNumber, @@ -177,7 +180,7 @@ impl Deserializable for ProvenBatch { let batch_expiration_block_num = BlockNumber::read_from(source)?; let transactions = OrderedTransactionHeaders::read_from(source)?; - Self::new( + Self::new_unchecked( id, reference_block_commitment, reference_block_num, diff --git a/crates/miden-protocol/src/block/block_number.rs b/crates/miden-protocol/src/block/block_number.rs index aec6613a48..8a001c5104 100644 --- a/crates/miden-protocol/src/block/block_number.rs +++ b/crates/miden-protocol/src/block/block_number.rs @@ -72,6 +72,12 @@ impl BlockNumber { pub fn checked_sub(&self, rhs: u32) -> Option { self.0.checked_sub(rhs).map(Self) } + + /// Saturating integer subtraction. Computes `self - rhs`, saturating at + /// [`BlockNumber::GENESIS`] instead of underflowing. + pub fn saturating_sub(&self, rhs: u32) -> Self { + Self(self.0.saturating_sub(rhs)) + } } impl Add for BlockNumber { diff --git a/crates/miden-protocol/src/constants.rs b/crates/miden-protocol/src/constants.rs index c10d263a2d..83f85f516e 100644 --- a/crates/miden-protocol/src/constants.rs +++ b/crates/miden-protocol/src/constants.rs @@ -10,7 +10,7 @@ pub const ACCOUNT_UPDATE_MAX_SIZE: u32 = 2u32.pow(18); pub const NOTE_MAX_SIZE: u32 = 2u32.pow(18); /// The maximum number of assets that can be stored in a single note. -pub const MAX_ASSETS_PER_NOTE: usize = 255; +pub const MAX_ASSETS_PER_NOTE: usize = 64; /// The maximum number of storage items that can accompany a single note. /// diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index d4d6169f7e..b13ec8d068 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -12,7 +12,7 @@ use miden_crypto::merkle::smt::{SmtLeafError, SmtProofError}; use miden_crypto::utils::HexParseError; use thiserror::Error; -use super::account::AccountId; +use super::account::{AccountId, RoleSymbol}; use super::asset::{AssetVaultKey, FungibleAsset, NonFungibleAsset, TokenSymbol}; use super::crypto::merkle::MerkleError; use super::note::NoteId; @@ -108,8 +108,6 @@ pub enum ComponentMetadataError { #[derive(Debug, Error)] pub enum AccountError { - #[error("failed to deserialize account code")] - AccountCodeDeserializationError(#[source] DeserializationError), #[error("account code does not contain an auth component")] AccountCodeNoAuthComponent, #[error("account code contains multiple auth components")] @@ -512,6 +510,66 @@ pub enum TokenSymbolError { DataNotFullyDecoded, } +impl From for TokenSymbolError { + fn from(value: ShortCapitalStringError) -> Self { + match value { + ShortCapitalStringError::ValueTooLarge(v) => Self::ValueTooLarge(v), + ShortCapitalStringError::ValueTooSmall(v) => Self::ValueTooSmall(v), + ShortCapitalStringError::InvalidLength(v) => Self::InvalidLength(v), + ShortCapitalStringError::InvalidCharacter => Self::InvalidCharacter, + ShortCapitalStringError::DataNotFullyDecoded => Self::DataNotFullyDecoded, + } + } +} + +// ROLE ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum RoleSymbolError { + #[error("role symbol value {0} cannot exceed {max}", max = RoleSymbol::MAX_ENCODED_VALUE)] + ValueTooLarge(u64), + #[error("role symbol value {0} cannot be less than {min}", min = RoleSymbol::MIN_ENCODED_VALUE)] + ValueTooSmall(u64), + #[error("role symbol should have length between 1 and 12 characters, but {0} was provided")] + InvalidLength(usize), + #[error("role symbol contains a character that is not uppercase ASCII or underscore")] + InvalidCharacter, + #[error("role symbol data left after decoding the specified number of characters")] + DataNotFullyDecoded, +} + +impl From for RoleSymbolError { + fn from(value: ShortCapitalStringError) -> Self { + match value { + ShortCapitalStringError::ValueTooLarge(v) => Self::ValueTooLarge(v), + ShortCapitalStringError::ValueTooSmall(v) => Self::ValueTooSmall(v), + ShortCapitalStringError::InvalidLength(v) => Self::InvalidLength(v), + ShortCapitalStringError::InvalidCharacter => Self::InvalidCharacter, + ShortCapitalStringError::DataNotFullyDecoded => Self::DataNotFullyDecoded, + } + } +} + +// SHORT CAPITAL STRING ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub(crate) enum ShortCapitalStringError { + #[error("short capital string value {0} is too large")] + ValueTooLarge(u64), + #[error("short capital string value {0} is too small")] + ValueTooSmall(u64), + #[error( + "short capital string should have length between 1 and 12 characters, but {0} was provided" + )] + InvalidLength(usize), + #[error("short capital string contains an invalid character")] + InvalidCharacter, + #[error("short capital string data left after decoding the specified number of characters")] + DataNotFullyDecoded, +} + // ASSET VAULT ERROR // ================================================================================================ diff --git a/crates/miden-protocol/src/lib.rs b/crates/miden-protocol/src/lib.rs index 39144b75a2..fa796f40f7 100644 --- a/crates/miden-protocol/src/lib.rs +++ b/crates/miden-protocol/src/lib.rs @@ -60,23 +60,7 @@ pub mod crypto { pub use miden_crypto::{SequentialCommit, dsa, hash, ies, merkle, rand, utils}; } -pub mod utils { - pub use miden_core::utils::*; - pub use miden_crypto::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; - pub use miden_utils_sync as sync; - - pub mod serde { - pub use miden_crypto::utils::{ - BudgetedReader, - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Serializable, - SliceReader, - }; - } -} +pub mod utils; pub mod vm { pub use miden_assembly_syntax::ast::{AttributeSet, QualifiedProcedureName}; diff --git a/crates/miden-protocol/src/note/assets.rs b/crates/miden-protocol/src/note/assets.rs index d8f0ec17f9..5e3309ba67 100644 --- a/crates/miden-protocol/src/note/assets.rs +++ b/crates/miden-protocol/src/note/assets.rs @@ -18,7 +18,7 @@ use crate::{Felt, Hasher, MAX_ASSETS_PER_NOTE, WORD_SIZE, Word}; /// An asset container for a note. /// -/// A note can contain between 0 and 255 assets. No duplicates are allowed, but the order of assets +/// A note can contain between 0 and 64 assets. No duplicates are allowed, but the order of assets /// is unspecified. /// /// All the assets in a note can be reduced to a single commitment which is computed by @@ -44,7 +44,7 @@ impl NoteAssets { /// /// # Errors /// Returns an error if: - /// - The list contains more than 256 assets. + /// - The list contains more than 64 assets. /// - There are duplicate assets in the list. pub fn new(assets: Vec) -> Result { if assets.len() > Self::MAX_NUM_ASSETS { @@ -184,15 +184,34 @@ impl Deserializable for NoteAssets { #[cfg(test)] mod tests { + use alloc::vec; + use alloc::vec::Vec; + + use assert_matches::assert_matches; + use super::NoteAssets; use crate::account::AccountId; use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; + use crate::errors::NoteError; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, }; + /// Helper to create `n` unique non-fungible assets. + fn make_non_fungible_assets(n: usize) -> Vec { + let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(); + (0..n) + .map(|i| { + // Use the index bytes to create unique asset data. + let data = (i as u64).to_le_bytes().to_vec(); + let details = NonFungibleAssetDetails::new(faucet_id, data).unwrap(); + Asset::NonFungible(NonFungibleAsset::new(&details).unwrap()) + }) + .collect() + } + #[test] fn iter_fungible_asset() { let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(); @@ -212,4 +231,22 @@ mod tests { assert_eq!(fungible_assets.next().unwrap(), asset2.unwrap_fungible()); assert_eq!(fungible_assets.next(), None); } + + #[test] + fn note_assets_at_max_succeeds() { + let assets = make_non_fungible_assets(NoteAssets::MAX_NUM_ASSETS); + assert_eq!(assets.len(), NoteAssets::MAX_NUM_ASSETS); + + let note_assets = NoteAssets::new(assets).unwrap(); + assert_eq!(note_assets.num_assets(), NoteAssets::MAX_NUM_ASSETS); + } + + #[test] + fn note_assets_exceeding_max_fails() { + let assets = make_non_fungible_assets(NoteAssets::MAX_NUM_ASSETS + 1); + assert_eq!(assets.len(), NoteAssets::MAX_NUM_ASSETS + 1); + + let result = NoteAssets::new(assets); + assert_matches!(result, Err(NoteError::TooManyAssets(n)) if n == NoteAssets::MAX_NUM_ASSETS + 1); + } } diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 04c36b9c08..535ff99e4d 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -14,6 +14,12 @@ use crate::Hasher; use crate::errors::NoteError; use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme}; +// CONSTANTS +// ================================================================================================ + +/// The number of bits by which the note type is offset in the first felt of the note metadata. +const NOTE_TYPE_SHIFT: u64 = 4; + // NOTE METADATA // ================================================================================================ @@ -34,7 +40,7 @@ use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme}; /// The header word has the following layout: /// /// ```text -/// 0th felt: [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bit)] +/// 0th felt: [sender_id_suffix (56 bits) | reserved (3 bits) | note_type (1 bit) | version (4 bits)] /// 1st felt: [sender_id_prefix (64 bits)] /// 2nd felt: [32 zero bits | note_tag (32 bits)] /// 3rd felt: [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] @@ -44,11 +50,13 @@ use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme}; /// - 1st felt: The lower 8 bits of the account ID suffix are `0` by construction, so that they can /// be overwritten with other data. The suffix' most significant bit must be zero such that the /// entire felt retains its validity even if all of its lower 8 bits are set to `1`. So the note -/// type can be comfortably encoded. +/// type and version can be comfortably encoded. /// - 2nd felt: Is equivalent to the prefix of the account ID so it inherits its validity. /// - 3rd felt: The upper 32 bits are always zero. /// - 4th felt: The upper 30 bits are always zero. /// +/// The version is hardcoded to 0 and is reserved to make it easier to introduce another version. +/// /// The value of the attachment word depends on the /// [`NoteAttachmentKind`](crate::note::NoteAttachmentKind): /// - [`NoteAttachmentKind::None`](crate::note::NoteAttachmentKind::None): Empty word. @@ -73,6 +81,12 @@ pub struct NoteMetadata { } impl NoteMetadata { + /// Version 0 of the note metadata encoding. + /// + /// If we make this public, we may want to instead consider introducing a `NoteMetadataVersion` + /// struct, similar to `AccountIdVersion`. + const VERSION_0: u8 = 0; + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -337,12 +351,12 @@ impl TryFrom for NoteMetadataHeader { // HELPER FUNCTIONS // ================================================================================================ -/// Merges the suffix of an [`AccountId`] and the [`NoteType`] into a single [`Felt`]. +/// Merges the suffix of an [`AccountId`] and note metadata into a single [`Felt`]. /// /// The layout is as follows: /// /// ```text -/// [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bits)] +/// [sender_id_suffix (56 bits) | reserved (3 bits) | note_type (1 bit) | version (4 bits)] /// ``` /// /// The most significant bit of the suffix is guaranteed to be zero, so the felt retains its @@ -353,28 +367,44 @@ fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType let mut merged = sender_id_suffix.as_canonical_u64(); let note_type_byte = note_type as u8; - debug_assert!(note_type_byte < 4, "note type must not contain values >= 4"); - merged |= note_type_byte as u64; + debug_assert!(note_type_byte < 2, "note type must not contain values >= 2"); + // note_type at bit 4, version at bits 0..=3 (hardcoded to NoteMetadata::VERSION_0_NUMBER) + merged |= (note_type_byte as u64) << NOTE_TYPE_SHIFT; + merged |= NoteMetadata::VERSION_0 as u64; // SAFETY: The most significant bit of the suffix is zero by construction so the u64 will be a // valid felt. Felt::try_from(merged).expect("encoded value should be a valid felt") } -/// Unmerges the sender ID suffix and note type. +/// Unmerges the sender ID suffix and note metadata (note type and version). fn unmerge_sender_suffix_and_note_type(element: Felt) -> Result<(Felt, NoteType), NoteError> { - const NOTE_TYPE_MASK: u8 = 0b11; - // Inverts the note type mask. - const SENDER_SUFFIX_MASK: u64 = !(NOTE_TYPE_MASK as u64); + // The mask that clears out the lower 8 bits to recover the sender suffix. + const SENDER_SUFFIX_MASK: u64 = 0xffff_ffff_ffff_ff00; + + let raw = element.as_canonical_u64(); + let version = (raw & 0b1111) as u8; + let note_type_bit = ((raw >> NOTE_TYPE_SHIFT) & 0b1) as u8; + let reserved = ((raw >> 5) & 0b111) as u8; + + if reserved != 0 { + return Err(NoteError::other("reserved bits in note metadata header must be zero")); + } + + if version != NoteMetadata::VERSION_0 { + return Err(NoteError::other(format!( + "unsupported note metadata version {version}, expected {}", + NoteMetadata::VERSION_0 + ))); + } - let note_type_byte = element.as_canonical_u64() as u8 & NOTE_TYPE_MASK; - let note_type = NoteType::try_from(note_type_byte).map_err(|source| { + let note_type = NoteType::try_from(note_type_bit).map_err(|source| { NoteError::other_with_source("failed to decode note type from metadata header", source) })?; // No bits were set so felt should still be valid. - let sender_suffix = Felt::try_from(element.as_canonical_u64() & SENDER_SUFFIX_MASK) - .expect("felt should still be valid"); + let sender_suffix = + Felt::try_from(raw & SENDER_SUFFIX_MASK).expect("felt should still be valid"); Ok((sender_suffix, note_type)) } diff --git a/crates/miden-protocol/src/note/note_id.rs b/crates/miden-protocol/src/note/note_id.rs index 343285a81e..8778f280a6 100644 --- a/crates/miden-protocol/src/note/note_id.rs +++ b/crates/miden-protocol/src/note/note_id.rs @@ -1,7 +1,7 @@ use alloc::string::String; use core::fmt::Display; -use miden_protocol_macros::WordWrapper; +use miden_crypto_derive::WordWrapper; use super::{Felt, Hasher, NoteDetails, Word}; use crate::WordError; diff --git a/crates/miden-protocol/src/note/note_type.rs b/crates/miden-protocol/src/note/note_type.rs index d72b953f86..204746d336 100644 --- a/crates/miden-protocol/src/note/note_type.rs +++ b/crates/miden-protocol/src/note/note_type.rs @@ -14,9 +14,10 @@ use crate::utils::serde::{ // NOTE TYPE // ================================================================================================ -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[repr(u8)] pub enum NoteType { + #[default] /// Notes with this type have only their hash published to the network. Private = Self::PRIVATE, @@ -25,17 +26,21 @@ pub enum NoteType { } impl NoteType { - // Keep these masks in sync with `miden-lib/asm/miden/kernels/tx/tx.masm` - pub const PUBLIC: u8 = 0b01; - pub const PRIVATE: u8 = 0b10; + const PRIVATE: u8 = 0; + const PUBLIC: u8 = 1; + + /// Returns the note type encoded to a 1-bit flag, where private is 0 and public is 1. + pub const fn as_u8(self) -> u8 { + self as u8 + } } // CONVERSIONS FROM NOTE TYPE // ================================================================================================ impl From for Felt { - fn from(id: NoteType) -> Self { - Felt::new(id as u64) + fn from(note_type: NoteType) -> Self { + Felt::from(note_type.as_u8()) } } @@ -54,38 +59,27 @@ impl TryFrom for NoteType { } } -impl TryFrom for NoteType { - type Error = NoteError; - - fn try_from(value: u16) -> Result { - Self::try_from(value as u64) - } -} - -impl TryFrom for NoteType { - type Error = NoteError; - - fn try_from(value: u32) -> Result { - Self::try_from(value as u64) - } -} - -impl TryFrom for NoteType { +impl TryFrom for NoteType { type Error = NoteError; - fn try_from(value: u64) -> Result { - let value: u8 = value - .try_into() - .map_err(|_| NoteError::UnknownNoteType(format!("0b{value:b}").into()))?; - value.try_into() + fn try_from(value: Felt) -> Result { + let byte = value.as_canonical_u64(); + Self::try_from( + u8::try_from(byte) + .map_err(|_| NoteError::UnknownNoteType(format!("0b{byte:b}").into()))?, + ) } } -impl TryFrom for NoteType { - type Error = NoteError; +// STRING CONVERSION +// ================================================================================================ - fn try_from(value: Felt) -> Result { - value.as_canonical_u64().try_into() +impl Display for NoteType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + NoteType::Private => write!(f, "private"), + NoteType::Public => write!(f, "public"), + } } } @@ -132,32 +126,47 @@ impl Deserializable for NoteType { } } -// DISPLAY +// TESTS // ================================================================================================ -impl Display for NoteType { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - NoteType::Private => write!(f, "private"), - NoteType::Public => write!(f, "public"), - } - } -} - -#[test] -fn test_from_str_note_type() { +#[cfg(test)] +mod tests { use assert_matches::assert_matches; + use super::*; use crate::alloc::string::ToString; - for string in ["private", "public"] { - let parsed_note_type = NoteType::from_str(string).unwrap(); - assert_eq!(parsed_note_type.to_string(), string); + #[rstest::rstest] + #[case::private(NoteType::Private)] + #[case::public(NoteType::Public)] + #[test] + fn test_note_type_roundtrip(#[case] note_type: NoteType) -> anyhow::Result<()> { + // String roundtrip + assert_eq!(note_type, note_type.to_string().parse()?); + + // Serialization roundtrip + assert_eq!(note_type, NoteType::read_from_bytes(¬e_type.to_bytes())?); + + // Byte conversion roundtrip + assert_eq!(note_type, NoteType::try_from(note_type.as_u8())?); + + // Felt conversion roundtrip + assert_eq!(note_type, NoteType::try_from(Felt::from(note_type))?); + + Ok(()) } - let public_type_invalid_err = NoteType::from_str("puBlIc").unwrap_err(); - assert_matches!(public_type_invalid_err, NoteError::UnknownNoteType(_)); + #[test] + fn test_from_str_note_type() { + for string in ["private", "public"] { + let parsed_note_type = NoteType::from_str(string).unwrap(); + assert_eq!(parsed_note_type.to_string(), string); + } - let invalid_type = NoteType::from_str("invalid").unwrap_err(); - assert_matches!(invalid_type, NoteError::UnknownNoteType(_)); + let public_type_invalid_err = NoteType::from_str("puBlIc").unwrap_err(); + assert_matches!(public_type_invalid_err, NoteError::UnknownNoteType(_)); + + let invalid_type = NoteType::from_str("invalid").unwrap_err(); + assert_matches!(invalid_type, NoteError::UnknownNoteType(_)); + } } diff --git a/crates/miden-protocol/src/note/nullifier.rs b/crates/miden-protocol/src/note/nullifier.rs index 2f728b4123..3b52e3613e 100644 --- a/crates/miden-protocol/src/note/nullifier.rs +++ b/crates/miden-protocol/src/note/nullifier.rs @@ -3,7 +3,7 @@ use core::fmt::{Debug, Display, Formatter}; use miden_core::WORD_SIZE; use miden_crypto::WordError; -use miden_protocol_macros::WordWrapper; +use miden_crypto_derive::WordWrapper; use super::{ ByteReader, diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 84480e9b5b..27d29f054a 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -264,7 +264,7 @@ impl TransactionAdviceInputs { // CODE_COMMITMENT -> [[ACCOUNT_PROCEDURE_DATA]] let code = account.code(); - self.add_map_entry(code.commitment(), code.as_elements()); + self.add_map_entry(code.commitment(), code.to_elements()); // --- account storage ---------------------------------------------------- diff --git a/crates/miden-protocol/src/transaction/kernel/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs index 95373d8788..500696a47d 100644 --- a/crates/miden-protocol/src/transaction/kernel/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -21,9 +21,9 @@ pub type StorageSlot = u8; // | Kernel data | 1_600 | 140 | 34 procedures in total, 4 elements each | // | Accounts data | 8_192 | 524_288 | 64 accounts max, 8192 elements each | // | Account delta | 532_480 | 263 | | -// | Input notes | 4_194_304 | 3_211_264 | nullifiers data segment (2^16 elements) | -// | | | | + 1024 input notes max, 3072 elements each | -// | Output notes | 16_777_216 | 3_145_728 | 1024 output notes max, 3072 elements each | +// | Input notes | 4_194_304 | 1_114_112 | nullifiers data segment (2^16 elements) | +// | | | | + 1024 input notes max, 1024 elements each | +// | Output notes | 16_777_216 | 1_048_576 | 1024 output notes max, 1024 elements each | // | Link Map Memory | 33_554_432 | 33_554_432 | Enough for 2_097_151 key-value pairs | // Relative layout of one account @@ -344,7 +344,7 @@ pub const NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR: MemoryAddress = // ================================================================================================ /// The size of the memory segment allocated to each note. -pub const NOTE_MEM_SIZE: MemoryAddress = 3072; +pub const NOTE_MEM_SIZE: MemoryAddress = 1024; #[allow(clippy::empty_line_after_outer_attr)] #[rustfmt::skip] @@ -358,11 +358,11 @@ pub const NOTE_MEM_SIZE: MemoryAddress = 3072; // │ NUM │ NOTE 0 │ NOTE 1 │ ... │ NOTE n │ PADDING │ NOTE 0 │ NOTE 1 │ ... │ NOTE n │ // │ NOTES │ NULLIFIER │ NULLIFIER │ │ NULLIFIER │ │ DATA │ DATA │ │ DATA │ // ├──────────┼───────────┼───────────┼─────┼────────────────┼─────────┼──────────┼────────┼───────┼────────┤ -// 4_194_304 4_194_308 4_194_312 4_194_304+4(n+1) 4_259_840 +3072 +6144 +3072n +// 4_194_304 4_194_308 4_194_312 4_194_304+4(n+1) 4_259_840 +1024 +2048 +1024n // // Here `n` represents number of input notes. // -// Each nullifier occupies a single word. A data section for each note consists of exactly 3072 +// Each nullifier occupies a single word. A data section for each note consists of exactly 1024 // elements and is laid out like so: // // ┌──────┬────────┬────────┬─────────┬────────────┬───────────┬──────────┬────────────┬───────┬ @@ -420,12 +420,12 @@ pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 44; // OUTPUT NOTES DATA // ------------------------------------------------------------------------------------------------ // Output notes section contains data of all notes produced by a transaction. The section starts at -// memory offset 16_777_216 with each note data laid out one after another in 512 word increments. +// memory offset 16_777_216 with each note data laid out one after another in 1024 elements chunks. // // ┌─────────────┬─────────────┬───────────────┬─────────────┐ // │ NOTE 0 DATA │ NOTE 1 DATA │ ... │ NOTE n DATA │ // └─────────────┴─────────────┴───────────────┴─────────────┘ -// 16_777_216 +3072 +6144 +3072n +// 16_777_216 +1024 +2048 +1024n // // The total number of output notes for a transaction is stored in the bookkeeping section of the // memory. Data section of each note is laid out like so: diff --git a/crates/miden-protocol/src/transaction/transaction_id.rs b/crates/miden-protocol/src/transaction/transaction_id.rs index ddf14c7532..4313e6c465 100644 --- a/crates/miden-protocol/src/transaction/transaction_id.rs +++ b/crates/miden-protocol/src/transaction/transaction_id.rs @@ -1,7 +1,7 @@ use alloc::string::String; use core::fmt::{Debug, Display}; -use miden_protocol_macros::WordWrapper; +use miden_crypto_derive::WordWrapper; use super::{Felt, Hasher, ProvenTransaction, WORD_SIZE, Word, ZERO}; use crate::asset::{Asset, FungibleAsset}; diff --git a/crates/miden-protocol/src/utils/mod.rs b/crates/miden-protocol/src/utils/mod.rs new file mode 100644 index 0000000000..f5f012c425 --- /dev/null +++ b/crates/miden-protocol/src/utils/mod.rs @@ -0,0 +1,19 @@ +pub use miden_core::utils::*; +pub use miden_crypto::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; +pub use miden_utils_sync as sync; + +pub mod serde { + pub use miden_crypto::utils::{ + BudgetedReader, + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, + SliceReader, + }; +} + +pub mod strings; + +pub(crate) use strings::ShortCapitalString; diff --git a/crates/miden-protocol/src/utils/strings.rs b/crates/miden-protocol/src/utils/strings.rs new file mode 100644 index 0000000000..01f57ab8d5 --- /dev/null +++ b/crates/miden-protocol/src/utils/strings.rs @@ -0,0 +1,228 @@ +use alloc::fmt; +use alloc::string::String; + +use crate::Felt; +use crate::errors::ShortCapitalStringError; + +/// A short string of uppercase ASCII (and optionally underscores) encoded into a [`Felt`] with a +/// configurable alphabet. +/// +/// Use [`Self::from_ascii_uppercase`] or [`Self::from_ascii_uppercase_and_underscore`] to construct +/// a validated value (same rules as [`crate::asset::TokenSymbol`] and +/// [`crate::account::RoleSymbol`]). +/// +/// The text is stored as a [`String`] and can be converted to a [`Felt`] encoding via +/// [`as_element()`](Self::as_element), and decoded back via +/// [`try_from_encoded_felt()`](Self::try_from_encoded_felt). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct ShortCapitalString(String); + +impl ShortCapitalString { + /// Maximum allowed string length. + pub const MAX_LENGTH: usize = 12; + + /// Constructs a value from up to 12 uppercase ASCII Latin letters (`A`–`Z`). + /// + /// # Errors + /// Returns an error if: + /// - The number of characters is less than 1 or greater than 12. + /// - The string contains a character that is not uppercase ASCII. + pub fn from_ascii_uppercase( + string: impl Into, + ) -> Result { + let string = string.into(); + let char_count = string.chars().count(); + if char_count == 0 || char_count > Self::MAX_LENGTH { + return Err(ShortCapitalStringError::InvalidLength(char_count)); + } + for character in string.chars() { + if !character.is_ascii_uppercase() { + return Err(ShortCapitalStringError::InvalidCharacter); + } + } + Ok(Self(string)) + } + + /// Constructs a value from up to 12 characters from `A`–`Z` and `_`. + /// + /// # Errors + /// Returns an error if: + /// - The number of characters is less than 1 or greater than 12. + /// - The string contains a character outside `A`–`Z` and `_`. + pub fn from_ascii_uppercase_and_underscore( + string: impl Into, + ) -> Result { + let string = string.into(); + let char_count = string.chars().count(); + if char_count == 0 || char_count > Self::MAX_LENGTH { + return Err(ShortCapitalStringError::InvalidLength(char_count)); + } + for character in string.chars() { + if !character.is_ascii_uppercase() && character != '_' { + return Err(ShortCapitalStringError::InvalidCharacter); + } + } + Ok(Self(string)) + } + + /// Returns the [`Felt`] encoding of this string. + /// + /// The alphabet used in the encoding process is provided by the `alphabet` argument. + /// + /// **Contract:** `alphabet` must contain **ASCII characters only**. Then each character + /// occupies one UTF-8 byte, so the radix is [`str::len`] and matches the number of Unicode + /// scalars. + /// + /// The encoding is performed by multiplying the intermediate encoded value by the length of + /// the used alphabet and adding the relative index of each character. At the end of the + /// encoding process, the character length of the initial string is added to the encoded value. + /// + /// # Errors + /// Returns an error if: + /// - The string contains a character that is not part of the provided alphabet. + pub fn as_element(&self, alphabet: &str) -> Result { + debug_assert!( + alphabet.is_ascii(), + "ShortCapitalString::as_element: alphabet must be ASCII-only" + ); + let alphabet_len = alphabet.len() as u64; + let mut encoded_value: u64 = 0; + + for character in self.0.chars() { + let digit = alphabet + .chars() + .position(|c| c == character) + .map(|pos| pos as u64) + .ok_or(ShortCapitalStringError::InvalidCharacter)?; + + encoded_value = encoded_value * alphabet_len + digit; + } + + // Append the original length so decoding is unambiguous. + let char_len = self.0.chars().count() as u64; + encoded_value = encoded_value * alphabet_len + char_len; + Ok(Felt::new(encoded_value)) + } + + /// Decodes an encoded [`Felt`] value into a [`ShortCapitalString`]. + /// + /// `encoded_string` is the field element that carries the short-string encoding (as produced by + /// [`as_element`](Self::as_element)). + /// + /// The alphabet used in the decoding process is provided by the `alphabet` argument. The same + /// **ASCII-only** contract as [`as_element`](Self::as_element) applies; radix is [`str::len`]. + /// + /// The decoding is performed by reading the encoded length from the least-significant digit, + /// then repeatedly taking modulus by alphabet length to recover each character index. + /// + /// # Errors + /// Returns an error if: + /// - The encoded value is outside of the provided `min_encoded_value..=max_encoded_value`. + /// - The decoded length is not between 1 and 12. + /// - Decoding leaves non-zero trailing data. + pub fn try_from_encoded_felt( + encoded_string: Felt, + alphabet: &str, + min_encoded_value: u64, + max_encoded_value: u64, + ) -> Result { + let encoded_value = encoded_string.as_canonical_u64(); + if encoded_value < min_encoded_value { + return Err(ShortCapitalStringError::ValueTooSmall(encoded_value)); + } + if encoded_value > max_encoded_value { + return Err(ShortCapitalStringError::ValueTooLarge(encoded_value)); + } + + debug_assert!( + alphabet.is_ascii(), + "ShortCapitalString::try_from_encoded_felt: alphabet must be ASCII-only" + ); + let alphabet_len = alphabet.len() as u64; + let mut remaining_value = encoded_value; + let string_len = (remaining_value % alphabet_len) as usize; + if string_len == 0 || string_len > Self::MAX_LENGTH { + return Err(ShortCapitalStringError::InvalidLength(string_len)); + } + remaining_value /= alphabet_len; + + let mut decoded = String::with_capacity(string_len); + for _ in 0..string_len { + let digit = (remaining_value % alphabet_len) as usize; + let character = + alphabet.chars().nth(digit).ok_or(ShortCapitalStringError::InvalidCharacter)?; + decoded.insert(0, character); + remaining_value /= alphabet_len; + } + + if remaining_value != 0 { + return Err(ShortCapitalStringError::DataNotFullyDecoded); + } + + Ok(Self(decoded)) + } +} + +impl fmt::Display for ShortCapitalString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::{String, ToString}; + + use assert_matches::assert_matches; + + use super::{Felt, ShortCapitalString}; + use crate::errors::ShortCapitalStringError; + + #[test] + fn short_capital_string_encode_decode_roundtrip() { + let short_string = ShortCapitalString::from_ascii_uppercase("MIDEN").unwrap(); + let encoded = short_string.as_element("ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap(); + let decoded = ShortCapitalString::try_from_encoded_felt( + encoded, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + 1, + 2481152873203736562, + ) + .unwrap(); + assert_eq!(decoded.to_string(), "MIDEN"); + + let name = String::from("MIDEN"); + let from_name = ShortCapitalString::from_ascii_uppercase(name).unwrap(); + assert_eq!(from_name.to_string(), "MIDEN"); + } + + #[test] + fn short_capital_string_rejects_invalid_values() { + assert_matches!( + ShortCapitalString::from_ascii_uppercase("").unwrap_err(), + ShortCapitalStringError::InvalidLength(0) + ); + assert_matches!( + ShortCapitalString::from_ascii_uppercase("ABCDEFGHIJKLM").unwrap_err(), + ShortCapitalStringError::InvalidLength(13) + ); + assert_matches!( + ShortCapitalString::from_ascii_uppercase("A_B").unwrap_err(), + ShortCapitalStringError::InvalidCharacter + ); + + assert_matches!( + ShortCapitalString::from_ascii_uppercase_and_underscore("MINTER-ADMIN").unwrap_err(), + ShortCapitalStringError::InvalidCharacter + ); + + let err = ShortCapitalString::try_from_encoded_felt( + Felt::ZERO, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + 1, + 2481152873203736562, + ) + .unwrap_err(); + assert_matches!(err, ShortCapitalStringError::ValueTooSmall(0)); + } +} diff --git a/crates/miden-standards/asm/account_components/auth/multisig_psm.masm b/crates/miden-standards/asm/account_components/auth/guarded_multisig.masm similarity index 63% rename from crates/miden-standards/asm/account_components/auth/multisig_psm.masm rename to crates/miden-standards/asm/account_components/auth/guarded_multisig.masm index 591ba376ab..29ffded0e5 100644 --- a/crates/miden-standards/asm/account_components/auth/multisig_psm.masm +++ b/crates/miden-standards/asm/account_components/auth/guarded_multisig.masm @@ -1,9 +1,9 @@ -# The MASM code of the Multi-Signature Authentication Component with Private State Manager. +# The MASM code of the Multi-Signature Authentication component integrated with a state guardian. # -# See the `AuthMultisigPsm` Rust type's documentation for more details. +# See the `AuthGuardedMultisig` Rust type's documentation for more details. use miden::standards::auth::multisig -use miden::standards::auth::psm +use miden::standards::auth::guardian pub use multisig::update_signers_and_threshold pub use multisig::get_threshold_and_num_approvers @@ -11,9 +11,9 @@ pub use multisig::set_procedure_threshold pub use multisig::get_signer_at pub use multisig::is_signer -pub use psm::update_psm_public_key +pub use guardian::update_guardian_public_key -#! Authenticate a transaction with multi-signature support and optional PSM verification. +#! Authenticate a transaction with multi-signature support and optional guardian verification. #! #! Inputs: #! Operand stack: [SALT] @@ -22,14 +22,14 @@ pub use psm::update_psm_public_key #! #! Invocation: call @auth_script -pub proc auth_tx_multisig_psm(salt: word) +pub proc auth_tx_guarded_multisig(salt: word) exec.multisig::auth_tx # => [TX_SUMMARY_COMMITMENT] dupw # => [TX_SUMMARY_COMMITMENT, TX_SUMMARY_COMMITMENT] - exec.psm::verify_signature + exec.guardian::verify_signature # => [TX_SUMMARY_COMMITMENT] exec.multisig::assert_new_tx diff --git a/crates/miden-standards/asm/standards/attachments/network_account_target.masm b/crates/miden-standards/asm/standards/attachments/network_account_target.masm index a5ee0bde40..d0a2e58eb5 100644 --- a/crates/miden-standards/asm/standards/attachments/network_account_target.masm +++ b/crates/miden-standards/asm/standards/attachments/network_account_target.masm @@ -108,7 +108,7 @@ pub proc active_account_matches_target_account swapw # => [METADATA_HEADER, NOTE_ATTACHMENT] - exec.note::extract_attachment_info_from_metadata + exec.note::metadata_into_attachment_info # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] swap diff --git a/crates/miden-standards/asm/standards/auth/guardian.masm b/crates/miden-standards/asm/standards/auth/guardian.masm new file mode 100644 index 0000000000..73f2322b73 --- /dev/null +++ b/crates/miden-standards/asm/standards/auth/guardian.masm @@ -0,0 +1,162 @@ +# State Guardian account component. +# This component is composed into account auth flows especially for multisig and adds +# an extra signature check by a dedicated guardian signer. +# +# A state guardian can help coordinate state availability for private accounts. + +use miden::protocol::auth::AUTH_UNAUTHORIZED_EVENT +use miden::protocol::native_account +use miden::standards::auth::tx_policy +use miden::standards::auth::signature + +# IMPORTANT SECURITY NOTES +# -------------------------------------------------------------------------------- +# - By default, exactly one valid guardian signature is required. +# - If `update_guardian_public_key` is the only non-auth account procedure called in the current +# transaction, `verify_signature` skips the guardian signature check so key rotation can proceed +# without the old guardian signer. +# - `update_guardian_public_key` rotates the guardian public key and corresponding +# scheme id using the fixed map key `GUARDIAN_MAP_KEY`. + + +# CONSTANTS +# ================================================================================================= + +# Storage Slots +# +# This authentication component uses named storage slots. +# - GUARDIAN_PUBLIC_KEYS_SLOT (map): +# GUARDIAN_MAP_KEY => GUARDIAN_PUBLIC_KEY +# where: GUARDIAN_MAP_KEY = [0, 0, 0, 0] +# +# - GUARDIAN_SCHEME_ID_SLOT (map): +# GUARDIAN_MAP_KEY => [scheme_id, 0, 0, 0] +# where: GUARDIAN_MAP_KEY = [0, 0, 0, 0] + +# The slot in this component's storage layout where the guardian public key map is stored. +# Map entries: [GUARDIAN_MAP_KEY] => [GUARDIAN_PUBLIC_KEY] +const GUARDIAN_PUBLIC_KEYS_SLOT = word("miden::standards::auth::guardian::pub_key") + +# The slot in this component's storage layout where the scheme id for the corresponding guardian +# public key map is stored. +# Map entries: [GUARDIAN_MAP_KEY] => [scheme_id, 0, 0, 0] +const GUARDIAN_SCHEME_ID_SLOT = word("miden::standards::auth::guardian::scheme") + +# Single-entry storage map key where guardian signer data is stored. +const GUARDIAN_MAP_KEY = [0, 0, 0, 0] + +# ERRORS +# ------------------------------------------------------------------------------------------------- +const ERR_INVALID_GUARDIAN_SIGNATURE = "invalid guardian signature" + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Updates the guardian public key. +#! +#! Inputs: [new_guardian_scheme_id, NEW_GUARDIAN_PUBLIC_KEY] +#! Outputs: [] +#! +#! Notes: +#! - This procedure only updates the guardian public key and corresponding scheme id. +#! - `verify_signature` skips guardian verification only when this is the only non-auth account +#! procedure called in the transaction. +#! +#! Invocation: call +@locals(1) +pub proc update_guardian_public_key(new_guardian_scheme_id: felt, new_guardian_public_key: word) + # Validate supported signature scheme before committing it to storage. + dup exec.signature::assert_supported_scheme + # => [new_guardian_scheme_id, NEW_GUARDIAN_PUBLIC_KEY] + + loc_store.0 + # => [NEW_GUARDIAN_PUBLIC_KEY] + + push.GUARDIAN_MAP_KEY + # => [GUARDIAN_MAP_KEY, NEW_GUARDIAN_PUBLIC_KEY] + + push.GUARDIAN_PUBLIC_KEYS_SLOT[0..2] + # => [guardian_pubkeys_slot_prefix, guardian_pubkeys_slot_suffix, + # GUARDIAN_MAP_KEY, NEW_GUARDIAN_PUBLIC_KEY] + + exec.native_account::set_map_item + # => [OLD_GUARDIAN_PUBLIC_KEY] + + dropw + # => [] + + # Store new scheme id as [scheme_id, 0, 0, 0] in the single-entry map. + loc_load.0 + # => [scheme_id] + + push.0.0.0 movup.3 + # => [NEW_GUARDIAN_SCHEME_ID_WORD] + + push.GUARDIAN_MAP_KEY + # => [GUARDIAN_MAP_KEY, NEW_GUARDIAN_SCHEME_ID_WORD] + + push.GUARDIAN_SCHEME_ID_SLOT[0..2] + # => [guardian_scheme_slot_prefix, guardian_scheme_slot_suffix, + # GUARDIAN_MAP_KEY, NEW_GUARDIAN_SCHEME_ID_WORD] + + exec.native_account::set_map_item + # => [OLD_GUARDIAN_SCHEME_ID_WORD] + + dropw + # => [] +end + +#! Conditionally verifies a guardian signature. +#! +#! Inputs: [MSG] +#! Outputs: [] +#! +#! Panics if: +#! - `update_guardian_public_key` is called together with another non-auth account procedure. +#! - `update_guardian_public_key` was not called and a valid guardian signature is missing or +#! invalid. +#! +#! Invocation: exec +pub proc verify_signature(msg: word) + procref.update_guardian_public_key + # => [UPDATE_GUARDIAN_PUBLIC_KEY_ROOT, MSG] + + exec.native_account::was_procedure_called + # => [was_update_guardian_public_key_called, MSG] + + if.true + exec.tx_policy::assert_only_one_non_auth_procedure_called + # => [MSG] + + exec.tx_policy::assert_no_input_or_output_notes + # => [MSG] + + dropw + # => [] + else + push.1 + # => [1, MSG] + + push.GUARDIAN_PUBLIC_KEYS_SLOT[0..2] + # => [guardian_pubkeys_slot_prefix, guardian_pubkeys_slot_suffix, 1, MSG] + + push.GUARDIAN_SCHEME_ID_SLOT[0..2] + # => [guardian_scheme_slot_prefix, guardian_scheme_slot_suffix, + # guardian_pubkeys_slot_prefix, guardian_pubkeys_slot_suffix, 1, MSG] + + exec.signature::verify_signatures + # => [num_verified_signatures, MSG] + + neq.1 + # => [is_not_exactly_one, MSG] + + if.true + emit.AUTH_UNAUTHORIZED_EVENT + push.0 assert.err=ERR_INVALID_GUARDIAN_SIGNATURE + end + # => [MSG] + + dropw + # => [] + end +end diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index ed20ff2325..b8d43b4dc5 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -727,6 +727,6 @@ pub proc auth_tx(salt: word) end # TX_SUMMARY_COMMITMENT is returned so wrappers can run optional checks - # (e.g. PSM) before replay-protection finalization. + # (e.g. guardian verification) before replay-protection finalization. # => [TX_SUMMARY_COMMITMENT] end diff --git a/crates/miden-standards/asm/standards/auth/psm.masm b/crates/miden-standards/asm/standards/auth/psm.masm deleted file mode 100644 index d778cafb14..0000000000 --- a/crates/miden-standards/asm/standards/auth/psm.masm +++ /dev/null @@ -1,158 +0,0 @@ -# Private State Manager (PSM) account component. -# This component is composed into account auth flows especially for multisig and adds -# an extra signature check by a dedicated private state manager signer. -# -# Private State Manager (PSM) is a cloud backup and synchronization layer for Miden private accounts -# See: https://github.com/OpenZeppelin/private-state-manager - -use miden::protocol::auth::AUTH_UNAUTHORIZED_EVENT -use miden::protocol::native_account -use miden::standards::auth::tx_policy -use miden::standards::auth::signature - -# IMPORTANT SECURITY NOTES -# -------------------------------------------------------------------------------- -# - By default, exactly one valid PSM signature is required. -# - If `update_psm_public_key` is the only non-auth account procedure called in the current -# transaction, `verify_signature` skips the PSM signature check so key rotation can proceed -# without the old PSM signer. -# - `update_psm_public_key` rotates the PSM public key and corresponding scheme id using the fixed -# map key `PSM_MAP_KEY`. - - -# CONSTANTS -# ================================================================================================= - -# Storage Slots -# -# This authentication component uses named storage slots. -# - PSM_PUBLIC_KEYS_SLOT (map): -# PSM_MAP_KEY => PSM_PUBLIC_KEY -# where: PSM_MAP_KEY = [0, 0, 0, 0] -# -# - PSM_SCHEME_ID_SLOT (map): -# PSM_MAP_KEY => [scheme_id, 0, 0, 0] -# where: PSM_MAP_KEY = [0, 0, 0, 0] - -# The slot in this component's storage layout where the PSM public key map is stored. -# Map entries: [PSM_MAP_KEY] => [PSM_PUBLIC_KEY] -const PSM_PUBLIC_KEYS_SLOT = word("miden::standards::auth::psm::pub_key") - -# The slot in this component's storage layout where the scheme id for the corresponding PSM public key map is stored. -# Map entries: [PSM_MAP_KEY] => [scheme_id, 0, 0, 0] -const PSM_SCHEME_ID_SLOT = word("miden::standards::auth::psm::scheme") - -# Single-entry storage map key where private state manager signer data is stored. -const PSM_MAP_KEY = [0, 0, 0, 0] - -# ERRORS -# ------------------------------------------------------------------------------------------------- -const ERR_INVALID_PSM_SIGNATURE = "invalid private state manager signature" - -# PUBLIC INTERFACE -# ================================================================================================ - -#! Updates the private state manager public key. -#! -#! Inputs: [new_psm_scheme_id, NEW_PSM_PUBLIC_KEY] -#! Outputs: [] -#! -#! Notes: -#! - This procedure only updates the PSM public key and corresponding scheme id. -#! - `verify_signature` skips PSM verification only when this is the only non-auth account -#! procedure called in the transaction. -#! -#! Invocation: call -@locals(1) -pub proc update_psm_public_key(new_psm_scheme_id: felt, new_psm_public_key: word) - # Validate supported signature scheme before committing it to storage. - dup exec.signature::assert_supported_scheme - # => [new_psm_scheme_id, NEW_PSM_PUBLIC_KEY] - - loc_store.0 - # => [NEW_PSM_PUBLIC_KEY] - - push.PSM_MAP_KEY - # => [PSM_MAP_KEY, NEW_PSM_PUBLIC_KEY] - - push.PSM_PUBLIC_KEYS_SLOT[0..2] - # => [psm_pubkeys_slot_prefix, psm_pubkeys_slot_suffix, PSM_MAP_KEY, NEW_PSM_PUBLIC_KEY] - - exec.native_account::set_map_item - # => [OLD_PSM_PUBLIC_KEY] - - dropw - # => [] - - # Store new scheme id as [scheme_id, 0, 0, 0] in the single-entry map. - loc_load.0 - # => [scheme_id] - - push.0.0.0 movup.3 - # => [NEW_PSM_SCHEME_ID_WORD] - - push.PSM_MAP_KEY - # => [PSM_MAP_KEY, NEW_PSM_SCHEME_ID_WORD] - - push.PSM_SCHEME_ID_SLOT[0..2] - # => [psm_scheme_slot_prefix, psm_scheme_slot_suffix, PSM_MAP_KEY, NEW_PSM_SCHEME_ID_WORD] - - exec.native_account::set_map_item - # => [OLD_PSM_SCHEME_ID_WORD] - - dropw - # => [] -end - -#! Conditionally verifies a private state manager signature. -#! -#! Inputs: [MSG] -#! Outputs: [] -#! -#! Panics if: -#! - `update_psm_public_key` is called together with another non-auth account procedure. -#! - `update_psm_public_key` was not called and a valid PSM signature is missing or invalid. -#! -#! Invocation: exec -pub proc verify_signature(msg: word) - procref.update_psm_public_key - # => [UPDATE_PSM_PUBLIC_KEY_ROOT, MSG] - - exec.native_account::was_procedure_called - # => [was_update_psm_public_key_called, MSG] - - if.true - exec.tx_policy::assert_only_one_non_auth_procedure_called - # => [MSG] - - exec.tx_policy::assert_no_input_or_output_notes - # => [MSG] - - dropw - # => [] - else - push.1 - # => [1, MSG] - - push.PSM_PUBLIC_KEYS_SLOT[0..2] - # => [psm_pubkeys_slot_prefix, psm_pubkeys_slot_suffix, 1, MSG] - - push.PSM_SCHEME_ID_SLOT[0..2] - # => [psm_scheme_slot_prefix, psm_scheme_slot_suffix, psm_pubkeys_slot_prefix, psm_pubkeys_slot_suffix, 1, MSG] - - exec.signature::verify_signatures - # => [num_verified_signatures, MSG] - - neq.1 - # => [is_not_exactly_one, MSG] - - if.true - emit.AUTH_UNAUTHORIZED_EVENT - push.0 assert.err=ERR_INVALID_PSM_SIGNATURE - end - # => [MSG] - - dropw - # => [] - end -end diff --git a/crates/miden-standards/asm/standards/auth/tx_policy.masm b/crates/miden-standards/asm/standards/auth/tx_policy.masm index 76da300070..8ad6264183 100644 --- a/crates/miden-standards/asm/standards/auth/tx_policy.masm +++ b/crates/miden-standards/asm/standards/auth/tx_policy.masm @@ -3,6 +3,8 @@ use miden::protocol::native_account use miden::protocol::tx const ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE = "procedure must be called alone" +const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES = "transaction must not include input notes" +const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_OUTPUT_NOTES = "transaction must not include output notes" const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES = "transaction must not include input or output notes" #! Asserts that exactly one non-auth account procedure was called in the current transaction. @@ -59,6 +61,34 @@ pub proc assert_only_one_non_auth_procedure_called # => [] end +#! Asserts that the current transaction does not consume input notes. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +pub proc assert_no_input_notes + exec.tx::get_num_input_notes + # => [num_input_notes] + + assertz.err=ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES + # => [] +end + +#! Asserts that the current transaction does not create output notes. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +pub proc assert_no_output_notes + exec.tx::get_num_output_notes + # => [num_output_notes] + + assertz.err=ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_OUTPUT_NOTES + # => [] +end + #! Asserts that the current transaction does not consume input notes or create output notes. #! #! Inputs: [] diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 57eaf2b416..ab2ed1c2e5 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -10,7 +10,6 @@ use miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT # ================================================================================================= const ASSET_PTR=0 -const PRIVATE_NOTE=2 # ERRORS # ================================================================================================= diff --git a/crates/miden-standards/asm/standards/notes/mint.masm b/crates/miden-standards/asm/standards/notes/mint.masm index ca287daf48..7780a67052 100644 --- a/crates/miden-standards/asm/standards/notes/mint.masm +++ b/crates/miden-standards/asm/standards/notes/mint.masm @@ -9,8 +9,8 @@ use miden::standards::faucets::network_fungible->network_faucet const MINT_NOTE_NUM_STORAGE_ITEMS_PRIVATE=12 const MINT_NOTE_MIN_NUM_STORAGE_ITEMS_PUBLIC=16 -const OUTPUT_NOTE_TYPE_PUBLIC=1 -const OUTPUT_NOTE_TYPE_PRIVATE=2 +use miden::protocol::note::NOTE_TYPE_PUBLIC +use miden::protocol::note::NOTE_TYPE_PRIVATE # Memory Addresses of MINT note storage # The attachment is at the same memory address for both private and public storage. @@ -98,7 +98,7 @@ pub proc main # => [RECIPIENT, pad(12)] # push note_type, and load tag and amount - push.OUTPUT_NOTE_TYPE_PUBLIC + push.NOTE_TYPE_PUBLIC mem_load.0 mem_load.1 # => [amount, tag, note_type, RECIPIENT, pad(12)] else @@ -114,7 +114,7 @@ pub proc main # => [RECIPIENT, pad(12)] # push note_type, and load tag and amount - push.OUTPUT_NOTE_TYPE_PRIVATE + push.NOTE_TYPE_PRIVATE mem_load.0 mem_load.1 # => [amount, tag, note_type, RECIPIENT, pad(12)] end diff --git a/crates/miden-standards/asm/standards/wallets/basic.masm b/crates/miden-standards/asm/standards/wallets/basic.masm index 57f72ec600..298d163cb4 100644 --- a/crates/miden-standards/asm/standards/wallets/basic.masm +++ b/crates/miden-standards/asm/standards/wallets/basic.masm @@ -6,7 +6,6 @@ use miden::protocol::active_note # CONSTANTS # ================================================================================================= -const PUBLIC_NOTE=1 #! Adds the provided asset to the active account. #! @@ -70,7 +69,7 @@ end #! #! Inputs: [] #! Outputs: [] -@locals(2048) +@locals(512) pub proc add_assets_to_account # write assets to local memory starting at offset 0 # we have allocated ASSET_SIZE * MAX_ASSETS_PER_NOTE number of locals so all assets should fit diff --git a/crates/miden-standards/build.rs b/crates/miden-standards/build.rs index aacb343c89..5409016190 100644 --- a/crates/miden-standards/build.rs +++ b/crates/miden-standards/build.rs @@ -32,15 +32,12 @@ fn main() -> Result<()> { // re-build when the MASM code changes println!("cargo::rerun-if-changed={ASM_DIR}/"); - // Copies the MASM code to the build directory let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let build_dir = env::var("OUT_DIR").unwrap(); - let src = Path::new(&crate_dir).join(ASM_DIR); - let dst = Path::new(&build_dir).to_path_buf(); - shared::copy_directory(src, &dst, ASM_DIR)?; - // set source directory to {OUT_DIR}/asm - let source_dir = dst.join(ASM_DIR); + // Read MASM sources directly from the crate's asm/ directory. + // No copy to OUT_DIR is needed because this crate doesn't mutate the source tree. + let source_dir = Path::new(&crate_dir).join(ASM_DIR); // set target directory to {OUT_DIR}/assets let target_dir = Path::new(&build_dir).join(ASSETS_DIR); @@ -201,58 +198,10 @@ mod shared { use fs_err as fs; use miden_assembly::Report; - use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; + use miden_assembly::diagnostics::{IntoDiagnostic, Result}; use regex::Regex; use walkdir::WalkDir; - /// Recursively copies `src` into `dst`. - /// - /// This function will overwrite the existing files if re-executed. - pub fn copy_directory, R: AsRef>( - src: T, - dst: R, - asm_dir: &str, - ) -> Result<()> { - let mut prefix = src.as_ref().canonicalize().unwrap(); - // keep all the files inside the `asm` folder - prefix.pop(); - - let target_dir = dst.as_ref().join(asm_dir); - if target_dir.exists() { - // Clear existing asm files that were copied earlier which may no longer exist. - fs::remove_dir_all(&target_dir) - .into_diagnostic() - .wrap_err("failed to remove ASM directory")?; - } - - // Recreate the directory structure. - fs::create_dir_all(&target_dir) - .into_diagnostic() - .wrap_err("failed to create ASM directory")?; - - let dst = dst.as_ref(); - let mut todo = vec![src.as_ref().to_path_buf()]; - - while let Some(goal) = todo.pop() { - for entry in fs::read_dir(goal).unwrap() { - let path = entry.unwrap().path(); - if path.is_dir() { - let src_dir = path.canonicalize().unwrap(); - let dst_dir = dst.join(src_dir.strip_prefix(&prefix).unwrap()); - if !dst_dir.exists() { - fs::create_dir_all(&dst_dir).unwrap(); - } - todo.push(src_dir); - } else { - let dst_file = dst.join(path.strip_prefix(&prefix).unwrap()); - fs::copy(&path, dst_file).unwrap(); - } - } - } - - Ok(()) - } - /// Returns a vector with paths to all MASM files in the specified directory and its /// subdirectories. /// diff --git a/crates/miden-standards/src/account/auth/multisig_psm.rs b/crates/miden-standards/src/account/auth/guarded_multisig.rs similarity index 61% rename from crates/miden-standards/src/account/auth/multisig_psm.rs rename to crates/miden-standards/src/account/auth/guarded_multisig.rs index 1e9ecc34b2..e988542972 100644 --- a/crates/miden-standards/src/account/auth/multisig_psm.rs +++ b/crates/miden-standards/src/account/auth/guarded_multisig.rs @@ -20,39 +20,39 @@ use miden_protocol::errors::AccountError; use miden_protocol::utils::sync::LazyLock; use super::multisig::{AuthMultisig, AuthMultisigConfig}; -use crate::account::components::multisig_psm_library; +use crate::account::components::guarded_multisig_library; // CONSTANTS // ================================================================================================ -static PSM_PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::auth::psm::pub_key") +static GUARDIAN_PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::guardian::pub_key") .expect("storage slot name should be valid") }); -static PSM_SCHEME_ID_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::auth::psm::scheme") +static GUARDIAN_SCHEME_ID_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::guardian::scheme") .expect("storage slot name should be valid") }); // MULTISIG AUTHENTICATION COMPONENT // ================================================================================================ -/// Configuration for [`AuthMultisigPsm`] component. +/// Configuration for [`AuthGuardedMultisig`] component. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuthMultisigPsmConfig { +pub struct AuthGuardedMultisigConfig { multisig: AuthMultisigConfig, - psm_config: PsmConfig, + guardian_config: GuardianConfig, } -/// Public configuration for the private state manager signer. +/// Public configuration for the guardian signer. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PsmConfig { +pub struct GuardianConfig { pub_key: PublicKeyCommitment, auth_scheme: AuthScheme, } -impl PsmConfig { +impl GuardianConfig { pub fn new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self { Self { pub_key, auth_scheme } } @@ -66,18 +66,18 @@ impl PsmConfig { } fn public_key_slot() -> &'static StorageSlotName { - &PSM_PUBKEY_SLOT_NAME + &GUARDIAN_PUBKEY_SLOT_NAME } fn scheme_id_slot() -> &'static StorageSlotName { - &PSM_SCHEME_ID_SLOT_NAME + &GUARDIAN_SCHEME_ID_SLOT_NAME } fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) { ( Self::public_key_slot().clone(), StorageSlotSchema::map( - "Private state manager public keys", + "Guardian public keys", SchemaType::u32(), SchemaType::pub_key(), ), @@ -88,7 +88,7 @@ impl PsmConfig { ( Self::scheme_id_slot().clone(), StorageSlotSchema::map( - "Private state manager scheme IDs", + "Guardian scheme IDs", SchemaType::u32(), SchemaType::auth_scheme(), ), @@ -98,22 +98,22 @@ impl PsmConfig { fn into_component_parts(self) -> (Vec, Vec<(StorageSlotName, StorageSlotSchema)>) { let mut storage_slots = Vec::with_capacity(2); - // Private state manager public key slot (map: [0, 0, 0, 0] -> pubkey) - let psm_public_key_entries = + // Guardian public key slot (map: [0, 0, 0, 0] -> pubkey) + let guardian_public_key_entries = [(StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from(self.pub_key))]; storage_slots.push(StorageSlot::with_map( Self::public_key_slot().clone(), - StorageMap::with_entries(psm_public_key_entries).unwrap(), + StorageMap::with_entries(guardian_public_key_entries).unwrap(), )); - // Private state manager scheme IDs slot (map: [0, 0, 0, 0] -> [scheme_id, 0, 0, 0]) - let psm_scheme_id_entries = [( + // Guardian scheme IDs slot (map: [0, 0, 0, 0] -> [scheme_id, 0, 0, 0]) + let guardian_scheme_id_entries = [( StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from([self.auth_scheme as u32, 0, 0, 0]), )]; storage_slots.push(StorageSlot::with_map( Self::scheme_id_slot().clone(), - StorageMap::with_entries(psm_scheme_id_entries).unwrap(), + StorageMap::with_entries(guardian_scheme_id_entries).unwrap(), )); let slot_metadata = vec![Self::public_key_slot_schema(), Self::auth_scheme_slot_schema()]; @@ -122,28 +122,28 @@ impl PsmConfig { } } -impl AuthMultisigPsmConfig { - /// Creates a new configuration with the given approvers, default threshold and PSM signer. +impl AuthGuardedMultisigConfig { + /// Creates a new configuration with the given approvers, default threshold and guardian signer. /// /// The `default_threshold` must be at least 1 and at most the number of approvers. - /// The private state manager public key must be different from all approver public keys. + /// The guardian public key must be different from all approver public keys. pub fn new( approvers: Vec<(PublicKeyCommitment, AuthScheme)>, default_threshold: u32, - psm_config: PsmConfig, + guardian_config: GuardianConfig, ) -> Result { let multisig = AuthMultisigConfig::new(approvers, default_threshold)?; if multisig .approvers() .iter() - .any(|(approver, _)| *approver == psm_config.pub_key()) + .any(|(approver, _)| *approver == guardian_config.pub_key()) { return Err(AccountError::other( - "private state manager public key must be different from approvers", + "guardian public key must be different from approvers", )); } - Ok(Self { multisig, psm_config }) + Ok(Self { multisig, guardian_config }) } /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and @@ -168,41 +168,40 @@ impl AuthMultisigPsmConfig { self.multisig.proc_thresholds() } - pub fn psm_config(&self) -> PsmConfig { - self.psm_config + pub fn guardian_config(&self) -> GuardianConfig { + self.guardian_config } - fn into_parts(self) -> (AuthMultisigConfig, PsmConfig) { - (self.multisig, self.psm_config) + fn into_parts(self) -> (AuthMultisigConfig, GuardianConfig) { + (self.multisig, self.guardian_config) } } -/// An [`AccountComponent`] implementing a multisig authentication with a private state manager. +/// An [`AccountComponent`] implementing multisig authentication integrated with a state guardian. /// /// It enforces a threshold of approver signatures for every transaction, with optional -/// per-procedure threshold overrides. With Private State Manager (PSM) is configured, -/// multisig authorization is combined with PSM authorization, so operations require both -/// multisig approval and a valid PSM signature. This substantially mitigates low-threshold -/// state-withholding scenarios since the PSM is expected to forward state updates to other -/// approvers. +/// per-procedure threshold overrides. When a guardian is configured, multisig authorization is +/// combined with guardian authorization, so operations require both multisig approval and a valid +/// guardian signature. This substantially mitigates low-threshold state-withholding scenarios +/// since the guardian is expected to forward state updates to other approvers. /// /// This component supports all account types. #[derive(Debug)] -pub struct AuthMultisigPsm { +pub struct AuthGuardedMultisig { multisig: AuthMultisig, - psm_config: PsmConfig, + guardian_config: GuardianConfig, } -impl AuthMultisigPsm { +impl AuthGuardedMultisig { /// The name of the component. - pub const NAME: &'static str = "miden::standards::components::auth::multisig_psm"; + pub const NAME: &'static str = "miden::standards::components::auth::guarded_multisig"; - /// Creates a new [`AuthMultisigPsm`] component from the provided configuration. - pub fn new(config: AuthMultisigPsmConfig) -> Result { - let (multisig_config, psm_config) = config.into_parts(); + /// Creates a new [`AuthGuardedMultisig`] component from the provided configuration. + pub fn new(config: AuthGuardedMultisigConfig) -> Result { + let (multisig_config, guardian_config) = config.into_parts(); Ok(Self { multisig: AuthMultisig::new(multisig_config)?, - psm_config, + guardian_config, }) } @@ -231,14 +230,14 @@ impl AuthMultisigPsm { AuthMultisig::procedure_thresholds_slot() } - /// Returns the [`StorageSlotName`] where the private state manager public key is stored. - pub fn psm_public_key_slot() -> &'static StorageSlotName { - PsmConfig::public_key_slot() + /// Returns the [`StorageSlotName`] where the guardian public key is stored. + pub fn guardian_public_key_slot() -> &'static StorageSlotName { + GuardianConfig::public_key_slot() } - /// Returns the [`StorageSlotName`] where the private state manager scheme IDs are stored. - pub fn psm_scheme_id_slot() -> &'static StorageSlotName { - PsmConfig::scheme_id_slot() + /// Returns the [`StorageSlotName`] where the guardian scheme IDs are stored. + pub fn guardian_scheme_id_slot() -> &'static StorageSlotName { + GuardianConfig::scheme_id_slot() } /// Returns the storage slot schema for the threshold configuration slot. @@ -266,14 +265,14 @@ impl AuthMultisigPsm { AuthMultisig::procedure_thresholds_slot_schema() } - /// Returns the storage slot schema for the private state manager public key slot. - pub fn psm_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - PsmConfig::public_key_slot_schema() + /// Returns the storage slot schema for the guardian public key slot. + pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + GuardianConfig::public_key_slot_schema() } - /// Returns the storage slot schema for the private state manager scheme IDs slot. - pub fn psm_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - PsmConfig::auth_scheme_slot_schema() + /// Returns the storage slot schema for the guardian scheme IDs slot. + pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + GuardianConfig::auth_scheme_slot_schema() } /// Returns the [`AccountComponentMetadata`] for this component. @@ -284,49 +283,50 @@ impl AuthMultisigPsm { Self::approver_auth_scheme_slot_schema(), Self::executed_transactions_slot_schema(), Self::procedure_thresholds_slot_schema(), - Self::psm_public_key_slot_schema(), - Self::psm_auth_scheme_slot_schema(), + Self::guardian_public_key_slot_schema(), + Self::guardian_auth_scheme_slot_schema(), ]) .expect("storage schema should be valid"); AccountComponentMetadata::new(Self::NAME, AccountType::all()) .with_description( - "Multisig authentication component with private state manager \ - using hybrid signature schemes", + "Guarded multisig authentication component integrated \ + with a state guardian using hybrid signature schemes", ) .with_storage_schema(storage_schema) } } -impl From for AccountComponent { - fn from(multisig: AuthMultisigPsm) -> Self { - let AuthMultisigPsm { multisig, psm_config } = multisig; +impl From for AccountComponent { + fn from(multisig: AuthGuardedMultisig) -> Self { + let AuthGuardedMultisig { multisig, guardian_config } = multisig; let multisig_component = AccountComponent::from(multisig); - let (psm_slots, psm_slot_metadata) = psm_config.into_component_parts(); + let (guardian_slots, guardian_slot_metadata) = guardian_config.into_component_parts(); let mut storage_slots = multisig_component.storage_slots().to_vec(); - storage_slots.extend(psm_slots); + storage_slots.extend(guardian_slots); let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component .storage_schema() .iter() .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone())) .collect(); - slot_schemas.extend(psm_slot_metadata); + slot_schemas.extend(guardian_slot_metadata); let storage_schema = StorageSchema::new(slot_schemas).expect("storage schema should be valid"); let metadata = AccountComponentMetadata::new( - AuthMultisigPsm::NAME, + AuthGuardedMultisig::NAME, multisig_component.supported_types().clone(), ) .with_description(multisig_component.metadata().description()) .with_version(multisig_component.metadata().version().clone()) .with_storage_schema(storage_schema); - AccountComponent::new(multisig_psm_library(), storage_slots, metadata).expect( - "Multisig auth component should satisfy the requirements of a valid account component", + AccountComponent::new(guarded_multisig_library(), storage_slots, metadata).expect( + "Guarded multisig auth component should satisfy the requirements of a valid account \ + component", ) } } @@ -345,14 +345,14 @@ mod tests { use super::*; use crate::account::wallets::BasicWallet; - /// Test multisig component setup with various configurations + /// Test guarded multisig component setup with various configurations. #[test] - fn test_multisig_component_setup() { + fn test_guarded_multisig_component_setup() { // Create test secret keys let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2(); let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2(); let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2(); - let psm_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak(); // Create approvers list for multisig config let approvers = vec![ @@ -363,18 +363,21 @@ mod tests { let threshold = 2u32; - // Create multisig component - let multisig_component = AuthMultisigPsm::new( - AuthMultisigPsmConfig::new( + // Create guarded multisig component. + let multisig_component = AuthGuardedMultisig::new( + AuthGuardedMultisigConfig::new( approvers.clone(), threshold, - PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + GuardianConfig::new( + guardian_key.public_key().to_commitment(), + guardian_key.auth_scheme(), + ), ) .expect("invalid multisig config"), ) - .expect("multisig component creation failed"); + .expect("guarded multisig component creation failed"); - // Build account with multisig component + // Build account with guarded multisig component. let account = AccountBuilder::new([0; 32]) .with_auth_component(multisig_component) .with_component(BasicWallet) @@ -384,7 +387,7 @@ mod tests { // Verify config slot: [threshold, num_approvers, 0, 0] let config_slot = account .storage() - .get_item(AuthMultisigPsm::threshold_config_slot()) + .get_item(AuthGuardedMultisig::threshold_config_slot()) .expect("config storage slot access failed"); assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); @@ -393,7 +396,7 @@ mod tests { let stored_pub_key = account .storage() .get_map_item( - AuthMultisigPsm::approver_public_keys_slot(), + AuthGuardedMultisig::approver_public_keys_slot(), Word::from([i as u32, 0, 0, 0]), ) .expect("approver public key storage map access failed"); @@ -405,44 +408,53 @@ mod tests { let stored_scheme_id = account .storage() .get_map_item( - AuthMultisigPsm::approver_scheme_ids_slot(), + AuthGuardedMultisig::approver_scheme_ids_slot(), Word::from([i as u32, 0, 0, 0]), ) .expect("approver scheme ID storage map access failed"); assert_eq!(stored_scheme_id, Word::from([*expected_auth_scheme as u32, 0, 0, 0])); } - // Verify private state manager signer is configured. - let psm_public_key = account + // Verify guardian signer is configured. + let guardian_public_key = account .storage() - .get_map_item(AuthMultisigPsm::psm_public_key_slot(), Word::from([0u32, 0, 0, 0])) - .expect("private state manager public key storage map access failed"); - assert_eq!(psm_public_key, Word::from(psm_key.public_key().to_commitment())); + .get_map_item( + AuthGuardedMultisig::guardian_public_key_slot(), + Word::from([0u32, 0, 0, 0]), + ) + .expect("guardian public key storage map access failed"); + assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment())); - let psm_scheme_id = account + let guardian_scheme_id = account .storage() - .get_map_item(AuthMultisigPsm::psm_scheme_id_slot(), Word::from([0u32, 0, 0, 0])) - .expect("private state manager scheme ID storage map access failed"); - assert_eq!(psm_scheme_id, Word::from([psm_key.auth_scheme() as u32, 0, 0, 0])); + .get_map_item( + AuthGuardedMultisig::guardian_scheme_id_slot(), + Word::from([0u32, 0, 0, 0]), + ) + .expect("guardian scheme ID storage map access failed"); + assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0])); } - /// Test multisig component with minimum threshold (1 of 1) + /// Test guarded multisig component with minimum threshold (1 of 1). #[test] - fn test_multisig_component_minimum_threshold() { + fn test_guarded_multisig_component_minimum_threshold() { let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(); - let psm_key = AuthSecretKey::new_falcon512_poseidon2(); + let guardian_key = AuthSecretKey::new_falcon512_poseidon2(); let approvers = vec![(pub_key, AuthScheme::EcdsaK256Keccak)]; let threshold = 1u32; - let multisig_component = AuthMultisigPsm::new( - AuthMultisigPsmConfig::new( + let multisig_component = AuthGuardedMultisig::new( + AuthGuardedMultisigConfig::new( approvers.clone(), threshold, - PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + GuardianConfig::new( + guardian_key.public_key().to_commitment(), + guardian_key.auth_scheme(), + ), ) .expect("invalid multisig config"), ) - .expect("multisig component creation failed"); + .expect("guarded multisig component creation failed"); let account = AccountBuilder::new([0; 32]) .with_auth_component(multisig_component) @@ -453,44 +465,53 @@ mod tests { // Verify storage layout let config_slot = account .storage() - .get_item(AuthMultisigPsm::threshold_config_slot()) + .get_item(AuthGuardedMultisig::threshold_config_slot()) .expect("config storage slot access failed"); assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); let stored_pub_key = account .storage() - .get_map_item(AuthMultisigPsm::approver_public_keys_slot(), Word::from([0u32, 0, 0, 0])) + .get_map_item( + AuthGuardedMultisig::approver_public_keys_slot(), + Word::from([0u32, 0, 0, 0]), + ) .expect("approver pub keys storage map access failed"); assert_eq!(stored_pub_key, Word::from(pub_key)); let stored_scheme_id = account .storage() - .get_map_item(AuthMultisigPsm::approver_scheme_ids_slot(), Word::from([0u32, 0, 0, 0])) + .get_map_item( + AuthGuardedMultisig::approver_scheme_ids_slot(), + Word::from([0u32, 0, 0, 0]), + ) .expect("approver scheme IDs storage map access failed"); assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])); } - /// Test multisig component setup with a private state manager. + /// Test guarded multisig component setup with a guardian. #[test] - fn test_multisig_component_with_psm() { + fn test_guarded_multisig_component_with_guardian() { let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2(); let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2(); - let psm_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak(); let approvers = vec![ (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), ]; - let multisig_component = AuthMultisigPsm::new( - AuthMultisigPsmConfig::new( + let multisig_component = AuthGuardedMultisig::new( + AuthGuardedMultisigConfig::new( approvers, 2, - PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + GuardianConfig::new( + guardian_key.public_key().to_commitment(), + guardian_key.auth_scheme(), + ), ) .expect("invalid multisig config"), ) - .expect("multisig component creation failed"); + .expect("guarded multisig component creation failed"); let account = AccountBuilder::new([0; 32]) .with_auth_component(multisig_component) @@ -498,31 +519,40 @@ mod tests { .build() .expect("account building failed"); - let psm_public_key = account + let guardian_public_key = account .storage() - .get_map_item(AuthMultisigPsm::psm_public_key_slot(), Word::from([0u32, 0, 0, 0])) - .expect("private state manager public key storage map access failed"); - assert_eq!(psm_public_key, Word::from(psm_key.public_key().to_commitment())); + .get_map_item( + AuthGuardedMultisig::guardian_public_key_slot(), + Word::from([0u32, 0, 0, 0]), + ) + .expect("guardian public key storage map access failed"); + assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment())); - let psm_scheme_id = account + let guardian_scheme_id = account .storage() - .get_map_item(AuthMultisigPsm::psm_scheme_id_slot(), Word::from([0u32, 0, 0, 0])) - .expect("private state manager scheme ID storage map access failed"); - assert_eq!(psm_scheme_id, Word::from([psm_key.auth_scheme() as u32, 0, 0, 0])); + .get_map_item( + AuthGuardedMultisig::guardian_scheme_id_slot(), + Word::from([0u32, 0, 0, 0]), + ) + .expect("guardian scheme ID storage map access failed"); + assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0])); } - /// Test multisig component error cases + /// Test guarded multisig component error cases. #[test] - fn test_multisig_component_error_cases() { + fn test_guarded_multisig_component_error_cases() { let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(); - let psm_key = AuthSecretKey::new_falcon512_poseidon2(); + let guardian_key = AuthSecretKey::new_falcon512_poseidon2(); let approvers = vec![(pub_key, AuthScheme::EcdsaK256Keccak)]; // Test threshold > number of approvers (should fail) - let result = AuthMultisigPsmConfig::new( + let result = AuthGuardedMultisigConfig::new( approvers, 2, - PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + GuardianConfig::new( + guardian_key.public_key().to_commitment(), + guardian_key.auth_scheme(), + ), ); assert!( @@ -533,13 +563,13 @@ mod tests { ); } - /// Test multisig component with duplicate approvers (should fail) + /// Test guarded multisig component with duplicate approvers (should fail). #[test] - fn test_multisig_component_duplicate_approvers() { + fn test_guarded_multisig_component_duplicate_approvers() { // Create secret keys for approvers let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak(); let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak(); - let psm_key = AuthSecretKey::new_falcon512_poseidon2(); + let guardian_key = AuthSecretKey::new_falcon512_poseidon2(); // Create approvers list with duplicate public keys let approvers = vec![ @@ -548,10 +578,13 @@ mod tests { (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), ]; - let result = AuthMultisigPsmConfig::new( + let result = AuthGuardedMultisigConfig::new( approvers, 2, - PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + GuardianConfig::new( + guardian_key.public_key().to_commitment(), + guardian_key.auth_scheme(), + ), ); assert!( result @@ -561,9 +594,9 @@ mod tests { ); } - /// Test multisig component rejects a private state manager key which is already an approver. + /// Test guarded multisig component rejects a guardian key which is already an approver. #[test] - fn test_multisig_component_psm_not_approver() { + fn test_guarded_multisig_component_guardian_not_approver() { let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak(); let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak(); @@ -572,17 +605,17 @@ mod tests { (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), ]; - let result = AuthMultisigPsmConfig::new( + let result = AuthGuardedMultisigConfig::new( approvers, 2, - PsmConfig::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + GuardianConfig::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), ); assert!( result .unwrap_err() .to_string() - .contains("private state manager public key must be different from approvers") + .contains("guardian public key must be different from approvers") ); } } diff --git a/crates/miden-standards/src/account/auth/mod.rs b/crates/miden-standards/src/account/auth/mod.rs index e999fab153..4a526bb77d 100644 --- a/crates/miden-standards/src/account/auth/mod.rs +++ b/crates/miden-standards/src/account/auth/mod.rs @@ -10,5 +10,5 @@ pub use singlesig_acl::{AuthSingleSigAcl, AuthSingleSigAclConfig}; mod multisig; pub use multisig::{AuthMultisig, AuthMultisigConfig}; -mod multisig_psm; -pub use multisig_psm::{AuthMultisigPsm, AuthMultisigPsmConfig, PsmConfig}; +mod guarded_multisig; +pub use guarded_multisig::{AuthGuardedMultisig, AuthGuardedMultisigConfig, GuardianConfig}; diff --git a/crates/miden-standards/src/account/auth/multisig.rs b/crates/miden-standards/src/account/auth/multisig/mod.rs similarity index 98% rename from crates/miden-standards/src/account/auth/multisig.rs rename to crates/miden-standards/src/account/auth/multisig/mod.rs index 196bb3de0c..31af0ca1e3 100644 --- a/crates/miden-standards/src/account/auth/multisig.rs +++ b/crates/miden-standards/src/account/auth/multisig/mod.rs @@ -1,3 +1,6 @@ +#[allow(dead_code)] +pub(crate) mod procedure_policies; + use alloc::collections::BTreeSet; use alloc::vec::Vec; @@ -130,9 +133,9 @@ impl AuthMultisigConfig { /// /// It enforces a threshold of approver signatures for every transaction, with optional /// per-procedure threshold overrides. Non-uniform thresholds (especially a threshold of one) -/// should be used with caution for private multisig accounts, without Private State Manager (PSM), -/// a single approver may advance state and withhold updates from other approvers, effectively -/// locking them out. +/// should be used with caution for private multisig accounts; without a guardian, a single +/// approver may advance state and withhold updates from other approvers, effectively locking +/// them out. /// /// This component supports all account types. #[derive(Debug)] diff --git a/crates/miden-standards/src/account/auth/multisig/procedure_policies.rs b/crates/miden-standards/src/account/auth/multisig/procedure_policies.rs new file mode 100644 index 0000000000..c83245cc0a --- /dev/null +++ b/crates/miden-standards/src/account/auth/multisig/procedure_policies.rs @@ -0,0 +1,246 @@ +use miden_protocol::Word; +use miden_protocol::errors::AccountError; + +/// Defines which execution modes a procedure policy supports and the corresponding threshold +/// values for each mode. +/// +/// A procedure can require the immediate threshold, the delayed threshold, or support both. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcedurePolicyExecutionMode { + ImmediateOnly { + immediate_threshold: u32, + }, + DelayOnly { + delay_threshold: u32, + }, + ImmediateOrDelay { + immediate_threshold: u32, + delay_threshold: u32, + }, +} + +/// Note Restrictions on whether transactions that call a procedure may consume input notes +/// or create output notes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(u8)] +pub enum ProcedurePolicyNoteRestriction { + #[default] + None = 0, + NoInputNotes = 1, + NoOutputNotes = 2, + NoInputOrOutputNotes = 3, +} + +/// Defines a per-procedure multisig policy. +/// +/// A procedure policy can override the default multisig requirements for a specific procedure. +/// It specifies: +/// - an execution mode, which determines whether the procedure can be executed immediately, after a +/// delay, or both +/// - note restrictions, which limit whether a transaction invoking the procedure may consume input +/// notes or create output notes +/// +/// Execution modes: +/// - Immediate execution: the action is authorized and executed within the current transaction. +/// - Delayed execution: the action is proposed first, and can only be executed after a required +/// time delay has elapsed. +/// +/// Thresholds: +/// - Immediate threshold: the number of signatures required to authorize immediate execution. +/// - Delayed threshold: the number of signatures required to authorize a delayed action. +/// +/// The thresholds for immediate and delayed execution may differ. +/// +/// The policy is encoded into the procedure-policy storage word as: +/// `[immediate_threshold, delayed_threshold, note_restrictions, 0]`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProcedurePolicy { + execution_mode: ProcedurePolicyExecutionMode, + note_restrictions: ProcedurePolicyNoteRestriction, +} + +impl ProcedurePolicy { + /// Creates an explicit procedure policy from an execution mode and note restriction pair. + /// + /// Common multisig cases should generally prefer the `with_*_threshold...` helpers and + /// configure note restrictions afterwards via [`ProcedurePolicy::with_note_restriction`]. + pub fn new( + execution_mode: ProcedurePolicyExecutionMode, + note_restrictions: ProcedurePolicyNoteRestriction, + ) -> Result { + Self::validate_execution_mode(execution_mode)?; + Ok(Self { execution_mode, note_restrictions }) + } + + pub fn with_immediate_threshold(immediate_threshold: u32) -> Result { + Self::new( + ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold }, + ProcedurePolicyNoteRestriction::None, + ) + } + + pub fn with_delay_threshold(delay_threshold: u32) -> Result { + Self::new( + ProcedurePolicyExecutionMode::DelayOnly { delay_threshold }, + ProcedurePolicyNoteRestriction::None, + ) + } + + pub fn with_immediate_and_delay_thresholds( + immediate_threshold: u32, + delay_threshold: u32, + ) -> Result { + Self::new( + ProcedurePolicyExecutionMode::ImmediateOrDelay { immediate_threshold, delay_threshold }, + ProcedurePolicyNoteRestriction::None, + ) + } + + pub const fn with_note_restriction( + mut self, + note_restrictions: ProcedurePolicyNoteRestriction, + ) -> Self { + self.note_restrictions = note_restrictions; + self + } + + pub const fn execution_mode(&self) -> ProcedurePolicyExecutionMode { + self.execution_mode + } + + pub const fn note_restrictions(&self) -> ProcedurePolicyNoteRestriction { + self.note_restrictions + } + + pub const fn immediate_threshold(&self) -> Option { + match self.execution_mode { + ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold } => { + Some(immediate_threshold) + }, + ProcedurePolicyExecutionMode::DelayOnly { .. } => None, + ProcedurePolicyExecutionMode::ImmediateOrDelay { immediate_threshold, .. } => { + Some(immediate_threshold) + }, + } + } + + pub const fn delay_threshold(&self) -> Option { + match self.execution_mode { + ProcedurePolicyExecutionMode::ImmediateOnly { .. } => None, + ProcedurePolicyExecutionMode::DelayOnly { delay_threshold } => Some(delay_threshold), + ProcedurePolicyExecutionMode::ImmediateOrDelay { delay_threshold, .. } => { + Some(delay_threshold) + }, + } + } + + fn validate_execution_mode( + execution_mode: ProcedurePolicyExecutionMode, + ) -> Result<(), AccountError> { + match execution_mode { + ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold } => { + if immediate_threshold == 0 { + return Err(AccountError::other( + "procedure policy immediate threshold must be at least 1", + )); + } + }, + ProcedurePolicyExecutionMode::DelayOnly { delay_threshold } => { + if delay_threshold == 0 { + return Err(AccountError::other( + "procedure policy delay threshold must be at least 1", + )); + } + }, + ProcedurePolicyExecutionMode::ImmediateOrDelay { + immediate_threshold, + delay_threshold, + } => { + if immediate_threshold == 0 || delay_threshold == 0 { + return Err(AccountError::other( + "immediate and delayed thresholds must both be at least 1", + )); + } + // Delayed execution is the lower-quorum option while immediate execution is + // higher-quorum path. If the delay threshold were greater than the + // immediate threshold, the "fast" path would be easier to satisfy + // than the delayed path, which contradicts that model. + if delay_threshold > immediate_threshold { + return Err(AccountError::other( + "delay threshold cannot exceed immediate threshold", + )); + } + }, + } + + Ok(()) + } + + pub fn to_word(self) -> Word { + let immediate_threshold = self.immediate_threshold().unwrap_or(0); + let delay_threshold = self.delay_threshold().unwrap_or(0); + + Word::from([immediate_threshold, delay_threshold, self.note_restrictions as u32, 0]) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use super::{ProcedurePolicy, ProcedurePolicyNoteRestriction}; + + #[test] + fn procedure_policy_word_encoding_matches_storage_layout() { + let policy = ProcedurePolicy::with_immediate_and_delay_thresholds(4, 3) + .unwrap() + .with_note_restriction(ProcedurePolicyNoteRestriction::NoInputOrOutputNotes); + + assert_eq!(policy.to_word(), [4u32, 3, 3, 0].into()); + } + + #[test] + fn procedure_policy_construction_rejects_invalid_combinations() { + assert!( + ProcedurePolicy::with_immediate_threshold(0) + .unwrap_err() + .to_string() + .contains("procedure policy immediate threshold must be at least 1") + ); + + assert!( + ProcedurePolicy::with_immediate_and_delay_thresholds(1, 0) + .unwrap_err() + .to_string() + .contains("immediate and delayed thresholds must both be at least 1") + ); + + assert!( + ProcedurePolicy::with_immediate_and_delay_thresholds(1, 2) + .unwrap_err() + .to_string() + .contains("delay threshold cannot exceed immediate threshold") + ); + } + + #[test] + fn procedure_policy_thresholds_are_exposed_with_getters() { + let procedure_policy = ProcedurePolicy::with_delay_threshold(2).unwrap(); + + assert_eq!(procedure_policy.immediate_threshold(), None); + assert_eq!(procedure_policy.delay_threshold(), Some(2)); + } + + #[test] + fn procedure_policy_note_restrictions_are_exposed_with_getters() { + let procedure_policy = ProcedurePolicy::with_immediate_threshold(2) + .unwrap() + .with_note_restriction(ProcedurePolicyNoteRestriction::NoInputNotes); + + assert_eq!(ProcedurePolicyNoteRestriction::default(), ProcedurePolicyNoteRestriction::None); + assert_eq!( + procedure_policy.note_restrictions(), + ProcedurePolicyNoteRestriction::NoInputNotes + ); + } +} diff --git a/crates/miden-standards/src/account/auth/singlesig.rs b/crates/miden-standards/src/account/auth/singlesig.rs index ee1e8401ef..a39b1a21d3 100644 --- a/crates/miden-standards/src/account/auth/singlesig.rs +++ b/crates/miden-standards/src/account/auth/singlesig.rs @@ -1,5 +1,5 @@ use miden_protocol::Word; -use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; +use miden_protocol::account::auth::{AuthScheme, PublicKey, PublicKeyCommitment}; use miden_protocol::account::component::{ AccountComponentMetadata, SchemaType, @@ -7,6 +7,7 @@ use miden_protocol::account::component::{ StorageSlotSchema, }; use miden_protocol::account::{AccountComponent, AccountType, StorageSlot, StorageSlotName}; +use miden_protocol::crypto::dsa::{ecdsa_k256_keccak, falcon512_poseidon2}; use miden_protocol::utils::sync::LazyLock; use crate::account::components::singlesig_library; @@ -52,6 +53,36 @@ impl AuthSingleSig { Self { pub_key, auth_scheme } } + /// Creates a new [`AuthSingleSig`] component using the Falcon512Poseidon2 signature scheme. + /// + /// The public key commitment is derived from the provided Falcon512 public key. + pub fn falcon512_poseidon2(pub_key: falcon512_poseidon2::PublicKey) -> Self { + Self { + pub_key: pub_key.into(), + auth_scheme: AuthScheme::Falcon512Poseidon2, + } + } + + /// Creates a new [`AuthSingleSig`] component using the EcdsaK256Keccak signature scheme. + /// + /// The public key commitment is derived from the provided ECDSA K256 public key. + pub fn ecdsa_k256_keccak(pub_key: ecdsa_k256_keccak::PublicKey) -> Self { + Self { + pub_key: pub_key.into(), + auth_scheme: AuthScheme::EcdsaK256Keccak, + } + } + + /// Creates a new [`AuthSingleSig`] component from a [`PublicKey`]. + /// + /// The authentication scheme and public key commitment are derived from the provided key. + pub fn from_public_key(pub_key: PublicKey) -> Self { + Self { + auth_scheme: pub_key.auth_scheme(), + pub_key: pub_key.to_commitment(), + } + } + /// Returns the [`StorageSlotName`] where the public key is stored. pub fn public_key_slot() -> &'static StorageSlotName { &PUBKEY_SLOT_NAME diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index a14d3ce523..723cbf5d14 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -60,13 +60,13 @@ static MULTISIG_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Multisig library is well-formed") }); -/// Initialize the Multisig PSM library only once. -static MULTISIG_PSM_LIBRARY: LazyLock = LazyLock::new(|| { +/// Initialize the Guarded Multisig library only once. +static GUARDED_MULTISIG_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/auth/multisig_psm.masl" + "/assets/account_components/auth/guarded_multisig.masl" )); - Library::read_from_bytes(bytes).expect("Shipped Multisig PSM library is well-formed") + Library::read_from_bytes(bytes).expect("Shipped Guarded Multisig library is well-formed") }); // Initialize the NoAuth library only once. @@ -165,9 +165,9 @@ pub fn multisig_library() -> Library { MULTISIG_LIBRARY.clone() } -/// Returns the Multisig PSM Library. -pub fn multisig_psm_library() -> Library { - MULTISIG_PSM_LIBRARY.clone() +/// Returns the Guarded Multisig Library. +pub fn guarded_multisig_library() -> Library { + GUARDED_MULTISIG_LIBRARY.clone() } /// Returns the NoAuth Library. @@ -187,7 +187,7 @@ pub enum StandardAccountComponent { AuthSingleSig, AuthSingleSigAcl, AuthMultisig, - AuthMultisigPsm, + AuthGuardedMultisig, AuthNoAuth, } @@ -201,7 +201,7 @@ impl StandardAccountComponent { Self::AuthSingleSig => SINGLESIG_LIBRARY.as_ref(), Self::AuthSingleSigAcl => SINGLESIG_ACL_LIBRARY.as_ref(), Self::AuthMultisig => MULTISIG_LIBRARY.as_ref(), - Self::AuthMultisigPsm => MULTISIG_PSM_LIBRARY.as_ref(), + Self::AuthGuardedMultisig => GUARDED_MULTISIG_LIBRARY.as_ref(), Self::AuthNoAuth => NO_AUTH_LIBRARY.as_ref(), }; @@ -254,8 +254,8 @@ impl StandardAccountComponent { Self::AuthMultisig => { component_interface_vec.push(AccountComponentInterface::AuthMultisig) }, - Self::AuthMultisigPsm => { - component_interface_vec.push(AccountComponentInterface::AuthMultisigPsm) + Self::AuthGuardedMultisig => { + component_interface_vec.push(AccountComponentInterface::AuthGuardedMultisig) }, Self::AuthNoAuth => { component_interface_vec.push(AccountComponentInterface::AuthNoAuth) @@ -275,7 +275,7 @@ impl StandardAccountComponent { Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::AuthSingleSig.extract_component(procedures_set, component_interface_vec); Self::AuthSingleSigAcl.extract_component(procedures_set, component_interface_vec); - Self::AuthMultisigPsm.extract_component(procedures_set, component_interface_vec); + Self::AuthGuardedMultisig.extract_component(procedures_set, component_interface_vec); Self::AuthMultisig.extract_component(procedures_set, component_interface_vec); Self::AuthNoAuth.extract_component(procedures_set, component_interface_vec); } diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 6527767f3f..6ce052af26 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -7,7 +7,7 @@ use miden_protocol::note::PartialNote; use miden_protocol::{Felt, Word}; use crate::AuthMethod; -use crate::account::auth::{AuthMultisig, AuthMultisigPsm, AuthSingleSig, AuthSingleSigAcl}; +use crate::account::auth::{AuthGuardedMultisig, AuthMultisig, AuthSingleSig, AuthSingleSigAcl}; use crate::account::interface::AccountInterfaceError; // ACCOUNT COMPONENT INTERFACE @@ -34,8 +34,8 @@ pub enum AccountComponentInterface { /// [`AuthMultisig`][crate::account::auth::AuthMultisig] module. AuthMultisig, /// Exposes procedures from the - /// [`AuthMultisigPsm`][crate::account::auth::AuthMultisigPsm] module. - AuthMultisigPsm, + /// [`AuthGuardedMultisig`][crate::account::auth::AuthGuardedMultisig] module. + AuthGuardedMultisig, /// Exposes procedures from the [`NoAuth`][crate::account::auth::NoAuth] module. /// /// This authentication scheme provides no cryptographic authentication and only increments @@ -64,7 +64,7 @@ impl AccountComponentInterface { AccountComponentInterface::AuthSingleSig => "SingleSig".to_string(), AccountComponentInterface::AuthSingleSigAcl => "SingleSig ACL".to_string(), AccountComponentInterface::AuthMultisig => "Multisig".to_string(), - AccountComponentInterface::AuthMultisigPsm => "Multisig PSM".to_string(), + AccountComponentInterface::AuthGuardedMultisig => "Guarded Multisig".to_string(), AccountComponentInterface::AuthNoAuth => "No Auth".to_string(), AccountComponentInterface::Custom(proc_root_vec) => { let result = proc_root_vec @@ -86,7 +86,7 @@ impl AccountComponentInterface { AccountComponentInterface::AuthSingleSig | AccountComponentInterface::AuthSingleSigAcl | AccountComponentInterface::AuthMultisig - | AccountComponentInterface::AuthMultisigPsm + | AccountComponentInterface::AuthGuardedMultisig | AccountComponentInterface::AuthNoAuth ) } @@ -112,12 +112,12 @@ impl AccountComponentInterface { AuthMultisig::approver_scheme_ids_slot(), )] }, - AccountComponentInterface::AuthMultisigPsm => { + AccountComponentInterface::AuthGuardedMultisig => { vec![extract_multisig_auth_method( storage, - AuthMultisigPsm::threshold_config_slot(), - AuthMultisigPsm::approver_public_keys_slot(), - AuthMultisigPsm::approver_scheme_ids_slot(), + AuthGuardedMultisig::threshold_config_slot(), + AuthGuardedMultisig::approver_public_keys_slot(), + AuthGuardedMultisig::approver_scheme_ids_slot(), )] }, AccountComponentInterface::AuthNoAuth => vec![AuthMethod::NoAuth], diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index f23b1414a7..ebeac7355e 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -13,8 +13,8 @@ use crate::account::components::{ StandardAccountComponent, basic_fungible_faucet_library, basic_wallet_library, + guarded_multisig_library, multisig_library, - multisig_psm_library, network_fungible_faucet_library, no_auth_library, singlesig_acl_library, @@ -113,9 +113,9 @@ impl AccountInterfaceExt for AccountInterface { component_proc_digests .extend(multisig_library().mast_forest().procedure_digests()); }, - AccountComponentInterface::AuthMultisigPsm => { + AccountComponentInterface::AuthGuardedMultisig => { component_proc_digests - .extend(multisig_psm_library().mast_forest().procedure_digests()); + .extend(guarded_multisig_library().mast_forest().procedure_digests()); }, AccountComponentInterface::AuthNoAuth => { component_proc_digests diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index ae91cf445f..1d473b9c7f 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -126,7 +126,7 @@ impl SwapNote { /// /// ```text /// [ - /// note_type (2 bits) | script_root (14 bits) + /// note_type (1 bit) | script_root (15 bits) /// | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits) /// ] /// ``` @@ -138,10 +138,10 @@ impl SwapNote { requested_asset: &Asset, ) -> NoteTag { let swap_root_bytes = Self::script().root().as_bytes(); - // Construct the swap use case ID from the 14 most significant bits of the script root. This - // leaves the two most significant bits zero. - let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 6; - swap_use_case_id |= (swap_root_bytes[1] >> 2) as u16; + // Construct the swap use case ID from the 15 most significant bits of the script root. This + // leaves the most significant bit zero. + let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 7; + swap_use_case_id |= (swap_root_bytes[1] >> 1) as u16; // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload. let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into(); @@ -152,7 +152,7 @@ impl SwapNote { let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); - let tag = ((note_type as u8 as u32) << 30) + let tag = ((note_type as u8 as u32) << 31) | ((swap_use_case_id as u32) << 16) | asset_pair as u32; @@ -419,18 +419,18 @@ mod tests { let actual_tag = SwapNote::build_tag(note_type, &offered_asset, &requested_asset); assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match"); - assert_eq!((actual_tag.as_u32() >> 30) as u8, note_type as u8, "note type should match"); + assert_eq!((actual_tag.as_u32() >> 31) as u8, note_type as u8, "note type should match"); // Check the 8 bits of the first script root byte. assert_eq!( - (actual_tag.as_u32() >> 22) as u8, + (actual_tag.as_u32() >> 23) as u8, SwapNote::script_root().as_bytes()[0], "swap script root byte 0 should match" ); - // Extract the 6 bits of the second script root byte and shift for comparison. + // Extract the 7 bits of the second script root byte and shift for comparison. assert_eq!( - ((actual_tag.as_u32() & 0b00000000_00111111_00000000_00000000) >> 16) as u8, - SwapNote::script_root().as_bytes()[1] >> 2, - "swap script root byte 1 should match with the lower two bits set to zero" + ((actual_tag.as_u32() & 0b00000000_01111111_00000000_00000000) >> 16) as u8, + SwapNote::script_root().as_bytes()[1] >> 1, + "swap script root byte 1 should match with the highest bit set to zero" ); } } diff --git a/crates/miden-standards/src/testing/mock_util_lib.rs b/crates/miden-standards/src/testing/mock_util_lib.rs index 9d6eebe6cd..7fce87a5c3 100644 --- a/crates/miden-standards/src/testing/mock_util_lib.rs +++ b/crates/miden-standards/src/testing/mock_util_lib.rs @@ -9,13 +9,14 @@ use crate::StandardsLib; const MOCK_UTIL_LIBRARY_CODE: &str = " use miden::protocol::output_note + use miden::protocol::note::NOTE_TYPE_PRIVATE use miden::standards::wallets::basic->wallet #! Inputs: [] #! Outputs: [note_idx] pub proc create_default_note push.1.2.3.4 # = RECIPIENT - push.2 # = NoteType::Private + push.NOTE_TYPE_PRIVATE # = NoteType::Private push.0 # = NoteTag # => [tag, note_type, RECIPIENT] diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs b/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs index 7ce54979cb..e190380e18 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs @@ -36,13 +36,7 @@ use miden_standards::note::{ use miden_standards::testing::mock_account::MockAccountExt; use miden_standards::testing::note::NoteBuilder; use miden_tx::auth::UnreachableAuth; -use miden_tx::{ - FailedNote, - NoteConsumptionChecker, - NoteConsumptionInfo, - TransactionExecutor, - TransactionExecutorError, -}; +use miden_tx::{NoteConsumptionChecker, TransactionExecutor, TransactionExecutorError}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -90,15 +84,16 @@ async fn check_note_consumability_standard_notes_success() -> anyhow::Result<()> .check_notes_consumability(target_account_id, block_ref, notes.clone(), tx_args) .await?; - assert_matches!(consumption_info, NoteConsumptionInfo { successful, failed, .. } => { - assert_eq!(successful.len(), notes.len()); + let successful = consumption_info.successful(); + let failed = consumption_info.failed(); - // we asserted that `successful` and `notes` vectors have the same length, so it's safe to - // check their equality that way - successful.iter().for_each(|successful_note| assert!(notes.contains(successful_note))); + assert_eq!(successful.len(), notes.len()); - assert!(failed.is_empty()); - }); + // we asserted that `successful` and `notes` vectors have the same length, so it's safe to + // check their equality that way + successful.iter().for_each(|s| assert!(notes.contains(s.note()))); + + assert!(failed.is_empty()); Ok(()) } @@ -137,14 +132,15 @@ async fn check_note_consumability_custom_notes_success( .check_notes_consumability(account_id, block_ref, notes.clone(), tx_args) .await?; - assert_matches!(consumption_info, NoteConsumptionInfo { successful, failed, .. }=> { - if notes.is_empty() { - assert!(successful.is_empty()); - assert!(failed.is_empty()); - } else { - assert_eq!(successful.len(), notes.len()); - } - }); + let successful = consumption_info.successful(); + let failed = consumption_info.failed(); + + if notes.is_empty() { + assert!(successful.is_empty()); + assert!(failed.is_empty()); + } else { + assert_eq!(successful.len(), notes.len()); + } Ok(()) } @@ -216,49 +212,41 @@ async fn check_note_consumability_partial_success() -> anyhow::Result<()> { .check_notes_consumability(account_id, block_ref, notes, tx_args) .await?; + let successful = consumption_info.successful(); + let failed = consumption_info.failed(); + + assert_eq!(failed.len(), 2); + assert_eq!(successful.len(), 3); + + // First failing note. + let first_failed = failed.first().expect("first failed notes should exist"); + assert_matches!( + first_failed.error(), + TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::OperationError { + err: miden_processor::operation::OperationError::DivideByZero, + .. + } + ) + ); + assert_eq!(first_failed.note().id(), failing_note_2.id()); + + // Second failing note. + let second_failed = failed.get(1).expect("second failed note should exist"); assert_matches!( - consumption_info, - NoteConsumptionInfo { - successful, - failed - } => { - assert_eq!(failed.len(), 2); - assert_eq!(successful.len(), 3); - - // First failing note. - assert_matches!( - failed.first().expect("first failed notes should exist"), - FailedNote { - note, - error: TransactionExecutorError::TransactionProgramExecutionFailed( - ExecutionError::OperationError { err: miden_processor::operation::OperationError::DivideByZero, .. }) - } => { - assert_eq!( - note.id(), - failing_note_2.id(), - ); - } - ); - // Second failing note. - assert_matches!( - failed.get(1).expect("second failed note should exist"), - FailedNote { - note, - error: TransactionExecutorError::TransactionProgramExecutionFailed( - ExecutionError::OperationError { err: miden_processor::operation::OperationError::DivideByZero, .. }) - } => { - assert_eq!( - note.id(), - failing_note_1.id(), - ); - } - ); - // Successful notes. - assert_eq!( - [successful[0].id(), successful[1].id(), successful[2].id()], - [successful_note_2.id(), successful_note_1.id(), successful_note_3.id()], - ); + second_failed.error(), + TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::OperationError { + err: miden_processor::operation::OperationError::DivideByZero, + .. } + ) + ); + assert_eq!(second_failed.note().id(), failing_note_1.id()); + // Successful notes. + assert_eq!( + [successful[0].note().id(), successful[1].note().id(), successful[2].note().id()], + [successful_note_2.id(), successful_note_1.id(), successful_note_3.id()], ); Ok(()) } @@ -298,16 +286,11 @@ async fn check_note_consumability_epilogue_failure() -> anyhow::Result<()> { .check_notes_consumability(account_id, block_ref, notes, tx_args) .await?; - assert_matches!( - consumption_info, - NoteConsumptionInfo { - successful, - failed - } => { - assert!(successful.is_empty()); - assert_eq!(failed.len(), 1); - } - ); + let successful = consumption_info.successful(); + let failed = consumption_info.failed(); + + assert!(successful.is_empty()); + assert_eq!(failed.len(), 1); Ok(()) } @@ -380,49 +363,42 @@ async fn check_note_consumability_epilogue_failure_with_new_combination() -> any .check_notes_consumability(account_id, block_ref, notes, tx_args) .await?; + let successful = consumption_info.successful(); + let failed = consumption_info.failed(); + + assert_eq!(failed.len(), 2); + assert_eq!(successful.len(), 3); + + // First failing note should be the note that does not cause epilogue failure. + let first_failed = failed.first().expect("first failed notes should exist"); assert_matches!( - consumption_info, - NoteConsumptionInfo { - successful, - failed - } => { - assert_eq!(failed.len(), 2); - assert_eq!(successful.len(), 3); - - // First failing note should be the note that does not cause epilogue failure. - assert_matches!( - failed.first().expect("first failed notes should exist"), - FailedNote { - note, - error: TransactionExecutorError::TransactionProgramExecutionFailed( - ExecutionError::OperationError { err: miden_processor::operation::OperationError::DivideByZero, .. }) - } => { - assert_eq!( - note.id(), - failing_note_1.id(), - ); - } - ); - // Second failing note should be the note that causes epilogue failure. - assert_matches!( - failed.get(1).expect("second failed note should exist"), - FailedNote { - note, - error: TransactionExecutorError::TransactionProgramExecutionFailed( - ExecutionError::OperationError { err: miden_processor::operation::OperationError::FailedAssertion { .. }, .. }) - } => { - assert_eq!( - note.id(), - fail_epilogue_note.id(), - ); - } - ); - // Successful notes. - assert_eq!( - [successful[0].id(), successful[1].id(), successful[2].id()], - [successful_note_1.id(), successful_note_2.id(), successful_note_3.id()], - ); + first_failed.error(), + TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::OperationError { + err: miden_processor::operation::OperationError::DivideByZero, + .. } + ) + ); + assert_eq!(first_failed.note().id(), failing_note_1.id()); + + // Second failing note should be the note that causes epilogue failure. + let second_failed = failed.get(1).expect("second failed note should exist"); + assert_matches!( + second_failed.error(), + TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::OperationError { + err: miden_processor::operation::OperationError::FailedAssertion { .. }, + .. + } + ) + ); + assert_eq!(second_failed.note().id(), fail_epilogue_note.id()); + + // Successful notes. + assert_eq!( + [successful[0].note().id(), successful[1].note().id(), successful[2].note().id()], + [successful_note_1.id(), successful_note_2.id(), successful_note_3.id()], ); Ok(()) } diff --git a/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs index de5171d810..779083be69 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs @@ -168,6 +168,60 @@ async fn test_active_note_get_sender() -> anyhow::Result<()> { Ok(()) } +#[rstest::rstest] +#[case(NoteType::Public)] +#[case(NoteType::Private)] +#[tokio::test] +async fn test_active_note_get_note_type(#[case] note_type: NoteType) -> anyhow::Result<()> { + let tx_context = { + let account = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); + let mut rng = miden_protocol::crypto::rand::RandomCoin::new(Word::default()); + let input_note = crate::utils::create_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + note_type, + [FungibleAsset::mock(100)], + &mut rng, + ); + TransactionContextBuilder::new(account) + .extend_input_notes(vec![input_note]) + .build()? + }; + + let code = " + use $kernel::prologue + use $kernel::note->note_internal + use miden::protocol::active_note + use miden::protocol::note + + begin + exec.prologue::prepare_transaction + exec.note_internal::prepare_note + dropw dropw dropw dropw + + exec.active_note::get_metadata + # => [NOTE_ATTACHMENT, METADATA_HEADER] + + dropw + # => [METADATA_HEADER] + + exec.note::metadata_into_note_type + # => [note_type] + + # truncate the stack + swapw dropw + end + "; + + let exec_output = tx_context.execute_code(code).await?; + + let actual_note_type = NoteType::try_from(exec_output.get_stack_element(0)) + .expect("stack element should be a valid note type"); + assert_eq!(actual_note_type, note_type); + + Ok(()) +} + #[tokio::test] async fn test_active_note_get_assets() -> anyhow::Result<()> { // Creates a mockchain with an account and a note that it can consume diff --git a/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs b/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs index 9ae7d70fbd..4930ab1673 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs @@ -2038,7 +2038,7 @@ fn foreign_account_data_memory_assertions( for (i, elements) in foreign_account .code() - .as_elements() + .to_elements() .chunks(AccountProcedureRoot::NUM_ELEMENTS) .enumerate() { diff --git a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index fbcc0cd51d..e03c3454ff 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs @@ -1,4 +1,5 @@ use alloc::string::String; +use alloc::vec::Vec; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId}; @@ -6,6 +7,7 @@ use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::errors::tx_kernel::{ ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS, + ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT, }; use miden_protocol::note::{ @@ -78,7 +80,7 @@ async fn test_create_note() -> anyhow::Result<()> { exec.prologue::prepare_transaction push.{recipient} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} exec.output_note::create @@ -88,7 +90,7 @@ async fn test_create_note() -> anyhow::Result<()> { end ", recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, tag = tag, ); @@ -157,7 +159,7 @@ fn note_creation_script(tag: Felt) -> String { exec.prologue::prepare_transaction push.{recipient} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} exec.output_note::create @@ -167,7 +169,7 @@ fn note_creation_script(tag: Felt) -> String { end ", recipient = Word::from([0, 1, 2, 3u32]), - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, ) } @@ -188,7 +190,7 @@ async fn test_create_note_too_many_notes() -> anyhow::Result<()> { exec.prologue::prepare_transaction push.{recipient} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} exec.output_note::create @@ -196,7 +198,7 @@ async fn test_create_note_too_many_notes() -> anyhow::Result<()> { ", tag = NoteTag::new(1234 << 16 | 5678), recipient = Word::from([0, 1, 2, 3u32]), - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, ); let exec_output = tx_context.execute_code(&code).await; @@ -264,7 +266,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { # create output note 1 push.{recipient_1} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag_1} exec.output_note::create # => [note_idx] @@ -276,7 +278,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { # create output note 2 push.{recipient_2} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag_2} exec.output_note::create # => [note_idx] @@ -303,7 +305,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { # => [OUTPUT_NOTES_COMMITMENT] end ", - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, recipient_1 = output_note_1.recipient().digest(), tag_1 = output_note_1.metadata().tag(), ASSET_1_KEY = asset_1.to_key_word(), @@ -373,7 +375,7 @@ async fn test_create_note_and_add_asset() -> anyhow::Result<()> { exec.prologue::prepare_transaction push.{recipient} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} exec.output_note::create @@ -395,7 +397,7 @@ async fn test_create_note_and_add_asset() -> anyhow::Result<()> { end ", recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, tag = tag, ASSET_KEY = asset.to_key_word(), ASSET_VALUE = asset.to_value_word(), @@ -443,7 +445,7 @@ async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { exec.prologue::prepare_transaction push.{recipient} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} exec.output_note::create # => [note_idx] @@ -480,7 +482,7 @@ async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { end ", recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, tag = tag, ASSET_KEY = asset.to_key_word(), ASSET_VALUE = asset.to_value_word(), @@ -572,7 +574,7 @@ async fn test_create_note_and_add_same_nft_twice() -> anyhow::Result<()> { # => [] push.{recipient} - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} exec.output_note::create # => [note_idx] @@ -592,7 +594,7 @@ async fn test_create_note_and_add_same_nft_twice() -> anyhow::Result<()> { end ", recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, tag = tag, ASSET_KEY = non_fungible_asset.to_key_word(), ASSET_VALUE = non_fungible_asset.to_value_word(), @@ -604,6 +606,79 @@ async fn test_create_note_and_add_same_nft_twice() -> anyhow::Result<()> { Ok(()) } +/// Tests adding assets to an output note at and beyond the `MAX_ASSETS_PER_NOTE` limit. +/// +/// - `at_max`: adding exactly `MAX_ASSETS_PER_NOTE` assets succeeds. +/// - `exceeding_max`: adding `MAX_ASSETS_PER_NOTE + 1` assets fails with +/// `ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT`. +#[rstest::rstest] +#[case::at_max(0, false)] +#[case::exceeding_max(1, true)] +#[tokio::test] +async fn test_add_assets_around_max_per_note( + #[case] extra_assets: usize, + #[case] expect_error: bool, +) -> anyhow::Result<()> { + use miden_protocol::MAX_ASSETS_PER_NOTE; + + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + let recipient = Word::from([0, 1, 2, 3u32]); + let tag = NoteTag::new(999 << 16 | 777); + + // Create the required number of unique non-fungible assets. + let num_assets = MAX_ASSETS_PER_NOTE + extra_assets; + let assets: Vec = (0..num_assets) + .map(|i| NonFungibleAsset::mock(&(i as u32).to_le_bytes())) + .collect(); + + // Build the MASM code: create a note, then add all assets one by one. + let mut add_assets_code = String::new(); + for (i, asset) in assets.iter().enumerate() { + let is_last = i == num_assets - 1; + // For all but the last asset, duplicate note_idx so it remains on the stack. + if !is_last { + add_assets_code.push_str("dup\n"); + } + add_assets_code.push_str(&format!( + "push.{ASSET_VALUE}\npush.{ASSET_KEY}\nexec.output_note::add_asset\n", + ASSET_KEY = asset.to_key_word(), + ASSET_VALUE = asset.to_value_word(), + )); + } + + let code = format!( + " + use $kernel::prologue + use miden::protocol::output_note + + begin + exec.prologue::prepare_transaction + + push.{recipient} + push.{NOTE_TYPE_PUBLIC} + push.{tag} + exec.output_note::create + # => [note_idx] + + {add_assets_code} + end + ", + recipient = recipient, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, + tag = tag, + add_assets_code = add_assets_code, + ); + + if expect_error { + let exec_output = tx_context.execute_code(&code).await; + assert_execution_error!(exec_output, ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT); + } else { + tx_context.execute_code(&code).await?; + } + Ok(()) +} + /// Tests that creating a note with a fungible asset with amount zero works. #[tokio::test] async fn creating_note_with_fungible_asset_amount_zero_works() -> anyhow::Result<()> { @@ -672,7 +747,7 @@ async fn test_build_recipient_hash() -> anyhow::Result<()> { exec.note::build_recipient_hash # => [RECIPIENT, pad(12)] - push.{PUBLIC_NOTE} + push.{NOTE_TYPE_PUBLIC} push.{tag} # => [tag, note_type, RECIPIENT] @@ -685,7 +760,7 @@ async fn test_build_recipient_hash() -> anyhow::Result<()> { ", script_root = input_note_1.script().clone().root(), output_serial_no = output_serial_no, - PUBLIC_NOTE = NoteType::Public as u8, + NOTE_TYPE_PUBLIC = NoteType::Public as u8, tag = tag, ); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs index 3cb661b3a2..4f762445d5 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -416,7 +416,7 @@ fn account_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transa for (i, elements) in inputs .account() .code() - .as_elements() + .to_elements() .chunks(AccountProcedureRoot::NUM_ELEMENTS) .enumerate() { diff --git a/crates/miden-testing/src/mock_chain/auth.rs b/crates/miden-testing/src/mock_chain/auth.rs index 5b7f06b06a..566d0904c9 100644 --- a/crates/miden-testing/src/mock_chain/auth.rs +++ b/crates/miden-testing/src/mock_chain/auth.rs @@ -7,14 +7,14 @@ use miden_protocol::account::AccountComponent; use miden_protocol::account::auth::{AuthScheme, AuthSecretKey, PublicKeyCommitment}; use miden_protocol::testing::noop_auth_component::NoopAuthComponent; use miden_standards::account::auth::{ + AuthGuardedMultisig, + AuthGuardedMultisigConfig, AuthMultisig, AuthMultisigConfig, - AuthMultisigPsm, - AuthMultisigPsmConfig, AuthSingleSig, AuthSingleSigAcl, AuthSingleSigAclConfig, - PsmConfig, + GuardianConfig, }; use miden_standards::testing::account_component::{ ConditionalAuthComponent, @@ -38,11 +38,11 @@ pub enum Auth { proc_threshold_map: Vec<(Word, u32)>, }, - /// Multisig with a private state manager. - MultisigPsm { + /// Guarded multisig. + GuardedMultisig { threshold: u32, approvers: Vec<(PublicKeyCommitment, AuthScheme)>, - psm_config: PsmConfig, + guardian_config: GuardianConfig, proc_threshold_map: Vec<(Word, u32)>, }, @@ -96,17 +96,18 @@ impl Auth { (component, None) }, - Auth::MultisigPsm { + Auth::GuardedMultisig { threshold, approvers, - psm_config, + guardian_config, proc_threshold_map, } => { - let config = AuthMultisigPsmConfig::new(approvers.clone(), *threshold, *psm_config) - .and_then(|cfg| cfg.with_proc_thresholds(proc_threshold_map.clone())) - .expect("invalid multisig psm config"); - let component = AuthMultisigPsm::new(config) - .expect("multisig psm component creation failed") + let config = + AuthGuardedMultisigConfig::new(approvers.clone(), *threshold, *guardian_config) + .and_then(|cfg| cfg.with_proc_thresholds(proc_threshold_map.clone())) + .expect("invalid guarded multisig config"); + let component = AuthGuardedMultisig::new(config) + .expect("guarded multisig component creation failed") .into(); (component, None) diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index 0b7d3d47c2..177c0f8958 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -31,7 +31,7 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { begin push.{attachment_word} push.{metadata_header} - exec.note::extract_attachment_info_from_metadata + exec.note::metadata_into_attachment_info # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] swap # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] diff --git a/crates/miden-testing/tests/auth/multisig_psm.rs b/crates/miden-testing/tests/auth/guarded_multisig.rs similarity index 68% rename from crates/miden-testing/tests/auth/multisig_psm.rs rename to crates/miden-testing/tests/auth/guarded_multisig.rs index 31d090d460..13bb9ae2aa 100644 --- a/crates/miden-testing/tests/auth/multisig_psm.rs +++ b/crates/miden-testing/tests/auth/guarded_multisig.rs @@ -14,8 +14,12 @@ use miden_protocol::testing::account_id::{ }; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; -use miden_standards::account::auth::{AuthMultisigPsm, AuthMultisigPsmConfig, PsmConfig}; -use miden_standards::account::components::multisig_psm_library; +use miden_standards::account::auth::{ + AuthGuardedMultisig, + AuthGuardedMultisigConfig, + GuardianConfig, +}; +use miden_standards::account::components::guarded_multisig_library; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ @@ -74,11 +78,11 @@ fn setup_keys_and_authenticators_with_scheme( Ok((secret_keys, auth_schemes, public_keys, authenticators)) } -/// Creates a multisig account configured with a private state manager signer. -fn create_multisig_account_with_psm( +/// Creates a guarded multisig account configured with a guardian signer. +fn create_guarded_multisig_account( threshold: u32, approvers: &[(PublicKey, AuthScheme)], - psm: PsmConfig, + guardian: GuardianConfig, asset_amount: u64, proc_threshold_map: Vec<(Word, u32)>, ) -> anyhow::Result { @@ -87,11 +91,11 @@ fn create_multisig_account_with_psm( .map(|(pub_key, auth_scheme)| (pub_key.to_commitment(), *auth_scheme)) .collect(); - let config = AuthMultisigPsmConfig::new(approvers, threshold, psm)? + let config = AuthGuardedMultisigConfig::new(approvers, threshold, guardian)? .with_proc_thresholds(proc_threshold_map)?; let multisig_account = AccountBuilder::new([0; 32]) - .with_auth_component(AuthMultisigPsm::new(config)?) + .with_auth_component(AuthGuardedMultisig::new(config)?) .with_component(BasicWallet) .account_type(AccountType::RegularAccountUpdatableCode) .storage_mode(AccountStorageMode::Public) @@ -105,13 +109,13 @@ fn create_multisig_account_with_psm( // TESTS // ================================================================================================ -/// Tests that multisig authentication requires an additional PSM signature when +/// Tests that guarded multisig authentication requires an additional guardian signature when /// configured. #[rstest] #[case::ecdsa(AuthScheme::EcdsaK256Keccak)] #[case::falcon(AuthScheme::Falcon512Poseidon2)] #[tokio::test] -async fn test_multisig_psm_signature_required( +async fn test_guarded_multisig_signature_required( #[case] auth_scheme: AuthScheme, ) -> anyhow::Result<()> { let (_secret_keys, auth_schemes, public_keys, authenticators) = @@ -122,14 +126,15 @@ async fn test_multisig_psm_signature_required( .map(|(pk, scheme)| (pk.clone(), *scheme)) .collect::>(); - let psm_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); - let psm_public_key = psm_secret_key.public_key(); - let psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&psm_secret_key)); + let guardian_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let guardian_public_key = guardian_secret_key.public_key(); + let guardian_authenticator = + BasicAuthenticator::new(core::slice::from_ref(&guardian_secret_key)); - let mut multisig_account = create_multisig_account_with_psm( + let mut multisig_account = create_guarded_multisig_account( 2, &approvers, - PsmConfig::new(psm_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), + GuardianConfig::new(guardian_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), 10, vec![], )?; @@ -168,8 +173,8 @@ async fn test_multisig_psm_signature_required( .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) .await?; - // Missing PSM signature must fail. - let without_psm_result = mock_chain + // Missing guardian signature must fail. + let without_guardian_result = mock_chain .build_tx_context(multisig_account.id(), &[input_note.id()], &[])? .extend_expected_output_notes(vec![RawOutputNote::Full(output_note.clone())]) .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) @@ -178,19 +183,22 @@ async fn test_multisig_psm_signature_required( .build()? .execute() .await; - assert!(matches!(without_psm_result, Err(TransactionExecutorError::Unauthorized(_)))); + assert!(matches!( + without_guardian_result, + Err(TransactionExecutorError::Unauthorized(_)) + )); - let psm_signature = psm_authenticator - .get_signature(psm_public_key.to_commitment(), &tx_summary_signing) + let guardian_signature = guardian_authenticator + .get_signature(guardian_public_key.to_commitment(), &tx_summary_signing) .await?; - // With PSM signature the transaction should succeed. + // With guardian signature the transaction should succeed. let tx_context_execute = mock_chain .build_tx_context(multisig_account.id(), &[input_note.id()], &[])? .extend_expected_output_notes(vec![RawOutputNote::Full(output_note)]) .add_signature(public_keys[0].to_commitment(), msg, sig_1) .add_signature(public_keys[1].to_commitment(), msg, sig_2) - .add_signature(psm_public_key.to_commitment(), msg, psm_signature) + .add_signature(guardian_public_key.to_commitment(), msg, guardian_signature) .auth_args(salt) .build()? .execute() @@ -211,12 +219,12 @@ async fn test_multisig_psm_signature_required( Ok(()) } -/// Tests that the PSM public key can be updated and then enforced. +/// Tests that the guardian public key can be updated and then enforced for guarded multisig. #[rstest] #[case::ecdsa(AuthScheme::EcdsaK256Keccak)] #[case::falcon(AuthScheme::Falcon512Poseidon2)] #[tokio::test] -async fn test_multisig_update_psm_public_key( +async fn test_guarded_multisig_update_guardian_public_key( #[case] auth_scheme: AuthScheme, ) -> anyhow::Result<()> { let (_secret_keys, auth_schemes, public_keys, authenticators) = @@ -227,19 +235,21 @@ async fn test_multisig_update_psm_public_key( .map(|(pk, scheme)| (pk.clone(), *scheme)) .collect::>(); - let old_psm_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); - let old_psm_public_key = old_psm_secret_key.public_key(); - let old_psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&old_psm_secret_key)); + let old_guardian_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let old_guardian_public_key = old_guardian_secret_key.public_key(); + let old_guardian_authenticator = + BasicAuthenticator::new(core::slice::from_ref(&old_guardian_secret_key)); - let new_psm_secret_key = AuthSecretKey::new_falcon512_poseidon2(); - let new_psm_public_key = new_psm_secret_key.public_key(); - let new_psm_auth_scheme = new_psm_secret_key.auth_scheme(); - let new_psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&new_psm_secret_key)); + let new_guardian_secret_key = AuthSecretKey::new_falcon512_poseidon2(); + let new_guardian_public_key = new_guardian_secret_key.public_key(); + let new_guardian_auth_scheme = new_guardian_secret_key.auth_scheme(); + let new_guardian_authenticator = + BasicAuthenticator::new(core::slice::from_ref(&new_guardian_secret_key)); - let multisig_account = create_multisig_account_with_psm( + let multisig_account = create_guarded_multisig_account( 2, &approvers, - PsmConfig::new(old_psm_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), + GuardianConfig::new(old_guardian_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), 10, vec![], )?; @@ -249,18 +259,18 @@ async fn test_multisig_update_psm_public_key( .build() .unwrap(); - let new_psm_key_word: Word = new_psm_public_key.to_commitment().into(); - let new_psm_scheme_id = new_psm_auth_scheme as u32; - let update_psm_script = CodeBuilder::new() - .with_dynamically_linked_library(multisig_psm_library())? + let new_guardian_key_word: Word = new_guardian_public_key.to_commitment().into(); + let new_guardian_scheme_id = new_guardian_auth_scheme as u32; + let update_guardian_script = CodeBuilder::new() + .with_dynamically_linked_library(guarded_multisig_library())? .compile_tx_script(format!( - "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::miden::standards::components::auth::multisig_psm::update_psm_public_key\n drop\n dropw\nend" + "begin\n push.{new_guardian_key_word}\n push.{new_guardian_scheme_id}\n call.::miden::standards::components::auth::guarded_multisig::update_guardian_public_key\n drop\n dropw\nend" ))?; let update_salt = Word::from([Felt::new(991); 4]); let tx_context_init = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .tx_script(update_psm_script.clone()) + .tx_script(update_guardian_script.clone()) .auth_args(update_salt) .build()?; @@ -278,10 +288,10 @@ async fn test_multisig_update_psm_public_key( .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) .await?; - // PSM key rotation intentionally skips PSM signature for this update tx. - let update_psm_tx = mock_chain + // Guardian key rotation intentionally skips guardian signature for this update tx. + let update_guardian_tx = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .tx_script(update_psm_script) + .tx_script(update_guardian_script) .add_signature(public_keys[0].to_commitment(), update_msg, sig_1) .add_signature(public_keys[1].to_commitment(), update_msg, sig_2) .auth_args(update_salt) @@ -290,24 +300,25 @@ async fn test_multisig_update_psm_public_key( .await?; let mut updated_multisig_account = multisig_account.clone(); - updated_multisig_account.apply_delta(update_psm_tx.account_delta())?; - let updated_psm_public_key = updated_multisig_account - .storage() - .get_map_item(AuthMultisigPsm::psm_public_key_slot(), Word::empty())?; - assert_eq!(updated_psm_public_key, Word::from(new_psm_public_key.to_commitment())); - let updated_psm_scheme_id = updated_multisig_account + updated_multisig_account.apply_delta(update_guardian_tx.account_delta())?; + let updated_guardian_public_key = updated_multisig_account .storage() - .get_map_item(AuthMultisigPsm::psm_scheme_id_slot(), Word::from([0u32, 0, 0, 0]))?; + .get_map_item(AuthGuardedMultisig::guardian_public_key_slot(), Word::empty())?; + assert_eq!(updated_guardian_public_key, Word::from(new_guardian_public_key.to_commitment())); + let updated_guardian_scheme_id = updated_multisig_account.storage().get_map_item( + AuthGuardedMultisig::guardian_scheme_id_slot(), + Word::from([0u32, 0, 0, 0]), + )?; assert_eq!( - updated_psm_scheme_id, - Word::from([new_psm_auth_scheme as u32, 0u32, 0u32, 0u32]) + updated_guardian_scheme_id, + Word::from([new_guardian_auth_scheme as u32, 0u32, 0u32, 0u32]) ); - mock_chain.add_pending_executed_transaction(&update_psm_tx)?; + mock_chain.add_pending_executed_transaction(&update_guardian_tx)?; mock_chain.prove_next_block()?; - // Build one tx summary after key update. Old PSM must fail and new PSM must pass on this same - // transaction. + // Build one tx summary after key update. Old GUARDIAN must fail and new GUARDIAN must pass on + // this same transaction. let next_salt = Word::from([Felt::new(992); 4]); let tx_context_init_next = mock_chain .build_tx_context(updated_multisig_account.id(), &[], &[])? @@ -327,31 +338,34 @@ async fn test_multisig_update_psm_public_key( let next_sig_2 = authenticators[1] .get_signature(public_keys[1].to_commitment(), &tx_summary_next_signing) .await?; - let old_psm_sig_next = old_psm_authenticator - .get_signature(old_psm_public_key.to_commitment(), &tx_summary_next_signing) + let old_guardian_sig_next = old_guardian_authenticator + .get_signature(old_guardian_public_key.to_commitment(), &tx_summary_next_signing) .await?; - let new_psm_sig_next = new_psm_authenticator - .get_signature(new_psm_public_key.to_commitment(), &tx_summary_next_signing) + let new_guardian_sig_next = new_guardian_authenticator + .get_signature(new_guardian_public_key.to_commitment(), &tx_summary_next_signing) .await?; - // Old PSM signature must fail after key update. - let with_old_psm_result = mock_chain + // Old guardian signature must fail after key update. + let with_old_guardian_result = mock_chain .build_tx_context(updated_multisig_account.id(), &[], &[])? .add_signature(public_keys[0].to_commitment(), next_msg, next_sig_1.clone()) .add_signature(public_keys[1].to_commitment(), next_msg, next_sig_2.clone()) - .add_signature(old_psm_public_key.to_commitment(), next_msg, old_psm_sig_next) + .add_signature(old_guardian_public_key.to_commitment(), next_msg, old_guardian_sig_next) .auth_args(next_salt) .build()? .execute() .await; - assert!(matches!(with_old_psm_result, Err(TransactionExecutorError::Unauthorized(_)))); + assert!(matches!( + with_old_guardian_result, + Err(TransactionExecutorError::Unauthorized(_)) + )); - // New PSM signature must pass. + // New guardian signature must pass. mock_chain .build_tx_context(updated_multisig_account.id(), &[], &[])? .add_signature(public_keys[0].to_commitment(), next_msg, next_sig_1) .add_signature(public_keys[1].to_commitment(), next_msg, next_sig_2) - .add_signature(new_psm_public_key.to_commitment(), next_msg, new_psm_sig_next) + .add_signature(new_guardian_public_key.to_commitment(), next_msg, new_guardian_sig_next) .auth_args(next_salt) .build()? .execute() @@ -360,12 +374,12 @@ async fn test_multisig_update_psm_public_key( Ok(()) } -/// Tests that `update_psm_public_key` must be the only account action in the transaction. +/// Tests that `update_guardian_public_key` must be the only account action in the transaction. #[rstest] #[case::ecdsa(AuthScheme::EcdsaK256Keccak)] #[case::falcon(AuthScheme::Falcon512Poseidon2)] #[tokio::test] -async fn test_multisig_update_psm_public_key_must_be_called_alone( +async fn test_guarded_multisig_update_guardian_public_key_must_be_called_alone( #[case] auth_scheme: AuthScheme, ) -> anyhow::Result<()> { let (_secret_keys, auth_schemes, public_keys, authenticators) = @@ -376,28 +390,29 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( .map(|(pk, scheme)| (pk.clone(), *scheme)) .collect::>(); - let old_psm_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); - let old_psm_public_key = old_psm_secret_key.public_key(); - let old_psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&old_psm_secret_key)); + let old_guardian_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let old_guardian_public_key = old_guardian_secret_key.public_key(); + let old_guardian_authenticator = + BasicAuthenticator::new(core::slice::from_ref(&old_guardian_secret_key)); - let new_psm_secret_key = AuthSecretKey::new_falcon512_poseidon2(); - let new_psm_public_key = new_psm_secret_key.public_key(); - let new_psm_auth_scheme = new_psm_secret_key.auth_scheme(); + let new_guardian_secret_key = AuthSecretKey::new_falcon512_poseidon2(); + let new_guardian_public_key = new_guardian_secret_key.public_key(); + let new_guardian_auth_scheme = new_guardian_secret_key.auth_scheme(); - let multisig_account = create_multisig_account_with_psm( + let multisig_account = create_guarded_multisig_account( 2, &approvers, - PsmConfig::new(old_psm_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), + GuardianConfig::new(old_guardian_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), 10, vec![], )?; - let new_psm_key_word: Word = new_psm_public_key.to_commitment().into(); - let new_psm_scheme_id = new_psm_auth_scheme as u32; - let update_psm_script = CodeBuilder::new() - .with_dynamically_linked_library(multisig_psm_library())? + let new_guardian_key_word: Word = new_guardian_public_key.to_commitment().into(); + let new_guardian_scheme_id = new_guardian_auth_scheme as u32; + let update_guardian_script = CodeBuilder::new() + .with_dynamically_linked_library(guarded_multisig_library())? .compile_tx_script(format!( - "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::miden::standards::components::auth::multisig_psm::update_psm_public_key\n drop\n dropw\nend" + "begin\n push.{new_guardian_key_word}\n push.{new_guardian_scheme_id}\n call.::miden::standards::components::auth::guarded_multisig::update_guardian_public_key\n drop\n dropw\nend" ))?; let mut mock_chain_builder = @@ -413,7 +428,7 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( let salt = Word::from([Felt::new(993); 4]); let tx_context_init = mock_chain .build_tx_context(multisig_account.id(), &[receive_asset_note.id()], &[])? - .tx_script(update_psm_script.clone()) + .tx_script(update_guardian_script.clone()) .auth_args(salt) .build()?; @@ -431,33 +446,39 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) .await?; - let without_psm_result = mock_chain + let without_guardian_result = mock_chain .build_tx_context(multisig_account.id(), &[receive_asset_note.id()], &[])? - .tx_script(update_psm_script.clone()) + .tx_script(update_guardian_script.clone()) .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) .add_signature(public_keys[1].to_commitment(), msg, sig_2.clone()) .auth_args(salt) .build()? .execute() .await; - assert_transaction_executor_error!(without_psm_result, ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE); + assert_transaction_executor_error!( + without_guardian_result, + ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE + ); - let old_psm_signature = old_psm_authenticator - .get_signature(old_psm_public_key.to_commitment(), &tx_summary_signing) + let old_guardian_signature = old_guardian_authenticator + .get_signature(old_guardian_public_key.to_commitment(), &tx_summary_signing) .await?; - let with_psm_result = mock_chain + let with_guardian_result = mock_chain .build_tx_context(multisig_account.id(), &[receive_asset_note.id()], &[])? - .tx_script(update_psm_script) + .tx_script(update_guardian_script) .add_signature(public_keys[0].to_commitment(), msg, sig_1) .add_signature(public_keys[1].to_commitment(), msg, sig_2) - .add_signature(old_psm_public_key.to_commitment(), msg, old_psm_signature) + .add_signature(old_guardian_public_key.to_commitment(), msg, old_guardian_signature) .auth_args(salt) .build()? .execute() .await; - assert_transaction_executor_error!(with_psm_result, ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE); + assert_transaction_executor_error!( + with_guardian_result, + ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE + ); // Also reject rotation transactions that touch notes even when no other account procedure is // called. @@ -471,12 +492,12 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( note_recipient, ); - let new_psm_key_word: Word = new_psm_public_key.to_commitment().into(); - let new_psm_scheme_id = new_psm_auth_scheme as u32; - let update_psm_with_output_script = CodeBuilder::new() - .with_dynamically_linked_library(multisig_psm_library())? + let new_guardian_key_word: Word = new_guardian_public_key.to_commitment().into(); + let new_guardian_scheme_id = new_guardian_auth_scheme as u32; + let update_guardian_with_output_script = CodeBuilder::new() + .with_dynamically_linked_library(guarded_multisig_library())? .compile_tx_script(format!( - "use miden::protocol::output_note\nbegin\n push.{recipient}\n push.{note_type}\n push.{tag}\n exec.output_note::create\n swapdw\n dropw\n dropw\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::miden::standards::components::auth::multisig_psm::update_psm_public_key\n drop\n dropw\nend", + "use miden::protocol::output_note\nbegin\n push.{recipient}\n push.{note_type}\n push.{tag}\n exec.output_note::create\n swapdw\n dropw\n dropw\n push.{new_guardian_key_word}\n push.{new_guardian_scheme_id}\n call.::miden::standards::components::auth::guarded_multisig::update_guardian_public_key\n drop\n dropw\nend", recipient = output_note.recipient().digest(), note_type = NoteType::Public as u8, tag = Felt::from(output_note.metadata().tag()), @@ -490,7 +511,7 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( let salt = Word::from([Felt::new(994); 4]); let tx_context_init = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .tx_script(update_psm_with_output_script.clone()) + .tx_script(update_guardian_with_output_script.clone()) .add_note_script(note_script.clone()) .extend_expected_output_notes(vec![RawOutputNote::Full(output_note.clone())]) .auth_args(salt) @@ -512,7 +533,7 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( let result = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .tx_script(update_psm_with_output_script) + .tx_script(update_guardian_with_output_script) .add_note_script(note_script) .extend_expected_output_notes(vec![RawOutputNote::Full(output_note)]) .add_signature(public_keys[0].to_commitment(), msg, sig_1) diff --git a/crates/miden-testing/tests/auth/mod.rs b/crates/miden-testing/tests/auth/mod.rs index 33d6f35bde..752dfd9700 100644 --- a/crates/miden-testing/tests/auth/mod.rs +++ b/crates/miden-testing/tests/auth/mod.rs @@ -4,4 +4,4 @@ mod multisig; mod hybrid_multisig; -mod multisig_psm; +mod guarded_multisig; diff --git a/crates/miden-tx-batch-prover/src/local_batch_prover.rs b/crates/miden-tx-batch-prover/src/local_batch_prover.rs index 4e7ccfffc7..09a5dc3fce 100644 --- a/crates/miden-tx-batch-prover/src/local_batch_prover.rs +++ b/crates/miden-tx-batch-prover/src/local_batch_prover.rs @@ -74,7 +74,7 @@ impl LocalBatchProver { batch_expiration_block_num, ) = proposed_batch.into_parts(); - ProvenBatch::new( + ProvenBatch::new_unchecked( id, block_header.commitment(), block_header.block_num(), diff --git a/crates/miden-tx/Cargo.toml b/crates/miden-tx/Cargo.toml index e78138760f..33db5ad224 100644 --- a/crates/miden-tx/Cargo.toml +++ b/crates/miden-tx/Cargo.toml @@ -14,9 +14,16 @@ version.workspace = true [features] concurrent = ["miden-prover/concurrent", "std"] -default = ["std"] -std = ["miden-processor/std", "miden-protocol/std", "miden-prover/std", "miden-standards/std", "miden-verifier/std"] -testing = ["miden-processor/testing", "miden-protocol/testing", "miden-standards/testing"] +default = ["std"] +std = [ + "concurrent", + "miden-processor/std", + "miden-protocol/std", + "miden-prover/std", + "miden-standards/std", + "miden-verifier/std", +] +testing = ["miden-processor/testing", "miden-protocol/testing", "miden-standards/testing"] [dependencies] # Workspace dependencies diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index f9727fcae2..4013fc52fe 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -50,12 +50,23 @@ pub(crate) enum TransactionCheckerError { TransactionPreparation(#[source] TransactionExecutorError), #[error("transaction execution prologue failed: {0}")] PrologueExecution(#[source] TransactionExecutorError), - #[error("transaction execution epilogue failed: {0}")] - EpilogueExecution(#[source] TransactionExecutorError), + #[error("transaction execution epilogue failed: {error}")] + EpilogueExecution { + error: TransactionExecutorError, + /// Cycle counts for notes that executed successfully before the epilogue failed. + successful_notes_cycle_counts: Vec, + }, #[error("transaction note execution failed on note index {failed_note_index}: {error}")] NoteExecution { failed_note_index: usize, error: TransactionExecutorError, + /// Cycle counts for notes that executed successfully before the failed note. + successful_notes_cycle_counts: Vec, + /// The number of cycles consumed by the failed note before it errored. + /// + /// This is `Some` when the failure was due to exceeding the cycle limit, and `None` + /// for other error types where the cycle count is not meaningful. + failed_note_cycle_count: Option, }, } @@ -64,7 +75,7 @@ impl From for TransactionExecutorError { match error { TransactionCheckerError::TransactionPreparation(error) => error, TransactionCheckerError::PrologueExecution(error) => error, - TransactionCheckerError::EpilogueExecution(error) => error, + TransactionCheckerError::EpilogueExecution { error, .. } => error, TransactionCheckerError::NoteExecution { error, .. } => error, } } diff --git a/crates/miden-tx/src/executor/mod.rs b/crates/miden-tx/src/executor/mod.rs index 279e5d8330..eb2541faf5 100644 --- a/crates/miden-tx/src/executor/mod.rs +++ b/crates/miden-tx/src/executor/mod.rs @@ -39,6 +39,7 @@ pub use notes_checker::{ MAX_NUM_CHECKER_NOTES, NoteConsumptionChecker, NoteConsumptionInfo, + SuccessfulNote, }; mod program_executor; diff --git a/crates/miden-tx/src/executor/notes_checker.rs b/crates/miden-tx/src/executor/notes_checker.rs index 69b71869e8..54e1bbf490 100644 --- a/crates/miden-tx/src/executor/notes_checker.rs +++ b/crates/miden-tx/src/executor/notes_checker.rs @@ -1,6 +1,7 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; +use miden_processor::ExecutionError; use miden_processor::advice::AdviceInputs; use miden_protocol::account::AccountId; use miden_protocol::block::BlockNumber; @@ -31,37 +32,99 @@ pub const MAX_NUM_CHECKER_NOTES: usize = 20; // NOTE CONSUMPTION INFO // ================================================================================================ +/// Represents a successfully consumed note along with the number of cycles it took to execute. +#[derive(Debug)] +pub struct SuccessfulNote { + note: Note, + num_cycles: usize, +} + +impl SuccessfulNote { + /// Constructs a new `SuccessfulNote`. + pub fn new(note: Note, num_cycles: usize) -> Self { + Self { note, num_cycles } + } + + /// Returns a reference to the note. + pub fn note(&self) -> &Note { + &self.note + } + + /// Returns the number of cycles consumed during execution. + pub fn num_cycles(&self) -> usize { + self.num_cycles + } +} + /// Represents a failed note consumption. #[derive(Debug)] pub struct FailedNote { - pub note: Note, - pub error: TransactionExecutorError, + note: Note, + error: TransactionExecutorError, + /// The number of cycles consumed by the note before it failed. + /// + /// This is `Some` when the failure was due to exceeding the cycle limit, and `None` + /// for other error types where the cycle count is not meaningful. + num_cycles: Option, } impl FailedNote { /// Constructs a new `FailedNote`. - pub fn new(note: Note, error: TransactionExecutorError) -> Self { - Self { note, error } + pub fn new(note: Note, error: TransactionExecutorError, num_cycles: Option) -> Self { + Self { note, error, num_cycles } + } + + /// Returns a reference to the note. + pub fn note(&self) -> &Note { + &self.note + } + + /// Returns a reference to the error. + pub fn error(&self) -> &TransactionExecutorError { + &self.error + } + + /// Returns the number of cycles consumed before failure, if available. + /// + /// This is `Some` when the failure was due to exceeding the cycle limit, and `None` + /// for other error types where the cycle count is not meaningful. + pub fn num_cycles(&self) -> Option { + self.num_cycles } } /// Contains information about the successful and failed consumption of notes. #[derive(Default, Debug)] pub struct NoteConsumptionInfo { - pub successful: Vec, - pub failed: Vec, + successful: Vec, + failed: Vec, } impl NoteConsumptionInfo { /// Creates a new [`NoteConsumptionInfo`] instance with the given successful notes. - pub fn new_successful(successful: Vec) -> Self { + pub fn new_successful(successful: Vec) -> Self { Self { successful, ..Default::default() } } /// Creates a new [`NoteConsumptionInfo`] instance with the given successful and failed notes. - pub fn new(successful: Vec, failed: Vec) -> Self { + pub fn new(successful: Vec, failed: Vec) -> Self { Self { successful, failed } } + + /// Returns a reference to the successfully consumed notes. + pub fn successful(&self) -> &[SuccessfulNote] { + &self.successful + } + + /// Returns a reference to the failed notes. + pub fn failed(&self) -> &[FailedNote] { + &self.failed + } + + /// Consumes the struct and returns the successful and failed notes. + pub fn into_parts(self) -> (Vec, Vec) { + (self.successful, self.failed) + } } // NOTE CONSUMPTION CHECKER @@ -179,7 +242,7 @@ where // try to consume the provided note match self.try_execute_notes(&mut tx_inputs).await { // execution succeeded - Ok(()) => Ok(NoteConsumptionStatus::Consumable), + Ok(_cycle_counts) => Ok(NoteConsumptionStatus::Consumable), Err(tx_checker_error) => { match tx_checker_error { // execution failed on the preparation stage, before we actually executed the tx @@ -195,9 +258,9 @@ where Ok(NoteConsumptionStatus::UnconsumableConditions) }, // execution failed during the epilogue - TransactionCheckerError::EpilogueExecution(epilogue_error) => { - Ok(handle_epilogue_error(epilogue_error)) - }, + TransactionCheckerError::EpilogueExecution { + error: epilogue_error, .. + } => Ok(handle_epilogue_error(epilogue_error)), } }, } @@ -228,15 +291,24 @@ where // Execute the candidate notes. tx_inputs.set_input_notes(candidate_notes.clone()); match self.try_execute_notes(&mut tx_inputs).await { - Ok(()) => { + Ok(cycle_counts) => { // A full set of successful notes has been found. - let successful = candidate_notes; + let successful = candidate_notes + .into_iter() + .zip(cycle_counts) + .map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles)) + .collect(); return Ok(NoteConsumptionInfo::new(successful, failed_notes)); }, - Err(TransactionCheckerError::NoteExecution { failed_note_index, error }) => { + Err(TransactionCheckerError::NoteExecution { + failed_note_index, + error, + failed_note_cycle_count, + .. + }) => { // SAFETY: Failed note index is in bounds of the candidate notes. let failed_note = candidate_notes.remove(failed_note_index); - failed_notes.push(FailedNote::new(failed_note, error)); + failed_notes.push(FailedNote::new(failed_note, error, failed_note_cycle_count)); // All possible candidate combinations have been attempted. if candidate_notes.is_empty() { @@ -244,7 +316,7 @@ where } // Continue and process the next set of candidates. }, - Err(TransactionCheckerError::EpilogueExecution(_)) => { + Err(TransactionCheckerError::EpilogueExecution { .. }) => { let consumption_info = self .find_largest_executable_combination( candidate_notes, @@ -277,6 +349,7 @@ where mut tx_inputs: TransactionInputs, ) -> NoteConsumptionInfo { let mut successful_notes = Vec::new(); + let mut successful_cycle_counts = Vec::new(); let mut failed_note_index = BTreeMap::new(); // Iterate by note count: try 1 note, then 2, then 3, etc. @@ -292,10 +365,12 @@ where tx_inputs.set_input_notes(successful_notes.clone()); match self.try_execute_notes(&mut tx_inputs).await { - Ok(()) => { + Ok(cycle_counts) => { // The successfully added note might have failed earlier. Remove it from the // failed list. failed_note_index.remove(¬e.id()); + // Store the cycle counts from the latest successful execution. + successful_cycle_counts = cycle_counts; // This combination succeeded; remove the most recently added note from // the remaining set. remaining_notes.remove(idx); @@ -306,31 +381,52 @@ where // continue to next note. let failed_note = successful_notes.pop().expect("successful notes should not be empty"); + + // Extract the failed note's cycle count if available. + let num_cycles = match &error { + TransactionCheckerError::NoteExecution { + failed_note_cycle_count, + .. + } => *failed_note_cycle_count, + _ => None, + }; + // Record the failed note (overwrite previous failures for the relevant // note). - failed_note_index - .insert(failed_note.id(), FailedNote::new(failed_note, error.into())); + failed_note_index.insert( + failed_note.id(), + FailedNote::new(failed_note, error.into(), num_cycles), + ); }, } } } + // Pair successful notes with their cycle counts from the last successful execution. + let successful = successful_notes + .into_iter() + .zip(successful_cycle_counts) + .map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles)) + .collect(); + // Append failed notes to the list of failed notes provided as input. failed_notes.extend(failed_note_index.into_values()); - NoteConsumptionInfo::new(successful_notes, failed_notes) + NoteConsumptionInfo::new(successful, failed_notes) } /// Attempts to execute a transaction with the provided input notes. /// /// This method executes the full transaction pipeline including prologue, note execution, - /// and epilogue phases. It returns `Ok(())` if all notes are successfully consumed, - /// or a specific [`NoteExecutionError`] indicating where and why the execution failed. + /// and epilogue phases. It returns `Ok(cycle_counts)` if all notes are successfully consumed + /// (where `cycle_counts` contains the number of cycles for each note), or a specific + /// [`TransactionCheckerError`] indicating where and why the execution failed. The order of the + /// returned `cycle_counts` is guaranteed to match the order of the input notes. async fn try_execute_notes( &self, tx_inputs: &mut TransactionInputs, - ) -> Result<(), TransactionCheckerError> { + ) -> Result, TransactionCheckerError> { if tx_inputs.input_notes().is_empty() { - return Ok(()); + return Ok(Vec::new()); } let (mut host, stack_inputs, advice_inputs) = @@ -347,6 +443,13 @@ where match result { Ok(execution_output) => { + let cycle_counts = host + .tx_progress() + .note_execution() + .iter() + .map(|(_, interval)| interval.len()) + .collect(); + // Set the advice inputs from the successful execution as advice inputs for // reexecution. This avoids calls to the data store (to load data lazily) that have // already been done as part of this execution. @@ -357,7 +460,7 @@ where ..Default::default() }; tx_inputs.set_advice_inputs(advice_inputs); - Ok(()) + Ok(cycle_counts) }, Err(error) => { let notes = host.tx_progress().note_execution(); @@ -372,13 +475,38 @@ where notes.split_last().expect("notes vector is not empty because of earlier check"); // If the interval end of the last note is specified, then an error occurred after - // notes processing. + // notes processing. All notes executed successfully in this case. if last_note_interval.end().is_some() { - Err(TransactionCheckerError::EpilogueExecution(error)) + let successful_notes_cycle_counts = + notes.iter().map(|(_, interval)| interval.len()).collect(); + Err(TransactionCheckerError::EpilogueExecution { + error, + successful_notes_cycle_counts, + }) } else { // Return the index of the failed note. let failed_note_index = success_notes.len(); - Err(TransactionCheckerError::NoteExecution { failed_note_index, error }) + let successful_notes_cycle_counts = + success_notes.iter().map(|(_, interval)| interval.len()).collect(); + + // Compute the failed note's cycle count when the failure was due to + // exceeding the cycle limit. In this case, the note's interval has a + // start but no end, and the total cycles consumed equals the max allowed. + let failed_note_cycle_count = match &error { + TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::CycleLimitExceeded(max_cycles), + ) => last_note_interval + .start() + .map(|start| *max_cycles as usize - usize::from(start)), + _ => None, + }; + + Err(TransactionCheckerError::NoteExecution { + failed_note_index, + error, + successful_notes_cycle_counts, + failed_note_cycle_count, + }) } }, } diff --git a/crates/miden-tx/src/lib.rs b/crates/miden-tx/src/lib.rs index 3dd06bdcc5..e5c670d6f4 100644 --- a/crates/miden-tx/src/lib.rs +++ b/crates/miden-tx/src/lib.rs @@ -16,6 +16,7 @@ pub use executor::{ NoteConsumptionChecker, NoteConsumptionInfo, ProgramExecutor, + SuccessfulNote, TransactionExecutor, TransactionExecutorHost, }; diff --git a/docs/src/account/code.md b/docs/src/account/code.md index ed6d60ab1e..fb2f10132d 100644 --- a/docs/src/account/code.md +++ b/docs/src/account/code.md @@ -38,3 +38,11 @@ Recall that an [account's nonce](index.md#nonce) must be incremented whenever it ### Procedure invocation checks The authentication procedure can base its authentication decision on whether a specific account procedure was called during the transaction. A procedure invocation is tracked by the kernel only if it invokes account-restricted kernel APIs (procedures that are only allowed to be called from the account context, e.g. `exec.faucet::mint`). Invocation of procedures that execute only local instructions (e.g., a noop `push.0 drop`) will not be tracked by the kernel. + +### Reentrancy + +The transaction kernel ensures that an authentication procedure cannot be called by note scripts or transaction scripts before the epilogue. However, it is theoretically possible for an authentication procedure to re-enter itself. + +In practice, most authentication procedures call `native_account::incr_nonce` on all successful execution paths. Since `incr_nonce` can only be called once per transaction, a re-entrant call would abort when attempting to increment the nonce a second time, effectively preventing reentrancy as a side effect. + +If an authentication procedure does not call `incr_nonce` on all successful execution paths, the author should ensure that the procedure does not re-enter itself if this would result in unintended behavior, as the transaction kernel does not enforce this. diff --git a/docs/src/asset.md b/docs/src/asset.md index 8cd17a0299..756e0fdebf 100644 --- a/docs/src/asset.md +++ b/docs/src/asset.md @@ -54,7 +54,7 @@ Non-fungible assets are encoded by hashing the `Asset` data into 32 bytes and pl ### Storage -[Accounts](./account) and [notes](note) have vaults used to store assets. Accounts use a sparse Merkle tree as a vault while notes use a simple list. This enables an account to store a practically unlimited number of assets while a note can only store 255 assets. +[Accounts](./account) and [notes](note) have vaults used to store assets. Accounts use a sparse Merkle tree as a vault while notes use a simple list. This enables an account to store a practically unlimited number of assets while a note can only store up to 64 assets.

Asset storage diff --git a/docs/src/note.md b/docs/src/note.md index cef4107b3b..b806faac84 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -32,7 +32,7 @@ These components are: An [asset](asset) container for a `Note`. ::: -A `Note` can contain from 0 up to 256 different assets. These assets represent fungible or non-fungible tokens, enabling flexible asset transfers. +A `Note` can contain from 0 up to 64 different assets. These assets represent fungible or non-fungible tokens, enabling flexible asset transfers. ### Script diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index 7981bba1d7..d79b4cbe18 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -125,7 +125,9 @@ Note utility procedures can be used to compute the required utility data or writ | `write_assets_to_memory` | Writes the assets data stored in the advice map to the memory specified by the provided destination pointer.

**Inputs:** `[ASSETS_COMMITMENT, num_assets, dest_ptr]`
**Outputs:** `[num_assets, dest_ptr]` | Any | | `build_recipient_hash` | Returns the `RECIPIENT` for a specified `SERIAL_NUM`, `SCRIPT_ROOT`, and storage commitment.

**Inputs:** `[SERIAL_NUM, SCRIPT_ROOT, STORAGE_COMMITMENT]`
**Outputs:** `[RECIPIENT]` | Any | | `build_recipient` | Builds the recipient hash from note storage, script root, and serial number.

**Inputs:** `[storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT]`
**Outputs:** `[RECIPIENT]` | Any | -| `extract_sender_from_metadata` | Extracts the sender ID from the provided metadata word.

**Inputs:** `[METADATA]`
**Outputs:** `[sender_id_suffix, sender_id_prefix]` | Any | +| `metadata_into_sender` | Extracts the sender ID from the provided metadata word.

**Inputs:** `[METADATA]`
**Outputs:** `[sender_id_suffix, sender_id_prefix]` | Any | +| `metadata_into_attachment_info` | Extracts the attachment kind and scheme from the provided metadata header.

**Inputs:** `[METADATA_HEADER]`
**Outputs:** `[attachment_kind, attachment_scheme]` | Any | +| `metadata_into_note_type` | Extracts the note type from the provided metadata header. The note type is encoded as a single bit (0 = Private, 1 = Public).

**Inputs:** `[METADATA_HEADER]`
**Outputs:** `[note_type]` | Any | ## Transaction Procedures (`miden::protocol::tx`) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6744e56e15..a37cb5745c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.90" +channel = "1.94" components = ["clippy", "rust-src", "rustfmt"] profile = "minimal" targets = ["wasm32-unknown-unknown"]