diff --git a/.github/actions/cleanup-runner/action.yml b/.github/actions/cleanup-runner/action.yml new file mode 100644 index 0000000000..22edac443f --- /dev/null +++ b/.github/actions/cleanup-runner/action.yml @@ -0,0 +1,12 @@ +name: 'Cleanup Runner' +description: 'Remove unused tools in the runner image to free disk space' + +runs: + using: 'composite' + steps: + - name: Remove unused tools in the runner image + shell: bash + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force || true + sudo docker builder prune -a --force || true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 772d3cbc8c..8da873adce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: # Only update the cache on push onto the next branch. This strikes a nice balance between @@ -34,31 +36,30 @@ jobs: make build-no-std make build-no-std-testing - feature-check: - name: Check feature combinations + check-features: + name: check all feature combinations runs-on: ubuntu-latest steps: - - uses: actions/checkout@main + - uses: actions/checkout@v4 + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: - # Only update the cache on push onto the next branch. This strikes a nice balance between - # cache hits and cache evictions (github has a 10GB cache limit). save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - # Install cargo-hack or restore from cache if already built. - - uses: taiki-e/cache-cargo-install-action@v2 - with: - tool: cargo-hack - - name: Update Rust toolchain + - name: Install rust run: rustup update --no-self-update - # Run cargo hack check with each feature to ensure crates compile with individual features - - name: Check each feature - run: cargo hack check --each-feature --workspace --exclude bench-prover + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + - name: Check all feature combinations + run: make check-features build-benches: name: Check benchmarks compilation runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: # Only update the cache on push onto the next branch. This strikes a nice balance between diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ca2e209143..c7ffceecce 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,6 +17,17 @@ permissions: contents: read jobs: + cargo-deny: + name: cargo-deny on ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-deny + - name: Check dependencies with cargo-deny + run: cargo deny check + typos: name: spellcheck runs-on: ubuntu-latest @@ -44,6 +55,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: # Only update the cache on push onto the next branch. This strikes a nice balance between @@ -60,6 +73,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: # Only update the cache on push onto the next branch. This strikes a nice balance between @@ -77,6 +92,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: # Only update the cache on push onto the next branch. This strikes a nice balance between @@ -93,6 +110,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: # Only update the cache on push onto the next branch. This strikes a nice balance between @@ -108,6 +127,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - name: Rustup run: rustup update --no-self-update - uses: Swatinem/rust-cache@v2 @@ -122,13 +143,7 @@ jobs: name: check for unused dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@main - - name: Install cargo-machete - uses: clechasseur/rs-cargo@v2 - with: - command: install - args: cargo-machete@0.7.0 + - name: Checkout + uses: actions/checkout@v4 - name: Machete - uses: clechasseur/rs-cargo@v2 - with: - command: machete \ No newline at end of file + uses: bnjbvr/cargo-machete@main diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml deleted file mode 100644 index 2f39de1937..0000000000 --- a/.github/workflows/msrv.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Check MSRV - -on: - push: - branches: [next] - pull_request: - types: [opened, reopened, synchronize] - -# Limits workflow concurrency to only the latest commit in the PR. -concurrency: - group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" - cancel-in-progress: true - -permissions: - contents: read - -jobs: - # Check MSRV (aka `rust-version`) in `Cargo.toml` is valid for workspace members - msrv: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y jq - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Install cargo-msrv - run: cargo install cargo-msrv - - name: Check MSRV for each workspace member - run: | - ./scripts/check-msrv.sh diff --git a/.github/workflows/release-plz-dry-run.yml b/.github/workflows/release-plz-dry-run.yml index b41850da1f..be787b1270 100644 --- a/.github/workflows/release-plz-dry-run.yml +++ b/.github/workflows/release-plz-dry-run.yml @@ -7,6 +7,10 @@ on: push: branches: [main, next] +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: true + jobs: release-plz-dry-run-release: name: Release-plz dry-run @@ -17,9 +21,22 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y jq - name: Update Rust toolchain + run: rustup update --no-self-update + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-binstall + - name: Install cargo-msrv + run: cargo binstall --no-confirm --force cargo-msrv + - name: Check MSRV for each workspace member run: | - rustup update --no-self-update + export PATH="$HOME/.cargo/bin:$PATH" + ./scripts/check-msrv.sh - name: Run release-plz uses: release-plz/action@v0.5 with: diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 78218b7a64..a9de6bdeb8 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -34,9 +34,22 @@ jobs: exit 1 fi echo "Release tag matches main HEAD — continuing." + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y jq - name: Update Rust toolchain + run: rustup update --no-self-update + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-binstall + - name: Install cargo-msrv + run: cargo binstall --no-confirm --force cargo-msrv + - name: Check MSRV for each workspace member run: | - rustup update --no-self-update + export PATH="$HOME/.cargo/bin:$PATH" + ./scripts/check-msrv.sh - name: Run release-plz uses: release-plz/action@v0.5 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 483daf8f7e..f08787e669 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 with: @@ -38,6 +40,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f33a95f77c..d5aa4d6d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## 0.13.0 (2026-01-16) + +### Features + +- [BREAKING] Refactored storage slots to be accessed by names instead of indices ([#1987](https://github.com/0xMiden/miden-base/pull/1987), [#2025](https://github.com/0xMiden/miden-base/pull/2025), [#2149](https://github.com/0xMiden/miden-base/pull/2149), [#2150](https://github.com/0xMiden/miden-base/pull/2150), [#2153](https://github.com/0xMiden/miden-base/pull/2153), [#2154](https://github.com/0xMiden/miden-base/pull/2154), [#2160](https://github.com/0xMiden/miden-base/pull/2160), [#2161](https://github.com/0xMiden/miden-base/pull/2161), [#2170](https://github.com/0xMiden/miden-base/pull/2170)). +- [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)). +- Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). +- Add `read_foreign_account_inputs()`, `read_vault_asset_witnesses()`, and `read_storage_map_witness()` for `TransactionInputs` ([#2246](https://github.com/0xMiden/miden-base/pull/2246)). +- [BREAKING] Introduced `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249), [#2252](https://github.com/0xMiden/miden-base/pull/2252), [#2260](https://github.com/0xMiden/miden-base/pull/2260), [#2268](https://github.com/0xMiden/miden-base/pull/2268), [#2279](https://github.com/0xMiden/miden-base/pull/2279)). +- Added `AccountSchemaCommitment` component to expose account storage schema commitments ([#2253](https://github.com/0xMiden/miden-base/pull/2253)). +- Introduced standard `NetworkAccountTarget` attachment for use in network transactions which replaces `NoteTag::NetworkAccount` ([#2257](https://github.com/0xMiden/miden-base/pull/2257)). +- Added an `AccountBuilder` extension trait to help build the schema commitment; added `AccountComponentMetadata` to `AccountComponent` ([#2269](https://github.com/0xMiden/miden-base/pull/2269)). +- Added `miden::standards::access::ownable` standard module for component ownership management, and integrated it into the `network_fungible` faucet (including new tests). ([#2228](https://github.com/0xMiden/miden-base/pull/2228)). + +### Changes + +- Added proc-macro `WordWrapper` to ease implementation of `Word`-wrapping types ([#2071](https://github.com/0xMiden/miden-base/pull/2108)). +- [BREAKING] Added `BlockBody` and `BlockProof` structs in preparation for validator signatures and deferred block proving ([#2012](https://github.com/0xMiden/miden-base/pull/2012)). +- [BREAKING] Renamed `TransactionEvent` into `TransactionEventId` and split event handling into data extraction and handling logic ([#2071](https://github.com/0xMiden/miden-base/pull/2071)). +- Split tx progress events out into a separate enum ([#2103](https://github.com/0xMiden/miden-base/pull/2103)). +- Added `note::get_network_account_tag` procedure ([#2120](https://github.com/0xMiden/miden-base/pull/2120)). +- [BREAKING] Updated MINT note to support both private and public output note creation ([#2123](https://github.com/0xMiden/miden-base/pull/2123)). +- [BREAKING] Removed `AccountComponentTemplate` in favor of instantiating components via `AccountComponent::from_package` ([#2127](https://github.com/0xMiden/miden-base/pull/2127)). +- [BREAKING] Added public key to, remove proof commitment from, `BlockHeader`, and add signing functionality through `BlockSigner` trait ([#2128](https://github.com/0xMiden/miden-base/pull/2128)). +- [BREAKING] Added fee to `TransactionHeader` ([#2131](https://github.com/0xMiden/miden-base/pull/2131)). +- Created `NullifierLeafValue` newtype wrapper ([#2136](https://github.com/0xMiden/miden-base/pull/2136)). +- [BREAKING] Increased `MAX_INPUTS_PER_NOTE` from 128 to 1024 ([#2139](https://github.com/0xMiden/miden-base/pull/2139)). +- Added the ability to get full public key from `TransactionAuthenticator` ([#2145](https://github.com/0xMiden/miden-base/pull/2145)). +- Added `TokenSymbol::from_static_str` const function for compile-time token symbol validation ([#2148](https://github.com/0xMiden/miden-base/pull/2148)). +- [BREAKING] Migrated to `miden-vm` v0.20 and `miden-crypto` v0.19 ([#2158](https://github.com/0xMiden/miden-base/pull/2158)). +- [BREAKING] Renamed `AccountProcedureInfo` into `AccountProcedureRoot` and remove storage offset and size ([#2162](https://github.com/0xMiden/miden-base/pull/2162)). +- [BREAKING] Made `AccountProcedureIndexMap` construction infallible ([#2163](https://github.com/0xMiden/miden-base/pull/2163)). +- [BREAKING] Renamed `tracked_procedure_roots_slot` to `trigger_procedure_roots_slot` in ACL auth components for naming consistency ([#2166](https://github.com/0xMiden/miden-base/pull/2166)). +- [BREAKING] Refactored `miden-objects` and `miden-lib` into `miden-protocol` and `miden-standards` ([#2184](https://github.com/0xMiden/miden-base/pull/2184), [#2191](https://github.com/0xMiden/miden-base/pull/2191), [#2197](https://github.com/0xMiden/miden-base/pull/2197), [#2255](https://github.com/0xMiden/miden-base/pull/2255)). +- Added `From<&ExecutedTransaction> for TransactionHeader` implementation ([#2178](https://github.com/0xMiden/miden-base/pull/2178)). +- [BREAKING] Refactored `AccountStorageDelta` to use a new `StorageSlotDelta` type ([#2182](https://github.com/0xMiden/miden-base/pull/2182)). +- [BREAKING] Removed OLD_MAP_ROOT from being returned when calling [`native_account::set_map_item`](crates/miden-lib/asm/miden/native_account.masm) ([#2194](https://github.com/0xMiden/miden-base/pull/2194)). +- [BREAKING] Refactored account component templates into `StorageSchema` ([#2193](https://github.com/0xMiden/miden-base/pull/2193)). +- [BREAKING] Refactored account component templates into `AccountStorageSchema` ([#2193](https://github.com/0xMiden/miden-base/pull/2193)). +- [BREAKING] Refactor note tags to be arbitrary `u32` values and drop previous validation ([#2219](https://github.com/0xMiden/miden-base/pull/2219)). +- [BREAKING] Refactored `InitStorageData` to support native types ([#2230](https://github.com/0xMiden/miden-base/pull/2230)). +- Refactored to no longer pad the note inputs on insertion into advice map ([#2232](https://github.com/0xMiden/miden-base/pull/2232)). +- Added `StorageSchema::commitment()` ([#2244](https://github.com/0xMiden/miden-base/pull/2244)). +- [BREAKING] `RpoFalcon512` was renamed to `Falcon512Rpo` everywhere, including procedure and file names ([#2264](https://github.com/0xMiden/miden-base/pull/2264)). +- [BREAKING] Removed top-level error exports from `miden-protocol` crate (the are still accessible under `miden_protocol::errors`). + ## 0.12.4 (2025-11-26) - Added the standard library's precompile registry to `TransactionVerifier` ([#2116](https://github.com/0xMiden/miden-base/pull/2116)). @@ -8,6 +54,8 @@ - Added `ecdsa_k256_keccak::PublicKey` as a valid template type ([#2097](https://github.com/0xMiden/miden-base/pull/2097)). - [BREAKING] Fix advice inputs in transaction inputs not being propagated through ([#2099](https://github.com/0xMiden/miden-base/pull/2099)). +- Add `S` generic to `NullifierTree` to allow usage with `LargeSmt`s ([#1353](https://github.com/0xMiden/miden-node/issues/1353)). +- [BREAKING] Pre-fetch note and fee asset witnesses before transaction execution ([#2113](https://github.com/0xMiden/miden-base/pull/2113)). ## 0.12.2 (2025-11-12) @@ -19,10 +67,10 @@ ## 0.12.1 (2025-11-06) -- Fixed incorrect detection of note inputs length during note creation ([#2066](https://github.com/0xMiden/miden-base/pull/2066)). - Made `InitStorageData::map_entries()` public ([#2055](https://github.com/0xMiden/miden-base/pull/2055)). - Enabled handling of empty maps in account component templates ([#2056](https://github.com/0xMiden/miden-base/pull/2056)). - Changed auth components to increment nonce if it is zero ([#2060](https://github.com/0xMiden/miden-base/pull/2060)). +- Fixed incorrect detection of note inputs length during note creation ([#2066](https://github.com/0xMiden/miden-base/pull/2066)). ## 0.12.0 (2025-11-05) @@ -55,9 +103,9 @@ - Added `account::get_initial_balance` procedure to `miden` lib ([#1959](https://github.com/0xMiden/miden-base/pull/1959)). - [BREAKING] Changed `Account` to `PartialAccount` conversion to generally track only minimal data ([#1963](https://github.com/0xMiden/miden-base/pull/1963)). - Added `MastArtifact`, `PackageExport`, `PackageManifest`, `AttributeSet`, `QualifiedProcedureName`, `Section` and `SectionId` to re-export section ([#1984](https://github.com/0xMiden/miden-base/pull/1984) and [#2015](https://github.com/0xMiden/miden-base/pull/2015)). -- [BREAKING] Enable computing the transaction ID from the data in a `TransactionHeader` ([#1973](https://github.com/0xMiden/miden-base/pull/1973)). +- [BREAKING] Enable computing the transaction ID from the data in a `TransactionHeader` ([#1973]https://github.com/0xMiden/miden-base/pull/1973). +- [BREAKING] Introduce `VaultKey` newtype wrapper for asset vault keys ([#1978]https://github.com/0xMiden/miden-base/pull/1978). - [BREAKING] Introduce `AssetVaultKey` newtype wrapper for asset vault keys ([#1978](https://github.com/0xMiden/miden-base/pull/1978), [#2024](https://github.com/0xMiden/miden-base/pull/2024)). -- [BREAKING] Change `Account` to `PartialAccount` conversion to generally track only minimal data ([#1963](https://github.com/0xMiden/miden-base/pull/1963)). - Added `network_fungible_faucet` and `MINT` & `BURN` notes ([#1925](https://github.com/0xMiden/miden-base/pull/1925)) - Removed `create_p2id_note` and `create_p2any_note` methods from `MockChainBuilder`, users should use `add_p2id_note` and `add_p2any_note` instead ([#1990](https://github.com/0xMiden/miden-base/issues/1990)). - [BREAKING] Introduced `AuthScheme` and `PublicKey` enums in `miden-objects::account::auth` module ([#1994](https://github.com/0xMiden/miden-base/pull/1994)). diff --git a/Cargo.lock b/Cargo.lock index abd03afadd..f13f1556f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -166,28 +156,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -226,15 +194,15 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bech32" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bench-note-checker" @@ -242,8 +210,8 @@ version = "0.1.0" dependencies = [ "anyhow", "criterion 0.6.0", - "miden-lib", - "miden-objects", + "miden-protocol", + "miden-standards", "miden-testing", "miden-tx", "serde", @@ -257,12 +225,10 @@ version = "0.1.0" dependencies = [ "anyhow", "criterion 0.6.0", - "miden-lib", - "miden-objects", - "miden-processor", + "miden-protocol", + "miden-standards", "miden-testing", "miden-tx", - "rand_chacha", "serde", "serde_json", "tokio", @@ -297,15 +263,16 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", ] [[package]] @@ -319,9 +286,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -330,10 +297,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] -name = "bytes" -version = "1.11.0" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cast" @@ -343,9 +310,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -423,18 +390,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstyle", "clap_lex", @@ -442,9 +409,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "color-eyre" @@ -487,9 +454,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "cpufeatures" @@ -638,7 +605,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -662,22 +629,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "proc-macro2", "quote", - "syn", + "rustc_version 0.4.1", + "syn 2.0.114", ] [[package]] @@ -771,18 +739,6 @@ dependencies = [ "log", ] -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "env_filter" version = "0.1.4" @@ -823,7 +779,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -876,9 +832,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "findshlibs" @@ -892,6 +848,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -918,9 +883,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "fs-err" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", ] @@ -969,7 +934,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1007,16 +972,17 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" dependencies = [ "cc", "cfg-if", "libc", "log", "rustversion", - "windows", + "windows-link", + "windows-result", ] [[package]] @@ -1032,9 +998,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1093,15 +1059,16 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash", "rayon", "serde", + "serde_core", ] [[package]] @@ -1110,6 +1077,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1136,9 +1109,9 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -1223,15 +1196,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", @@ -1242,13 +1215,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1263,9 +1236,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1332,9 +1305,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" @@ -1365,9 +1338,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -1379,7 +1352,7 @@ dependencies = [ "generator", "scoped-tls", "tracing", - "tracing-subscriber 0.3.20", + "tracing-subscriber 0.3.22", ] [[package]] @@ -1406,11 +1379,27 @@ dependencies = [ "libc", ] +[[package]] +name = "miden-agglayer" +version = "0.13.0" +dependencies = [ + "fs-err", + "miden-agglayer", + "miden-assembly", + "miden-core", + "miden-core-lib", + "miden-protocol", + "miden-standards", + "miden-utils-sync", + "regex", + "walkdir", +] + [[package]] name = "miden-air" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06acfd2ddc25b68f9d23d2add3f15c0ec3f9890ce6418409d71bea9dc6590bd0" +checksum = "3d819876b9e9b630e63152400e6df2a201668a9bdfd33d54d6806b9d7b992ff8" dependencies = [ "miden-core", "miden-utils-indexing", @@ -1421,10 +1410,11 @@ dependencies = [ [[package]] name = "miden-assembly" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219b9e48bb286b58a23bb65cf74baa1b24ddbcb462ca625b38186674571047" +checksum = "24c6a18e29c03141cf9044604390a00691c7342924ec865b4acfdd560ff41ede" dependencies = [ + "env_logger", "log", "miden-assembly-syntax", "miden-core", @@ -1435,11 +1425,12 @@ dependencies = [ [[package]] name = "miden-assembly-syntax" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eeaef2853061c54527bb2664c0c832ce3d1f80847c79512455fec3b93057f2a" +checksum = "7458ff670f5a514bf972aa84d6e1851a4c4e9afa351f53b71bdc2218b99254b6" dependencies = [ "aho-corasick", + "env_logger", "lalrpop", "lalrpop-util", "log", @@ -1448,6 +1439,7 @@ dependencies = [ "miden-utils-diagnostics", "midenc-hir-type", "proptest", + "proptest-derive", "regex", "rustc_version 0.4.1", "semver 1.0.27", @@ -1457,40 +1449,61 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.12.4" +version = "0.13.0" dependencies = [ - "miden-lib", - "miden-objects", + "miden-protocol", "thiserror", ] [[package]] name = "miden-core" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "452a00429d05c416001ec0578291eb88e115cf94fc22b3308267abfdcd813440" +checksum = "21a5c9c8c3d42ae8381ed49e47ff9ad2d2e345c4726761be36b7d4000ebb40ae" dependencies = [ - "enum_dispatch", + "derive_more", + "itertools 0.14.0", "miden-crypto", "miden-debug-types", "miden-formatting", + "miden-utils-core-derive", "miden-utils-indexing", "num-derive", "num-traits", + "proptest", + "proptest-derive", "thiserror", "winter-math", "winter-utils", ] +[[package]] +name = "miden-core-lib" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6556494ea5576803730fa15015bee6bd9d1a117450f22e7df0883421e7423674" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-core", + "miden-crypto", + "miden-processor", + "miden-utils-sync", + "sha2", + "thiserror", +] + [[package]] name = "miden-crypto" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb82051002f9c64878d3b105a7b924de1ee92019231923380cf4ecd7b824f9a" +checksum = "dc7981c1d907bb9864e24f2bd6304c4fca03a41fc4606c09edd6a7f5a8fc80fc" dependencies = [ "blake3", "cc", "chacha20poly1305", + "curve25519-dalek", "ed25519-dalek", "flume", "glob", @@ -1502,9 +1515,10 @@ dependencies = [ "num-complex", "rand", "rand_chacha", - "rand_core 0.9.3", + "rand_core 0.9.5", "rand_hc", "rayon", + "sha2", "sha3", "subtle", "thiserror", @@ -1516,19 +1530,19 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2222f37355ea975f40acd3c098a437574a31a4d8a2c193cf4e9fead2beede577" +checksum = "83479e7af490784c6f2d2e02cec5210fd6e5bc6ce3d4427734e36a773bca72d2" dependencies = [ "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "miden-debug-types" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eed62ac0ca7420e49148fd306c74786b23a8d31df6da6277c671ba3e5c619a" +checksum = "19123e896f24b575e69921a79a39a0a4babeb98404a8601017feb13b75d653b3" dependencies = [ "memchr", "miden-crypto", @@ -1551,30 +1565,11 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "miden-lib" -version = "0.12.4" -dependencies = [ - "Inflector", - "anyhow", - "assert_matches", - "fs-err", - "miden-assembly", - "miden-core", - "miden-objects", - "miden-processor", - "miden-stdlib", - "rand", - "regex", - "thiserror", - "walkdir", -] - [[package]] name = "miden-mast-package" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d13e6ba2b357551598f13396ed52f8f21aa99979aa3b338bb5521feeda19c8a" +checksum = "f0d6a322b91efa1bb71e224395ca1fb9ca00e2614f89427e35d8c42a903868a3" dependencies = [ "derive_more", "miden-assembly-syntax", @@ -1605,7 +1600,7 @@ dependencies = [ "supports-color", "supports-hyperlinks", "supports-unicode", - "syn", + "syn 2.0.114", "terminal_size", "textwrap", "thiserror", @@ -1621,69 +1616,83 @@ checksum = "86a905f3ea65634dd4d1041a4f0fd0a3e77aa4118341d265af1a94339182222f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] -name = "miden-objects" -version = "0.12.4" +name = "miden-processor" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a659fac55de14647e2695f03d96b83ff94fe65fd31e74d81c225ec52af25acf" +dependencies = [ + "itertools 0.14.0", + "miden-air", + "miden-core", + "miden-debug-types", + "miden-utils-diagnostics", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", + "tokio", + "tracing", + "winter-prover", +] + +[[package]] +name = "miden-protocol" +version = "0.13.0" dependencies = [ "anyhow", "assert_matches", "bech32", "color-eyre", "criterion 0.5.1", + "fs-err", "getrandom 0.3.4", - "log", "miden-air", "miden-assembly", "miden-assembly-syntax", "miden-core", + "miden-core-lib", "miden-crypto", "miden-mast-package", - "miden-objects", "miden-processor", - "miden-stdlib", + "miden-protocol", + "miden-protocol-macros", "miden-utils-sync", "miden-verifier", "pprof", "rand", + "rand_chacha", "rand_xoshiro", + "regex", "rstest", "semver 1.0.27", "serde", "tempfile", "thiserror", "toml", + "walkdir", "winter-air", "winter-rand-utils", ] [[package]] -name = "miden-processor" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ef77929651b8755965cde8f589bd38e2345a619d54cab6427f91aa23c47f6a" +name = "miden-protocol-macros" +version = "0.13.0" dependencies = [ - "itertools 0.14.0", - "miden-air", - "miden-core", - "miden-debug-types", - "miden-utils-diagnostics", - "miden-utils-indexing", - "paste", - "rayon", - "thiserror", - "tokio", - "tracing", - "winter-prover", + "miden-protocol", + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] name = "miden-prover" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c30a5d10baeec17b9336de8544cb7f9b96b32de757c4cfb8d95ee0521bb5cd" +checksum = "4e5df61f50f27886f6f777d6e0cdf785f7db87dd881799a84a801e7330c189c8" dependencies = [ "miden-air", "miden-debug-types", @@ -1694,38 +1703,46 @@ dependencies = [ ] [[package]] -name = "miden-stdlib" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e90a5de45a1e6213ff17b66fff8accde0bbc64264e2c22bbcb9a895f8f3b767" +name = "miden-standards" +version = "0.13.0" dependencies = [ - "env_logger", + "anyhow", + "assert_matches", "fs-err", "miden-assembly", "miden-core", - "miden-crypto", + "miden-core-lib", "miden-processor", - "miden-utils-sync", + "miden-protocol", + "miden-standards", + "rand", + "regex", "thiserror", + "walkdir", ] [[package]] name = "miden-testing" -version = "0.12.4" +version = "0.13.0" dependencies = [ "anyhow", "assert_matches", + "hex", "itertools 0.14.0", + "miden-agglayer", + "miden-assembly", "miden-block-prover", - "miden-lib", - "miden-objects", + "miden-core-lib", + "miden-crypto", "miden-processor", + "miden-protocol", + "miden-standards", "miden-tx", "miden-tx-batch-prover", + "primitive-types", "rand", "rand_chacha", "rstest", - "thiserror", "tokio", "winter-rand-utils", "winterfell", @@ -1733,36 +1750,45 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.12.4" +version = "0.13.0" dependencies = [ "anyhow", "assert_matches", "miden-assembly", - "miden-lib", - "miden-objects", "miden-processor", + "miden-protocol", "miden-prover", + "miden-standards", "miden-tx", "miden-verifier", - "rand", "rstest", "thiserror", - "tokio", ] [[package]] name = "miden-tx-batch-prover" -version = "0.12.4" +version = "0.13.0" dependencies = [ - "miden-objects", + "miden-protocol", "miden-tx", ] +[[package]] +name = "miden-utils-core-derive" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa207ffd8b26a79d9b5b246a352812f0015c0bb8f75492ec089c5c8e6d5f9e2b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "miden-utils-diagnostics" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3ff4c019d96539a7066626efb4dce5c9fb7b0e44e961b0c2571e78f34236d5" +checksum = "6b2f55477d410542a5d8990ca04856adf5bef91bfa3b54ca3c03a5ff14a6e25c" dependencies = [ "miden-crypto", "miden-debug-types", @@ -1773,18 +1799,18 @@ dependencies = [ [[package]] name = "miden-utils-indexing" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c798250bee4e856d4f18c161e91cdcbef1906f6614d00cf0063b47031c0f8cc6" +checksum = "f39efae17e14ec8f8a1266cffd29eb7a08ac837143cd09223b1af361bbb55730" dependencies = [ "thiserror", ] [[package]] name = "miden-utils-sync" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feebe7d896c013ea74dbc98de978836606356a044d4ed3b61ded54d3b319d89f" +checksum = "da7fa8f5fd27f122c83f55752f2a964bbfc2b713de419e9c152f7dcc05c194ec" dependencies = [ "lock_api", "loom", @@ -1793,9 +1819,9 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f8e47b78bba1fe1b31faee8f12aafd95385f6d6a8b108b03e92f5d743bb29f" +checksum = "fbddac2e76486fb657929338323c68b9e7f40e33b8cfb593d0fb5bf637db046e" dependencies = [ "miden-air", "miden-core", @@ -1830,7 +1856,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -1900,7 +1926,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2019,7 +2045,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2110,9 +2136,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -2161,6 +2187,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "primitive-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721a1da530b5a2633218dc9f75713394c983c352be88d2d7c9ee85e2c4c21794" +dependencies = [ + "fixed-hash", + "uint", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2172,9 +2208,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -2194,6 +2230,17 @@ dependencies = [ "unarray", ] +[[package]] +name = "proptest-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6dc647500e84a25a85b100e76c85b8ace114c209432dc174f20aac11d4ed6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "quick-xml" version = "0.26.0" @@ -2205,9 +2252,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -2225,7 +2272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2235,7 +2282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2244,14 +2291,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2271,7 +2318,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2280,7 +2327,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2391,15 +2438,15 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn", + "syn 2.0.114", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc_version" @@ -2434,9 +2481,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -2451,12 +2498,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -2544,28 +2585,28 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "indexmap", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -2674,6 +2715,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "str_stack" version = "0.1.0" @@ -2718,9 +2765,9 @@ dependencies = [ [[package]] name = "supports-hyperlinks" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "supports-unicode" @@ -2730,9 +2777,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "symbolic-common" -version = "12.16.3" +version = "12.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03f433c9befeea460a01d750e698aa86caf86dcfbd77d552885cd6c89d52f50" +checksum = "520cf51c674f8b93d533f80832babe413214bb766b6d7cb74ee99ad2971f8467" dependencies = [ "debugid", "memmap2", @@ -2742,9 +2789,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.16.3" +version = "12.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d359ef6192db1760a34321ec4f089245ede4342c27e59be99642f12a859de8" +checksum = "9f0de2ee0ffa2641e17ba715ad51d48b9259778176517979cb38b6aa86fa7425" dependencies = [ "rustc-demangle", "symbolic-common", @@ -2752,9 +2799,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2769,22 +2827,22 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] [[package]] name = "term" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ "windows-sys 0.61.2", ] @@ -2836,7 +2894,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2860,9 +2918,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "pin-project-lite", "tokio-macros", @@ -2876,14 +2934,14 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2892,12 +2950,10 @@ dependencies = [ [[package]] name = "tokio-test" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" dependencies = [ - "async-stream", - "bytes", "futures-core", "tokio", "tokio-stream", @@ -2905,9 +2961,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "indexmap", "serde_core", @@ -2920,18 +2976,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", @@ -2941,24 +2997,24 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2967,20 +3023,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3020,9 +3076,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -3058,6 +3114,18 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" @@ -3112,9 +3180,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", "wasm-bindgen", @@ -3159,18 +3227,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -3181,9 +3249,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3191,31 +3259,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -3252,112 +3320,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core", - "windows-link 0.1.3", -] - [[package]] name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3384,7 +3359,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3418,15 +3393,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3519,9 +3485,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -3578,7 +3544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d31a19dae58475d019850e25b0170e94b16d382fbf6afee9c0e80fdc935e73e" dependencies = [ "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3641,9 +3607,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "x25519-dalek" @@ -3657,22 +3623,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -3680,3 +3646,9 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/Cargo.toml b/Cargo.toml index adad727e05..7b3b758c32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,11 @@ members = [ "bin/bench-note-checker", "bin/bench-transaction", + "crates/miden-agglayer", "crates/miden-block-prover", - "crates/miden-lib", - "crates/miden-objects", + "crates/miden-protocol", + "crates/miden-protocol-macros", + "crates/miden-standards", "crates/miden-testing", "crates/miden-tx", "crates/miden-tx-batch-prover", @@ -19,7 +21,7 @@ homepage = "https://miden.xyz" license = "MIT" repository = "https://github.com/0xMiden/miden-base" rust-version = "1.90" -version = "0.12.4" +version = "0.13.0" [profile.release] codegen-units = 1 @@ -40,30 +42,34 @@ lto = true [workspace.dependencies] # Workspace crates -miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.12" } -miden-lib = { default-features = false, path = "crates/miden-lib", version = "0.12" } -miden-objects = { default-features = false, path = "crates/miden-objects", version = "0.12" } -miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.12" } -miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.12" } -miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.12" } +miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.13" } +miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.13" } +miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.13" } +miden-protocol-macros = { default-features = false, path = "crates/miden-protocol-macros", version = "0.13" } +miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.13" } +miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.13" } +miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.13" } +miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.13" } # Miden dependencies -miden-air = { default-features = false, version = "0.19" } -miden-assembly = { default-features = false, version = "0.19" } -miden-assembly-syntax = { default-features = false, version = "0.19" } -miden-core = { default-features = false, version = "0.19" } -miden-crypto = { default-features = false, version = "0.18.2" } -miden-mast-package = { default-features = false, version = "0.19" } -miden-processor = { default-features = false, version = "0.19" } -miden-prover = { default-features = false, version = "0.19" } -miden-stdlib = { default-features = false, version = "0.19.1" } -miden-utils-sync = { default-features = false, version = "0.19" } -miden-verifier = { default-features = false, version = "0.19" } +miden-air = { default-features = false, version = "0.20" } +miden-assembly = { default-features = false, version = "0.20" } +miden-assembly-syntax = { default-features = false, version = "0.20" } +miden-core = { default-features = false, version = "0.20" } +miden-core-lib = { default-features = false, version = "0.20" } +miden-crypto = { default-features = false, version = "0.19" } +miden-mast-package = { default-features = false, version = "0.20" } +miden-processor = { default-features = false, version = "0.20" } +miden-prover = { default-features = false, version = "0.20" } +miden-utils-sync = { default-features = false, version = "0.20" } +miden-verifier = { default-features = false, version = "0.20" } # External dependencies -anyhow = { default-features = false, features = ["backtrace", "std"], version = "1.0" } -assert_matches = { default-features = false, version = "1.5" } -rand = { default-features = false, version = "0.9" } -rstest = { version = "0.26" } -thiserror = { default-features = false, version = "2.0" } -tokio = { default-features = false, features = ["sync"], version = "1" } +anyhow = { default-features = false, features = ["backtrace", "std"], version = "1.0" } +assert_matches = { default-features = false, version = "1.5" } +primitive-types = { default-features = false, version = "0.14" } +rand = { default-features = false, version = "0.9" } +rand_chacha = { default-features = false, version = "0.9" } +rstest = { version = "0.26" } +thiserror = { default-features = false, version = "2.0" } +tokio = { default-features = false, features = ["sync"], version = "1" } diff --git a/Makefile b/Makefile index 2af8b9b5ab..10f3f8823a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: WARNINGS=RUSTDOCFLAGS="-D warnings" # Enable file generation in the `src` directory. -# This is used in the build scripts of miden-lib. +# This is used in the build scripts of miden-protocol and miden-standards. BUILD_GENERATED_FILES_IN_SRC=BUILD_GENERATED_FILES_IN_SRC=1 # Enable backtraces for tests where we return an anyhow::Result. If enabled, anyhow::Error will # then contain a `Backtrace` and print it when a test returns an error. @@ -60,6 +60,7 @@ lint: ## Runs all linting tasks at once (Clippy, fixing, formatting, typos) @$(BUILD_GENERATED_FILES_IN_SRC) $(MAKE) clippy-no-std @$(BUILD_GENERATED_FILES_IN_SRC) $(MAKE) typos-check @$(BUILD_GENERATED_FILES_IN_SRC) $(MAKE) toml + cargo machete # --- docs ---------------------------------------------------------------------------------------- @@ -107,6 +108,11 @@ check: ## Check all targets and features for errors without code generation check-no-std: ## Check the no-std target without any features for errors without code generation $(BUILD_GENERATED_FILES_IN_SRC) cargo check --no-default-features --target wasm32-unknown-unknown --workspace --lib + +.PHONY: check-features +check-features: ## Checks all feature combinations compile without warnings using cargo-hack + @scripts/check-features.sh + # --- building ------------------------------------------------------------------------------------ .PHONY: build diff --git a/README.md b/README.md index 394bf72716..df78fec0f4 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ If you want to join the technical discussion or learn more about the project, pl ## Status and features -Miden is currently on release v0.12. This is an early version of the protocol and its components. We expect to keep making changes (including breaking changes) to all components. +Miden is currently on release v0.13. This is an early version of the protocol and its components. We expect to keep making changes (including breaking changes) to all components. ### Feature highlights @@ -46,8 +46,8 @@ Miden is currently on release v0.12. This is an early version of the protocol an | Crate | Description | | ------------------------------- | ------------------------------------------------------------------------------- | -| [objects](crates/miden-objects) | Contains core components defining the Miden rollup protocol. | -| [miden-lib](crates/miden-lib) | Contains the code of the Miden rollup kernels and standardized smart contracts. | +| [miden-protocol](crates/miden-protocol) | Contains core components defining the Miden protocol, including the transaction kernel. | +| [miden-standards](crates/miden-standards) | Contains the code of Miden's standardized smart contracts. | | [miden-tx](crates/miden-tx) | Contains tool for creating, executing, and proving Miden rollup transaction. | | [bench-tx](bin/bench-tx) | Contains transaction execution and proving benchmarks. | diff --git a/bin/bench-note-checker/Cargo.toml b/bin/bench-note-checker/Cargo.toml index 9496b74ecb..630d1b9189 100644 --- a/bin/bench-note-checker/Cargo.toml +++ b/bin/bench-note-checker/Cargo.toml @@ -12,10 +12,10 @@ version = "0.1.0" [dependencies] # Workspace dependencies -miden-lib = { workspace = true } -miden-objects = { features = ["testing"], workspace = true } -miden-testing = { workspace = true } -miden-tx = { workspace = true } +miden-protocol = { features = ["testing"], workspace = true } +miden-standards = { workspace = true } +miden-testing = { workspace = true } +miden-tx = { workspace = true } # External dependencies anyhow = { workspace = true } diff --git a/bin/bench-note-checker/src/lib.rs b/bin/bench-note-checker/src/lib.rs index 36761b03e6..cb08ab832e 100644 --- a/bin/bench-note-checker/src/lib.rs +++ b/bin/bench-note-checker/src/lib.rs @@ -1,12 +1,12 @@ -use miden_lib::testing::note::NoteBuilder; -use miden_objects::account::AccountId; -use miden_objects::asset::FungibleAsset; -use miden_objects::crypto::rand::RpoRandomCoin; -use miden_objects::note::{Note, NoteType}; -use miden_objects::testing::account_id::{ +use miden_protocol::account::AccountId; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::crypto::rand::RpoRandomCoin; +use miden_protocol::note::{Note, NoteType}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER, }; +use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain, TxContextInput}; use miden_tx::auth::UnreachableAuth; use miden_tx::{NoteConsumptionChecker, TransactionExecutor}; diff --git a/bin/bench-transaction/Cargo.toml b/bin/bench-transaction/Cargo.toml index a269825112..9b07fccd84 100644 --- a/bin/bench-transaction/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -17,20 +17,16 @@ path = "src/time_counting_benchmarks/prove.rs" [dependencies] # Workspace dependencies -miden-lib = { workspace = true } -miden-objects = { features = ["testing"], workspace = true } -miden-testing = { workspace = true } -miden-tx = { workspace = true } - -# Miden dependencies -miden-processor = { workspace = true } +miden-protocol = { features = ["testing"], workspace = true } +miden-standards = { workspace = true } +miden-testing = { workspace = true } +miden-tx = { workspace = true } # External dependencies -anyhow = { workspace = true } -rand_chacha = { default-features = false, version = "0.9" } -serde = { features = ["derive"], version = "1.0" } -serde_json = { features = ["preserve_order"], package = "serde_json", version = "1.0" } -tokio = { features = ["macros", "rt"], workspace = true } +anyhow = { workspace = true } +serde = { features = ["derive"], version = "1.0" } +serde_json = { features = ["preserve_order"], package = "serde_json", version = "1.0" } +tokio = { features = ["macros", "rt"], workspace = true } [dev-dependencies] criterion = { features = ["async_tokio", "html_reports"], version = "0.6" } diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs index d784ef52ae..d0ac16fce2 100644 --- a/bin/bench-transaction/src/context_setups.rs +++ b/bin/bench-transaction/src/context_setups.rs @@ -1,10 +1,10 @@ use anyhow::Result; -use miden_lib::utils::ScriptBuilder; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::ACCOUNT_ID_SENDER; -use miden_objects::transaction::OutputNote; -use miden_objects::{Felt, Word}; +use miden_protocol::Word; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::note::NoteType; +use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; +use miden_protocol::transaction::OutputNote; +use miden_standards::code_builder::CodeBuilder; use miden_testing::{Auth, MockChain, TransactionContext}; /// Returns the transaction context which could be used to run the transaction which creates a @@ -25,22 +25,20 @@ pub fn tx_create_single_p2id_note() -> Result { let tx_note_creation_script = format!( " - use.miden::output_note - use.std::sys + use miden::protocol::output_note + use miden::core::sys begin # create an output note with fungible asset push.{RECIPIENT} - push.{note_execution_hint} push.{note_type} - push.0 # aux push.{tag} - call.output_note::create + exec.output_note::create # => [note_idx] # move the asset to the note push.{asset} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw # => [note_idx] @@ -49,13 +47,12 @@ pub fn tx_create_single_p2id_note() -> Result { end ", RECIPIENT = output_note.recipient().digest(), - note_execution_hint = Felt::from(output_note.metadata().execution_hint()), note_type = NoteType::Public as u8, tag = output_note.metadata().tag(), asset = Word::from(fungible_asset), ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_note_creation_script)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_note_creation_script)?; // construct the transaction context mock_chain diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 1a5865ad1f..49a916c390 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -5,7 +5,7 @@ use std::fs::{read_to_string, write}; use std::path::Path; use anyhow::Context; -use miden_objects::transaction::TransactionMeasurements; +use miden_protocol::transaction::TransactionMeasurements; use serde::Serialize; use serde_json::{Value, from_str, to_string_pretty}; diff --git a/bin/bench-transaction/src/main.rs b/bin/bench-transaction/src/main.rs index c036936eea..651be7993b 100644 --- a/bin/bench-transaction/src/main.rs +++ b/bin/bench-transaction/src/main.rs @@ -3,7 +3,7 @@ use std::io::Write; use std::path::Path; use anyhow::{Context, Result}; -use miden_objects::transaction::TransactionMeasurements; +use miden_protocol::transaction::TransactionMeasurements; mod context_setups; use context_setups::{ diff --git a/bin/bench-transaction/src/time_counting_benchmarks/prove.rs b/bin/bench-transaction/src/time_counting_benchmarks/prove.rs index 857fb57121..5dafb4604d 100644 --- a/bin/bench-transaction/src/time_counting_benchmarks/prove.rs +++ b/bin/bench-transaction/src/time_counting_benchmarks/prove.rs @@ -4,7 +4,7 @@ use std::time::Duration; use anyhow::Result; use bench_transaction::context_setups::{tx_consume_single_p2id_note, tx_consume_two_p2id_notes}; use criterion::{BatchSize, Criterion, SamplingMode, criterion_group, criterion_main}; -use miden_objects::transaction::{ExecutedTransaction, ProvenTransaction}; +use miden_protocol::transaction::{ExecutedTransaction, ProvenTransaction}; use miden_tx::LocalTransactionProver; // BENCHMARK NAMES diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml new file mode 100644 index 0000000000..0bae5deba1 --- /dev/null +++ b/crates/miden-agglayer/Cargo.toml @@ -0,0 +1,44 @@ +[package] +authors.workspace = true +categories = ["no-std"] +description = "Agglayer components for the Miden protocol" +edition.workspace = true +homepage.workspace = true +keywords = ["agglayer", "miden"] +license.workspace = true +name = "miden-agglayer" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lib] +bench = false + +[features] +default = ["std"] +std = ["miden-assembly/std", "miden-core/std"] +testing = ["miden-protocol/testing"] + +[dependencies] +# Miden dependencies +miden-assembly = { workspace = true } +miden-core = { workspace = true } +miden-protocol = { workspace = true } +miden-standards = { workspace = true } +miden-utils-sync = { workspace = true } + +[dev-dependencies] +miden-agglayer = { features = ["testing"], path = "." } + +[build-dependencies] +fs-err = { version = "3" } +miden-assembly = { workspace = true } +miden-core = { workspace = true } +miden-core-lib = { workspace = true } +miden-protocol = { features = ["testing"], workspace = true } +miden-standards = { workspace = true } +regex = { version = "1.11" } +walkdir = { version = "2.5" } + +[package.metadata.cargo-machete] +ignored = ["miden-core-lib", "miden-standards"] diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm new file mode 100644 index 0000000000..4c12783065 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -0,0 +1,260 @@ +use miden::agglayer::bridge_in +use miden::agglayer::asset_conversion +use miden::protocol::active_account +use miden::protocol::active_note +use miden::standards::faucets +use miden::protocol::note +use miden::protocol::tx +use miden::core::mem + + +# CONSTANTS +# ================================================================================================= + +# The slot in this component's storage layout where the bridge account ID is stored. +const BRIDGE_ID_SLOT = word("miden::agglayer::faucet") + +const PROOF_DATA_WORD_LEN = 134 +const LEAF_DATA_WORD_LEN = 6 +const OUTPUT_NOTE_DATA_WORD_LEN = 2 + +const PROOF_DATA_START_PTR = 0 +const LEAF_DATA_START_PTR = 536 +const OUTPUT_NOTE_DATA_START_PTR = 568 + +# Memory Addresses +const PROOF_DATA_KEY_MEM_ADDR = 700 +const LEAF_DATA_KEY_MEM_ADDR = 704 +const OUTPUT_NOTE_DATA_MEM_ADDR = 708 +const CLAIM_NOTE_DATA_MEM_ADDR = 712 + +const OUTPUT_NOTE_INPUTS_MEM_ADDR = 0 +const OUTPUT_NOTE_TAG_MEM_ADDR = 574 +const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 548 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 552 + +# P2ID output note constants +const P2ID_SCRIPT_ROOT = [13362761878458161062, 15090726097241769395, 444910447169617901, 3558201871398422326] +const P2ID_NOTE_NUM_INPUTS = 2 +const OUTPUT_NOTE_TYPE_PUBLIC = 1 +const EXECUTION_HINT_ALWAYS = 1 +const OUTPUT_NOTE_AUX = 0 + +const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 +# ERRORS +# ================================================================================================= + +const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" + +#! Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY] +#! Outputs: [] +#! +#! Panics if: +#! - the bridge account ID is not properly configured in storage. +#! - the foreign procedure invocation fails. +#! - the claim proof validation fails. +#! +#! Invocation: exec +proc validate_claim + # Get bridge_in::check_claim_proof procedure MAST root + procref.bridge_in::check_claim_proof + # => [BRIDGE_PROC_MAST_ROOT] + + push.BRIDGE_ID_SLOT[0..2] + # => [bridge_id_idx, BRIDGE_PROC_MAST_ROOT] + + # Get Bridge AccountId + exec.active_account::get_item + # => [bridge_account_id_prefix, bridge_account_id_suffix, 0, 0, BRIDGE_PROC_MAST_ROOT] + + movup.2 drop movup.2 drop + # => [bridge_account_id_prefix, bridge_account_id_suffix, BRIDGE_PROC_MAST_ROOT] + + # Call check_claim_proof procedure on Bridge + # Calling: bridge_in::check_claim_proof + exec.tx::execute_foreign_procedure + # => [validation_result] + + # Assert valid proof data + assert.err=ERR_INVALID_CLAIM_PROOF drop + # => [] +end + +# Inputs: [] +# Outputs: [U256[0], U256[1]] +proc get_raw_claim_amount + padw mem_loadw_be.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 + padw mem_loadw_be.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 +end + +# Inputs: [U256[0], U256[1]] +# Outputs: [amount] +proc scale_down_amount + repeat.7 drop end +end + +# Inputs: [] +# Outputs: [prefix, suffix] +proc get_destination_account_id + mem_load.543 mem_load.544 +end + +# Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] +# Outputs: [] +proc batch_pipe_double_words + # 1) Verify PROOF_DATA_KEY + mem_storew_be.PROOF_DATA_KEY_MEM_ADDR + adv.push_mapval + # => [PROOF_DATA_KEY] + + push.PROOF_DATA_START_PTR push.PROOF_DATA_WORD_LEN + exec.mem::pipe_double_words_preimage_to_memory drop + + # 2) Verify LEAF_DATA_KEY + mem_storew_be.LEAF_DATA_KEY_MEM_ADDR + adv.push_mapval + # => [LEAF_DATA_KEY] + + push.LEAF_DATA_START_PTR push.LEAF_DATA_WORD_LEN + exec.mem::pipe_double_words_preimage_to_memory drop + + # 3) Verify OUTPUT_NOTE_DATA_KEY + mem_storew_be.OUTPUT_NOTE_DATA_MEM_ADDR + adv.push_mapval + # => [OUTPUT_NOTE_DATA_KEY] + + push.OUTPUT_NOTE_DATA_START_PTR push.OUTPUT_NOTE_DATA_WORD_LEN + exec.mem::pipe_double_words_preimage_to_memory drop +end + +#! Builds a P2ID output note for the claim recipient. +#! +#! This procedure expects the claim data to be already written to memory via batch_pipe_double_words. +#! It reads the destination account ID, amount, and other note parameters from memory to construct +#! the output note. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Note: This procedure will be refactored in a follow-up to use leaf data to build the output note. +proc build_p2id_output_note + # Build P2ID output note + push.P2ID_SCRIPT_ROOT[0..4] + # => [SCRIPT_ROOT] + + swapw mem_loadw_be.OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR + # => [SERIAL_NUM, SCRIPT_ROOT] + + push.P2ID_NOTE_NUM_INPUTS + # => [num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] + + exec.get_destination_account_id + # => [account_id_prefix, account_id_suffix, num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] + + mem_store.0 mem_store.1 + # => [num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] + + push.OUTPUT_NOTE_INPUTS_MEM_ADDR + # => [inputs_ptr = 0, num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] + + exec.note::build_recipient + # => [RECIPIENT] + + push.OUTPUT_NOTE_TYPE_PUBLIC + # => [note_type, RECIPIENT] + + mem_load.OUTPUT_NOTE_TAG_MEM_ADDR + # => [tag, RECIPIENT] + + exec.get_raw_claim_amount + # => [AMOUNT[1], AMOUNT[0], tag, note_type, RECIPIENT] + + # TODO: implement scale down logic; stubbed out for now + exec.asset_conversion::scale_u256_to_native_amount + # => [amount, tag, note_type, RECIPIENT] + + exec.faucets::distribute + # => [pad(16)] +end + +#! Validates a claim against the AggLayer bridge and mints the corresponding asset to the recipient. +#! +#! This procedure validates the rollup exit root Merkle Proof via FPI against the agglayer bridge, +#! and if validation passes, mints the asset and creates an output note for the recipient. +#! +#! TODO: Expand this description to cover the double-spend protection mechanism in detail. +#! Double-spend can be prevented in two ways: +#! 1) While it's possible to create two identical P2ID notes, only one can actually be consumed. +#! If the claim note is consumed twice, only one P2ID output note will be successfully consumed. +#! 2) We can have a mapping in the bridge or in the faucet that stores consumed claim proofs +#! as a hash -> bool value (similar to how it's done in the agglayer solidity contract). +#! +#! Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY, pad(4)] +#! Outputs: [pad(16)] +#! +#! Advice map: { +#! PROOF_DATA_KEY => [ +#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts) +#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts) +#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) +#! ], +#! LEAF_DATA_KEY => [ +#! originNetwork[1], // Origin network identifier (1 felt, uint32) +#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) +#! destinationNetwork[1], // Destination network identifier (1 felt, uint32) +#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) +#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) +#! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! EMPTY_WORD // padding +#! ], +#! OUTPUT_NOTE_DATA_KEY => [ +#! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) +#! agglayer_faucet_account_id[2], // Agglayer faucet account ID (2 felts, prefix and suffix) +#! output_note_tag[1], // P2ID output note tag +#! ] +#! } +#! +#! Panics if: +#! - the rollup exit root Merkle Proof validation via FPI fails. +#! - any of the validations in faucets::distribute fail. +#! +#! Invocation: call +pub proc claim + # Check AdviceMap values hash to keys & write CLAIM inputs & DATA_KEYs to global memory + exec.batch_pipe_double_words + # => [] + + # VALIDATE CLAIM + mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR padw + mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR + # => [PROOF_DATA_KEY, LEAF_DATA_KEY] + + # Errors on invalid proof + exec.validate_claim + # => [] + + # Create P2ID output note + exec.build_p2id_output_note + # => [] +end + +#! Burns the fungible asset from the active note. +#! +#! This procedure retrieves the asset from the active note and burns it. The note must contain +#! exactly one asset, which must be a fungible asset issued by this faucet. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the procedure is not called from a note context (active_note::get_assets will fail). +#! - the note does not contain exactly one asset. +#! - the transaction is executed against an account which is not a fungible asset faucet. +#! - the transaction is executed against a faucet which is not the origin of the specified asset. +#! - the amount about to be burned is greater than the outstanding supply of the asset. +#! +#! Invocation: call +pub use ::miden::standards::faucets::basic_fungible::burn diff --git a/crates/miden-agglayer/asm/bridge/asset_conversion.masm b/crates/miden-agglayer/asm/bridge/asset_conversion.masm new file mode 100644 index 0000000000..e4f59f17d4 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/asset_conversion.masm @@ -0,0 +1,114 @@ +use miden::core::math::u64 +use miden::core::word + +# CONSTANTS +# ================================================================================================= + +const MAX_SCALING_FACTOR=18 + +# ERRORS +# ================================================================================================= +const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT="maximum scaling factor is 18" + +#! Calculate 10^scale where scale is a u8 exponent. +#! +#! Inputs: [scale] +#! Outputs: [10^scale] +#! +#! Where: +#! - scale is expected to be a small integer (0-18 typical for crypto decimals) +#! +#! Panics if: +#! - scale > 18 (overflow protection) +proc pow10 + u32assert.err=ERR_SCALE_AMOUNT_EXCEEDED_LIMIT + # => [scale] + + dup u32lte.MAX_SCALING_FACTOR assert.err=ERR_SCALE_AMOUNT_EXCEEDED_LIMIT + # => [scale] + + push.1 swap + # => [scale, result] + + dup neq.0 + # => [is_not_zero, scale, result] + + # Loop to calculate 10^scale + while.true + # => [scale, result] + + # result *= 10 + swap mul.10 swap + # => [scale, result*10] + + # scale -= 1 + sub.1 + # => [scale-1, result*10] + + dup neq.0 + # => [is_not_zero, scale-1, result*10] + end + # => [0, result] + + drop + # => [result] +end + +#! Convert an asset amount to a scaled U256 representation for bridging to Agglayer. +#! +#! This procedure is used to convert Miden asset amounts to EVM asset amounts. +#! It multiplies the input amount by 10^target_scale to adjust for decimal differences +#! between the current representation and the target chain's native decimals. +#! +#! The procedure first calculates 10^target_scale using the pow10 helper, then converts +#! both the amount and scale factor to U64 format, performs U64 multiplication, and +#! returns the result as 8 u32 limbs in little-endian order (U256 format). +#! +#! Inputs: [amount, target_scale] +#! Outputs: [[RESULT_U256[0], RESULT_U256[1]]] +#! +#! Where: +#! - amount: The asset amount to be converted (range: 0 to 2^63 - 2^31) +#! - target_scale: Exponent for scaling factor (10^target_scale) +#! - [RESULT_U256[0], RESULT_U256[1]]: U256 value as 8 u32 limbs in little-endian order +#! (least significant limb at the top of the stack, each limb stored in little-endian format) +#! +#! Examples: +#! - USDC: amount=1000000000, target_scale=0 → 1000000000 (no scaling) +#! - ETH: amount=1e10, target_scale=8 → 1e18 +#! +#! Invocation: exec +pub proc scale_native_amount_to_u256 + swap + # => [target_scale, amount] + + exec.pow10 + # => [scale, amount] + + u32split + # => [scale_hi, scale_lo, amount] + + movup.2 u32split + # => [amount_hi, amount_lo, scale_hi, scale_lo] + + # Perform U64 multiplication: amount * scale + # This is safe because both the scaling factor and amount are guaranteed to be smaller + # than 2^64, so we will never overflow a 256-bit value. + exec.u64::overflowing_mul + # => [res_hi, res_mid_hi, res_mid_lo, res_lo] + + exec.word::reverse + # => [res_lo, res_mid_lo, res_mid_hi, res_hi] + + # convert to U256 & little endian + padw swapw + # => [RESULT_U256[0], RESULT_U256[1]] +end + +#! TODO: implement scaling down +#! +#! Inputs: [U256[0], U256[1]] +#! Outputs: [amount] +pub proc scale_u256_to_native_amount + repeat.7 drop end +end diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm new file mode 100644 index 0000000000..1862fc5e35 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -0,0 +1,43 @@ +use miden::agglayer::crypto_utils + +# Inputs: [] +# Output: [GER_ROOT[8]] +pub proc get_rollup_exit_root + # Push dummy GER (8 elements) + push.0.0.0.0.0.0.0.0 # dummy GER +end + +#! Checks the validity of the GET proof +#! +#! Inputs: +#! Operand stack: [PROOF_DATA_KEY, LEAF_DATA_KEY, pad(8)] +#! Advice map: { +#! PROOF_DATA_KEY => [ +#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts) +#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts) +#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) +#! ], +#! LEAF_DATA_KEY => [ +#! originNetwork[1], // Origin network identifier (1 felt, uint32) +#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) +#! destinationNetwork[1], // Destination network identifier (1 felt, uint32) +#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) +#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) +#! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! EMPTY_WORD // padding +#! ], +#! } +#! +#! Invocation: call +pub proc check_claim_proof + exec.get_rollup_exit_root + # => [GER_ROOT[8], CLAIM_NOTE_RPO_COMMITMENT] + + # Check CLAIM note proof data against current GER + exec.crypto_utils::verify_claim_proof + # => [is_valid_claim_proof] + + swap drop +end diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm new file mode 100644 index 0000000000..3c53b62763 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -0,0 +1,160 @@ +use miden::protocol::active_note +use miden::protocol::note +use miden::protocol::output_note +use miden::core::crypto::hashes::keccak256 +use miden::core::crypto::hashes::rpo256 +use miden::core::word +use miden::agglayer::local_exit_tree + +# CONSTANTS +# ================================================================================================= +const MMR_PTR=42 +const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") + +const BURN_NOTE_ROOT = [15615638671708113717, 1774623749760042586, 2028263167268363492, 12931944505143778072] +const PUBLIC_NOTE=1 +const NUM_BURN_NOTE_INPUTS=0 +const BURN_ASSET_MEM_PTR=24 + +#! Computes the SERIAL_NUM of the outputted BURN note. +#! +#! The serial number is computed as hash(B2AGG_SERIAL_NUM, ASSET). +#! +#! Inputs: [ASSET] +#! Outputs: [SERIAL_NUM] +#! +#! Where: +#! - ASSET is the asset for which to compute the burn note serial number. +#! - SERIAL_NUM is the computed serial number for the BURN note. +#! +#! Invocation: exec +proc compute_burn_note_serial_num + exec.active_note::get_serial_number + # => [B2AGG_SERIAL_NUM, ASSET] + + exec.rpo256::merge + # => [SERIAL_NUM] +end + +#! Creates a BURN note for the specified asset. +#! +#! This procedure creates an output note that represents a burn operation for the given asset. +#! The note is configured with the appropriate recipient, tag, and execution hint. +#! +#! Inputs: [ASSET] +#! Outputs: [] +#! +#! Where: +#! - ASSET is the asset to be burned. +#! +#! Invocation: exec +@locals(8) +proc create_burn_note + loc_storew_be.0 dupw + # => [ASSET, ASSET] + + movup.2 drop movup.2 drop + # => [faucet_id_prefix, faucet_id_suffix, ASSET] + + exec.note::build_note_tag_for_network_account + # => [network_faucet_tag, ASSET] + + loc_store.5 + # => [ASSET] + + exec.compute_burn_note_serial_num + # => [SERIAL_NUM] + + push.BURN_NOTE_ROOT swapw + # => [SERIAL_NUM, SCRIPT_ROOT] + + push.NUM_BURN_NOTE_INPUTS push.0 + # => [inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] + + exec.note::build_recipient + # => [RECIPIENT] + + push.PUBLIC_NOTE + loc_load.5 + # => [tag, note_type, RECIPIENT] + + call.output_note::create + # => [note_idx] + + movdn.4 loc_loadw_be.0 + # => [ASSET, note_idx] + + exec.output_note::add_asset + # => [] +end + +#! Bridges an asset out via the AggLayer +#! +#! This procedure handles the complete bridge-out operation, including: +#! - Converting asset data to u32 format +#! - Computing Keccak hash of the data +#! - Adding the hash to the MMR frontier +#! - Storing the updated MMR root in account storage +#! - Creating a BURN note with the bridged out asset +#! +#! Inputs: [ASSET, dest_network, dest_address(5)] +#! Outputs: [] +#! +#! Where: +#! - ASSET is the asset to be bridged out. +#! - dest_network is the u32 destination network/chain ID. +#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. +#! +#! Invocation: call +pub proc bridge_out + mem_storew_be.BURN_ASSET_MEM_PTR + # => [ASSET, dest_network, dest_address(5)] + + # @dev TODO: Look up asset faucet id in asset registry + # -> return scaling factor + + # @dev TODO: Convert ASSET amount to EVM amount using scaling factor + # -> return amount from here: https://github.com/0xMiden/miden-base/pull/2141 + + # Converting SCALED_ASSET, dest_network, dest_address(5) to u32 representation + # in preparation for keccak256 hashing + + # keccak256 inputs: + # => [ASSET, dest_network, dest_address(5)] + # TODO we should convert Miden->Ethereum asset values, incl. amount conversion etc. + + # TODO: make building bridge message a separate procedure + # TODO: match Agglayer addLeafBridge logic + # TODO: convert Miden asset amount to Ethereum amount + # Store ASSET as u32 limbs in memory starting at address 0 + push.0 movdn.4 exec.word::store_word_u32s_le + # => [dest_network, dest_address(5)] + + # Store [dest_network, dest_address[0..3]] as u32 limbs in memory starting at address 8 + push.8 movdn.4 exec.word::store_word_u32s_le + # => [dest_address(2), 0, 0] + + # Store [dest_address[3..5], 0, 0] as u32 limbs in memory starting at address 16 + push.16 movdn.4 exec.word::store_word_u32s_le + # => [] + + # 1 u32 = 4 bytes + # 10 u32 values = 40 bytes + push.40 push.0 + # => [ptr, len_bytes] + + exec.keccak256::hash_bytes + # => [DIGEST_U32[8]] + + # adding DIGEST_U32 double word leaf to mmr frontier + exec.local_exit_tree::add_asset_message + # => [] + + # creating BURN output note for ASSET + mem_loadw_be.BURN_ASSET_MEM_PTR + # => [ASSET] + + exec.create_burn_note + # => [] +end + diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm new file mode 100644 index 0000000000..7796c1f94f --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -0,0 +1,82 @@ +use miden::core::crypto::hashes::keccak256 + +#! Given the leaf data returns the leaf value. +#! +#! Inputs: [leaf_type, origin_network, ORIGIN_ADDRESS, destination_network, DESTINATION_ADDRESS, amount, METADATA_HASH] +#! Outputs: [LEAF_VALUE] +#! +#! Where: +#! - leaf_type is the leaf type: [0] transfer Ether / ERC20 tokens, [1] message. +#! - origin_network is the origin network identifier. +#! - ORIGIN_ADDRESS is the origin token address (5 elements) +#! - destination_network is the destination network identifier. +#! - DESTINATION_ADDRESS is the destination address (5 elements). +#! - amount is the amount: [0] Amount of tokens/ether, [1] Amount of ether. +#! - METADATA_HASH is the hash of the metadata (8 elements). +#! - LEAF_VALUE is the computed leaf value (8 elements). +#! +#! This function computes the keccak256 hash of the abi.encodePacked data. +#! +#! Invocation: exec +pub proc get_leaf_value + # TODO: implement getLeafValue() + # https://github.com/agglayer/agglayer-contracts/blob/e468f9b0967334403069aa650d9f1164b1731ebb/contracts/v2/lib/DepositContractV2.sol#L22 + + # stubbed out: + push.1.1.1.1 + push.1.1.1.1 + + # exec.keccak256::hash_bytes + # => [LEAF_VALUE[8]] +end + +#! Verify leaf and checks that it has not been claimed. +#! +#! This procedure verifies that a claim proof is valid against the Global Exit Tree (GET) +#! and that the leaf has not been previously claimed. +#! +#! Inputs: +#! Operand stack: [GER_ROOT[8], CLAIM_PROOF_RPO_COMMITMENT, pad(12)] +#! Advice map: { +#! PROOF_DATA_KEY => [ +#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts) +#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts) +#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) +#! ], +#! LEAF_DATA_KEY => [ +#! originNetwork[1], // Origin network identifier (1 felt, uint32) +#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) +#! destinationNetwork[1], // Destination network identifier (1 felt, uint32) +#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) +#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) +#! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! EMPTY_WORD // padding +#! ], +#! } +#! Outputs: +#! Operand stack: [is_valid] +#! +#! Where: +#! - RPO_CLAIM_NOTE_INPUTS_COMMITMENT is the RPO hash commitment of all claim note inputs +#! - leafType is the leaf type: [0] transfer Ether / ERC20 tokens, [1] message +#! - originNetwork is the origin network identifier (u32 as Felt) +#! - originAddress is the origin address (5 felts representing address) +#! - destinationNetwork is the destination network identifier (u32 as Felt) +#! - destinationAddress is the destination address (5 felts representing address) +#! - amount is the amount of tokens (u256 as Felt) +#! - metadata is the metadata (4 felts representing 4 u32 0 values) +#! - index is the index of the leaf (u32 as Felt) +#! - claimRoot is the claim root (8 felts representing bytes32) +#! - smtProof is the SMT proof data (570 felts) +#! - is_valid is 1 if the leaf is valid and not claimed, 0 otherwise +#! +#! Invocation: exec +pub proc verify_claim_proof + # TODO: Implement actual Global Exit Tree proof verification + + # For now, drop all inputs and return 1 (valid) + dropw dropw dropw dropw + push.1 +end diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm new file mode 100644 index 0000000000..57a8e9f298 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -0,0 +1,88 @@ +use miden::core::crypto::hashes::keccak256 +use miden::core::word + +# CONSTANTS +# ================================================================================================= + +const U32_MAX=4294967295 +const TWO_POW_32=4294967296 + +const ERR_NOT_U32="address limb is not u32" +const ERR_ADDR4_NONZERO="most-significant 4 bytes (addr4) must be zero" +const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" + + +# ETHEREUM ADDRESS PROCEDURES +# ================================================================================================= + +#! Builds a single felt from two u32 limbs (little-endian limb order). +#! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. +#! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping +#! through u32split and comparing the limbs. +#! +#! Inputs: [lo, hi] +#! Outputs: [felt] +proc build_felt + # --- validate u32 limbs --- + u32assert2.err=ERR_NOT_U32 + # => [lo, hi] + + # keep copies for the overflow check + dup.1 dup.1 + # => [lo, hi, lo, hi] + + # felt = (hi * 2^32) + lo + swap + push.TWO_POW_32 mul + add + # => [felt, lo, hi] + + # ensure no reduction mod p happened: + # split felt back into (hi, lo) and compare to inputs + dup u32split + # => [hi2, lo2, felt, lo, hi] + + movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD + # => [lo2, felt, lo] + + movup.2 assert_eq.err=ERR_FELT_OUT_OF_FIELD + # => [felt] +end + +#! Converts an Ethereum address format (address[5] type) back into an AccountId [prefix, suffix] type. +#! +#! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*: +#! addr0 = bytes[16..19] (least-significant 4 bytes) +#! addr1 = bytes[12..15] +#! addr2 = bytes[ 8..11] +#! addr3 = bytes[ 4.. 7] +#! addr4 = bytes[ 0.. 3] (most-significant 4 bytes) +#! +#! The most-significant 4 bytes must be zero for a valid AccountId conversion (addr4 == 0). +#! The remaining 16 bytes are treated as two 8-byte words (conceptual u64 values): +#! prefix = (addr3 << 32) | addr2 # bytes[4..11] +#! suffix = (addr1 << 32) | addr0 # bytes[12..19] +#! +#! These 8-byte words are represented as field elements by packing two u32 limbs into a felt. +#! The packing is done via build_felt, which validates limbs are u32 and checks the packed value +#! did not reduce mod p (i.e. the word fits in the field). +#! +#! Inputs: [addr0, addr1, addr2, addr3, addr4] +#! Outputs: [prefix, suffix] +#! +#! Invocation: exec +pub proc to_account_id + # addr4 must be 0 (most-significant limb) + movup.4 + eq.0 assert.err=ERR_ADDR4_NONZERO + # => [addr0, addr1, addr2, addr3] + + exec.build_felt + # => [suffix, addr2, addr3] + + movdn.2 + # => [addr2, addr3, suffix] + + exec.build_felt + # => [prefix, suffix] +end diff --git a/crates/miden-agglayer/asm/bridge/local_exit_tree.masm b/crates/miden-agglayer/asm/bridge/local_exit_tree.masm new file mode 100644 index 0000000000..89e744507b --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/local_exit_tree.masm @@ -0,0 +1,120 @@ +use miden::protocol::active_account +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================= + +const MMR_PTR=42 +const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") + +#! Adds a leaf to the MMR frontier using Keccak hashing (stubbed implementation). +#! +#! This is a stubbed implementation that currently drops all inputs without performing +#! the actual MMR frontier addition operation. +#! +#! Inputs: [LEAF[1], LEAF[0], mmr_ptr] +#! Outputs: [] +#! +#! Where: +#! - LEAF[1], LEAF[0] are the leaf data to add to the MMR frontier. +#! - mmr_ptr is the pointer to the MMR frontier data structure. +#! +#! Invocation: exec +proc mmr_frontier_keccak_add + dropw dropw drop + # => [] +end + +#! Gets the root of the MMR frontier using Keccak hashing (stubbed implementation). +#! +#! This is a stubbed implementation that returns placeholder values instead of +#! computing the actual MMR frontier root. +#! +#! Inputs: [mmr_ptr] +#! Outputs: [ROOT[1], ROOT[0]] +#! +#! Where: +#! - ROOT[1], ROOT[0] are the root hash components of the MMR frontier whose memory location starts at mmr_ptr +#! +#! Invocation: exec +pub proc mmr_frontier_keccak_get_root + # stubbed out for now + drop + # => [] + + push.0.0.0.1 push.LOCAL_EXIT_TREE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY] + + exec.active_account::get_map_item + # => [ROOT[0]] + + push.0.0.0.0 push.LOCAL_EXIT_TREE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, ROOT[0]] + + exec.active_account::get_map_item + # => [ROOT[1], ROOT[0]] +end + +#! Writes the MMR frontier root to account storage. +#! +#! This procedure retrieves the current MMR frontier root and stores it as a double word +#! in the account's storage map. The root is split across two storage keys: +#! - Key [0,0,0,0] stores ROOT[1] (high part) +#! - Key [0,0,0,1] stores ROOT[0] (low part) +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +proc write_mmr_frontier_root + push.MMR_PTR + # => [MMR_PTR] + + # getting mmr frontier root + exec.mmr_frontier_keccak_get_root + # => [ROOT[1], ROOT[0]] + + # writing double word root to map keys [0,0,0,0] & [0,0,0,1] + push.0.0.0.0 push.LOCAL_EXIT_TREE_SLOT[0..2] + # => [index, KEY, ROOT[1], ROOT[0]] + + exec.native_account::set_map_item + # => [OLD_MAP_ROOT, OLD_MAP_VALUE, ROOT[0]] + + dropw dropw + # => [ROOT[0]] + + push.1.0.0.0 push.LOCAL_EXIT_TREE_SLOT[0..2] + # => [index, KEY, ROOT[0]] + + exec.native_account::set_map_item + # => [OLD_MAP_ROOT, OLD_MAP_VALUE] + + dropw dropw + # => [] +end + +#! Adds an asset message to the MMR frontier and updates the stored root. +#! +#! This procedure takes a Keccak digest (represented as 8 u32 values) and adds it +#! as a leaf to the MMR frontier. After adding the leaf, it updates the MMR root +#! in the account's storage to reflect the new state. +#! +#! Inputs: [DIGEST_U32[8]] +#! Outputs: [] +#! +#! Where: +#! - DIGEST_U32[8] is a Keccak256 hash represented as 8 u32 values (256 bits total). +#! +#! Invocation: exec +pub proc add_asset_message + push.MMR_PTR movdn.8 + # => [LEAF[1], LEAF[0], mmr_ptr] + + exec.mmr_frontier_keccak_add + # => [] + + exec.write_mmr_frontier_root + # => [] +end + diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm new file mode 100644 index 0000000000..80bdcfbb7f --- /dev/null +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -0,0 +1,88 @@ +use miden::agglayer::bridge_out +use miden::protocol::account_id +use miden::protocol::active_account +use miden::protocol::active_note +use miden::standards::wallets::basic->basic_wallet + +# CONSTANTS +# ================================================================================================= + +const B2AGG_NOTE_INPUTS_COUNT=6 + +# ERRORS +# ================================================================================================= +const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS="B2AGG script requires exactly 1 note asset" + +const ERR_B2AGG_WRONG_NUMBER_OF_INPUTS="B2AGG script expects exactly 6 note inputs" + +#! Bridge-to-AggLayer (B2AGG) note script: bridges assets from Miden to an AggLayer-connected chain. +#! +#! This note can be consumed in two ways: +#! - If the consuming account is the sender (reclaim): the note's assets are added back to the consuming account. +#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note, +#! and the note details are hashed into a leaf and appended to the Local Exit Tree. +#! global exit root (GER) merkle tree structure. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Note inputs are assumed to be as follows: +#! - destination_network: u32 value representing the target chain ID +#! - destination_address: split into 5 u32 values representing a 20-byte Ethereum address: +#! - destination_address_0: bytes 0-3 +#! - destination_address_1: bytes 4-7 +#! - destination_address_2: bytes 8-11 +#! - destination_address_3: bytes 12-15 +#! - destination_address_4: bytes 16-19 +#! +#! Panics if: +#! - The note does not contain exactly 6 inputs. +#! - The note does not contain exactly 1 asset. +#! +begin + dropw + # => [pad(16)] + + # Check if reclaim + exec.active_account::get_id + # => [account_id_prefix, account_id_suffix, pad(16)] + + exec.active_note::get_sender + # => [sender_id_prefix, sender_id_suffix, account_id_prefix, account_id_suffix, pad(16)] + + exec.account_id::is_equal + # => [reclaim, pad(16)] + + # B2AGG note is being reclaimed; adding note assets to account + if.true + exec.basic_wallet::add_assets_to_account + # => [pad(16)] + else + # Store note inputs -> mem[8..14] + push.8 exec.active_note::get_inputs + # => [num_inputs, dest_ptr, pad(16)] + + push.B2AGG_NOTE_INPUTS_COUNT assert_eq.err=ERR_B2AGG_WRONG_NUMBER_OF_INPUTS drop + # => [pad(16)] + + # Store note assets -> mem[0..4] + push.0 exec.active_note::get_assets + # => [num_assets, ptr, pad(16)] + + # Must be exactly 1 asset + push.1 assert_eq.err=ERR_B2AGG_WRONG_NUMBER_OF_ASSETS drop + # => [pad(16)] + + # load the 6 B2AGG note input felts as two words + mem_loadw_be.12 swapw.2 mem_loadw_be.8 swapw + # => [EMPTY_WORD, dest_network, dest_address(5), pad(6)] + + # Load ASSET onto the stack + mem_loadw_be.0 + # => [ASSET, dest_network, dest_address(5), pad(6)] + + call.bridge_out::bridge_out + # => [pad(16)] + end + # => [pad(16)] +end diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm new file mode 100644 index 0000000000..83c41a65bc --- /dev/null +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -0,0 +1,211 @@ +use miden::agglayer::agglayer_faucet -> agg_faucet +use miden::protocol::account_id +use miden::protocol::active_account +use miden::protocol::active_note +use miden::protocol::note +use miden::core::crypto::hashes::keccak256 +use miden::core::crypto::hashes::rpo256 +use miden::core::mem + +# CONSTANTS +# ================================================================================================= + +const PROOF_DATA_SIZE = 536 +const LEAF_DATA_SIZE = 24 +const OUTPUT_NOTE_SIZE = 8 + +const PROOF_DATA_START_PTR = 0 +const LEAF_DATA_START_PTR = 536 +const OUTPUT_NOTE_DATA_START_PTR = 568 + +const TARGET_FAUCET_PREFIX_MEM_ADDR = 572 +const TARGET_FAUCET_SUFFIX_MEM_ADDR = 573 + +# ERRORS +# ================================================================================================= + +const ERR_CLAIM_TARGET_ACCT_MISMATCH = "CLAIM's target account address and transaction address do not match" + +#! Asserts that the consuming account matches the target agglayer faucet account. +#! +#! This procedure ensures that only the specified agglayer faucet account can consume +#! this CLAIM note. It assumes that the note inputs have already been loaded into memory +#! via active_note::get_inputs. +#! +#! Inputs: [] +#! Output: [] +#! +#! Panics if: +#! - The consuming account ID does not match the target faucet account ID stored in memory +proc assert_aggfaucet_is_consumer + # Load target faucet ID (assumes active_note::get_inputs has been called) + mem_load.TARGET_FAUCET_SUFFIX_MEM_ADDR mem_load.TARGET_FAUCET_PREFIX_MEM_ADDR + # => [target_faucet_prefix, target_faucet_suffix] + + exec.active_account::get_id + # => [account_id_prefix, account_id_suffix, target_faucet_prefix, target_faucet_suffix] + + # ensure only the specified target faucet can consume this CLAIM note, not any other account + exec.account_id::is_equal assert.err=ERR_CLAIM_TARGET_ACCT_MISMATCH + # => [] +end + +#! Reads claim data from memory and inserts it into the advice map under three separate keys. +#! +#! This procedure organizes the claim note data into three logical groups and inserts them +#! into the advice map under separate keys for easier access. +#! +#! Inputs: [] +#! Outputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] +#! +#! Advice map entries created: +#! PROOF_DATA_KEY => [ +#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts) +#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts) +#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) +#! ] +#! +#! LEAF_DATA_KEY => [ +#! originNetwork[1], // Origin network identifier (1 felt, uint32) +#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) +#! destinationNetwork[1], // Destination network identifier (1 felt, uint32) +#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) +#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) +#! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! EMPTY_WORD // padding +#! ] +#! +#! TODO: Will be removed in future PR +#! OUTPUT_NOTE_DATA_KEY => [ +#! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) +#! target_faucet_account_id[2], // Target faucet account ID (2 felts, prefix and suffix) +#! output_note_tag[1], // P2ID output note tag +#! ] +#! +#! Invocation: exec +proc write_claim_data_into_advice_map_by_key + # 1) Get OUTPUT_NOTE_DATA_KEY + push.OUTPUT_NOTE_SIZE push.OUTPUT_NOTE_DATA_START_PTR + exec.rpo256::hash_elements + # => [OUTPUT_NOTE_DATA_KEY] + + push.OUTPUT_NOTE_SIZE add.OUTPUT_NOTE_DATA_START_PTR push.OUTPUT_NOTE_DATA_START_PTR + movdn.5 movdn.5 + # => [OUTPUT_NOTE_DATA_KEY, start_ptr, end_ptr] + + adv.insert_mem + # OS => [OUTPUT_NOTE_DATA_KEY, start_ptr, end_ptr, pad(16)] + # AM => {OUTPUT_NOTE_DATA_KEY: mem[start_ptr..end_ptr] } + + movup.4 drop movup.4 drop + # => [OUTPUT_NOTE_DATA_KEY] + + # 2) Get LEAF_DATA_KEY + push.LEAF_DATA_SIZE push.LEAF_DATA_START_PTR + exec.rpo256::hash_elements + # => [LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + + push.LEAF_DATA_SIZE add.LEAF_DATA_START_PTR push.LEAF_DATA_START_PTR + movdn.5 movdn.5 + # => [LEAF_DATA_KEY, start_ptr, end_ptr, OUTPUT_NOTE_DATA_KEY] + + adv.insert_mem + # OS => [LEAF_DATA_KEY, start_ptr, end_ptr] + # AM => {LEAF_DATA_KEY: mem[start_ptr..end_ptr] } + movup.4 drop movup.4 drop + # => [LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + + # 3) Get PROOF_DATA_KEY + push.PROOF_DATA_SIZE push.PROOF_DATA_START_PTR + exec.rpo256::hash_elements + # => [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + + push.PROOF_DATA_SIZE push.PROOF_DATA_START_PTR + movdn.5 movdn.5 + # => [PROOF_DATA_KEY, start_ptr, end_ptr, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + + adv.insert_mem + # OS => [PROOF_DATA_KEY, start_ptr, end_ptr, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # AM => {PROOF_DATA_KEY: mem[start_ptr..end_ptr] } + + movup.4 drop movup.4 drop + # => [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] +end + +#! Agglayer Faucet CLAIM script: claims assets by calling the agglayer faucet's claim function. +#! +#! This note can only be consumed by the specific agglayer faucet account whose ID is provided +#! in the note inputs (target_faucet_account_id). Upon consumption, it will create a P2ID note. +#! +#! Requires that the account exposes: +#! - agglayer::agglayer_faucet::claim procedure. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! NoteInputs layout (575 felts total): +#! - smtProofLocalExitRoot [0..255] : 256 felts +#! - smtProofRollupExitRoot [256..511]: 256 felts +#! - globalIndex [512..519]: 8 felts +#! - mainnetExitRoot [520..527]: 8 felts +#! - rollupExitRoot [528..535]: 8 felts +#! - originNetwork [536] : 1 felt +#! - originTokenAddress [537..541]: 5 felts +#! - destinationNetwork [542] : 1 felt +#! - destinationAddress [543..547]: 5 felts +#! - amount [548..555]: 8 felts +#! - metadata [556..563]: 8 felts +#! - EMPTY_WORD [564..567]: 4 felts +#! - output_p2id_serial_num [568..571]: 4 felts +#! - target_faucet_account_id [572..573]: 2 felts +#! - output_note_tag [574] : 1 felt +#! +#! Where: +#! - smtProofLocalExitRoot: SMT proof for local exit root (bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! - smtProofRollupExitRoot: SMT proof for rollup exit root (bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! - globalIndex: Global index (uint256 as 8 u32 felts). This is a packed "locator" for the leaf being claimed: +#! - mainnetFlag (1 bit): 1 = leaf came from L1 (Mainnet Exit Tree), 0 = leaf came from an L2 rollup +#! - rollupIndex (32 bits): which rollup (only used when mainnetFlag=0) +#! - localRootIndex (32 bits): leaf index / depositCount in the origin chain's Local Exit Tree +#! - Top 191 bits are ignored (not required to be zero), so indexers must decode it exactly like the contract does +#! - mainnetExitRoot: Mainnet exit root hash (bytes32 as 8 u32 felts) +#! - rollupExitRoot: Rollup exit root hash (bytes32 as 8 u32 felts) +#! - originNetwork: Origin network identifier (uint32) +#! - originTokenAddress: Origin token address (address as 5 u32 felts) +#! - destinationNetwork: Destination network identifier (uint32) +#! - destinationAddress: Destination address (address as 5 u32 felts) +#! - amount: Amount of tokens (uint256 as 8 u32 felts) +#! - metadata: ABI encoded metadata (fixed size) +#! - EMPTY_WORD: Padding word +#! - output_p2id_serial_num: P2ID note serial number (Word) +#! - target_faucet_account_id: Target agglayer faucet account ID (prefix and suffix). Only this specific +#! account can consume the note - any other account will cause a panic. +#! - output_note_tag: P2ID output note tag +#! +#! Panics if: +#! - account does not expose claim procedure. +#! - target faucet account ID does not match the consuming account ID. +begin + dropw + # => [pad(16)] + + # Load CLAIM note inputs into memory, starting at address 0 + push.0 exec.active_note::get_inputs drop drop + # => [pad(16)] + + # Check consuming account == aggfaucet + exec.assert_aggfaucet_is_consumer + # => [pad(16)] + + exec.write_claim_data_into_advice_map_by_key + # => [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY, pad(4)] + + # Call the Aggfaucet Claim procedure + call.agg_faucet::claim + # => [pad(16), pad(12)] + + dropw dropw dropw + # => [pad(16)] +end diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs new file mode 100644 index 0000000000..fbd2cd06e7 --- /dev/null +++ b/crates/miden-agglayer/build.rs @@ -0,0 +1,497 @@ +use std::env; +use std::path::Path; + +use fs_err as fs; +use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; +use miden_assembly::utils::Serializable; +use miden_assembly::{Assembler, Library, Report}; +use miden_protocol::transaction::TransactionKernel; + +// 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"; +const ASM_BRIDGE_DIR: &str = "bridge"; + +const AGGLAYER_ERRORS_FILE: &str = "src/errors/agglayer.rs"; +const AGGLAYER_ERRORS_ARRAY_NAME: &str = "AGGLAYER_ERRORS"; + +// PRE-PROCESSING +// ================================================================================================ + +/// Read and parse the contents from `./asm`. +/// - Compiles the contents of asm/note_scripts directory into individual .masb files. +/// - Compiles the contents of asm/account_components directory into individual .masl files. +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"); + + // 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); + + // set target directory to {OUT_DIR}/assets + let target_dir = Path::new(&build_dir).join(ASSETS_DIR); + + // compile agglayer library + let agglayer_lib = + compile_agglayer_lib(&source_dir, &target_dir, TransactionKernel::assembler())?; + + let mut assembler = TransactionKernel::assembler(); + assembler.link_static_library(agglayer_lib)?; + + // compile note scripts + compile_note_scripts( + &source_dir.join(ASM_NOTE_SCRIPTS_DIR), + &target_dir.join(ASM_NOTE_SCRIPTS_DIR), + assembler.clone(), + )?; + + generate_error_constants(&source_dir)?; + + Ok(()) +} + +// COMPILE AGGLAYER LIB +// ================================================================================================ + +/// Reads the MASM files from "{source_dir}/bridge" directory, compiles them into a Miden +/// assembly library, saves the library into "{target_dir}/agglayer.masl", and returns the compiled +/// library. +fn compile_agglayer_lib( + source_dir: &Path, + target_dir: &Path, + mut assembler: Assembler, +) -> Result { + let source_dir = source_dir.join(ASM_BRIDGE_DIR); + + // Add the miden-standards library to the assembler so agglayer components can use it + let standards_lib = miden_standards::StandardsLib::default(); + assembler.link_static_library(standards_lib)?; + + let agglayer_lib = assembler.assemble_library_from_dir(source_dir, "miden::agglayer")?; + + let output_file = target_dir.join("agglayer").with_extension(Library::LIBRARY_EXTENSION); + agglayer_lib.write_to_file(output_file).into_diagnostic()?; + + Ok(agglayer_lib) +} + +// COMPILE EXECUTABLE MODULES +// ================================================================================================ + +/// Reads all MASM files from the "{source_dir}", complies each file individually into a MASB +/// file, and stores the compiled files into the "{target_dir}". +/// +/// The source files are expected to contain executable programs. +fn compile_note_scripts( + source_dir: &Path, + target_dir: &Path, + mut assembler: Assembler, +) -> Result<()> { + fs::create_dir_all(target_dir) + .into_diagnostic() + .wrap_err("failed to create note_scripts directory")?; + + // Add the miden-standards library to the assembler so note scripts can use it + let standards_lib = miden_standards::StandardsLib::default(); + assembler.link_static_library(standards_lib)?; + + for masm_file_path in shared::get_masm_files(source_dir).unwrap() { + // read the MASM file, parse it, and serialize the parsed AST to bytes + let code = assembler.clone().assemble_program(masm_file_path.clone())?; + + let bytes = code.to_bytes(); + + let masm_file_name = masm_file_path + .file_name() + .expect("file name should exist") + .to_str() + .ok_or_else(|| Report::msg("failed to convert file name to &str"))?; + let mut masb_file_path = target_dir.join(masm_file_name); + + // write the binary MASB to the output dir + masb_file_path.set_extension("masb"); + fs::write(masb_file_path, bytes).unwrap(); + } + Ok(()) +} + +// COMPILE ACCOUNT COMPONENTS (DEPRECATED) +// ================================================================================================ + +/// Compiles the bridge components in `source_dir` into MASL libraries and stores the compiled +/// files in `target_dir`. +/// +/// NOTE: This function is deprecated and replaced by compile_agglayer_lib +fn _compile_bridge_components( + source_dir: &Path, + target_dir: &Path, + mut assembler: Assembler, +) -> Result { + if !target_dir.exists() { + fs::create_dir_all(target_dir).unwrap(); + } + + // Add the miden-standards library to the assembler so agglayer components can use it + let standards_lib = miden_standards::StandardsLib::default(); + assembler.link_static_library(standards_lib)?; + + // Compile all components together as a single library under the "miden::agglayer" namespace + // This allows cross-references between components (e.g., bridge_out using + // miden::agglayer::local_exit_tree) + let agglayer_library = assembler.assemble_library_from_dir(source_dir, "miden::agglayer")?; + + // Write the combined library + let library_path = target_dir.join("agglayer").with_extension(Library::LIBRARY_EXTENSION); + agglayer_library.write_to_file(library_path).into_diagnostic()?; + + // Also write individual component files for reference + let masm_files = shared::get_masm_files(source_dir).unwrap(); + for masm_file_path in &masm_files { + let component_name = masm_file_path + .file_stem() + .expect("masm file should have a file stem") + .to_str() + .expect("file stem should be valid UTF-8") + .to_owned(); + + let component_source_code = fs::read_to_string(masm_file_path) + .expect("reading the component's MASM source code should succeed"); + + let individual_file_path = target_dir.join(&component_name).with_extension("masm"); + fs::write(individual_file_path, component_source_code).into_diagnostic()?; + } + + Ok(agglayer_library) +} + +// ERROR CONSTANTS FILE GENERATION +// ================================================================================================ + +/// Reads all MASM files from the `asm_source_dir` and extracts its error constants and their +/// associated error message and generates a Rust file for each category of errors. +/// For example: +/// +/// ```text +/// const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY="new account must have an empty vault" +/// ``` +/// +/// would generate a Rust file for transaction kernel errors (since the error belongs to that +/// category, identified by the category extracted from `ERR_`) with - roughly - the +/// following content: +/// +/// ```rust +/// pub const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY: MasmError = +/// MasmError::from_static_str("new account must have an empty vault"); +/// ``` +/// +/// and add the constant to the error constants array. +/// +/// 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(()); + } + + // Miden agglayer errors + // ------------------------------------------ + + let errors = shared::extract_all_masm_errors(asm_source_dir) + .context("failed to extract all masm errors")?; + shared::generate_error_file( + shared::ErrorModule { + file_name: AGGLAYER_ERRORS_FILE, + array_name: AGGLAYER_ERRORS_ARRAY_NAME, + is_crate_local: false, + }, + errors, + )?; + + Ok(()) +} + +/// This module should be kept in sync with the copy in miden-protocol's and miden-standards' +/// build.rs. +mod shared { + use std::collections::BTreeMap; + use std::fmt::Write; + use std::io::{self}; + use std::path::{Path, PathBuf}; + + use fs_err as fs; + use miden_assembly::Report; + use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; + 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. + pub fn get_masm_files>(dir_path: P) -> Result> { + let mut files = Vec::new(); + + let path = dir_path.as_ref(); + if path.is_dir() { + let entries = fs::read_dir(path) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read directory {}", path.display()))?; + for entry in entries { + let file = entry.into_diagnostic().wrap_err("failed to read directory entry")?; + let file_path = file.path(); + if is_masm_file(&file_path).into_diagnostic()? { + files.push(file_path); + } + } + } else { + println!("cargo:warn=The specified path is not a directory."); + } + + Ok(files) + } + + /// Returns true if the provided path resolves to a file with `.masm` extension. + /// + /// # Errors + /// Returns an error if the path could not be converted to a UTF-8 string. + pub fn is_masm_file(path: &Path) -> io::Result { + if let Some(extension) = path.extension() { + let extension = extension + .to_str() + .ok_or_else(|| io::Error::other("invalid UTF-8 filename"))? + .to_lowercase(); + Ok(extension == "masm") + } else { + Ok(false) + } + } + + /// Extract all masm errors from the given path and returns a map by error category. + pub fn extract_all_masm_errors(asm_source_dir: &Path) -> Result> { + // We use a BTree here to order the errors by their categories which is the first part after + // the ERR_ prefix and to allow for the same error to be defined multiple times in + // different files (as long as the constant name and error messages match). + let mut errors = BTreeMap::new(); + + // Walk all files of the kernel source directory. + for entry in WalkDir::new(asm_source_dir) { + let entry = entry.into_diagnostic()?; + if !is_masm_file(entry.path()).into_diagnostic()? { + continue; + } + let file_contents = std::fs::read_to_string(entry.path()).into_diagnostic()?; + extract_masm_errors(&mut errors, &file_contents)?; + } + + let errors = errors + .into_iter() + .map(|(error_name, error)| NamedError { name: error_name, message: error.message }) + .collect(); + + Ok(errors) + } + + /// Extracts the errors from a single masm file and inserts them into the provided map. + pub fn extract_masm_errors( + errors: &mut BTreeMap, + file_contents: &str, + ) -> Result<()> { + let regex = Regex::new(r#"const\s*ERR_(?.*)\s*=\s*"(?.*)""#).unwrap(); + + for capture in regex.captures_iter(file_contents) { + let error_name = capture + .name("name") + .expect("error name should be captured") + .as_str() + .trim() + .to_owned(); + let error_message = capture + .name("message") + .expect("error code should be captured") + .as_str() + .trim() + .to_owned(); + + if let Some(ExtractedError { message: existing_error_message, .. }) = + errors.get(&error_name) + && existing_error_message != &error_message + { + return Err(Report::msg(format!( + "Transaction kernel error constant ERR_{error_name} is already defined elsewhere but its error message is different" + ))); + } + + // Enforce the "no trailing punctuation" rule from the Rust error guidelines on MASM + // errors. + if error_message.ends_with(".") { + return Err(Report::msg(format!( + "Error messages should not end with a period: `ERR_{error_name}: {error_message}`" + ))); + } + + errors.insert(error_name, ExtractedError { message: error_message }); + } + + Ok(()) + } + + pub fn is_new_error_category<'a>( + last_error: &mut Option<&'a str>, + current_error: &'a str, + ) -> bool { + let is_new = match last_error { + Some(last_err) => { + let last_category = + last_err.split("_").next().expect("there should be at least one entry"); + let new_category = + current_error.split("_").next().expect("there should be at least one entry"); + last_category != new_category + }, + None => false, + }; + + last_error.replace(current_error); + + is_new + } + + /// Generates the content of an error file for the given category and the set of errors and + /// writes it to the category's file. + pub fn generate_error_file(module: ErrorModule, errors: Vec) -> Result<()> { + let mut output = String::new(); + + if module.is_crate_local { + writeln!(output, "use crate::errors::MasmError;\n").unwrap(); + } else { + writeln!(output, "use miden_protocol::errors::MasmError;\n").unwrap(); + } + + writeln!( + output, + "// 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, ...). +" + ) + .unwrap(); + + writeln!( + output, + "// {} +// ================================================================================================ +", + module.array_name.replace("_", " ") + ) + .unwrap(); + + let mut last_error = None; + for named_error in errors.iter() { + let NamedError { name, message } = named_error; + + // Group errors into blocks separate by newlines. + if is_new_error_category(&mut last_error, name) { + writeln!(output).into_diagnostic()?; + } + + writeln!(output, "/// Error Message: \"{message}\"").into_diagnostic()?; + writeln!( + output, + r#"pub const ERR_{name}: MasmError = MasmError::from_static_str("{message}");"# + ) + .into_diagnostic()?; + } + + std::fs::write(module.file_name, output).into_diagnostic()?; + + Ok(()) + } + + pub type ErrorName = String; + + #[derive(Debug, Clone)] + pub struct ExtractedError { + pub message: String, + } + + #[derive(Debug, Clone)] + pub struct NamedError { + pub name: ErrorName, + pub message: String, + } + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct ErrorModule { + pub file_name: &'static str, + pub array_name: &'static str, + pub is_crate_local: bool, + } +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs new file mode 100644 index 0000000000..efa9275dee --- /dev/null +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -0,0 +1,33 @@ +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: "most-significant 4 bytes (addr4) must be zero" +pub const ERR_ADDR4_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes (addr4) must be zero"); + +/// 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: "B2AGG script expects exactly 6 note inputs" +pub const ERR_B2AGG_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("B2AGG script expects exactly 6 note inputs"); + +/// Error Message: "CLAIM's target account address and transaction address do not match" +pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM's target account address and transaction address do not match"); + +/// 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: "invalid claim proof" +pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); + +/// Error Message: "address limb is not u32" +pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); + +/// Error Message: "maximum scaling factor is 18" +pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("maximum scaling factor is 18"); diff --git a/crates/miden-agglayer/src/errors/mod.rs b/crates/miden-agglayer/src/errors/mod.rs new file mode 100644 index 0000000000..2a86602e6f --- /dev/null +++ b/crates/miden-agglayer/src/errors/mod.rs @@ -0,0 +1,3 @@ +// Include generated error constants +#[cfg(any(feature = "testing", test))] +include!("agglayer.rs"); diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs new file mode 100644 index 0000000000..f2a94ed6df --- /dev/null +++ b/crates/miden-agglayer/src/eth_address.rs @@ -0,0 +1,243 @@ +use alloc::format; +use alloc::string::{String, ToString}; +use core::fmt; + +use miden_core::FieldElement; +use miden_protocol::Felt; +use miden_protocol::account::AccountId; +use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; + +// ================================================================================================ +// ETHEREUM ADDRESS +// ================================================================================================ + +/// Represents an Ethereum address format (20 bytes). +/// +/// # Representations used in this module +/// +/// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the +/// most-significant byte). +/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *little-endian limb order*: +/// - addr0 = bytes[16..19] (least-significant 4 bytes) +/// - addr1 = bytes[12..15] +/// - addr2 = bytes[ 8..11] +/// - addr3 = bytes[ 4.. 7] +/// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) +/// - Embedded AccountId format: `0x00000000 || prefix(8) || suffix(8)`, where: +/// - prefix = (addr3 << 32) | addr2 = bytes[4..11] as a big-endian u64 +/// - suffix = (addr1 << 32) | addr0 = bytes[12..19] as a big-endian u64 +/// +/// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure +/// `Felt::new(u64)` does not reduce mod p (checked explicitly in `to_account_id`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EthAddressFormat([u8; 20]); + +impl EthAddressFormat { + // EXTERNAL API - For integrators (Gateway, claim managers, etc.) + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`EthAddressFormat`] from a 20-byte array. + pub const fn new(bytes: [u8; 20]) -> Self { + Self(bytes) + } + + /// Creates an [`EthAddressFormat`] from a hex string (with or without "0x" prefix). + /// + /// # Errors + /// + /// Returns an error if the hex string is invalid or the hex part is not exactly 40 characters. + pub fn from_hex(hex_str: &str) -> Result { + let hex_part = hex_str.strip_prefix("0x").unwrap_or(hex_str); + if hex_part.len() != 40 { + return Err(AddressConversionError::InvalidHexLength); + } + + let prefixed_hex = if hex_str.starts_with("0x") { + hex_str.to_string() + } else { + format!("0x{}", hex_str) + }; + + let bytes: [u8; 20] = hex_to_bytes(&prefixed_hex)?; + Ok(Self(bytes)) + } + + /// Creates an [`EthAddressFormat`] from an [`AccountId`]. + /// + /// **External API**: This function is used by integrators (Gateway, claim managers) to convert + /// Miden AccountIds into the Ethereum address format for constructing CLAIM notes or + /// interfacing when calling the Agglayer Bridge function bridgeAsset(). + /// + /// This conversion is infallible: an [`AccountId`] is two felts, and `as_int()` yields `u64` + /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). + /// + /// # Example + /// ```ignore + /// let destination_address = EthAddressFormat::from_account_id(destination_account_id).into_bytes(); + /// // then construct the CLAIM note with destination_address... + /// ``` + pub fn from_account_id(account_id: AccountId) -> Self { + let felts: [Felt; 2] = account_id.into(); + + let mut out = [0u8; 20]; + out[4..12].copy_from_slice(&felts[0].as_int().to_be_bytes()); + out[12..20].copy_from_slice(&felts[1].as_int().to_be_bytes()); + + Self(out) + } + + /// Returns the raw 20-byte array. + pub const fn as_bytes(&self) -> &[u8; 20] { + &self.0 + } + + /// Converts the address into a 20-byte array. + pub const fn into_bytes(self) -> [u8; 20] { + self.0 + } + + /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). + pub fn to_hex(&self) -> String { + bytes_to_hex_string(self.0) + } + + // INTERNAL API - For CLAIM note processing + // -------------------------------------------------------------------------------------------- + + /// Converts the Ethereum address format into an array of 5 [`Felt`] values for MASM processing. + /// + /// **Internal API**: This function is used internally during CLAIM note processing to convert + /// the address format into the MASM `address[5]` representation expected by the + /// `to_account_id` procedure. + /// + /// The returned order matches the MASM `address\[5\]` convention (*little-endian limb order*): + /// - addr0 = bytes[16..19] (least-significant 4 bytes) + /// - addr1 = bytes[12..15] + /// - addr2 = bytes[ 8..11] + /// - addr3 = bytes[ 4.. 7] + /// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) + /// + /// Each limb is interpreted as a big-endian `u32` and stored in a [`Felt`]. + pub fn to_elements(&self) -> [Felt; 5] { + let mut result = [Felt::ZERO; 5]; + + // i=0 -> bytes[16..20], i=4 -> bytes[0..4] + for (felt, chunk) in result.iter_mut().zip(self.0.chunks(4).skip(1).rev()) { + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + // u32 values always fit in Felt, so this conversion is safe + *felt = Felt::try_from(value as u64).expect("u32 value should always fit in Felt"); + } + + result + } + + /// Converts the Ethereum address format back to an [`AccountId`]. + /// + /// **Internal API**: This function is used internally during CLAIM note processing to extract + /// the original AccountId from the Ethereum address format. It mirrors the functionality of + /// the MASM `to_account_id` procedure. + /// + /// # Errors + /// + /// Returns an error if: + /// - the first 4 bytes are not zero (not in the embedded AccountId format), + /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p, + /// - or the resulting felts do not form a valid [`AccountId`]. + pub fn to_account_id(&self) -> Result { + let (prefix, suffix) = Self::bytes20_to_prefix_suffix(self.0)?; + + // Use `Felt::try_from(u64)` to avoid potential truncating conversion + let prefix_felt = + Felt::try_from(prefix).map_err(|_| AddressConversionError::FeltOutOfField)?; + + let suffix_felt = + Felt::try_from(suffix).map_err(|_| AddressConversionError::FeltOutOfField)?; + + AccountId::try_from([prefix_felt, suffix_felt]) + .map_err(|_| AddressConversionError::InvalidAccountId) + } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. + /// Requires the first 4 bytes be zero. + /// Returns prefix and suffix values that match the MASM little-endian limb implementation: + /// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2 + /// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0 + fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddressConversionError> { + if bytes[0..4] != [0, 0, 0, 0] { + return Err(AddressConversionError::NonZeroBytePrefix); + } + + let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); + let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); + + Ok((prefix, suffix)) + } +} + +impl fmt::Display for EthAddressFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl From<[u8; 20]> for EthAddressFormat { + fn from(bytes: [u8; 20]) -> Self { + Self(bytes) + } +} + +impl From for EthAddressFormat { + fn from(account_id: AccountId) -> Self { + EthAddressFormat::from_account_id(account_id) + } +} + +impl From for [u8; 20] { + fn from(addr: EthAddressFormat) -> Self { + addr.0 + } +} + +// ================================================================================================ +// ADDRESS CONVERSION ERROR +// ================================================================================================ + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AddressConversionError { + NonZeroWordPadding, + NonZeroBytePrefix, + InvalidHexLength, + InvalidHexChar(char), + HexParseError, + FeltOutOfField, + InvalidAccountId, +} + +impl fmt::Display for AddressConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AddressConversionError::NonZeroWordPadding => write!(f, "non-zero word padding"), + AddressConversionError::NonZeroBytePrefix => { + write!(f, "address has non-zero 4-byte prefix") + }, + AddressConversionError::InvalidHexLength => { + write!(f, "invalid hex length (expected 40 hex chars)") + }, + AddressConversionError::InvalidHexChar(c) => write!(f, "invalid hex character: {}", c), + AddressConversionError::HexParseError => write!(f, "hex parse error"), + AddressConversionError::FeltOutOfField => { + write!(f, "packed 64-bit word does not fit in the field") + }, + AddressConversionError::InvalidAccountId => write!(f, "invalid AccountId"), + } + } +} + +impl From for AddressConversionError { + fn from(_err: HexParseError) -> Self { + AddressConversionError::HexParseError + } +} diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs new file mode 100644 index 0000000000..7020a384a4 --- /dev/null +++ b/crates/miden-agglayer/src/lib.rs @@ -0,0 +1,609 @@ +#![no_std] + +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use miden_assembly::Library; +use miden_assembly::utils::Deserializable; +use miden_core::{Felt, FieldElement, Program, Word}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountId, + AccountStorageMode, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteTag, + NoteType, +}; +use miden_standards::account::auth::NoAuth; +use miden_standards::account::faucets::NetworkFungibleFaucet; +use miden_utils_sync::LazyLock; + +pub mod errors; +pub mod eth_address; +pub mod utils; + +pub use eth_address::EthAddressFormat; +use utils::bytes32_to_felts; + +// AGGLAYER NOTE SCRIPTS +// ================================================================================================ + +// 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")); + Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed") +}); + +/// Returns the B2AGG (Bridge to AggLayer) note script. +pub fn b2agg_script() -> Program { + B2AGG_SCRIPT.clone() +} + +// 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 program = Program::read_from_bytes(bytes).expect("Shipped CLAIM script is well-formed"); + NoteScript::new(program) +}); + +/// Returns the CLAIM (Bridge from AggLayer) note script. +pub fn claim_script() -> NoteScript { + CLAIM_SCRIPT.clone() +} + +// AGGLAYER ACCOUNT COMPONENTS +// ================================================================================================ + +// Initialize the unified AggLayer library only once +static AGGLAYER_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/agglayer.masl")); + Library::read_from_bytes(bytes).expect("Shipped AggLayer library is well-formed") +}); + +/// Returns the unified AggLayer Library containing all agglayer modules. +pub fn agglayer_library() -> Library { + AGGLAYER_LIBRARY.clone() +} + +/// Returns the Bridge Out Library. +/// +/// Note: This is now the same as agglayer_library() since all agglayer components +/// are compiled into a single library. +pub fn bridge_out_library() -> Library { + agglayer_library() +} + +/// Returns the Local Exit Tree Library. +/// +/// Note: This is now the same as agglayer_library() since all agglayer components +/// are compiled into a single library. +pub fn local_exit_tree_library() -> Library { + agglayer_library() +} + +/// Creates a Local Exit Tree component with the specified storage slots. +/// +/// This component uses the local_exit_tree library and can be added to accounts +/// that need to manage local exit tree functionality. +pub fn local_exit_tree_component(storage_slots: Vec) -> AccountComponent { + let library = local_exit_tree_library(); + + AccountComponent::new(library, storage_slots) + .expect("local_exit_tree component should satisfy the requirements of a valid account component") + .with_supports_all_types() +} + +/// Creates a Bridge Out component with the specified storage slots. +/// +/// This component uses the bridge_out library and can be added to accounts +/// that need to bridge assets out to the AggLayer. +pub fn bridge_out_component(storage_slots: Vec) -> AccountComponent { + let library = bridge_out_library(); + + AccountComponent::new(library, storage_slots) + .expect("bridge_out component should satisfy the requirements of a valid account component") + .with_supports_all_types() +} + +/// Returns the Bridge In Library. +/// +/// Note: This is now the same as agglayer_library() since all agglayer components +/// are compiled into a single library. +pub fn bridge_in_library() -> Library { + agglayer_library() +} + +/// Creates a Bridge In component with the specified storage slots. +/// +/// This component uses the agglayer library and can be added to accounts +/// that need to bridge assets in from the AggLayer. +pub fn bridge_in_component(storage_slots: Vec) -> AccountComponent { + let library = bridge_in_library(); + + AccountComponent::new(library, storage_slots) + .expect("bridge_in component should satisfy the requirements of a valid account component") + .with_supports_all_types() +} + +/// Returns the Agglayer Faucet Library. +/// +/// Note: This is now the same as agglayer_library() since all agglayer components +/// are compiled into a single library. +pub fn agglayer_faucet_library() -> Library { + agglayer_library() +} + +/// Creates an Agglayer Faucet component with the specified storage slots. +/// +/// This component combines network faucet functionality with bridge validation +/// via Foreign Procedure Invocation (FPI). It provides a "claim" procedure that +/// validates CLAIM notes against a bridge MMR account before minting assets. +pub fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { + let library = agglayer_faucet_library(); + + AccountComponent::new(library, storage_slots) + .expect("agglayer_faucet component should satisfy the requirements of a valid account component") + .with_supports_all_types() +} + +/// Creates a combined Bridge Out component that includes both bridge_out and local_exit_tree +/// modules. +/// +/// This is a convenience function that creates a component with multiple modules. +/// For more fine-grained control, use the individual component functions and combine them +/// using the AccountBuilder pattern. +pub fn bridge_out_with_local_exit_tree_component( + storage_slots: Vec, +) -> Vec { + vec![ + bridge_out_component(storage_slots.clone()), + local_exit_tree_component(vec![]), // local_exit_tree typically doesn't need storage slots + ] +} + +/// Creates an Asset Conversion component with the specified storage slots. +/// +/// This component uses the agglayer library (which includes asset_conversion) and can be added to +/// accounts that need to convert assets between Miden and Ethereum formats. +pub fn asset_conversion_component(storage_slots: Vec) -> AccountComponent { + let library = agglayer_library(); + + AccountComponent::new(library, storage_slots) + .expect("asset_conversion component should satisfy the requirements of a valid account component") + .with_supports_all_types() +} + +// AGGLAYER ACCOUNT CREATION HELPERS +// ================================================================================================ + +/// Creates a bridge account component with the standard bridge storage slot. +/// +/// This is a convenience function that creates the bridge storage slot with the standard +/// name "miden::agglayer::bridge" and returns the bridge_out component. +/// +/// # Returns +/// Returns an [`AccountComponent`] configured for bridge operations with MMR validation. +pub fn create_bridge_account_component() -> AccountComponent { + let bridge_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge") + .expect("Bridge storage slot name should be valid"); + let bridge_storage_slots = vec![StorageSlot::with_empty_map(bridge_storage_slot_name)]; + bridge_out_component(bridge_storage_slots) +} + +/// Creates an agglayer faucet account component with the specified configuration. +/// +/// This function creates all the necessary storage slots for an agglayer faucet: +/// - Network faucet metadata slot (max_supply, decimals, token_symbol) +/// - Bridge account reference slot for FPI validation +/// +/// # Parameters +/// - `token_symbol`: The symbol for the fungible token (e.g., "AGG") +/// - `decimals`: Number of decimal places for the token +/// - `max_supply`: Maximum supply of the token +/// - `bridge_account_id`: The account ID of the bridge account for validation +/// +/// # Returns +/// Returns an [`AccountComponent`] configured for agglayer faucet operations. +/// +/// # Panics +/// Panics if the token symbol is invalid or storage slot names are malformed. +pub fn create_agglayer_faucet_component( + token_symbol: &str, + decimals: u8, + max_supply: Felt, + bridge_account_id: AccountId, +) -> AccountComponent { + // Create network faucet metadata slot: [max_supply, decimals, token_symbol, 0] + let token_symbol = TokenSymbol::new(token_symbol).expect("Token symbol should be valid"); + let metadata_word = + Word::new([max_supply, Felt::from(decimals), token_symbol.into(), FieldElement::ZERO]); + let metadata_slot = + StorageSlot::with_value(NetworkFungibleFaucet::metadata_slot().clone(), metadata_word); + + // Create agglayer-specific bridge storage slot + let bridge_account_id_word = Word::new([ + Felt::new(0), + Felt::new(0), + bridge_account_id.suffix(), + bridge_account_id.prefix().as_felt(), + ]); + let agglayer_storage_slot_name = StorageSlotName::new("miden::agglayer::faucet") + .expect("Agglayer faucet storage slot name should be valid"); + let bridge_slot = StorageSlot::with_value(agglayer_storage_slot_name, bridge_account_id_word); + + // Combine all storage slots for the agglayer faucet component + let agglayer_storage_slots = vec![metadata_slot, bridge_slot]; + agglayer_faucet_component(agglayer_storage_slots) +} + +/// Creates a complete bridge account builder with the standard configuration. +pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { + let bridge_component = create_bridge_account_component(); + Account::builder(seed.into()) + .storage_mode(AccountStorageMode::Public) + .with_component(bridge_component) +} + +/// Creates a new bridge account with the standard configuration. +/// +/// This creates a new account suitable for production use. +pub fn create_bridge_account(seed: Word) -> Account { + create_bridge_account_builder(seed) + .with_auth_component(AccountComponent::from(NoAuth)) + .build() + .expect("Bridge account should be valid") +} + +/// Creates an existing bridge account with the standard configuration. +/// +/// This creates an existing account suitable for testing scenarios. +#[cfg(any(feature = "testing", test))] +pub fn create_existing_bridge_account(seed: Word) -> Account { + create_bridge_account_builder(seed) + .with_auth_component(AccountComponent::from(NoAuth)) + .build_existing() + .expect("Bridge account should be valid") +} + +/// Creates a complete agglayer faucet account builder with the specified configuration. +pub fn create_agglayer_faucet_builder( + seed: Word, + token_symbol: &str, + decimals: u8, + max_supply: Felt, + bridge_account_id: AccountId, +) -> AccountBuilder { + let agglayer_component = + create_agglayer_faucet_component(token_symbol, decimals, max_supply, bridge_account_id); + + Account::builder(seed.into()) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Network) + .with_component(agglayer_component) +} + +/// Creates a new agglayer faucet account with the specified configuration. +/// +/// This creates a new account suitable for production use. +pub fn create_agglayer_faucet( + seed: Word, + token_symbol: &str, + decimals: u8, + max_supply: Felt, + bridge_account_id: AccountId, +) -> Account { + create_agglayer_faucet_builder(seed, token_symbol, decimals, max_supply, bridge_account_id) + .with_auth_component(AccountComponent::from(NoAuth)) + .build() + .expect("Agglayer faucet account should be valid") +} + +/// Creates an existing agglayer faucet account with the specified configuration. +/// +/// This creates an existing account suitable for testing scenarios. +#[cfg(any(feature = "testing", test))] +pub fn create_existing_agglayer_faucet( + seed: Word, + token_symbol: &str, + decimals: u8, + max_supply: Felt, + bridge_account_id: AccountId, +) -> Account { + create_agglayer_faucet_builder(seed, token_symbol, decimals, max_supply, bridge_account_id) + .with_auth_component(AccountComponent::from(NoAuth)) + .build_existing() + .expect("Agglayer faucet account should be valid") +} + +// AGGLAYER NOTE CREATION HELPERS +// ================================================================================================ + +/// Parameters for creating a CLAIM note. +/// +/// This struct groups all the parameters needed to create a CLAIM note that exactly +/// matches the agglayer claimAsset function signature. +pub struct ClaimNoteParams<'a, R: FeltRng> { + /// AGGLAYER claimAsset function parameters + /// SMT proof for local exit root (bytes32\[_DEPOSIT_CONTRACT_TREE_DEPTH\]) + pub smt_proof_local_exit_root: Vec, + /// SMT proof for rollup exit root (bytes32\[_DEPOSIT_CONTRACT_TREE_DEPTH\]) + pub smt_proof_rollup_exit_root: Vec, + /// Global index (uint256 as 8 u32 felts) + pub global_index: [Felt; 8], + /// Mainnet exit root hash (bytes32 as 32-byte array) + pub mainnet_exit_root: &'a [u8; 32], + /// Rollup exit root hash (bytes32 as 32-byte array) + pub rollup_exit_root: &'a [u8; 32], + /// Origin network identifier (uint32) + pub origin_network: Felt, + /// Origin token address (address as 20-byte array) + pub origin_token_address: &'a [u8; 20], + /// Destination network identifier (uint32) + pub destination_network: Felt, + /// Destination address (address as 20-byte array) + pub destination_address: &'a [u8; 20], + /// Amount of tokens (uint256 as 8 u32 felts) + pub amount: [Felt; 8], + /// ABI encoded metadata (fixed size of 8 felts) + pub metadata: [Felt; 8], + /// CLAIM note required parameters + /// CLAIM note sender account id + pub claim_note_creator_account_id: AccountId, + /// Agglayer faucet AccountId + pub agglayer_faucet_account_id: AccountId, + /// Output P2ID note tag + pub output_note_tag: NoteTag, + /// P2ID note serial number (4 felts as Word) + pub p2id_serial_number: Word, + /// TODO: remove and use destination_address: [u8; 20] + pub destination_account_id: AccountId, + /// RNG for creating CLAIM note serial number + pub rng: &'a mut R, +} + +/// Generates a CLAIM note - a note that instructs an agglayer faucet to validate and mint assets. +/// +/// # Parameters +/// - `params`: The parameters for creating the CLAIM note (including RNG) +/// +/// # Errors +/// Returns an error if note creation fails. +pub fn create_claim_note(params: ClaimNoteParams<'_, R>) -> Result { + // Validate SMT proof lengths - each should be 256 felts (32 bytes32 values * 8 u32 per bytes32) + if params.smt_proof_local_exit_root.len() != 256 { + return Err(NoteError::other(alloc::format!( + "SMT proof local exit root must be exactly 256 felts, got {}", + params.smt_proof_local_exit_root.len() + ))); + } + if params.smt_proof_rollup_exit_root.len() != 256 { + return Err(NoteError::other(alloc::format!( + "SMT proof rollup exit root must be exactly 256 felts, got {}", + params.smt_proof_rollup_exit_root.len() + ))); + } + // Create claim inputs matching exactly the agglayer claimAsset function parameters + let mut claim_inputs = vec![]; + + // 1) PROOF DATA + // smtProofLocalExitRoot (256 felts) - first SMT proof parameter + claim_inputs.extend(params.smt_proof_local_exit_root); + // smtProofRollupExitRoot (256 felts) - second SMT proof parameter + claim_inputs.extend(params.smt_proof_rollup_exit_root); + + // globalIndex (uint256 as 8 u32 felts) + claim_inputs.extend(params.global_index); + + // mainnetExitRoot (bytes32 as 8 u32 felts) + let mainnet_exit_root_felts = bytes32_to_felts(params.mainnet_exit_root); + claim_inputs.extend(mainnet_exit_root_felts); + + // rollupExitRoot (bytes32 as 8 u32 felts) + let rollup_exit_root_felts = bytes32_to_felts(params.rollup_exit_root); + claim_inputs.extend(rollup_exit_root_felts); + + // 2) LEAF DATA + // originNetwork (uint32 as Felt) + claim_inputs.push(params.origin_network); + + // originTokenAddress (address as 5 u32 felts) + let origin_token_address_felts = + EthAddressFormat::new(*params.origin_token_address).to_elements().to_vec(); + claim_inputs.extend(origin_token_address_felts); + + // destinationNetwork (uint32 as Felt) + claim_inputs.push(params.destination_network); + + // destinationAddress (address as 5 u32 felts) + // Use AccountId prefix and suffix directly to get [suffix, prefix, 0, 0, 0] + // TODO: refactor to use destination_address: [u8; 20] instead once conversion function + // exists [u8; 20] -> [address as 5 Felts] + let destination_address_felts = vec![ + params.destination_account_id.prefix().as_felt(), + params.destination_account_id.suffix(), + Felt::new(0), + Felt::new(0), + Felt::new(0), + ]; + claim_inputs.extend(destination_address_felts); + + // amount (uint256 as 8 u32 felts) + claim_inputs.extend(params.amount); + + // metadata (fixed size of 8 felts) + claim_inputs.extend(params.metadata); + + let padding = vec![Felt::ZERO; 4]; + claim_inputs.extend(padding); + + // 3) CLAIM NOTE DATA + // TODO: deterministically compute serial number of p2id hash(GER, leaf index) + // output_p2id_serial_num (4 felts as Word) + claim_inputs.extend(params.p2id_serial_number); + + // agglayer_faucet_account_id (2 felts: prefix and suffix) + claim_inputs.push(params.agglayer_faucet_account_id.prefix().as_felt()); + claim_inputs.push(params.agglayer_faucet_account_id.suffix()); + + // output note tag + claim_inputs.push(params.output_note_tag.as_u32().into()); + + let inputs = NoteInputs::new(claim_inputs)?; + + let tag = NoteTag::with_account_target(params.agglayer_faucet_account_id); + + let claim_script = claim_script(); + let serial_num = params.rng.draw_word(); + + let note_type = NoteType::Public; + + // Use a default sender since we don't have sender anymore - create from destination address + let metadata = NoteMetadata::new(params.claim_note_creator_account_id, note_type, tag); + let assets = NoteAssets::new(vec![])?; + let recipient = NoteRecipient::new(serial_num, claim_script, inputs); + + Ok(Note::new(assets, metadata, recipient)) +} + +// TESTING HELPERS +// ================================================================================================ + +#[cfg(any(feature = "testing", test))] +/// Type alias for the complex return type of claim_note_test_inputs. +/// +/// Contains: +/// - smt_proof_local_exit_root: `Vec` (256 felts) +/// - smt_proof_rollup_exit_root: `Vec` (256 felts) +/// - global_index: [Felt; 8] +/// - mainnet_exit_root: [u8; 32] +/// - rollup_exit_root: [u8; 32] +/// - origin_network: Felt +/// - origin_token_address: [u8; 20] +/// - destination_network: Felt +/// - destination_address: [u8; 20] +/// - amount: [Felt; 8] +/// - metadata: [Felt; 8] +pub type ClaimNoteTestInputs = ( + Vec, + Vec, + [Felt; 8], + [u8; 32], + [u8; 32], + Felt, + [u8; 20], + Felt, + [u8; 20], + [Felt; 8], + [Felt; 8], +); + +#[cfg(any(feature = "testing", test))] +/// Returns dummy test inputs for creating CLAIM notes. +/// +/// This is a convenience function for testing that provides realistic dummy data +/// for all the agglayer claimAsset function inputs. +/// +/// # Parameters +/// - `amount`: The amount as a single Felt for Miden operations +/// - `destination_account_id`: The destination account ID to convert to address bytes +/// +/// # Returns +/// A tuple containing: +/// - smt_proof_local_exit_root: `Vec` (256 felts) +/// - smt_proof_rollup_exit_root: `Vec` (256 felts) +/// - global_index: [Felt; 8] +/// - mainnet_exit_root: [u8; 32] +/// - rollup_exit_root: [u8; 32] +/// - origin_network: Felt +/// - origin_token_address: [u8; 20] +/// - destination_network: Felt +/// - destination_address: [u8; 20] +/// - amount: [Felt; 8] +/// - metadata: [Felt; 8] +pub fn claim_note_test_inputs( + amount: Felt, + destination_account_id: AccountId, +) -> ClaimNoteTestInputs { + // Create SMT proofs with 256 felts each (32 bytes32 values * 8 u32 per bytes32) + let smt_proof_local_exit_root = vec![Felt::new(0); 256]; + let smt_proof_rollup_exit_root = vec![Felt::new(0); 256]; + let global_index = [ + Felt::new(12345), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + ]; + + let mainnet_exit_root: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, + ]; + + let rollup_exit_root: [u8; 32] = [ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, + ]; + + let origin_network = Felt::new(1); + + let origin_token_address: [u8; 20] = [ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, + ]; + + let destination_network = Felt::new(2); + + // Convert AccountId to destination address bytes + let destination_address = + EthAddressFormat::from_account_id(destination_account_id).into_bytes(); + + // Convert amount Felt to u256 array for agglayer + let amount_u256 = [ + amount, + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + ]; + let metadata: [Felt; 8] = [Felt::new(0); 8]; + + ( + smt_proof_local_exit_root, + smt_proof_rollup_exit_root, + global_index, + mainnet_exit_root, + rollup_exit_root, + origin_network, + origin_token_address, + destination_network, + destination_address, + amount_u256, + metadata, + ) +} diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs new file mode 100644 index 0000000000..88850de58c --- /dev/null +++ b/crates/miden-agglayer/src/utils.rs @@ -0,0 +1,28 @@ +use miden_core::FieldElement; +use miden_protocol::Felt; + +// UTILITY FUNCTIONS +// ================================================================================================ + +/// Converts a bytes32 value (32 bytes) into an array of 8 Felt values. +/// +/// Note: These utility functions will eventually be replaced with similar functions from miden-vm. +pub fn bytes32_to_felts(bytes32: &[u8; 32]) -> [Felt; 8] { + let mut result = [Felt::ZERO; 8]; + for (i, chunk) in bytes32.chunks(4).enumerate() { + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + result[i] = Felt::from(value); + } + result +} + +/// Convert 8 Felt values (u32 limbs in little-endian order) to U256 bytes in little-endian format. +pub fn felts_to_u256_bytes(limbs: [Felt; 8]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (i, limb) in limbs.iter().enumerate() { + let u32_value = limb.as_int() as u32; + let limb_bytes = u32_value.to_le_bytes(); + bytes[i * 4..(i + 1) * 4].copy_from_slice(&limb_bytes); + } + bytes +} diff --git a/crates/miden-block-prover/Cargo.toml b/crates/miden-block-prover/Cargo.toml index d774870d03..be8732f5d7 100644 --- a/crates/miden-block-prover/Cargo.toml +++ b/crates/miden-block-prover/Cargo.toml @@ -19,6 +19,5 @@ bench = false testing = [] [dependencies] -miden-lib = { workspace = true } -miden-objects = { workspace = true } -thiserror = { workspace = true } +miden-protocol = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/miden-block-prover/src/errors.rs b/crates/miden-block-prover/src/errors.rs index b1c99cac18..4ae35efc30 100644 --- a/crates/miden-block-prover/src/errors.rs +++ b/crates/miden-block-prover/src/errors.rs @@ -1,30 +1,8 @@ -use miden_objects::{AccountTreeError, NullifierTreeError, Word}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ProvenBlockError { - #[error("nullifier witness has a different root than the current nullifier tree root")] - NullifierWitnessRootMismatch(#[source] NullifierTreeError), - - #[error("failed to track account witness")] - AccountWitnessTracking { source: AccountTreeError }, - - #[error("account ID prefix already exists in the tree")] - AccountIdPrefixDuplicate { source: AccountTreeError }, - - #[error( - "account tree root of the previous block header is {prev_block_account_root} but the root of the partial tree computed from account witnesses is {stale_account_root}, indicating that the witnesses are stale" - )] - StaleAccountTreeRoot { - prev_block_account_root: Word, - stale_account_root: Word, - }, - - #[error( - "nullifier tree root of the previous block header is {prev_block_nullifier_root} but the root of the partial tree computed from nullifier witnesses is {stale_nullifier_root}, indicating that the witnesses are stale" - )] - StaleNullifierTreeRoot { - prev_block_nullifier_root: Word, - stale_nullifier_root: Word, - }, -} +// BLOCK PROVER ERROR +// ================================================================================================ + +/// Represents errors that can occur during block proving. +/// +/// NOTE: Block proving is not yet implemented. This is a placeholder enum. +#[derive(Debug, thiserror::Error)] +pub enum BlockProverError {} diff --git a/crates/miden-block-prover/src/lib.rs b/crates/miden-block-prover/src/lib.rs index 5497535f0c..a4e18d92c2 100644 --- a/crates/miden-block-prover/src/lib.rs +++ b/crates/miden-block-prover/src/lib.rs @@ -1,5 +1,5 @@ -mod errors; -pub use errors::ProvenBlockError; - mod local_block_prover; pub use local_block_prover::LocalBlockProver; + +mod errors; +pub use errors::BlockProverError; diff --git a/crates/miden-block-prover/src/local_block_prover.rs b/crates/miden-block-prover/src/local_block_prover.rs index 5637009e29..4960d1a593 100644 --- a/crates/miden-block-prover/src/local_block_prover.rs +++ b/crates/miden-block-prover/src/local_block_prover.rs @@ -1,32 +1,12 @@ -use std::collections::BTreeMap; -use std::vec::Vec; +use miden_protocol::batch::OrderedBatches; +use miden_protocol::block::{BlockHeader, BlockInputs, BlockProof}; -use miden_lib::transaction::TransactionKernel; -use miden_objects::Word; -use miden_objects::account::AccountId; -use miden_objects::block::{ - AccountUpdateWitness, - BlockAccountUpdate, - BlockHeader, - BlockNoteIndex, - BlockNoteTree, - BlockNumber, - NullifierWitness, - OutputNoteBatch, - PartialAccountTree, - PartialNullifierTree, - ProposedBlock, - ProvenBlock, -}; -use miden_objects::note::Nullifier; -use miden_objects::transaction::PartialBlockchain; - -use crate::errors::ProvenBlockError; +use crate::BlockProverError; // LOCAL BLOCK PROVER // ================================================================================================ -/// A local prover for blocks, proving a [`ProposedBlock`] and returning a [`ProvenBlock`]. +/// A local prover for blocks in the chain. #[derive(Clone)] pub struct LocalBlockProver {} @@ -38,283 +18,29 @@ impl LocalBlockProver { Self {} } - /// Proves the provided [`ProposedBlock`] into a [`ProvenBlock`]. - /// - /// For now this does not actually verify the batches or create a block proof, but will be added - /// in the future. + /// Generates a proof of a block in the chain based on the given header and inputs. /// - /// # Errors - /// - /// Returns an error if: - /// - the account witnesses provided in the proposed block result in a different account tree - /// root than the contained previous block header commits to. - /// - the nullifier witnesses provided in the proposed block result in a different nullifier - /// tree root than the contained previous block header commits to. - /// - the account tree root in the previous block header does not match the root of the tree - /// computed from the account witnesses. - /// - the nullifier tree root in the previous block header does not match the root of the tree - /// computed from the nullifier witnesses. - pub fn prove(&self, proposed_block: ProposedBlock) -> Result { - self.prove_without_batch_verification_inner(proposed_block) + /// NOTE: Block proving is not yet implemented. This is a placeholder struct. + pub fn prove( + &self, + _tx_batches: OrderedBatches, + _block_header: BlockHeader, + _block_inputs: BlockInputs, + ) -> Result { + Ok(BlockProof {}) } - /// Proves the provided [`ProposedBlock`] into a [`ProvenBlock`], **without verifying batches - /// and proving the block**. + /// A mock implementation of the execution of a proof of a block in the chain based on the given + /// header and inputs. /// /// This is exposed for testing purposes. #[cfg(any(feature = "testing", test))] pub fn prove_dummy( &self, - proposed_block: ProposedBlock, - ) -> Result { - self.prove_without_batch_verification_inner(proposed_block) + _tx_batches: OrderedBatches, + _block_header: BlockHeader, + _block_inputs: BlockInputs, + ) -> Result { + Ok(BlockProof {}) } - - /// Proves the provided [`ProposedBlock`] into a [`ProvenBlock`]. - /// - /// The assumptions of this method are that the checks made by construction of a - /// [`ProposedBlock`] are enforced. - /// - /// See [`Self::prove`] for more details. - fn prove_without_batch_verification_inner( - &self, - proposed_block: ProposedBlock, - ) -> Result { - // Get the block number and timestamp of the new block and compute the tx commitment. - // -------------------------------------------------------------------------------------------- - - let block_num = proposed_block.block_num(); - let timestamp = proposed_block.timestamp(); - - // Split the proposed block into its parts. - // -------------------------------------------------------------------------------------------- - - let ( - batches, - account_updated_witnesses, - output_note_batches, - created_nullifiers, - partial_blockchain, - prev_block_header, - ) = proposed_block.into_parts(); - - let prev_block_commitment = prev_block_header.commitment(); - // For now we copy the parameters of the previous header, which means the parameters set on - // the genesis block will be passed through. Eventually, the contained base fees will be - // updated based on the demand in the currently proposed block. - let fee_parameters = prev_block_header.fee_parameters().clone(); - - // Compute the root of the block note tree. - // -------------------------------------------------------------------------------------------- - - let note_tree = compute_block_note_tree(&output_note_batches); - let note_root = note_tree.root(); - - // Insert the created nullifiers into the nullifier tree to compute its new root. - // -------------------------------------------------------------------------------------------- - - let (created_nullifiers, new_nullifier_root) = - compute_nullifiers(created_nullifiers, &prev_block_header, block_num)?; - - // Insert the state commitments of updated accounts into the account tree to compute its new - // root. - // -------------------------------------------------------------------------------------------- - - let new_account_root = - compute_account_root(&account_updated_witnesses, &prev_block_header)?; - - // Insert the previous block header into the block partial blockchain to get the new chain - // commitment. - // -------------------------------------------------------------------------------------------- - - let new_chain_commitment = compute_chain_commitment(partial_blockchain, prev_block_header); - - // Transform the account update witnesses into block account updates. - // -------------------------------------------------------------------------------------------- - - let updated_accounts = account_updated_witnesses - .into_iter() - .map(|(account_id, update_witness)| { - let ( - _initial_state_commitment, - final_state_commitment, - // Note that compute_account_root took out this value so it should not be used. - _initial_state_proof, - details, - ) = update_witness.into_parts(); - BlockAccountUpdate::new(account_id, final_state_commitment, details) - }) - .collect(); - - // Aggregate the verified transactions of all batches. - // -------------------------------------------------------------------------------------------- - - let txs = batches.into_transactions(); - let tx_commitment = txs.commitment(); - - // Construct the new block header. - // -------------------------------------------------------------------------------------------- - - // Currently undefined and reserved for future use. - // See miden-base/1155. - let version = 0; - let tx_kernel_commitment = TransactionKernel.to_commitment(); - - // For now, we're not actually proving the block. - let proof_commitment = Word::empty(); - - let header = BlockHeader::new( - version, - prev_block_commitment, - block_num, - new_chain_commitment, - new_account_root, - new_nullifier_root, - note_root, - tx_commitment, - tx_kernel_commitment, - proof_commitment, - fee_parameters, - timestamp, - ); - - // Construct the new proven block. - // -------------------------------------------------------------------------------------------- - - let proven_block = ProvenBlock::new_unchecked( - header, - updated_accounts, - output_note_batches, - created_nullifiers, - txs, - ); - - Ok(proven_block) - } -} - -/// Computes the new nullifier root by inserting the nullifier witnesses into a partial nullifier -/// tree and marking each nullifier as spent in the given block number. Returns the list of -/// nullifiers and the new nullifier tree root. -fn compute_nullifiers( - created_nullifiers: BTreeMap, - prev_block_header: &BlockHeader, - block_num: BlockNumber, -) -> Result<(Vec, Word), ProvenBlockError> { - // If no nullifiers were created, the nullifier tree root is unchanged. - if created_nullifiers.is_empty() { - return Ok((Vec::new(), prev_block_header.nullifier_root())); - } - - let nullifiers: Vec = created_nullifiers.keys().copied().collect(); - - // First, reconstruct the current nullifier tree with the merkle paths of the nullifiers we want - // to update. - // Due to the guarantees of ProposedBlock we can safely assume that each nullifier is mapped to - // its corresponding nullifier witness, so we don't have to check again whether they match. - let mut partial_nullifier_tree = - PartialNullifierTree::with_witnesses(created_nullifiers.into_values()) - .map_err(ProvenBlockError::NullifierWitnessRootMismatch)?; - - // Check the nullifier tree root in the previous block header matches the reconstructed tree's - // root. - if prev_block_header.nullifier_root() != partial_nullifier_tree.root() { - return Err(ProvenBlockError::StaleNullifierTreeRoot { - prev_block_nullifier_root: prev_block_header.nullifier_root(), - stale_nullifier_root: partial_nullifier_tree.root(), - }); - } - - // Second, mark each nullifier as spent in the tree. Note that checking whether each nullifier - // is unspent is checked as part of the proposed block. - - // SAFETY: As mentioned above, we can safely assume that each nullifier's witness was - // added and every nullifier should be tracked by the partial tree and - // therefore updatable. - partial_nullifier_tree.mark_spent(nullifiers.iter().copied(), block_num).expect( - "nullifiers' merkle path should have been added to the partial tree and the nullifiers should be unspent", - ); - - Ok((nullifiers, partial_nullifier_tree.root())) -} - -/// Adds the commitment of the previous block header to the partial blockchain to compute the new -/// chain commitment. -fn compute_chain_commitment( - mut partial_blockchain: PartialBlockchain, - prev_block_header: BlockHeader, -) -> Word { - // SAFETY: This does not panic as long as the block header we're adding is the next one in the - // chain which is validated as part of constructing a `ProposedBlock`. - partial_blockchain.add_block(prev_block_header, true); - partial_blockchain.peaks().hash_peaks() -} - -/// Computes the new account tree root after the given updates. -/// -/// It uses a PartialMerkleTree for now while we use a SimpleSmt for the account tree. Once that is -/// updated to an Smt, we can use a PartialSmt instead. -fn compute_account_root( - updated_accounts: &[(AccountId, AccountUpdateWitness)], - prev_block_header: &BlockHeader, -) -> Result { - // If no accounts were updated, the account tree root is unchanged. - if updated_accounts.is_empty() { - return Ok(prev_block_header.account_root()); - } - - // First reconstruct the current account tree from the provided merkle paths. - // If a witness points to a leaf where multiple account IDs share the same prefix, this will - // return an error. - let mut partial_account_tree = PartialAccountTree::with_witnesses( - updated_accounts.iter().map(|(_, update_witness)| update_witness.to_witness()), - ) - .map_err(|source| ProvenBlockError::AccountWitnessTracking { source })?; - - // Check the account tree root in the previous block header matches the reconstructed tree's - // root. - if prev_block_header.account_root() != partial_account_tree.root() { - return Err(ProvenBlockError::StaleAccountTreeRoot { - prev_block_account_root: prev_block_header.account_root(), - stale_account_root: partial_account_tree.root(), - }); - } - - // Second, update the account tree by inserting the new final account state commitments to - // compute the new root of the account tree. - // If an account ID's prefix already exists in the tree, this will return an error. - // Note that we have inserted all witnesses that we want to update into the partial account - // tree, so we should not run into the untracked key error. - partial_account_tree - .upsert_state_commitments(updated_accounts.iter().map(|(account_id, update_witness)| { - (*account_id, update_witness.final_state_commitment()) - })) - .map_err(|source| ProvenBlockError::AccountIdPrefixDuplicate { source })?; - - Ok(partial_account_tree.root()) -} - -/// Compute the block note tree from the output note batches. -fn compute_block_note_tree(output_note_batches: &[OutputNoteBatch]) -> BlockNoteTree { - let output_notes_iter = - output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| { - notes.iter().map(move |(note_idx_in_batch, note)| { - ( - // SAFETY: The proposed block contains at most the max allowed number of - // batches and each batch is guaranteed to contain at most - // the max allowed number of output notes. - BlockNoteIndex::new(batch_idx, *note_idx_in_batch) - .expect("max batches in block and max notes in batches should be enforced"), - note.id(), - *note.metadata(), - ) - }) - }); - - // SAFETY: We only construct proposed blocks that: - // - do not contain duplicates - // - contain at most the max allowed number of batches and each batch is guaranteed to contain - // at most the max allowed number of output notes. - BlockNoteTree::with_entries(output_notes_iter) - .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum") } diff --git a/crates/miden-lib/asm/account_components/network_fungible_faucet.masm b/crates/miden-lib/asm/account_components/network_fungible_faucet.masm deleted file mode 100644 index b88db104ce..0000000000 --- a/crates/miden-lib/asm/account_components/network_fungible_faucet.masm +++ /dev/null @@ -1,6 +0,0 @@ -# The MASM code of the Network Fungible Faucet Account Component. -# -# See the `NetworkFungibleFaucet` Rust type's documentation for more details. - -export.::miden::contracts::faucets::network_fungible::distribute -export.::miden::contracts::faucets::network_fungible::burn diff --git a/crates/miden-lib/asm/kernels/transaction/lib/constants.masm b/crates/miden-lib/asm/kernels/transaction/lib/constants.masm deleted file mode 100644 index 2a5e76a9f3..0000000000 --- a/crates/miden-lib/asm/kernels/transaction/lib/constants.masm +++ /dev/null @@ -1,153 +0,0 @@ -# CONSTANTS -# ================================================================================================= - -# The number of elements in a Word -const.WORD_SIZE=4 - -# The maximum number of input values associated with a single note. -const.MAX_INPUTS_PER_NOTE=128 - -# The maximum number of assets that can be stored in a single note. -const.MAX_ASSETS_PER_NOTE=256 - -# The maximum number of notes that can be consumed in a single transaction. -const.MAX_INPUT_NOTES_PER_TX=1024 - -# The size of the memory segment allocated to each note. -const.NOTE_MEM_SIZE=2048 - -# The depth of the Merkle tree used to commit to notes produced in a block. -const.NOTE_TREE_DEPTH=16 - -# The maximum number of notes that can be created in a single transaction. -const.MAX_OUTPUT_NOTES_PER_TX=1024 - -# TYPES -# ================================================================================================= - -# Type of storage slot item in the account storage -const.STORAGE_SLOT_TYPE_VALUE=0 -const.STORAGE_SLOT_TYPE_MAP=1 -const.STORAGE_SLOT_TYPE_ARRAY=2 - -# PROCEDURES -# ================================================================================================= - -#! Returns the number of elements in a Word. -#! -#! Inputs: [] -#! Outputs: [word_size] -#! -#! Where: -#! - word_size is the number of elements in a Word. -export.get_word_size - push.WORD_SIZE -end - -#! Returns the max allowed number of input values per note. -#! -#! Inputs: [] -#! Outputs: [max_inputs_per_note] -#! -#! Where: -#! - max_inputs_per_note is the max inputs per note. -export.::$kernel::util::note::get_max_inputs_per_note - -#! Returns the max allowed number of assets per note. -#! -#! Inputs: [] -#! Outputs: [max_assets_per_note] -#! -#! Where: -#! - max_assets_per_note is the max assets per note. -export.get_max_assets_per_note - push.MAX_ASSETS_PER_NOTE -end - -#! Returns the maximum number of notes that can be consumed in a single transaction. -#! -#! Inputs: [] -#! Outputs: [max_num_input_notes] -#! -#! Where: -#! - max_num_input_notes is the max number of input notes. -export.get_max_num_input_notes - push.MAX_INPUT_NOTES_PER_TX -end - -#! Returns the size of the memory segment allocated to each note. -#! -#! Inputs: [] -#! Outputs: [note_mem_size] -#! -#! Where: -#! - note_mem_size is the size of the memory segment allocated to each note. -export.get_note_mem_size - push.NOTE_MEM_SIZE -end - -#! Returns the depth of the Merkle tree used to commit to notes produced in a block. -#! -#! Inputs: [] -#! Outputs: [note_tree_depth] -#! -#! Where: -#! - note_tree_depth is the depth of the Merkle tree used to commit to notes produced in a block. -export.get_note_tree_depth - push.NOTE_TREE_DEPTH -end - -#! Returns the maximum number of notes that can be created in a single transaction. -#! -#! Inputs: [] -#! Outputs: [max_num_output_notes] -#! -#! Where: -#! - max_num_output_notes is the max number of notes that can be created in a single transaction. -export.get_max_num_output_notes - push.MAX_OUTPUT_NOTES_PER_TX -end - -#! Returns the root of an empty Sparse Merkle Tree. -#! -#! Inputs: [] -#! Outputs: [EMPTY_SMT_ROOT] -#! -#! Where: -#! - EMPTY_SMT_ROOT is the root of an empty Sparse Merkle Tree. -export.get_empty_smt_root - push.15321474589252129342.17373224439259377994.15071539326562317628.3312677166725950353 -end - -#! Returns the type of storage slot value in the account storage. -#! -#! Inputs: [] -#! Outputs: [type_storage_value] -#! -#! Where: -#! - type_storage_value is the type of storage slot item in the account storage. -export.get_storage_slot_type_value - push.STORAGE_SLOT_TYPE_VALUE -end - -#! Returns the type of storage slot map in the account storage. -#! -#! Inputs: [] -#! Outputs: [type_storage_map] -#! -#! Where: -#! - type_storage_map is the type of storage slot item in the account storage. -export.get_storage_slot_type_map - push.STORAGE_SLOT_TYPE_MAP -end - -#! Returns the type of storage slot array in the account storage. -#! -#! Inputs: [] -#! Outputs: [type_storage_array] -#! -#! Where: -#! - type_storage_array is the type of storage slot item in the account storage. -export.get_storage_slot_type_array - push.STORAGE_SLOT_TYPE_ARRAY -end diff --git a/crates/miden-lib/asm/miden/contracts/wallets/basic.masm b/crates/miden-lib/asm/miden/contracts/wallets/basic.masm deleted file mode 100644 index 0094eec5af..0000000000 --- a/crates/miden-lib/asm/miden/contracts/wallets/basic.masm +++ /dev/null @@ -1,61 +0,0 @@ -use.miden::native_account -use.miden::output_note - -# CONSTANTS -# ================================================================================================= -const.PUBLIC_NOTE=1 - -#! Adds the provided asset to the active account. -#! -#! Inputs: [ASSET, pad(12)] -#! Outputs: [pad(16)] -#! -#! Where: -#! - ASSET is the asset to be received, can be fungible or non-fungible -#! -#! Panics if: -#! - the same non-fungible asset already exists in the account. -#! - adding a fungible asset would result in amount overflow, i.e., -#! the total amount would be greater than 2^63. -#! -#! Invocation: call -export.receive_asset - exec.native_account::add_asset - # => [ASSET', pad(12)] - - # drop the final asset - dropw - # => [pad(16)] -end - -#! Removes the specified asset from the account and adds it to the output note with the specified -#! index. -#! -#! This procedure is expected to be invoked using a `call` instruction. It makes no guarantees about -#! the contents of the `PAD` elements shown below. It is the caller's responsibility to make sure -#! these elements do not contain any meaningful data. -#! -#! Inputs: [ASSET, note_idx, pad(11)] -#! Outputs: [ASSET, note_idx, pad(11)] -#! -#! Where: -#! - note_idx is the index of the output note. -#! - ASSET is the fungible or non-fungible asset of interest. -#! -#! Panics if: -#! - the fungible asset is not found in the vault. -#! - the amount of the fungible asset in the vault is less than the amount to be removed. -#! - the non-fungible asset is not found in the vault. -#! -#! Invocation: call -export.move_asset_to_note - # remove the asset from the account - exec.native_account::remove_asset - # => [ASSET, note_idx, pad(11)] - - dupw dup.8 movdn.4 - # => [ASSET, note_idx, ASSET, note_idx, pad(11)] - - exec.output_note::add_asset - # => [ASSET, note_idx, pad(11)] -end diff --git a/crates/miden-lib/asm/note_scripts/MINT.masm b/crates/miden-lib/asm/note_scripts/MINT.masm deleted file mode 100644 index fec23f48a4..0000000000 --- a/crates/miden-lib/asm/note_scripts/MINT.masm +++ /dev/null @@ -1,61 +0,0 @@ -use.miden::active_note -use.miden::contracts::faucets::network_fungible->network_faucet - -# CONSTANTS -# ================================================================================================= - -const.MINT_NOTE_INPUTS_NUMBER=9 - -# ERRORS -# ================================================================================================= -const.ERR_MINT_WRONG_NUMBER_OF_INPUTS="MINT script expects exactly 9 note inputs" - -#! Network Faucet MINT script: mints assets by calling the network faucet's distribute function. -#! This note is intended to be executed against a network fungible faucet account. -#! -#! Requires that the account exposes: -#! - miden::contracts::faucets::network_fungible::distribute procedure. -#! -#! Inputs: [ARGS, pad(12)] -#! Outputs: [pad(16)] -#! -#! Note inputs are assumed to be as follows (in order): -#! - RECIPIENT: The recipient account ID (4 elements) -#! - Output note config (4 elements): -#! - execution_hint: Execution hint for the output note -#! - note_type: Type of the output note -#! - aux: Auxiliary data for the output note -#! - tag: Note tag for the output note -#! - amount: The amount to mint -#! -#! Panics if: -#! - account does not expose distribute procedure. -#! - the number of inputs is not exactly 9. -begin - dropw - # => [pad(16)] - - # Load note inputs into memory starting at address 0 - push.0 exec.active_note::get_inputs - # => [num_inputs, inputs_ptr, pad(16)] - - # Verify we have the correct number of inputs - eq.MINT_NOTE_INPUTS_NUMBER assert.err=ERR_MINT_WRONG_NUMBER_OF_INPUTS drop - # => [pad(16)] - - # Load amount - mem_loadw_be.0 - # => [RECIPIENT, pad(12)] - - swapw mem_loadw_be.4 - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] - - mem_load.8 - # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] - - movup.9 drop - # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - call.network_faucet::distribute - # => [pad(16)] -end diff --git a/crates/miden-lib/asm/shared_utils/util/note.masm b/crates/miden-lib/asm/shared_utils/util/note.masm deleted file mode 100644 index dc1bc21855..0000000000 --- a/crates/miden-lib/asm/shared_utils/util/note.masm +++ /dev/null @@ -1,19 +0,0 @@ -# CONSTANTS -# ================================================================================================= - -# The maximum number of input values associated with a single note. -const.MAX_INPUTS_PER_NOTE=128 - -# PROCEDURES -# ================================================================================================= - -#! Returns the max allowed number of input values per note. -#! -#! Inputs: [] -#! Outputs: [max_inputs_per_note] -#! -#! Where: -#! - max_inputs_per_note is the max inputs per note. -export.get_max_inputs_per_note - push.MAX_INPUTS_PER_NOTE -end diff --git a/crates/miden-lib/build.rs b/crates/miden-lib/build.rs deleted file mode 100644 index 10254ae490..0000000000 --- a/crates/miden-lib/build.rs +++ /dev/null @@ -1,917 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::env; -use std::fmt::Write; -use std::io::{self}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use fs_err as fs; -use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr, miette}; -use miden_assembly::utils::Serializable; -use miden_assembly::{ - Assembler, - DefaultSourceManager, - KernelLibrary, - Library, - LibraryNamespace, - Report, -}; -use regex::Regex; -use walkdir::WalkDir; - -/// A map where the key is the error name and the value is the error code with the message. -type ErrorCategoryMap = BTreeMap>; - -// 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_MIDEN_DIR: &str = "miden"; -const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts"; -const ASM_ACCOUNT_COMPONENTS_DIR: &str = "account_components"; -const SHARED_UTILS_DIR: &str = "shared_utils"; -const SHARED_MODULES_DIR: &str = "shared_modules"; -const ASM_TX_KERNEL_DIR: &str = "kernels/transaction"; -const KERNEL_PROCEDURES_RS_FILE: &str = "src/transaction/kernel_procedures.rs"; - -const TX_KERNEL_ERRORS_FILE: &str = "src/errors/tx_kernel_errors.rs"; -const NOTE_SCRIPT_ERRORS_FILE: &str = "src/errors/note_script_errors.rs"; - -const TX_KERNEL_ERRORS_ARRAY_NAME: &str = "TX_KERNEL_ERRORS"; -const NOTE_SCRIPT_ERRORS_ARRAY_NAME: &str = "NOTE_SCRIPT_ERRORS"; - -const TX_KERNEL_ERROR_CATEGORIES: [TxKernelErrorCategory; 14] = [ - TxKernelErrorCategory::Kernel, - TxKernelErrorCategory::Prologue, - TxKernelErrorCategory::Epilogue, - TxKernelErrorCategory::Tx, - TxKernelErrorCategory::Note, - TxKernelErrorCategory::Account, - TxKernelErrorCategory::ForeignAccount, - TxKernelErrorCategory::Faucet, - TxKernelErrorCategory::FungibleAsset, - TxKernelErrorCategory::NonFungibleAsset, - TxKernelErrorCategory::Vault, - TxKernelErrorCategory::LinkMap, - TxKernelErrorCategory::InputNote, - TxKernelErrorCategory::OutputNote, -]; - -// PRE-PROCESSING -// ================================================================================================ - -/// Read and parse the contents from `./asm`. -/// - Compiles contents of asm/miden directory into a Miden library file (.masl) under miden -/// namespace. -/// - Compiles contents of asm/scripts directory into individual .masb files. -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"); - - // 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(); - copy_directory(src, &dst)?; - - // set source directory to {OUT_DIR}/asm - let source_dir = dst.join(ASM_DIR); - - // copy the shared modules to the kernel and miden library folders - copy_shared_modules(&source_dir)?; - - // set target directory to {OUT_DIR}/assets - let target_dir = Path::new(&build_dir).join(ASSETS_DIR); - - // compile transaction kernel - let mut assembler = - compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?; - - // compile miden library - let miden_lib = compile_miden_lib(&source_dir, &target_dir, assembler.clone())?; - assembler.link_dynamic_library(miden_lib)?; - - // compile note scripts - compile_note_scripts( - &source_dir.join(ASM_NOTE_SCRIPTS_DIR), - &target_dir.join(ASM_NOTE_SCRIPTS_DIR), - assembler.clone(), - )?; - - // compile account components - compile_account_components( - &source_dir.join(ASM_ACCOUNT_COMPONENTS_DIR), - &target_dir.join(ASM_ACCOUNT_COMPONENTS_DIR), - assembler, - )?; - - generate_error_constants(&source_dir)?; - - generate_event_constants(&source_dir, &target_dir)?; - Ok(()) -} - -// COMPILE TRANSACTION KERNEL -// ================================================================================================ - -/// Reads the transaction kernel MASM source from the `source_dir`, compiles it, saves the results -/// to the `target_dir`, and returns an [Assembler] instantiated with the compiled kernel. -/// -/// Additionally it compiles the transaction script executor program, see the -/// [compile_tx_script_main] procedure for details. -/// -/// `source_dir` is expected to have the following structure: -/// -/// - {source_dir}/api.masm -> defines exported procedures from the transaction kernel. -/// - {source_dir}/main.masm -> defines the executable program of the transaction kernel. -/// - {source_dir}/tx_script_main -> defines the executable program of the arbitrary transaction -/// script. -/// - {source_dir}/lib -> contains common modules used by both api.masm and main.masm. -/// -/// The compiled files are written as follows: -/// -/// - {target_dir}/tx_kernel.masl -> contains kernel library compiled from api.masm. -/// - {target_dir}/tx_kernel.masb -> contains the executable compiled from main.masm. -/// - {target_dir}/tx_script_main.masb -> contains the executable compiled from -/// tx_script_main.masm. -/// - src/transaction/procedures/kernel_v0.rs -> contains the kernel procedures table. -fn compile_tx_kernel(source_dir: &Path, target_dir: &Path) -> Result { - let shared_utils_path = Path::new(ASM_DIR).join(SHARED_UTILS_DIR); - let kernel_namespace = LibraryNamespace::Kernel; - - let mut assembler = build_assembler(None)?; - // add the shared util modules to the kernel lib under the kernel::util namespace - assembler.compile_and_statically_link_from_dir(kernel_namespace.clone(), &shared_utils_path)?; - - // assemble the kernel library and write it to the "tx_kernel.masl" file - let kernel_lib = assembler - .assemble_kernel_from_dir(source_dir.join("api.masm"), Some(source_dir.join("lib")))?; - - // generate kernel `procedures.rs` file - generate_kernel_proc_hash_file(kernel_lib.clone())?; - - let output_file = target_dir.join("tx_kernel").with_extension(Library::LIBRARY_EXTENSION); - kernel_lib.write_to_file(output_file).into_diagnostic()?; - - let assembler = build_assembler(Some(kernel_lib))?; - - // assemble the kernel program and write it to the "tx_kernel.masb" file - let mut main_assembler = assembler.clone(); - // add the shared util modules to the kernel lib under the kernel::util namespace - main_assembler - .compile_and_statically_link_from_dir(kernel_namespace.clone(), &shared_utils_path)?; - main_assembler - .compile_and_statically_link_from_dir(kernel_namespace.clone(), source_dir.join("lib"))?; - - let main_file_path = source_dir.join("main.masm"); - let kernel_main = main_assembler.clone().assemble_program(main_file_path)?; - - let masb_file_path = target_dir.join("tx_kernel.masb"); - kernel_main.write_to_file(masb_file_path).into_diagnostic()?; - - // compile the transaction script main program - compile_tx_script_main(source_dir, target_dir, main_assembler)?; - - #[cfg(any(feature = "testing", test))] - { - let mut kernel_lib_assembler = assembler.clone(); - // Build kernel as a library and save it to file. - // This is needed in test assemblers to access individual procedures which would otherwise - // be hidden when using KernelLibrary (api.masm) - - // add the shared util modules to the kernel lib under the kernel::util namespace - kernel_lib_assembler - .compile_and_statically_link_from_dir(kernel_namespace.clone(), &shared_utils_path)?; - - let test_lib = kernel_lib_assembler - .assemble_library_from_dir(source_dir.join("lib"), kernel_namespace) - .unwrap(); - - let masb_file_path = - target_dir.join("kernel_library").with_extension(Library::LIBRARY_EXTENSION); - test_lib.write_to_file(masb_file_path).into_diagnostic()?; - } - - Ok(assembler) -} - -/// Reads the transaction script executor MASM source from the `source_dir/tx_script_main.masm`, -/// compiles it and saves the results to the `target_dir` as a `tx_script_main.masb` binary file. -fn compile_tx_script_main( - source_dir: &Path, - target_dir: &Path, - main_assembler: Assembler, -) -> Result<()> { - // assemble the transaction script executor program and write it to the "tx_script_main.masb" - // file. - let tx_script_main_file_path = source_dir.join("tx_script_main.masm"); - let tx_script_main = main_assembler.assemble_program(tx_script_main_file_path)?; - - let masb_file_path = target_dir.join("tx_script_main.masb"); - tx_script_main.write_to_file(masb_file_path).into_diagnostic() -} - -/// Generates kernel `procedures.rs` file based on the kernel library -fn generate_kernel_proc_hash_file(kernel: KernelLibrary) -> Result<()> { - // Because the kernel Rust file will be stored under ./src, this should be a no-op if we can't - // write there - if !BUILD_GENERATED_FILES_IN_SRC { - return Ok(()); - } - - let (_, module_info, _) = kernel.into_parts(); - - let to_exclude = BTreeSet::from_iter(["exec_kernel_proc"]); - let offsets_filename = Path::new(ASM_DIR).join(ASM_MIDEN_DIR).join("kernel_proc_offsets.masm"); - let offsets = parse_proc_offsets(&offsets_filename)?; - let generated_procs: BTreeMap = module_info - .procedures() - .filter(|(_, proc_info)| !to_exclude.contains::(proc_info.name.as_ref())) - .map(|(_, proc_info)| { - let name = proc_info.name.to_string(); - - let Some(&offset) = offsets.get(&name) else { - panic!("Offset constant for function `{name}` not found in `{offsets_filename:?}`"); - }; - - (offset, format!(" // {name}\n word!(\"{}\"),", proc_info.digest)) - }) - .collect(); - - let proc_count = generated_procs.len(); - let generated_procs: String = generated_procs.into_iter().enumerate().map(|(index, (offset, txt))| { - if index != offset { - panic!("Offset constants in the file `{offsets_filename:?}` are not contiguous (missing offset: {index})"); - } - - txt - }).collect::>().join("\n"); - - fs::write( - KERNEL_PROCEDURES_RS_FILE, - format!( - r#"// This file is generated by build.rs, do not modify - -use miden_objects::{{Word, word}}; - -// KERNEL PROCEDURES -// ================================================================================================ - -/// Hashes of all dynamically executed kernel procedures. -pub const KERNEL_PROCEDURES: [Word; {proc_count}] = [ -{generated_procs} -]; -"#, - ), - ) - .into_diagnostic() -} - -fn parse_proc_offsets(filename: impl AsRef) -> Result> { - let regex: Regex = Regex::new(r"^const\.(?P\w+)_OFFSET\s*=\s*(?P\d+)").unwrap(); - let mut result = BTreeMap::new(); - for line in fs::read_to_string(filename).into_diagnostic()?.lines() { - if let Some(captures) = regex.captures(line) { - result.insert( - captures["name"].to_string().to_lowercase(), - captures["offset"].parse().into_diagnostic()?, - ); - } - } - - Ok(result) -} - -// COMPILE MIDEN LIB -// ================================================================================================ - -/// Reads the MASM files from "{source_dir}/miden" directory, compiles them into a Miden assembly -/// library, saves the library into "{target_dir}/miden.masl", and returns the compiled library. -fn compile_miden_lib( - source_dir: &Path, - target_dir: &Path, - mut assembler: Assembler, -) -> Result { - let source_dir = source_dir.join(ASM_MIDEN_DIR); - let shared_path = Path::new(ASM_DIR).join(SHARED_UTILS_DIR); - - let miden_namespace = "miden".parse::().expect("invalid base namespace"); - // add the shared modules to the kernel lib under the miden::util namespace - assembler.compile_and_statically_link_from_dir(miden_namespace.clone(), &shared_path)?; - - let miden_lib = assembler.assemble_library_from_dir(source_dir, miden_namespace)?; - - let output_file = target_dir.join("miden").with_extension(Library::LIBRARY_EXTENSION); - miden_lib.write_to_file(output_file).into_diagnostic()?; - - Ok(miden_lib) -} - -// COMPILE EXECUTABLE MODULES -// ================================================================================================ - -/// Reads all MASM files from the "{source_dir}", complies each file individually into a MASB -/// file, and stores the compiled files into the "{target_dir}". -/// -/// The source files are expected to contain executable programs. -fn compile_note_scripts(source_dir: &Path, target_dir: &Path, assembler: Assembler) -> Result<()> { - fs::create_dir_all(target_dir) - .into_diagnostic() - .wrap_err("failed to create note_scripts directory")?; - - for masm_file_path in get_masm_files(source_dir).unwrap() { - // read the MASM file, parse it, and serialize the parsed AST to bytes - let code = assembler.clone().assemble_program(masm_file_path.clone())?; - - let bytes = code.to_bytes(); - - let masm_file_name = masm_file_path - .file_name() - .expect("file name should exist") - .to_str() - .ok_or_else(|| Report::msg("failed to convert file name to &str"))?; - let mut masb_file_path = target_dir.join(masm_file_name); - - // write the binary MASB to the output dir - masb_file_path.set_extension("masb"); - fs::write(masb_file_path, bytes).unwrap(); - } - Ok(()) -} - -// COMPILE ACCOUNT COMPONENTS -// ================================================================================================ - -/// Compiles the account components in `source_dir` into MASL libraries and stores the compiled -/// files in `target_dir`. -fn compile_account_components( - source_dir: &Path, - target_dir: &Path, - assembler: Assembler, -) -> Result<()> { - if !target_dir.exists() { - fs::create_dir_all(target_dir).unwrap(); - } - - for masm_file_path in get_masm_files(source_dir).unwrap() { - let component_name = masm_file_path - .file_stem() - .expect("masm file should have a file stem") - .to_str() - .expect("file stem should be valid UTF-8") - .to_owned(); - - // Read the source code to string instead of passing it to assemble_library directly since - // that would attempt to interpret the path as a LibraryPath which would fail. - let component_source_code = fs::read_to_string(masm_file_path) - .expect("reading the component's MASM source code should succeed"); - - let component_library = assembler - .clone() - .assemble_library([component_source_code]) - .expect("library assembly should succeed"); - let component_file_path = - target_dir.join(component_name).with_extension(Library::LIBRARY_EXTENSION); - component_library.write_to_file(component_file_path).into_diagnostic()?; - } - - Ok(()) -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Returns a new [Assembler] loaded with miden-stdlib and the specified kernel, if provided. -/// -/// The returned assembler will be in the `debug` mode if the `with-debug-info` feature is enabled. -fn build_assembler(kernel: Option) -> Result { - kernel - .map(|kernel| Assembler::with_kernel(Arc::new(DefaultSourceManager::default()), kernel)) - .unwrap_or_default() - .with_debug_mode(cfg!(feature = "with-debug-info")) - .with_dynamic_library(miden_stdlib::StdLibrary::default()) -} - -/// Recursively copies `src` into `dst`. -/// -/// This function will overwrite the existing files if re-executed. -fn copy_directory, R: AsRef>(src: T, dst: R) -> 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(()) -} - -/// Copies the content of the build `shared_modules` folder to the `lib` and `miden` build folders. -/// This is required to include the shared modules as APIs of the `kernel` and `miden` libraries. -/// -/// This is done to make it possible to import the modules in the `shared_modules` folder directly, -/// i.e. "use.$kernel::account_id". -fn copy_shared_modules>(source_dir: T) -> Result<()> { - // source is expected to be an `OUT_DIR/asm` folder - let shared_modules_dir = source_dir.as_ref().join(SHARED_MODULES_DIR); - - for module_path in get_masm_files(shared_modules_dir).unwrap() { - let module_name = module_path.file_name().unwrap(); - - // copy to kernel lib - let kernel_lib_folder = source_dir.as_ref().join(ASM_TX_KERNEL_DIR).join("lib"); - fs::copy(&module_path, kernel_lib_folder.join(module_name)).into_diagnostic()?; - - // copy to miden lib - let miden_lib_folder = source_dir.as_ref().join(ASM_MIDEN_DIR); - fs::copy(&module_path, miden_lib_folder.join(module_name)).into_diagnostic()?; - } - - Ok(()) -} - -/// Returns a vector with paths to all MASM files in the specified directory. -/// -/// All non-MASM files are skipped. -fn get_masm_files>(dir_path: P) -> Result> { - let mut files = Vec::new(); - - let path = dir_path.as_ref(); - if path.is_dir() { - let entries = fs::read_dir(path) - .into_diagnostic() - .wrap_err_with(|| format!("failed to read directory {}", path.display()))?; - for entry in entries { - let file = entry.into_diagnostic().wrap_err("failed to read directory entry")?; - let file_path = file.path(); - if is_masm_file(&file_path).into_diagnostic()? { - files.push(file_path); - } - } - } else { - println!("cargo:warn=The specified path is not a directory."); - } - - Ok(files) -} - -/// Returns true if the provided path resolves to a file with `.masm` extension. -/// -/// # Errors -/// Returns an error if the path could not be converted to a UTF-8 string. -fn is_masm_file(path: &Path) -> io::Result { - if let Some(extension) = path.extension() { - let extension = extension - .to_str() - .ok_or_else(|| io::Error::other("invalid UTF-8 filename"))? - .to_lowercase(); - Ok(extension == "masm") - } else { - Ok(false) - } -} - -// ERROR CONSTANTS FILE GENERATION -// ================================================================================================ - -/// Reads all MASM files from the `asm_source_dir` and extracts its error constants and their -/// associated error message and generates a Rust file for each category of errors. -/// For example: -/// -/// ```text -/// const.ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY="new account must have an empty vault" -/// ``` -/// -/// would generate a Rust file for transaction kernel errors (since the error belongs to that -/// category, identified by the category extracted from `ERR_`) with - roughly - the -/// following content: -/// -/// ```rust -/// pub const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY: MasmError = -/// MasmError::from_static_str("new account must have an empty vault"); -/// ``` -/// -/// and add the constant to the error constants array. -/// -/// 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(()); - } - - let categories = - extract_all_masm_errors(asm_source_dir).context("failed to extract all masm errors")?; - - for (category, errors) in categories { - // Generate the errors file. - let error_file_content = generate_error_file_content(category, errors)?; - std::fs::write(category.error_file_name(), error_file_content).into_diagnostic()?; - } - - Ok(()) -} - -/// Extract all masm errors from the given path and returns a map by error category. -fn extract_all_masm_errors(asm_source_dir: &Path) -> Result { - // We use a BTree here to order the errors by their categories which is the first part after the - // ERR_ prefix and to allow for the same error to be defined multiple times in different files - // (as long as the constant name and error messages match). - let mut errors = BTreeMap::new(); - - // Walk all files of the kernel source directory. - for entry in WalkDir::new(asm_source_dir) { - let entry = entry.into_diagnostic()?; - if !is_masm_file(entry.path()).into_diagnostic()? { - continue; - } - let file_contents = std::fs::read_to_string(entry.path()).into_diagnostic()?; - extract_masm_errors(&mut errors, &file_contents)?; - } - - let mut category_map: BTreeMap> = BTreeMap::new(); - - for (error_name, error) in errors.into_iter() { - let category = ErrorCategory::match_category(&error_name)?; - - let named_error = NamedError { name: error_name, message: error.message }; - - category_map.entry(category).or_default().push(named_error); - } - - Ok(category_map) -} - -/// Extracts the errors from a single masm file and inserts them into the provided map. -fn extract_masm_errors( - errors: &mut BTreeMap, - file_contents: &str, -) -> Result<()> { - let regex = Regex::new(r#"const(\.|\ )ERR_(?.*)\ ?=\ ?"(?.*)""#).unwrap(); - - for capture in regex.captures_iter(file_contents) { - let error_name = capture - .name("name") - .expect("error name should be captured") - .as_str() - .trim() - .to_owned(); - let error_message = capture - .name("message") - .expect("error code should be captured") - .as_str() - .trim() - .to_owned(); - - if let Some(ExtractedError { message: existing_error_message, .. }) = - errors.get(&error_name) - && existing_error_message != &error_message - { - return Err(Report::msg(format!( - "Transaction kernel error constant ERR_{error_name} is already defined elsewhere but its error message is different" - ))); - } - - // Enforce the "no trailing punctuation" rule from the Rust error guidelines on MASM errors. - if error_message.ends_with(".") { - return Err(Report::msg(format!( - "Error messages should not end with a period: `ERR_{error_name}: {error_message}`" - ))); - } - - errors.insert(error_name, ExtractedError { message: error_message }); - } - - Ok(()) -} - -fn is_new_error_category<'a>(last_error: &mut Option<&'a str>, current_error: &'a str) -> bool { - let is_new = match last_error { - Some(last_err) => { - let last_category = - last_err.split("_").next().expect("there should be at least one entry"); - let new_category = - current_error.split("_").next().expect("there should be at least one entry"); - last_category != new_category - }, - None => false, - }; - - last_error.replace(current_error); - - is_new -} - -/// Generates the content of an error file for the given category and the set of errors. -fn generate_error_file_content(category: ErrorCategory, errors: Vec) -> Result { - let mut output = String::new(); - - writeln!(output, "use crate::errors::MasmError;\n").unwrap(); - - writeln!( - output, - "// This file is generated by build.rs, do not modify manually. -// It is generated by extracting errors from the masm files in the `miden-lib/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, Prologue, -// Non-Fungible-Asset, ...). -" - ) - .unwrap(); - - writeln!( - output, - "// {} -// ================================================================================================ -", - category.array_name().replace("_", " ") - ) - .unwrap(); - - let mut last_error = None; - for named_error in errors.iter() { - let NamedError { name, message } = named_error; - - // Group errors into blocks separate by newlines. - if is_new_error_category(&mut last_error, name) { - writeln!(output).into_diagnostic()?; - } - - writeln!(output, "/// Error Message: \"{message}\"").into_diagnostic()?; - writeln!( - output, - r#"pub const ERR_{name}: MasmError = MasmError::from_static_str("{message}");"# - ) - .into_diagnostic()?; - } - - Ok(output) -} - -type ErrorName = String; - -#[derive(Debug, Clone)] -struct ExtractedError { - message: String, -} - -#[derive(Debug, Clone)] -struct NamedError { - name: ErrorName, - message: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum ErrorCategory { - TxKernel, - NoteScript, -} - -impl ErrorCategory { - pub const fn error_file_name(&self) -> &'static str { - match self { - ErrorCategory::TxKernel => TX_KERNEL_ERRORS_FILE, - ErrorCategory::NoteScript => NOTE_SCRIPT_ERRORS_FILE, - } - } - - pub const fn array_name(&self) -> &'static str { - match self { - ErrorCategory::TxKernel => TX_KERNEL_ERRORS_ARRAY_NAME, - ErrorCategory::NoteScript => NOTE_SCRIPT_ERRORS_ARRAY_NAME, - } - } - - pub fn match_category(error_name: &ErrorName) -> Result { - for kernel_category in TX_KERNEL_ERROR_CATEGORIES { - if error_name.starts_with(kernel_category.category_name()) { - return Ok(ErrorCategory::TxKernel); - } - } - - // If the error is not a tx kernel error, consider it a note script error. - Ok(ErrorCategory::NoteScript) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum TxKernelErrorCategory { - Kernel, - Prologue, - Epilogue, - Tx, - Note, - Account, - ForeignAccount, - Faucet, - FungibleAsset, - NonFungibleAsset, - Vault, - LinkMap, - InputNote, - OutputNote, -} - -impl TxKernelErrorCategory { - pub const fn category_name(&self) -> &'static str { - match self { - TxKernelErrorCategory::Kernel => "KERNEL", - TxKernelErrorCategory::Prologue => "PROLOGUE", - TxKernelErrorCategory::Epilogue => "EPILOGUE", - TxKernelErrorCategory::Tx => "TX", - TxKernelErrorCategory::Note => "NOTE", - TxKernelErrorCategory::Account => "ACCOUNT", - TxKernelErrorCategory::ForeignAccount => "FOREIGN_ACCOUNT", - TxKernelErrorCategory::Faucet => "FAUCET", - TxKernelErrorCategory::FungibleAsset => "FUNGIBLE_ASSET", - TxKernelErrorCategory::NonFungibleAsset => "NON_FUNGIBLE_ASSET", - TxKernelErrorCategory::Vault => "VAULT", - TxKernelErrorCategory::LinkMap => "LINK_MAP", - TxKernelErrorCategory::InputNote => "INPUT_NOTE", - TxKernelErrorCategory::OutputNote => "OUTPUT_NOTE", - } - } -} - -// EVENT CONSTANTS FILE GENERATION -// ================================================================================================ - -/// Reads all MASM files from the `asm_source_dir` and extracts event definitions, -/// then generates the transaction_events.rs file with constants. -fn generate_event_constants(asm_source_dir: &Path, target_dir: &Path) -> Result<()> { - // Extract all event definitions from MASM files - let events = extract_all_event_definitions(asm_source_dir)?; - - // Generate the events file in OUT_DIR - let event_file_content = generate_event_file_content(&events).into_diagnostic()?; - let event_file_path = target_dir.join("transaction_events.rs"); - fs::write(event_file_path, event_file_content).into_diagnostic()?; - - Ok(()) -} - -/// Extract all `const.X=event("x")` definitions from all MASM files -fn extract_all_event_definitions(asm_source_dir: &Path) -> Result> { - // collect mappings event path to const variable name, we want a unique mapping - // which we use to generate the constants and enum variant names - let mut events = BTreeMap::new(); - - // Walk all MASM files - for entry in WalkDir::new(asm_source_dir) { - let entry = entry.into_diagnostic()?; - if !is_masm_file(entry.path()).into_diagnostic()? { - continue; - } - let file_contents = fs::read_to_string(entry.path()).into_diagnostic()?; - extract_event_definitions_from_file(&mut events, &file_contents, entry.path())?; - } - - Ok(events) -} - -/// Extract event definitions from a single MASM file in two possible forms: -/// - `const.${X}=event("${x::path}")` -/// - `const ${X} = event("${x::path}")` -fn extract_event_definitions_from_file( - events: &mut BTreeMap, - file_contents: &str, - file_path: &Path, -) -> Result<()> { - let regex = Regex::new(r#"const(\.|\ )(\w+)\ ?=\ ?event\("([^"]+)"\)"#).unwrap(); - - for capture in regex.captures_iter(file_contents) { - let const_name = capture.get(2).expect("const name should be captured"); - let event_path = capture.get(3).expect("event path should be captured"); - - let event_path = event_path.as_str(); - let const_name = const_name.as_str(); - - let const_name_wo_suffix = - if let Some((const_name_wo_suffix, _)) = const_name.rsplit_once("_EVENT") { - const_name_wo_suffix.to_string() - } else { - const_name.to_owned() - }; - - if !event_path.starts_with("miden::") { - // we ignore any `stdlib::` prefixed ones - if !event_path.starts_with("stdlib::") { - return Err(miette::miette!( - "unhandled `event_path={event_path}`, doesn't with `stdlib::` nor with `miden::`." - )); - } - continue; - } - - // Check for duplicates with different definitions - if let Some(existing_const_name) = events.get(event_path) { - if existing_const_name != &const_name_wo_suffix { - println!( - "cargo:warning=Duplicate event definition found {event_path} with different definitions names: - '{existing_const_name}' vs '{const_name}' in {}", - file_path.display() - ); - } - } else { - events.insert(event_path.to_owned(), const_name_wo_suffix.to_owned()); - } - } - - Ok(()) -} - -/// Generate the content of the transaction_events.rs file -fn generate_event_file_content( - events: &BTreeMap, -) -> std::result::Result { - use std::fmt::Write; - - let mut output = String::new(); - - writeln!(&mut output, "// This file is generated by build.rs, do not modify")?; - writeln!(&mut output)?; - - // Generate constants - // - // Note: If we ever encounter two constants `const.X`, that are both named `X` we will error - // when attempting to generate the rust code. Currently this is a side-effect, but we - // want to error out as early as possible: - // TODO: make the error out at build-time to be able to present better error hints - for (event_path, event_name) in events { - let value = miden_core::EventId::from_name(event_path).as_felt().as_int(); - debug_assert!(!event_name.is_empty()); - writeln!(&mut output, "const {}: u64 = {};", event_name, value)?; - } - - { - writeln!(&mut output)?; - - writeln!(&mut output)?; - - writeln!( - &mut output, - r###" -use alloc::collections::BTreeMap; - -pub(crate) static EVENT_NAME_LUT: ::miden_objects::utils::sync::LazyLock> = - ::miden_objects::utils::sync::LazyLock::new(|| {{ - BTreeMap::from_iter([ -"### - )?; - - for (event_path, const_name) in events { - writeln!(&mut output, " ({}, \"{}\"),", const_name, event_path)?; - } - - writeln!( - &mut output, - r###" ]) -}});"### - )?; - } - - Ok(output) -} diff --git a/crates/miden-lib/src/account/auth/ecdsa_k256_keccak.rs b/crates/miden-lib/src/account/auth/ecdsa_k256_keccak.rs deleted file mode 100644 index d7005a1028..0000000000 --- a/crates/miden-lib/src/account/auth/ecdsa_k256_keccak.rs +++ /dev/null @@ -1,39 +0,0 @@ -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountComponent, StorageSlot}; - -use crate::account::components::ecdsa_k256_keccak_library; - -/// An [`AccountComponent`] implementing the ECDSA K256 Keccak signature scheme for authentication -/// of transactions. -/// -/// It reexports the procedures from `miden::contracts::auth::basic`. When linking against this -/// component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be available to the -/// assembler which is the case when using [`TransactionKernel::assembler()`][kasm]. The procedures -/// of this component are: -/// - `auth_tx_ecdsa_k256_keccak`, which can be used to verify a signature provided via the advice -/// stack to authenticate a transaction. -/// -/// This component supports all account types. -/// -/// [kasm]: crate::transaction::TransactionKernel::assembler -pub struct AuthEcdsaK256Keccak { - pub_key: PublicKeyCommitment, -} - -impl AuthEcdsaK256Keccak { - /// Creates a new [`AuthEcdsaK256Keccak`] component with the given `public_key`. - pub fn new(pub_key: PublicKeyCommitment) -> Self { - Self { pub_key } - } -} - -impl From for AccountComponent { - fn from(ecdsa: AuthEcdsaK256Keccak) -> Self { - AccountComponent::new( - ecdsa_k256_keccak_library(), - vec![StorageSlot::Value(ecdsa.pub_key.into())], - ) - .expect("ecdsa component should satisfy the requirements of a valid account component") - .with_supports_all_types() - } -} diff --git a/crates/miden-lib/src/account/auth/rpo_falcon_512.rs b/crates/miden-lib/src/account/auth/rpo_falcon_512.rs deleted file mode 100644 index a8e3e2ada8..0000000000 --- a/crates/miden-lib/src/account/auth/rpo_falcon_512.rs +++ /dev/null @@ -1,39 +0,0 @@ -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountComponent, StorageSlot}; - -use crate::account::components::rpo_falcon_512_library; - -/// An [`AccountComponent`] implementing the RpoFalcon512 signature scheme for authentication of -/// transactions. -/// -/// It reexports the procedures from `miden::contracts::auth::basic`. When linking against this -/// component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be available to the -/// assembler which is the case when using [`TransactionKernel::assembler()`][kasm]. The procedures -/// of this component are: -/// - `auth_tx_rpo_falcon512`, which can be used to verify a signature provided via the advice stack -/// to authenticate a transaction. -/// -/// This component supports all account types. -/// -/// [kasm]: crate::transaction::TransactionKernel::assembler -pub struct AuthRpoFalcon512 { - pub_key: PublicKeyCommitment, -} - -impl AuthRpoFalcon512 { - /// Creates a new [`AuthRpoFalcon512`] component with the given `public_key`. - pub fn new(pub_key: PublicKeyCommitment) -> Self { - Self { pub_key } - } -} - -impl From for AccountComponent { - fn from(falcon: AuthRpoFalcon512) -> Self { - AccountComponent::new( - rpo_falcon_512_library(), - vec![StorageSlot::Value(falcon.pub_key.into())], - ) - .expect("falcon component should satisfy the requirements of a valid account component") - .with_supports_all_types() - } -} diff --git a/crates/miden-lib/src/errors/mod.rs b/crates/miden-lib/src/errors/mod.rs deleted file mode 100644 index 6bd4bda685..0000000000 --- a/crates/miden-lib/src/errors/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[cfg(any(feature = "testing", test))] -#[rustfmt::skip] -pub mod tx_kernel_errors; - -#[cfg(any(feature = "testing", test))] -#[rustfmt::skip] -pub mod note_script_errors; - -mod masm_error; -pub use masm_error::MasmError; - -mod script_builder_errors; -pub use script_builder_errors::ScriptBuilderError; - -mod transaction_errors; -pub use transaction_errors::{TransactionEventError, TransactionTraceParsingError}; diff --git a/crates/miden-lib/src/errors/transaction_errors.rs b/crates/miden-lib/src/errors/transaction_errors.rs deleted file mode 100644 index 8c8013c8e4..0000000000 --- a/crates/miden-lib/src/errors/transaction_errors.rs +++ /dev/null @@ -1,26 +0,0 @@ -use miden_core::EventId; -use thiserror::Error; - -use crate::transaction::TransactionEvent; - -// TRANSACTION EVENT PARSING ERROR -// ================================================================================================ - -#[derive(Debug, Error)] -pub enum TransactionEventError { - #[error("event id {0} is not a valid transaction event")] - InvalidTransactionEvent(EventId, Option<&'static str>), - #[error("event id {0} is not a transaction kernel event")] - NotTransactionEvent(EventId, Option<&'static str>), - #[error("event id {0} can only be emitted from the root context")] - NotRootContext(TransactionEvent), -} - -// TRANSACTION TRACE PARSING ERROR -// ================================================================================================ - -#[derive(Debug, Error)] -pub enum TransactionTraceParsingError { - #[error("trace id {0} is an unknown transaction kernel trace")] - UnknownTransactionTrace(u32), -} diff --git a/crates/miden-lib/src/lib.rs b/crates/miden-lib/src/lib.rs deleted file mode 100644 index 85e2797b9a..0000000000 --- a/crates/miden-lib/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![no_std] - -use alloc::sync::Arc; - -#[macro_use] -extern crate alloc; - -#[cfg(feature = "std")] -extern crate std; - -use miden_objects::assembly::Library; -use miden_objects::assembly::mast::MastForest; -use miden_objects::utils::serde::Deserializable; -use miden_objects::utils::sync::LazyLock; - -mod auth; -pub use auth::AuthScheme; - -pub mod account; -pub mod errors; -pub mod note; -pub mod transaction; -pub mod utils; - -#[cfg(any(feature = "testing", test))] -pub mod testing; - -// RE-EXPORTS -// ================================================================================================ -pub use miden_stdlib::StdLibrary; - -// CONSTANTS -// ================================================================================================ - -const MIDEN_LIB_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/assets/miden.masl")); - -// MIDEN LIBRARY -// ================================================================================================ - -#[derive(Clone)] -pub struct MidenLib(Library); - -impl MidenLib { - /// Returns a reference to the [`MastForest`] of the inner [`Library`]. - pub fn mast_forest(&self) -> &Arc { - self.0.mast_forest() - } -} - -impl AsRef for MidenLib { - fn as_ref(&self) -> &Library { - &self.0 - } -} - -impl From for Library { - fn from(value: MidenLib) -> Self { - value.0 - } -} - -impl Default for MidenLib { - fn default() -> Self { - static MIDEN_LIB: LazyLock = LazyLock::new(|| { - let contents = - Library::read_from_bytes(MIDEN_LIB_BYTES).expect("failed to read miden lib masl!"); - MidenLib(contents) - }); - MIDEN_LIB.clone() - } -} - -// TESTS -// ================================================================================================ - -// NOTE: Most kernel-related tests can be found under /miden-tx/kernel_tests -#[cfg(all(test, feature = "std"))] -mod tests { - use miden_objects::assembly::LibraryPath; - - use super::MidenLib; - - #[test] - fn test_compile() { - let path = "miden::active_account::get_id".parse::().unwrap(); - let miden = MidenLib::default(); - let exists = miden.0.module_infos().any(|module| { - module - .procedures() - .any(|(_, proc)| module.path().clone().append(&proc.name).unwrap() == path) - }); - - assert!(exists); - } -} diff --git a/crates/miden-lib/src/transaction/outputs.rs b/crates/miden-lib/src/transaction/outputs.rs deleted file mode 100644 index bb28b753a8..0000000000 --- a/crates/miden-lib/src/transaction/outputs.rs +++ /dev/null @@ -1,65 +0,0 @@ -use miden_objects::account::{AccountHeader, AccountId}; -use miden_objects::{AccountError, Felt, WORD_SIZE, Word, WordError}; - -use super::memory::{ - ACCT_CODE_COMMITMENT_OFFSET, - ACCT_DATA_MEM_SIZE, - ACCT_ID_AND_NONCE_OFFSET, - ACCT_NONCE_IDX, - ACCT_STORAGE_COMMITMENT_OFFSET, - ACCT_VAULT_ROOT_OFFSET, - MemoryOffset, -}; -use crate::transaction::memory::{ACCT_ID_PREFIX_IDX, ACCT_ID_SUFFIX_IDX}; - -// STACK OUTPUTS -// ================================================================================================ - -/// The index of the word at which the final account nonce is stored on the output stack. -pub const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0; - -/// The index of the word at which the account update commitment is stored on the output stack. -pub const ACCOUNT_UPDATE_COMMITMENT_WORD_IDX: usize = 1; - -/// The index of the word at which the fee asset is stored on the output stack. -pub const FEE_ASSET_WORD_IDX: usize = 2; - -/// The index of the item at which the expiration block height is stored on the output stack. -pub const EXPIRATION_BLOCK_ELEMENT_IDX: usize = 12; - -// ACCOUNT HEADER EXTRACTOR -// ================================================================================================ - -/// Parses the account header data returned by the VM into individual account component commitments. -/// Returns a tuple of account ID, vault root, storage commitment, code commitment, and nonce. -pub fn parse_final_account_header(elements: &[Felt]) -> Result { - if elements.len() != ACCT_DATA_MEM_SIZE { - return Err(AccountError::HeaderDataIncorrectLength { - actual: elements.len(), - expected: ACCT_DATA_MEM_SIZE, - }); - } - - let id = AccountId::try_from([ - elements[ACCT_ID_AND_NONCE_OFFSET as usize + ACCT_ID_PREFIX_IDX], - elements[ACCT_ID_AND_NONCE_OFFSET as usize + ACCT_ID_SUFFIX_IDX], - ]) - .map_err(AccountError::FinalAccountHeaderIdParsingFailed)?; - let nonce = elements[ACCT_ID_AND_NONCE_OFFSET as usize + ACCT_NONCE_IDX]; - let vault_root = parse_word(elements, ACCT_VAULT_ROOT_OFFSET) - .expect("we should have sliced off exactly 4 bytes"); - let storage_commitment = parse_word(elements, ACCT_STORAGE_COMMITMENT_OFFSET) - .expect("we should have sliced off exactly 4 bytes"); - let code_commitment = parse_word(elements, ACCT_CODE_COMMITMENT_OFFSET) - .expect("we should have sliced off exactly 4 bytes"); - - Ok(AccountHeader::new(id, nonce, vault_root, storage_commitment, code_commitment)) -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Creates a new `Word` instance from the slice of `Felt`s using provided offset. -fn parse_word(data: &[Felt], offset: MemoryOffset) -> Result { - Word::try_from(&data[offset as usize..offset as usize + WORD_SIZE]) -} diff --git a/crates/miden-lib/src/utils/mod.rs b/crates/miden-lib/src/utils/mod.rs deleted file mode 100644 index a826650cf9..0000000000 --- a/crates/miden-lib/src/utils/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod script_builder; - -pub use miden_objects::utils::*; -pub use script_builder::ScriptBuilder; - -pub use crate::errors::ScriptBuilderError; diff --git a/crates/miden-objects/src/account/code/procedure.rs b/crates/miden-objects/src/account/code/procedure.rs deleted file mode 100644 index 8281bc532e..0000000000 --- a/crates/miden-objects/src/account/code/procedure.rs +++ /dev/null @@ -1,246 +0,0 @@ -use alloc::string::ToString; -use alloc::sync::Arc; - -use miden_core::mast::MastForest; -use miden_core::prettier::PrettyPrint; -use miden_processor::{MastNode, MastNodeExt, MastNodeId}; - -use super::Felt; -use crate::utils::serde::{ - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Serializable, -}; -use crate::{AccountError, FieldElement, Word}; - -// ACCOUNT PROCEDURE INFO -// ================================================================================================ - -/// Information about a procedure exposed in a public account interface. -/// -/// The info included the MAST root of the procedure, the storage offset applied to all account -/// storage-related accesses made by this procedure and the storage size allowed to be accessed -/// by this procedure. -/// -/// The offset is applied to any accesses made from within the procedure to the associated -/// account's storage. For example, if storage offset for a procedure is set to 1, a call -/// to the account::get_item(storage_slot=4) made from this procedure would actually access -/// storage slot with index 5. -/// -/// The size is used to limit how many storage slots a given procedure can access in the associated -/// account's storage. For example, if storage size for a procedure is set to 3, the procedure will -/// be bounded to access storage slots in the range [storage_offset, storage_offset + 3 - 1]. -/// Furthermore storage_size = 0 indicates that a procedure does not need to access storage. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct AccountProcedureInfo { - mast_root: Word, - storage_offset: u8, - storage_size: u8, -} - -impl AccountProcedureInfo { - /// The number of field elements needed to represent an [AccountProcedureInfo] in kernel memory. - pub const NUM_ELEMENTS_PER_PROC: usize = 8; - - // CONSTRUCTOR - // -------------------------------------------------------------------------------------------- - - /// Returns a new instance of an [AccountProcedureInfo]. - /// - /// # Errors - /// - If `storage_size` is 0 and `storage_offset` is not 0. - /// - If `storage_size + storage_offset` is greater than `MAX_NUM_STORAGE_SLOTS`. - pub fn new( - mast_root: Word, - storage_offset: u8, - storage_size: u8, - ) -> Result { - if storage_size == 0 && storage_offset != 0 { - return Err(AccountError::PureProcedureWithStorageOffset); - } - - // Check if the addition would exceed AccountStorage::MAX_NUM_STORAGE_SLOTS (= 255) which is - // the case if the addition overflows. - if storage_offset.checked_add(storage_size).is_none() { - return Err(AccountError::StorageOffsetPlusSizeOutOfBounds( - storage_offset as u16 + storage_size as u16, - )); - } - - Ok(Self { mast_root, storage_offset, storage_size }) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns a reference to the procedure's mast root. - pub fn mast_root(&self) -> &Word { - &self.mast_root - } - - /// Returns the procedure's storage offset. - pub fn storage_offset(&self) -> u8 { - self.storage_offset - } - - /// Returns the procedure's storage size. - pub fn storage_size(&self) -> u8 { - self.storage_size - } -} - -impl From for [Felt; 8] { - fn from(value: AccountProcedureInfo) -> Self { - let mut result = [Felt::ZERO; 8]; - - // copy mast_root into first 4 elements - result[0..4].copy_from_slice(value.mast_root().as_elements()); - - // copy the storage offset into value[4] - result[4] = Felt::from(value.storage_offset); - - // copy the storage size into value[5] - result[5] = Felt::from(value.storage_size); - - result - } -} - -impl TryFrom<[Felt; 8]> for AccountProcedureInfo { - type Error = AccountError; - - fn try_from(value: [Felt; 8]) -> Result { - // get mast_root from first 4 elements - let mast_root = Word::from(<[Felt; 4]>::try_from(&value[0..4]).unwrap()); - - // get storage_offset form value[4] - let storage_offset: u8 = value[4].try_into().map_err(|_| { - AccountError::AccountCodeProcedureStorageOffsetTooLarge(mast_root, value[4]) - })?; - - // get storage_size form value[5] - let storage_size: u8 = value[5].try_into().map_err(|_| { - AccountError::AccountCodeProcedureStorageSizeTooLarge(mast_root, value[5]) - })?; - - // Check if the remaining values are 0 - if value[6] != Felt::ZERO || value[7] != Felt::ZERO { - return Err(AccountError::AccountCodeProcedureInvalidPadding(mast_root)); - } - - Ok(Self { mast_root, storage_offset, storage_size }) - } -} - -impl Serializable for AccountProcedureInfo { - fn write_into(&self, target: &mut W) { - target.write(self.mast_root); - target.write_u8(self.storage_offset); - target.write_u8(self.storage_size) - } - - fn get_size_hint(&self) -> usize { - self.mast_root.get_size_hint() - + self.storage_offset.get_size_hint() - + self.storage_size.get_size_hint() - } -} - -impl Deserializable for AccountProcedureInfo { - fn read_from(source: &mut R) -> Result { - let mast_root: Word = source.read()?; - let storage_offset = source.read_u8()?; - let storage_size = source.read_u8()?; - Self::new(mast_root, storage_offset, storage_size) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} - -// PRINTABLE PROCEDURE -// ================================================================================================ - -/// A printable representation of a single account procedure. -#[derive(Debug, Clone)] -pub struct PrintableProcedure { - mast: Arc, - procedure_info: AccountProcedureInfo, - entrypoint: MastNodeId, -} - -impl PrintableProcedure { - /// Creates a new PrintableProcedure instance from its components. - pub(crate) fn new( - mast: Arc, - procedure_info: AccountProcedureInfo, - entrypoint: MastNodeId, - ) -> Self { - Self { mast, procedure_info, entrypoint } - } - - fn entrypoint(&self) -> &MastNode { - &self.mast[self.entrypoint] - } - - pub(crate) fn storage_offset(&self) -> u8 { - self.procedure_info.storage_offset() - } - - pub(crate) fn storage_size(&self) -> u8 { - self.procedure_info.storage_size() - } - - pub(crate) fn mast_root(&self) -> &Word { - self.procedure_info.mast_root() - } -} - -impl PrettyPrint for PrintableProcedure { - fn render(&self) -> miden_core::prettier::Document { - use miden_core::prettier::*; - - indent( - 4, - const_text("begin") + nl() + self.entrypoint().to_pretty_print(&self.mast).render(), - ) + nl() - + const_text("end") - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - - use miden_core::Felt; - use miden_crypto::utils::{Deserializable, Serializable}; - - use crate::account::{AccountCode, AccountProcedureInfo}; - - #[test] - fn test_from_to_account_procedure() { - let account_code = AccountCode::mock(); - - let procedure = account_code.procedures()[0]; - - // from procedure to [Felt; 8] - let felts: [Felt; 8] = procedure.into(); - - // try_from [Felt; 8] to procedure - let final_procedure: AccountProcedureInfo = felts.try_into().unwrap(); - - assert_eq!(procedure, final_procedure); - } - - #[test] - fn test_serde_account_procedure() { - let account_code = AccountCode::mock(); - - let serialized = account_code.procedures()[0].to_bytes(); - let deserialized = AccountProcedureInfo::read_from_bytes(&serialized).unwrap(); - - assert_eq!(account_code.procedures()[0], deserialized); - } -} diff --git a/crates/miden-objects/src/account/component/template/mod.rs b/crates/miden-objects/src/account/component/template/mod.rs deleted file mode 100644 index c646fa50a1..0000000000 --- a/crates/miden-objects/src/account/component/template/mod.rs +++ /dev/null @@ -1,527 +0,0 @@ -use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::str::FromStr; - -use miden_assembly::Library; -use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_processor::DeserializationError; -use semver::Version; - -use super::AccountType; -use crate::errors::AccountComponentTemplateError; - -mod storage; -pub use storage::*; - -// ACCOUNT COMPONENT TEMPLATE -// ================================================================================================ - -/// Represents a template containing a component's metadata and its associated library. -/// -/// The [AccountComponentTemplate] encapsulates all necessary information to initialize and manage -/// an account component within the system. It includes the configuration details and the compiled -/// library code required for the component's operation. -/// -/// A template can be instantiated into [AccountComponent](super::AccountComponent) objects. -/// The component metadata can be defined with placeholders that can be replaced at instantiation -/// time. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct AccountComponentTemplate { - /// The component's metadata. This describes the component and how the storage is laid out, - /// alongside how storage values are initialized. - metadata: AccountComponentMetadata, - /// The account component's assembled code. This defines all functionality related to the - /// component. - library: Library, -} - -impl AccountComponentTemplate { - /// Creates a new [AccountComponentTemplate]. - /// - /// This template holds everything needed to describe and implement a component, including the - /// compiled procedures (via the [Library]) and the metadata that defines the component’s - /// storage layout ([AccountComponentMetadata]). The metadata can include storage placeholders - /// that get filled in at the time of the [AccountComponent](super::AccountComponent) - /// instantiation. - pub fn new(metadata: AccountComponentMetadata, library: Library) -> Self { - Self { metadata, library } - } - - /// Returns a reference to the template's [AccountComponentMetadata]. - pub fn metadata(&self) -> &AccountComponentMetadata { - &self.metadata - } - - /// Returns a reference to the underlying [Library] of this component. - pub fn library(&self) -> &Library { - &self.library - } -} - -impl Serializable for AccountComponentTemplate { - fn write_into(&self, target: &mut W) { - target.write(&self.metadata); - target.write(&self.library); - } -} - -impl Deserializable for AccountComponentTemplate { - fn read_from( - source: &mut R, - ) -> Result { - // Read and deserialize the configuration from a TOML string. - let metadata: AccountComponentMetadata = source.read()?; - let library = Library::read_from(source)?; - - Ok(AccountComponentTemplate::new(metadata, library)) - } -} - -// ACCOUNT COMPONENT METADATA -// ================================================================================================ - -/// Represents the full component template configuration. -/// -/// An account component metadata describes the component alongside its storage layout. -/// On the storage layout, placeholders can be utilized to identify values that should be provided -/// at the moment of instantiation. -/// -/// When the `std` feature is enabled, this struct allows for serialization and deserialization to -/// and from a TOML file. -/// -/// # Guarantees -/// -/// - The metadata's storage layout does not contain duplicate slots, and it always starts at slot -/// index 0. -/// - Storage slots are laid out in a contiguous manner. -/// - Each placeholder represents a single value. The expected placeholders can be retrieved with -/// [AccountComponentMetadata::get_placeholder_requirements()], which returns a map from keys to -/// [PlaceholderTypeRequirement] (which, in turn, indicates the expected value type for the -/// placeholder). -/// -/// # Example -/// -/// ``` -/// # use semver::Version; -/// # use std::collections::{BTreeMap, BTreeSet}; -/// # use miden_objects::{testing::account_code::CODE, account::{ -/// # AccountComponent, AccountComponentMetadata, StorageEntry, -/// # StorageValueName, -/// # AccountComponentTemplate, FeltRepresentation, WordRepresentation, TemplateType}, -/// # assembly::Assembler, Felt}; -/// # use miden_objects::account::InitStorageData; -/// # fn main() -> Result<(), Box> { -/// let first_felt = FeltRepresentation::from(Felt::new(0u64)); -/// let second_felt = FeltRepresentation::from(Felt::new(1u64)); -/// let third_felt = FeltRepresentation::from(Felt::new(2u64)); -/// // Templated element: -/// let last_element = -/// FeltRepresentation::new_template(TemplateType::new("felt")?, StorageValueName::new("foo")?); -/// -/// let word_representation = WordRepresentation::new_value( -/// [first_felt, second_felt, third_felt, last_element], -/// Some(StorageValueName::new("test_value")?.into()), -/// ) -/// .with_description("this is the first entry in the storage layout"); -/// let storage_entry = StorageEntry::new_value(0, word_representation); -/// -/// let init_storage_data = InitStorageData::new( -/// [(StorageValueName::new("test_value.foo")?, "300".to_string())], -/// BTreeMap::new(), -/// ); -/// -/// let component_template = AccountComponentMetadata::new( -/// "test name".into(), -/// "description of the component".into(), -/// Version::parse("0.1.0")?, -/// BTreeSet::new(), -/// vec![storage_entry], -/// )?; -/// -/// let library = Assembler::default().assemble_library([CODE]).unwrap(); -/// let template = AccountComponentTemplate::new(component_template, library); -/// -/// let component = AccountComponent::from_template(&template, &init_storage_data)?; -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))] -pub struct AccountComponentMetadata { - /// The human-readable name of the component. - name: String, - - /// A brief description of what this component is and how it works. - description: String, - - /// The version of the component using semantic versioning. - /// This can be used to track and manage component upgrades. - version: Version, - - /// A set of supported target account types for this component. - supported_types: BTreeSet, - - /// A list of storage entries defining the component's storage layout and initialization - /// values. - storage: Vec, -} - -impl AccountComponentMetadata { - /// Create a new [AccountComponentMetadata]. - /// - /// # Errors - /// - /// - If the specified storage slots contain duplicates. - /// - If the slot numbers do not start at zero. - /// - If the slots are not contiguous. - pub fn new( - name: String, - description: String, - version: Version, - targets: BTreeSet, - storage: Vec, - ) -> Result { - let component = Self { - name, - description, - version, - supported_types: targets, - storage, - }; - component.validate()?; - Ok(component) - } - - /// Retrieves a map of unique storage placeholder names mapped to their expected type that - /// require a value at the moment of component instantiation. - /// - /// These values will be used for initializing storage slot values, or storage map entries. - /// For a full example on how a placeholder may be utilized, please refer to the docs for - /// [AccountComponentMetadata]. - /// - /// Types for the returned storage placeholders are inferred based on their location in the - /// storage layout structure. - pub fn get_placeholder_requirements( - &self, - ) -> BTreeMap { - let mut templates = BTreeMap::new(); - for entry in self.storage_entries() { - for (name, requirement) in entry.template_requirements() { - templates.insert(name, requirement); - } - } - - templates - } - - /// Returns the name of the account component. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the description of the account component. - pub fn description(&self) -> &str { - &self.description - } - - /// Returns the semantic version of the account component. - pub fn version(&self) -> &Version { - &self.version - } - - /// Returns the account types supported by the component. - pub fn supported_types(&self) -> &BTreeSet { - &self.supported_types - } - - /// Returns the list of storage entries of the component. - pub fn storage_entries(&self) -> &Vec { - &self.storage - } - - /// Validate the [AccountComponentMetadata]. - /// - /// # Errors - /// - /// - If the specified storage entries contain duplicate names. - /// - If the template contains duplicate placeholder names. - /// - If the slot numbers do not start at zero. - /// - If the slots are not contiguous. - fn validate(&self) -> Result<(), AccountComponentTemplateError> { - let mut all_slots: Vec = - self.storage.iter().flat_map(|entry| entry.slot_indices()).collect(); - - // Check that slots start at 0 and are contiguous - all_slots.sort_unstable(); - if let Some(&first_slot) = all_slots.first() - && first_slot != 0 - { - return Err(AccountComponentTemplateError::StorageSlotsDoNotStartAtZero(first_slot)); - } - - for slots in all_slots.windows(2) { - if slots[1] == slots[0] { - return Err(AccountComponentTemplateError::DuplicateSlot(slots[0])); - } - - if slots[1] != slots[0] + 1 { - return Err(AccountComponentTemplateError::NonContiguousSlots(slots[0], slots[1])); - } - } - - // Check for duplicate storage entry names - let mut seen_names = BTreeSet::new(); - for entry in self.storage_entries() { - entry.validate()?; - if let Some(name) = entry.name() { - let name_existed = !seen_names.insert(name.as_str()); - if name_existed { - return Err(AccountComponentTemplateError::DuplicateEntryNames(name.clone())); - } - } - } - - // Check for duplicate storage placeholder names - let mut seen_placeholder_names = BTreeSet::new(); - for entry in self.storage_entries() { - for (name, _) in entry.template_requirements() { - if !seen_placeholder_names.insert(name.clone()) { - return Err(AccountComponentTemplateError::DuplicatePlaceholderName(name)); - } - } - } - - Ok(()) - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for AccountComponentMetadata { - fn write_into(&self, target: &mut W) { - self.name.write_into(target); - self.description.write_into(target); - self.version.to_string().write_into(target); - self.supported_types.write_into(target); - self.storage.write_into(target); - } -} - -impl Deserializable for AccountComponentMetadata { - fn read_from(source: &mut R) -> Result { - Ok(Self { - name: String::read_from(source)?, - description: String::read_from(source)?, - version: semver::Version::from_str(&String::read_from(source)?).map_err( - |err: semver::Error| DeserializationError::InvalidValue(err.to_string()), - )?, - supported_types: BTreeSet::::read_from(source)?, - storage: Vec::::read_from(source)?, - }) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use std::collections::{BTreeMap, BTreeSet}; - use std::string::ToString; - - use assert_matches::assert_matches; - use miden_assembly::Assembler; - use miden_core::utils::{Deserializable, Serializable}; - use miden_core::{Felt, FieldElement}; - use semver::Version; - - use super::FeltRepresentation; - use crate::AccountError; - use crate::account::component::FieldIdentifier; - use crate::account::component::template::storage::StorageEntry; - use crate::account::component::template::{ - AccountComponentMetadata, - AccountComponentTemplate, - InitStorageData, - }; - use crate::account::{AccountComponent, StorageValueName}; - use crate::errors::AccountComponentTemplateError; - use crate::testing::account_code::CODE; - - fn default_felt_array() -> [FeltRepresentation; 4] { - [ - FeltRepresentation::from(Felt::ZERO), - FeltRepresentation::from(Felt::ZERO), - FeltRepresentation::from(Felt::ZERO), - FeltRepresentation::from(Felt::ZERO), - ] - } - - #[test] - fn contiguous_value_slots() { - let storage = vec![ - StorageEntry::new_value(0, default_felt_array()), - StorageEntry::new_multislot( - FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()), - 1..3, - vec![default_felt_array(), default_felt_array()], - ), - ]; - - let original_config = AccountComponentMetadata { - name: "test".into(), - description: "desc".into(), - version: Version::parse("0.1.0").unwrap(), - supported_types: BTreeSet::new(), - storage, - }; - - let serialized = original_config.to_toml().unwrap(); - let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap(); - assert_eq!(deserialized, original_config); - } - - #[test] - fn new_non_contiguous_value_slots() { - let storage = vec![ - StorageEntry::new_value(0, default_felt_array()), - StorageEntry::new_value(2, default_felt_array()), - ]; - - let result = AccountComponentMetadata::new( - "test".into(), - "desc".into(), - Version::parse("0.1.0").unwrap(), - BTreeSet::new(), - storage, - ); - assert_matches!(result, Err(AccountComponentTemplateError::NonContiguousSlots(0, 2))); - } - - #[test] - fn binary_serde_roundtrip() { - let storage = vec![ - StorageEntry::new_multislot( - FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()), - 1..3, - vec![default_felt_array(), default_felt_array()], - ), - StorageEntry::new_value(0, default_felt_array()), - ]; - - let component_metadata = AccountComponentMetadata { - name: "test".into(), - description: "desc".into(), - version: Version::parse("0.1.0").unwrap(), - supported_types: BTreeSet::new(), - storage, - }; - - let library = Assembler::default().assemble_library([CODE]).unwrap(); - let template = AccountComponentTemplate::new(component_metadata, library); - let _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap(); - - let serialized = template.to_bytes(); - let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap(); - - assert_eq!(deserialized, template); - } - - #[test] - pub fn fail_on_duplicate_key() { - let toml_text = r#" - name = "Test Component" - description = "This is a test component" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "map" - description = "A storage map entry" - slot = 0 - values = [ - { key = "0x1", value = ["0x3", "0x1", "0x2", "0x3"] }, - { key = "0x1", value = ["0x1", "0x2", "0x3", "0x10"] } - ] - "#; - - let result = AccountComponentMetadata::from_toml(toml_text); - assert_matches!(result, Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(_))); - } - - #[test] - pub fn fail_on_duplicate_placeholder_name() { - let toml_text = r#" - name = "Test Component" - description = "tests for two duplicate placeholders" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "map" - slot = 0 - values = [ - { key = "0x1", value = [{type = "felt", name = "test"}, "0x1", "0x2", "0x3"] }, - { key = "0x2", value = ["0x1", "0x2", "0x3", {type = "token_symbol", name = "test"}] } - ] - "#; - - let result = AccountComponentMetadata::from_toml(toml_text).unwrap_err(); - assert_matches::assert_matches!( - result, - AccountComponentTemplateError::DuplicatePlaceholderName(_) - ); - } - - #[test] - pub fn fail_duplicate_key_instance() { - let _ = color_eyre::install(); - - let toml_text = r#" - name = "Test Component" - description = "This is a test component" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "map" - description = "A storage map entry" - slot = 0 - values = [ - { key = ["0", "0", "0", "1"], value = ["0x9", "0x12", "0x31", "0x18"] }, - { key = { name="duplicate_key" }, value = ["0x1", "0x2", "0x3", "0x4"] } - ] - "#; - - let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - let library = Assembler::default().assemble_library([CODE]).unwrap(); - let template = AccountComponentTemplate::new(metadata, library); - - // Fail to instantiate on a duplicate key - - let init_storage_data = InitStorageData::new( - [( - StorageValueName::new("map.duplicate_key").unwrap(), - "0x0000000000000000000000000000000000000000000000000100000000000000".to_string(), - )], - BTreeMap::new(), - ); - let account_component = AccountComponent::from_template(&template, &init_storage_data); - assert_matches!( - account_component, - Err(AccountError::AccountComponentTemplateInstantiationError( - AccountComponentTemplateError::StorageMapHasDuplicateKeys(_) - )) - ); - - // Successfully instantiate a map (keys are not duplicate) - let valid_init_storage_data = InitStorageData::new( - [(StorageValueName::new("map.duplicate_key").unwrap(), "0x30".to_string())], - BTreeMap::new(), - ); - AccountComponent::from_template(&template, &valid_init_storage_data).unwrap(); - } -} diff --git a/crates/miden-objects/src/account/component/template/storage/entry_content.rs b/crates/miden-objects/src/account/component/template/storage/entry_content.rs deleted file mode 100644 index 54780435b3..0000000000 --- a/crates/miden-objects/src/account/component/template/storage/entry_content.rs +++ /dev/null @@ -1,748 +0,0 @@ -use alloc::boxed::Box; -use alloc::collections::BTreeSet; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::iter; - -use super::placeholder::{PlaceholderTypeRequirement, TEMPLATE_REGISTRY, TemplateType}; -use super::{ - FieldIdentifier, - InitStorageData, - MapEntry, - StorageValueName, - TemplateRequirementsIter, -}; -use crate::account::StorageMap; -use crate::account::component::template::AccountComponentTemplateError; -use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{Felt, FieldElement, Word}; - -// WORDS -// ================================================================================================ - -/// Defines how a word is represented within the component's storage description. -/// -/// Each word representation can be: -/// - A template that defines a type but does not carry a value. -/// - A predefined value that may contain a hardcoded word or a mix of fixed and templated felts. -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(clippy::large_enum_variant)] -pub enum WordRepresentation { - /// A templated value that serves as a placeholder for instantiation. - /// - /// This variant defines a type but does not store a value. The actual value is provided at the - /// time of instantiation. The name is required to identify this template externally. - Template { - /// The type associated with this templated word. - r#type: TemplateType, - identifier: FieldIdentifier, - }, - - /// A predefined value that can be used directly within storage. - /// - /// This variant may contain either a fully hardcoded word or a structured set of felts, some - /// of which may themselves be templates. - Value { - identifier: Option, - /// The 4-felt representation of the stored word. - value: [FeltRepresentation; 4], - }, -} - -impl WordRepresentation { - /// Constructs a new `Template` variant. - pub fn new_template(r#type: TemplateType, identifier: FieldIdentifier) -> Self { - WordRepresentation::Template { r#type, identifier } - } - - /// Constructs a new `Value` variant. - pub fn new_value( - value: impl Into<[FeltRepresentation; 4]>, - identifier: Option, - ) -> Self { - WordRepresentation::Value { identifier, value: value.into() } - } - - /// Sets the description of the [`WordRepresentation`] and returns `self`. - pub fn with_description(self, description: impl Into) -> Self { - match self { - WordRepresentation::Template { r#type, identifier } => WordRepresentation::Template { - r#type, - identifier: FieldIdentifier { - name: identifier.name, - description: Some(description.into()), - }, - }, - WordRepresentation::Value { identifier, value } => WordRepresentation::Value { - identifier: identifier.map(|id| FieldIdentifier { - name: id.name, - description: Some(description.into()), - }), - value, - }, - } - } - - /// Returns the name associated with the word representation. - /// - For the `Template` variant, it always returns a reference to the name. - /// - For the `Value` variant, it returns `Some` if a name is present, or `None` otherwise. - pub fn name(&self) -> Option<&StorageValueName> { - match self { - WordRepresentation::Template { identifier, .. } => Some(&identifier.name), - WordRepresentation::Value { identifier, .. } => identifier.as_ref().map(|id| &id.name), - } - } - - /// Returns the description associated with the word representation. - /// Both variants store an `Option`, which is converted to an `Option<&str>`. - pub fn description(&self) -> Option<&str> { - match self { - WordRepresentation::Template { identifier, .. } => identifier.description.as_deref(), - WordRepresentation::Value { identifier, .. } => { - identifier.as_ref().and_then(|id| id.description.as_deref()) - }, - } - } - - /// Returns the type name. - pub fn word_type(&self) -> TemplateType { - match self { - WordRepresentation::Template { r#type, .. } => r#type.clone(), - WordRepresentation::Value { .. } => TemplateType::native_word(), - } - } - - /// Returns the value (an array of 4 `FeltRepresentation`s) if this is a `Value` - /// variant; otherwise, returns `None`. - pub fn value(&self) -> Option<&[FeltRepresentation; 4]> { - match self { - WordRepresentation::Value { value, .. } => Some(value), - WordRepresentation::Template { .. } => None, - } - } - - /// Returns an iterator over the word's placeholders. - /// - /// For [`WordRepresentation::Value`], it corresponds to the inner iterators (since inner - /// elements can be templated as well). - /// For [`WordRepresentation::Template`] it returns the words's placeholder requirements - /// as defined. - pub fn template_requirements( - &self, - placeholder_prefix: StorageValueName, - ) -> TemplateRequirementsIter<'_> { - let placeholder_key = - placeholder_prefix.with_suffix(self.name().unwrap_or(&StorageValueName::empty())); - match self { - WordRepresentation::Template { identifier, r#type } => Box::new(iter::once(( - placeholder_key, - PlaceholderTypeRequirement { - description: identifier.description.clone(), - r#type: r#type.clone(), - }, - ))), - WordRepresentation::Value { value, .. } => Box::new( - value - .iter() - .flat_map(move |felt| felt.template_requirements(placeholder_key.clone())), - ), - } - } - - /// Attempts to convert the [WordRepresentation] into a [Word]. - /// - /// If the representation is a template, the value is retrieved from - /// `init_storage_data`, identified by its key. If any of the inner elements - /// within the value are a template, they are retrieved in the same way. - pub(crate) fn try_build_word( - &self, - init_storage_data: &InitStorageData, - placeholder_prefix: StorageValueName, - ) -> Result { - match self { - WordRepresentation::Template { identifier, r#type } => { - let placeholder_path = placeholder_prefix.with_suffix(&identifier.name); - let maybe_value = init_storage_data.get(&placeholder_path); - if let Some(value) = maybe_value { - let parsed_value = TEMPLATE_REGISTRY - .try_parse_word(r#type, value) - .map_err(AccountComponentTemplateError::StorageValueParsingError)?; - - Ok(parsed_value) - } else { - Err(AccountComponentTemplateError::PlaceholderValueNotProvided( - placeholder_path, - )) - } - }, - WordRepresentation::Value { value, identifier } => { - let mut result = [Felt::ZERO; 4]; - - for (index, felt_repr) in value.iter().enumerate() { - let placeholder = placeholder_prefix.clone().with_suffix( - identifier - .as_ref() - .map(|id| &id.name) - .unwrap_or(&StorageValueName::empty()), - ); - result[index] = felt_repr.try_build_felt(init_storage_data, placeholder)?; - } - // SAFETY: result is guaranteed to have all its 4 indices rewritten - Ok(Word::from(result)) - }, - } - } - - /// Validates that the defined type exists and all the inner felt types exist as well - pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { - // Check that type exists in registry - let type_exists = TEMPLATE_REGISTRY.contains_word_type(&self.word_type()); - if !type_exists { - return Err(AccountComponentTemplateError::InvalidType( - self.word_type().to_string(), - "Word".into(), - )); - } - - if let Some(felts) = self.value() { - for felt in felts { - felt.validate()?; - } - } - - Ok(()) - } -} - -impl Serializable for WordRepresentation { - fn write_into(&self, target: &mut W) { - match self { - WordRepresentation::Template { identifier, r#type } => { - target.write_u8(0); - target.write(identifier); - target.write(r#type); - }, - WordRepresentation::Value { identifier, value } => { - target.write_u8(1); - target.write(identifier); - target.write(value); - }, - } - } -} - -impl Deserializable for WordRepresentation { - fn read_from(source: &mut R) -> Result { - let tag = source.read_u8()?; - match tag { - 0 => { - let identifier = FieldIdentifier::read_from(source)?; - let r#type = TemplateType::read_from(source)?; - Ok(WordRepresentation::Template { identifier, r#type }) - }, - 1 => { - let identifier = Option::::read_from(source)?; - let value = <[FeltRepresentation; 4]>::read_from(source)?; - Ok(WordRepresentation::Value { identifier, value }) - }, - other => Err(DeserializationError::InvalidValue(format!( - "unknown tag '{other}' for WordRepresentation" - ))), - } - } -} - -impl From<[FeltRepresentation; 4]> for WordRepresentation { - fn from(value: [FeltRepresentation; 4]) -> Self { - WordRepresentation::new_value(value, Option::::None) - } -} - -impl From<[Felt; 4]> for WordRepresentation { - fn from(value: [Felt; 4]) -> Self { - WordRepresentation::new_value( - value.map(FeltRepresentation::from), - Option::::None, - ) - } -} - -// FELTS -// ================================================================================================ - -/// Supported element representations for a component's storage entries. -/// -/// Each felt element in a storage entry can either be: -/// - A concrete value that holds a predefined felt. -/// - A template that specifies the type of felt expected, with the actual value to be provided -/// later. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FeltRepresentation { - /// A concrete felt value. - /// - /// This variant holds a felt that is part of the component's storage. - /// The optional name allows for identification, and the description offers additional context. - Value { - /// An optional identifier for this felt value. - /// An optional explanation of the felt's purpose. - identifier: Option, - /// The actual felt value. - value: Felt, - }, - - /// A templated felt element. - /// - /// This variant specifies the expected type of the felt without providing a concrete value. - /// The name is required to uniquely identify the template, and an optional description can - /// further clarify its intended use. - Template { - /// The expected type for this felt element. - r#type: TemplateType, - /// A unique name for the felt template. - /// An optional description that explains the purpose of this template. - identifier: FieldIdentifier, - }, -} - -impl FeltRepresentation { - /// Creates a new [`FeltRepresentation::Value`] variant. - pub fn new_value(value: impl Into, name: Option) -> Self { - FeltRepresentation::Value { - value: value.into(), - identifier: name.map(FieldIdentifier::with_name), - } - } - - /// Creates a new [`FeltRepresentation::Template`] variant. - /// - /// The name will be used for identification at the moment of instantiating the componentn. - pub fn new_template(r#type: TemplateType, name: StorageValueName) -> Self { - FeltRepresentation::Template { - r#type, - identifier: FieldIdentifier::with_name(name), - } - } - - /// Sets the description of the [`FeltRepresentation`] and returns `self`. - pub fn with_description(self, description: impl Into) -> Self { - match self { - FeltRepresentation::Template { r#type, identifier } => FeltRepresentation::Template { - r#type, - identifier: FieldIdentifier { - name: identifier.name, - description: Some(description.into()), - }, - }, - FeltRepresentation::Value { identifier, value } => FeltRepresentation::Value { - identifier: identifier.map(|id| FieldIdentifier { - name: id.name, - description: Some(description.into()), - }), - value, - }, - } - } - - /// Returns the felt type. - pub fn felt_type(&self) -> TemplateType { - match self { - FeltRepresentation::Template { r#type, .. } => r#type.clone(), - FeltRepresentation::Value { .. } => TemplateType::native_felt(), - } - } - - /// Attempts to convert the [FeltRepresentation] into a [Felt]. - /// - /// If the representation is a template, the value is retrieved from `init_storage_data`, - /// identified by its key. Otherwise, the returned value is just the inner element. - pub(crate) fn try_build_felt( - &self, - init_storage_data: &InitStorageData, - placeholder_prefix: StorageValueName, - ) -> Result { - match self { - FeltRepresentation::Template { identifier, r#type } => { - let placeholder_key = placeholder_prefix.with_suffix(&identifier.name); - let raw_value = init_storage_data.get(&placeholder_key).ok_or( - AccountComponentTemplateError::PlaceholderValueNotProvided(placeholder_key), - )?; - - Ok(TEMPLATE_REGISTRY - .try_parse_felt(r#type, raw_value) - .map_err(AccountComponentTemplateError::StorageValueParsingError)?) - }, - FeltRepresentation::Value { value, .. } => Ok(*value), - } - } - - /// Returns an iterator over the felt's template. - /// - /// For [`FeltRepresentation::Value`], these is an empty set; for - /// [`FeltRepresentation::Template`] it returns the felt's placeholder key based on the - /// felt's name within the component description. - pub fn template_requirements( - &self, - placeholder_prefix: StorageValueName, - ) -> TemplateRequirementsIter<'_> { - match self { - FeltRepresentation::Template { identifier, r#type } => Box::new(iter::once(( - placeholder_prefix.with_suffix(&identifier.name), - PlaceholderTypeRequirement { - description: identifier.description.clone(), - r#type: r#type.clone(), - }, - ))), - _ => Box::new(iter::empty()), - } - } - - /// Validates that the defined Felt type exists - pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { - // Check that type exists in registry - let type_exists = TEMPLATE_REGISTRY.contains_felt_type(&self.felt_type()); - if !type_exists { - return Err(AccountComponentTemplateError::InvalidType( - self.felt_type().to_string(), - "Felt".into(), - )); - } - Ok(()) - } -} - -impl From for FeltRepresentation { - fn from(value: Felt) -> Self { - FeltRepresentation::new_value(value, Option::::None) - } -} - -impl Default for FeltRepresentation { - fn default() -> Self { - FeltRepresentation::new_value(Felt::default(), Option::::None) - } -} - -impl Serializable for FeltRepresentation { - fn write_into(&self, target: &mut W) { - match self { - FeltRepresentation::Value { identifier, value } => { - target.write_u8(0); - target.write(identifier); - target.write(value); - }, - FeltRepresentation::Template { identifier, r#type } => { - target.write_u8(1); - target.write(identifier); - target.write(r#type); - }, - } - } -} - -impl Deserializable for FeltRepresentation { - fn read_from(source: &mut R) -> Result { - let tag = source.read_u8()?; - match tag { - 0 => { - let identifier = Option::::read_from(source)?; - let value = Felt::read_from(source)?; - Ok(FeltRepresentation::Value { value, identifier }) - }, - 1 => { - let identifier = FieldIdentifier::read_from(source)?; - let r#type = TemplateType::read_from(source)?; - Ok(FeltRepresentation::Template { r#type, identifier }) - }, - other => Err(DeserializationError::InvalidValue(format!( - "unknown tag '{other}' for FeltRepresentation" - ))), - } - } -} - -// MAP REPRESENTATION -// ================================================================================================ - -/// Supported map representations for a component's storage entries. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] -pub enum MapRepresentation { - /// A map whose contents are provided during instantiation via placeholders. - Template { - /// The human-readable identifier of the map slot. - identifier: FieldIdentifier, - }, - /// A map with statically defined key/value pairs. - Value { - /// The human-readable identifier of the map slot. - identifier: FieldIdentifier, - /// Storage map entries, consisting of a list of keys associated with their values. - entries: Vec, - }, -} - -impl MapRepresentation { - /// Creates a new `MapRepresentation` from a vector of map entries. - pub fn new_value(entries: Vec, name: impl Into) -> Self { - MapRepresentation::Value { - entries, - identifier: FieldIdentifier::with_name(name.into()), - } - } - - /// Creates a new templated map representation. - pub fn new_template(name: impl Into) -> Self { - MapRepresentation::Template { - identifier: FieldIdentifier::with_name(name.into()), - } - } - - /// Sets the description of the [`MapRepresentation`] and returns `self`. - pub fn with_description(self, description: impl Into) -> Self { - match self { - MapRepresentation::Template { identifier } => MapRepresentation::Template { - identifier: FieldIdentifier { - name: identifier.name, - description: Some(description.into()), - }, - }, - MapRepresentation::Value { identifier, entries } => MapRepresentation::Value { - entries, - identifier: FieldIdentifier { - name: identifier.name, - description: Some(description.into()), - }, - }, - } - } - - /// Returns an iterator over all of the storage entries' placeholder keys, alongside their - /// expected type. - pub fn template_requirements(&self) -> TemplateRequirementsIter<'_> { - match self { - MapRepresentation::Template { identifier } => Box::new(iter::once(( - identifier.name.clone(), - PlaceholderTypeRequirement { - description: identifier.description.clone(), - r#type: TemplateType::storage_map(), - }, - ))), - MapRepresentation::Value { identifier, entries } => Box::new( - entries - .iter() - .flat_map(move |entry| entry.template_requirements(identifier.name.clone())), - ), - } - } - - /// Returns a reference to map entries. - pub fn entries(&self) -> &[MapEntry] { - match self { - MapRepresentation::Value { entries, .. } => entries, - MapRepresentation::Template { .. } => &[], - } - } - - /// Returns a reference to the map's name within the storage metadata. - pub fn name(&self) -> &StorageValueName { - match self { - MapRepresentation::Template { identifier } - | MapRepresentation::Value { identifier, .. } => &identifier.name, - } - } - - /// Returns a reference to the field's description. - pub fn description(&self) -> Option<&String> { - match self { - MapRepresentation::Template { identifier } - | MapRepresentation::Value { identifier, .. } => identifier.description.as_ref(), - } - } - - /// Returns the number of statically defined key-value pairs in the map. - pub fn len(&self) -> usize { - match self { - MapRepresentation::Value { entries, .. } => entries.len(), - MapRepresentation::Template { .. } => 0, - } - } - - /// Returns `true` if there are no statically defined entries in the map. - pub fn is_empty(&self) -> bool { - match self { - MapRepresentation::Value { entries, .. } => entries.is_empty(), - MapRepresentation::Template { .. } => true, - } - } - - /// Attempts to convert the [MapRepresentation] into a [StorageMap]. - /// - /// If any of the inner elements are templates, their values are retrieved from - /// `init_storage_data`, identified by their key. - pub fn try_build_map( - &self, - init_storage_data: &InitStorageData, - ) -> Result { - match self { - MapRepresentation::Value { identifier, entries } => { - let entries = entries - .iter() - .map(|map_entry| { - let key = map_entry - .key() - .try_build_word(init_storage_data, identifier.name.clone())?; - let value = map_entry - .value() - .try_build_word(init_storage_data, identifier.name.clone())?; - Ok((key, value)) - }) - .collect::, _>>()?; - - StorageMap::with_entries(entries).map_err(|err| { - AccountComponentTemplateError::StorageMapHasDuplicateKeys(Box::new(err)) - }) - }, - MapRepresentation::Template { identifier } => { - if let Some(entries) = init_storage_data.map_entries(&identifier.name) { - return StorageMap::with_entries(entries.clone()).map_err(|err| { - AccountComponentTemplateError::StorageMapHasDuplicateKeys(Box::new(err)) - }); - } - - Err(AccountComponentTemplateError::PlaceholderValueNotProvided( - identifier.name.clone(), - )) - }, - } - } - - /// Validates the map representation by checking for duplicate keys and placeholder validity. - pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { - match self { - MapRepresentation::Template { .. } => Ok(()), - MapRepresentation::Value { entries, .. } => { - let mut seen_keys = BTreeSet::new(); - for entry in entries.iter() { - entry.key().validate()?; - entry.value().validate()?; - if let Ok(key) = entry - .key() - .try_build_word(&InitStorageData::default(), StorageValueName::empty()) - && !seen_keys.insert(key) - { - return Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys( - Box::from(format!("key `{key}` is duplicated")), - )); - } - } - - Ok(()) - }, - } - } -} - -impl Serializable for MapRepresentation { - fn write_into(&self, target: &mut W) { - match self { - MapRepresentation::Value { identifier, entries } => { - target.write_u8(0u8); - target.write(identifier); - target.write(entries); - }, - MapRepresentation::Template { identifier } => { - target.write_u8(1u8); - target.write(identifier); - }, - } - } -} - -impl Deserializable for MapRepresentation { - fn read_from(source: &mut R) -> Result { - let tag = source.read_u8()?; - match tag { - 0 => { - let identifier = FieldIdentifier::read_from(source)?; - let entries = Vec::::read_from(source)?; - Ok(MapRepresentation::Value { entries, identifier }) - }, - 1 => { - let identifier = FieldIdentifier::read_from(source)?; - Ok(MapRepresentation::Template { identifier }) - }, - other => Err(DeserializationError::InvalidValue(format!( - "unknown tag '{other}' for MapRepresentation" - ))), - } - } -} - -// MULTI-WORD VALUE -// ================================================================================================ - -/// Defines how multi-slot values are represented within the component's storage description. -/// -/// Each multi-word value representation can be: -/// - A predefined value that may contain a hardcoded word or a mix of fixed and templated felts. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MultiWordRepresentation { - // TODO: Once there are multi-slot template types, add a MultiWordRepresentation::Template - // here - Value { - /// The human-readable name of this multi-slot entry. - identifier: FieldIdentifier, - /// A list of values to fill the logical slot, with a length equal to the number of slots. - values: Vec<[FeltRepresentation; 4]>, - }, -} - -impl MultiWordRepresentation { - /// Returns the number of words in this representation. - pub fn num_words(&self) -> usize { - match self { - MultiWordRepresentation::Value { values, .. } => values.len(), - } - } - - /// Validates the multi-slot value. - pub fn validate(&self) -> Result<(), AccountComponentTemplateError> { - match self { - MultiWordRepresentation::Value { values, .. } => { - for slot_word in values { - for felt_in_slot in slot_word { - felt_in_slot.validate()?; - } - } - }, - } - Ok(()) - } -} - -impl Serializable for MultiWordRepresentation { - fn write_into(&self, target: &mut W) { - match self { - MultiWordRepresentation::Value { identifier, values } => { - target.write_u8(0u8); - target.write(identifier); - target.write(values); - }, - } - } -} -impl Deserializable for MultiWordRepresentation { - fn read_from(source: &mut R) -> Result { - let variant_tag = source.read_u8()?; - match variant_tag { - 0 => { - let identifier: FieldIdentifier = source.read()?; - let values: Vec<[FeltRepresentation; 4]> = source.read()?; - Ok(MultiWordRepresentation::Value { identifier, values }) - }, - _ => Err(DeserializationError::InvalidValue(format!( - "unknown variant tag '{variant_tag}' for MultiWordRepresentation" - ))), - } - } -} diff --git a/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs b/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs deleted file mode 100644 index e5955d19e2..0000000000 --- a/crates/miden-objects/src/account/component/template/storage/init_storage_data.rs +++ /dev/null @@ -1,63 +0,0 @@ -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; - -use super::StorageValueName; -use crate::Word; - -/// Represents the data required to initialize storage entries when instantiating an -/// [AccountComponent](crate::account::AccountComponent) from a -/// [template](crate::account::AccountComponentTemplate). -/// -/// An [`InitStorageData`] can be created from a TOML string when the `std` feature flag is set. -#[derive(Clone, Debug, Default)] -pub struct InitStorageData { - // TODO: Both the below fields could be a single field with a two variant enum - // (eg, BTreeMap) - /// A mapping of storage placeholder names to their corresponding storage values. - value_entries: BTreeMap, - /// A mapping of map placeholder names to their corresponding key/value entries. - map_entries: BTreeMap>, -} - -impl InitStorageData { - /// Creates a new instance of [InitStorageData]. - /// - /// A [`BTreeMap`] is constructed from the passed iterator, so duplicate keys will cause - /// overridden values. - /// - /// # Parameters - /// - /// - `entries`: An iterable collection of key-value pairs. - /// - `map_entries`: An iterable collection of storage map entries keyed by placeholder. - pub fn new( - entries: impl IntoIterator, - map_entries: impl IntoIterator)>, - ) -> Self { - let value_entries = entries - .into_iter() - .filter(|(entry_name, _)| !entry_name.as_str().is_empty()) - .collect::>(); - - InitStorageData { - value_entries, - map_entries: map_entries.into_iter().collect(), - } - } - - /// Retrieves a reference to the storage placeholders. - pub fn placeholders(&self) -> &BTreeMap { - &self.value_entries - } - - /// Returns a reference to the name corresponding to the placeholder, or - /// [`Option::None`] if the placeholder is not present. - pub fn get(&self, key: &StorageValueName) -> Option<&String> { - self.value_entries.get(key) - } - - /// Returns the map entries associated with the given placeholder name, if any. - pub fn map_entries(&self, key: &StorageValueName) -> Option<&Vec<(Word, Word)>> { - self.map_entries.get(key) - } -} diff --git a/crates/miden-objects/src/account/component/template/storage/mod.rs b/crates/miden-objects/src/account/component/template/storage/mod.rs deleted file mode 100644 index e1380e025f..0000000000 --- a/crates/miden-objects/src/account/component/template/storage/mod.rs +++ /dev/null @@ -1,1129 +0,0 @@ -use alloc::boxed::Box; -use alloc::string::String; -use alloc::vec::Vec; -use core::ops::Range; - -use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_core::{Felt, FieldElement}; -use miden_processor::DeserializationError; - -mod entry_content; -pub use entry_content::*; - -use super::AccountComponentTemplateError; -use crate::Word; -use crate::account::StorageSlot; - -mod placeholder; -pub use placeholder::{ - PlaceholderTypeRequirement, - StorageValueName, - StorageValueNameError, - TemplateType, - TemplateTypeError, -}; - -mod init_storage_data; -pub use init_storage_data::InitStorageData; - -#[cfg(feature = "std")] -pub mod toml; - -/// Alias used for iterators that collect all placeholders and their types within a component -/// template. -pub type TemplateRequirementsIter<'a> = - Box + 'a>; - -// IDENTIFIER -// ================================================================================================ - -/// An identifier for a storage entry field. -/// -/// An identifier consists of a name that identifies the field, and an optional description. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FieldIdentifier { - /// A human-readable identifier for the template. - pub name: StorageValueName, - /// An optional description explaining the purpose of this template. - pub description: Option, -} - -impl FieldIdentifier { - /// Creates a new `FieldIdentifier` with the given name and no description. - pub fn with_name(name: StorageValueName) -> Self { - Self { name, description: None } - } - - /// Creates a new `FieldIdentifier` with the given name and description. - pub fn with_description(name: StorageValueName, description: impl Into) -> Self { - Self { - name, - description: Some(description.into()), - } - } - - /// Returns the identifier name. - pub fn name(&self) -> &StorageValueName { - &self.name - } - - /// Returns the identifier description. - pub fn description(&self) -> Option<&String> { - self.description.as_ref() - } -} - -impl From for FieldIdentifier { - fn from(value: StorageValueName) -> Self { - FieldIdentifier::with_name(value) - } -} - -impl Serializable for FieldIdentifier { - fn write_into(&self, target: &mut W) { - target.write(&self.name); - target.write(&self.description); - } -} - -impl Deserializable for FieldIdentifier { - fn read_from(source: &mut R) -> Result { - let name = StorageValueName::read_from(source)?; - let description = Option::::read_from(source)?; - Ok(FieldIdentifier { name, description }) - } -} - -// STORAGE ENTRY -// ================================================================================================ - -/// Represents a single entry in the component's storage layout. -/// -/// Each entry can describe: -/// - A value slot with a single word. -/// - A map slot with a key-value map that occupies one storage slot. -/// - A multi-slot entry spanning multiple contiguous slots with multiple words (but not maps) that -/// represent a single logical value. -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(clippy::large_enum_variant)] -pub enum StorageEntry { - /// A value slot, which can contain one word. - Value { - /// The numeric index of this map slot in the component's storage. - slot: u8, - /// A description of a word, representing either a predefined value or a templated one. - word_entry: WordRepresentation, - }, - - /// A map slot, containing multiple key-value pairs. Keys and values are hex-encoded strings. - Map { - /// The numeric index of this map slot in the component's storage. - slot: u8, - /// A list of key-value pairs to initialize in this map slot. - map: MapRepresentation, - }, - - /// A multi-slot entry, representing a single logical value across multiple slots. - MultiSlot { - /// The indices of the slots that form this multi-slot entry. - slots: Range, - /// A description of the values. - word_entries: MultiWordRepresentation, - }, -} - -impl StorageEntry { - pub fn new_value(slot: u8, word_entry: impl Into) -> Self { - StorageEntry::Value { slot, word_entry: word_entry.into() } - } - - pub fn new_map(slot: u8, map: MapRepresentation) -> Self { - StorageEntry::Map { slot, map } - } - - pub fn new_multislot( - identifier: FieldIdentifier, - slots: Range, - values: Vec<[FeltRepresentation; 4]>, - ) -> Self { - StorageEntry::MultiSlot { - slots, - word_entries: MultiWordRepresentation::Value { identifier, values }, - } - } - - pub fn name(&self) -> Option<&StorageValueName> { - match self { - StorageEntry::Value { word_entry, .. } => word_entry.name(), - StorageEntry::Map { map, .. } => Some(map.name()), - StorageEntry::MultiSlot { word_entries, .. } => match word_entries { - MultiWordRepresentation::Value { identifier, .. } => Some(&identifier.name), - }, - } - } - - /// Returns the slot indices that the storage entry covers. - pub fn slot_indices(&self) -> Range { - match self { - StorageEntry::MultiSlot { slots, .. } => slots.clone(), - StorageEntry::Value { slot, .. } | StorageEntry::Map { slot, .. } => *slot..*slot + 1, - } - } - - /// Returns an iterator over all of the storage entries's value names, alongside their - /// expected type. - pub fn template_requirements(&self) -> TemplateRequirementsIter<'_> { - match self { - StorageEntry::Value { word_entry, .. } => { - word_entry.template_requirements(StorageValueName::empty()) - }, - StorageEntry::Map { map, .. } => map.template_requirements(), - StorageEntry::MultiSlot { word_entries, .. } => match word_entries { - MultiWordRepresentation::Value { identifier, values } => { - Box::new(values.iter().flat_map(move |word| { - word.iter() - .flat_map(move |f| f.template_requirements(identifier.name.clone())) - })) - }, - }, - } - } - - /// Attempts to convert the storage entry into a list of [`StorageSlot`]. - /// - /// - [`StorageEntry::Value`] would convert to a [`StorageSlot::Value`] - /// - [`StorageEntry::MultiSlot`] would convert to as many [`StorageSlot::Value`] as required by - /// the defined type - /// - [`StorageEntry::Map`] would convert to a [`StorageSlot::Map`] - /// - /// Each of the entry's values could be templated. These values are replaced for values found - /// in `init_storage_data`, identified by its key. - pub fn try_build_storage_slots( - &self, - init_storage_data: &InitStorageData, - ) -> Result, AccountComponentTemplateError> { - match self { - StorageEntry::Value { word_entry, .. } => { - let slot = - word_entry.try_build_word(init_storage_data, StorageValueName::empty())?; - Ok(vec![StorageSlot::Value(slot)]) - }, - StorageEntry::Map { map, .. } => { - let storage_map = map.try_build_map(init_storage_data)?; - Ok(vec![StorageSlot::Map(storage_map)]) - }, - StorageEntry::MultiSlot { word_entries, .. } => { - match word_entries { - MultiWordRepresentation::Value { identifier, values } => { - Ok(values - .iter() - .map(|word_repr| { - let mut result = [Felt::ZERO; 4]; - - for (index, felt_repr) in word_repr.iter().enumerate() { - result[index] = felt_repr.try_build_felt( - init_storage_data, - identifier.name.clone(), - )?; - } - // SAFETY: result is guaranteed to have all its 4 indices rewritten - Ok(StorageSlot::Value(Word::from(result))) - }) - .collect::, _>>()?) - }, - } - }, - } - } - - /// Validates the storage entry for internal consistency. - pub(super) fn validate(&self) -> Result<(), AccountComponentTemplateError> { - match self { - StorageEntry::Map { map, .. } => map.validate(), - StorageEntry::MultiSlot { slots, word_entries, .. } => { - if slots.len() == 1 { - return Err(AccountComponentTemplateError::MultiSlotSpansOneSlot); - } - - if slots.len() != word_entries.num_words() { - return Err(AccountComponentTemplateError::MultiSlotArityMismatch); - } - - word_entries.validate() - }, - StorageEntry::Value { word_entry, .. } => Ok(word_entry.validate()?), - } - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for StorageEntry { - fn write_into(&self, target: &mut W) { - match self { - StorageEntry::Value { slot, word_entry } => { - target.write_u8(0u8); - target.write_u8(*slot); - target.write(word_entry); - }, - StorageEntry::Map { slot, map } => { - target.write_u8(1u8); - target.write_u8(*slot); - target.write(map); - }, - StorageEntry::MultiSlot { word_entries, slots } => { - target.write_u8(2u8); - target.write(word_entries); - target.write(slots.start); - target.write(slots.end); - }, - } - } -} - -impl Deserializable for StorageEntry { - fn read_from(source: &mut R) -> Result { - let variant_tag = source.read_u8()?; - match variant_tag { - 0 => { - let slot = source.read_u8()?; - let word_entry: WordRepresentation = source.read()?; - Ok(StorageEntry::Value { slot, word_entry }) - }, - 1 => { - let slot = source.read_u8()?; - let map: MapRepresentation = source.read()?; - Ok(StorageEntry::Map { slot, map }) - }, - 2 => { - let word_entries: MultiWordRepresentation = source.read()?; - let slots_start: u8 = source.read()?; - let slots_end: u8 = source.read()?; - Ok(StorageEntry::MultiSlot { - slots: slots_start..slots_end, - word_entries, - }) - }, - _ => Err(DeserializationError::InvalidValue(format!( - "unknown variant tag '{variant_tag}' for StorageEntry" - ))), - } - } -} - -// MAP ENTRY -// ================================================================================================ - -/// Key-value entry for storage maps. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] -pub struct MapEntry { - key: WordRepresentation, - value: WordRepresentation, -} - -impl MapEntry { - pub fn new(key: impl Into, value: impl Into) -> Self { - Self { key: key.into(), value: value.into() } - } - - pub fn key(&self) -> &WordRepresentation { - &self.key - } - - pub fn value(&self) -> &WordRepresentation { - &self.value - } - - pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) { - let MapEntry { key, value } = self; - (key, value) - } - - pub fn template_requirements( - &self, - placeholder_prefix: StorageValueName, - ) -> TemplateRequirementsIter<'_> { - let key_iter = self.key.template_requirements(placeholder_prefix.clone()); - let value_iter = self.value.template_requirements(placeholder_prefix); - - Box::new(key_iter.chain(value_iter)) - } -} - -impl Serializable for MapEntry { - fn write_into(&self, target: &mut W) { - self.key.write_into(target); - self.value.write_into(target); - } -} - -impl Deserializable for MapEntry { - fn read_from(source: &mut R) -> Result { - let key = WordRepresentation::read_from(source)?; - let value = WordRepresentation::read_from(source)?; - Ok(MapEntry { key, value }) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use alloc::collections::{BTreeMap, BTreeSet}; - use alloc::string::ToString; - use core::error::Error; - use core::panic; - - use miden_assembly::Assembler; - use miden_core::utils::{Deserializable, Serializable}; - use miden_core::{EMPTY_WORD, Felt, Word}; - use semver::Version; - - use crate::account::component::FieldIdentifier; - use crate::account::component::template::storage::placeholder::TemplateType; - use crate::account::component::template::{ - AccountComponentMetadata, - InitStorageData, - MapEntry, - MapRepresentation, - StorageValueName, - }; - use crate::account::{ - AccountComponent, - AccountComponentTemplate, - AccountType, - FeltRepresentation, - StorageEntry, - StorageSlot, - TemplateTypeError, - WordRepresentation, - }; - use crate::errors::AccountComponentTemplateError; - use crate::testing::account_code::CODE; - use crate::{AccountError, word}; - - #[test] - fn test_storage_entry_serialization() { - let felt_array: [FeltRepresentation; 4] = [ - FeltRepresentation::from(Felt::new(0xabc)), - FeltRepresentation::from(Felt::new(1218)), - FeltRepresentation::from(Felt::new(0xdba3)), - FeltRepresentation::new_template( - TemplateType::native_felt(), - StorageValueName::new("slot3").unwrap(), - ) - .with_description("dummy description"), - ]; - - let test_word: Word = word!("0x000001"); - let test_word = test_word.map(FeltRepresentation::from); - - let map_representation = MapRepresentation::new_value( - vec![ - MapEntry { - key: WordRepresentation::new_template( - TemplateType::native_word(), - StorageValueName::new("foo").unwrap().into(), - ), - value: WordRepresentation::new_value(test_word.clone(), None), - }, - MapEntry { - key: WordRepresentation::new_value(test_word.clone(), None), - value: WordRepresentation::new_template( - TemplateType::native_word(), - StorageValueName::new("bar").unwrap().into(), - ), - }, - MapEntry { - key: WordRepresentation::new_template( - TemplateType::native_word(), - StorageValueName::new("baz").unwrap().into(), - ), - value: WordRepresentation::new_value(test_word, None), - }, - ], - StorageValueName::new("map").unwrap(), - ) - .with_description("a storage map description"); - - let storage = vec![ - StorageEntry::new_value(0, felt_array.clone()), - StorageEntry::new_map(1, map_representation), - StorageEntry::new_multislot( - FieldIdentifier::with_description( - StorageValueName::new("multi").unwrap(), - "Multi slot entry", - ), - 2..4, - vec![ - [ - FeltRepresentation::new_template( - TemplateType::native_felt(), - StorageValueName::new("test").unwrap(), - ), - FeltRepresentation::new_template( - TemplateType::native_felt(), - StorageValueName::new("test2").unwrap(), - ), - FeltRepresentation::new_template( - TemplateType::native_felt(), - StorageValueName::new("test3").unwrap(), - ), - FeltRepresentation::new_template( - TemplateType::native_felt(), - StorageValueName::new("test4").unwrap(), - ), - ], - felt_array, - ], - ), - StorageEntry::new_value( - 4, - WordRepresentation::new_template( - TemplateType::native_word(), - StorageValueName::new("single").unwrap().into(), - ), - ), - ]; - - let config = AccountComponentMetadata { - name: "Test Component".into(), - description: "This is a test component".into(), - version: Version::parse("1.0.0").unwrap(), - supported_types: BTreeSet::from([AccountType::FungibleFaucet]), - storage, - }; - let toml = config.to_toml().unwrap(); - let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap(); - - assert_eq!(deserialized, config); - } - - #[test] - pub fn toml_serde_roundtrip() { - let toml_text = r#" - name = "Test Component" - description = "This is a test component" - version = "1.0.1" - supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"] - - [[storage]] - name = "map_entry" - slot = 0 - values = [ - { key = "0x1", value = ["0x1","0x2","0x3","0"]}, - { key = "0x3", value = "0x123" }, - { key = { name = "map_key_template", description = "this tests that the default type is correctly set"}, value = "0x3" }, - ] - - [[storage]] - name = "token_metadata" - description = "Contains metadata about the token associated to the faucet account" - slot = 1 - value = [ - { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder - { type = "token_symbol", value = "TST" }, # hardcoded non-felt type - { type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder - { value = "0" }, - ] - - [[storage]] - name = "default_recallable_height" - slot = 2 - type = "word" - "#; - - let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - let requirements = component_metadata.get_placeholder_requirements(); - - assert_eq!(requirements.len(), 4); - - let supply = requirements - .get(&StorageValueName::new("token_metadata.max_supply").unwrap()) - .unwrap(); - assert_eq!(supply.r#type.as_str(), "felt"); - - let decimals = requirements - .get(&StorageValueName::new("token_metadata.decimals").unwrap()) - .unwrap(); - assert_eq!(decimals.r#type.as_str(), "u8"); - - let default_recallable_height = requirements - .get(&StorageValueName::new("default_recallable_height").unwrap()) - .unwrap(); - assert_eq!(default_recallable_height.r#type.as_str(), "word"); - - let map_key_template = requirements - .get(&StorageValueName::new("map_entry.map_key_template").unwrap()) - .unwrap(); - assert_eq!(map_key_template.r#type.as_str(), "word"); - - let library = Assembler::default().assemble_library([CODE]).unwrap(); - let template = AccountComponentTemplate::new(component_metadata, library); - - let template_bytes = template.to_bytes(); - let template_deserialized = - AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap(); - assert_eq!(template, template_deserialized); - - // Fail to parse because 2800 > u8 - let storage_placeholders = InitStorageData::new( - [ - ( - StorageValueName::new("map_entry.map_key_template").unwrap(), - "0x123".to_string(), - ), - ( - StorageValueName::new("token_metadata.max_supply").unwrap(), - 20_000u64.to_string(), - ), - (StorageValueName::new("token_metadata.decimals").unwrap(), "2800".into()), - (StorageValueName::new("default_recallable_height").unwrap(), "0".into()), - ], - BTreeMap::new(), - ); - - let component = AccountComponent::from_template(&template, &storage_placeholders); - assert_matches::assert_matches!( - component, - Err(AccountError::AccountComponentTemplateInstantiationError( - AccountComponentTemplateError::StorageValueParsingError( - TemplateTypeError::ParseError { .. } - ) - )) - ); - - // Instantiate successfully - let storage_placeholders = InitStorageData::new( - [ - ( - StorageValueName::new("map_entry.map_key_template").unwrap(), - "0x123".to_string(), - ), - ( - StorageValueName::new("token_metadata.max_supply").unwrap(), - 20_000u64.to_string(), - ), - (StorageValueName::new("token_metadata.decimals").unwrap(), "128".into()), - (StorageValueName::new("default_recallable_height").unwrap(), "0x0".into()), - ], - BTreeMap::new(), - ); - - let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap(); - assert_eq!( - component.supported_types(), - &[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode] - .into_iter() - .collect() - ); - - let storage_map = component.storage_slots.first().unwrap(); - match storage_map { - StorageSlot::Map(storage_map) => assert_eq!(storage_map.entries().count(), 3), - _ => panic!("should be map"), - } - - let value_entry = component.storage_slots().get(2).unwrap(); - match value_entry { - StorageSlot::Value(v) => { - assert_eq!(v, &EMPTY_WORD) - }, - _ => panic!("should be value"), - } - - let failed_instantiation = - AccountComponent::from_template(&template, &InitStorageData::default()); - - assert_matches::assert_matches!( - failed_instantiation, - Err(AccountError::AccountComponentTemplateInstantiationError( - AccountComponentTemplateError::PlaceholderValueNotProvided(_) - )) - ); - } - - #[test] - fn test_no_duplicate_slot_names() { - let toml_text = r#" - name = "Test Component" - description = "This is a test component" - version = "1.0.1" - supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"] - - [[storage]] - name = "test_duplicate" - slot = 0 - type = "felt" # Felt is not a valid type for word slots - "#; - - let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err(); - assert_matches::assert_matches!(err, AccountComponentTemplateError::InvalidType(_, _)) - } - - #[test] - fn parses_and_instantiates_ecdsa_template() { - let toml = r#" - name = "ecdsa_auth" - description = "Ecdsa authentication component, for verifying ECDSA K256 Keccak signatures." - version = "0.1.0" - supported-types = ["RegularAccountUpdatableCode", "RegularAccountImmutableCode", "FungibleFaucet", "NonFungibleFaucet"] - - [[storage]] - name = "ecdsa_pubkey" - description = "ecdsa public key" - slot = 0 - type = "auth::ecdsa_k256_keccak::pub_key" - "#; - - let metadata = AccountComponentMetadata::from_toml(toml).unwrap(); - assert_eq!(metadata.storage_entries().len(), 1); - assert_eq!(metadata.storage_entries()[0].name().unwrap().as_str(), "ecdsa_pubkey"); - - let library = Assembler::default().assemble_library([CODE]).unwrap(); - let template = AccountComponentTemplate::new(metadata, library); - - let init_storage = InitStorageData::new( - [(StorageValueName::new("ecdsa_pubkey").unwrap(), "0x1234".into())], - BTreeMap::new(), - ); - - let component = AccountComponent::from_template(&template, &init_storage).unwrap(); - let slot = component.storage_slots().first().expect("missing storage slot"); - match slot { - StorageSlot::Value(word) => { - let expected = Word::parse( - "0x0000000000000000000000000000000000000000000000000000000000001234", - ) - .unwrap(); - assert_eq!(word, &expected); - }, - _ => panic!("expected value storage slot"), - } - } - - #[test] - fn map_template_can_build_from_entries() { - let map_name = StorageValueName::new("procedure_thresholds").unwrap(); - let map_entry = StorageEntry::new_map(0, MapRepresentation::new_template(map_name.clone())); - - let init_data = InitStorageData::from_toml( - r#" - procedure_thresholds = [ - { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000010" }, - { key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = "0x0000000000000000000000000000000000000000000000000000000000000020" } - ] - "#, - ) - .unwrap(); - - let entries = init_data.map_entries(&map_name).expect("map entries missing"); - assert_eq!(entries.len(), 2); - assert_eq!( - entries[0], - ( - Word::parse("0x0000000000000000000000000000000000000000000000000000000000000001",) - .unwrap(), - Word::parse("0x0000000000000000000000000000000000000000000000000000000000000010",) - .unwrap(), - ) - ); - - let slots = map_entry.try_build_storage_slots(&init_data).unwrap(); - assert_eq!(slots.len(), 1); - - match &slots[0] { - StorageSlot::Map(storage_map) => { - assert_eq!(storage_map.num_entries(), 2); - let main_key = Word::parse( - "0x0000000000000000000000000000000000000000000000000000000000000001", - ) - .unwrap(); - let main_value_expected = Word::parse( - "0x0000000000000000000000000000000000000000000000000000000000000010", - ) - .unwrap(); - assert_eq!(storage_map.get(&main_key), main_value_expected); - }, - _ => panic!("expected map storage slot"), - } - } - - #[test] - fn map_template_requires_entries() { - let map_name = StorageValueName::new("procedure_thresholds").unwrap(); - let map_entry = StorageEntry::new_map(0, MapRepresentation::new_template(map_name.clone())); - - let result = map_entry.try_build_storage_slots(&InitStorageData::default()); - - assert_matches::assert_matches!( - result, - Err(AccountComponentTemplateError::PlaceholderValueNotProvided(name)) - if name.as_str() == "procedure_thresholds" - ); - - // try with an empty list - - let init_data = InitStorageData::from_toml( - r#" - procedure_thresholds = [] - "#, - ) - .unwrap(); - - let result = map_entry.try_build_storage_slots(&init_data).unwrap(); - - assert_eq!(result.len(), 1); - match &result[0] { - StorageSlot::Map(storage_map) => assert_eq!(storage_map.num_entries(), 0), - _ => panic!("expected map storage slot"), - } - } - - #[test] - fn map_placeholder_requirement_is_reported() { - let targets = [AccountType::RegularAccountImmutableCode].into_iter().collect(); - let map = - MapRepresentation::new_template(StorageValueName::new("procedure_thresholds").unwrap()) - .with_description("Configures procedure thresholds"); - - let metadata = AccountComponentMetadata::new( - "test".into(), - "desc".into(), - Version::new(1, 0, 0), - targets, - vec![StorageEntry::new_map(0, map)], - ) - .unwrap(); - - let requirements = metadata.get_placeholder_requirements(); - let requirement = requirements - .get(&StorageValueName::new("procedure_thresholds").unwrap()) - .expect("map placeholder should be reported"); - - assert_eq!(requirement.r#type.as_str(), "map"); - assert_eq!(requirement.description.as_deref(), Some("Configures procedure thresholds"),); - } - - #[test] - fn toml_template_map_roundtrip() { - let toml_text = r#" - name = "Test Component" - description = "Component with templated map" - version = "1.0.0" - supported-types = ["RegularAccountImmutableCode"] - - [[storage]] - name = "my_map" - description = "Some description" - slot = 0 - type = "map" - "#; - - let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - assert_eq!(metadata.storage_entries().len(), 1); - match metadata.storage_entries().first().unwrap() { - StorageEntry::Map { map, .. } => match map { - MapRepresentation::Template { identifier } => { - assert_eq!(identifier.name.as_str(), "my_map"); - assert_eq!(identifier.description.as_deref(), Some("Some description")); - }, - MapRepresentation::Value { .. } => panic!("expected template map"), - }, - _ => panic!("expected map storage entry"), - } - - let toml_roundtrip = metadata.to_toml().unwrap(); - assert!(toml_roundtrip.contains("type = \"map\"")); - } - - #[test] - fn toml_map_with_empty_values_creates_value_map() { - // Test that when type = "map" and values = [] is specified, - // it creates a MapRepresentation::Value with empty entries, - // not a MapRepresentation::Template - let toml_text = r#" - name = "Test Component" - description = "Component with map having empty values" - version = "1.0.0" - supported-types = ["RegularAccountImmutableCode"] - - [[storage]] - name = "executed_transactions" - description = "Map which stores executed transactions" - slot = 0 - type = "map" - values = [] - "#; - - let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - assert_eq!(metadata.storage_entries().len(), 1); - match metadata.storage_entries().first().unwrap() { - StorageEntry::Map { map, .. } => match map { - MapRepresentation::Value { identifier, entries } => { - assert_eq!(identifier.name.as_str(), "executed_transactions"); - assert_eq!( - identifier.description.as_deref(), - Some("Map which stores executed transactions") - ); - assert!(entries.is_empty(), "Expected empty entries for map with values = []"); - }, - MapRepresentation::Template { .. } => { - panic!("expected value map with empty entries, not template map") - }, - }, - _ => panic!("expected map storage entry"), - } - } - - #[test] - fn map_placeholder_populated_via_toml_array() { - let storage_entry = StorageEntry::new_map( - 0, - MapRepresentation::new_template(StorageValueName::new("my_map").unwrap()), - ); - - let init_data = InitStorageData::from_toml( - r#" - my_map = [ - { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000090" }, - { key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = ["1", "2", "3", "4"] } - ] - other_placeholder = "0xAB" - "#, - ) - .unwrap(); - - assert_eq!( - init_data.get(&StorageValueName::new("other_placeholder").unwrap()).unwrap(), - "0xAB" - ); - - let slots = storage_entry.try_build_storage_slots(&init_data).unwrap(); - assert_eq!(slots.len(), 1); - match &slots[0] { - StorageSlot::Map(storage_map) => { - assert_eq!(storage_map.num_entries(), 2); - let second_value = Word::from([ - Felt::new(1u64), - Felt::new(2u64), - Felt::new(3u64), - Felt::new(4u64), - ]); - let second_key = Word::try_from( - "0x0000000000000000000000000000000000000000000000000000000000000002", - ) - .unwrap(); - assert_eq!(storage_map.get(&second_key), second_value); - }, - _ => panic!("expected map storage slot"), - } - } - - #[test] - fn toml_map_type_with_values_is_invalid() { - let toml_text = r#" - name = "Invalid" - description = "Invalid map" - version = "1.0.0" - supported-types = ["RegularAccountImmutableCode"] - - [[storage]] - name = "bad_map" - slot = 0 - type = "map" - values = [ { key = "0x1", value = "0x2" } ] - "#; - - let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - match metadata.storage_entries().first().unwrap() { - StorageEntry::Map { map, .. } => match map { - MapRepresentation::Value { entries, .. } => { - assert_eq!(entries.len(), 1); - }, - _ => panic!("expected static map"), - }, - _ => panic!("expected map storage entry"), - } - } - - #[test] - fn toml_map_values_with_non_map_type_is_invalid() { - let toml_text = r#" - name = "Invalid" - description = "Invalid map" - version = "1.0.0" - supported-types = ["RegularAccountImmutableCode"] - - [[storage]] - name = "bad_map" - slot = 0 - type = "word" - values = [ { key = "0x1", value = "0x2" } ] - "#; - - let result = AccountComponentMetadata::from_toml(toml_text); - assert_matches::assert_matches!( - result, - Err(AccountComponentTemplateError::TomlDeserializationError(_)) - ); - } - - #[test] - fn toml_fail_multislot_arity_mismatch() { - let toml_text = r#" - name = "Test Component" - description = "Test multislot arity mismatch" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "multislot_test" - slots = [0, 1] - values = [ - [ "0x1", "0x2", "0x3", "0x4" ] - ] - "#; - - let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err(); - assert_matches::assert_matches!(err, AccountComponentTemplateError::MultiSlotArityMismatch); - } - - #[test] - fn toml_fail_multislot_duplicate_slot() { - let toml_text = r#" - name = "Test Component" - description = "Test multislot duplicate slot" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "multislot_duplicate" - slots = [0, 1] - values = [ - [ "0x1", "0x2", "0x3", "0x4" ], - [ "0x5", "0x6", "0x7", "0x8" ] - ] - - [[storage]] - name = "multislot_duplicate" - slots = [1, 2] - values = [ - [ "0x1", "0x2", "0x3", "0x4" ], - [ "0x5", "0x6", "0x7", "0x8" ] - ] - "#; - - let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err(); - assert_matches::assert_matches!(err, AccountComponentTemplateError::DuplicateSlot(1)); - } - - #[test] - fn toml_fail_multislot_non_contiguous_slots() { - let toml_text = r#" - name = "Test Component" - description = "Test multislot non contiguous" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "multislot_non_contiguous" - slots = [0, 2] - values = [ - [ "0x1", "0x2", "0x3", "0x4" ], - [ "0x5", "0x6", "0x7", "0x8" ] - ] - "#; - - let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err(); - // validate inner serde error - assert!(err.source().unwrap().to_string().contains("are not contiguous")); - } - - #[test] - fn toml_fail_duplicate_storage_entry_names() { - let toml_text = r#" - name = "Test Component" - description = "Component with duplicate storage entry names" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - # placeholder - name = "duplicate" - slot = 0 - type = "word" - - [[storage]] - name = "duplicate" - slot = 1 - value = [ "0x1", "0x1", "0x1", "0x1" ] - "#; - - let result = AccountComponentMetadata::from_toml(toml_text); - assert_matches::assert_matches!( - result.unwrap_err(), - AccountComponentTemplateError::DuplicateEntryNames(_) - ); - } - - #[test] - fn toml_fail_multislot_spans_one_slot() { - let toml_text = r#" - name = "Test Component" - description = "Test multislot spans one slot" - version = "1.0.1" - supported-types = ["RegularAccountImmutableCode"] - - [[storage]] - name = "multislot_one_slot" - slots = [0] - values = [ - [ "0x1", "0x2", "0x3", "0x4" ], - ] - "#; - - let result = AccountComponentMetadata::from_toml(toml_text); - assert_matches::assert_matches!( - result.unwrap_err(), - AccountComponentTemplateError::MultiSlotSpansOneSlot - ); - } - - #[test] - fn test_toml_multislot_success() { - let toml_text = r#" - name = "Test Component" - description = "A multi-slot success scenario" - version = "1.0.1" - supported-types = ["FungibleFaucet"] - - [[storage]] - name = "multi_slot_example" - slots = [0, 1, 2] - values = [ - ["0x1", "0x2", "0x3", "0x4"], - ["0x5", "0x6", "0x7", "0x8"], - ["0x9", "0xa", "0xb", "0xc"] - ] - "#; - - let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap(); - match &metadata.storage_entries()[0] { - StorageEntry::MultiSlot { slots, word_entries } => match word_entries { - crate::account::component::template::MultiWordRepresentation::Value { - identifier, - values, - } => { - assert_eq!(identifier.name.as_str(), "multi_slot_example"); - assert_eq!(slots, &(0..3)); - assert_eq!(values.len(), 3); - }, - }, - _ => panic!("expected multislot"), - } - } -} diff --git a/crates/miden-objects/src/account/component/template/storage/placeholder.rs b/crates/miden-objects/src/account/component/template/storage/placeholder.rs deleted file mode 100644 index a4079f36d1..0000000000 --- a/crates/miden-objects/src/account/component/template/storage/placeholder.rs +++ /dev/null @@ -1,549 +0,0 @@ -use alloc::boxed::Box; -use alloc::collections::BTreeMap; -use alloc::string::{String, ToString}; -use core::error::Error; -use core::fmt::{self, Display}; - -use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_core::{Felt, Word}; -use miden_crypto::dsa::{ecdsa_k256_keccak, rpo_falcon512}; -use miden_processor::DeserializationError; -use thiserror::Error; - -use crate::asset::TokenSymbol; -use crate::utils::sync::LazyLock; - -/// A global registry for template converters. -/// -/// It is used during component instantiation to dynamically convert template placeholders into -/// their respective storage values. -pub static TEMPLATE_REGISTRY: LazyLock = LazyLock::new(|| { - let mut registry = TemplateRegistry::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::(); - registry.register_word_type::(); - registry -}); - -// STORAGE VALUE NAME -// ================================================================================================ - -/// A simple wrapper type around a string key that identifies values. -/// -/// A storage value name is a string that identifies dynamic values within a component's metadata -/// storage entries. -/// -/// These names can be chained together, in a way that allows composing unique keys for inner -/// templated elements. -/// -/// At component instantiation, a map of names to values must be provided to dynamically -/// replace these placeholders with the instance's actual values. -#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] -#[cfg_attr(feature = "std", serde(transparent))] -pub struct StorageValueName { - fully_qualified_name: String, -} - -impl StorageValueName { - /// Creates a new [`StorageValueName`] from the provided string. - /// - /// A [`StorageValueName`] serves as an identifier for storage values that are determined at - /// instantiation time of an [AccountComponentTemplate](super::super::AccountComponentTemplate). - /// - /// The key can consist of one or more segments separated by dots (`.`). - /// Each segment must be non-empty and may contain only alphanumeric characters, underscores - /// (`_`), or hyphens (`-`). - /// - /// # Errors - /// - /// This method returns an error if: - /// - Any segment (or the whole key) is empty. - /// - Any segment contains invalid characters. - pub fn new(base: impl Into) -> Result { - let base: String = base.into(); - for segment in base.split('.') { - Self::validate_segment(segment)?; - } - Ok(Self { fully_qualified_name: base }) - } - - /// Creates an empty [`StorageValueName`]. - pub(crate) fn empty() -> Self { - StorageValueName { fully_qualified_name: String::default() } - } - - /// Adds a suffix to the storage value name, separated by a period. - #[must_use] - pub fn with_suffix(self, suffix: &StorageValueName) -> StorageValueName { - let mut key = self; - if !suffix.as_str().is_empty() { - if !key.as_str().is_empty() { - key.fully_qualified_name.push('.'); - } - key.fully_qualified_name.push_str(suffix.as_str()); - } - - key - } - - /// Returns the fully qualified name as a string slice. - pub fn as_str(&self) -> &str { - &self.fully_qualified_name - } - - fn validate_segment(segment: &str) -> Result<(), StorageValueNameError> { - if segment.is_empty() { - return Err(StorageValueNameError::EmptySegment); - } - if let Some(offending_char) = - segment.chars().find(|&c| !(c.is_ascii_alphanumeric() || c == '_' || c == '-')) - { - return Err(StorageValueNameError::InvalidCharacter { - part: segment.to_string(), - character: offending_char, - }); - } - - Ok(()) - } -} - -impl Display for StorageValueName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl Serializable for StorageValueName { - fn write_into(&self, target: &mut W) { - target.write(&self.fully_qualified_name); - } -} - -impl Deserializable for StorageValueName { - fn read_from(source: &mut R) -> Result { - let key: String = source.read()?; - Ok(StorageValueName { fully_qualified_name: key }) - } -} - -#[derive(Debug, Error)] -pub enum StorageValueNameError { - #[error("key segment is empty")] - EmptySegment, - #[error("key segment '{part}' contains invalid character '{character}'")] - InvalidCharacter { part: String, character: char }, -} - -// TEMPLATE TYPE ERROR -// ================================================================================================ - -/// Errors that can occur when parsing or converting template types. -/// -/// This enum covers various failure cases including parsing errors, conversion errors, -/// unsupported conversions, and cases where a required type is not found in the registry. -#[derive(Debug, Error)] -pub enum TemplateTypeError { - #[error("conversion error: {0}")] - ConversionError(String), - #[error("felt type ` {0}` not found in the type registry")] - FeltTypeNotFound(TemplateType), - #[error("invalid type name `{0}`: {1}")] - InvalidTypeName(String, String), - #[error("failed to parse input `{input}` as `{template_type}`")] - ParseError { - input: String, - template_type: TemplateType, - source: Box, - }, - #[error("word type ` {0}` not found in the type registry")] - WordTypeNotFound(TemplateType), -} - -impl TemplateTypeError { - /// Creates a [`TemplateTypeError::ParseError`]. - pub fn parse( - input: impl Into, - template_type: TemplateType, - source: impl Error + Send + Sync + 'static, - ) -> Self { - TemplateTypeError::ParseError { - input: input.into(), - template_type, - source: Box::new(source), - } - } -} - -// TEMPLATE TYPE -// ================================================================================================ - -/// A newtype wrapper around a `String`, representing a template's type identifier. -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] -#[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] -#[cfg_attr(feature = "std", serde(transparent))] -pub struct TemplateType(String); - -impl TemplateType { - /// Creates a new [`TemplateType`] from a `String`. - /// - /// The name must follow a Rust-style namespace format, consisting of one or more segments - /// (non-empty, and alphanumerical) separated by double-colon (`::`) delimiters. - /// - /// # Errors - /// - /// - If the identifier is empty. - /// - If it is composed of one or more segments separated by `::`. - /// - If any segment is empty or contains something other than alphanumerical - /// characters/underscores. - pub fn new(s: impl Into) -> Result { - let s = s.into(); - if s.is_empty() { - return Err(TemplateTypeError::InvalidTypeName( - s.clone(), - "template type identifier is empty".to_string(), - )); - } - for segment in s.split("::") { - if segment.is_empty() { - return Err(TemplateTypeError::InvalidTypeName( - s.clone(), - "empty segment in template type identifier".to_string(), - )); - } - if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { - return Err(TemplateTypeError::InvalidTypeName( - s.clone(), - format!("segment '{segment}' contains invalid characters"), - )); - } - } - Ok(Self(s)) - } - - /// Returns the [`TemplateType`] for the native [`Felt`] type. - pub fn native_felt() -> TemplateType { - TemplateType::new("felt").expect("type is well formed") - } - - /// Returns the [`TemplateType`] for the native [`Word`] type. - pub fn native_word() -> TemplateType { - TemplateType::new("word").expect("type is well formed") - } - - /// Returns the [`TemplateType`] for storage map placeholders. - pub fn storage_map() -> TemplateType { - TemplateType::new("map").expect("type is well formed") - } - - /// Returns a reference to the inner string. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl Display for TemplateType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl Serializable for TemplateType { - fn write_into(&self, target: &mut W) { - target.write(self.0.clone()) - } -} - -impl Deserializable for TemplateType { - fn read_from(source: &mut R) -> Result { - let id: String = source.read()?; - - TemplateType::new(id).map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} - -// TEMPLATE REQUIREMENT -// ================================================================================================ - -/// Describes the expected type and additional metadata for a templated storage entry. -/// -/// A `PlaceholderTypeRequirement` specifies the expected type identifier for a storage entry as -/// well as an optional description. This information is used to validate and provide context for -/// dynamic storage values. -#[derive(Debug)] -pub struct PlaceholderTypeRequirement { - /// The expected type identifier. - pub r#type: TemplateType, - /// An optional description providing additional context. - pub description: Option, -} - -// TEMPLATE TRAITS -// ================================================================================================ - -/// Trait for converting a string into a single `Felt`. -pub trait TemplateFelt { - /// Returns the type identifier. - fn type_name() -> TemplateType; - /// Parses the input string into a `Felt`. - fn parse_felt(input: &str) -> Result; -} - -/// Trait for converting a string into a single `Word`. -pub trait TemplateWord { - /// Returns the type identifier. - fn type_name() -> TemplateType; - /// Parses the input string into a `Word`. - fn parse_word(input: &str) -> Result; -} - -// FELT IMPLS FOR NATIVE TYPES -// ================================================================================================ - -impl TemplateFelt for u8 { - fn type_name() -> TemplateType { - TemplateType::new("u8").expect("type is well formed") - } - - fn parse_felt(input: &str) -> Result { - let native: u8 = input - .parse() - .map_err(|err| TemplateTypeError::parse(input.to_string(), Self::type_name(), err))?; - Ok(Felt::from(native)) - } -} - -impl TemplateFelt for u16 { - fn type_name() -> TemplateType { - TemplateType::new("u16").expect("type is well formed") - } - - fn parse_felt(input: &str) -> Result { - let native: u16 = input - .parse() - .map_err(|err| TemplateTypeError::parse(input.to_string(), Self::type_name(), err))?; - Ok(Felt::from(native)) - } -} - -impl TemplateFelt for u32 { - fn type_name() -> TemplateType { - TemplateType::new("u32").expect("type is well formed") - } - - fn parse_felt(input: &str) -> Result { - let native: u32 = input - .parse() - .map_err(|err| TemplateTypeError::parse(input.to_string(), Self::type_name(), err))?; - Ok(Felt::from(native)) - } -} - -impl TemplateFelt for Felt { - fn type_name() -> TemplateType { - TemplateType::new("felt").expect("type is well formed") - } - - fn parse_felt(input: &str) -> Result { - let n = if let Some(hex) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) { - u64::from_str_radix(hex, 16) - } else { - input.parse::() - } - .map_err(|err| TemplateTypeError::parse(input.to_string(), Self::type_name(), err))?; - Felt::try_from(n).map_err(|_| TemplateTypeError::ConversionError(input.to_string())) - } -} - -impl TemplateFelt for TokenSymbol { - fn type_name() -> TemplateType { - TemplateType::new("token_symbol").expect("type is well formed") - } - fn parse_felt(input: &str) -> Result { - let token = TokenSymbol::new(input) - .map_err(|err| TemplateTypeError::parse(input.to_string(), Self::type_name(), err))?; - Ok(Felt::from(token)) - } -} - -// WORD IMPLS FOR NATIVE TYPES -// ================================================================================================ - -#[derive(Debug, Error)] -#[error("error parsing word: {0}")] -struct WordParseError(String); - -/// Pads a hex string to 64 characters (excluding the 0x prefix). -/// -/// If the input starts with "0x" and has fewer than 64 hex characters after the prefix, -/// it will be left-padded with zeros. Otherwise, returns the input unchanged. -fn pad_hex_string(input: &str) -> String { - if input.starts_with("0x") && input.len() < 66 { - // 66 = "0x" + 64 hex chars - let hex_part = &input[2..]; - let padding = "0".repeat(64 - hex_part.len()); - format!("0x{}{}", padding, hex_part) - } else { - input.to_string() - } -} - -impl TemplateWord for Word { - fn type_name() -> TemplateType { - TemplateType::native_word() - } - fn parse_word(input: &str) -> Result { - let padded_input = pad_hex_string(input); - - Word::try_from(padded_input.as_str()).map_err(|err| { - TemplateTypeError::parse( - input.to_string(), // Use original input in error - Self::type_name(), - WordParseError(err.to_string()), - ) - }) - } -} - -impl TemplateWord for rpo_falcon512::PublicKey { - fn type_name() -> TemplateType { - TemplateType::new("auth::rpo_falcon512::pub_key").expect("type is well formed") - } - fn parse_word(input: &str) -> Result { - let padded_input = pad_hex_string(input); - - Word::try_from(padded_input.as_str()).map_err(|err| { - TemplateTypeError::parse( - input.to_string(), // Use original input in error - Self::type_name(), - WordParseError(err.to_string()), - ) - }) - } -} - -impl TemplateWord for ecdsa_k256_keccak::PublicKey { - fn type_name() -> TemplateType { - TemplateType::new("auth::ecdsa_k256_keccak::pub_key").expect("type is well formed") - } - fn parse_word(input: &str) -> Result { - let padded_input = pad_hex_string(input); - - Word::try_from(padded_input.as_str()).map_err(|err| { - TemplateTypeError::parse( - input.to_string(), - Self::type_name(), - WordParseError(err.to_string()), - ) - }) - } -} - -// TYPE ALIASES FOR CONVERTER CLOSURES -// ================================================================================================ - -/// Type alias for a function that converts a string into a [`Felt`] value. -type TemplateFeltConverter = fn(&str) -> Result; - -/// Type alias for a function that converts a string into a [`Word`]. -type TemplateWordConverter = fn(&str) -> Result; - -// TODO: Implement converting to list of words for multi-slot values - -// TEMPLATE REGISTRY -// ================================================================================================ - -/// Registry for template converters. -/// -/// This registry maintains mappings from type identifiers (as strings) to conversion functions for -/// [`Felt`], [`Word`], and [`Vec`] types. It is used to dynamically parse template inputs -/// into their corresponding storage representations. -#[derive(Clone, Debug, Default)] -pub struct TemplateRegistry { - felt: BTreeMap, - word: BTreeMap, -} - -impl TemplateRegistry { - /// Creates a new, empty `TemplateRegistry`. - /// - /// The registry is initially empty and conversion functions can be registered using the - /// `register_*_type` methods. - pub fn new() -> Self { - Self { ..Default::default() } - } - - /// Registers a `TemplateFelt` converter, to interpret a string as a [`Felt``]. - pub fn register_felt_type(&mut self) { - let key = T::type_name(); - self.felt.insert(key, T::parse_felt); - } - - /// Registers a `TemplateWord` converter, to interpret a string as a [`Word`]. - pub fn register_word_type(&mut self) { - let key = T::type_name(); - self.word.insert(key, T::parse_word); - } - - /// Attempts to parse a string into a `Felt` using the registered converter for the given type - /// name. - /// - /// # Arguments - /// - /// - type_name: A string that acts as the type identifier. - /// - value: The string representation of the value to be parsed. - /// - /// # Errors - /// - /// - If the type is not registered or if the conversion fails. - pub fn try_parse_felt( - &self, - type_name: &TemplateType, - value: &str, - ) -> Result { - let converter = self - .felt - .get(type_name) - .ok_or(TemplateTypeError::FeltTypeNotFound(type_name.clone()))?; - converter(value) - } - - /// Attempts to parse a string into a `Word` using the registered converter for the given type - /// name. - /// - /// # Arguments - /// - /// - type_name: A string that acts as the type identifier. - /// - value: The string representation of the value to be parsed. - /// - /// # Errors - /// - /// - If the type is not registered or if the conversion fails. - pub fn try_parse_word( - &self, - type_name: &TemplateType, - value: &str, - ) -> Result { - let converter = self - .word - .get(type_name) - .ok_or(TemplateTypeError::WordTypeNotFound(type_name.clone()))?; - converter(value) - } - - /// Returns `true` if a `TemplateFelt` is registered for the given type. - pub fn contains_felt_type(&self, type_name: &TemplateType) -> bool { - self.felt.contains_key(type_name) - } - - /// Returns `true` if a `TemplateWord` is registered for the given type. - pub fn contains_word_type(&self, type_name: &TemplateType) -> bool { - self.word.contains_key(type_name) - } -} diff --git a/crates/miden-objects/src/account/component/template/storage/toml.rs b/crates/miden-objects/src/account/component/template/storage/toml.rs deleted file mode 100644 index 396c88f0c7..0000000000 --- a/crates/miden-objects/src/account/component/template/storage/toml.rs +++ /dev/null @@ -1,789 +0,0 @@ -use alloc::collections::BTreeMap; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::fmt; - -use miden_core::{Felt, Word}; -use serde::de::value::MapAccessDeserializer; -use serde::de::{self, Error, MapAccess, SeqAccess, Visitor}; -use serde::ser::{SerializeMap, SerializeStruct}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use thiserror::Error; - -use super::placeholder::TemplateType; -use super::{ - FeltRepresentation, - InitStorageData, - MapEntry, - MapRepresentation, - MultiWordRepresentation, - StorageEntry, - StorageValueNameError, - WordRepresentation, -}; -use crate::account::component::FieldIdentifier; -use crate::account::component::template::storage::placeholder::{TEMPLATE_REGISTRY, TemplateFelt}; -use crate::account::{AccountComponentMetadata, StorageValueName}; -use crate::errors::AccountComponentTemplateError; - -// ACCOUNT COMPONENT METADATA TOML FROM/TO -// ================================================================================================ - -impl AccountComponentMetadata { - /// Deserializes `toml_string` and validates the resulting [AccountComponentMetadata] - /// - /// # Errors - /// - /// - If deserialization fails - /// - If the template specifies storage slots with duplicates. - /// - If the template includes slot numbers that do not start at zero. - /// - If storage slots in the template are not contiguous. - pub fn from_toml(toml_string: &str) -> Result { - let component: AccountComponentMetadata = toml::from_str(toml_string) - .map_err(AccountComponentTemplateError::TomlDeserializationError)?; - - component.validate()?; - Ok(component) - } - - /// Serializes the account component template into a TOML string. - pub fn to_toml(&self) -> Result { - let toml = - toml::to_string(self).map_err(AccountComponentTemplateError::TomlSerializationError)?; - Ok(toml) - } -} - -// WORD REPRESENTATION SERIALIZATION -// ================================================================================================ - -impl Serialize for WordRepresentation { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - WordRepresentation::Template { identifier, r#type } => { - let mut state = serializer.serialize_struct("WordRepresentation", 3)?; - state.serialize_field("name", &identifier.name())?; - state.serialize_field("description", &identifier.description())?; - state.serialize_field("type", r#type)?; - state.end() - }, - WordRepresentation::Value { identifier, value } => { - let mut state = serializer.serialize_struct("WordRepresentation", 3)?; - - state.serialize_field("name", &identifier.as_ref().map(|id| id.name()))?; - state.serialize_field( - "description", - &identifier.as_ref().map(|id| id.description()), - )?; - state.serialize_field("value", value)?; - state.end() - }, - } - } -} - -impl<'de> Deserialize<'de> for WordRepresentation { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct WordRepresentationVisitor; - - impl<'de> Visitor<'de> for WordRepresentationVisitor { - type Value = WordRepresentation; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string or a map representing a WordRepresentation") - } - - // A bare string is interpreted it as a Value variant. - fn visit_str(self, value: &str) -> Result - where - E: Error, - { - let parsed_value = Word::parse(value).map_err(|_err| { - E::invalid_value( - serde::de::Unexpected::Str(value), - &"a valid hexadecimal string", - ) - })?; - Ok(<[Felt; _]>::from(&parsed_value).into()) - } - - fn visit_string(self, value: String) -> Result - where - E: Error, - { - self.visit_str(&value) - } - - fn visit_seq(self, seq: A) -> Result - where - A: SeqAccess<'de>, - { - // Deserialize as a list of felt representations - let elements: Vec = - Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?; - if elements.len() != 4 { - return Err(Error::invalid_length( - elements.len(), - &"expected an array of 4 elements", - )); - } - let value: [FeltRepresentation; 4] = - elements.try_into().expect("length was checked"); - Ok(WordRepresentation::new_value(value, None)) - } - - fn visit_map(self, map: M) -> Result - where - M: MapAccess<'de>, - { - #[derive(Deserialize, Debug)] - struct WordRepresentationHelper { - name: Option, - description: Option, - // The "value" field (if present) must be an array of 4 FeltRepresentations. - value: Option<[FeltRepresentation; 4]>, - #[serde(rename = "type")] - r#type: Option, - } - - let helper = - WordRepresentationHelper::deserialize(MapAccessDeserializer::new(map))?; - - if let Some(value) = helper.value { - let identifier = helper - .name - .map(|n| parse_field_identifier::(n, helper.description.clone())) - .transpose()?; - Ok(WordRepresentation::Value { value, identifier }) - } else { - // Otherwise, we expect a Template variant (name is required for identification) - let identifier = expect_parse_field_identifier::( - helper.name, - helper.description, - "word template", - )?; - let r#type = helper.r#type.unwrap_or_else(TemplateType::native_word); - Ok(WordRepresentation::Template { r#type, identifier }) - } - } - } - - deserializer.deserialize_any(WordRepresentationVisitor) - } -} - -// FELT REPRESENTATION SERIALIZATION -// ================================================================================================ - -impl Serialize for FeltRepresentation { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - FeltRepresentation::Value { identifier, value } => { - let hex = value.to_string(); - if identifier.is_none() { - serializer.serialize_str(&hex) - } else { - let mut state = serializer.serialize_struct("FeltRepresentation", 3)?; - if let Some(id) = identifier { - state.serialize_field("name", &id.name)?; - state.serialize_field("description", &id.description)?; - } - state.serialize_field("value", &hex)?; - state.end() - } - }, - FeltRepresentation::Template { identifier, r#type } => { - let mut state = serializer.serialize_struct("FeltRepresentation", 3)?; - state.serialize_field("name", &identifier.name)?; - state.serialize_field("description", &identifier.description)?; - state.serialize_field("type", r#type)?; - state.end() - }, - } - } -} - -impl<'de> Deserialize<'de> for FeltRepresentation { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - // Felts can be deserialized as either: - // - // - Scalars (parsed from strings) - // - A table object that can or cannot hardcode a value. If not present, this is a - // placeholder type - #[derive(Deserialize)] - #[serde(untagged)] - enum Intermediate { - Map { - name: Option, - description: Option, - #[serde(default)] - value: Option, - #[serde(rename = "type")] - r#type: Option, - }, - Scalar(String), - } - - let intermediate = Intermediate::deserialize(deserializer)?; - match intermediate { - Intermediate::Scalar(s) => { - let felt = Felt::parse_felt(&s) - .map_err(|e| D::Error::custom(format!("failed to parse Felt: {e}")))?; - Ok(FeltRepresentation::Value { identifier: None, value: felt }) - }, - Intermediate::Map { name, description, value, r#type } => { - // Get the defined type, or the default if it was not specified - let felt_type = r#type.unwrap_or_else(TemplateType::native_felt); - if let Some(val_str) = value { - // Parse into felt from the input string - let felt = - TEMPLATE_REGISTRY.try_parse_felt(&felt_type, &val_str).map_err(|e| { - D::Error::custom(format!("failed to parse {felt_type} as Felt: {e}")) - })?; - let identifier = name - .map(|n| parse_field_identifier::(n, description.clone())) - .transpose()?; - Ok(FeltRepresentation::Value { identifier, value: felt }) - } else { - // No value provided, so this is a placeholder - let identifier = expect_parse_field_identifier::( - name, - description, - "map template", - )?; - Ok(FeltRepresentation::Template { r#type: felt_type, identifier }) - } - }, - } - } -} - -// STORAGE VALUES -// ================================================================================================ - -/// Represents the type of values that can be found in a storage slot's `values` field. -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -enum StorageValues { - /// List of individual words (for multi-slot entries). - Words(Vec<[FeltRepresentation; 4]>), - /// List of key-value entries (for map storage slots). - MapEntries(Vec), -} - -// STORAGE ENTRY SERIALIZATION -// ================================================================================================ - -#[derive(Default, Debug, Deserialize, Serialize)] -struct RawStorageEntry { - #[serde(flatten)] - identifier: Option, - slot: Option, - slots: Option>, - #[serde(rename = "type")] - word_type: Option, - value: Option<[FeltRepresentation; 4]>, - values: Option, -} - -impl From for RawStorageEntry { - fn from(entry: StorageEntry) -> Self { - match entry { - StorageEntry::Value { slot, word_entry } => match word_entry { - WordRepresentation::Value { identifier, value } => RawStorageEntry { - slot: Some(slot), - identifier, - value: Some(value), - ..Default::default() - }, - WordRepresentation::Template { identifier, r#type } => RawStorageEntry { - slot: Some(slot), - identifier: Some(identifier), - word_type: Some(r#type), - ..Default::default() - }, - }, - StorageEntry::Map { slot, map } => match map { - MapRepresentation::Value { identifier, entries } => RawStorageEntry { - slot: Some(slot), - identifier: Some(FieldIdentifier { - name: identifier.name, - description: identifier.description, - }), - values: Some(StorageValues::MapEntries(entries)), - ..Default::default() - }, - MapRepresentation::Template { identifier } => RawStorageEntry { - slot: Some(slot), - identifier: Some(FieldIdentifier { - name: identifier.name, - description: identifier.description, - }), - word_type: Some(TemplateType::storage_map()), - ..Default::default() - }, - }, - StorageEntry::MultiSlot { slots, word_entries } => match word_entries { - MultiWordRepresentation::Value { identifier, values } => RawStorageEntry { - slot: None, - identifier: Some(identifier), - slots: Some(slots.collect()), - values: Some(StorageValues::Words(values)), - ..Default::default() - }, - }, - } - } -} - -impl Serialize for StorageEntry { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let raw_storage_entry: RawStorageEntry = self.clone().into(); - raw_storage_entry.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for StorageEntry { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let raw = RawStorageEntry::deserialize(deserializer)?; - - if let Some(word_entry) = raw.value { - // If a value was provided, this is a WordRepresentation::Value entry - let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "value entry"))?; - let identifier = raw.identifier; - Ok(StorageEntry::Value { - slot, - word_entry: WordRepresentation::Value { value: word_entry, identifier }, - }) - } else if let Some(StorageValues::MapEntries(map_entries)) = raw.values { - // If `values` field contains key/value pairs, deserialize as map - let identifier = - raw.identifier.ok_or_else(|| missing_field_for("identifier", "map entry"))?; - let name = identifier.name; - let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "map entry"))?; - if let Some(word_type) = raw.word_type.clone() - && word_type != TemplateType::storage_map() - { - return Err(D::Error::custom( - "map storage entries with `values` must have `type = \"map\"`", - )); - } - let mut map = MapRepresentation::new_value(map_entries, name); - if let Some(desc) = identifier.description { - map = map.with_description(desc); - } - Ok(StorageEntry::Map { slot, map }) - } else if let Some(word_type) = raw.word_type.clone() - && word_type == TemplateType::storage_map() - { - let identifier = - raw.identifier.ok_or_else(|| missing_field_for("identifier", "map entry"))?; - let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "map entry"))?; - let FieldIdentifier { name, description } = identifier; - - // If values is specified (even if empty), create a value map. - // Due to #[serde(untagged)] on StorageValues, values = [] gets deserialized - // as StorageValues::Words(vec![]), so we need to treat it as an empty map. - // Otherwise, create a template map. - let mut map = if raw.values.is_some() { - MapRepresentation::new_value(Vec::new(), name) - } else { - MapRepresentation::new_template(name) - }; - - if let Some(desc) = description { - map = map.with_description(desc); - } - Ok(StorageEntry::Map { slot, map }) - } else if let Some(StorageValues::Words(values)) = raw.values { - let identifier = raw - .identifier - .ok_or_else(|| missing_field_for("identifier", "multislot entry"))?; - - let mut slots = - raw.slots.ok_or_else(|| missing_field_for("slots", "multislot entry"))?; - - // Sort so we can check contiguity - slots.sort_unstable(); - for pair in slots.windows(2) { - if pair[1] != pair[0] + 1 { - return Err(serde::de::Error::custom(format!( - "`slots` in the `{}` storage entry are not contiguous", - identifier.name - ))); - } - } - let start = slots[0]; - let end = slots.last().expect("checked validity") + 1; - Ok(StorageEntry::new_multislot(identifier, start..end, values)) - } else if let Some(word_type) = raw.word_type { - // If a type was provided instead, this is a WordRepresentation::Template entry - let slot = raw.slot.ok_or_else(|| missing_field_for("slot", "single-slot entry"))?; - let identifier = raw - .identifier - .ok_or_else(|| missing_field_for("identifier", "single-slot entry"))?; - let word_entry = WordRepresentation::Template { r#type: word_type, identifier }; - Ok(StorageEntry::Value { slot, word_entry }) - } else { - Err(D::Error::custom("placeholder storage entries require the `type` field")) - } - } -} - -// INIT STORAGE DATA -// ================================================================================================ - -impl InitStorageData { - /// Creates an instance of [`InitStorageData`] from a TOML string. - /// - /// This method parses the provided TOML and flattens nested tables into - /// dot‑separated keys using [`StorageValueName`] as keys. All values are converted to plain - /// strings (so that, for example, `key = 10` and `key = "10"` both yield - /// `String::from("10")` as the value). - /// - /// # Errors - /// - /// - If duplicate keys or empty tables are found in the string - /// - If the TOML string includes arrays - pub fn from_toml(toml_str: &str) -> Result { - let value: toml::Value = toml::from_str(toml_str)?; - let mut value_entries = BTreeMap::new(); - let mut map_entries = BTreeMap::new(); - // Start with an empty prefix (i.e. the default, which is an empty string) - Self::flatten_parse_toml_value( - StorageValueName::empty(), - value, - &mut value_entries, - &mut map_entries, - )?; - - Ok(InitStorageData::new(value_entries, map_entries)) - } - - /// Recursively flattens a TOML `Value` into a flat mapping. - /// - /// When recursing into nested tables, keys are combined using - /// [`StorageValueName::with_suffix`]. If an encountered table is empty (and not the top-level), - /// an error is returned. Arrays are not supported. - fn flatten_parse_toml_value( - prefix: StorageValueName, - value: toml::Value, - value_entries: &mut BTreeMap, - map_entries: &mut BTreeMap>, - ) -> Result<(), InitStorageDataError> { - match value { - toml::Value::Table(table) => { - // If this is not the root and the table is empty, error - if !prefix.as_str().is_empty() && table.is_empty() { - return Err(InitStorageDataError::EmptyTable(prefix.as_str().into())); - } - for (key, val) in table { - // Create a new key and combine it with the current prefix. - let new_key = StorageValueName::new(key.to_string()) - .map_err(InitStorageDataError::InvalidStorageValueName)?; - let new_prefix = prefix.clone().with_suffix(&new_key); - Self::flatten_parse_toml_value(new_prefix, val, value_entries, map_entries)?; - } - }, - toml::Value::Array(items) if items.is_empty() => { - if prefix.as_str().is_empty() { - return Err(InitStorageDataError::ArraysNotSupported); - } - map_entries.insert(prefix, Vec::new()); - }, - toml::Value::Array(items) => { - if prefix.as_str().is_empty() - || !items.iter().all(|item| matches!(item, toml::Value::Table(_))) - { - return Err(InitStorageDataError::ArraysNotSupported); - } - - let entries = items - .into_iter() - .map(parse_map_entry_value) - .collect::, _>>()?; - map_entries.insert(prefix, entries); - }, - toml_value => { - // Get the string value, or convert to string if it's some other type - let value = match toml_value { - toml::Value::String(s) => s.clone(), - _ => toml_value.to_string(), - }; - value_entries.insert(prefix, value); - }, - } - Ok(()) - } -} - -#[derive(Debug, Error)] -pub enum InitStorageDataError { - #[error("failed to parse TOML")] - InvalidToml(#[from] toml::de::Error), - - #[error("empty table encountered for key `{0}`")] - EmptyTable(String), - - #[error("invalid input: arrays are not supported")] - ArraysNotSupported, - - #[error("invalid storage value name")] - InvalidStorageValueName(#[source] StorageValueNameError), - - #[error("invalid map entry: {0}")] - InvalidMapEntry(String), -} - -impl Serialize for FieldIdentifier { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(2))?; - map.serialize_entry("name", &self.name)?; - map.serialize_entry("description", &self.description)?; - map.end() - } -} - -struct FieldIdentifierVisitor; - -impl<'de> Visitor<'de> for FieldIdentifierVisitor { - type Value = FieldIdentifier; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with 'name' and optionally 'description'") - } - - fn visit_map(self, mut map: M) -> Result - where - M: MapAccess<'de>, - { - let mut name = None; - let mut description = None; - while let Some(key) = map.next_key::()? { - match key.as_str() { - "name" => { - name = Some(map.next_value()?); - }, - "description" => { - let d: String = map.next_value()?; - // Normalize empty or whitespace-only strings into None - description = if d.trim().is_empty() { None } else { Some(d) }; - }, - _ => { - // Ignore other values as FieldIdentifiers are flattened within other structs - let _: de::IgnoredAny = map.next_value()?; - }, - } - } - let name = name.ok_or_else(|| de::Error::missing_field("name"))?; - Ok(FieldIdentifier { name, description }) - } -} - -impl<'de> Deserialize<'de> for FieldIdentifier { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(FieldIdentifierVisitor) - } -} - -// UTILS / HELPERS -// ================================================================================================ - -fn missing_field_for(field: &str, context: &str) -> E { - E::custom(format!("missing '{field}' field for {context}")) -} - -/// Checks than an optional (but expected) name field has been defined and is correct. -fn expect_parse_field_identifier( - n: Option, - description: Option, - context: &str, -) -> Result { - let name = n.ok_or_else(|| missing_field_for("name", context))?; - parse_field_identifier(name, description) -} - -/// Tries to parse a string into a [FieldIdentifier]. -fn parse_field_identifier( - n: String, - description: Option, -) -> Result { - StorageValueName::new(n) - .map_err(|err| E::custom(format!("invalid `name`: {err}"))) - .map(|storage_name| { - if let Some(desc) = description { - FieldIdentifier::with_description(storage_name, desc) - } else { - FieldIdentifier::with_name(storage_name) - } - }) -} - -/// Parses a `{ key, value }` TOML table into a `(Word, Word)` pair, rejecting templates. -fn parse_map_entry_value(item: toml::Value) -> Result<(Word, Word), InitStorageDataError> { - // Try to deserialize the user input as a map entry - let entry: MapEntry = MapEntry::deserialize(item) - .map_err(|err| InitStorageDataError::InvalidMapEntry(err.to_string()))?; - - // Make sure the entry does not contain templates, only static - if entry.key().template_requirements(StorageValueName::empty()).next().is_some() - || entry.value().template_requirements(StorageValueName::empty()).next().is_some() - { - return Err(InitStorageDataError::InvalidMapEntry( - "map entries cannot contain templates".into(), - )); - } - - // Interpret the user input as static words - let key = entry - .key() - .try_build_word(&InitStorageData::default(), StorageValueName::empty()) - .map_err(|err| InitStorageDataError::InvalidMapEntry(err.to_string()))?; - let value = entry - .value() - .try_build_word(&InitStorageData::default(), StorageValueName::empty()) - .map_err(|err| InitStorageDataError::InvalidMapEntry(err.to_string()))?; - - Ok((key, value)) -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use alloc::string::ToString; - use core::error::Error; - - use super::*; - use crate::account::component::toml::InitStorageDataError; - - #[test] - fn from_toml_str_with_nested_table_and_flattened() { - let toml_table = r#" - [token_metadata] - max_supply = "1000000000" - symbol = "ETH" - decimals = "9" - "#; - - let toml_inline = r#" - token_metadata.max_supply = "1000000000" - token_metadata.symbol = "ETH" - token_metadata.decimals = "9" - "#; - - let storage_table = InitStorageData::from_toml(toml_table).unwrap(); - let storage_inline = InitStorageData::from_toml(toml_inline).unwrap(); - - assert_eq!(storage_table.placeholders(), storage_inline.placeholders()); - } - - #[test] - fn from_toml_str_with_deeply_nested_tables() { - let toml_str = r#" - [a] - b = "0xb" - - [a.c] - d = "0xd" - - [x.y.z] - w = 42 # NOTE: This gets parsed as string - "#; - - let storage = InitStorageData::from_toml(toml_str).expect("Failed to parse TOML"); - let key1 = StorageValueName::new("a.b".to_string()).unwrap(); - let key2 = StorageValueName::new("a.c.d".to_string()).unwrap(); - let key3 = StorageValueName::new("x.y.z.w".to_string()).unwrap(); - - assert_eq!(storage.get(&key1).unwrap(), "0xb"); - assert_eq!(storage.get(&key2).unwrap(), "0xd"); - assert_eq!(storage.get(&key3).unwrap(), "42"); - } - - #[test] - fn test_error_on_array() { - let toml_str = r#" - token_metadata.v = [1, 2, 3] - "#; - - let result = InitStorageData::from_toml(toml_str); - assert_matches::assert_matches!( - result.unwrap_err(), - InitStorageDataError::ArraysNotSupported - ); - } - - #[test] - fn parse_map_entries_from_array() { - let toml_str = r#" - my_map = [ - { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000010" }, - { key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = ["1", "2", "3", "4"] } - ] - "#; - - let storage = InitStorageData::from_toml(toml_str).expect("Failed to parse map entries"); - let map_name = StorageValueName::new("my_map").unwrap(); - let entries = storage.map_entries(&map_name).expect("map entries missing"); - assert_eq!(entries.len(), 2); - - let first_key = - Word::try_from("0x0000000000000000000000000000000000000000000000000000000000000001") - .unwrap(); - assert_eq!(entries[0].0, first_key); - - let second_value = - Word::from([Felt::new(1u64), Felt::new(2u64), Felt::new(3u64), Felt::new(4u64)]); - assert_eq!(entries[1].1, second_value); - } - - #[test] - fn error_on_empty_subtable() { - let toml_str = r#" - [a] - b = {} - "#; - - let result = InitStorageData::from_toml(toml_str); - assert_matches::assert_matches!(result.unwrap_err(), InitStorageDataError::EmptyTable(_)); - } - - #[test] - fn error_on_duplicate_keys() { - let toml_str = r#" - token_metadata.max_supply = "1000000000" - token_metadata.max_supply = "500000000" - "#; - - let result = InitStorageData::from_toml(toml_str).unwrap_err(); - // TOML does not support duplicate keys - assert_matches::assert_matches!(result, InitStorageDataError::InvalidToml(_)); - assert!(result.source().unwrap().to_string().contains("duplicate")); - } -} diff --git a/crates/miden-objects/src/account/delta/storage.rs b/crates/miden-objects/src/account/delta/storage.rs deleted file mode 100644 index baeda54931..0000000000 --- a/crates/miden-objects/src/account/delta/storage.rs +++ /dev/null @@ -1,603 +0,0 @@ -use alloc::collections::BTreeMap; -use alloc::collections::btree_map::Entry; -use alloc::string::ToString; -use alloc::vec::Vec; - -use super::{ - AccountDeltaError, - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Serializable, - Word, -}; -use crate::account::{StorageMap, StorageSlotType}; -use crate::{EMPTY_WORD, Felt, LexicographicWord, ZERO}; - -// ACCOUNT STORAGE DELTA -// ================================================================================================ - -/// [AccountStorageDelta] stores the differences between two states of account storage. -/// -/// The delta consists of two maps: -/// - A map containing the updates to value storage slots. The keys in this map are indexes of the -/// updated storage slots and the values are the new values for these slots. -/// - A map containing updates to storage maps. The keys in this map are indexes of the updated -/// storage slots and the values are corresponding storage map delta objects. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct AccountStorageDelta { - /// The updates to the value slots of the account. - values: BTreeMap, - /// The updates to the map slots of the account. - maps: BTreeMap, -} - -impl AccountStorageDelta { - /// Creates a new, empty storage delta. - pub fn new() -> Self { - Self { - values: BTreeMap::new(), - maps: BTreeMap::new(), - } - } - - /// Creates a new storage delta from the provided fields. - /// - /// # Errors - /// - /// Returns an error if: - /// - Any of the updated slot is referenced from both maps, which means a slot is treated as - /// both a value and a map slot. - pub fn from_parts( - values: BTreeMap, - maps: BTreeMap, - ) -> Result { - let delta = Self { values, maps }; - delta.validate()?; - - Ok(delta) - } - - /// Returns the slot type of the provided slot index or `None` if no such slot exists. - pub(crate) fn slot_type(&self, slot_index: u8) -> Option { - if self.values().contains_key(&slot_index) { - Some(StorageSlotType::Value) - } else if self.maps().contains_key(&slot_index) { - Some(StorageSlotType::Map) - } else { - None - } - } - - /// Returns a reference to the updated values in this storage delta. - pub fn values(&self) -> &BTreeMap { - &self.values - } - - /// Returns a reference to the updated maps in this storage delta. - pub fn maps(&self) -> &BTreeMap { - &self.maps - } - - /// Returns true if storage delta contains no updates. - pub fn is_empty(&self) -> bool { - self.values.is_empty() && self.maps.is_empty() - } - - /// Tracks a slot change - pub fn set_item(&mut self, slot_index: u8, new_slot_value: Word) { - self.values.insert(slot_index, new_slot_value); - } - - /// Tracks a map item change - pub fn set_map_item(&mut self, slot_index: u8, key: Word, new_value: Word) { - self.maps.entry(slot_index).or_default().insert(key, new_value); - } - - /// Inserts an empty storage map delta for the provided slot index. - /// - /// This is useful for full state deltas to represent an empty map in the delta. - pub fn insert_empty_map_delta(&mut self, slot_index: u8) { - self.maps.entry(slot_index).or_default(); - } - - /// Merges another delta into this one, overwriting any existing values. - pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> { - self.values.extend(other.values); - - // merge maps - for (slot, update) in other.maps.into_iter() { - match self.maps.entry(slot) { - Entry::Vacant(entry) => { - entry.insert(update); - }, - Entry::Occupied(mut entry) => entry.get_mut().merge(update), - } - } - - self.validate() - } - - /// Checks whether this storage delta is valid. - /// - /// # Errors - /// - /// Returns an error if: - /// - Any of the updated slot is referenced from both maps, which means a slot is treated as - /// both a value and a map slot. - fn validate(&self) -> Result<(), AccountDeltaError> { - for slot in self.maps.keys() { - if self.values.contains_key(slot) { - return Err(AccountDeltaError::StorageSlotUsedAsDifferentTypes(*slot)); - } - } - - Ok(()) - } - - /// Returns an iterator of all the cleared storage slots. - fn cleared_slots(&self) -> impl Iterator + '_ { - self.values.iter().filter(|&(_, value)| value.is_empty()).map(|(slot, _)| *slot) - } - - /// Returns an iterator of all the updated storage slots. - fn updated_slots(&self) -> impl Iterator + '_ { - self.values.iter().filter(|&(_, value)| !value.is_empty()) - } - - /// Appends the storage slots delta to the given `elements` from which the delta commitment will - /// be computed. - pub(super) fn append_delta_elements(&self, elements: &mut Vec) { - const DOMAIN_VALUE: Felt = Felt::new(2); - const DOMAIN_MAP: Felt = Felt::new(3); - - let highest_value_slot_idx = self.values.last_key_value().map(|(slot_idx, _)| slot_idx); - let highest_map_slot_idx = self.maps.last_key_value().map(|(slot_idx, _)| slot_idx); - let highest_slot_idx = - highest_value_slot_idx.max(highest_map_slot_idx).copied().unwrap_or(0); - - for slot_idx in 0..=highest_slot_idx { - let slot_idx_felt = Felt::from(slot_idx); - - // The storage delta ensures that the value slots and map slots do not have overlapping - // slot indices, so at most one of them will return `Some` for a given slot index. - match self.values.get(&slot_idx) { - Some(new_value) => { - elements.extend_from_slice(&[DOMAIN_VALUE, slot_idx_felt, ZERO, ZERO]); - elements.extend_from_slice(new_value.as_elements()); - }, - None => { - if let Some(map_delta) = self.maps().get(&slot_idx) { - for (key, value) in map_delta.entries() { - elements.extend_from_slice(key.inner().as_elements()); - elements.extend_from_slice(value.as_elements()); - } - - let num_changed_entries = Felt::try_from(map_delta.num_entries()).expect( - "number of changed entries should not exceed max representable felt", - ); - - elements.extend_from_slice(&[ - DOMAIN_MAP, - slot_idx_felt, - num_changed_entries, - ZERO, - ]); - elements.extend_from_slice(EMPTY_WORD.as_elements()); - } - }, - } - } - } - - /// Consumes self and returns the underlying parts of the storage delta. - pub fn into_parts(self) -> (BTreeMap, BTreeMap) { - (self.values, self.maps) - } -} - -impl Default for AccountStorageDelta { - fn default() -> Self { - Self::new() - } -} - -#[cfg(any(feature = "testing", test))] -impl AccountStorageDelta { - /// Creates an [AccountStorageDelta] from the given iterators. - pub fn from_iters( - cleared_values: impl IntoIterator, - updated_values: impl IntoIterator, - updated_maps: impl IntoIterator, - ) -> Self { - Self { - values: BTreeMap::from_iter( - cleared_values.into_iter().map(|key| (key, EMPTY_WORD)).chain(updated_values), - ), - maps: BTreeMap::from_iter(updated_maps), - } - } -} - -impl Serializable for AccountStorageDelta { - fn write_into(&self, target: &mut W) { - let cleared: Vec = self.cleared_slots().collect(); - let updated: Vec<(&u8, &Word)> = self.updated_slots().collect(); - - target.write_u8(cleared.len() as u8); - target.write_many(cleared.iter()); - - target.write_u8(updated.len() as u8); - target.write_many(updated.iter()); - - target.write_u8(self.maps.len() as u8); - target.write_many(self.maps.iter()); - } - - fn get_size_hint(&self) -> usize { - let u8_size = 0u8.get_size_hint(); - let word_size = EMPTY_WORD.get_size_hint(); - - let mut storage_map_delta_size = 0; - for (slot, storage_map_delta) in self.maps.iter() { - // The serialized size of each entry is the combination of slot (key) and the delta - // (value). - storage_map_delta_size += slot.get_size_hint() + storage_map_delta.get_size_hint(); - } - - // Length Prefixes - u8_size * 3 + - // Cleared Slots - self.cleared_slots().count() * u8_size + - // Updated Slots - self.updated_slots().count() * (u8_size + word_size) + - // Storage Map Delta - storage_map_delta_size - } -} - -impl Deserializable for AccountStorageDelta { - fn read_from(source: &mut R) -> Result { - let mut values = BTreeMap::new(); - - let num_cleared_items = source.read_u8()? as usize; - for _ in 0..num_cleared_items { - let cleared_slot = source.read_u8()?; - values.insert(cleared_slot, EMPTY_WORD); - } - - let num_updated_items = source.read_u8()? as usize; - for _ in 0..num_updated_items { - let (updated_slot, updated_value) = source.read()?; - values.insert(updated_slot, updated_value); - } - - let num_maps = source.read_u8()? as usize; - let maps = source.read_many::<(u8, StorageMapDelta)>(num_maps)?.into_iter().collect(); - - Self::from_parts(values, maps) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} - -// STORAGE MAP DELTA -// ================================================================================================ - -/// [StorageMapDelta] stores the differences between two states of account storage maps. -/// -/// The differences are represented as leaf updates: a map of updated item key ([Word]) to -/// value ([Word]). For cleared items the value is [EMPTY_WORD]. -/// -/// The [`LexicographicWord`] wrapper is necessary to order the keys in the same way as the -/// in-kernel account delta which uses a link map. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct StorageMapDelta(BTreeMap); - -impl StorageMapDelta { - /// Creates a new storage map delta from the provided leaves. - pub fn new(map: BTreeMap) -> Self { - Self(map) - } - - /// Returns the number of changed entries in this map delta. - pub fn num_entries(&self) -> usize { - self.0.len() - } - - /// Returns a reference to the updated entries in this storage map delta. - /// - /// Note that the returned key is the raw map key. - pub fn entries(&self) -> &BTreeMap { - &self.0 - } - - /// Inserts an item into the storage map delta. - pub fn insert(&mut self, raw_key: Word, value: Word) { - self.0.insert(LexicographicWord::new(raw_key), value); - } - - /// Returns true if storage map delta contains no updates. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Merge `other` into this delta, giving precedence to `other`. - pub fn merge(&mut self, other: Self) { - // Aggregate the changes into a map such that `other` overwrites self. - self.0.extend(other.0); - } - - /// Returns a mutable reference to the underlying map. - pub fn as_map_mut(&mut self) -> &mut BTreeMap { - &mut self.0 - } - - /// Returns an iterator of all the cleared keys in the storage map. - fn cleared_keys(&self) -> impl Iterator + '_ { - self.0.iter().filter(|&(_, value)| value.is_empty()).map(|(key, _)| key.inner()) - } - - /// Returns an iterator of all the updated entries in the storage map. - fn updated_entries(&self) -> impl Iterator + '_ { - self.0.iter().filter_map(|(key, value)| { - if !value.is_empty() { - Some((key.inner(), value)) - } else { - None - } - }) - } -} - -#[cfg(any(feature = "testing", test))] -impl StorageMapDelta { - /// Creates a new [StorageMapDelta] from the provided iterators. - pub fn from_iters( - cleared_leaves: impl IntoIterator, - updated_leaves: impl IntoIterator, - ) -> Self { - Self(BTreeMap::from_iter( - cleared_leaves - .into_iter() - .map(|key| (LexicographicWord::new(key), EMPTY_WORD)) - .chain( - updated_leaves - .into_iter() - .map(|(key, value)| (LexicographicWord::new(key), value)), - ), - )) - } - - /// Consumes self and returns the underlying map. - pub fn into_map(self) -> BTreeMap { - self.0 - } -} - -/// Converts a [StorageMap] into a [StorageMapDelta] for initial delta construction. -impl From for StorageMapDelta { - fn from(map: StorageMap) -> Self { - StorageMapDelta::new( - map.into_entries() - .into_iter() - .map(|(key, value)| (LexicographicWord::new(key), value)) - .collect(), - ) - } -} - -impl Serializable for StorageMapDelta { - fn write_into(&self, target: &mut W) { - let cleared: Vec<&Word> = self.cleared_keys().collect(); - let updated: Vec<(&Word, &Word)> = self.updated_entries().collect(); - - target.write_usize(cleared.len()); - target.write_many(cleared.iter()); - - target.write_usize(updated.len()); - target.write_many(updated.iter()); - } - - fn get_size_hint(&self) -> usize { - let word_size = EMPTY_WORD.get_size_hint(); - - let cleared_keys_count = self.cleared_keys().count(); - let updated_entries_count = self.updated_entries().count(); - - // Cleared Keys - cleared_keys_count.get_size_hint() + - cleared_keys_count * Word::SERIALIZED_SIZE + - - // Updated Entries - updated_entries_count.get_size_hint() + - updated_entries_count * (Word::SERIALIZED_SIZE + word_size) - } -} - -impl Deserializable for StorageMapDelta { - fn read_from(source: &mut R) -> Result { - let mut map = BTreeMap::new(); - - let cleared_count = source.read_usize()?; - for _ in 0..cleared_count { - let cleared_key = source.read()?; - map.insert(LexicographicWord::new(cleared_key), EMPTY_WORD); - } - - let updated_count = source.read_usize()?; - for _ in 0..updated_count { - let (updated_key, updated_value) = source.read()?; - map.insert(LexicographicWord::new(updated_key), updated_value); - } - - Ok(Self::new(map)) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use anyhow::Context; - - use super::{AccountStorageDelta, Deserializable, Serializable}; - use crate::account::StorageMapDelta; - use crate::testing::storage::AccountStorageDeltaBuilder; - use crate::{ONE, Word, ZERO}; - - #[test] - fn account_storage_delta_validation() { - let delta = AccountStorageDelta::from_iters( - [1, 2, 3], - [(4, Word::from([ONE, ONE, ONE, ONE])), (5, Word::from([ONE, ONE, ONE, ZERO]))], - [], - ); - assert!(delta.validate().is_ok()); - - let bytes = delta.to_bytes(); - assert_eq!(AccountStorageDelta::read_from_bytes(&bytes), Ok(delta)); - - // duplicate across cleared items and maps - let delta = AccountStorageDelta::from_iters( - [1, 2, 3], - [(2, Word::from([ONE, ONE, ONE, ONE])), (5, Word::from([ONE, ONE, ONE, ZERO]))], - [(1, StorageMapDelta::default())], - ); - assert!(delta.validate().is_err()); - - let bytes = delta.to_bytes(); - assert!(AccountStorageDelta::read_from_bytes(&bytes).is_err()); - - // duplicate across updated items and maps - let delta = AccountStorageDelta::from_iters( - [1, 3], - [(2, Word::from([ONE, ONE, ONE, ONE])), (5, Word::from([ONE, ONE, ONE, ZERO]))], - [(2, StorageMapDelta::default())], - ); - assert!(delta.validate().is_err()); - - let bytes = delta.to_bytes(); - assert!(AccountStorageDelta::read_from_bytes(&bytes).is_err()); - } - - #[test] - fn test_is_empty() { - let storage_delta = AccountStorageDelta::new(); - assert!(storage_delta.is_empty()); - - let storage_delta = AccountStorageDelta::from_iters([1], [], []); - assert!(!storage_delta.is_empty()); - - let storage_delta = - AccountStorageDelta::from_iters([], [(2, Word::from([ONE, ONE, ONE, ONE]))], []); - assert!(!storage_delta.is_empty()); - - let storage_delta = - AccountStorageDelta::from_iters([], [], [(3, StorageMapDelta::default())]); - assert!(!storage_delta.is_empty()); - } - - #[test] - fn test_serde_account_storage_delta() { - let storage_delta = AccountStorageDelta::new(); - let serialized = storage_delta.to_bytes(); - let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_delta); - - let storage_delta = AccountStorageDelta::from_iters([1], [], []); - let serialized = storage_delta.to_bytes(); - let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_delta); - - let storage_delta = - AccountStorageDelta::from_iters([], [(2, Word::from([ONE, ONE, ONE, ONE]))], []); - let serialized = storage_delta.to_bytes(); - let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_delta); - - let storage_delta = - AccountStorageDelta::from_iters([], [], [(3, StorageMapDelta::default())]); - let serialized = storage_delta.to_bytes(); - let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_delta); - } - - #[test] - fn test_serde_storage_map_delta() { - let storage_map_delta = StorageMapDelta::default(); - let serialized = storage_map_delta.to_bytes(); - let deserialized = StorageMapDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_map_delta); - - let storage_map_delta = StorageMapDelta::from_iters([Word::from([ONE, ONE, ONE, ONE])], []); - let serialized = storage_map_delta.to_bytes(); - let deserialized = StorageMapDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_map_delta); - - let storage_map_delta = - StorageMapDelta::from_iters([], [(Word::empty(), Word::from([ONE, ONE, ONE, ONE]))]); - let serialized = storage_map_delta.to_bytes(); - let deserialized = StorageMapDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, storage_map_delta); - } - - #[rstest::rstest] - #[case::some_some(Some(1), Some(2), Some(2))] - #[case::none_some(None, Some(2), Some(2))] - #[case::some_none(Some(1), None, None)] - #[test] - fn merge_items( - #[case] x: Option, - #[case] y: Option, - #[case] expected: Option, - ) -> anyhow::Result<()> { - /// Creates a delta containing the item as an update if Some, else with the item cleared. - fn create_delta(item: Option) -> anyhow::Result { - const SLOT: u8 = 123; - let item = item.map(|x| (SLOT, Word::from([x, 0, 0, 0]))); - - AccountStorageDeltaBuilder::new() - .add_cleared_items(item.is_none().then_some(SLOT)) - .add_updated_values(item) - .build() - .context("failed to build storage delta") - } - - let mut delta_x = create_delta(x)?; - let delta_y = create_delta(y)?; - let expected = create_delta(expected)?; - - delta_x.merge(delta_y).context("failed to merge deltas")?; - - assert_eq!(delta_x, expected); - - Ok(()) - } - - #[rstest::rstest] - #[case::some_some(Some(1), Some(2), Some(2))] - #[case::none_some(None, Some(2), Some(2))] - #[case::some_none(Some(1), None, None)] - #[test] - fn merge_maps(#[case] x: Option, #[case] y: Option, #[case] expected: Option) { - fn create_delta(value: Option) -> StorageMapDelta { - let key = Word::from([10u32, 0, 0, 0]); - match value { - Some(value) => { - StorageMapDelta::from_iters([], [(key, Word::from([value, 0, 0, 0]))]) - }, - None => StorageMapDelta::from_iters([key], []), - } - } - - let mut delta_x = create_delta(x); - let delta_y = create_delta(y); - let expected = create_delta(expected); - - delta_x.merge(delta_y); - - assert_eq!(delta_x, expected); - } -} diff --git a/crates/miden-objects/src/account/storage/header.rs b/crates/miden-objects/src/account/storage/header.rs deleted file mode 100644 index 5690e180f4..0000000000 --- a/crates/miden-objects/src/account/storage/header.rs +++ /dev/null @@ -1,209 +0,0 @@ -use alloc::vec::Vec; - -use super::{AccountStorage, Felt, Hasher, StorageSlot, StorageSlotType, Word}; -use crate::utils::serde::{ - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Serializable, -}; -use crate::{AccountError, ZERO}; - -// ACCOUNT STORAGE HEADER -// ================================================================================================ - -/// Storage slot header is a lighter version of the [StorageSlot] storing only the type and the -/// top-level value for the slot, and being, in fact, just a thin wrapper around a tuple. -/// -/// That is, for complex storage slot (e.g., storage map), the header contains only the commitment -/// to the underlying data. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct StorageSlotHeader(StorageSlotType, Word); - -impl StorageSlotHeader { - /// Returns a new instance of storage slot header from the provided storage slot type and value. - pub fn new(value: &(StorageSlotType, Word)) -> Self { - Self(value.0, value.1) - } - - /// Returns this storage slot header as field elements. - /// - /// This is done by converting this storage slot into 8 field elements as follows: - /// ```text - /// [SLOT_VALUE, slot_type, 0, 0, 0] - /// ``` - pub fn as_elements(&self) -> [Felt; StorageSlot::NUM_ELEMENTS_PER_STORAGE_SLOT] { - let mut elements = [ZERO; StorageSlot::NUM_ELEMENTS_PER_STORAGE_SLOT]; - elements[0..4].copy_from_slice(self.1.as_elements()); - elements[4..8].copy_from_slice(self.0.as_word().as_elements()); - elements - } -} - -impl From<&StorageSlot> for StorageSlotHeader { - fn from(value: &StorageSlot) -> Self { - Self(value.slot_type(), value.value()) - } -} - -/// Account storage header is a lighter version of the [AccountStorage] storing only the type and -/// the top-level value for each storage slot. -/// -/// That is, for complex storage slots (e.g., storage maps), the header contains only the commitment -/// to the underlying data. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccountStorageHeader { - slots: Vec<(StorageSlotType, Word)>, -} - -impl AccountStorageHeader { - // CONSTRUCTOR - // -------------------------------------------------------------------------------------------- - - /// Returns a new instance of account storage header initialized with the provided slots. - /// - /// # Panics - /// - If the number of provided slots is greater than [AccountStorage::MAX_NUM_STORAGE_SLOTS]. - pub fn new(slots: Vec<(StorageSlotType, Word)>) -> Self { - assert!(slots.len() <= AccountStorage::MAX_NUM_STORAGE_SLOTS); - Self { slots } - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns an iterator over the storage header slots. - pub fn slots(&self) -> impl Iterator { - self.slots.iter() - } - - /// Returns an iterator over the storage header map slots. - pub fn map_slot_roots(&self) -> impl Iterator { - self.slots - .iter() - .filter(|(slot_type, _)| matches!(slot_type, StorageSlotType::Map)) - .map(|x| x.1) - } - - /// Returns the number of slots contained in the storage header. - pub fn num_slots(&self) -> u8 { - // SAFETY: The constructors of this type ensure this value fits in a u8. - self.slots.len() as u8 - } - - /// Returns a slot contained in the storage header at a given index. - /// - /// # Errors - /// - If the index is out of bounds. - pub fn slot(&self, index: usize) -> Result<&(StorageSlotType, Word), AccountError> { - self.slots.get(index).ok_or(AccountError::StorageIndexOutOfBounds { - slots_len: self.slots.len() as u8, - index: index as u8, - }) - } - - // NOTE: The way of computing the commitment should be kept in sync with `AccountStorage` - /// Computes the account storage header commitment. - pub fn compute_commitment(&self) -> Word { - Hasher::hash_elements(&self.as_elements()) - } - - /// Indicates whether the slot at `index` is a map slot. - /// - /// # Errors - /// - If `index` exceeds the slot count. - pub fn is_map_slot(&self, index: usize) -> Result { - match self.slot(index)?.0 { - StorageSlotType::Map => Ok(true), - StorageSlotType::Value => Ok(false), - } - } - - /// Converts storage slots of this account storage header into a vector of field elements. - /// - /// This is done by first converting each storage slot into exactly 8 elements as follows: - /// ```text - /// [STORAGE_SLOT_VALUE, storage_slot_type, 0, 0, 0] - /// ``` - /// And then concatenating the resulting elements into a single vector. - pub fn as_elements(&self) -> Vec { - self.slots - .iter() - .flat_map(|slot| StorageSlotHeader::new(slot).as_elements()) - .collect() - } -} - -impl From<&AccountStorage> for AccountStorageHeader { - fn from(value: &AccountStorage) -> Self { - value.to_header() - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for AccountStorageHeader { - fn write_into(&self, target: &mut W) { - let len = self.slots.len() as u8; - target.write_u8(len); - target.write_many(self.slots()) - } -} - -impl Deserializable for AccountStorageHeader { - fn read_from(source: &mut R) -> Result { - let len = source.read_u8()?; - let slots = source.read_many(len as usize)?; - // number of storage slots is guaranteed to be smaller than or equal to 255 - Ok(Self::new(slots)) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use miden_core::Felt; - use miden_core::utils::{Deserializable, Serializable}; - - use super::AccountStorageHeader; - use crate::Word; - use crate::account::{AccountStorage, StorageSlotType}; - - #[test] - fn test_from_account_storage() { - let storage_map = AccountStorage::mock_map(); - - // create new storage header from AccountStorage - let slots = vec![ - (StorageSlotType::Value, Word::from([1, 2, 3, 4u32])), - ( - StorageSlotType::Value, - Word::from([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]), - ), - (StorageSlotType::Map, storage_map.root()), - ]; - - let expected_header = AccountStorageHeader { slots }; - let account_storage = AccountStorage::mock(); - - assert_eq!(expected_header, AccountStorageHeader::from(&account_storage)) - } - - #[test] - fn test_serde_account_storage_header() { - // create new storage header - let storage = AccountStorage::mock(); - let storage_header = AccountStorageHeader::from(&storage); - - // serde storage header - let bytes = storage_header.to_bytes(); - let deserialized = AccountStorageHeader::read_from_bytes(&bytes).unwrap(); - - // assert deserialized == storage header - assert_eq!(storage_header, deserialized); - } -} diff --git a/crates/miden-objects/src/account/storage/mod.rs b/crates/miden-objects/src/account/storage/mod.rs deleted file mode 100644 index 168f12ee85..0000000000 --- a/crates/miden-objects/src/account/storage/mod.rs +++ /dev/null @@ -1,372 +0,0 @@ -use alloc::string::ToString; -use alloc::vec::Vec; - -use super::{ - AccountError, - AccountStorageDelta, - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Felt, - Hasher, - Serializable, - Word, -}; -use crate::account::{AccountComponent, AccountType}; - -mod slot; -pub use slot::{SlotName, StorageSlot, StorageSlotType}; - -mod map; -pub use map::{PartialStorageMap, StorageMap, StorageMapWitness}; - -mod header; -pub use header::{AccountStorageHeader, StorageSlotHeader}; - -mod partial; -pub use partial::PartialStorage; - -// ACCOUNT STORAGE -// ================================================================================================ - -/// Account storage is composed of a variable number of index-addressable [StorageSlot]s up to -/// 255 slots in total. -/// -/// Each slot has a type which defines its size and structure. Currently, the following types are -/// supported: -/// - [StorageSlot::Value]: contains a single [Word] of data (i.e., 32 bytes). -/// - [StorageSlot::Map]: contains a [StorageMap] which is a key-value map where both keys and -/// values are [Word]s. The value of a storage slot containing a map is the commitment to the -/// underlying map. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct AccountStorage { - slots: Vec, -} - -impl AccountStorage { - /// The maximum number of storage slots allowed in an account storage. - pub const MAX_NUM_STORAGE_SLOTS: usize = 255; - - // CONSTRUCTOR - // -------------------------------------------------------------------------------------------- - - /// Returns a new instance of account storage initialized with the provided items. - /// - /// # Errors - /// - /// Returns an error if: - /// - The number of [`StorageSlot`]s exceeds 255. - pub fn new(slots: Vec) -> Result { - let num_slots = slots.len(); - - if num_slots > Self::MAX_NUM_STORAGE_SLOTS { - return Err(AccountError::StorageTooManySlots(num_slots as u64)); - } - - Ok(Self { slots }) - } - - /// Creates an [`AccountStorage`] from the provided components' storage slots. - /// - /// If the account type is faucet the reserved slot (slot 0) will be initialized. - /// - For Fungible Faucets the value is [`StorageSlot::empty_value`]. - /// - For Non-Fungible Faucets the value is [`StorageSlot::empty_map`]. - /// - /// If the storage needs to be initialized with certain values in that slot, those can be added - /// after construction with the standard set methods for items and maps. - /// - /// # Errors - /// - /// Returns an error if: - /// - The number of [`StorageSlot`]s of all components exceeds 255. - pub(super) fn from_components( - components: &[AccountComponent], - account_type: AccountType, - ) -> Result { - let mut storage_slots = match account_type { - AccountType::FungibleFaucet => vec![StorageSlot::empty_value()], - AccountType::NonFungibleFaucet => vec![StorageSlot::empty_map()], - _ => vec![], - }; - - storage_slots - .extend(components.iter().flat_map(|component| component.storage_slots()).cloned()); - - Self::new(storage_slots) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns a commitment to this storage. - pub fn commitment(&self) -> Word { - build_slots_commitment(&self.slots) - } - - /// Returns the number of slots in the account's storage. - pub fn num_slots(&self) -> u8 { - // SAFETY: The constructors of account storage ensure that the number of slots fits into a - // u8. - self.slots.len() as u8 - } - - /// Returns a reference to the storage slots. - pub fn slots(&self) -> &[StorageSlot] { - &self.slots - } - - /// Consumes self and returns the storage slots of the account storage. - pub fn into_slots(self) -> Vec { - self.slots - } - - /// Returns an [AccountStorageHeader] for this account storage. - pub fn to_header(&self) -> AccountStorageHeader { - AccountStorageHeader::new( - self.slots.iter().map(|slot| (slot.slot_type(), slot.value())).collect(), - ) - } - - /// Returns an item from the storage at the specified index. - /// - /// # Errors: - /// - If the index is out of bounds - pub fn get_item(&self, index: u8) -> Result { - self.slots - .get(index as usize) - .ok_or(AccountError::StorageIndexOutOfBounds { - slots_len: self.slots.len() as u8, - index, - }) - .map(|slot| slot.value()) - } - - /// Returns a map item from a map located in storage at the specified index. - /// - /// # Errors: - /// - If the index is out of bounds - /// - If the [StorageSlot] is not [StorageSlotType::Map] - pub fn get_map_item(&self, index: u8, key: Word) -> Result { - match self.slots.get(index as usize).ok_or(AccountError::StorageIndexOutOfBounds { - slots_len: self.slots.len() as u8, - index, - })? { - StorageSlot::Map(map) => Ok(map.get(&key)), - _ => Err(AccountError::StorageSlotNotMap(index)), - } - } - - /// Converts storage slots of this account storage into a vector of field elements. - /// - /// This is done by first converting each storage slot into exactly 8 elements as follows: - /// ```text - /// [STORAGE_SLOT_VALUE, storage_slot_type, 0, 0, 0] - /// ``` - /// And then concatenating the resulting elements into a single vector. - pub fn as_elements(&self) -> Vec { - slots_as_elements(self.slots()) - } - - // STATE MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Applies the provided delta to this account storage. - /// - /// # Errors: - /// - If the updates violate storage constraints. - pub(super) fn apply_delta(&mut self, delta: &AccountStorageDelta) -> Result<(), AccountError> { - let len = self.slots.len() as u8; - - // update storage maps - for (&idx, map) in delta.maps().iter() { - let storage_slot = self - .slots - .get_mut(idx as usize) - .ok_or(AccountError::StorageIndexOutOfBounds { slots_len: len, index: idx })?; - - let storage_map = match storage_slot { - StorageSlot::Map(map) => map, - _ => return Err(AccountError::StorageSlotNotMap(idx)), - }; - - storage_map.apply_delta(map)?; - } - - // update storage values - for (&idx, &value) in delta.values().iter() { - self.set_item(idx, value)?; - } - - Ok(()) - } - - /// Updates the value of the storage slot at the specified index. - /// - /// This method should be used only to update value slots. For updating values - /// in storage maps, please see [AccountStorage::set_map_item()]. - /// - /// # Errors: - /// - If the index is out of bounds - /// - If the [StorageSlot] is not [StorageSlotType::Value] - pub fn set_item(&mut self, index: u8, value: Word) -> Result { - // check if index is in bounds - let num_slots = self.slots.len(); - - if index as usize >= num_slots { - return Err(AccountError::StorageIndexOutOfBounds { - slots_len: self.slots.len() as u8, - index, - }); - } - - let old_value = match self.slots[index as usize] { - StorageSlot::Value(value) => value, - // return an error if the type != Value - _ => return Err(AccountError::StorageSlotNotValue(index)), - }; - - // update the value of the storage slot - self.slots[index as usize] = StorageSlot::Value(value); - - Ok(old_value) - } - - /// Updates the value of a key-value pair of a storage map at the specified index. - /// - /// This method should be used only to update storage maps. For updating values - /// in storage slots, please see [AccountStorage::set_item()]. - /// - /// # Errors: - /// - If the index is out of bounds - /// - If the [StorageSlot] is not [StorageSlotType::Map] - pub fn set_map_item( - &mut self, - index: u8, - key: Word, - value: Word, - ) -> Result<(Word, Word), AccountError> { - // check if index is in bounds - let num_slots = self.slots.len(); - - if index as usize >= num_slots { - return Err(AccountError::StorageIndexOutOfBounds { - slots_len: self.slots.len() as u8, - index, - }); - } - - let storage_map = match self.slots[index as usize] { - StorageSlot::Map(ref mut map) => map, - _ => return Err(AccountError::StorageSlotNotMap(index)), - }; - - // get old map root to return - let old_root = storage_map.root(); - - // update the key-value pair in the map - let old_value = storage_map.insert(key, value)?; - - Ok((old_root, old_value)) - } -} - -// ITERATORS -// ================================================================================================ - -impl IntoIterator for AccountStorage { - type Item = StorageSlot; - type IntoIter = alloc::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.slots.into_iter() - } -} - -// HELPER FUNCTIONS -// ------------------------------------------------------------------------------------------------ - -/// Converts given slots into field elements -fn slots_as_elements(slots: &[StorageSlot]) -> Vec { - slots - .iter() - .flat_map(|slot| StorageSlotHeader::from(slot).as_elements()) - .collect() -} - -/// Computes the commitment to the given slots -pub fn build_slots_commitment(slots: &[StorageSlot]) -> Word { - let elements = slots_as_elements(slots); - Hasher::hash_elements(&elements) -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for AccountStorage { - fn write_into(&self, target: &mut W) { - target.write_u8(self.slots().len() as u8); - target.write_many(self.slots()); - } - - fn get_size_hint(&self) -> usize { - // Size of the serialized slot length. - let u8_size = 0u8.get_size_hint(); - let mut size = u8_size; - - for slot in self.slots() { - size += slot.get_size_hint(); - } - - size - } -} - -impl Deserializable for AccountStorage { - fn read_from(source: &mut R) -> Result { - let num_slots = source.read_u8()? as usize; - let slots = source.read_many::(num_slots)?; - - Self::new(slots).map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use super::{ - AccountStorage, - Deserializable, - Serializable, - StorageMap, - Word, - build_slots_commitment, - }; - use crate::account::StorageSlot; - - #[test] - fn test_serde_account_storage() { - // empty storage - let storage = AccountStorage::new(vec![]).unwrap(); - let bytes = storage.to_bytes(); - assert_eq!(storage, AccountStorage::read_from_bytes(&bytes).unwrap()); - - // storage with values for default types - let storage = AccountStorage::new(vec![ - StorageSlot::Value(Word::empty()), - StorageSlot::Map(StorageMap::default()), - ]) - .unwrap(); - let bytes = storage.to_bytes(); - assert_eq!(storage, AccountStorage::read_from_bytes(&bytes).unwrap()); - } - - #[test] - fn test_account_storage_slots_commitment() { - let storage = AccountStorage::mock(); - let storage_slots_commitment = build_slots_commitment(storage.slots()); - assert_eq!(storage_slots_commitment, storage.commitment()) - } -} diff --git a/crates/miden-objects/src/account/storage/slot/slot_name.rs b/crates/miden-objects/src/account/storage/slot/slot_name.rs deleted file mode 100644 index 0efbb96a97..0000000000 --- a/crates/miden-objects/src/account/storage/slot/slot_name.rs +++ /dev/null @@ -1,314 +0,0 @@ -use alloc::borrow::Cow; -use alloc::string::String; - -use crate::errors::SlotNameError; - -/// The name of an account storage slot. -/// -/// A typical slot name looks like this: -/// -/// ```text -/// miden::basic_fungible_faucet::metadata -/// ``` -/// -/// The double-colon (`::`) serves as a separator and the strings in between the separators are -/// called components. -/// -/// It is generally recommended that slot names have at least three components and follow this -/// structure: -/// -/// ```text -/// organization::component::slot_name -/// ``` -/// -/// ## Requirements -/// -/// For a string to be a valid slot name it needs to satisfy the following criteria: -/// - It needs to have at least 2 components. -/// - Each component must consist of at least one character. -/// - Each component must only consist of the characters `a` to `z`, `A` to `Z`, `0` to `9` or `_` -/// (underscore). -/// - Each component must not start with an underscore. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SlotName { - name: Cow<'static, str>, -} - -impl SlotName { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - // The minimum number of components that a slot name must contain. - pub(crate) const MIN_NUM_COMPONENTS: usize = 2; - - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Constructs a new [`SlotName`] from a static string. - /// - /// This function is `const` and can be used to define slot names as constants, e.g.: - /// - /// ```rust - /// # use miden_objects::account::SlotName; - /// const SLOT_NAME: SlotName = SlotName::from_static_str("miden::basic_fungible_faucet::metadata"); - /// ``` - /// - /// This is convenient because using a string that is not a valid slot name fails to compile. - /// - /// # Panics - /// - /// Panics if: - /// - the slot name is invalid (see the type-level docs for the requirements). - pub const fn from_static_str(name: &'static str) -> Self { - match Self::validate(name) { - Ok(()) => Self { name: Cow::Borrowed(name) }, - // We cannot format the error in a const context. - Err(_) => panic!("invalid slot name"), - } - } - - /// Constructs a new [`SlotName`] from a string. - /// - /// # Errors - /// - /// Returns an error if: - /// - the slot name is invalid (see the type-level docs for the requirements). - pub fn new(name: impl Into) -> Result { - let name = name.into(); - Self::validate(&name)?; - Ok(Self { name: Cow::Owned(name) }) - } - - // ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the slot name as a string slice. - pub fn as_str(&self) -> &str { - &self.name - } - - // HELPERS - // -------------------------------------------------------------------------------------------- - - /// Validates a slot name. - /// - /// This checks that components are separated by double colons, that each component contains - /// only valid characters and that the name is not empty or starts or ends with a colon. - /// - /// We must check the validity of a slot name against the raw bytes of the UTF-8 string because - /// typical character APIs are not available in a const version. We can do this because any byte - /// in a UTF-8 string that is an ASCII character never represents anything other than such a - /// character, even though UTF-8 can contain multibyte sequences: - /// - /// > UTF-8, the object of this memo, has a one-octet encoding unit. It uses all bits of an - /// > octet, but has the quality of preserving the full US-ASCII range: US-ASCII characters - /// > are encoded in one octet having the normal US-ASCII value, and any octet with such a value - /// > can only stand for a US-ASCII character, and nothing else. - /// > https://www.rfc-editor.org/rfc/rfc3629 - const fn validate(name: &str) -> Result<(), SlotNameError> { - let bytes = name.as_bytes(); - let mut idx = 0; - let mut num_components = 0; - - if bytes.is_empty() { - return Err(SlotNameError::TooShort); - } - - // Slot names must not start with a colon or underscore. - // SAFETY: We just checked that we're not dealing with an empty slice. - if bytes[0] == b':' { - return Err(SlotNameError::UnexpectedColon); - } else if bytes[0] == b'_' { - return Err(SlotNameError::UnexpectedUnderscore); - } - - while idx < bytes.len() { - let byte = bytes[idx]; - - let is_colon = byte == b':'; - - if is_colon { - // A colon must always be followed by another colon. In other words, we - // expect a double colon. - if (idx + 1) < bytes.len() { - if bytes[idx + 1] != b':' { - return Err(SlotNameError::UnexpectedColon); - } - } else { - return Err(SlotNameError::UnexpectedColon); - } - - // A component cannot end with a colon, so this allows us to validate the start of a - // component: It must not start with a colon or an underscore. - if (idx + 2) < bytes.len() { - if bytes[idx + 2] == b':' { - return Err(SlotNameError::UnexpectedColon); - } else if bytes[idx + 2] == b'_' { - return Err(SlotNameError::UnexpectedUnderscore); - } - } else { - return Err(SlotNameError::UnexpectedColon); - } - - // Advance past the double colon. - idx += 2; - - // A double colon completes a slot name component. - num_components += 1; - } else if Self::is_valid_char(byte) { - idx += 1; - } else { - return Err(SlotNameError::InvalidCharacter); - } - } - - // The last component is not counted as part of the loop because no double colon follows. - num_components += 1; - - if num_components < Self::MIN_NUM_COMPONENTS { - return Err(SlotNameError::TooShort); - } - - Ok(()) - } - - /// Returns `true` if the given byte is a valid slot name character, `false` otherwise. - const fn is_valid_char(byte: u8) -> bool { - byte.is_ascii_alphanumeric() || byte == b'_' - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - - use super::*; - - // A string containing all allowed characters of a slot name. - const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; - - // Const function tests - // -------------------------------------------------------------------------------------------- - - const _NAME0: SlotName = SlotName::from_static_str("name::component"); - const _NAME1: SlotName = SlotName::from_static_str("one::two::three::four::five"); - const _NAME2: SlotName = SlotName::from_static_str("one::two_three::four"); - - #[test] - #[should_panic(expected = "invalid slot name")] - fn slot_name_panics_on_invalid_character() { - SlotName::from_static_str("miden!::component"); - } - - #[test] - #[should_panic(expected = "invalid slot name")] - fn slot_name_panics_on_invalid_character2() { - SlotName::from_static_str("miden_ö::component"); - } - - #[test] - #[should_panic(expected = "invalid slot name")] - fn slot_name_panics_when_too_short() { - SlotName::from_static_str("one"); - } - - #[test] - #[should_panic(expected = "invalid slot name")] - fn slot_name_panics_on_component_starting_with_underscores() { - SlotName::from_static_str("one::_two"); - } - - // Invalid colon or underscore tests - // -------------------------------------------------------------------------------------------- - - #[test] - fn slot_name_fails_on_invalid_colon_placement() { - // Single colon. - assert_matches!(SlotName::new(":").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new("0::1:").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new(":0::1").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new("0::1:2").unwrap_err(), SlotNameError::UnexpectedColon); - - // Double colon (placed invalidly). - assert_matches!(SlotName::new("::").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new("1::2::").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new("::1::2").unwrap_err(), SlotNameError::UnexpectedColon); - - // Triple colon. - assert_matches!(SlotName::new(":::").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new("1::2:::").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new(":::1::2").unwrap_err(), SlotNameError::UnexpectedColon); - assert_matches!(SlotName::new("1::2:::3").unwrap_err(), SlotNameError::UnexpectedColon); - } - - #[test] - fn slot_name_fails_on_invalid_underscore_placement() { - assert_matches!( - SlotName::new("_one::two").unwrap_err(), - SlotNameError::UnexpectedUnderscore - ); - assert_matches!( - SlotName::new("one::_two").unwrap_err(), - SlotNameError::UnexpectedUnderscore - ); - } - - // Num components tests - // -------------------------------------------------------------------------------------------- - - #[test] - fn slot_name_fails_on_empty_string() { - assert_matches!(SlotName::new("").unwrap_err(), SlotNameError::TooShort); - } - - #[test] - fn slot_name_fails_on_single_component() { - assert_matches!(SlotName::new("single_component").unwrap_err(), SlotNameError::TooShort); - } - - // Alphabet validation tests - // -------------------------------------------------------------------------------------------- - - #[test] - fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> { - let name = format!("{FULL_ALPHABET}::second"); - let slot_name = SlotName::new(&name)?; - assert_eq!(slot_name.as_str(), name); - - Ok(()) - } - - #[test] - fn slot_name_fails_on_invalid_character() { - assert_matches!( - SlotName::new("na#me::second").unwrap_err(), - SlotNameError::InvalidCharacter - ); - assert_matches!( - SlotName::new("first_entry::secönd").unwrap_err(), - SlotNameError::InvalidCharacter - ); - assert_matches!( - SlotName::new("first::sec::th!rd").unwrap_err(), - SlotNameError::InvalidCharacter - ); - } - - // Valid slot name tests - // -------------------------------------------------------------------------------------------- - - #[test] - fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> { - SlotName::new("miden::component")?; - Ok(()) - } - - #[test] - fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> { - SlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?; - Ok(()) - } -} diff --git a/crates/miden-objects/src/block/nullifier_tree.rs b/crates/miden-objects/src/block/nullifier_tree.rs deleted file mode 100644 index ce99372208..0000000000 --- a/crates/miden-objects/src/block/nullifier_tree.rs +++ /dev/null @@ -1,337 +0,0 @@ -use alloc::string::ToString; -use alloc::vec::Vec; - -use miden_core::EMPTY_WORD; -use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_processor::DeserializationError; - -use crate::Word; -use crate::block::{BlockNumber, NullifierWitness}; -use crate::crypto::merkle::{MutationSet, SMT_DEPTH, Smt}; -use crate::errors::NullifierTreeError; -use crate::note::Nullifier; - -/// The sparse merkle tree of all nullifiers in the blockchain. -/// -/// A nullifier can only ever be spent once and its value in the tree is the block number at which -/// it was spent. -/// -/// The tree guarantees that once a nullifier has been inserted into the tree, its block number does -/// not change. Note that inserting the nullifier multiple times with the same block number is -/// valid. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NullifierTree { - smt: Smt, -} - -impl NullifierTree { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// The depth of the nullifier tree. - pub const DEPTH: u8 = SMT_DEPTH; - - /// The value of an unspent nullifier in the tree. - pub const UNSPENT_NULLIFIER: Word = EMPTY_WORD; - - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new, empty nullifier tree. - pub fn new() -> Self { - Self { smt: Smt::new() } - } - - /// Construct a new nullifier tree from the provided entries. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided entries contain multiple block numbers for the same nullifier. - pub fn with_entries( - entries: impl IntoIterator, - ) -> Result { - let leaves = entries.into_iter().map(|(nullifier, block_num)| { - (nullifier.as_word(), Self::block_num_to_leaf_value(block_num)) - }); - - let smt = Smt::with_entries(leaves) - .map_err(NullifierTreeError::DuplicateNullifierBlockNumbers)?; - - Ok(Self { smt }) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the root of the nullifier SMT. - pub fn root(&self) -> Word { - self.smt.root() - } - - /// Returns the number of spent nullifiers in this tree. - pub fn num_nullifiers(&self) -> usize { - self.smt.num_entries() - } - - /// Returns an iterator over the nullifiers and their block numbers in the tree. - pub fn entries(&self) -> impl Iterator { - self.smt.entries().map(|(nullifier, block_num)| { - (Nullifier::from(*nullifier), Self::leaf_value_to_block_num(*block_num)) - }) - } - - /// Returns a [`NullifierWitness`] of the leaf associated with the `nullifier`. - /// - /// Conceptually, such a witness is a Merkle path to the leaf, as well as the leaf itself. - /// - /// This witness is a proof of the current block number of the given nullifier. If that block - /// number is zero, it proves that the nullifier is unspent. - pub fn open(&self, nullifier: &Nullifier) -> NullifierWitness { - NullifierWitness::new(self.smt.open(&nullifier.as_word())) - } - - /// Returns the block number for the given nullifier or `None` if the nullifier wasn't spent - /// yet. - pub fn get_block_num(&self, nullifier: &Nullifier) -> Option { - let value = self.smt.get_value(&nullifier.as_word()); - if value == Self::UNSPENT_NULLIFIER { - return None; - } - - Some(Self::leaf_value_to_block_num(value)) - } - - /// Computes a mutation set resulting from inserting the provided nullifiers into this nullifier - /// tree. - /// - /// # Errors - /// - /// Returns an error if: - /// - a nullifier in the provided iterator was already spent. - pub fn compute_mutations( - &self, - nullifiers: impl IntoIterator, - ) -> Result - where - I: Iterator + Clone, - { - let nullifiers = nullifiers.into_iter(); - for (nullifier, _) in nullifiers.clone() { - if self.get_block_num(&nullifier).is_some() { - return Err(NullifierTreeError::NullifierAlreadySpent(nullifier)); - } - } - - let mutation_set = self - .smt - .compute_mutations(nullifiers.into_iter().map(|(nullifier, block_num)| { - (nullifier.as_word(), Self::block_num_to_leaf_value(block_num)) - })) - .map_err(NullifierTreeError::ComputeMutations)?; - - Ok(NullifierMutationSet::new(mutation_set)) - } - - // PUBLIC MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Marks the given nullifier as spent at the given block number. - /// - /// # Errors - /// - /// Returns an error if: - /// - the nullifier was already spent. - pub fn mark_spent( - &mut self, - nullifier: Nullifier, - block_num: BlockNumber, - ) -> Result<(), NullifierTreeError> { - let prev_nullifier_value = self - .smt - .insert(nullifier.as_word(), Self::block_num_to_leaf_value(block_num)) - .map_err(NullifierTreeError::MaxLeafEntriesExceeded)?; - - if prev_nullifier_value != Self::UNSPENT_NULLIFIER { - Err(NullifierTreeError::NullifierAlreadySpent(nullifier)) - } else { - Ok(()) - } - } - - /// Applies mutations to the nullifier tree. - /// - /// # Errors - /// - /// Returns an error if: - /// - `mutations` was computed on a tree with a different root than this one. - pub fn apply_mutations( - &mut self, - mutations: NullifierMutationSet, - ) -> Result<(), NullifierTreeError> { - self.smt - .apply_mutations(mutations.into_mutation_set()) - .map_err(NullifierTreeError::TreeRootConflict) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Returns the nullifier's leaf value in the SMT by its block number. - pub(super) fn block_num_to_leaf_value(block: BlockNumber) -> Word { - Word::from([block.as_u32(), 0, 0, 0]) - } - - /// Given the leaf value of the nullifier SMT, returns the nullifier's block number. - /// - /// There are no nullifiers in the genesis block. The value zero is instead used to signal - /// absence of a value. - fn leaf_value_to_block_num(value: Word) -> BlockNumber { - let block_num: u32 = - value[0].as_int().try_into().expect("invalid block number found in store"); - - block_num.into() - } -} - -impl Default for NullifierTree { - fn default() -> Self { - Self::new() - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for NullifierTree { - fn write_into(&self, target: &mut W) { - self.entries().collect::>().write_into(target); - } -} - -impl Deserializable for NullifierTree { - fn read_from(source: &mut R) -> Result { - let entries = Vec::<(Nullifier, BlockNumber)>::read_from(source)?; - Self::with_entries(entries) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} - -// NULLIFIER MUTATION SET -// ================================================================================================ - -/// A newtype wrapper around a [`MutationSet`] for use in the [`NullifierTree`]. -/// -/// It guarantees that applying the contained mutations will result in a nullifier tree where -/// nullifier's block numbers are not updated (except if they were unspent before), ensuring that -/// nullifiers are only spent once. -/// -/// It is returned by and used in methods on the [`NullifierTree`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NullifierMutationSet { - mutation_set: MutationSet<{ NullifierTree::DEPTH }, Word, Word>, -} - -impl NullifierMutationSet { - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`AccountMutationSet`] from the provided raw mutation set. - fn new(mutation_set: MutationSet<{ NullifierTree::DEPTH }, Word, Word>) -> Self { - Self { mutation_set } - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns a reference to the underlying [`MutationSet`]. - pub fn as_mutation_set(&self) -> &MutationSet<{ NullifierTree::DEPTH }, Word, Word> { - &self.mutation_set - } - - // PUBLIC MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Consumes self and returns the underlying [`MutationSet`]. - pub fn into_mutation_set(self) -> MutationSet<{ NullifierTree::DEPTH }, Word, Word> { - self.mutation_set - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - - use super::NullifierTree; - use crate::block::BlockNumber; - use crate::note::Nullifier; - use crate::{NullifierTreeError, Word}; - - #[test] - fn leaf_value_encoding() { - let block_num = 123; - let nullifier_value = NullifierTree::block_num_to_leaf_value(block_num.into()); - assert_eq!(nullifier_value, Word::from([block_num, 0, 0, 0u32])); - } - - #[test] - fn leaf_value_decoding() { - let block_num = 123; - let nullifier_value = Word::from([block_num, 0, 0, 0u32]); - let decoded_block_num = NullifierTree::leaf_value_to_block_num(nullifier_value); - - assert_eq!(decoded_block_num, block_num.into()); - } - - #[test] - fn apply_mutations() { - let nullifier1 = Nullifier::dummy(1); - let nullifier2 = Nullifier::dummy(2); - let nullifier3 = Nullifier::dummy(3); - - let block1 = BlockNumber::from(1); - let block2 = BlockNumber::from(2); - let block3 = BlockNumber::from(3); - - let mut tree = NullifierTree::with_entries([(nullifier1, block1)]).unwrap(); - - // Check that passing nullifier2 twice with different values will use the last value. - let mutations = tree - .compute_mutations([(nullifier2, block1), (nullifier3, block3), (nullifier2, block2)]) - .unwrap(); - - tree.apply_mutations(mutations).unwrap(); - - assert_eq!(tree.num_nullifiers(), 3); - assert_eq!(tree.get_block_num(&nullifier1).unwrap(), block1); - assert_eq!(tree.get_block_num(&nullifier2).unwrap(), block2); - assert_eq!(tree.get_block_num(&nullifier3).unwrap(), block3); - } - - #[test] - fn nullifier_already_spent() { - let nullifier1 = Nullifier::dummy(1); - - let block1 = BlockNumber::from(1); - let block2 = BlockNumber::from(2); - - let mut tree = NullifierTree::with_entries([(nullifier1, block1)]).unwrap(); - - // Attempt to insert nullifier 1 again at _the same_ block number. - let err = tree.clone().compute_mutations([(nullifier1, block1)]).unwrap_err(); - assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); - - let err = tree.clone().mark_spent(nullifier1, block1).unwrap_err(); - assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); - - // Attempt to insert nullifier 1 again at a different block number. - let err = tree.clone().compute_mutations([(nullifier1, block2)]).unwrap_err(); - assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); - - let err = tree.mark_spent(nullifier1, block2).unwrap_err(); - assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); - } -} diff --git a/crates/miden-objects/src/block/proven_block.rs b/crates/miden-objects/src/block/proven_block.rs deleted file mode 100644 index 736a64cd85..0000000000 --- a/crates/miden-objects/src/block/proven_block.rs +++ /dev/null @@ -1,198 +0,0 @@ -use alloc::vec::Vec; - -use crate::block::{ - BlockAccountUpdate, - BlockHeader, - BlockNoteIndex, - BlockNoteTree, - OutputNoteBatch, -}; -use crate::note::Nullifier; -use crate::transaction::{OrderedTransactionHeaders, OutputNote}; -use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{MIN_PROOF_SECURITY_LEVEL, Word}; - -// PROVEN BLOCK -// ================================================================================================ - -/// A block in the Miden chain. -/// -/// A block is built from batches of transactions, i.e. multiple -/// [`ProvenBatch`](crate::batch::ProvenBatch)es, and each batch contains multiple -/// [`ProvenTransaction`](crate::transaction::ProvenTransaction)s. -/// -/// It consists of the following components: -/// - A [`BlockHeader`] committing to the current state of the chain and against which account, note -/// or nullifier inclusion or absence can be proven. See its documentation for details on what it -/// commits to. Eventually, it will also contain a ZK proof of the validity of the block. -/// - A list of account updates for all accounts updated in this block. For private accounts, the -/// update contains only the new account state commitments while for public accounts, the update -/// also includes the delta which can be applied to the previous account state to get the new -/// account state. -/// - A list of new notes created in this block. For private notes, the block contains only note IDs -/// and note metadata while for public notes the full note details are included. -/// - A list of new nullifiers created for all notes that were consumed in the block. -/// - A list of transaction headers that were included in the block. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProvenBlock { - /// The header of the block, committing to the current state of the chain. - header: BlockHeader, - - /// Account updates for the block. - updated_accounts: Vec, - - /// Note batches created by the transactions in this block. - output_note_batches: Vec, - - /// Nullifiers created by the transactions in this block through the consumption of notes. - created_nullifiers: Vec, - - /// The aggregated and flattened transaction headers of all batches in the order in which they - /// appeared in the proposed block. - transactions: OrderedTransactionHeaders, -} - -impl ProvenBlock { - /// Returns a new [`ProvenBlock`] instantiated from the provided components. - /// - /// # Warning - /// - /// This constructor does not do any validation, so passing incorrect values may lead to later - /// panics. - pub fn new_unchecked( - header: BlockHeader, - updated_accounts: Vec, - output_note_batches: Vec, - created_nullifiers: Vec, - transactions: OrderedTransactionHeaders, - ) -> Self { - Self { - header, - updated_accounts, - output_note_batches, - created_nullifiers, - transactions, - } - } - - /// Returns the commitment to this block. - pub fn commitment(&self) -> Word { - self.header.commitment() - } - - /// Returns the header of this block. - pub fn header(&self) -> &BlockHeader { - &self.header - } - - /// Returns the slice of [`BlockAccountUpdate`]s for all accounts updated in this block. - pub fn updated_accounts(&self) -> &[BlockAccountUpdate] { - &self.updated_accounts - } - - /// Returns the slice of [`OutputNoteBatch`]es for all output notes created in this block. - pub fn output_note_batches(&self) -> &[OutputNoteBatch] { - &self.output_note_batches - } - - /// Returns the proof security level of the block. - pub fn proof_security_level(&self) -> u32 { - MIN_PROOF_SECURITY_LEVEL - } - - /// Returns an iterator over all [`OutputNote`]s created in this block. - /// - /// Each note is accompanied by a corresponding index specifying where the note is located - /// in the block's [`BlockNoteTree`]. - pub fn output_notes(&self) -> impl Iterator { - self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| { - notes.iter().map(move |(note_idx_in_batch, note)| { - ( - // SAFETY: The proven block contains at most the max allowed number of batches - // and each batch is guaranteed to contain at most the - // max allowed number of output notes. - BlockNoteIndex::new(batch_idx, *note_idx_in_batch) - .expect("max batches in block and max notes in batches should be enforced"), - note, - ) - }) - }) - } - - /// Returns the [`BlockNoteTree`] containing all [`OutputNote`]s created in this block. - pub fn build_output_note_tree(&self) -> BlockNoteTree { - let entries = self - .output_notes() - .map(|(note_index, note)| (note_index, note.id(), *note.metadata())); - - // SAFETY: We only construct proven blocks that: - // - do not contain duplicates - // - contain at most the max allowed number of batches and each batch is guaranteed to - // contain at most the max allowed number of output notes. - BlockNoteTree::with_entries(entries) - .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum") - } - - /// Returns a reference to the slice of nullifiers for all notes consumed in the block. - pub fn created_nullifiers(&self) -> &[Nullifier] { - &self.created_nullifiers - } - - /// Returns the [`OrderedTransactionHeaders`] of all transactions included in this block. - pub fn transactions(&self) -> &OrderedTransactionHeaders { - &self.transactions - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for ProvenBlock { - fn write_into(&self, target: &mut W) { - self.header.write_into(target); - self.updated_accounts.write_into(target); - self.output_note_batches.write_into(target); - self.created_nullifiers.write_into(target); - self.transactions.write_into(target); - } -} - -impl Deserializable for ProvenBlock { - fn read_from(source: &mut R) -> Result { - let block = Self { - header: BlockHeader::read_from(source)?, - updated_accounts: >::read_from(source)?, - output_note_batches: >::read_from(source)?, - created_nullifiers: >::read_from(source)?, - transactions: OrderedTransactionHeaders::read_from(source)?, - }; - - Ok(block) - } -} - -// TESTING -// ================================================================================================ - -#[cfg(any(feature = "testing", test))] -impl ProvenBlock { - /// Returns a mutable reference to the block's account updates for testing purposes. - pub fn updated_accounts_mut(&mut self) -> &mut Vec { - &mut self.updated_accounts - } - - /// Returns a mutable reference to the block's nullifiers for testing purposes. - pub fn created_nullifiers_mut(&mut self) -> &mut Vec { - &mut self.created_nullifiers - } - - /// Returns a mutable reference to the block's output note batches for testing purposes. - pub fn output_note_batches_mut(&mut self) -> &mut Vec { - &mut self.output_note_batches - } - - /// Sets the block's header for testing purposes. - pub fn set_block_header(&mut self, header: BlockHeader) { - self.header = header; - } -} diff --git a/crates/miden-objects/src/note/metadata.rs b/crates/miden-objects/src/note/metadata.rs deleted file mode 100644 index 31cdd14bcd..0000000000 --- a/crates/miden-objects/src/note/metadata.rs +++ /dev/null @@ -1,376 +0,0 @@ -use alloc::string::ToString; - -use super::execution_hint::NoteExecutionHint; -use super::{ - AccountId, - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Felt, - NoteError, - NoteTag, - NoteType, - Serializable, - Word, -}; - -// NOTE METADATA -// ================================================================================================ - -/// Metadata associated with a note. -/// -/// Note type and tag must be internally consistent according to the following rules: -/// -/// - For private and encrypted notes, the two most significant bits of the tag must be `0b11`. -/// - For public notes, the two most significant bits of the tag can be set to any value. -/// -/// # Word layout & validity -/// -/// [`NoteMetadata`] can be encoded into a [`Word`] with the following layout: -/// -/// ```text -/// 1st felt: [sender_id_prefix (64 bits)] -/// 2nd felt: [sender_id_suffix (56 bits) | note_type (2 bits) | note_execution_hint_tag (6 bits)] -/// 3rd felt: [note_execution_hint_payload (32 bits) | note_tag (32 bits)] -/// 4th felt: [aux (64 bits)] -/// ``` -/// -/// The rationale for the above layout is to ensure the validity of each felt: -/// - 1st felt: Is equivalent to the prefix of the account ID so it inherits its validity. -/// - 2nd 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 is designed such that it retains its felt validity -/// even if all of its lower 8 bits are be set to `1`. This is because the most significant bit is -/// always zero. -/// - 3rd felt: The note execution hint payload must contain at least one `0` bit in its encoding, -/// so the upper 32 bits of the felt will contain at least one `0` bit making the entire felt -/// valid. -/// - 4th felt: The `aux` value must be a felt itself. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct NoteMetadata { - /// The ID of the account which created the note. - sender: AccountId, - - /// Defines how the note is to be stored (e.g. public or private). - note_type: NoteType, - - /// A value which can be used by the recipient(s) to identify notes intended for them. - tag: NoteTag, - - /// An arbitrary user-defined value. - aux: Felt, - - /// Specifies when a note is ready to be consumed. - execution_hint: NoteExecutionHint, -} - -impl NoteMetadata { - /// Returns a new [NoteMetadata] instantiated with the specified parameters. - /// - /// # Errors - /// Returns an error if the note type and note tag are inconsistent. - pub fn new( - sender: AccountId, - note_type: NoteType, - tag: NoteTag, - execution_hint: NoteExecutionHint, - aux: Felt, - ) -> Result { - let tag = tag.validate(note_type)?; - Ok(Self { - sender, - note_type, - tag, - aux, - execution_hint, - }) - } - - /// Returns the account which created the note. - pub fn sender(&self) -> AccountId { - self.sender - } - - /// Returns the note's type. - pub fn note_type(&self) -> NoteType { - self.note_type - } - - /// Returns the tag associated with the note. - pub fn tag(&self) -> NoteTag { - self.tag - } - - /// Returns the execution hint associated with the note. - pub fn execution_hint(&self) -> NoteExecutionHint { - self.execution_hint - } - - /// Returns the note's aux field. - pub fn aux(&self) -> Felt { - self.aux - } - - /// Returns `true` if the note is private. - pub fn is_private(&self) -> bool { - self.note_type == NoteType::Private - } -} - -impl From for Word { - /// Convert a [`NoteMetadata`] into a [`Word`]. - /// - /// The produced layout of the word is documented on the [`NoteMetadata`] type. - fn from(metadata: NoteMetadata) -> Self { - (&metadata).into() - } -} - -impl From<&NoteMetadata> for Word { - /// Convert a [`NoteMetadata`] into a [`Word`]. - /// - /// The produced layout of the word is documented on the [`NoteMetadata`] type. - fn from(metadata: &NoteMetadata) -> Self { - let mut elements = Word::empty(); - elements[0] = metadata.sender.prefix().as_felt(); - elements[1] = merge_id_type_and_hint_tag( - metadata.sender.suffix(), - metadata.note_type, - metadata.execution_hint, - ); - elements[2] = merge_note_tag_and_hint_payload(metadata.execution_hint, metadata.tag); - elements[3] = metadata.aux; - elements - } -} - -impl TryFrom for NoteMetadata { - type Error = NoteError; - - /// Tries to decode a [`Word`] into a [`NoteMetadata`]. - /// - /// The expected layout of the word is documented on the [`NoteMetadata`] type. - fn try_from(elements: Word) -> Result { - let sender_id_prefix: Felt = elements[0]; - - let (sender_id_suffix, note_type, execution_hint_tag) = - unmerge_id_type_and_hint_tag(elements[1])?; - - let sender = AccountId::try_from([sender_id_prefix, sender_id_suffix]) - .map_err(NoteError::NoteSenderInvalidAccountId)?; - - let (execution_hint, note_tag) = - unmerge_note_tag_and_hint_payload(elements[2], execution_hint_tag)?; - - Self::new(sender, note_type, note_tag, execution_hint, elements[3]) - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for NoteMetadata { - fn write_into(&self, target: &mut W) { - Word::from(self).write_into(target); - } -} - -impl Deserializable for NoteMetadata { - fn read_from(source: &mut R) -> Result { - let word = Word::read_from(source)?; - Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Merges the suffix of an [`AccountId`], a [`NoteType`] and the tag of a -/// [`NoteExecutionHint`] into a single [`Felt`]. -/// -/// The layout is as follows: -/// -/// ```text -/// [sender_id_suffix (56 bits) | note_type (2 bits) | note_execution_hint_tag (6 bits)] -/// ``` -/// -/// One of the upper 16 bits is guaranteed to be zero due to the guarantees of the epoch in the -/// account ID. -/// -/// Note that `sender_id_suffix` is the suffix of the sender's account ID. -fn merge_id_type_and_hint_tag( - sender_id_suffix: Felt, - note_type: NoteType, - note_execution_hint: NoteExecutionHint, -) -> Felt { - let mut merged = sender_id_suffix.as_int(); - - let type_bits = note_type as u8; - let (tag_bits, _) = note_execution_hint.into_parts(); - - debug_assert!(type_bits & 0b1111_1100 == 0, "note type must not contain values >= 4"); - debug_assert!( - tag_bits & 0b1100_0000 == 0, - "note execution hint tag must not contain values >= 64" - ); - - // Note: The least significant byte of the second AccountId felt is zero by construction so we - // can overwrite it. - merged |= (type_bits << 6) as u64; - merged |= tag_bits as u64; - - // SAFETY: The most significant bit of the suffix is zero by construction so the bytes will be a - // valid felt. - Felt::try_from(merged).expect("encoded value should be a valid felt") -} - -/// Unmerges the given felt into the suffix of an [`AccountId`], a [`NoteType`] and the tag of -/// a [`NoteExecutionHint`]. -fn unmerge_id_type_and_hint_tag(element: Felt) -> Result<(Felt, NoteType, u8), NoteError> { - let element = element.as_int(); - - // Cut off the least significant byte. - let least_significant_byte = element as u8; - let note_type_bits = (least_significant_byte & 0b1100_0000) >> 6; - let tag_bits = least_significant_byte & 0b0011_1111; - - let note_type = NoteType::try_from(note_type_bits)?; - - // Set least significant byte to zero. - let element = element & 0xffff_ffff_ffff_ff00; - - // SAFETY: The input was a valid felt and we cleared additional bits and did not set any - // bits, so it must still be a valid felt. - let sender_id_suffix = Felt::try_from(element).expect("element should still be valid"); - - Ok((sender_id_suffix, note_type, tag_bits)) -} - -/// Merges the [`NoteExecutionHint`] payload and a [`NoteTag`] into a single [`Felt`]. -/// -/// The layout is as follows: -/// -/// ```text -/// [note_execution_hint_payload (32 bits) | note_tag (32 bits)] -/// ``` -/// -/// One of the upper 32 bits is guaranteed to be zero. -fn merge_note_tag_and_hint_payload( - note_execution_hint: NoteExecutionHint, - note_tag: NoteTag, -) -> Felt { - let (_, payload) = note_execution_hint.into_parts(); - let note_tag: u32 = note_tag.into(); - - debug_assert_ne!( - payload, - u32::MAX, - "payload should never be u32::MAX as it would produce an invalid felt" - ); - - let felt_int = ((payload as u64) << 32) | (note_tag as u64); - - // SAFETY: The payload is guaranteed to never be u32::MAX so at least one of the upper 32 bits - // is zero, hence the felt is valid even if note_tag is u32::MAX. - Felt::try_from(felt_int).expect("bytes should be a valid felt") -} - -/// Unmerges the given felt into a [`NoteExecutionHint`] payload and a [`NoteTag`] and constructs a -/// [`NoteExecutionHint`] from the unmerged payload and the given `note_execution_hint_tag`. -fn unmerge_note_tag_and_hint_payload( - element: Felt, - note_execution_hint_tag: u8, -) -> Result<(NoteExecutionHint, NoteTag), NoteError> { - let element = element.as_int(); - - let payload = (element >> 32) as u32; - let note_tag = (element & 0xffff_ffff) as u32; - - let execution_hint = NoteExecutionHint::from_parts(note_execution_hint_tag, payload)?; - let note_tag = NoteTag::from(note_tag); - - Ok((execution_hint, note_tag)) -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - - use anyhow::Context; - - use super::*; - use crate::testing::account_id::ACCOUNT_ID_MAX_ONES; - - #[test] - fn note_metadata_serde() -> anyhow::Result<()> { - // Use the Account ID with the maximum one bits to test if the merge function always - // produces valid felts. - let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); - let note_type = NoteType::Public; - let tag = NoteTag::from_account_id(sender); - let aux = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap(); - - for execution_hint in [ - NoteExecutionHint::always(), - NoteExecutionHint::none(), - NoteExecutionHint::on_block_slot(10, 11, 12), - NoteExecutionHint::after_block((u32::MAX - 1).into()).unwrap(), - ] { - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux).unwrap(); - NoteMetadata::read_from_bytes(&metadata.to_bytes()) - .context(format!("failed for execution hint {execution_hint:?}"))?; - } - - Ok(()) - } - - #[test] - fn merge_and_unmerge_id_type_and_hint() { - // Use the Account ID with the maximum one bits to test if the merge function always - // produces valid felts. - let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); - let sender_id_suffix = sender.suffix(); - - let note_type = NoteType::Public; - let note_execution_hint = NoteExecutionHint::OnBlockSlot { - round_len: 10, - slot_len: 11, - slot_offset: 12, - }; - - let merged_value = - merge_id_type_and_hint_tag(sender_id_suffix, note_type, note_execution_hint); - let (extracted_suffix, extracted_note_type, extracted_note_execution_hint_tag) = - unmerge_id_type_and_hint_tag(merged_value).unwrap(); - - assert_eq!(note_type, extracted_note_type); - assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag); - assert_eq!(sender_id_suffix, extracted_suffix); - - let note_type = NoteType::Private; - let note_execution_hint = NoteExecutionHint::Always; - - let merged_value = - merge_id_type_and_hint_tag(sender_id_suffix, note_type, note_execution_hint); - let (extracted_suffix, extracted_note_type, extracted_note_execution_hint_tag) = - unmerge_id_type_and_hint_tag(merged_value).unwrap(); - - assert_eq!(note_type, extracted_note_type); - assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag); - assert_eq!(sender_id_suffix, extracted_suffix); - - let note_type = NoteType::Private; - let note_execution_hint = NoteExecutionHint::None; - - let merged_value = - merge_id_type_and_hint_tag(sender_id_suffix, note_type, note_execution_hint); - let (extracted_suffix, extracted_note_type, extracted_note_execution_hint_tag) = - unmerge_id_type_and_hint_tag(merged_value).unwrap(); - - assert_eq!(note_type, extracted_note_type); - assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag); - assert_eq!(sender_id_suffix, extracted_suffix); - } -} diff --git a/crates/miden-objects/src/note/note_tag.rs b/crates/miden-objects/src/note/note_tag.rs deleted file mode 100644 index 4da8f012fb..0000000000 --- a/crates/miden-objects/src/note/note_tag.rs +++ /dev/null @@ -1,670 +0,0 @@ -use core::fmt; - -use miden_crypto::Felt; - -use super::{ - AccountId, - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - NoteError, - NoteType, - Serializable, -}; -use crate::account::AccountStorageMode; - -// CONSTANTS -// ================================================================================================ -const NETWORK_EXECUTION: u8 = 0; -const LOCAL_EXECUTION: u8 = 1; - -// The 2 most significant bits are set to `0b00`. -const NETWORK_ACCOUNT: u32 = 0; -// The 2 most significant bits are set to `0b01`. -const NETWORK_PUBLIC_USECASE: u32 = 0x4000_0000; -// The 2 most significant bits are set to `0b10`. -const LOCAL_PUBLIC_ANY: u32 = 0x8000_0000; -// The 2 most significant bits are set to `0b11`. -const LOCAL_ANY: u32 = 0xc000_0000; - -/// [super::Note]'s execution mode hints. -/// -/// The execution hints are _not_ enforced, therefore function only as hints. For example, if a -/// note's tag is created with the [NoteExecutionMode::Network], further validation is necessary to -/// check the account_id is known, that the account's state is public on chain, and the account is -/// controlled by the network. -/// -/// The goal of the hint is to allow for a network node to quickly filter notes that are not -/// intended for network execution, and skip the validation steps mentioned above. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum NoteExecutionMode { - Network = NETWORK_EXECUTION, - Local = LOCAL_EXECUTION, -} - -// NOTE TAG -// ================================================================================================ - -/// [NoteTag]`s are best effort filters for notes registered with the network. -/// -/// Tags are light-weight values used to speed up queries. The 2 most significant bits of the tags -/// have the following interpretation: -/// -/// | Prefix | Name | [`NoteExecutionMode`] | Target | Allowed [`NoteType`] | -/// | :----: | :--------------------: | :-------------------: | :----------------------: | :------------------: | -/// | `0b00` | `NetworkAccount` | Network | Network Account | [`NoteType::Public`] | -/// | `0b01` | `NetworkUseCase` | Network | Use case | [`NoteType::Public`] | -/// | `0b10` | `LocalPublicAny` | Local | Any | [`NoteType::Public`] | -/// | `0b11` | `LocalAny` | Local | Any | Any | -/// -/// Where: -/// -/// - [`NoteExecutionMode`] is set to [`NoteExecutionMode::Network`] to hint a [`Note`](super::Note) -/// should be consumed by the network. These notes will be further validated and if possible -/// consumed by it. -/// - Target describes how to further interpret the bits in the tag. -/// - For tags with a specific target, the rest of the tag is interpreted as a partial -/// [`AccountId`]. For network accounts these are the first 30 bits of the ID while for local -/// account targets, the first 14 bits are used - a trade-off between privacy and uniqueness. -/// - For use case values, the meaning of the rest of the tag is not specified by the protocol and -/// can be used by applications built on top of the rollup. -/// -/// The note type is the only value enforced by the protocol. The rationale is that any note -/// intended to be consumed by the network must be public to have all the details available. The -/// public note for local execution is intended to allow users to search for notes that can be -/// consumed right away, without requiring an off-band communication channel. -/// -/// **Note on Type Safety** -/// -/// Each enum variant contains the raw encoding of the note tag, where the first two bits -/// _should_ correspond to the variant's prefix (as defined in the table above). However, because -/// enum variants are always public, it is possible to instantiate this enum where this invariant -/// does not hold, e.g. `NoteTag::NetworkAccount(0b11...)`. For that reason, the enum variants -/// should take precedence in case of such a mismatch and the inner value **should not be accessed -/// directly**. Instead, only rely on [`NoteTag::as_u32`] to access the encoded value, which will -/// always return the correct value. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum NoteTag { - /// Represents a tag for a note intended for network execution, targeted at a network account. - /// The note must be public. - NetworkAccount(u32), - /// Represents a tag for a note intended for network execution for a public use case. The note - /// must be public. - NetworkUseCase(u16, u16), - /// Represents a tag for a note intended for local execution. - /// - /// This is used for two purposes: - /// - A public use case. - /// - A note targeted at any type of account. - /// - /// In all cases, the note must be **public**. - LocalPublicAny(u32), - /// Represents a tag for a note intended for local execution. - /// - /// This is used for two purposes: - /// - A private use case. - /// - A note targeted at any type of account. - /// - /// In all cases, the note can be of any type. - LocalAny(u32), -} - -impl NoteTag { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// The exponent of the maximum allowed use case id. In other words, 2^exponent is the maximum - /// allowed use case id. - pub(crate) const MAX_USE_CASE_ID_EXPONENT: u8 = 14; - /// The default note tag length for an account ID with local execution. - pub const DEFAULT_LOCAL_TAG_LENGTH: u8 = 14; - /// The default note tag length for an account ID with network execution. - pub const DEFAULT_NETWORK_TAG_LENGTH: u8 = 30; - /// The maximum number of bits that can be encoded into the tag for local accounts. - pub const MAX_LOCAL_TAG_LENGTH: u8 = 30; - - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Returns a new [NoteTag::NetworkAccount] or [NoteTag::LocalAny] instantiated from the - /// specified account ID. - /// - /// The tag is constructed as follows: - /// - /// - For local execution ([`AccountStorageMode::Private`] or [`AccountStorageMode::Public`]), - /// the two most significant bits are set to `0b11`, which allows for any note type to be - /// used. The following 14 bits are set to the most significant bits of the account ID, and - /// the remaining 16 bits are set to 0. - /// - For network execution ([`AccountStorageMode::Network`]), the most significant bits are set - /// to `0b00` and the remaining bits are set to the 30 most significant bits of the account - /// ID. - pub fn from_account_id(account_id: AccountId) -> Self { - match account_id.storage_mode() { - AccountStorageMode::Network => Self::from_network_account_id(account_id), - AccountStorageMode::Private | AccountStorageMode::Public => { - // safe to unwrap since DEFAULT_LOCAL_TAG_LENGTH < MAX_LOCAL_TAG_LENGTH - Self::from_local_account_id(account_id, Self::DEFAULT_LOCAL_TAG_LENGTH).unwrap() - }, - } - } - - /// Constructs a [`NoteTag::LocalAny`] from the given `account_id` and `tag_len`. - /// - /// The tag is constructed as follows: - /// - /// - The two most significant bits are set to `0b11` to indicate a [LOCAL_ANY] tag. - /// - The next `tag_len` bits are set to the most significant bits of the account ID prefix. - /// - The remaining bits are set to zero. - /// - /// # Errors - /// - /// Returns an error if `tag_len` is larger than [`NoteTag::MAX_LOCAL_TAG_LENGTH`]. - pub(crate) fn from_local_account_id( - account_id: AccountId, - tag_len: u8, - ) -> Result { - if tag_len > Self::MAX_LOCAL_TAG_LENGTH { - return Err(NoteError::NoteTagLengthTooLarge(tag_len)); - } - - let prefix_id: u64 = account_id.prefix().into(); - - // Shift the high bits of the account ID such that they are laid out as: - // [34 zero bits | remaining high bits (30 bits)]. - let high_bits = prefix_id >> 34; - - // This is equivalent to the following layout, interpreted as a u32: - // [2 zero bits | remaining high bits (30 bits)]. - let high_bits = high_bits as u32; - - // Select the top `tag_len` bits of the account ID, i.e.: - // [2 zero bits | remaining high bits (tag_len bits) | (30 - tag_len) zero bits]. - let high_bits = high_bits & (u32::MAX << (32 - 2 - tag_len)); - - // Set the local execution tag in the two most significant bits. - Ok(Self::LocalAny(LOCAL_ANY | high_bits)) - } - - /// Constructs a [`NoteTag::NetworkAccount`] from the specified `account_id`. - /// - /// The tag is constructed as follows: - /// - /// - The two most significant bits are set to `0b00` to indicate a [NETWORK_ACCOUNT] tag. - /// - The remaining bits are set to the 30 most significant bits of the account ID. - pub(crate) fn from_network_account_id(account_id: AccountId) -> Self { - let prefix_id: u64 = account_id.prefix().into(); - - // Shift the high bits of the account ID such that they are laid out as: - // [34 zero bits | remaining high bits (30 bits)]. - let high_bits = prefix_id >> 34; - - // This is equivalent to the following layout, interpreted as a u32: - // [2 zero bits | remaining high bits (30 bits)]. - // The two most significant zero bits match the tag we need for network - Self::NetworkAccount(high_bits as u32) - } - - /// Returns a new [`NoteTag::NetworkUseCase`] or [`NoteTag::LocalPublicAny`] - /// instantiated for a custom use case which requires a public note. - /// - /// The public use_case tag requires a [NoteType::Public] note. - /// - /// The two high bits are set to the `b10` or `b01` depending on the execution hint, the next 14 - /// bits are set to the `use_case_id`, and the low 16 bits are set to `payload`. - /// - /// # Errors - /// - /// - If `use_case_id` is larger than or equal to $2^{14}$. - pub fn for_public_use_case( - use_case_id: u16, - payload: u16, - execution: NoteExecutionMode, - ) -> Result { - if (use_case_id >> 14) != 0 { - return Err(NoteError::NoteTagUseCaseTooLarge(use_case_id)); - } - - match execution { - NoteExecutionMode::Network => { - let use_case_bits = (NETWORK_PUBLIC_USECASE >> 16) as u16 | use_case_id; - Ok(Self::NetworkUseCase(use_case_bits, payload)) - }, - NoteExecutionMode::Local => { - let tag_u32 = LOCAL_PUBLIC_ANY | ((use_case_id as u32) << 16) | (payload as u32); - Ok(Self::LocalPublicAny(tag_u32)) - }, - } - } - - /// Returns a new [`NoteTag::LocalAny`] instantiated for a custom local use case. - /// - /// The local use_case tag is the only tag type that allows for [NoteType::Private] notes. - /// - /// The two high bits are set to the `b11`, the next 14 bits are set to the `use_case_id`, and - /// the low 16 bits are set to `payload`. - /// - /// # Errors - /// - /// - If `use_case_id` is larger than or equal to 2^14. - pub fn for_local_use_case(use_case_id: u16, payload: u16) -> Result { - if (use_case_id >> NoteTag::MAX_USE_CASE_ID_EXPONENT) != 0 { - return Err(NoteError::NoteTagUseCaseTooLarge(use_case_id)); - } - - let use_case_bits = (use_case_id as u32) << 16; - let payload_bits = payload as u32; - - Ok(Self::LocalAny(LOCAL_ANY | use_case_bits | payload_bits)) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns true if the note is intended for execution by a specific account, i.e. - /// [`NoteTag::NetworkAccount`] - pub fn is_single_target(&self) -> bool { - matches!(self, NoteTag::NetworkAccount(_)) - } - - /// Returns note execution mode defined by this tag. - /// - /// If the most significant bit of the tag is 0 the note is intended for local execution; - /// otherwise, the note is intended for network execution. - pub fn execution_mode(&self) -> NoteExecutionMode { - match self { - NoteTag::NetworkAccount(_) | NoteTag::NetworkUseCase(..) => NoteExecutionMode::Network, - NoteTag::LocalAny(_) | NoteTag::LocalPublicAny(..) => NoteExecutionMode::Local, - } - } - - /// Returns the inner u32 value of this tag. - pub fn as_u32(&self) -> u32 { - // Note that we always set the two most significant bits to the prefix corresponding to the - // enum variant. See the note on type safety on the NoteTag docs. - - /// Masks out the two most significant bits, leaving all lower bits untouched. - const LOW_BITS_MASK: u32 = 0x3fff_ffff; - match self { - NoteTag::NetworkAccount(tag) => *tag & LOW_BITS_MASK, - NoteTag::NetworkUseCase(use_case_bits, payload_bits) => { - ((*use_case_bits as u32) << 16 | *payload_bits as u32) & LOW_BITS_MASK - | NETWORK_PUBLIC_USECASE - }, - NoteTag::LocalPublicAny(tag) => (*tag & LOW_BITS_MASK) | LOCAL_PUBLIC_ANY, - NoteTag::LocalAny(tag) => (*tag & LOW_BITS_MASK) | LOCAL_ANY, - } - } - - // UTILITY METHODS - // -------------------------------------------------------------------------------------------- - - /// Returns an error if this tag is not consistent with the specified note type, and self - /// otherwise. - pub fn validate(&self, note_type: NoteType) -> Result { - if self.execution_mode() == NoteExecutionMode::Network && note_type != NoteType::Public { - return Err(NoteError::NetworkExecutionRequiresPublicNote(note_type)); - } - - // Ensure the note is public if the note tag requires it. - if self.requires_public_note() && note_type != NoteType::Public { - Err(NoteError::PublicNoteRequired(note_type)) - } else { - Ok(*self) - } - } - - /// Returns `true` if the note tag requires a public note. - fn requires_public_note(&self) -> bool { - matches!( - self, - NoteTag::NetworkAccount(_) | NoteTag::NetworkUseCase(_, _) | NoteTag::LocalPublicAny(_) - ) - } -} - -impl fmt::Display for NoteTag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_u32()) - } -} - -// CONVERSIONS INTO NOTE TAG -// ================================================================================================ - -impl From for NoteTag { - fn from(value: u32) -> Self { - // Mask out the two most significant bits. - match value & 0xc0000000 { - NETWORK_ACCOUNT => Self::NetworkAccount(value), - NETWORK_PUBLIC_USECASE => Self::NetworkUseCase((value >> 16) as u16, value as u16), - LOCAL_ANY => Self::LocalAny(value), - LOCAL_PUBLIC_ANY => Self::LocalPublicAny(value), - _ => { - // This branch should be unreachable because `prefix` is derived from - // the top 2 bits of a u32, which can only be 0, 1, 2, or 3. - unreachable!("Invalid value encountered: {:b}", value); - }, - } - } -} - -// CONVERSIONS FROM NOTE TAG -// ================================================================================================ - -impl From for u32 { - fn from(value: NoteTag) -> Self { - value.as_u32() - } -} - -impl From for Felt { - fn from(value: NoteTag) -> Self { - Felt::from(value.as_u32()) - } -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for NoteTag { - fn write_into(&self, target: &mut W) { - self.as_u32().write_into(target); - } -} - -impl Deserializable for NoteTag { - fn read_from(source: &mut R) -> Result { - let tag = u32::read_from(source)?; - Ok(Self::from(tag)) - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - - use assert_matches::assert_matches; - - use super::{NoteExecutionMode, NoteTag}; - use crate::NoteError; - use crate::account::AccountId; - use crate::note::NoteType; - use crate::note::note_tag::{ - LOCAL_ANY, - LOCAL_PUBLIC_ANY, - NETWORK_ACCOUNT, - NETWORK_PUBLIC_USECASE, - }; - use crate::testing::account_id::{ - ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET, - ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, - ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, - ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, - ACCOUNT_ID_PRIVATE_SENDER, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3, - ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, - ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE, - ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2, - ACCOUNT_ID_SENDER, - }; - - #[test] - fn from_account_id() { - let private_accounts = [ - AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(), - AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap(), - AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(), - AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(), - ]; - let public_accounts = [ - AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2) - .unwrap(), - AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(), - AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap(), - AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2).unwrap(), - AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap(), - AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(), - AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1).unwrap(), - ]; - let network_accounts = [ - AccountId::try_from(ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE).unwrap(), - AccountId::try_from(ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET).unwrap(), - AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET).unwrap(), - ]; - - for account_id in network_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Network); - - tag.validate(NoteType::Public) - .expect("network execution should require notes to be public"); - assert_matches!( - tag.validate(NoteType::Private), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Private)) - ); - assert_matches!( - tag.validate(NoteType::Encrypted), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Encrypted)) - ); - } - - for account_id in private_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(!tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Local); - - // for local execution[`NoteExecutionMode::Local`], all notes are allowed - tag.validate(NoteType::Public) - .expect("local execution should support public notes"); - tag.validate(NoteType::Private) - .expect("local execution should support private notes"); - tag.validate(NoteType::Encrypted) - .expect("local execution should support encrypted notes"); - } - - for account_id in public_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(!tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Local); - - // for local execution[`NoteExecutionMode::Local`], all notes are allowed - tag.validate(NoteType::Public) - .expect("local execution should support public notes"); - tag.validate(NoteType::Private) - .expect("local execution should support private notes"); - tag.validate(NoteType::Encrypted) - .expect("local execution should support encrypted notes"); - } - - for account_id in network_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Network); - - // for network execution[`NoteExecutionMode::Network`], only public notes are allowed - tag.validate(NoteType::Public) - .expect("network execution should support public notes"); - assert_matches!( - tag.validate(NoteType::Private), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Private)) - ); - assert_matches!( - tag.validate(NoteType::Encrypted), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Encrypted)) - ); - } - } - - #[test] - fn from_private_account_id() { - /// Private Account ID with the following bit pattern in the first and second byte: - /// 0b11001100_01010101 - /// ^^^^^^^^ ^^^^^^ <- 14 bits of the local tag. - const PRIVATE_ACCOUNT_INT: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE - | 0x0055_0000_0000_0000_0000_0000_0000_0000; - let private_account_id = AccountId::try_from(PRIVATE_ACCOUNT_INT).unwrap(); - - // Expected private tag of variant `NoteTag::LocalAny`. - let expected_private_tag = 0b11110011_00010101_00000000_00000000; - - assert_eq!(NoteTag::from_account_id(private_account_id).as_u32(), expected_private_tag); - } - - #[test] - fn from_public_account_id() { - /// Public Account ID with the following bit pattern in the first and second byte: - /// 0b10101010_01010101 - /// ^^^^^^^^ ^^^^^^ <- 14 bits of the local tag. - const PUBLIC_ACCOUNT_INT: u128 = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE - | 0x0055_ccaa_0000_0000_0000_0000_0000_0000; - let public_account_id = AccountId::try_from(PUBLIC_ACCOUNT_INT).unwrap(); - - // Expected public tag of variant `NoteTag::LocalAny`. - let expected_public_local_tag = 0b11101010_10010101_00000000_00000000u32; - - assert_eq!(NoteTag::from_account_id(public_account_id).as_u32(), expected_public_local_tag); - } - - #[test] - fn from_network_account_id() { - /// Network Account ID with the following bit pattern in the first four bytes: - /// 0b10101010_11001100_01110111_11001100 - /// ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^ <- 30 bits of the network tag. - const NETWORK_ACCOUNT_INT: u128 = ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE - | 0x00cc_77cc_0000_0000_0000_0000_0000_0000; - let network_account_id = AccountId::try_from(NETWORK_ACCOUNT_INT).unwrap(); - - // Expected network tag of variant `NoteTag::NetworkAccount`. - let expected_network_tag = 0b00101010_10110011_00011101_11110011; - - assert_eq!(NoteTag::from_account_id(network_account_id).as_u32(), expected_network_tag); - } - - #[test] - fn for_public_use_case() { - // NETWORK - // ---------------------------------------------------------------------------------------- - let tag = NoteTag::for_public_use_case(0b0, 0b0, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01000000_00000000_00000000_00000000u32); - - tag.validate(NoteType::Public).unwrap(); - - assert_matches!( - tag.validate(NoteType::Private).unwrap_err(), - NoteError::NetworkExecutionRequiresPublicNote(NoteType::Private) - ); - assert_matches!( - tag.validate(NoteType::Encrypted).unwrap_err(), - NoteError::NetworkExecutionRequiresPublicNote(NoteType::Encrypted) - ); - - let tag = NoteTag::for_public_use_case(0b1, 0b0, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01000000_00000001_00000000_00000000u32); - - let tag = NoteTag::for_public_use_case(0b0, 0b1, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01000000_00000000_00000000_00000001u32); - - let tag = NoteTag::for_public_use_case(1 << 13, 0b0, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01100000_00000000_00000000_00000000u32); - - // LOCAL - // ---------------------------------------------------------------------------------------- - let tag = NoteTag::for_public_use_case(0b0, 0b0, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10000000_00000000_00000000_00000000u32); - - tag.validate(NoteType::Public).unwrap(); - assert_matches!( - tag.validate(NoteType::Private).unwrap_err(), - NoteError::PublicNoteRequired(NoteType::Private) - ); - assert_matches!( - tag.validate(NoteType::Encrypted).unwrap_err(), - NoteError::PublicNoteRequired(NoteType::Encrypted) - ); - - let tag = NoteTag::for_public_use_case(0b0, 0b1, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10000000_00000000_00000000_00000001u32); - - let tag = NoteTag::for_public_use_case(0b1, 0b0, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10000000_00000001_00000000_00000000u32); - - let tag = NoteTag::for_public_use_case(1 << 13, 0b0, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10100000_00000000_00000000_00000000u32); - - assert_matches!( - NoteTag::for_public_use_case(1 << 15, 0b0, NoteExecutionMode::Local).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 15 - ); - assert_matches!( - NoteTag::for_public_use_case(1 << 14, 0b0, NoteExecutionMode::Local).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 14 - ); - } - - #[test] - fn for_private_use_case() { - let tag = NoteTag::for_local_use_case(0b0, 0b0).unwrap(); - assert_eq!( - tag.as_u32() >> 30, - LOCAL_ANY >> 30, - "local use case prefix should be local any" - ); - assert_eq!(tag.as_u32(), 0b11000000_00000000_00000000_00000000u32); - - tag.validate(NoteType::Public) - .expect("local execution should support public notes"); - tag.validate(NoteType::Private) - .expect("local execution should support private notes"); - tag.validate(NoteType::Encrypted) - .expect("local execution should support encrypted notes"); - - let tag = NoteTag::for_local_use_case(0b0, 0b1).unwrap(); - assert_eq!(tag.as_u32(), 0b11000000_00000000_00000000_00000001u32); - - let tag = NoteTag::for_local_use_case(0b1, 0b0).unwrap(); - assert_eq!(tag.as_u32(), 0b11000000_00000001_00000000_00000000u32); - - let tag = NoteTag::for_local_use_case(1 << 13, 0b0).unwrap(); - assert_eq!(tag.as_u32(), 0b11100000_00000000_00000000_00000000u32); - - assert_matches!( - NoteTag::for_local_use_case(1 << 15, 0b0).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 15 - ); - assert_matches!( - NoteTag::for_local_use_case(1 << 14, 0b0).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 14 - ); - } - - /// Tests that as_u32 returns the correct prefix independent of the inner value. - #[test] - fn note_tag_as_u32() { - const HIGH_BITS_MASK: u32 = 0xc000_0000; - - assert_eq!(NoteTag::NetworkAccount(u32::MAX).as_u32() & HIGH_BITS_MASK, NETWORK_ACCOUNT); - assert_eq!( - NoteTag::NetworkUseCase(u16::MAX, u16::MAX).as_u32() & HIGH_BITS_MASK, - NETWORK_PUBLIC_USECASE - ); - assert_eq!(NoteTag::LocalPublicAny(u32::MAX).as_u32() & HIGH_BITS_MASK, LOCAL_PUBLIC_ANY); - assert_eq!(NoteTag::LocalAny(0).as_u32() & HIGH_BITS_MASK, LOCAL_ANY); - } -} diff --git a/crates/miden-objects/src/testing/storage.rs b/crates/miden-objects/src/testing/storage.rs deleted file mode 100644 index 85f8f673f5..0000000000 --- a/crates/miden-objects/src/testing/storage.rs +++ /dev/null @@ -1,146 +0,0 @@ -use alloc::collections::BTreeMap; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; - -use miden_core::{Felt, Word}; -use miden_crypto::EMPTY_WORD; - -use crate::AccountDeltaError; -use crate::account::{ - AccountStorage, - AccountStorageDelta, - StorageMap, - StorageMapDelta, - StorageSlot, -}; -use crate::note::NoteAssets; - -// ACCOUNT STORAGE DELTA BUILDER -// ================================================================================================ - -#[derive(Clone, Debug, Default)] -pub struct AccountStorageDeltaBuilder { - values: BTreeMap, - maps: BTreeMap, -} - -impl AccountStorageDeltaBuilder { - // CONSTRUCTORS - // ------------------------------------------------------------------------------------------- - - pub fn new() -> Self { - Self { - values: BTreeMap::new(), - maps: BTreeMap::new(), - } - } - - // MODIFIERS - // ------------------------------------------------------------------------------------------- - - pub fn add_cleared_items(mut self, items: impl IntoIterator) -> Self { - self.values.extend(items.into_iter().map(|slot| (slot, EMPTY_WORD))); - self - } - - pub fn add_updated_values(mut self, items: impl IntoIterator) -> Self { - self.values.extend(items); - self - } - - pub fn add_updated_maps( - mut self, - items: impl IntoIterator, - ) -> Self { - self.maps.extend(items); - self - } - - // BUILDERS - // ------------------------------------------------------------------------------------------- - - pub fn build(self) -> Result { - AccountStorageDelta::from_parts(self.values, self.maps) - } -} - -// ACCOUNT STORAGE UTILS -// ================================================================================================ - -pub struct SlotWithIndex { - pub slot: StorageSlot, - pub index: u8, -} - -// CONSTANTS -// ================================================================================================ - -pub const FAUCET_STORAGE_DATA_SLOT: u8 = 0; - -pub const STORAGE_INDEX_0: u8 = 0; -pub const STORAGE_INDEX_1: u8 = 1; -pub const STORAGE_INDEX_2: u8 = 2; - -pub const STORAGE_VALUE_0: Word = - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); -pub const STORAGE_VALUE_1: Word = - Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]); -pub const STORAGE_LEAVES_2: [(Word, Word); 2] = [ - ( - Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), - Word::new([Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)]), - ), - ( - Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]), - Word::new([Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)]), - ), -]; - -impl AccountStorage { - /// Create account storage: - pub fn mock() -> Self { - AccountStorage::new(Self::mock_storage_slots()).unwrap() - } - - pub fn mock_storage_slots() -> Vec { - vec![Self::mock_item_0().slot, Self::mock_item_1().slot, Self::mock_item_2().slot] - } - - pub fn mock_item_0() -> SlotWithIndex { - SlotWithIndex { - slot: StorageSlot::Value(STORAGE_VALUE_0), - index: STORAGE_INDEX_0, - } - } - - pub fn mock_item_1() -> SlotWithIndex { - SlotWithIndex { - slot: StorageSlot::Value(STORAGE_VALUE_1), - index: STORAGE_INDEX_1, - } - } - - pub fn mock_item_2() -> SlotWithIndex { - SlotWithIndex { - slot: StorageSlot::Map(Self::mock_map()), - index: STORAGE_INDEX_2, - } - } - - pub fn mock_map() -> StorageMap { - StorageMap::with_entries(STORAGE_LEAVES_2).unwrap() - } -} - -// UTILITIES -// -------------------------------------------------------------------------------------------- - -/// Returns a list of strings, one for each note asset. -pub fn prepare_assets(note_assets: &NoteAssets) -> Vec { - let mut assets = Vec::new(); - for &asset in note_assets.iter() { - let asset_word = Word::from(asset); - assets.push(asset_word.to_string()); - } - assets -} diff --git a/crates/miden-objects/src/transaction/inputs/mod.rs b/crates/miden-objects/src/transaction/inputs/mod.rs deleted file mode 100644 index aaabfd9081..0000000000 --- a/crates/miden-objects/src/transaction/inputs/mod.rs +++ /dev/null @@ -1,263 +0,0 @@ -use alloc::vec::Vec; -use core::fmt::Debug; - -use miden_core::utils::{Deserializable, Serializable}; - -use super::PartialBlockchain; -use crate::TransactionInputError; -use crate::account::{AccountCode, PartialAccount}; -use crate::block::{BlockHeader, BlockNumber}; -use crate::note::{Note, NoteInclusionProof}; -use crate::transaction::{TransactionArgs, TransactionScript}; - -mod account; -pub use account::AccountInputs; - -mod notes; -use miden_processor::AdviceInputs; -pub use notes::{InputNote, InputNotes, ToInputNoteCommitments}; - -// TRANSACTION INPUTS -// ================================================================================================ - -/// Contains the data required to execute a transaction. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TransactionInputs { - account: PartialAccount, - block_header: BlockHeader, - blockchain: PartialBlockchain, - input_notes: InputNotes, - tx_args: TransactionArgs, - advice_inputs: AdviceInputs, - foreign_account_code: Vec, -} - -impl TransactionInputs { - // CONSTRUCTOR - // -------------------------------------------------------------------------------------------- - - /// Returns new [`TransactionInputs`] instantiated with the specified parameters. - /// - /// # Errors - /// - /// Returns an error if: - /// - The partial blockchain does not track the block headers required to prove inclusion of any - /// authenticated input note. - pub fn new( - account: PartialAccount, - block_header: BlockHeader, - blockchain: PartialBlockchain, - input_notes: InputNotes, - ) -> Result { - // Check that the partial blockchain and block header are consistent. - if blockchain.chain_length() != block_header.block_num() { - return Err(TransactionInputError::InconsistentChainLength { - expected: block_header.block_num(), - actual: blockchain.chain_length(), - }); - } - if blockchain.peaks().hash_peaks() != block_header.chain_commitment() { - return Err(TransactionInputError::InconsistentChainCommitment { - expected: block_header.chain_commitment(), - actual: blockchain.peaks().hash_peaks(), - }); - } - // Validate the authentication paths of the input notes. - for note in input_notes.iter() { - if let InputNote::Authenticated { note, proof } = note { - let note_block_num = proof.location().block_num(); - let block_header = if note_block_num == block_header.block_num() { - &block_header - } else { - blockchain.get_block(note_block_num).ok_or( - TransactionInputError::InputNoteBlockNotInPartialBlockchain(note.id()), - )? - }; - validate_is_in_block(note, proof, block_header)?; - } - } - - Ok(Self { - account, - block_header, - blockchain, - input_notes, - tx_args: TransactionArgs::default(), - advice_inputs: AdviceInputs::default(), - foreign_account_code: Vec::new(), - }) - } - - /// Replaces the transaction inputs and assigns the given foreign account code. - pub fn with_foreign_account_code(mut self, foreign_account_code: Vec) -> Self { - self.foreign_account_code = foreign_account_code; - self - } - - /// Replaces the transaction inputs and assigns the given transaction arguments. - pub fn with_tx_args(mut self, tx_args: TransactionArgs) -> Self { - self.set_tx_args_inner(tx_args); - self - } - - /// Replaces the transaction inputs and assigns the given advice inputs. - pub fn with_advice_inputs(mut self, advice_inputs: AdviceInputs) -> Self { - self.set_advice_inputs(advice_inputs); - self - } - - // MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Replaces the input notes for the transaction. - pub fn set_input_notes(&mut self, new_notes: Vec) { - self.input_notes = new_notes.into(); - } - - /// Replaces the advice inputs for the transaction. - /// - /// Note: the advice stack from the provided advice inputs is discarded. - pub fn set_advice_inputs(&mut self, new_advice_inputs: AdviceInputs) { - let AdviceInputs { map, store, .. } = new_advice_inputs; - self.advice_inputs = AdviceInputs { stack: Default::default(), map, store }; - self.tx_args.extend_advice_inputs(self.advice_inputs.clone()); - } - - /// Updates the transaction arguments of the inputs. - #[cfg(feature = "testing")] - pub fn set_tx_args(&mut self, tx_args: TransactionArgs) { - self.set_tx_args_inner(tx_args); - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the account against which the transaction is executed. - pub fn account(&self) -> &PartialAccount { - &self.account - } - - /// Returns block header for the block referenced by the transaction. - pub fn block_header(&self) -> &BlockHeader { - &self.block_header - } - - /// Returns partial blockchain containing authentication paths for all notes consumed by the - /// transaction. - pub fn blockchain(&self) -> &PartialBlockchain { - &self.blockchain - } - - /// Returns the notes to be consumed in the transaction. - pub fn input_notes(&self) -> &InputNotes { - &self.input_notes - } - - /// Returns the block number referenced by the inputs. - pub fn ref_block(&self) -> BlockNumber { - self.block_header.block_num() - } - - /// Returns the transaction script to be executed. - pub fn tx_script(&self) -> Option<&TransactionScript> { - self.tx_args.tx_script() - } - - /// Returns the foreign account code to be executed. - pub fn foreign_account_code(&self) -> &[AccountCode] { - &self.foreign_account_code - } - - /// Returns the advice inputs to be consumed in the transaction. - pub fn advice_inputs(&self) -> &AdviceInputs { - &self.advice_inputs - } - - /// Returns the transaction arguments to be consumed in the transaction. - pub fn tx_args(&self) -> &TransactionArgs { - &self.tx_args - } - - // CONVERSIONS - // -------------------------------------------------------------------------------------------- - - /// Consumes these transaction inputs and returns their underlying components. - pub fn into_parts( - self, - ) -> ( - PartialAccount, - BlockHeader, - PartialBlockchain, - InputNotes, - TransactionArgs, - ) { - (self.account, self.block_header, self.blockchain, self.input_notes, self.tx_args) - } - - // HELPER METHODS - // -------------------------------------------------------------------------------------------- - - /// Replaces the current tx_args with the provided value. - /// - /// This also appends advice inputs from these transaction inputs to the advice inputs of the - /// tx args. - fn set_tx_args_inner(&mut self, tx_args: TransactionArgs) { - self.tx_args = tx_args; - self.tx_args.extend_advice_inputs(self.advice_inputs.clone()); - } -} - -impl Serializable for TransactionInputs { - fn write_into(&self, target: &mut W) { - self.account.write_into(target); - self.block_header.write_into(target); - self.blockchain.write_into(target); - self.input_notes.write_into(target); - self.tx_args.write_into(target); - self.advice_inputs.write_into(target); - self.foreign_account_code.write_into(target); - } -} - -impl Deserializable for TransactionInputs { - fn read_from( - source: &mut R, - ) -> Result { - let account = PartialAccount::read_from(source)?; - let block_header = BlockHeader::read_from(source)?; - let blockchain = PartialBlockchain::read_from(source)?; - let input_notes = InputNotes::read_from(source)?; - let tx_args = TransactionArgs::read_from(source)?; - let advice_inputs = AdviceInputs::read_from(source)?; - let foreign_account_code = Vec::::read_from(source)?; - - Ok(TransactionInputs { - account, - block_header, - blockchain, - input_notes, - tx_args, - advice_inputs, - foreign_account_code, - }) - } -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Validates whether the provided note belongs to the note tree of the specified block. -fn validate_is_in_block( - note: &Note, - proof: &NoteInclusionProof, - block_header: &BlockHeader, -) -> Result<(), TransactionInputError> { - let note_index = proof.location().node_index_in_block().into(); - let note_commitment = note.commitment(); - proof - .note_path() - .verify(note_index, note_commitment, &block_header.note_root()) - .map_err(|_| { - TransactionInputError::InputNoteNotInBlock(note.id(), proof.location().block_num()) - }) -} diff --git a/crates/miden-protocol-macros/Cargo.toml b/crates/miden-protocol-macros/Cargo.toml new file mode 100644 index 0000000000..0ec8930785 --- /dev/null +++ b/crates/miden-protocol-macros/Cargo.toml @@ -0,0 +1,27 @@ +[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 new file mode 100644 index 0000000000..1b7676ec70 --- /dev/null +++ b/crates/miden-protocol-macros/README.md @@ -0,0 +1,69 @@ +# 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 new file mode 100644 index 0000000000..100ccac410 --- /dev/null +++ b/crates/miden-protocol-macros/src/lib.rs @@ -0,0 +1,171 @@ +//! 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 new file mode 100644 index 0000000000..05a925e4d5 --- /dev/null +++ b/crates/miden-protocol-macros/tests/integration_test.rs @@ -0,0 +1,42 @@ +#[cfg(test)] +mod tests { + use miden_protocol::{Felt, FieldElement, 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-objects/Cargo.toml b/crates/miden-protocol/Cargo.toml similarity index 77% rename from crates/miden-objects/Cargo.toml rename to crates/miden-protocol/Cargo.toml index cce6b3ddc8..ea4b9c629d 100644 --- a/crates/miden-objects/Cargo.toml +++ b/crates/miden-protocol/Cargo.toml @@ -4,9 +4,9 @@ categories = ["no-std"] description = "Core components of the Miden protocol" edition.workspace = true homepage.workspace = true -keywords = ["miden", "objects"] +keywords = ["miden", "protocol"] license.workspace = true -name = "miden-objects" +name = "miden-protocol" readme = "README.md" repository.workspace = true rust-version.workspace = true @@ -31,30 +31,32 @@ std = [ "miden-processor/std", "miden-verifier/std", ] -testing = ["dep:rand_xoshiro", "dep:winter-rand-utils", "miden-air/testing"] +testing = ["dep:rand_chacha", "dep:rand_xoshiro", "dep:winter-rand-utils", "miden-air/testing"] [dependencies] # Miden dependencies miden-assembly = { workspace = true } miden-assembly-syntax = { workspace = true } miden-core = { workspace = true } +miden-core-lib = { workspace = true } miden-crypto = { workspace = true } miden-mast-package = { workspace = true } miden-processor = { workspace = true } -miden-stdlib = { workspace = true } +miden-protocol-macros = { workspace = true } miden-utils-sync = { workspace = true } miden-verifier = { workspace = true } winter-rand-utils = { optional = true, version = "0.13" } # External dependencies bech32 = { default-features = false, features = ["alloc"], version = "0.11" } -log = { optional = true, version = "0.4" } rand = { workspace = true } rand_xoshiro = { default-features = false, optional = true, version = "0.7" } semver = { features = ["serde"], version = "1.0" } serde = { features = ["derive"], optional = true, version = "1.0" } thiserror = { workspace = true } toml = { optional = true, version = "0.9" } +# for SecretKey generation +rand_chacha = { optional = true, workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { features = ["wasm_js"], version = "0.3" } @@ -63,7 +65,7 @@ getrandom = { features = ["wasm_js"], version = "0.3" } anyhow = { features = ["backtrace", "std"], workspace = true } assert_matches = { workspace = true } criterion = { default-features = false, features = ["html_reports"], version = "0.5" } -miden-objects = { features = ["testing"], path = "." } +miden-protocol = { features = ["testing"], path = "." } pprof = { default-features = false, features = ["criterion", "flamegraph"], version = "0.15" } rstest = { workspace = true } tempfile = { version = "3.19" } @@ -71,3 +73,11 @@ winter-air = { version = "0.13" } # for HashFunction/ExecutionProof::new_dummy color-eyre = { version = "0.5" } miden-air = { features = ["std", "testing"], workspace = true } + +[build-dependencies] +fs-err = { version = "3" } +miden-assembly = { workspace = true } +miden-core = { workspace = true } +miden-core-lib = { workspace = true } +regex = { version = "1.11" } +walkdir = { version = "2.5" } diff --git a/crates/miden-objects/README.md b/crates/miden-protocol/README.md similarity index 97% rename from crates/miden-objects/README.md rename to crates/miden-protocol/README.md index bfa78937c5..c731a35159 100644 --- a/crates/miden-objects/README.md +++ b/crates/miden-protocol/README.md @@ -1,6 +1,6 @@ -# Miden Objects +# Miden Protocol -This crates contains core components defining the Miden protocol. +This crates contains core components defining the Miden protocol including the Miden protocol kernels. ## Modules diff --git a/crates/miden-lib/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm similarity index 84% rename from crates/miden-lib/asm/kernels/transaction/api.masm rename to crates/miden-protocol/asm/kernels/transaction/api.masm index 48b764e071..a67556ad67 100644 --- a/crates/miden-lib/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1,12 +1,12 @@ -use.$kernel::account -use.$kernel::account_delta -use.$kernel::account_id -use.$kernel::faucet -use.$kernel::input_note -use.$kernel::memory -use.$kernel::note -use.$kernel::output_note -use.$kernel::tx +use $kernel::account +use $kernel::account_delta +use $kernel::account_id +use $kernel::faucet +use $kernel::input_note +use $kernel::memory +use $kernel::note +use $kernel::output_note +use $kernel::tx # NOTE # ================================================================================================= @@ -20,25 +20,25 @@ use.$kernel::tx # ERRORS # ================================================================================================= -const.ERR_FAUCET_STORAGE_DATA_SLOT_IS_RESERVED="for faucets the FAUCET_STORAGE_DATA_SLOT storage slot is reserved and can not be used with set_account_item" +const ERR_FAUCET_STORAGE_DATA_SLOT_IS_RESERVED="for faucets the FAUCET_STORAGE_DATA_SLOT storage slot is reserved and can not be used with set_account_item" -const.ERR_FAUCET_TOTAL_ISSUANCE_PROC_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_FAUCET="the faucet_get_total_fungible_asset_issuance procedure can only be called on a fungible faucet" +const ERR_FAUCET_TOTAL_ISSUANCE_PROC_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_FAUCET="the faucet_get_total_fungible_asset_issuance procedure can only be called on a fungible faucet" -const.ERR_FAUCET_IS_NF_ASSET_ISSUED_PROC_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET="the faucet_is_non_fungible_asset_issued procedure can only be called on a non-fungible faucet" +const ERR_FAUCET_IS_NF_ASSET_ISSUED_PROC_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET="the faucet_is_non_fungible_asset_issued procedure can only be called on a non-fungible faucet" -const.ERR_KERNEL_PROCEDURE_OFFSET_OUT_OF_BOUNDS="provided kernel procedure offset is out of bounds" +const ERR_KERNEL_PROCEDURE_OFFSET_OUT_OF_BOUNDS="provided kernel procedure offset is out of bounds" -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note assets of active note because no note is currently being processed" +const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note assets of active note because no note is currently being processed" -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_RECIPIENT_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note recipient of active note because no note is currently being processed" +const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_RECIPIENT_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note recipient of active note because no note is currently being processed" -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note metadata of active note because no note is currently being processed" +const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note metadata of active note because no note is currently being processed" -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note inputs of active note because no note is currently being processed" +const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note inputs of active note because no note is currently being processed" -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SCRIPT_ROOT_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note script root of active note because no note is currently being processed" +const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SCRIPT_ROOT_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note script root of active note because no note is currently being processed" -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note serial number of active note because no note is currently being processed" +const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note serial number of active note because no note is currently being processed" # AUTHENTICATION # ================================================================================================= @@ -46,20 +46,20 @@ const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROCESSE #! Authenticates that the invocation of a kernel procedure originates from the account context. #! #! Inputs: [] -#! Outputs: [storage_offset, storage_size] +#! Outputs: [] #! #! Panics if: #! - the invocation of the kernel procedure does not originate from the account context. #! #! Invocation: exec -proc.authenticate_account_origin +proc authenticate_account_origin # get the hash of the caller padw caller # => [CALLER] # assert that the caller is from the user context exec.account::authenticate_and_track_procedure - # => [storage_offset, storage_size] + # => [] end #! Asserts that the invocation of a kernel procedure originates from the authentication procedure of @@ -73,7 +73,7 @@ end #! of the account. #! #! Invocation: exec -proc.assert_auth_procedure_origin +proc assert_auth_procedure_origin # get the hash of the caller padw caller # => [CALLER] @@ -98,7 +98,7 @@ end #! - INIT_COMMITMENT is the initial account commitment. #! #! Invocation: dynexec -export.account_get_initial_commitment +pub proc account_get_initial_commitment # get the initial account commitment exec.account::get_initial_commitment # => [INIT_COMMITMENT, pad(16)] @@ -117,7 +117,7 @@ end #! - ACCOUNT_COMMITMENT is the commitment of the account data. #! #! Invocation: dynexec -export.account_compute_commitment +pub proc account_compute_commitment # compute the active account commitment exec.account::compute_commitment # => [ACCOUNT_COMMITMENT, pad(16)] @@ -148,7 +148,7 @@ end #! - the vault or storage delta is not empty but the nonce increment is zero. #! #! Invocation: dynexec -export.account_compute_delta_commitment +pub proc account_compute_delta_commitment # compute the account delta commitment exec.account_delta::compute_commitment # => [DELTA_COMMITMENT, pad(16)] @@ -169,7 +169,7 @@ end #! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. #! #! Invocation: dynexec -export.account_get_id +pub proc account_get_id # get the native account ID exec.memory::get_native_account_id # => [native_account_id_prefix, native_account_id_suffix, is_native, pad(15)] @@ -215,7 +215,7 @@ end #! - nonce is the active account's nonce. #! #! Invocation: dynexec -export.account_get_nonce +pub proc account_get_nonce # get the account nonce exec.account::get_nonce # => [nonce, pad(16)] @@ -241,7 +241,7 @@ end #! - the nonce has already been incremented. #! #! Invocation: dynexec -export.account_incr_nonce +pub proc account_incr_nonce # check that this procedure was executed against the native account exec.memory::assert_native_account # => [pad(16)] @@ -268,9 +268,9 @@ end #! - CODE_COMMITMENT is the commitment of the account code. #! #! Invocation: dynexec -export.account_get_code_commitment +pub proc account_get_code_commitment # authenticate that the procedure invocation originates from the account context - exec.authenticate_account_origin drop drop + exec.authenticate_account_origin # => [pad(16)] # get the account code commitment @@ -291,7 +291,7 @@ end #! - INIT_STORAGE_COMMITMENT is the initial account storage commitment. #! #! Invocation: dynexec -export.account_get_initial_storage_commitment +pub proc account_get_initial_storage_commitment # get the initial account storage commitment exec.account::get_initial_storage_commitment # => [INIT_STORAGE_COMMITMENT, pad(16)] @@ -310,9 +310,9 @@ end #! - STORAGE_COMMITMENT is the commitment of the account storage. #! #! Invocation: dynexec -export.account_compute_storage_commitment +pub proc account_compute_storage_commitment # authenticate that the procedure invocation originates from the account context - exec.authenticate_account_origin drop drop + exec.authenticate_account_origin # => [pad(16)] # compute the account storage commitment @@ -326,25 +326,22 @@ end #! Gets an item from the account storage. #! -#! Inputs: [index, pad(15)] +#! Inputs: [slot_id_prefix, slot_id_suffix, pad(14)] #! Outputs: [VALUE, pad(12)] #! #! Where: -#! - index is the index of the item to get. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value of the item. #! #! Panics if: -#! - the index is out of bounds. +#! - a slot with the provided slot ID does not exist in account storage. #! #! Invocation: dynexec -export.account_get_item +pub proc account_get_item # authenticate that the procedure invocation originates from the account context exec.authenticate_account_origin - # => [storage_offset, storage_size, index, pad(15)] - - # apply offset to storage slot index - exec.account::apply_storage_offset - # => [index_with_offset, pad(15)] + # => [slot_id_prefix, slot_id_suffix, pad(14)] # fetch the account storage item exec.account::get_item @@ -357,67 +354,66 @@ end #! Sets an item in the account storage. #! -#! Inputs: [index, VALUE, pad(11)] +#! Inputs: [slot_id_prefix, slot_id_suffix, VALUE, pad(10)] #! Outputs: [OLD_VALUE, pad(12)] #! #! Where: -#! - index is the index of the item to set. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value to set. #! - OLD_VALUE is the previous value of the item. #! #! Panics if: -#! - the index is out of bounds. +#! - a slot with the provided slot ID does not exist in account storage. #! - the invocation of this procedure does not originate from the native account. +#! - the native account is a faucet and the provided slot ID points to the reserved faucet storage slot. #! #! Invocation: dynexec -export.account_set_item +pub proc account_set_item # check that this procedure was executed against the native account exec.memory::assert_native_account - # => [index, VALUE, pad(11)] + # => [slot_id_prefix, slot_id_suffix, VALUE, pad(10)] # if the transaction is being executed against a faucet account then assert - # index != FAUCET_STORAGE_DATA_SLOT (reserved slot) - dup exec.account::get_faucet_storage_data_slot eq + # the slot that is being written to is not the reserved faucet slot. + dup.1 dup.1 exec.account::is_faucet_storage_data_slot + # => [is_faucet_storage_data_slot, slot_id_prefix, slot_id_suffix, VALUE, pad(10)] + exec.account::get_id swap drop exec.account_id::is_faucet + # => [is_faucet_account, is_faucet_storage_data_slot, slot_id_prefix, slot_id_suffix, VALUE, pad(10)] + and assertz.err=ERR_FAUCET_STORAGE_DATA_SLOT_IS_RESERVED - # => [index, VALUE, pad(11)] + # => [slot_id_prefix, slot_id_suffix, VALUE, pad(10)] # authenticate that the procedure invocation originates from the account context exec.authenticate_account_origin - # => [storage_offset, storage_size, index, VALUE, pad(11)] - - # apply offset to storage slot index - exec.account::apply_storage_offset - # => [index_with_offset, VALUE, pad(11)] + # => [slot_id_prefix, slot_id_suffix, VALUE, pad(10)] # set the account storage item exec.account::set_item # => [OLD_VALUE, pad(12)] end -#! Returns the VALUE located under the specified KEY within the map contained in the given -#! account storage slot. +#! Returns the VALUE located under the specified KEY within the map contained in the account +#! storage slot identified by the slot ID. #! -#! Inputs: [index, KEY, pad(11)] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY, pad(10)] #! Outputs: [VALUE, pad(12)] #! #! Where: -#! - index is the index of the storage slot that contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value of the map item at KEY. #! #! Panics if: -#! - the index is out of bounds (>255). +#! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. #! #! Invocation: dynexec -export.account_get_map_item +pub proc account_get_map_item # authenticate that the procedure invocation originates from the account context exec.authenticate_account_origin - # => [storage_offset, storage_size, index, KEY, pad(11)] - - # apply offset to storage slot index - exec.account::apply_storage_offset - # => [index_with_offset, KEY, pad(11)] + # => [slot_id_prefix, slot_id_suffix, KEY, pad(10)] # fetch the map item from account storage exec.account::get_map_item @@ -426,25 +422,22 @@ end #! Gets an item from the account storage at its initial state (beginning of transaction). #! -#! Inputs: [index, pad(15)] +#! Inputs: [slot_id_prefix, slot_id_suffix, pad(14)] #! Outputs: [INIT_VALUE, pad(12)] #! #! Where: -#! - index is the index of the item to get. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - INIT_VALUE is the initial value of the item at the beginning of the transaction. #! #! Panics if: -#! - the index is out of bounds. +#! - a slot with the provided slot ID does not exist in account storage. #! #! Invocation: dynexec -export.account_get_initial_item +pub proc account_get_initial_item # authenticate that the procedure invocation originates from the account context exec.authenticate_account_origin - # => [storage_offset, storage_size, index, pad(15)] - - # apply offset to storage slot index - exec.account::apply_storage_offset - # => [index_with_offset, pad(15)] + # => [slot_id_prefix, slot_id_suffix, pad(14)] # fetch the initial account storage item exec.account::get_initial_item @@ -455,29 +448,27 @@ export.account_get_initial_item # => [INIT_VALUE, pad(12)] end -#! Returns the initial VALUE located under the specified KEY within the map contained in the given -#! account storage slot at the beginning of the transaction. +#! Returns the initial VALUE located under the specified KEY within the map contained in the +#! account storage slot identified by the slot ID at the beginning of the transaction. #! -#! Inputs: [index, KEY, pad(11)] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY, pad(10)] #! Outputs: [INIT_VALUE, pad(12)] #! #! Where: -#! - index is the index of the storage slot that contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - the slot must point to the root of the storage map. #! - INIT_VALUE is the initial value of the map item at KEY at the beginning of the transaction. #! #! Panics if: -#! - the index is out of bounds (>255). +#! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. #! #! Invocation: dynexec -export.account_get_initial_map_item +pub proc account_get_initial_map_item # authenticate that the procedure invocation originates from the account context exec.authenticate_account_origin - # => [storage_offset, storage_size, index, KEY, pad(11)] - - # apply offset to storage slot index - exec.account::apply_storage_offset - # => [index_with_offset, KEY, pad(11)] + # => [slot_id_prefix, slot_id_suffix, KEY, pad(10)] # fetch the initial map item from account storage exec.account::get_initial_map_item @@ -487,39 +478,40 @@ end #! Stores NEW_VALUE under the specified KEY within the map contained in the given account storage #! slot. #! -#! Inputs: [index, KEY, NEW_VALUE, pad(7)] -#! Outputs: [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE, pad(6)] +#! Outputs: [OLD_VALUE, pad(12)] #! #! Where: -#! - index is the index of the storage slot which contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - the slot must point to the root of the storage map. #! - NEW_VALUE is the value of the new map item for the respective KEY. #! - OLD_VALUE is the value of the old map item for the respective KEY. #! - KEY is the key of the new item. -#! - OLD_MAP_ROOT is the root of the old map before insertion #! #! Panics if: -#! - the index is out of bounds (>255). +#! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. #! - the procedure is called from a non-account context. #! - the invocation of this procedure does not originate from the native account. #! #! Invocation: dynexec -export.account_set_map_item +pub proc account_set_map_item # check that this procedure was executed against the native account exec.memory::assert_native_account - # => [index, KEY, NEW_VALUE, pad(7)] + # => [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE, pad(6)] # authenticate that the procedure invocation originates from the account context exec.authenticate_account_origin - # => [storage_offset, storage_size, index, KEY, NEW_VALUE, pad(7)] - - # apply offset to storage slot index - exec.account::apply_storage_offset - # => [index_with_offset, KEY, NEW_VALUE, pad(7)] + # => [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE, pad(6)] # set the new map item exec.account::set_map_item - # => [OLD_MAP_ROOT, OLD_VALUE, pad(8)] + # => [OLD_VALUE, pad(16)] + + # truncate the stack + swapw dropw + # => [OLD_VALUE, pad(12)] end #! Returns the vault root of the active account at the beginning of the transaction. @@ -531,7 +523,7 @@ end #! - INIT_VAULT_ROOT is the initial account vault root. #! #! Invocation: dynexec -export.account_get_initial_vault_root +pub proc account_get_initial_vault_root # get the initial account vault root exec.account::get_initial_vault_root # => [INIT_VAULT_ROOT, pad(16)] @@ -550,7 +542,7 @@ end #! - VAULT_ROOT is the root of the account vault. #! #! Invocation: dynexec -export.account_get_vault_root +pub proc account_get_vault_root # fetch the account vault root exec.memory::get_account_vault_root # => [VAULT_ROOT, pad(16)] @@ -580,13 +572,13 @@ end #! - the invocation of this procedure does not originate from the native account. #! #! Invocation: dynexec -export.account_add_asset +pub proc account_add_asset # check that this procedure was executed against the native account exec.memory::assert_native_account # => [ASSET, pad(12)] # authenticate that the procedure invocation originates from the account context - exec.authenticate_account_origin drop drop + exec.authenticate_account_origin # => [ASSET, pad(12)] # add the specified asset to the account vault, emitting the corresponding events @@ -609,13 +601,13 @@ end #! - the invocation of this procedure does not originate from the native account. #! #! Invocation: dynexec -export.account_remove_asset +pub proc account_remove_asset # check that this procedure was executed against the native account exec.memory::assert_native_account # => [ASSET, pad(12)] # authenticate that the procedure invocation originates from the account context - exec.authenticate_account_origin drop drop + exec.authenticate_account_origin # => [ASSET, pad(12)] # remove the specified asset from the account vault, emitting the corresponding events @@ -638,7 +630,7 @@ end #! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: dynexec -export.account_get_balance +pub proc account_get_balance exec.account::get_balance # => [balance, pad(15)] end @@ -658,7 +650,7 @@ end #! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: dynexec -export.account_get_initial_balance +pub proc account_get_initial_balance exec.account::get_initial_balance # => [init_balance, pad(15)] end @@ -677,7 +669,7 @@ end #! - the ASSET is a fungible asset. #! #! Invocation: dynexec -export.account_has_non_fungible_asset +pub proc account_has_non_fungible_asset exec.account::has_non_fungible_asset # => [has_asset, pad(15)] end @@ -695,7 +687,7 @@ end #! - the procedure root is not part of the account code. #! #! Invocation: dynexec -export.account_was_procedure_called +pub proc account_was_procedure_called # check that this procedure was executed against the native account exec.memory::assert_native_account # => [PROC_ROOT, pad(12)] @@ -714,7 +706,7 @@ end #! - num_procedures is the number of procedures in the active account. #! #! Invocation: dynexec -export.account_get_num_procedures +pub proc account_get_num_procedures # get the number of procedures exec.memory::get_num_account_procedures # => [num_procedures, pad(16)] @@ -737,15 +729,13 @@ end #! - the procedure index is out of bounds. #! #! Invocation: dynexec -export.account_get_procedure_root - # get the procedure information - exec.account::get_procedure_info - # => [PROC_ROOT, storage_offset, storage_size, pad(15)] - - swapw dropw - # => [PROC_ROOT, pad(13)] +pub proc account_get_procedure_root + # get the procedure root + exec.account::get_procedure_root + # => [PROC_ROOT, pad(15)] - movup.4 drop + # truncate the stack + movup.4 drop movup.4 drop movup.4 drop # => [PROC_ROOT, pad(12)] end @@ -763,7 +753,7 @@ end #! available on the active account. #! #! Invocation: dynexec -export.account_has_procedure +pub proc account_has_procedure # check if the procedure was called exec.account::has_procedure # => [is_procedure_available, pad(15)] @@ -792,13 +782,13 @@ end #! - if the non-fungible asset being minted already exists. #! #! Invocation: dynexec -export.faucet_mint_asset +pub proc faucet_mint_asset # check that this procedure was executed against the native account exec.memory::assert_native_account # => [ASSET, pad(12)] # authenticate that the procedure invocation originates from the account context - exec.authenticate_account_origin drop drop + exec.authenticate_account_origin # => [ASSET, pad(12)] # mint the asset @@ -827,13 +817,13 @@ end #! transaction via a note or the accounts vault. #! #! Invocation: dynexec -export.faucet_burn_asset +pub proc faucet_burn_asset # check that this procedure was executed against the native account exec.memory::assert_native_account # => [ASSET, pad(12)] # authenticate that the procedure invocation originates from the account context - exec.authenticate_account_origin drop drop + exec.authenticate_account_origin # => [ASSET, pad(12)] # burn the asset @@ -854,7 +844,7 @@ end #! - the transaction is not being executed against a fungible faucet. #! #! Invocation: dynexec -export.faucet_get_total_fungible_asset_issuance +pub proc faucet_get_total_fungible_asset_issuance # assert that we are executing a transaction against a fungible faucet (access checks) exec.account::get_id swap drop exec.account_id::is_fungible_faucet assert.err=ERR_FAUCET_TOTAL_ISSUANCE_PROC_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_FAUCET @@ -884,7 +874,7 @@ end #! - the ASSET is not associated with the faucet the transaction is being executed against. #! #! Invocation: dynexec -export.faucet_is_non_fungible_asset_issued +pub proc faucet_is_non_fungible_asset_issued # assert that we are executing a transaction against a non-fungible faucet (access checks) exec.account::get_id swap drop exec.account_id::is_non_fungible_faucet assert.err=ERR_FAUCET_IS_NF_ASSET_ISSUED_PROC_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET @@ -921,7 +911,7 @@ end #! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_assets_info +pub proc input_note_get_assets_info # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr @@ -959,7 +949,7 @@ end #! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_recipient +pub proc input_note_get_recipient # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr @@ -982,14 +972,15 @@ end #! Returns the metadata of the specified input note. #! #! Inputs: [is_active_note, note_index, pad(14)] -#! Outputs: [METADATA, pad(12)] +#! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] #! #! Where: #! - is_active_note is the boolean flag indicating whether we should return the metadata from #! the active note or from the note with the specified index. #! - note_index is the index of the input note whose metadata should be returned. Notice that if #! is_active_note is 1, note_index is ignored. -#! - METADATA is the metadata of the specified input note. +#! - METADATA_HEADER is the metadata header of the specified input note. +#! - NOTE_ATTACHMENT is the attachment of the specified input note. #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. @@ -997,7 +988,7 @@ end #! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_metadata +pub proc input_note_get_metadata # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr @@ -1008,13 +999,21 @@ export.input_note_get_metadata dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED # => [input_note_ptr, pad(15)] + # make stack truncation at the end of the procedure easier + push.0 swap + # => [input_note_ptr, pad(16)] + # get the metadata - exec.memory::get_input_note_metadata - # => [METADATA, pad(15)] + dup exec.memory::get_input_note_metadata_header + # => [METADATA_HEADER, input_note_ptr, pad(16)] + + # get the attachment + movup.4 exec.memory::get_input_note_attachment + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(16)] # truncate the stack - swapw drop drop drop movdn.4 - # => [METADATA, pad(12)] + swapdw dropw dropw + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] end #! Returns the serial number of the specified input note. @@ -1035,7 +1034,7 @@ end #! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_serial_number +pub proc input_note_get_serial_number # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr @@ -1076,7 +1075,7 @@ end #! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_inputs_info +pub proc input_note_get_inputs_info # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr @@ -1120,7 +1119,7 @@ end #! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_script_root +pub proc input_note_get_script_root # get the input note pointer depending on whether the requested note is current or it was # requested by index. exec.get_requested_note_ptr @@ -1147,22 +1146,20 @@ end #! Creates a new note and returns its index. #! -#! Inputs: [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] +#! Inputs: [tag, note_type, RECIPIENT, pad(10)] #! Outputs: [note_idx, pad(15)] #! #! Where: #! - tag is the tag to be included in the note. -#! - aux is the auxiliary metadata to be included in the note. #! - note_type is the note storage type. -#! - execution_hint is the note execution hint tag and payload. #! - RECIPIENT is the recipient of the note. #! - note_idx is the index of the created note. #! #! Invocation: dynexec -export.output_note_create +pub proc output_note_create # check that this procedure was executed against the native account exec.memory::assert_native_account - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] + # => [tag, note_type, RECIPIENT, pad(10)] exec.output_note::create # => [note_idx, pad(15)] @@ -1181,7 +1178,7 @@ end #! - the procedure is called when the active account is not the native one. #! #! Invocation: dynexec -export.output_note_add_asset +pub proc output_note_add_asset # check that this procedure was executed against the native account exec.memory::assert_native_account # => [note_idx, ASSET, pad(11)] @@ -1190,6 +1187,33 @@ export.output_note_add_asset # => [pad(16)] end +#! Sets the attachment of the note specified by the index. +#! +#! Inputs: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(9)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - note_idx is the index of the note on which the attachment is set. +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - attachment_kind is the kind of the attachment content. +#! - ATTACHMENT is the attachment to be set. +#! +#! Panics if: +#! - the procedure is called when the active account is not the native one. +#! - the note index points to a non-existent output note. +#! - the attachment kind or scheme does not fit into a u32. +#! - the attachment kind is an unknown variant. +#! +#! Invocation: dynexec +pub proc output_note_set_attachment + # check that this procedure was executed against the native account + exec.memory::assert_native_account + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(9)] + + exec.output_note::set_attachment + # => [pad(16)] +end + #! Returns the information about assets in the output note with the specified index. #! #! Inputs: [note_index, pad(15)] @@ -1204,7 +1228,7 @@ end #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: dynexec -export.output_note_get_assets_info +pub proc output_note_get_assets_info # assert that the provided note index is less than the total number of output notes exec.output_note::assert_note_index_in_bounds # => [note_index, pad(15)] @@ -1233,7 +1257,7 @@ end #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: dynexec -export.output_note_get_recipient +pub proc output_note_get_recipient # assert that the provided note index is less than the total number of output notes exec.output_note::assert_note_index_in_bounds # => [note_index, pad(15)] @@ -1254,32 +1278,41 @@ end #! Returns the metadata of the output note with the specified index. #! #! Inputs: [note_index, pad(15)] -#! Outputs: [METADATA, pad(12)] +#! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] #! #! Where: #! - note_index is the index of the output note whose metadata should be returned. -#! - METADATA is the metadata of the output note. +#! - METADATA_HEADER is the metadata header of the specified output note. +#! - NOTE_ATTACHMENT is the attachment of the specified output note. #! #! Panics if: #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: dynexec -export.output_note_get_metadata +pub proc output_note_get_metadata # assert that the provided note index is less than the total number of output notes exec.output_note::assert_note_index_in_bounds # => [note_index, pad(15)] - + # get the note data pointer by the provided index exec.memory::get_output_note_ptr # => [note_ptr, pad(15)] + # make stack truncation at the end of the procedure easier + push.0 swap + # => [note_ptr, pad(16)] + # get the metadata - exec.memory::get_output_note_metadata - # => [METADATA, pad(15)] + dup exec.memory::get_output_note_metadata_header + # => [METADATA_HEADER, note_ptr, pad(16)] + + # get the attachment + movup.4 exec.memory::get_output_note_attachment + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(16)] # truncate the stack - swapw drop drop drop movdn.4 - # => [METADATA, pad(12)] + swapdw dropw dropw + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] end # TRANSACTION @@ -1300,7 +1333,7 @@ end #! - INPUT_NOTES_COMMITMENT is the input notes commitment hash. #! #! Invocation: dynexec -export.tx_get_input_notes_commitment +pub proc tx_get_input_notes_commitment exec.tx::get_input_notes_commitment # => [INPUT_NOTES_COMMITMENT, pad(16)] @@ -1319,7 +1352,7 @@ end #! - OUTPUT_NOTES_COMMITMENT is the output notes commitment. #! #! Invocation: dynexec -export.tx_get_output_notes_commitment +pub proc tx_get_output_notes_commitment # get the output notes commitment exec.tx::get_output_notes_commitment # => [OUTPUT_NOTES_COMMITMENT, pad(16)] @@ -1338,7 +1371,7 @@ end #! - num_input_notes is the total number of input notes consumed by this transaction. #! #! Invocation: dynexec -export.tx_get_num_input_notes +pub proc tx_get_num_input_notes # get the number of input notes exec.tx::get_num_input_notes # => [num_input_notes, pad(16)] @@ -1357,7 +1390,7 @@ end #! - num_output_notes is the number of output notes created in this transaction so far. #! #! Invocation: dynexec -export.tx_get_num_output_notes +pub proc tx_get_num_output_notes # get the number of input notes exec.tx::get_num_output_notes # => [num_output_notes, pad(16)] @@ -1376,7 +1409,7 @@ end #! - BLOCK_COMMITMENT is the commitment of the transaction reference block. #! #! Invocation: dynexec -export.tx_get_block_commitment +pub proc tx_get_block_commitment exec.tx::get_block_commitment # => [BLOCK_COMMITMENT, pad(16)] @@ -1394,7 +1427,7 @@ end #! - num is the transaction reference block number. #! #! Invocation: dynexec -export.tx_get_block_number +pub proc tx_get_block_number # get the block number exec.tx::get_block_number # => [num, pad(16)] @@ -1411,7 +1444,7 @@ end #! #! Where: #! - timestamp is the timestamp of the reference block for this transaction. -export.tx_get_block_timestamp +pub proc tx_get_block_timestamp # get the transaction reference block timestamp exec.tx::get_block_timestamp # => [timestamp, pad(16)] @@ -1449,14 +1482,13 @@ end #! - STORAGE_SLOT_DATA is the data contained in the storage slot which is constructed as follows: #! [SLOT_VALUE, slot_type, 0, 0, 0]. #! - CODE_COMMITMENT is the commitment of the foreign account's code. -#! - ACCOUNT_PROCEDURE_DATA is the information about account procedure which is constructed as -#! follows: [PROCEDURE_MAST_ROOT, storage_offset, 0, 0, 0]. +#! - ACCOUNT_PROCEDURE_DATA are the roots of the public procedures of the foreign account. #! #! Panics if: #! - foreign context is created against the native account. #! #! Invocation: dynexec -export.tx_start_foreign_context +pub proc tx_start_foreign_context # get the memory address and a flag whether this account was already loaded. exec.account::get_account_data_ptr # OS => [was_loaded, ptr, foreign_account_id_prefix, foreign_account_id_suffix, pad(14)] @@ -1489,7 +1521,7 @@ end #! - the active account is the native account. #! #! Invocation: dynexec -export.tx_end_foreign_context +pub proc tx_end_foreign_context exec.memory::pop_ptr_from_account_stack # => [pad(16)] end @@ -1508,7 +1540,7 @@ end #! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). #! #! Invocation: dynexec -export.tx_update_expiration_block_delta +pub proc tx_update_expiration_block_delta exec.tx::update_expiration_block_delta # => [pad(16)] end @@ -1522,7 +1554,7 @@ end #! - block_height_delta is the stored expiration time delta (1 to 0xFFFF). #! #! Invocation: dynexec -export.tx_get_expiration_delta +pub proc tx_get_expiration_delta exec.tx::get_expiration_delta # => [block_height_delta, pad(16)] @@ -1548,7 +1580,7 @@ end #! - the provided procedure offset exceeds the number of kernel procedures. #! #! Invocation: syscall -export.exec_kernel_proc +pub proc exec_kernel_proc # check that the provided procedure offset is within expected bounds dup exec.memory::get_num_kernel_procedures lt assert.err=ERR_KERNEL_PROCEDURE_OFFSET_OUT_OF_BOUNDS @@ -1584,7 +1616,7 @@ end #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. -proc.get_requested_note_ptr +proc get_requested_note_ptr # get the memory pointer to the note with the specified index and verify it is valid swap exec.input_note::get_input_note_ptr swap # => [is_active_note, indexed_input_note_ptr] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm similarity index 63% rename from crates/miden-lib/asm/kernels/transaction/lib/account.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/account.masm index d4f6b69e42..7c2458f0ba 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -1,168 +1,183 @@ -use.$kernel::account_delta -use.$kernel::account_id -use.$kernel::asset_vault -use.$kernel::constants -use.$kernel::memory - -use.std::collections::smt -use.std::crypto::hashes::rpo -use.std::mem -use.std::word +use $kernel::account_delta +use $kernel::account_id +use $kernel::asset_vault +use $kernel::constants::ACCOUNT_PROCEDURE_DATA_LENGTH +use $kernel::constants::EMPTY_SMT_ROOT +use $kernel::constants::STORAGE_SLOT_TYPE_MAP +use $kernel::constants::STORAGE_SLOT_TYPE_VALUE +use $kernel::memory +use miden::core::collections::smt +use miden::core::collections::sorted_array +use miden::core::crypto::hashes::rpo256 +use miden::core::mem +use miden::core::word # ERRORS # ================================================================================================= -const.ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE="account nonce can only be incremented once" +const ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE="account nonce can only be incremented once" -const.ERR_ACCOUNT_NONCE_AT_MAX="account nonce is already at its maximum possible value" +const ERR_ACCOUNT_NONCE_AT_MAX="account nonce is already at its maximum possible value" -const.ERR_ACCOUNT_CODE_IS_NOT_UPDATABLE="account code must be updatable for it to be possible to set new code" +const ERR_ACCOUNT_CODE_IS_NOT_UPDATABLE="account code must be updatable for it to be possible to set new code" -const.ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH="ID of the new account does not match the ID computed from the seed and commitments" +const ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH="ID of the new account does not match the ID computed from the seed and commitments" -const.ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT="failed to write an account value item to a non-value storage slot" +const ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT="failed to write an account value item to a non-value storage slot" -const.ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT="failed to write an account map item to a non-map storage slot" +const ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT="failed to write an account map item to a non-map storage slot" -const.ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE="account procedure is not part of the account code" +const ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE="procedure is not part of the account code" -const.ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS="provided procedure index is out of bounds" +const ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS="provided procedure index is out of bounds" -const.ERR_ACCOUNT_PROC_NOT_AUTH_PROC="account procedure is not the authentication procedure; some procedures (e.g. `incr_nonce`) can be called only from the authentication procedure" +const ERR_ACCOUNT_PROC_NOT_AUTH_PROC="account procedure is not the authentication procedure; some procedures (e.g. `incr_nonce`) can be called only from the authentication procedure" -const.ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS="provided storage slot index is out of bounds" +const ERR_ACCOUNT_STORAGE_SLOTS_MUST_BE_SORTED_AND_UNIQUE="slot IDs must be unique and sorted in ascending order" -const.ERR_FAUCET_INVALID_STORAGE_OFFSET="storage offset is invalid for a faucet account (0 is prohibited as it is the reserved data slot for faucets)" +const ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME="storage slot with the provided name does not exist" -const.ERR_ACCOUNT_CODE_COMMITMENT_MISMATCH="computed account code commitment does not match recorded account code commitment" +const ERR_ACCOUNT_CODE_COMMITMENT_MISMATCH="computed account code commitment does not match recorded account code commitment" -const.ERR_ACCOUNT_TOO_MANY_PROCEDURES="number of account procedures exceeds the maximum limit of 256" +const ERR_ACCOUNT_NOT_ENOUGH_PROCEDURES="number of account procedures must be at least 2" -const.ERR_ACCOUNT_TOO_MANY_STORAGE_SLOTS="number of account storage slots exceeds the maximum limit of 255" +const ERR_ACCOUNT_TOO_MANY_PROCEDURES="number of account procedures exceeds the maximum limit of 256" -const.ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH="computed account storage commitment does not match recorded account storage commitment" +const ERR_ACCOUNT_TOO_MANY_STORAGE_SLOTS="number of account storage slots exceeds the maximum limit of 255" -const.ERR_ACCOUNT_STORAGE_MAP_ENTRIES_DO_NOT_MATCH_MAP_ROOT="storage map entries provided as advice inputs do not have the same storage map root as the root of the map the new account commits to" +const ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH="computed account storage commitment does not match recorded account storage commitment" -const.ERR_ACCOUNT_INVALID_STORAGE_OFFSET_FOR_SIZE="storage size can only be zero if storage offset is also zero" +const ERR_ACCOUNT_STORAGE_MAP_ENTRIES_DO_NOT_MATCH_MAP_ROOT="storage map entries provided as advice inputs do not have the same storage map root as the root of the map the new account commits to" -const.ERR_FOREIGN_ACCOUNT_ID_IS_ZERO="ID of the provided foreign account equals zero" +const ERR_FOREIGN_ACCOUNT_ID_IS_ZERO="ID of the provided foreign account equals zero" -const.ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED="maximum allowed number of foreign account to be loaded (64) was exceeded" +const ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED="maximum allowed number of foreign account to be loaded (64) was exceeded" -const.ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT="commitment of the foreign account in the advice provider does not match the commitment in the account tree" +const ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT="commitment of the foreign account in the advice provider does not match the commitment in the account tree" -const.ERR_ACCOUNT_ID_UNKNOWN_VERSION="unknown version in account ID" +const ERR_ACCOUNT_ID_UNKNOWN_VERSION="unknown version in account ID" -const.ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE="unknown account storage mode in account ID" +const ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE="unknown account storage mode in account ID" -const.ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT="failed to read an account map item from a non-map storage slot" +const ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT="failed to read an account map item from a non-map storage slot" # CONSTANTS # ================================================================================================= -# The account storage slot at which faucet data is stored. +# The name of the account storage slot at which faucet data is stored. # Fungible faucet: The faucet data consists of [0, 0, 0, total_issuance] # Non-fungible faucet: The faucet data consists of SMT root containing minted non-fungible assets. -const.FAUCET_STORAGE_DATA_SLOT=0 +const FAUCET_SYSDATA_SLOT=word("miden::protocol::faucet::sysdata") # The maximum storage slot index -const.MAX_STORAGE_SLOT_INDEX=254 +const MAX_STORAGE_SLOT_INDEX=254 # The maximum number of account storage slots. -const.MAX_NUM_STORAGE_SLOTS=MAX_STORAGE_SLOT_INDEX+1 +const MAX_NUM_STORAGE_SLOTS=MAX_STORAGE_SLOT_INDEX+1 + +# The minimum number of account interface procedures. +const MIN_NUM_PROCEDURES=2 # The maximum number of account interface procedures. -const.MAX_NUM_PROCEDURES=256 +const MAX_NUM_PROCEDURES=256 # Given the least significant 32 bits of an account ID's prefix, this mask defines the bits used # to determine the account version. -const.ACCOUNT_VERSION_MASK_U32=0x0f # 0b1111 +const ACCOUNT_VERSION_MASK_U32=0x0f # 0b1111 # Given the least significant 32 bits of an account ID's prefix, this mask defines the bits used # to determine the account type. -const.ACCOUNT_ID_TYPE_MASK_U32=0x30 # 0b11_0000 +const ACCOUNT_ID_TYPE_MASK_U32=0x30 # 0b11_0000 # Given the least significant 32 bits of an account ID's first felt, this mask defines the bits used # to determine the account storage mode. -const.ACCOUNT_ID_STORAGE_MODE_MASK_U32=0xC0 # 0b1100_0000 +const ACCOUNT_ID_STORAGE_MODE_MASK_U32=0xC0 # 0b1100_0000 # Given the least significant 32 bits of an account ID's first felt with the storage mode mask # applied, this value defines the public storage mode. -const.ACCOUNT_ID_STORAGE_MODE_PUBLIC_U32=0 # 0b0000_0000 +const ACCOUNT_ID_STORAGE_MODE_PUBLIC_U32=0 # 0b0000_0000 # Given the least significant 32 bits of an account ID's first felt with the storage mode mask # applied, this value defines the private storage mode. -const.ACCOUNT_ID_STORAGE_MODE_PRIVATE_U32=0x80 # 0b1000_0000 +const ACCOUNT_ID_STORAGE_MODE_PRIVATE_U32=0x80 # 0b1000_0000 # Bit pattern for an account w/ immutable code, after the account type mask has been applied. -const.REGULAR_ACCOUNT_IMMUTABLE_CODE=0 # 0b00_0000 +const REGULAR_ACCOUNT_IMMUTABLE_CODE=0 # 0b00_0000 # Bit pattern for an account w/ updatable code, after the account type mask has been applied. -const.REGULAR_ACCOUNT_UPDATABLE_CODE=0x10 # 0b01_0000 +const REGULAR_ACCOUNT_UPDATABLE_CODE=0x10 # 0b01_0000 # Bit pattern for a fungible faucet w/ immutable code, after the account type mask has been applied. -const.FUNGIBLE_FAUCET_ACCOUNT=0x20 # 0b10_0000 +const FUNGIBLE_FAUCET_ACCOUNT=0x20 # 0b10_0000 # Bit pattern for a non-fungible faucet w/ immutable code, after the account type mask has been # applied. -const.NON_FUNGIBLE_FAUCET_ACCOUNT=0x30 # 0b11_0000 +const NON_FUNGIBLE_FAUCET_ACCOUNT=0x30 # 0b11_0000 # Bit pattern for a faucet account, after the account type mask has been applied. -const.FAUCET_ACCOUNT=0x20 # 0b10_0000 +const FAUCET_ACCOUNT=0x20 # 0b10_0000 # Depth of the account database tree. -const.ACCOUNT_TREE_DEPTH=64 +const ACCOUNT_TREE_DEPTH=64 # The number of field elements it takes to store one account storage slot. -const.ACCOUNT_STORAGE_SLOT_DATA_LENGTH=8 +const ACCOUNT_STORAGE_SLOT_DATA_LENGTH=8 + +# The offset of the slot type in the storage slot. +const ACCOUNT_SLOT_TYPE_OFFSET=1 + +# The offset of the slot's ID suffix in the storage slot. +const ACCOUNT_SLOT_ID_SUFFIX_OFFSET=2 + +# The offset of the slot's ID prefix in the storage slot. +const ACCOUNT_SLOT_ID_PREFIX_OFFSET=3 -# The number of field elements it takes to store one account procedure. -const.ACCOUNT_PROCEDURE_DATA_LENGTH=8 +# The offset of the slot value in the storage slot. +const ACCOUNT_SLOT_VALUE_OFFSET=4 # EVENTS # ================================================================================================= # Event emitted before a foreign account is loaded from the advice inputs. -const.ACCOUNT_BEFORE_FOREIGN_LOAD_EVENT=event("miden::account::before_foreign_load") +const ACCOUNT_BEFORE_FOREIGN_LOAD_EVENT=event("miden::account::before_foreign_load") # Event emitted before an asset is added to the account vault. -const.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT=event("miden::account::vault_before_add_asset") +const ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT=event("miden::account::vault_before_add_asset") # Event emitted after an asset is added to the account vault. -const.ACCOUNT_VAULT_AFTER_ADD_ASSET_EVENT=event("miden::account::vault_after_add_asset") +const ACCOUNT_VAULT_AFTER_ADD_ASSET_EVENT=event("miden::account::vault_after_add_asset") # Event emitted before an asset is removed from the account vault. -const.ACCOUNT_VAULT_BEFORE_REMOVE_ASSET_EVENT=event("miden::account::vault_before_remove_asset") +const ACCOUNT_VAULT_BEFORE_REMOVE_ASSET_EVENT=event("miden::account::vault_before_remove_asset") # Event emitted after an asset is removed from the account vault. -const.ACCOUNT_VAULT_AFTER_REMOVE_ASSET_EVENT=event("miden::account::vault_after_remove_asset") +const ACCOUNT_VAULT_AFTER_REMOVE_ASSET_EVENT=event("miden::account::vault_after_remove_asset") # Event emitted before a fungible asset's balance is fetched from the account vault. -const.ACCOUNT_VAULT_BEFORE_GET_BALANCE_EVENT=event("miden::account::vault_before_get_balance") +const ACCOUNT_VAULT_BEFORE_GET_BALANCE_EVENT=event("miden::account::vault_before_get_balance") # Event emitted before it is checked whether a non-fungible asset exists in the account vault. -const.ACCOUNT_VAULT_BEFORE_HAS_NON_FUNGIBLE_ASSET_EVENT=event("miden::account::vault_before_has_non_fungible_asset") +const ACCOUNT_VAULT_BEFORE_HAS_NON_FUNGIBLE_ASSET_EVENT=event("miden::account::vault_before_has_non_fungible_asset") # Event emitted before an account storage item is updated. -const.ACCOUNT_STORAGE_BEFORE_SET_ITEM_EVENT=event("miden::account::storage_before_set_item") +const ACCOUNT_STORAGE_BEFORE_SET_ITEM_EVENT=event("miden::account::storage_before_set_item") # Event emitted after an account storage item is updated. -const.ACCOUNT_STORAGE_AFTER_SET_ITEM_EVENT=event("miden::account::storage_after_set_item") +const ACCOUNT_STORAGE_AFTER_SET_ITEM_EVENT=event("miden::account::storage_after_set_item") # Event emitted before an account storage map item is accessed. -const.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT=event("miden::account::storage_before_get_map_item") +const ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT=event("miden::account::storage_before_get_map_item") # Event emitted before an account storage map item is updated. -const.ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM_EVENT=event("miden::account::storage_before_set_map_item") +const ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM_EVENT=event("miden::account::storage_before_set_map_item") # Event emitted after an account storage map item is updated. -const.ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM_EVENT=event("miden::account::storage_after_set_map_item") +const ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM_EVENT=event("miden::account::storage_after_set_map_item") # Event emitted before an account nonce is incremented. -const.ACCOUNT_BEFORE_INCREMENT_NONCE_EVENT=event("miden::account::before_increment_nonce") +const ACCOUNT_BEFORE_INCREMENT_NONCE_EVENT=event("miden::account::before_increment_nonce") # Event emitted after an account nonce is incremented. -const.ACCOUNT_AFTER_INCREMENT_NONCE_EVENT=event("miden::account::after_increment_nonce") +const ACCOUNT_AFTER_INCREMENT_NONCE_EVENT=event("miden::account::after_increment_nonce") # Event emitted to push the index of the account procedure at the top of the operand stack onto # the advice stack. -const.ACCOUNT_PUSH_PROCEDURE_INDEX_EVENT=event("miden::account::push_procedure_index") +const ACCOUNT_PUSH_PROCEDURE_INDEX_EVENT=event("miden::account::push_procedure_index") # CONSTANT ACCESSORS # ================================================================================================= @@ -172,12 +187,13 @@ const.ACCOUNT_PUSH_PROCEDURE_INDEX_EVENT=event("miden::account::push_procedure_i #! Non-fungible faucet: The faucet data consists of SMT root containing minted non-fungible assets. #! #! Inputs: [] -#! Outputs: [faucet_storage_data_slot] +#! Outputs: [faucet_slot_id_prefix, faucet_slot_id_suffix] #! #! Where: -#! - faucet_storage_data_slot is the account storage slot at which faucet data is stored. -export.get_faucet_storage_data_slot - push.FAUCET_STORAGE_DATA_SLOT +#! - faucet_slot_id{prefix,suffix} are the prefix and suffix felts of the slot identifier, at which +#! faucet data is stored. +pub proc get_faucet_sysdata_slot_id + push.FAUCET_SYSDATA_SLOT[0..2] end #! Returns the maximum number of account storage slots. @@ -187,7 +203,7 @@ end #! #! Where: #! - max_num_storage_slots is the maximum number of account storage slots. -export.get_max_num_storage_slots +pub proc get_max_num_storage_slots push.MAX_NUM_STORAGE_SLOTS end @@ -198,7 +214,7 @@ end #! #! Where: #! - max_num_procedures is the maximum number of account interface procedures. -export.get_max_num_procedures +pub proc get_max_num_procedures push.MAX_NUM_PROCEDURES end @@ -215,7 +231,7 @@ end #! #! Where: #! - act_acct_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. -export.memory::get_account_id->get_id +pub use memory::get_account_id->get_id #! Returns the nonce of the active account. #! @@ -224,7 +240,7 @@ export.memory::get_account_id->get_id #! #! Where: #! - nonce is the account nonce. -export.memory::get_account_nonce->get_nonce +pub use memory::get_account_nonce->get_nonce #! Increments the account nonce by one and returns the new nonce. #! @@ -235,7 +251,7 @@ export.memory::get_account_nonce->get_nonce #! #! Panics if: #! - the nonce has already been incremented. -export.incr_nonce +pub proc incr_nonce exec.account_delta::was_nonce_incremented # => [was_nonce_incremented] @@ -276,7 +292,7 @@ end #! #! Where: #! - INIT_COMMITMENT is the initial account commitment. -export.get_initial_commitment +pub proc get_initial_commitment # determine whether the active account is native exec.memory::is_native_account # => [is_native_account] @@ -303,7 +319,7 @@ end #! #! Where: #! - ACCOUNT_COMMITMENT is the commitment of the account data. -export.compute_commitment +pub proc compute_commitment # if outdated, recompute the storage commitment and store it in the memory exec.refresh_storage_commitment # => [] @@ -314,11 +330,12 @@ export.compute_commitment # stream account data and compute sequential hash. We perform two `mem_stream` operations # because the account data consists of exactly 4 words. - mem_stream hperm mem_stream hperm + mem_stream exec.rpo256::permute + mem_stream exec.rpo256::permute # => [RATE, RATE, PERM, account_data_ptr'] # extract account commitment - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [ACCOUNT_COMMITMENT, account_data_ptr'] # drop account_data_ptr @@ -335,7 +352,7 @@ end #! #! Where: #! - CODE_COMMITMENT is the commitment of the account code. -export.memory::get_account_code_commitment->get_code_commitment +pub use memory::get_account_code_commitment->get_code_commitment ### STORAGE COMMITMENT ################################################### @@ -346,7 +363,7 @@ export.memory::get_account_code_commitment->get_code_commitment #! #! Where: #! - INIT_ACCOUNT_STORAGE_COMMITMENT is the initial account storage commitment. -export.get_initial_storage_commitment +pub proc get_initial_storage_commitment # Get the storage commitment of the active account. For the foreign account this commitment will # be initial. exec.memory::get_account_storage_commitment @@ -373,7 +390,7 @@ end #! #! Where: #! - STORAGE_COMMITMENT is the commitment of the active account storage. -export.compute_storage_commitment +pub proc compute_storage_commitment # if outdated, recompute the storage commitment and store it in the memory exec.refresh_storage_commitment @@ -391,7 +408,7 @@ end #! #! Where: #! - INIT_ACCOUNT_VAULT_ROOT is the initial account vault root. -export.get_initial_vault_root +pub proc get_initial_vault_root # Get the vault root of the active account. For the foreign account this root will be equal to # the initial one. exec.memory::get_account_vault_root @@ -416,40 +433,77 @@ end #! Gets an item from the account storage. #! -#! Note: -#! - We assume that index has been validated and is within bounds. -#! -#! Inputs: [index] +#! Inputs: [slot_id_prefix, slot_id_suffix] #! Outputs: [VALUE] #! #! Where: -#! - index is the index of the item to get. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value of the item. -export.get_item +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +pub proc get_item # get account storage slots section offset - exec.memory::get_account_storage_slots_section_ptr - # => [acct_storage_slots_section_offset, index] + exec.memory::get_account_active_storage_slots_section_ptr + # => [acct_storage_slots_section_offset, slot_id_prefix, slot_id_suffix] + + exec.find_storage_slot + # => [slot_ptr] # get the item from storage exec.get_item_raw # => [VALUE] end -#! Gets an item from the account storage at its initial state (beginning of transaction). +#! Gets an item and its slot type from the account storage. #! -#! Note: -#! - We assume that index has been validated and is within bounds. +#! Inputs: [slot_id_prefix, slot_id_suffix] +#! Outputs: [VALUE, slot_type] #! -#! Inputs: [index] +#! Where: +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - VALUE is the value of the item. +#! - slot_type is the type of the slot. +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +pub proc get_typed_item + # get account storage slots section offset + exec.memory::get_account_active_storage_slots_section_ptr + # => [acct_storage_slots_section_offset, slot_id_prefix, slot_id_suffix] + + exec.find_storage_slot + # => [slot_ptr] + + dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load + # => [slot_type, slot_ptr] + + # load the value + swap exec.get_item_raw + # => [VALUE, slot_type] +end + +#! Gets an item from the account storage at its initial state (beginning of transaction). +#! +#! Inputs: [slot_id_prefix, slot_id_suffix] #! Outputs: [INIT_VALUE] #! #! Where: -#! - index is the index of the item to get. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - INIT_VALUE is the initial value of the item at the beginning of the transaction. -export.get_initial_item +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +pub proc get_initial_item # get account initial storage slots section offset exec.memory::get_account_initial_storage_slots_ptr - # => [account_initial_storage_slots_ptr, index] + # => [account_initial_storage_slots_ptr, slot_id_prefix, slot_id_suffix] + + exec.find_storage_slot + # => [slot_ptr] # get the item from initial storage exec.get_item_raw @@ -458,70 +512,72 @@ end #! Sets an item in the account storage. #! -#! Note: -#! - We assume that index has been validated and is within bounds. -#! -#! Inputs: [index, VALUE] +#! Inputs: [slot_id_prefix, slot_id_suffix, VALUE] #! Outputs: [OLD_VALUE] #! #! Where: -#! - index is the index of the item to set. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value to set. #! - OLD_VALUE is the previous value of the item. #! #! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. #! - the storage slot type is not value. -export.set_item +pub proc set_item emit.ACCOUNT_STORAGE_BEFORE_SET_ITEM_EVENT - # => [index, VALUE] + # => [slot_id_prefix, slot_id_suffix, VALUE] + + exec.memory::get_account_active_storage_slots_section_ptr + # => [storage_slots_ptr, slot_id_prefix, slot_id_suffix, VALUE] - # get storage slot type - dup exec.get_storage_slot_type - # => [storage_slot_type, index, VALUE] + exec.find_storage_slot + # => [slot_ptr, VALUE] - # check if type == slot - exec.constants::get_storage_slot_type_value eq + # load the slot type + dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load + # => [slot_type, slot_ptr, VALUE] + + # assert slot_type is value + push.STORAGE_SLOT_TYPE_VALUE eq assert.err=ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT - # => [index, VALUE] + # => [slot_ptr, VALUE] - # duplicate the index and the VALUE enabling emission of an - # event after an account storage item is being updated - movdn.4 dupw dup.8 - # => [index, VALUE, VALUE, index] + movdn.4 + # => [VALUE, slot_ptr] - # set VALUE in the storage slot - exec.set_item_raw - # => [OLD_VALUE, VALUE, index] + # load the old value of the slot + dup.4 exec.get_item_raw + # => [OLD_VALUE, VALUE, slot_ptr] - # emit event to signal that an account storage item is being updated swapw movup.8 - emit.ACCOUNT_STORAGE_AFTER_SET_ITEM_EVENT drop dropw + # => [slot_ptr, VALUE, OLD_VALUE] + + emit.ACCOUNT_STORAGE_AFTER_SET_ITEM_EVENT + # => [slot_ptr, VALUE, OLD_VALUE] + + # store the new value into the slot + exec.set_item_raw # => [OLD_VALUE] end -#! Returns the VALUE located under the specified KEY within the map contained in the given -#! account storage slot. +#! Returns the VALUE located under the specified KEY within the map contained in the account +#! storage slot identified by the slot ID. #! -#! Inputs: [index, KEY] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY] #! Outputs: [VALUE] #! -#! Note: -#! - We assume that index has been validated and is within bounds. -#! #! Where: -#! - index is the index of the storage slot that contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value of the map item at KEY. #! #! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. -export.get_map_item - # duplicate index for later use - dup movdn.5 - # => [index, KEY, index] - - # fetch the account storage item, which is ROOT of the map - exec.get_item swapw - # => [KEY, ROOT, index] +pub proc get_map_item + exec.memory::get_account_active_storage_slots_section_ptr + # => [storage_slots_ptr, slot_id_prefix, slot_id_suffix, KEY] exec.get_map_item_raw end @@ -529,138 +585,65 @@ end #! Returns the VALUE located under the specified KEY within the map contained in the given #! account storage slot at its initial state (beginning of transaction). #! -#! Inputs: [index, KEY] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY] #! Outputs: [INIT_VALUE] #! -#! Note: -#! - We assume that index has been validated and is within bounds. -#! #! Where: -#! - index is the index of the storage slot that contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - INIT_VALUE is the initial value of the map item at KEY at the beginning of the transaction. #! #! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. -export.get_initial_map_item - # duplicate index for later use - dup movdn.5 - # => [index, KEY, index] - - # fetch the initial account storage item, which is ROOT of the map - exec.get_initial_item swapw - # => [KEY, INIT_ROOT, index] +pub proc get_initial_map_item + exec.memory::get_account_initial_storage_slots_ptr + # => [initial_storage_slots_ptr, slot_id_prefix, slot_id_suffix, KEY] exec.get_map_item_raw end #! Stores NEW_VALUE under the specified KEY within the map contained in the given account storage slot. #! -#! Note: -#! - We assume that index has been validated and is within bounds. -#! -#! Inputs: [index, KEY, NEW_VALUE] -#! Outputs: [OLD_MAP_ROOT, OLD_MAP_VALUE] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE] +#! Outputs: [OLD_VALUE] #! #! Where: -#! - index is the index of the storage slot which contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - the slot must point to the root of the storage map. #! - NEW_VALUE is the value to set under KEY. #! - KEY is the key to set. -#! - OLD_MAP_VALUE is the previous value of the item. -#! - OLD_MAP_ROOT is the root of the old map before insertion +#! - OLD_VALUE is the previous value of the item. #! #! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. #! - the storage slot type is not map. -#! - no map is found for ROOT. -export.set_map_item.12 - # store index for later - dup loc_store.0 exec.get_item - # => [OLD_ROOT, KEY, NEW_VALUE, ...] +#! - no map with the root of the slot is found. +pub proc set_map_item + exec.memory::get_account_active_storage_slots_section_ptr + # => [storage_slots_ptr, slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE] - movdnw.2 loc_load.0 - # => [index, KEY, NEW_VALUE, OLD_ROOT, ...] - - emit.ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM_EVENT - # => [index, KEY, NEW_VALUE, OLD_ROOT, ...] + # resolve the slot name to its pointer + exec.find_storage_slot + # => [slot_ptr, KEY, NEW_VALUE] - # check if storage type is map - exec.get_storage_slot_type - # => [slot_type, KEY, NEW_VALUE, OLD_ROOT] + # load the slot type + dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load + # => [slot_type, slot_ptr, KEY, NEW_VALUE] - # check if slot_type == map - exec.constants::get_storage_slot_type_map eq + # assert slot_type is map + push.STORAGE_SLOT_TYPE_MAP eq assert.err=ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT - # => [KEY, NEW_VALUE, OLD_ROOT] - - # duplicate the original KEY and the NEW_VALUE to be able to emit an event after the - # account storage item was updated - movupw.2 dupw.2 dupw.2 - # => [KEY, NEW_VALUE, OLD_ROOT, KEY, NEW_VALUE] - - # see hash_map_key's docs for why this is done - exec.hash_map_key - # => [HASHED_KEY, NEW_VALUE, OLD_ROOT, KEY, NEW_VALUE] - - # set the NEW_VALUE under HASHED_KEY in the tree - # note smt::set expects the stack to be [NEW_VALUE, HASHED_KEY, OLD_ROOT] - swapw exec.smt::set - # => [OLD_MAP_VALUE, NEW_ROOT, KEY, NEW_VALUE] - - # store OLD_MAP_VALUE and NEW_ROOT until the end of the procedure - loc_storew_be.4 dropw loc_storew_be.8 dropw - # => [KEY, NEW_VALUE] - - dupw.1 - # => [NEW_VALUE, KEY, NEW_VALUE] - - padw loc_loadw_be.4 - # => [OLD_MAP_VALUE, NEW_VALUE, KEY, NEW_VALUE] - - dupw.2 - # => [KEY, OLD_MAP_VALUE, NEW_VALUE, KEY, NEW_VALUE] - - # emit event to signal that an account storage item is being updated - loc_load.0 - # => [index, KEY, OLD_MAP_VALUE, NEW_VALUE, KEY, NEW_VALUE] - - emit.ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM_EVENT - # => [index, KEY, OLD_MAP_VALUE, NEW_VALUE, KEY, NEW_VALUE] - - exec.account_delta::set_map_item - # => [KEY, NEW_VALUE] - - # load OLD_MAP_VALUE and NEW_ROOT on the top of the stack - loc_loadw_be.8 swapw loc_loadw_be.4 swapw - # => [NEW_ROOT, OLD_MAP_VALUE, ...] - - # set the root of the map in the respective account storage slot - loc_load.0 exec.set_item_raw - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, ...] + # => [slot_ptr, KEY, NEW_VALUE] + + exec.set_map_item_raw + # => [OLD_VALUE] end -#! Applies storage offset to provided storage slot index for storage access. +#! Returns the type of the storage slot at the provided index. #! -#! Inputs: [storage_offset, storage_size, slot_index] -#! Outputs: [offset_slot_index] -#! -#! Where: -#! - storage_offset is the offset of the storage for this account component. -#! - storage_size is the number of storage slots accessible from this account component. -#! - slot_index is the index of the storage slot to be accessed. -#! - offset_slot_index is the final index of the storage slot with the storage offset applied to it. -#! -#! Panics if: -#! - the computed index is out of bounds -export.apply_storage_offset - # offset index - dup movup.3 add - # => [offset_slot_index, storage_offset, storage_size] - - # verify that slot_index is in bounds - movdn.2 add dup.1 gt assert.err=ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS - # => [offset_slot_index] -end - -#! Returns the type of the requested storage slot. +#! WARNING: The index must be in bounds. #! #! Inputs: [index] #! Outputs: [slot_type] @@ -668,18 +651,18 @@ end #! Where: #! - index is the location in memory of the storage slot. #! - slot_type is the type of the storage slot. -#! -#! Panics if: -#! - the slot index is out of bounds. -export.get_storage_slot_type - # check that index is in bounds - dup exec.memory::get_num_storage_slots lt assert.err=ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS - # => [index] - - exec.memory::get_account_storage_slots_section_ptr - # => [curr_account_storage_slots_section_ptr, index] - - exec.memory::get_storage_slot_type +pub proc get_storage_slot_type + # convert the index into a memory offset + mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH + # => [offset] + + # compute storage slot ptr + exec.memory::get_native_account_active_storage_slots_ptr + add + # => [slot_ptr] + + # load the slot type + add.ACCOUNT_SLOT_TYPE_OFFSET mem_load # => [slot_type] end @@ -703,7 +686,7 @@ end #! - the total value of the fungible asset is greater than or equal to 2^63 after the new asset was #! added. #! - the vault already contains the same non-fungible asset. -export.add_asset_to_vault +pub proc add_asset_to_vault # duplicate the ASSET to be able to emit an event after an asset is being added dupw # => [ASSET, ASSET] @@ -743,7 +726,7 @@ end #! - the fungible asset is not found in the vault. #! - the amount of the fungible asset in the vault is less than the amount to be removed. #! - the non-fungible asset is not found in the vault. -export.remove_asset_from_vault +pub proc remove_asset_from_vault # fetch the vault root exec.memory::get_account_vault_root_ptr movdn.4 # => [ASSET, acct_vault_root_ptr] @@ -776,7 +759,7 @@ end #! #! Panics if: #! - the provided faucet ID is not an ID of a fungible faucet. -export.get_balance +pub proc get_balance # get the vault root exec.memory::get_account_vault_root_ptr movdn.2 # => [faucet_id_prefix, faucet_id_suffix, vault_root_ptr] @@ -803,7 +786,7 @@ end #! #! Panics if: #! - the provided faucet ID is not an ID of a fungible faucet. -export.get_initial_balance +pub proc get_initial_balance # get the vault root associated with the initial vault root of the native account exec.memory::get_account_initial_vault_root_ptr movdn.2 # => [faucet_id_prefix, faucet_id_suffix, init_native_vault_root_ptr] @@ -829,7 +812,7 @@ end #! #! Panics if: #! - the ASSET is a fungible asset. -export.has_non_fungible_asset +pub proc has_non_fungible_asset # get the vault root exec.memory::get_account_vault_root_ptr movdn.4 # => [ASSET, vault_root_ptr] @@ -846,161 +829,32 @@ end # CODE # ------------------------------------------------------------------------------------------------- -#! Validates all account procedure's storage metadata. -#! -#! Inputs: [] -#! Outputs: [] -#! -#! Panics if: -#! - Storage offset + storage size > number of storage slots. -#! - Storage size is zero and storage offset is non-zero. -#! - This is validated to ensure users do not accidentally set a non-zero offset with a -#! zero size which would prevent any access to storage. -#! - The storage offset of a faucet account's procedure is 0 with a size != 0. -#! - This prevents access to the reserved storage slot. -export.validate_procedure_metadata - # get number of account procedures and number of storage slots - exec.memory::get_num_account_procedures exec.memory::get_num_storage_slots - # => [num_storage_slots, num_account_procedures] - - # prepare stack for looping - push.0.1 - # => [start_loop, index, num_storage_slots, num_account_procedures] - - # check if the account is a faucet - exec.get_id swap drop exec.account_id::is_faucet - # => [is_faucet, start_loop, index, num_storage_slots, num_account_procedures] - - # we do not check if num_account_procedures == 0 here because a valid - # account has between 1 and 256 procedures with associated offsets - if.true - # This branch handles procedures from faucet accounts. - while.true - # get storage offset and size from memory - dup exec.get_procedure_metadata - # => [storage_offset, storage_size, index, num_storage_slots, num_account_procedures] - - # Procedures that do not access storage are defined with (offset, size) = (0, 0). - # But we want to fail on tuples defined with a zero size but non-zero offset, since that - # is a logic error. - # We assert this with: (size == 0 && offset != 0) == 0. - dup.1 eq.0 dup.1 eq.0 not and assertz.err=ERR_ACCOUNT_INVALID_STORAGE_OFFSET_FOR_SIZE - # => [storage_offset, storage_size, index, num_storage_slots, num_account_procedures] - - # No procedure should access the reserved faucet slot (slot 0). However (0, 0) should - # still be allowed per the above. - # We assert this with: (offset == 0 && size != 0) == 0. - dup.1 eq.0 not dup.1 eq.0 and assertz.err=ERR_FAUCET_INVALID_STORAGE_OFFSET - # => [storage_offset, storage_size, index, num_storage_slots, num_account_procedures] - - # assert that storage limit is in bounds - add dup.2 lte assert.err=ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS - # => [index, num_storage_slots, num_account_procedures] - - # check if we should continue looping - add.1 dup dup.3 lt - # => [should_loop, index, num_storage_slots, num_account_procedures] - end - else - # This branch handles procedures from regular accounts. - while.true - # get storage offset and size from memory - dup exec.get_procedure_metadata - # => [storage_offset, storage_size, index, num_storage_slots, num_account_procedures] - - # Procedures that do not access storage are defined with (offset, size) = (0, 0). - # But we want to fail on tuples defined with a zero size but non-zero offset, since that - # is a logic error. - # We assert this with: (size == 0 && offset != 0) == 0. - dup.1 eq.0 dup.1 eq.0 not and assertz.err=ERR_ACCOUNT_INVALID_STORAGE_OFFSET_FOR_SIZE - # => [storage_offset, storage_size, index, num_storage_slots, num_account_procedures] - - # assert that storage limit is in bounds - add dup.2 lte assert.err=ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS - # => [index, num_storage_slots, num_account_procedures] - - # check if we should continue looping - add.1 dup dup.3 lt - # => [should_loop, index, num_storage_slots, num_account_procedures] - end - end - - # clean stack - drop drop drop - # => [] -end - -#! Returns the procedure information. -#! -#! Inputs: [index] -#! Outputs: [PROC_ROOT, storage_offset, storage_size] -#! -#! Where: -#! - PROC_ROOT is the hash of the procedure. -#! - storage_offset is the procedure storage offset. -#! - storage_size is the number of storage slots the procedure is allowed to access. -#! -#! Panics if: -#! - the procedure index is out of bounds. -export.get_procedure_info - # check that index < number of procedures contained in the account code - dup exec.memory::get_num_account_procedures - u32assert2.err=ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS u32lt assert.err=ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS - # => [index] - - # get procedure pointer - exec.memory::get_account_procedure_ptr - # => [proc_ptr] - - # get metadata pointer - dup add.4 swap - # => [proc_ptr, metadata_ptr] - - # load procedure information from memory - padw movup.4 mem_loadw_be padw movup.8 mem_loadw_be - # => [METADATA, PROC_ROOT] - # more explicitly: - # => [0, 0, storage_size, storage_offset, PROC_ROOT] - - # keep relevant data - drop drop - # => [storage_size, storage_offset, PROC_ROOT] - - swap movdn.5 movdn.5 - # => [PROC_ROOT, storage_offset, storage_size] -end - #! Verifies that the procedure root is part of the account code and tracks whether it has been #! called. #! #! Inputs: [PROC_ROOT] -#! Outputs: [storage_offset, storage_size] +#! Outputs: [] #! #! Where: #! - PROC_ROOT is the hash of the procedure to authenticate. -#! - storage_offset is the procedure storage offset. -#! - storage_size is the number of storage slots the procedure is allowed to access. #! #! Panics if: #! - the procedure root is not part of the account code. -export.authenticate_and_track_procedure +pub proc authenticate_and_track_procedure # load procedure index emit.ACCOUNT_PUSH_PROCEDURE_INDEX_EVENT adv_push.1 # => [index, PROC_ROOT] - dup movdn.5 - # get procedure info (PROC_ROOT, storage_offset, storage_size) from memory stored at index - exec.get_procedure_info - # => [MEM_PROC_ROOT, storage_offset, storage_size, PROC_ROOT, index] + dup movdn.5 exec.get_procedure_root + # => [MEM_PROC_ROOT, PROC_ROOT, index] - # verify that PROC_ROOT exists in memory at index - movup.4 movdn.9 movup.4 movdn.9 assert_eqw.err=ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE - # => [storage_offset, storage_size, index] + # verify that PROC_ROOT and MEM_PROC_ROOT fetched from memory match + assert_eqw.err=ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE + # => [index] # Set the was_called flag to 1 for this procedure - movup.2 exec.set_was_procedure_called - - # => [storage_offset, storage_size] + exec.set_was_procedure_called + # => [] end #! Asserts that the specified procedure root is the root of the authentication procedure for an @@ -1014,7 +868,7 @@ end #! #! Panics if: #! - the procedure root is not the authentication procedure. -export.assert_auth_procedure +pub proc assert_auth_procedure # authentication procedure is always at index 0 push.0 # => [index, PROC_ROOT] @@ -1023,7 +877,7 @@ export.assert_auth_procedure dup exec.set_was_procedure_called # => [index, PROC_ROOT] - # get procedure info (PROC_ROOT, storage_offset, storage_size) from memory stored at index + # get procedure root from memory stored at index exec.get_procedure_root # => [MEM_PROC_ROOT, PROC_ROOT] @@ -1044,7 +898,7 @@ end #! #! Inputs: [] #! Outputs: [] -export.validate_seed +pub proc validate_seed # Compute the hash of (SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, EMPTY_WORD). # --------------------------------------------------------------------------------------------- @@ -1070,7 +924,7 @@ export.validate_seed # perform first permutation of seed and code_commitment (from advice stack) # perm(seed, code_commitment) - hperm + exec.rpo256::permute # => [RATE, RATE, PERM, EMPTY_WORD] # clear rate elements @@ -1081,11 +935,11 @@ export.validate_seed swapw exec.memory::get_account_storage_commitment swapw # => [EMPTY_WORD, STORAGE_COMMITMENT, PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, CAP] # extract digest - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [DIGEST] # Shape suffix to set the lower 8 bits to zero and compare the computed and provided ID. @@ -1107,6 +961,106 @@ export.validate_seed # => [] end +#! Validates that slot IDs are sorted in ascending order and that slot IDs are unique. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Pancis if: +#! - each slot's ID is not strictly less than the next slot's ID. +#! - this ensures sorting and uniqueness among slot IDs. +pub proc validate_storage + exec.memory::get_num_storage_slots + # => [num_slots] + + # compute the number of slot ID comparisons we need to make + # generally, we need num_storage_slots - 1 comparisons, e.g., if we have 3 slots, we need 2 + # comparisons: 2 with 1 and 1 with 0. + # we subtract 1 if any slots exist and 0 otherwise, notably: + # maps 1 storage slot -> 0 comparisons, since 1 slot is always sorted and unique + # maps 0 storage slots -> 0 comparisons + dup u32gt.0 + # => [has_slots, num_slots] + + sub + # => [num_comparisons] + + # loop if we need to compare slots + dup neq.0 + # => [should_loop, num_comparisons] + + while.true + # first iteration: number of comparisons = current slot index + # => [curr_slot_idx] + + dup exec.get_slot_id + # => [curr_slot_id_prefix, curr_slot_id_suffix, curr_slot_idx] + + # we are guaranteed to not underflow because curr_slot_idx is at least 1 at the + # beginning of the loop + dup.2 sub.1 + # => [prev_slot_idx, curr_slot_id_prefix, curr_slot_id_suffix, curr_slot_idx] + + exec.get_slot_id + # => [prev_slot_id_prefix, prev_slot_id_suffix, curr_slot_id_prefix, curr_slot_id_suffix, curr_slot_idx] + + # this effectively checks that slots are sorted _and_ unique, since duplicate slot IDs are + # not less than each other + exec.is_slot_id_lt + # => [is_prev_lt_curr, curr_slot_idx] + + assert.err=ERR_ACCOUNT_STORAGE_SLOTS_MUST_BE_SORTED_AND_UNIQUE + # => [curr_slot_idx] + + sub.1 dup neq.0 + # => [should_continue, prev_slot_idx] + end + # => [prev_slot_idx] + + drop + # => [] +end + +#! Returns 1 if the previous slot ID is smaller than the current slot ID, 0 otherwise. +#! +#! In the slot ID comparison, the prefix takes precedence over the suffix. +#! +#! This procedure is public so it can be tested. +#! +#! Inputs: [prev_slot_id_prefix, prev_slot_id_suffix, curr_slot_id_prefix, curr_slot_id_suffix] +#! Outputs: [is_prev_lt_curr] +pub proc is_slot_id_lt + movup.2 + # => [curr_slot_id_prefix, prev_slot_id_prefix, prev_slot_id_suffix, curr_slot_id_suffix] + + # compute prev == curr for prefix + dup dup.2 eq + # => [is_prefix_eq, curr_slot_id_prefix, prev_slot_id_prefix, prev_slot_id_suffix, curr_slot_id_suffix] + + movdn.4 + # => [curr_slot_id_prefix, prev_slot_id_prefix, prev_slot_id_suffix, curr_slot_id_suffix, is_prefix_eq] + + # compute prev < curr for prefix + lt + # => [is_prev_lt_curr_prefix, prev_slot_id_suffix, curr_slot_id_suffix, is_prefix_eq] + + swap.2 + # => [curr_slot_id_suffix, prev_slot_id_suffix, is_prev_lt_curr_prefix, is_prefix_eq] + + # compute prev < curr for suffix + lt + # => [is_prev_lt_curr_suffix, is_prev_lt_curr_prefix, is_prefix_eq] + + movup.2 + # => [is_prefix_eq, is_prev_lt_curr_suffix, is_prev_lt_curr_prefix] + + # compute result as is_prefix_lt || (is_suffix_lt && is_prefix_eq) + # is_suffix_lt only affects the result if the prefix was equal, otherwise the prefix + # determines the outcome + and or + # => [is_prev_lt_curr] +end + # DATA LOADERS # ------------------------------------------------------------------------------------------------- @@ -1133,15 +1087,14 @@ end #! - STORAGE_SLOT_DATA is the data contained in the storage slot which is constructed as follows: #! [SLOT_VALUE, slot_type, 0, 0, 0]. #! - CODE_COMMITMENT is the commitment to the account's code. -#! - ACCOUNT_PROCEDURE_DATA is the information about account procedures which is constructed as -#! follows: [PROCEDURE_MAST_ROOT, storage_offset, 0, 0, 0]. +#! - ACCOUNT_PROCEDURE_DATA are the roots of the public procedures of the foreign account. #! #! Panics if: #! - the number of account procedures exceeded the maximum limit of 256. #! - the computed account code commitment does not match the provided account code commitment. #! - the number of account storage slots exceeded the maximum limit of 255. #! - the computed account storage commitment does not match the provided account storage commitment. -export.load_foreign_account +pub proc load_foreign_account emit.ACCOUNT_BEFORE_FOREIGN_LOAD_EVENT # => [account_id_prefix, account_id_suffix] @@ -1210,7 +1163,7 @@ end #! Panics if: #! - the number of account storage slots exceeded the maximum limit of 255. #! - the computed account storage commitment does not match the provided account storage commitment. -export.save_account_storage_data +pub proc save_account_storage_data # move storage slot data from the advice map to the advice stack adv.push_mapvaln # OS => [STORAGE_COMMITMENT] @@ -1233,13 +1186,14 @@ export.save_account_storage_data # AS => [[STORAGE_SLOT_DATA]] # setup acct_storage_slots_ptr and end_ptr for reading from advice stack - mul.8 exec.memory::get_account_storage_slots_section_ptr dup movdn.2 add swap + mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH exec.memory::get_account_active_storage_slots_section_ptr + dup movdn.2 add swap # OS => [acct_storage_slots_ptr, end_ptr, STORAGE_COMMITMENT] # AS => [[STORAGE_SLOT_DATA]] # pad stack before reading from advice stack padw padw padw - # OS => [PAD, PAD, PAD, acct_proc_offset, end_ptr, STORAGE_COMMITMENT] + # OS => [PAD, PAD, PAD, acct_storage_slots_ptr, end_ptr, STORAGE_COMMITMENT] # AS => [[STORAGE_SLOT_DATA]] # read the data from advice stack to memory and hash @@ -1248,7 +1202,7 @@ export.save_account_storage_data # AS => [] # extract the digest - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # OS => [DIGEST, end_ptr', STORAGE_COMMITMENT] # drop end_ptr @@ -1273,13 +1227,13 @@ end #! #! Where: #! - CODE_COMMITMENT is the commitment of the active account's code. -#! - ACCOUNT_PROCEDURE_DATA is the information about account procedure which is constructed as -#! follows: [PROCEDURE_MAST_ROOT, storage_offset, storage_size, 0, 0] +#! - ACCOUNT_PROCEDURE_DATA are the roots of the public procedures of the account laid out as +#! [ACCOUNT_PROCEDURE_DATA]. #! #! Panics if: #! - the number of account procedures exceeded the maximum limit of 256. #! - the computed account code commitment does not match the provided account code commitment. -export.save_account_procedure_data +pub proc save_account_procedure_data # move procedure data from the advice map to the advice stack adv.push_mapvaln # OS => [CODE_COMMITMENT] @@ -1291,34 +1245,38 @@ export.save_account_procedure_data # OS => [num_procs, CODE_COMMITMENT] # AS => [[ACCOUNT_PROCEDURE_DATA]] - # assert that account does not exceed allowed maximum number of procedures - dup exec.get_max_num_procedures lte assert.err=ERR_ACCOUNT_TOO_MANY_PROCEDURES + # make sure number of procedures is a valid u32, so we can use u32 operations for validation + u32assert.err=ERR_ACCOUNT_TOO_MANY_PROCEDURES # OS => [num_procs, CODE_COMMITMENT] # AS => [[ACCOUNT_PROCEDURE_DATA]] - # store number of procedures in memory - dup exec.memory::set_num_account_procedures + # assert the account has at least the minimum number of procedures + dup u32gte.MIN_NUM_PROCEDURES assert.err=ERR_ACCOUNT_NOT_ENOUGH_PROCEDURES # OS => [num_procs, CODE_COMMITMENT] # AS => [[ACCOUNT_PROCEDURE_DATA]] - # setup acct_proc_offset and end_ptr for reading from advice stack - exec.memory::get_account_procedure_ptr - push.0 exec.memory::get_account_procedure_ptr - # OS => [acct_proc_offset, end_ptr, CODE_COMMITMENT] + # assert the account does not exceed the maximum number of procedures + dup u32lte.MAX_NUM_PROCEDURES assert.err=ERR_ACCOUNT_TOO_MANY_PROCEDURES + # OS => [num_procs, CODE_COMMITMENT] # AS => [[ACCOUNT_PROCEDURE_DATA]] - # pad stack before reading from advice stack - padw padw padw - # OS => [PAD, PAD, PAD, acct_proc_offset, end_ptr, CODE_COMMITMENT] + # store number of procedures in memory + dup exec.memory::set_num_account_procedures + # OS => [num_procs, CODE_COMMITMENT] + # AS => [[ACCOUNT_PROCEDURE_DATA]] + + # setup start ptr and num procedures for reading from advice stack + exec.memory::get_account_procedures_section_ptr swap + # OS => [num_procs, account_proc_ptr, CODE_COMMITMENT] # AS => [[ACCOUNT_PROCEDURE_DATA]] # read the data from advice stack to memory and hash - exec.mem::pipe_double_words_to_memory + exec.mem::pipe_words_to_memory # OS => [PERM, PERM, PERM, end_ptr', CODE_COMMITMENT] # AS => [] # extract the digest - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # OS => [DIGEST, end_ptr', CODE_COMMITMENT] # drop end_ptr @@ -1338,7 +1296,7 @@ end #! #! Inputs: [] #! Outputs: [] -export.insert_new_storage +pub proc insert_new_storage exec.memory::get_num_storage_slots # => [num_slots] @@ -1353,7 +1311,7 @@ export.insert_new_storage dup exec.get_storage_slot_type # => [slot_type, slot_idx] - exec.constants::get_storage_slot_type_map eq + push.STORAGE_SLOT_TYPE_MAP eq # => [is_map_slot_type, slot_idx] if.true @@ -1386,77 +1344,77 @@ end #! Operand stack: [slot_idx] #! Advice map: { MAP_ROOT: [MAP_ENTRIES] } #! Outputs: [] -proc.insert_and_validate_storage_map - dup exec.memory::get_account_storage_slots_section_ptr - # => [storage_slots_ptr, slot_idx, slot_idx] +proc insert_and_validate_storage_map + mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH + exec.memory::get_account_active_storage_slots_section_ptr + add + # => [slot_ptr] - exec.get_item_raw - # => [MAP_ROOT, slot_idx] + dup exec.get_item_raw + # => [MAP_ROOT, slot_ptr] # overwrite the map root with the root of an empty SMT, so we can insert the entries of # the map into an empty map and then check whether the resulting root matches MAP_ROOT. - exec.constants::get_empty_smt_root + push.EMPTY_SMT_ROOT dup.8 - # => [slot_idx, EMPTY_SMT_ROOT, MAP_ROOT, slot_idx] + # => [slot_ptr, EMPTY_SMT_ROOT, MAP_ROOT, slot_ptr] - exec.set_item_raw dropw - # => [MAP_ROOT, slot_idx] + exec.set_item_raw + # => [MAP_ROOT, slot_ptr] adv.push_mapvaln - # OS => [MAP_ROOT, slot_idx] + # OS => [MAP_ROOT, slot_ptr] # AS => [num_elements, [MAP_ENTRIES]] movup.4 - # OS => [slot_idx, MAP_ROOT] + # OS => [slot_ptr, MAP_ROOT] # AS => [num_elements, [MAP_ENTRIES]] adv_push.1 - # OS => [num_elements, slot_idx, MAP_ROOT] + # OS => [num_elements, slot_ptr, MAP_ROOT] # AS => [[MAP_ENTRIES]] push.8 u32assert2.err="number of storage map elements should fit into a u32" - # OS => [8, num_elements, slot_idx, MAP_ROOT] + # OS => [8, num_elements, slot_ptr, MAP_ROOT] # AS => [[MAP_ENTRIES]] # check that num_elements % 8 = 0 so we can use an equality check for the loop condition # this also computes number_entries which is num_elements / 8. u32divmod eq.0 assert.err="number of storage map elements must be a multiple of 8" - # OS => [num_entries, slot_idx, MAP_ROOT] + # OS => [num_entries, slot_ptr, MAP_ROOT] # AS => [[MAP_ENTRIES]] # loop if there are more than 0 storage map elements dup neq.0 - # OS => [should_loop, num_entries, slot_idx, MAP_ROOT] + # OS => [should_loop, num_entries, slot_ptr, MAP_ROOT] # AS => [[MAP_ENTRIES]] while.true sub.1 - # => [remaining_entries, slot_idx, MAP_ROOT] + # => [remaining_entries, slot_ptr, MAP_ROOT] # push a key-value pair (8 felts) to the operand stack adv_push.8 - # => [KEY, VALUE, remaining_entries, slot_idx, MAP_ROOT] + # => [KEY, VALUE, remaining_entries, slot_ptr, MAP_ROOT] dup.9 - # => [slot_idx, KEY, VALUE, remaining_entries, slot_idx, MAP_ROOT] + # => [slot_ptr, KEY, VALUE, remaining_entries, slot_ptr, MAP_ROOT] # insert the key-value pair into account storage - # this could be optimized to avoid the map slot type assertion and reading and writing the - # root on every call - exec.set_map_item dropw dropw - # => [remaining_entries, slot_idx, MAP_ROOT] + # this could be optimized to avoid reading and writing the root on every call + exec.set_map_item_raw dropw + # => [remaining_entries, slot_ptr, MAP_ROOT] dup neq.0 - # => [should_continue, remaining_entries, slot_idx, MAP_ROOT] + # => [should_continue, remaining_entries, slot_ptr, MAP_ROOT] end - # OS => [remaining_entries, slot_idx, MAP_ROOT] + # OS => [remaining_entries, slot_ptr, MAP_ROOT] # AS => [] drop - # => [slot_idx, MAP_ROOT] + # => [slot_ptr, MAP_ROOT] # load the root after all entries have been inserted - exec.memory::get_account_storage_slots_section_ptr exec.get_item_raw # => [CURRENT_MAP_ROOT, MAP_ROOT] @@ -1469,102 +1427,329 @@ end # HELPER PROCEDURES # ================================================================================================= -#! Gets an item from storage using the provided storage slots pointer and index. +#! Returns 1 if the provided slot ID is equal to the reserved faucet storage data slot, 0 +#! otherwise. #! -#! Note: -#! - We assume that index has been validated and is within bounds. +#! Inputs: [slot_id_prefix, slot_id_suffix] +#! Outputs: [is_faucet_storage_data_slot] +#! +#! Where: +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - is_faucet_storage_data_slot is a boolean value indicating whether the provided slot is the +#! reserved faucet data slot. +pub proc is_faucet_storage_data_slot + exec.get_faucet_sysdata_slot_id + # => [faucet_slot_id_prefix, faucet_slot_id_suffix, slot_id_prefix, slot_id_suffix] + + movup.2 eq + # => [prefix_eq, faucet_slot_id_suffix, slot_id_suffix] + + movdn.2 eq + # => [prefix_eq, suffix_eq] + + and + # => [is_faucet_storage_data_slot] +end + +#! Gets the initial and current value of an item from the storage slot at the provided index for +#! the native account. +#! +#! WARNING: The index must be in bounds. +#! +#! Inputs: [index] +#! Outputs: [INITIAL_VALUE, CURRENT_VALUE, slot_id_prefix, slot_id_suffix] +#! +#! Where: +#! - index is the index of the slot. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - INITIAL_VALUE is the initial value of the item at the beginning of the transaction. +#! - CURRENT_VALUE is the current value of the item. +pub proc get_item_delta + # convert the index into a memory offset + mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH + # => [offset] + + # get account storage slots section offset + exec.memory::get_native_account_active_storage_slots_ptr + # => [storage_slots_ptr, offset] + + dup.1 add dup + # => [slot_ptr, slot_ptr, offset] + + # load the slot ID + add.ACCOUNT_SLOT_ID_SUFFIX_OFFSET mem_load + # => [slot_id_suffix, slot_ptr, offset] + + dup.1 add.ACCOUNT_SLOT_ID_PREFIX_OFFSET mem_load + # => [slot_id_prefix, slot_id_suffix, slot_ptr, offset] + + # load the current value + movup.2 exec.get_item_raw + # => [CURRENT_VALUE, slot_id_prefix, slot_id_suffix, offset] + + # get account initial storage slots section offset + exec.memory::get_account_initial_storage_slots_ptr + # => [init_storage_slots_ptr, CURRENT_VALUE, slot_id_prefix, slot_id_suffix, offset] + + movup.7 add + # => [init_slot_ptr, CURRENT_VALUE, slot_id_prefix, slot_id_suffix] + + exec.get_item_raw + # => [INITIAL_VALUE, CURRENT_VALUE, slot_id_prefix, slot_id_suffix] +end + +#! Gets the slot ID of the storage slot at the provided index. +#! +#! WARNING: The index must be in bounds. +#! +#! Inputs: [index] +#! Outputs: [slot_id_prefix, slot_id_suffix] +#! +#! Where: +#! - index is the index of the slot. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +pub proc get_slot_id + # convert the index into a memory offset + mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH + # => [offset] + + exec.memory::get_account_active_storage_slots_section_ptr + add + # => [slot_ptr] + + dup add.ACCOUNT_SLOT_ID_SUFFIX_OFFSET mem_load + # => [slot_id_suffix, slot_ptr] + + swap add.ACCOUNT_SLOT_ID_PREFIX_OFFSET mem_load + # => [slot_id_prefix, slot_id_suffix] +end + +#! Sets the value of the storage slot located at the memory address specified by the provided +#! pointer. +#! +#! The provided pointer must be a valid pointer into the current storage slot section. +#! +#! WARNING: This must only be used on the native account. +#! +#! Inputs: [slot_ptr, VALUE] +#! Outputs: [] +#! +#! Where: +#! - slot_ptr is the pointer to a slot. +#! - VALUE is the new value of the item. +proc set_item_raw + add.ACCOUNT_SLOT_VALUE_OFFSET mem_storew_be dropw + # => [] + + # set the storage commitment dirty flag to indicate that the commitment is outdated + push.1 + exec.memory::set_native_account_storage_commitment_dirty_flag + # => [] +end + +#! Stores NEW_VALUE under the specified KEY within the map contained in the given account storage slot. +#! +#! WARNING: The slot ptr must point to a map slot. +#! WARNING: This must only be used on the native account. #! -#! Inputs: [storage_slots_ptr, index] +#! Inputs: [slot_ptr, KEY, NEW_VALUE] +#! Outputs: [OLD_VALUE] +#! +#! Where: +#! - slot_ptr is the pointer to a slot. +#! - KEY is the key to set. +#! - NEW_VALUE is the value to set under KEY. +#! - OLD_VALUE is the previous value of the item. +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +#! - no map with the root of the slot is found. +#! +#! Locals: +#! - 0: slot_ptr +#! - 4..8: OLD_MAP_VALUE +#! - 8..12: OLD_MAP_ROOT +@locals(8) +proc set_map_item_raw + # store slot_ptr until the end of the procedure + dup loc_store.0 + # => [slot_ptr, KEY, NEW_VALUE] + + emit.ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM_EVENT + # => [slot_ptr, KEY, NEW_VALUE] + + # load the current map root from memory + exec.get_item_raw + # => [OLD_MAP_ROOT, KEY, NEW_VALUE] + + # duplicate the KEY and the NEW_VALUE for account delta insertion and event emission + dupw.2 dupw.2 + # => [KEY, NEW_VALUE, OLD_MAP_ROOT, KEY, NEW_VALUE] + + # see hash_map_key's docs for why this is done + exec.hash_map_key swapw + # => [NEW_VALUE, HASHED_KEY, OLD_MAP_ROOT, KEY, NEW_VALUE] + + # set the NEW_VALUE under HASHED_KEY in the tree + exec.smt::set + # => [OLD_VALUE, NEW_ROOT, KEY, NEW_VALUE] + + # store OLD_VALUE until the end of the procedure + loc_storew_be.4 swapw + # => [NEW_ROOT, OLD_VALUE, KEY, NEW_VALUE] + + # store NEW_ROOT into the map slot's VALUE + loc_load.0 exec.set_item_raw + # => [OLD_VALUE, KEY, NEW_VALUE] + + swapw + # => [KEY, OLD_VALUE, NEW_VALUE] + + loc_load.0 + # => [slot_ptr, KEY, OLD_VALUE, NEW_VALUE] + + # emit event to signal that an account storage map item is being updated + emit.ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM_EVENT + # => [slot_ptr, KEY, OLD_VALUE, NEW_VALUE] + + # convert the slot ptr to a slot index for the delta API + exec.slot_ptr_to_index + # => [slot_idx, KEY, OLD_VALUE, NEW_VALUE] + + exec.account_delta::set_map_item + # => [] + + # load OLD_VALUE as return value on the stack + padw loc_loadw_be.4 + # => [OLD_VALUE] +end + +#! Gets the value of the storage slot located at the memory address specified by the provided +#! pointer. +#! +#! The provided pointer must be a valid pointer into the initial or current storage slot section. +#! +#! Inputs: [slot_ptr] #! Outputs: [VALUE] #! #! Where: -#! - storage_slots_ptr is the pointer to the storage slots section. -#! - index is the index of the item to get. +#! - slot_ptr is the pointer to a slot. #! - VALUE is the value of the item. -export.get_item_raw - # get the item from storage - swap mul.8 add padw movup.4 mem_loadw_be +proc get_item_raw + # offset the pointer to point to the VALUE in the slot + add.ACCOUNT_SLOT_VALUE_OFFSET + # => [slot_value_ptr] + + # load the item from memory + padw movup.4 mem_loadw_be # => [VALUE] end -#! Shared procedure for getting a map item from a storage slot without checking the index. -#! -#! WARNING: Must be called with an index that is in bounds. +#! Finds the storage map root in the storage slot with the provided name in the provided storage +#! slots section and returns the VALUE associated with the KEY in the corresponding map. #! -#! Inputs: [KEY, ROOT, index] +#! Inputs: [storage_slots_ptr, slot_id_prefix, slot_id_suffix, KEY] #! Outputs: [VALUE] #! #! Where: #! - KEY is the key to look up in the map. -#! - ROOT is the root of the map. -#! - index is the index of the storage slot that contains the map root. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value of the map item at KEY. #! #! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. -proc.get_map_item_raw - # check storage slot type - dup.8 exec.get_storage_slot_type - # => [slot_type, KEY, ROOT, index] +proc get_map_item_raw + exec.find_storage_slot + # => [slot_ptr, KEY] - # check if storage slot type is map - exec.constants::get_storage_slot_type_map eq + emit.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT + # => [slot_ptr, KEY] + + # load the slot type + dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load + # => [slot_type, slot_ptr, KEY] + + # assert slot_type is map + push.STORAGE_SLOT_TYPE_MAP eq assert.err=ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT - # => [KEY, ROOT, index] + # => [slot_ptr, KEY] - emit.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT - # => [KEY, ROOT, index] + # load the storage map root from the slot + exec.get_item_raw + # => [ROOT, KEY] # see hash_map_key's docs for why this is done - exec.hash_map_key - # => [HASHED_KEY, ROOT, index] + swapw exec.hash_map_key + # => [HASHED_KEY, ROOT] # fetch the VALUE located under HASHED_KEY in the tree exec.smt::get - # => [VALUE, ROOT, index] + # => [VALUE, ROOT] # remove the ROOT from the stack swapw dropw - # => [VALUE, index] - - movup.4 drop # => [VALUE] end -#! Sets an item in the account storage. Doesn't emit any events. +#! Finds the slot identified by the key [slot_id_prefix, slot_id_suffix, 0, 0] (stack order) and +#! returns the pointer to that slot. #! -#! Inputs: [index, NEW_VALUE] -#! Outputs: [OLD_VALUE] +#! Inputs: [storage_slots_ptr, slot_id_prefix, slot_id_suffix] +#! Outputs: [slot_ptr] #! #! Where: -#! - index is the index of the item to set. -#! - NEW_VALUE is the value to set. -#! - OLD_VALUE is the previous value of the item. -proc.set_item_raw - # get old value from storage - dup movdn.5 exec.get_item - # => [OLD_VALUE, NEW_VALUE, index] +#! - storage_slots_ptr is the pointer to the storage slots section. +#! - slot_ptr is the pointer to the resolved storage slot. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +proc find_storage_slot + # construct the start and end pointers of the storage slot section in which we will search + dup exec.memory::get_num_storage_slots mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH add + # => [storage_slots_end_ptr, storage_slots_start_ptr, slot_id_prefix, slot_id_suffix] - # arrange stack for storage update - swapw movup.8 - # => [index, NEW_VALUE, OLD_VALUE] + movdn.3 movdn.2 + # => [slot_id_prefix, slot_id_suffix, storage_slots_start_ptr, storage_slots_end_ptr] - # get account storage slots section offset - exec.memory::get_account_storage_slots_section_ptr - # => [acct_storage_slots_section_offset, index, NEW_VALUE, OLD_VALUE] + # find the slot whose slot key matches [slot_id_prefix, slot_id_suffix, 0, 0] + # if the slot key does not exist, this procedure will validate its absence + exec.sorted_array::find_half_key_value + # => [is_slot_found, slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] - # update storage - swap mul.8 add mem_storew_be - # => [NEW_VALUE, OLD_VALUE] + assert.err=ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME + # => [slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] - # update the storage commitment dirty flag, indicating that the commitment is outdated - push.1 - exec.memory::set_native_account_storage_commitment_dirty_flag - # => [NEW_VALUE, OLD_VALUE] + swap.2 drop drop + # => [slot_ptr] +end - # drop value - dropw - # => [OLD_VALUE] +#! Computes the slot index from a given slot ptr. +#! +#! WARNING: The slot ptr is assumed to be a valid pointer to a slot in an account's storage slots +#! section (**not** the initial one). In particular, this means the slot_ptr must be a valid u32. +#! +#! Inputs: [slot_ptr] +#! Outputs: [slot_idx] +#! +#! Where: +#! - slot_ptr is the pointer to a "current" storage slot in the native account's memory section. +#! - slot_idx is the index of the storage slot. +proc slot_ptr_to_index + exec.memory::get_account_active_storage_slots_section_ptr + sub + # => [slot_offset] + + # compute slot_offset / ACCOUNT_STORAGE_SLOT_DATA_LENGTH to get the index + # we can assume the slot_ptr is a valid u32. + u32div.ACCOUNT_STORAGE_SLOT_DATA_LENGTH + # => [slot_idx] end #! Returns the procedure root. @@ -1577,9 +1762,10 @@ end #! #! Panics if: #! - the procedure index is out of bounds. -proc.get_procedure_root +proc get_procedure_root dup exec.memory::get_num_account_procedures - u32assert2.err=ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS u32lt assert.err=ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS + u32assert2.err=ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS + u32lt assert.err=ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS # => [index] # get procedure pointer @@ -1591,27 +1777,6 @@ proc.get_procedure_root # => [PROC_ROOT] end -#! Returns the procedure metadata. -#! -#! Note: -#! - We assume that index has been validated and is within bounds. -#! -#! Inputs: [index] -#! Outputs: [storage_offset, storage_size] -#! -#! Where: -#! - storage_offset is the procedure storage offset. -#! - storage_size is the number of storage slots the procedure is allowed to access. -proc.get_procedure_metadata - # get procedure storage metadata pointer - padw exec.memory::get_account_procedure_ptr add.4 - # => [storage_offset_ptr, EMPTY_WORD] - - # load procedure metadata from memory and keep relevant data - mem_loadw_be drop drop swap - # => [storage_offset, storage_size] -end - #! Returns the pointer to the next vacant memory slot if the account was not loaded before, and the #! pointer to the account data otherwise. #! @@ -1629,7 +1794,7 @@ end #! Panics if: #! - the prefix or suffix of the provided foreign account ID equal zero. #! - the maximum allowed number of foreign account to be loaded (64) was exceeded. -export.get_account_data_ptr +pub proc get_account_data_ptr # check that foreign account ID is not equal zero dup.1 eq.0 dup.1 eq.0 and not assert.err=ERR_FOREIGN_ACCOUNT_ID_IS_ZERO # => [foreign_account_id_prefix, foreign_account_id_suffix] @@ -1690,7 +1855,7 @@ end #! #! Panics if: #! - the hash of the active account is not represented in the account database. -export.validate_active_foreign_account +pub proc validate_active_foreign_account # get the account database root exec.memory::get_account_db_root # => [ACCOUNT_DB_ROOT] @@ -1721,8 +1886,8 @@ end #! #! Inputs: [KEY] #! Outputs: [HASHED_KEY] -proc.hash_map_key - hash +proc hash_map_key + exec.rpo256::hash # => [HASHED_KEY] end @@ -1735,7 +1900,7 @@ end #! #! Inputs: [] #! Outputs: [] -proc.refresh_storage_commitment +proc refresh_storage_commitment # First we should determine whether the storage commitment should be recomputed. We should do so # if the active account is native and the storage commitment is outdated (the dirty flag equals # 1). Otherwise the commitment value is guaranteed to be up-to-date. @@ -1751,7 +1916,8 @@ proc.refresh_storage_commitment # => [num_storage_slots] # setup start and end ptr - mul.8 exec.memory::get_account_storage_slots_section_ptr dup movdn.2 add swap + mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH exec.memory::get_account_active_storage_slots_section_ptr + dup movdn.2 add swap # => [start_ptr, end_ptr] # pad stack to read and hash from memory @@ -1759,11 +1925,11 @@ proc.refresh_storage_commitment # => [PAD, PAD, PAD, start_ptr, end_ptr] # hash elements from memory - exec.rpo::absorb_double_words_from_memory + exec.rpo256::absorb_double_words_from_memory # => [PERM, PERM, PERM, start_ptr, end_ptr] # extract the digest - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [DIGEST, end_ptr, end_ptr] # clean stack @@ -1796,7 +1962,7 @@ end #! #! Panics if: #! - the procedure root is not part of the account code. -export.was_procedure_called +pub proc was_procedure_called # load procedure index emit.ACCOUNT_PUSH_PROCEDURE_INDEX_EVENT adv_push.1 # => [index, PROC_ROOT] @@ -1829,7 +1995,7 @@ end #! #! Inputs: [proc_idx] #! Outputs: [] -export.set_was_procedure_called +pub proc set_was_procedure_called exec.memory::get_account_procedures_call_tracking_ptr # => [was_called_offset, proc_idx] @@ -1849,7 +2015,7 @@ end #! - PROC_ROOT is the hash of the procedure of interest. #! - is_procedure_available is the binary flag indicating whether the procedure with PROC_ROOT is #! available on the active account. -export.has_procedure +pub proc has_procedure # get the end pointer of the procedure section (where we should stop iterating) exec.memory::get_num_account_procedures exec.memory::get_account_procedure_ptr @@ -1883,18 +2049,18 @@ export.has_procedure # => [PROC_ROOT, curr_proc_ptr, end_ptr, is_procedure_available'] # move the current procedure pointer - movup.4 add.8 - # => [curr_proc_ptr + 8, PROC_ROOT, end_ptr, is_procedure_available'] + movup.4 add.ACCOUNT_PROCEDURE_DATA_LENGTH + # => [curr_proc_ptr + 4, PROC_ROOT, end_ptr, is_procedure_available'] # compute should_loop flag: we should continue iterating if - # !(is_procedure_available' || curr_proc_ptr + 8 == end_ptr), i.e. we didn't find the + # !(is_procedure_available' || curr_proc_ptr + 4 == end_ptr), i.e. we didn't find the # procedure and we didn't reach the end of the procedures memory block dup dup.6 eq dup.7 or eq.0 - # => [should_loop, curr_proc_ptr + 8, PROC_ROOT, end_ptr, is_procedure_available'] + # => [should_loop, curr_proc_ptr + 4, PROC_ROOT, end_ptr, is_procedure_available'] # rearrange the stack swap movdn.5 - # => [should_loop, PROC_ROOT, curr_proc_ptr + 8, end_ptr, is_procedure_available'] + # => [should_loop, PROC_ROOT, curr_proc_ptr + 4, end_ptr, is_procedure_available'] end # => [PROC_ROOT, curr_proc_ptr', end_ptr, is_procedure_available'] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm similarity index 87% rename from crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm index d207ecaa22..229103d606 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm @@ -1,29 +1,29 @@ -use.$kernel::account -use.$kernel::asset -use.$kernel::asset_vault -use.$kernel::constants -use.$kernel::link_map -use.$kernel::memory -use.std::crypto::hashes::rpo -use.std::word +use $kernel::account +use $kernel::asset +use $kernel::asset_vault +use $kernel::constants::STORAGE_SLOT_TYPE_VALUE +use $kernel::link_map +use $kernel::memory +use miden::core::crypto::hashes::rpo256 +use miden::core::word # ERRORS # ================================================================================================= -const.ERR_ACCOUNT_DELTA_NONCE_MUST_BE_INCREMENTED_IF_VAULT_OR_STORAGE_CHANGED="nonce must be incremented if account vault or account storage changed" +const ERR_ACCOUNT_DELTA_NONCE_MUST_BE_INCREMENTED_IF_VAULT_OR_STORAGE_CHANGED="nonce must be incremented if account vault or account storage changed" # CONSTANTS # ================================================================================================= # The domain of an asset in the delta commitment. -const.DOMAIN_ASSET=1 +const DOMAIN_ASSET = 1 # The domain of a value storage slot in the delta commitment. -const.DOMAIN_VALUE=2 +const DOMAIN_VALUE = 2 # The domain of a map storage slot in the delta commitment. -const.DOMAIN_MAP=3 +const DOMAIN_MAP = 3 # The maximum value a felt can represent. -const.FELT_MAX=0xffffffff00000000 +const FELT_MAX = 0xffffffff00000000 # PROCEDURES # ================================================================================================= @@ -43,7 +43,7 @@ const.FELT_MAX=0xffffffff00000000 #! #! Panics if: #! - the vault or storage delta is not empty but the nonce increment is zero. -export.compute_commitment +pub proc compute_commitment # pad capacity element of the hasher padw # => [CAPACITY] @@ -60,7 +60,7 @@ export.compute_commitment padw # => [EMPTY_WORD, ID_AND_NONCE, CAPACITY] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] # save the ID and nonce digest (the 2nd rate word) for a later check @@ -76,7 +76,7 @@ export.compute_commitment exec.update_storage_delta # => [RATE, RATE, PERM, ID_AND_NONCE_DIGEST] - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [DELTA_COMMITMENT, ID_AND_NONCE_DIGEST] exec.was_nonce_incremented not @@ -103,7 +103,7 @@ end #! #! Inputs: [RATE, RATE, PERM] #! Outputs: [RATE, RATE, PERM] -proc.update_storage_delta +proc update_storage_delta exec.memory::get_num_storage_slots movdn.12 # => [RATE, RATE, PERM, num_storage_slots] @@ -144,17 +144,12 @@ end #! #! Inputs: [slot_idx, RATE, RATE, PERM] #! Outputs: [RATE, RATE, PERM] -proc.update_slot_delta - # we use memory::get_storage_slot_type instead of the procedure in account to - # avoid the assertion overhead - dup exec.memory::get_native_account_storage_slots_ptr - # => [native_account_storage_slots_section_ptr, slot_idx, slot_idx, RATE, RATE, PERM] - - exec.memory::get_storage_slot_type +proc update_slot_delta + dup exec.account::get_storage_slot_type # => [storage_slot_type, slot_idx, RATE, RATE, PERM] - # check if type == slot - exec.constants::get_storage_slot_type_value eq + # check if slot is of type value + push.STORAGE_SLOT_TYPE_VALUE eq # => [is_value_slot_type, slot_idx, RATE, RATE, PERM] if.true @@ -169,65 +164,46 @@ end #! #! Inputs: [slot_idx, RATE, RATE, PERM] #! Outputs: [RATE, RATE, PERM] -proc.update_value_slot_delta - dup exec.account::get_item - # => [CURRENT_VALUE, slot_idx, RATE, RATE, PERM] - - dup.4 exec.account::get_initial_item - # => [INIT_VALUE, CURRENT_VALUE, slot_idx, RATE, RATE, PERM] - - # if account is new, replace INIT_VALUE with EMPTY_WORD - # we need to do this specifically for new accounts because for those, get_initial_item returns - # the same as get_item but we want the delta for value slots to be from an empty value to the - # final value - # use get_init_nonce so the delta is still correctly computed when the nonce has already been - # incremented - padw exec.memory::get_init_nonce eq.0 - # => [is_account_new, EMPTY_WORD, INIT_VALUE, CURRENT_VALUE, slot_idx, RATE, RATE, PERM] - - # If is_account_new EMPTY_WORD remains. - # If !is_account_new INIT_VALUE remains. - cdropw - # => [INIT_VALUE', CURRENT_VALUE, slot_idx, RATE, RATE, PERM] +proc update_value_slot_delta + exec.account::get_item_delta + # => [INIT_VALUE, CURRENT_VALUE, slot_id_prefix, slot_id_suffix, RATE, RATE, PERM] exec.word::test_eq not - # => [was_changed, INIT_VALUE', CURRENT_VALUE, slot_idx, RATE, RATE, PERM] + # => [was_changed, INIT_VALUE, CURRENT_VALUE, slot_id_prefix, slot_id_suffix, RATE, RATE, PERM] # set was_changed to true if the account is new - # this means a storage slot initialized with an empty word is included in the commitment for - # new accounts - # more generally, we want the delta for a new account to include all its newly added values, - # regardless of the exact value, because the initial delta for an account must represent its - # full state - exec.memory::get_init_nonce eq.0 or - # => [was_changed, INIT_VALUE', CURRENT_VALUE, slot_idx, RATE, RATE, PERM] + # generally, the delta for a new account must include all its storage slots, regardless of the + # initial value and even if it is an empty word, because the initial delta for an account must + # represent its full state + exec.memory::is_new_account or + # => [was_changed, INIT_VALUE, CURRENT_VALUE, slot_id_prefix, slot_id_suffix, RATE, RATE, PERM] # only include in delta if the slot's value has changed or the account is new if.true # drop init value dropw - # => [CURRENT_VALUE, slot_idx, RATE, RATE, PERM] + # => [CURRENT_VALUE, slot_id_prefix, slot_id_suffix, RATE, RATE, PERM] # build value slot metadata - push.DOMAIN_VALUE - # => [domain, CURRENT_VALUE, slot_idx, RATE, RATE, PERM] + push.DOMAIN_VALUE push.0 + # => [0, domain, CURRENT_VALUE, slot_id_prefix, slot_id_suffix, RATE, RATE, PERM] - movup.5 push.0.0 - # => [0, 0, slot_idx, domain, CURRENT_VALUE, RATE, RATE, PERM] + movup.7 movup.7 + # => [slot_id_prefix, slot_id_suffix, 0, domain, CURRENT_VALUE, RATE, RATE, PERM] # clear rate elements swapdw dropw dropw - # => [0, 0, slot_idx, domain, CURRENT_VALUE, PERM] + # => [slot_id_prefix, slot_id_suffix, 0, domain, CURRENT_VALUE, PERM] # arrange rate words in correct order swapw - # => [CURRENT_VALUE, 0, 0, slot_idx, domain, PERM] + # => [CURRENT_VALUE, slot_id_prefix, slot_id_suffix, 0, domain, PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] else - # drop init value, current value and slot idx - dropw dropw drop + # drop init value, current value and slot name + dropw dropw drop drop # => [RATE, RATE, PERM] end # => [RATE, RATE, PERM] @@ -239,18 +215,23 @@ end #! Outputs: [RATE, RATE, PERM] #! #! Locals: -#! 0: slot_idx -#! 1: has_next -#! 2: iter -#! 3: num_changed_entries -proc.update_map_slot_delta.4 +#! - 0: slot_id_suffix +#! - 1: slot_id_prefix +#! - 2: has_next +#! - 3: iter +#! - 4: num_changed_entries +@locals(5) +proc update_map_slot_delta # initialize num_changed_entries = 0 # this is necessary because this procedure can be called multiple times and the second # invocation shouldn't reuse the first invocation's value - push.0 loc_store.3 + push.0 loc_store.4 # => [slot_idx, RATE, RATE, PERM] - dup loc_store.0 + dup exec.account::get_slot_id + # => [slot_id_prefix, slot_id_suffix, slot_idx, RATE, RATE, PERM] + + loc_store.1 loc_store.0 # => [slot_idx, RATE, RATE, PERM] exec.memory::get_account_delta_storage_map_ptr @@ -265,11 +246,11 @@ proc.update_map_slot_delta.4 # => [KEY, INIT_VALUE, NEW_VALUE, has_next, iter, ...] # store has_next - movup.12 loc_store.1 + movup.12 loc_store.2 # => [KEY, INIT_VALUE, NEW_VALUE, iter, ...] # store iter - movup.12 loc_store.2 + movup.12 loc_store.3 # => [KEY, INIT_VALUE, NEW_VALUE, ...] swapw.2 @@ -284,15 +265,15 @@ proc.update_map_slot_delta.4 swapw dropw # => [NEW_VALUE, KEY, RATE, RATE, PERM] - # increment number of changed entries in local 3 - loc_load.3 add.1 loc_store.3 + # increment number of changed entries in local + loc_load.4 add.1 loc_store.4 # => [NEW_VALUE, KEY, RATE, RATE, PERM] # drop previous RATE elements swapdw dropw dropw # => [NEW_VALUE, KEY, PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] else # discard the key and init and new value words loaded from the map @@ -302,10 +283,10 @@ proc.update_map_slot_delta.4 # => [RATE, RATE, PERM] # load iter and has_next - loc_load.2 + loc_load.3 # => [iter, RATE, RATE, PERM] - loc_load.1 + loc_load.2 # => [has_next, iter, RATE, RATE, PERM] end @@ -315,12 +296,12 @@ proc.update_map_slot_delta.4 # only include the map slot metadata if there were entries in the map that resulted in an # update to the hasher state - loc_load.3 neq.0 + loc_load.4 neq.0 # => [is_num_changed_entries_non_zero, RATE, RATE, PERM] # if the account is new (nonce == 0) include the map header even if it is an empty map # in order to have the delta commit to this initial storage slot. - exec.memory::get_init_nonce eq.0 or + exec.memory::is_new_account or # => [should_include_map_header, RATE, RATE, PERM] if.true @@ -328,10 +309,10 @@ proc.update_map_slot_delta.4 dropw dropw # => [PERM] - push.DOMAIN_MAP loc_load.0 loc_load.3 push.0 padw - # => [EMPTY_WORD, [0, num_changed_entries, slot_idx, domain], PERM] + push.DOMAIN_MAP loc_load.4 loc_load.0 loc_load.1 padw + # => [EMPTY_WORD, [slot_id_prefix, slot_id_suffix, num_changed_entries, domain], PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] end # => [RATE, RATE, PERM] @@ -341,7 +322,8 @@ end #! #! Inputs: [RATE, RATE, PERM] #! Outputs: [RATE, RATE, PERM] -proc.update_fungible_asset_delta.2 +@locals(2) +proc update_fungible_asset_delta exec.memory::get_account_delta_fungible_asset_ptr # => [account_delta_fungible_asset_ptr, RATE, RATE, PERM] @@ -395,7 +377,7 @@ proc.update_fungible_asset_delta.2 swapdw dropw dropw # => [[faucet_id_prefix, faucet_id_suffix, 0, delta_amount_abs], [0, 0, was_added, domain], PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] else # discard values loaded from map: KEY, VALUE0 @@ -421,7 +403,8 @@ end #! #! Inputs: [RATE, RATE, PERM] #! Outputs: [RATE, RATE, PERM] -proc.update_non_fungible_asset_delta.2 +@locals(2) +proc update_non_fungible_asset_delta exec.memory::get_account_delta_non_fungible_asset_ptr # => [account_delta_non_fungible_asset_ptr, RATE, RATE, PERM] @@ -466,7 +449,7 @@ proc.update_non_fungible_asset_delta.2 swapdw dropw dropw # => [ASSET, [0, 0, was_added, domain], PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] else # discard the two key and value words loaded from the map @@ -498,7 +481,7 @@ end #! #! Where: #! - was_nonce_incremented is the boolean flag indicating whether the account nonce was incremented. -export.was_nonce_incremented +pub proc was_nonce_incremented exec.memory::get_init_nonce # => [init_nonce] @@ -518,7 +501,7 @@ end #! #! Where: #! - ASSET is the asset. -export.add_asset +pub proc add_asset # check if the asset is a fungible asset exec.asset::is_fungible_asset # => [is_fungible_asset, ASSET] @@ -548,7 +531,7 @@ end #! #! Where: #! - ASSET is the asset. -export.remove_asset +pub proc remove_asset # check if the asset is a fungible asset exec.asset::is_fungible_asset # => [is_fungible_asset, ASSET, vault_root_ptr] @@ -576,7 +559,7 @@ end #! Where: #! - ASSET_KEY is the asset key of the fungible asset. #! - amount is the amount by which the fungible asset's amount increases. -export.add_fungible_asset +pub proc add_fungible_asset dupw exec.memory::get_account_delta_fungible_asset_ptr # => [fungible_delta_map_ptr, ASSET_KEY, ASSET_KEY, amount] @@ -613,7 +596,7 @@ end #! Where: #! - ASSET_KEY is the asset key of the fungible asset. #! - amount is the amount by which the fungible asset's amount decreases. -export.remove_fungible_asset +pub proc remove_fungible_asset dupw exec.memory::get_account_delta_fungible_asset_ptr # => [fungible_delta_map_ptr, ASSET_KEY, ASSET_KEY, amount] @@ -663,7 +646,7 @@ end #! #! Where: #! - ASSET is the non-fungible asset to be added. -export.add_non_fungible_asset +pub proc add_non_fungible_asset dupw exec.memory::get_account_delta_non_fungible_asset_ptr # => [non_fungible_delta_map_ptr, ASSET, ASSET] @@ -697,7 +680,7 @@ end #! #! Where: #! - ASSET is the non-fungible asset to be removed. -export.remove_non_fungible_asset +pub proc remove_non_fungible_asset dupw exec.memory::get_account_delta_non_fungible_asset_ptr # => [non_fungible_delta_map_ptr, ASSET, ASSET] @@ -735,7 +718,8 @@ end #! - KEY is the key in the storage map that is being updated. #! - PREV_VALUE is the previous value of the key in the storage map that is being updated. #! - NEW_VALUE is the new value of the key in the storage map that is being updated. -export.set_map_item.8 +@locals(8) +pub proc set_map_item # retrieve the link map ptr to the storage map delta for the provided index exec.memory::get_account_delta_storage_map_ptr # => [account_delta_storage_map_ptr, KEY, PREV_VALUE, NEW_VALUE] @@ -828,7 +812,7 @@ end #! - is_delta_amount_positive indicates whether the delta amount is positive. #! - delta_amount_abs is the absolute value of the delta_amount, meaning negative values are #! remapped to their positive equivalent value. -proc.delta_amount_absolute +proc delta_amount_absolute dup exec.is_delta_amount_negative not # => [is_positive, delta_amount] @@ -849,7 +833,7 @@ end #! Where: #! - delta_amount is the delta amount that can span the entire felt range. #! - is_delta_amount_negative indicates whether the delta amount represents a negative value. -proc.is_delta_amount_negative +proc is_delta_amount_negative # delta_amount represents a negative number if it is greater than the max amount exec.asset::get_fungible_asset_max_amount gt # => [is_delta_amount_negative] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/asset.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm similarity index 85% rename from crates/miden-lib/asm/kernels/transaction/lib/asset.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/asset.masm index df64d68e36..6429f273d0 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/asset.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm @@ -1,22 +1,21 @@ -use.$kernel::account -use.$kernel::account_id +use $kernel::account_id # ERRORS # ================================================================================================= -const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO="malformed fungible asset: `ASSET[1]` must be 0" +const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO="malformed fungible asset: `ASSET[1]` must be 0" -const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID="malformed fungible asset: `ASSET[2]` and `ASSET[3]` must be a valid fungible faucet id" +const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID="malformed fungible asset: `ASSET[2]` and `ASSET[3]` must be a valid fungible faucet id" -const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS="malformed fungible asset: `ASSET[0]` exceeds the maximum allowed amount" +const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS="malformed fungible asset: `ASSET[0]` exceeds the maximum allowed amount" -const.ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID="malformed non-fungible asset: `ASSET[3]` is not a valid non-fungible faucet id" +const ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID="malformed non-fungible asset: `ASSET[3]` is not a valid non-fungible faucet id" -const.ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO="malformed non-fungible asset: the most significant bit must be 0" +const ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO="malformed non-fungible asset: the most significant bit must be 0" -const.ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the fungible asset is not this faucet" +const ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the fungible asset is not this faucet" -const.ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the non-fungible asset is not this faucet" +const ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the non-fungible asset is not this faucet" # CONSTANT ACCESSORS # ================================================================================================= @@ -28,7 +27,7 @@ const.ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the non-fungibl #! #! Where: #! - fungible_asset_max_amount is the maximum amount of a fungible asset. -export.::$kernel::util::asset::get_fungible_asset_max_amount +pub use ::$kernel::util::asset::get_fungible_asset_max_amount # PROCEDURES # ================================================================================================= @@ -43,7 +42,7 @@ export.::$kernel::util::asset::get_fungible_asset_max_amount #! #! Panics if: #! - the asset is not well formed. -export.validate_fungible_asset +pub proc validate_fungible_asset # assert that ASSET[1] == ZERO dup.2 not assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO # => [ASSET] @@ -58,7 +57,7 @@ export.validate_fungible_asset # => [ASSET] # assert that the max amount (ASSET[0]) of a fungible asset is not exceeded - dup.3 exec.get_fungible_asset_max_amount lte + dup.3 exec.::$kernel::util::asset::get_fungible_asset_max_amount lte assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS # => [ASSET] end @@ -71,7 +70,7 @@ end #! Where: #! - ASSET is the asset to check. #! - is_fungible_asset is a boolean indicating whether the asset is fungible. -export.is_fungible_asset +pub proc is_fungible_asset # check the first element, it will be: # - zero for a fungible asset # - non zero for a non-fungible asset @@ -89,7 +88,7 @@ end #! #! Panics if: #! - the asset is not well formed. -export.validate_non_fungible_asset +pub proc validate_non_fungible_asset # assert that ASSET[3] is a valid account ID prefix # hack: because we only have the prefix we add a 0 as the suffix which is always valid push.0 dup.1 exec.account_id::validate @@ -109,7 +108,7 @@ end #! Where: #! - ASSET is the asset to check. #! - is_non_fungible_asset is a boolean indicating whether the asset is non-fungible. -export.is_non_fungible_asset +pub proc is_non_fungible_asset # check the first element, it will be: # - zero for a fungible asset # - non zero for a non-fungible asset @@ -127,7 +126,7 @@ end #! #! Panics if: #! - the asset is not well formed. -export.validate_asset +pub proc validate_asset # check if the asset is fungible exec.is_fungible_asset # => [is_fungible_asset, ASSET] @@ -150,7 +149,7 @@ end #! Where: #! - faucet_id_prefix is the prefix of the faucet's account ID. #! - ASSET is the asset to validate. -export.validate_fungible_asset_origin +pub proc validate_fungible_asset_origin # assert the origin of the asset is the faucet_id provided via the stack dup.3 dup.3 # => [asset_id_prefix, asset_id_suffix, faucet_id_prefix, faucet_id_suffix, ASSET] @@ -171,7 +170,7 @@ end #! Where: #! - faucet_id_prefix is the prefix of the faucet's account ID. #! - ASSET is the asset to validate. -export.validate_non_fungible_asset_origin +pub proc validate_non_fungible_asset_origin # assert the origin of the asset is the faucet_id prefix provided via the stack dup.1 assert_eq.err=ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN # => [ASSET] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm similarity index 92% rename from crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm index 6d439c8e71..2559bed3bc 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm @@ -1,44 +1,36 @@ -use.std::collections::smt -use.std::word +use miden::core::collections::smt +use miden::core::word -use.$kernel::account_id -use.$kernel::asset -use.$kernel::memory +use $kernel::account_id +use $kernel::asset +use $kernel::memory # ERRORS # ================================================================================================= -const.ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET="get_balance can only be called on a fungible asset" +const ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET="get_balance can only be called on a fungible asset" -const.ERR_VAULT_PEEK_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET="peek_balance can only be called on a fungible asset" +const ERR_VAULT_PEEK_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET="peek_balance can only be called on a fungible asset" -const.ERR_VAULT_HAS_NON_FUNGIBLE_ASSET_PROC_CAN_BE_CALLED_ONLY_WITH_NON_FUNGIBLE_ASSET="the has_non_fungible_asset procedure can only be called on a non-fungible faucet" +const ERR_VAULT_HAS_NON_FUNGIBLE_ASSET_PROC_CAN_BE_CALLED_ONLY_WITH_NON_FUNGIBLE_ASSET="the has_non_fungible_asset procedure can only be called on a non-fungible faucet" -const.ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding the fungible asset to the vault would exceed the max amount of 9223372036854775807" +const ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding the fungible asset to the vault would exceed the max amount of 9223372036854775807" -const.ERR_VAULT_ADD_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to add fungible asset to the asset vault due to the initial value being invalid" +const ERR_VAULT_ADD_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to add fungible asset to the asset vault due to the initial value being invalid" -const.ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="the non-fungible asset already exists in the asset vault" +const ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="the non-fungible asset already exists in the asset vault" -const.ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW="failed to remove the fungible asset from the vault since the amount of the asset in the vault is less than the amount to remove" +const ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW="failed to remove the fungible asset from the vault since the amount of the asset in the vault is less than the amount to remove" -const.ERR_VAULT_REMOVE_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to remove fungible asset from the asset vault due to the initial value being invalid" +const ERR_VAULT_REMOVE_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to remove fungible asset from the asset vault due to the initial value being invalid" -const.ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND="failed to remove non-existent non-fungible asset from the vault" - -# EVENTS -# ================================================================================================= - -# The event is from `stdlib`, visible through the prefix and is _not_ a `TransactionEvent`. -# Care must be taken it stays in sync with the `miden-stdlib` definition. Note that generally speaking -# we should not emit `"stdlib"` events, consider this "hijacking", tread carefully. -const.SMT_PEEK_EVENT=event("stdlib::collections::smt::smt_peek") +const ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND="failed to remove non-existent non-fungible asset from the vault" # CONSTANTS # ================================================================================================= # The bitmask that when applied will set the fungible bit to zero. -const.INVERSE_FUNGIBLE_BITMASK_U32=0xffffffdf # last byte: 0b1101_1111 +const INVERSE_FUNGIBLE_BITMASK_U32=0xffffffdf # last byte: 0b1101_1111 # ACCESSORS # ================================================================================================= @@ -56,7 +48,7 @@ const.INVERSE_FUNGIBLE_BITMASK_U32=0xffffffdf # last byte: 0b1101_1111 #! #! Panics if: #! - the provided faucet ID is not an ID of a fungible faucet. -export.get_balance +pub proc get_balance # assert that the faucet id is a fungible faucet dup exec.account_id::is_fungible_faucet assert.err=ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET @@ -102,7 +94,7 @@ end #! #! Panics if: #! - the asset is not a fungible asset. -export.peek_balance +pub proc peek_balance # assert that the faucet id is a fungible faucet dup exec.account_id::is_fungible_faucet assert.err=ERR_VAULT_PEEK_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET @@ -118,7 +110,7 @@ export.peek_balance # => [ASSET_KEY, ASSET_VAULT_ROOT] # lookup asset - emit.SMT_PEEK_EVENT + exec.smt::peek # OS => [ASSET_KEY, ASSET_VAULT_ROOT] # AS => [ASSET] @@ -148,7 +140,7 @@ end #! #! Panics if: #! - the ASSET is a fungible asset. -export.has_non_fungible_asset +pub proc has_non_fungible_asset # check if the asset is a non-fungible asset exec.asset::is_non_fungible_asset assert.err=ERR_VAULT_HAS_NON_FUNGIBLE_ASSET_PROC_CAN_BE_CALLED_ONLY_WITH_NON_FUNGIBLE_ASSET @@ -190,7 +182,7 @@ end #! #! Panics if: #! - the total value of assets is greater than or equal to 2^63. -export.add_fungible_asset +pub proc add_fungible_asset # Create the asset key from the asset. # --------------------------------------------------------------------------------------------- @@ -211,7 +203,7 @@ export.add_fungible_asset mem_loadw_be swapw # => [ASSET_KEY, VAULT_ROOT, faucet_id_prefix, faucet_id_suffix, amount, vault_root_ptr] - emit.SMT_PEEK_EVENT adv_loadw + exec.smt::peek adv_loadw # => [CUR_VAULT_VALUE, VAULT_ROOT, faucet_id_prefix, faucet_id_suffix, amount, vault_root_ptr] swapw # => [VAULT_ROOT, CUR_VAULT_VALUE, faucet_id_prefix, faucet_id_suffix, amount, vault_root_ptr] @@ -294,7 +286,7 @@ end #! #! Panics if: #! - the vault already contains the same non-fungible asset. -export.add_non_fungible_asset +pub proc add_non_fungible_asset # Build the asset key from the non-fungible asset. # --------------------------------------------------------------------------------------------- @@ -341,7 +333,7 @@ end #! - the asset is not valid. #! - the total value of two fungible assets is greater than or equal to 2^63. #! - the vault already contains the same non-fungible asset. -export.add_asset +pub proc add_asset # check if the asset is a fungible asset exec.asset::is_fungible_asset # => [is_fungible_asset, ASSET] @@ -381,7 +373,8 @@ end #! #! Panics if: #! - the amount of the asset in the vault is less than the amount to be removed. -export.remove_fungible_asset.4 +@locals(4) +pub proc remove_fungible_asset exec.build_fungible_asset_vault_key # => [ASSET_KEY, ASSET, vault_root_ptr] @@ -465,7 +458,7 @@ end #! #! Panics if: #! - the non-fungible asset is not found in the vault. -export.remove_non_fungible_asset +pub proc remove_non_fungible_asset # build non-fungible asset key dupw exec.build_non_fungible_asset_vault_key padw # => [pad(4), ASSET_KEY, ASSET, vault_root_ptr] @@ -504,7 +497,7 @@ end #! - the fungible asset is not found in the vault. #! - the amount of the fungible asset in the vault is less than the amount to be removed. #! - the non-fungible asset is not found in the vault. -export.remove_asset +pub proc remove_asset # check if the asset is a fungible asset exec.asset::is_fungible_asset # => [is_fungible_asset, ASSET, vault_root_ptr] @@ -534,7 +527,7 @@ end #! - faucet_id_{prefix, suffix} are the prefix and suffix felts of the faucet id of the fungible #! asset of interest. #! - asset is the peeked asset from the vault. -proc.peek_asset +proc peek_asset # load the asset vault root from memory padw movup.8 mem_loadw_be # => [ASSET_VAULT_ROOT, ASSET_KEY] @@ -543,7 +536,7 @@ proc.peek_asset # => [ASSET_KEY, ASSET_VAULT_ROOT] # lookup asset - emit.SMT_PEEK_EVENT + exec.smt::peek # OS => [ASSET_KEY, ASSET_VAULT_ROOT] # AS => [ASSET] @@ -566,7 +559,7 @@ end #! Where: #! - ASSET is the non-fungible asset for which the vault key is built. #! - ASSET_KEY is the vault key of the non-fungible asset. -export.build_non_fungible_asset_vault_key +pub proc build_non_fungible_asset_vault_key # create the asset key from the non-fungible asset by swapping hash0 with the faucet id # => [faucet_id_prefix, hash2, hash1, hash0] swap.3 @@ -596,7 +589,7 @@ end #! Where: #! - ASSET is the fungible asset for which the vault key is built. #! - ASSET_KEY is the vault key of the fungible asset. -export.build_fungible_asset_vault_key +pub proc build_fungible_asset_vault_key # => [faucet_id_prefix, faucet_id_suffix, 0, amount] push.0.0 diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm b/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm new file mode 100644 index 0000000000..5c58398d3c --- /dev/null +++ b/crates/miden-protocol/asm/kernels/transaction/lib/constants.masm @@ -0,0 +1,37 @@ +# CONSTANTS +# ================================================================================================= + +# The number of elements in a Word +pub const WORD_SIZE = 4 + +# The maximum number of input values associated with a single note. +pub const MAX_INPUTS_PER_NOTE = 1024 + +# The maximum number of assets that can be stored in a single note. +pub const MAX_ASSETS_PER_NOTE = 256 + +# 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 = 2048 + +# The depth of the Merkle tree used to commit to notes produced in a block. +pub const NOTE_TREE_DEPTH = 16 + +# The maximum number of notes that can be created in a single transaction. +pub const MAX_OUTPUT_NOTES_PER_TX = 1024 + +# The number of field elements per account procedure data entry (procedure MAST root). +pub const ACCOUNT_PROCEDURE_DATA_LENGTH = 4 + +# TYPES +# ================================================================================================= + +# Root of an empty Sparse Merkle Tree +pub const EMPTY_SMT_ROOT = [15321474589252129342, 17373224439259377994, 15071539326562317628, 3312677166725950353] + +# Type of storage slot item in the account storage +pub const STORAGE_SLOT_TYPE_VALUE = 0 +pub const STORAGE_SLOT_TYPE_MAP = 1 +pub const STORAGE_SLOT_TYPE_ARRAY = 2 diff --git a/crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm similarity index 93% rename from crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index f7281e5be9..52916708b9 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -1,54 +1,55 @@ -use.$kernel::account -use.$kernel::account_delta -use.$kernel::asset_vault -use.$kernel::constants -use.$kernel::memory -use.$kernel::note +use $kernel::account +use $kernel::account_delta +use $kernel::asset_vault +use $kernel::constants::NOTE_MEM_SIZE +use $kernel::memory +use $kernel::note -use.std::word +use miden::core::crypto::hashes::rpo256 +use miden::core::word # ERRORS # ================================================================================================= -const.ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME="total number of assets in the account and all involved notes must stay the same" +const ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME="total number of assets in the account and all involved notes must stay the same" -const.ERR_EPILOGUE_EXECUTED_TRANSACTION_IS_EMPTY="executed transaction neither changed the account state, nor consumed any notes" +const ERR_EPILOGUE_EXECUTED_TRANSACTION_IS_EMPTY="executed transaction neither changed the account state, nor consumed any notes" -const.ERR_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT="auth procedure had been called from outside the epilogue" +const ERR_EPILOGUE_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT="auth procedure has been called from outside the epilogue" -const.ERR_EPILOGUE_NONCE_CANNOT_BE_0="nonce cannot be 0 after an account-creating transaction" +const ERR_EPILOGUE_NONCE_CANNOT_BE_0="nonce cannot be 0 after an account-creating transaction" # CONSTANTS # ================================================================================================= # Event emitted to signal that the compute_fee procedure has obtained the current number of cycles. -const.EPILOGUE_AFTER_TX_CYCLES_OBTAINED_EVENT=event("miden::epilogue::after_tx_cycles_obtained") +const EPILOGUE_AFTER_TX_CYCLES_OBTAINED_EVENT=event("miden::epilogue::after_tx_cycles_obtained") # Event emitted to signal that the fee was computed. -const.EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT_EVENT=event("miden::epilogue::before_tx_fee_removed_from_account") +const EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT_EVENT=event("miden::epilogue::before_tx_fee_removed_from_account") # Event emitted to signal that an execution of the authentication procedure has started. -const.EPILOGUE_AUTH_PROC_START_EVENT=event("miden::epilogue::auth_proc_start") +const EPILOGUE_AUTH_PROC_START_EVENT=event("miden::epilogue::auth_proc_start") # Event emitted to signal that an execution of the authentication procedure has ended. -const.EPILOGUE_AUTH_PROC_END_EVENT=event("miden::epilogue::auth_proc_end") +const EPILOGUE_AUTH_PROC_END_EVENT=event("miden::epilogue::auth_proc_end") # An additional number of cyclces to account for the number of cycles that smt::set will take when # removing the computed fee from the asset vault. # Theoretically, this can safely be set to worst_case_cycles - best_case_cycles of smt::set. That's # because we can assume that the measured number of cycles already contains at least the best case # number of cycles, and so we only need to add the difference. -const.SMT_SET_ADDITIONAL_CYCLES=250 +const SMT_SET_ADDITIONAL_CYCLES=250 # The number of cycles the epilogue is estimated to take after compute_fee has been executed, # including an unknown cycle number of the above-mentioned call to smt::set. It is safe to assume # that this includes at least smt::set's best case number of cycles. # This can be _estimated_ using the transaction measurements on ExecutedTransaction and can be set # to the lowest observed value. -const.NUM_POST_COMPUTE_FEE_CYCLES=500 +const NUM_POST_COMPUTE_FEE_CYCLES=500 # The number of cycles the epilogue is estimated to take after compute_fee has been executed. -const.ESTIMATED_AFTER_COMPUTE_FEE_CYCLES=NUM_POST_COMPUTE_FEE_CYCLES+SMT_SET_ADDITIONAL_CYCLES +const ESTIMATED_AFTER_COMPUTE_FEE_CYCLES=NUM_POST_COMPUTE_FEE_CYCLES+SMT_SET_ADDITIONAL_CYCLES # OUTPUT NOTES PROCEDURES # ================================================================================================= @@ -72,7 +73,7 @@ const.ESTIMATED_AFTER_COMPUTE_FEE_CYCLES=NUM_POST_COMPUTE_FEE_CYCLES+SMT_SET_ADD #! - output_note_ptr is the start boundary of the output notes section. #! - output_notes_end_ptr is the end boundary of the output notes section. #! - mem[i] is the memory value stored at some address i. -proc.copy_output_notes_to_advice_map +proc copy_output_notes_to_advice_map # get the number of notes created by the transaction exec.memory::get_num_output_notes # => [num_notes, OUTPUT_NOTES_COMMITMENT] @@ -117,7 +118,7 @@ end #! #! Inputs: [] #! Outputs: [] -proc.build_output_vault +proc build_output_vault # copy final account vault root to output account vault root exec.memory::get_account_vault_root exec.memory::set_output_vault_root dropw # => [] @@ -187,7 +188,7 @@ proc.build_output_vault # => [note_data_ptr, output_note_end_ptr] # increment output note pointer and check if we should loop again - exec.constants::get_note_mem_size add dup.1 dup.1 neq + push.NOTE_MEM_SIZE add dup.1 dup.1 neq # => [should_loop, output_note_ptr, output_notes_end_ptr] end @@ -203,7 +204,7 @@ end #! #! Inputs: [] #! Outputs: [] -proc.execute_auth_procedure +proc execute_auth_procedure emit.EPILOGUE_AUTH_PROC_START_EVENT padw padw padw @@ -220,7 +221,7 @@ proc.execute_auth_procedure # if auth procedure was called already, it must have been called by a user, which is disallowed exec.account::was_procedure_called - assertz.err=ERR_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT + assertz.err=ERR_EPILOGUE_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT # => [auth_procedure_ptr, AUTH_ARGS, pad(12)] # execute the auth procedure @@ -247,7 +248,7 @@ end #! #! Where: #! - fee_amount is the computed fee amount of the transaction in the native asset. -proc.compute_fee +proc compute_fee # get the number of cycles the transaction has taken to execute up this point clk # => [num_current_cycles] @@ -282,7 +283,7 @@ end #! - fee_amount is the computed fee amount of the transaction in the native asset. #! - FEE_ASSET is the fungible asset with amount set to fee_amount and the faucet ID set to the #! native asset. -proc.build_native_fee_asset +proc build_native_fee_asset exec.memory::get_native_asset_id # => [native_asset_id_prefix, native_asset_id_suffix, fee_amount] @@ -308,7 +309,7 @@ end #! #! Panics if: #! - the account vault does not contain the computed fee. -proc.compute_and_remove_fee +proc compute_and_remove_fee # compute the fee the tx needs to pay exec.compute_fee # => [fee_amount] @@ -377,7 +378,8 @@ end #! changing the account's state. The latter is validated as part of computing the account delta #! commitment. #! - the account vault does not contain the computed fee. -export.finalize_transaction.12 +@locals(12) +pub proc finalize_transaction # make sure that the context was switched back to the native account exec.memory::assert_native_account @@ -487,7 +489,7 @@ export.finalize_transaction.12 adv.insert_hdword # => [ACCOUNT_DELTA_COMMITMENT, FINAL_ACCOUNT_COMMITMENT] - hmerge + exec.rpo256::merge # => [ACCOUNT_UPDATE_COMMITMENT] # ------ Build output stack ------ diff --git a/crates/miden-lib/asm/kernels/transaction/lib/faucet.masm b/crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm similarity index 89% rename from crates/miden-lib/asm/kernels/transaction/lib/faucet.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm index 7cd5f4e515..e0cdb36314 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/faucet.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm @@ -1,21 +1,21 @@ -use.$kernel::account -use.$kernel::account_id -use.$kernel::asset -use.$kernel::asset_vault -use.$kernel::memory +use $kernel::account +use $kernel::account_id +use $kernel::asset +use $kernel::asset_vault +use $kernel::memory # ERRORS # ================================================================================================= -const.ERR_FAUCET_NEW_TOTAL_SUPPLY_WOULD_EXCEED_MAX_ASSET_AMOUNT="asset mint operation would cause the new total supply to exceed the maximum allowed asset amount" +const ERR_FAUCET_NEW_TOTAL_SUPPLY_WOULD_EXCEED_MAX_ASSET_AMOUNT="asset mint operation would cause the new total supply to exceed the maximum allowed asset amount" -const.ERR_FAUCET_BURN_CANNOT_EXCEED_EXISTING_TOTAL_SUPPLY="asset amount to burn can not exceed the existing total supply" +const ERR_FAUCET_BURN_CANNOT_EXCEED_EXISTING_TOTAL_SUPPLY="asset amount to burn can not exceed the existing total supply" -const.ERR_FAUCET_NON_FUNGIBLE_ASSET_ALREADY_ISSUED="failed to mint new non-fungible asset because it was already issued" +const ERR_FAUCET_NON_FUNGIBLE_ASSET_ALREADY_ISSUED="failed to mint new non-fungible asset because it was already issued" -const.ERR_FAUCET_BURN_NON_FUNGIBLE_ASSET_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET="the burn_non_fungible_asset procedure can only be called on a non-fungible faucet" +const ERR_FAUCET_BURN_NON_FUNGIBLE_ASSET_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET="the burn_non_fungible_asset procedure can only be called on a non-fungible faucet" -const.ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND="failed to burn non-existent non-fungible asset in the vault" +const ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND="failed to burn non-existent non-fungible asset in the vault" # FUNGIBLE ASSETS # ================================================================================================== @@ -36,14 +36,14 @@ const.ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND="failed to burn non-existe #! executed against. #! - the asset is not well formed. #! - the total issuance after minting is greater than the maximum amount allowed. -export.mint_fungible_asset +pub proc mint_fungible_asset # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid exec.account::get_id exec.asset::validate_fungible_asset_origin # => [ASSET] # get the current total issuance - exec.account::get_faucet_storage_data_slot exec.account::get_item + exec.account::get_faucet_sysdata_slot_id exec.account::get_item # => [TOTAL_ISSUANCE, ASSET] # prepare stack to ensure that minting the asset will not exceed the maximum @@ -55,7 +55,7 @@ export.mint_fungible_asset # => [amount, TOTAL_ISSUANCE, ASSET] # update the total issuance - add exec.account::get_faucet_storage_data_slot exec.account::set_item dropw + add exec.account::get_faucet_sysdata_slot_id exec.account::set_item dropw # => [ASSET] # add the asset to the input vault for asset preservation checks @@ -78,14 +78,14 @@ end #! executed against. #! - the asset is not well formed. #! - the amount being burned is greater than the total input to the transaction. -proc.burn_fungible_asset +proc burn_fungible_asset # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid exec.account::get_id exec.asset::validate_fungible_asset_origin # => [ASSET] # fetch TOTAL_ISSUANCE such that we can compute the new total issuance - exec.account::get_faucet_storage_data_slot exec.account::get_item + exec.account::get_faucet_sysdata_slot_id exec.account::get_item # => [TOTAL_ISSUANCE, ASSET] # assert that the asset amount being burned is less or equal to the total issuance @@ -93,7 +93,7 @@ proc.burn_fungible_asset # => [amount, TOTAL_ISSUANCE, ASSET] # compute new total issuance - sub exec.account::get_faucet_storage_data_slot exec.account::set_item dropw + sub exec.account::get_faucet_sysdata_slot_id exec.account::set_item dropw # => [ASSET] # remove the asset from the input vault @@ -109,9 +109,9 @@ end #! Where: #! - total_issuance is the total issuance of the fungible faucet the transaction is being executed #! against. -export.get_total_issuance +pub proc get_total_issuance # fetch the TOTAL_ISSUANCE from storage - exec.account::get_faucet_storage_data_slot exec.account::get_item + exec.account::get_faucet_sysdata_slot_id exec.account::get_item # => [TOTAL_ISSUANCE] # extract the total_issuance and purge the padding @@ -136,7 +136,7 @@ end #! - the non-fungible asset being minted is not associated with the faucet the transaction is being #! executed against. #! - the non-fungible asset being minted already exists. -proc.mint_non_fungible_asset +proc mint_non_fungible_asset # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid exec.account::get_id swap drop exec.asset::validate_non_fungible_asset_origin @@ -147,11 +147,11 @@ proc.mint_non_fungible_asset # => [ASSET_KEY, ASSET, ASSET] # get the faucet storage data slot - exec.account::get_faucet_storage_data_slot - # => [faucet_storage_data_slot, ASSET_KEY, ASSET, ASSET] + exec.account::get_faucet_sysdata_slot_id + # => [faucet_slot_name_prefix, faucet_slot_name_suffix, ASSET_KEY, ASSET, ASSET] # insert the non-fungible asset into the tracking SMT - exec.account::set_map_item dropw + exec.account::set_map_item # => [OLD_VAL, ASSET] # Assert the `OLD_VAL` is an EMPTY_WORD, indicating that the non-fungible asset has not been @@ -180,7 +180,7 @@ end #! executed against. #! - the non-fungible asset being burned does not exist or was not provided as input to the #! transaction via a note or the accounts vault. -proc.burn_non_fungible_asset +proc burn_non_fungible_asset # assert that we are executing a transaction against the non-fungible faucet (access checks) exec.account::get_id swap drop exec.account_id::is_non_fungible_faucet assert.err=ERR_FAUCET_BURN_NON_FUNGIBLE_ASSET_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET @@ -195,11 +195,11 @@ proc.burn_non_fungible_asset # => [ASSET_KEY, EMPTY_WORD, ASSET] # get the faucet storage data slot - exec.account::get_faucet_storage_data_slot + exec.account::get_faucet_sysdata_slot_id # => [faucet_storage_data_slot, ASSET_KEY, EMPTY_WORD, ASSET] # remove the non-fungible asset from the tracking SMT - exec.account::set_map_item dropw + exec.account::set_map_item # => [OLD_VAL, ASSET] # Assert the `OLD_VAL` is not an EMPTY_WORD, indicating that the non-fungible asset exists. We @@ -225,7 +225,7 @@ end #! Panics if: #! - the ASSET is a fungible asset. #! - the ASSET is not associated with the faucet the transaction is being executed against. -export.is_non_fungible_asset_issued +pub proc is_non_fungible_asset_issued # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid exec.account::get_id swap drop exec.asset::validate_non_fungible_asset_origin @@ -236,7 +236,7 @@ export.is_non_fungible_asset_issued # => [ASSET_KEY] # get the storage index where faucet's assets map is stored - exec.account::get_faucet_storage_data_slot + exec.account::get_faucet_sysdata_slot_id # => [map_slot_index, ASSET_KEY] # get the non-fungible asset stored by the computed account key @@ -270,7 +270,7 @@ end #! - For fungible faucets if the total issuance after minting is greater than the maximum amount #! allowed. #! - For non-fungible faucets if the non-fungible asset being minted already exists. -export.mint +pub proc mint # check if the asset is a fungible asset exec.asset::is_fungible_asset # => [is_fungible_asset, ASSET] @@ -303,7 +303,7 @@ end #! transaction. #! - For non-fungible faucets if the non-fungible asset being burned does not exist or was not #! provided as input to the transaction via a note or the accounts vault. -export.burn +pub proc burn # check if the asset is a fungible asset exec.asset::is_fungible_asset # => [is_fungible_asset, ASSET] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/input_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/input_note.masm similarity index 92% rename from crates/miden-lib/asm/kernels/transaction/lib/input_note.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/input_note.masm index 717bc67ee0..dd247047ce 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/input_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/input_note.masm @@ -1,9 +1,9 @@ -use.$kernel::memory +use $kernel::memory # ERRORS # ================================================================================================= -const.ERR_INPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested input note index should be less than the total number of input notes" +const ERR_INPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested input note index should be less than the total number of input notes" # INPUT NOTE PROCEDURES # ================================================================================================= @@ -17,7 +17,7 @@ const.ERR_INPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested input note index should be l #! - input_note_ptr is the memory address of the data segment for the requested input note. #! - num_assets is the number of assets in the specified note. #! - ASSETS_COMMITMENT is a sequential hash of the assets in the specified note. -export.get_assets_info +pub proc get_assets_info # get the number of assets in the note dup exec.memory::get_input_note_num_assets # => [num_assets, input_note_ptr] @@ -31,7 +31,7 @@ end #! #! Inputs: [note_index] #! Outputs: [note_index] -export.assert_note_index_in_bounds +pub proc assert_note_index_in_bounds # assert that the provided note index is less than the total number of notes dup exec.memory::get_num_input_notes # => [input_notes_num, note_index, note_index] @@ -53,7 +53,7 @@ end #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. -export.get_input_note_ptr +pub proc get_input_note_ptr # asset that the provided input note index is valid exec.assert_note_index_in_bounds # => [idx] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/link_map.masm b/crates/miden-protocol/asm/kernels/transaction/lib/link_map.masm similarity index 94% rename from crates/miden-lib/asm/kernels/transaction/lib/link_map.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/link_map.masm index bef4faffff..caa4323e13 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/link_map.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/link_map.masm @@ -1,6 +1,6 @@ -use.std::collections::smt -use.std::word -use.$kernel::memory +use miden::core::collections::smt +use miden::core::word +use $kernel::memory # A link map is a map data structure based on a sorted linked list. # @@ -119,58 +119,58 @@ use.$kernel::memory # ERRORS # ================================================================================================= -const.ERR_LINK_MAP_CANNOT_BE_EMPTY_ON_ABSENCE_AFTER_ENTRY="map cannot be empty when proving absence after an entry" +const ERR_LINK_MAP_CANNOT_BE_EMPTY_ON_ABSENCE_AFTER_ENTRY="map cannot be empty when proving absence after an entry" -const.ERR_LINK_MAP_PROVIDED_KEY_NOT_EQUAL_TO_ENTRY_KEY="provided key does not match key in map entry" +const ERR_LINK_MAP_PROVIDED_KEY_NOT_EQUAL_TO_ENTRY_KEY="provided key does not match key in map entry" -const.ERR_LINK_MAP_PROVIDED_KEY_NOT_GREATER_THAN_ENTRY_KEY="provided key is not greater than the entry key" +const ERR_LINK_MAP_PROVIDED_KEY_NOT_GREATER_THAN_ENTRY_KEY="provided key is not greater than the entry key" -const.ERR_LINK_MAP_PROVIDED_KEY_NOT_LESS_THAN_ENTRY_KEY="provided key is not less than the entry key" +const ERR_LINK_MAP_PROVIDED_KEY_NOT_LESS_THAN_ENTRY_KEY="provided key is not less than the entry key" -const.ERR_LINK_MAP_ENTRY_PTR_IS_OUTSIDE_VALID_MEMORY_REGION="host-provided entry ptr is outside the valid memory region" +const ERR_LINK_MAP_ENTRY_PTR_IS_OUTSIDE_VALID_MEMORY_REGION="host-provided entry ptr is outside the valid memory region" -const.ERR_LINK_MAP_ENTRY_PTR_IS_NOT_ENTRY_ALIGNED="host-provided entry ptr is not 'link map entry'-aligned" +const ERR_LINK_MAP_ENTRY_PTR_IS_NOT_ENTRY_ALIGNED="host-provided entry ptr is not 'link map entry'-aligned" -const.ERR_LINK_MAP_MAP_PTR_IN_ENTRY_DOES_NOT_MATCH_EXPECTED_MAP_PTR="map ptr stored in host-provided entry does not match actual pointer of the map" +const ERR_LINK_MAP_MAP_PTR_IN_ENTRY_DOES_NOT_MATCH_EXPECTED_MAP_PTR="map ptr stored in host-provided entry does not match actual pointer of the map" # CONSTANTS # ================================================================================================= # The offset of the prev_entry_ptr in an entry's metadata. -const.PREV_ENTRY_PTR_OFFSET=1 +const PREV_ENTRY_PTR_OFFSET=1 # The offset of the next_entry_ptr in an entry's metadata. -const.NEXT_ENTRY_PTR_OFFSET=2 +const NEXT_ENTRY_PTR_OFFSET=2 # The offset of the KEY in an entry. -const.KEY_OFFSET=4 +const KEY_OFFSET=4 # The offset of the VALUE0 in an entry. -const.VALUE0_OFFSET=8 +const VALUE0_OFFSET=8 # The offset of the VALUE1 in an entry. -const.VALUE1_OFFSET=12 +const VALUE1_OFFSET=12 # The value of the Update operation for a set event. -const.INSERT_OPERATION_UPDATE=0 +const INSERT_OPERATION_UPDATE=0 # The value of the InsertAtHead operation for a set event. -const.INSERT_OPERATION_AT_HEAD=1 +const INSERT_OPERATION_AT_HEAD=1 # The value of the Found operation for a get event. -const.GET_OPERATION_FOUND=0 +const GET_OPERATION_FOUND=0 # The value of the AbsentAtHead operation for a get event. -const.GET_OPERATION_ABSENT_AT_HEAD=1 +const GET_OPERATION_ABSENT_AT_HEAD=1 # EVENTS # ================================================================================================= # Event emitted when an entry is set. -const.LINK_MAP_SET_EVENT=event("miden::link_map::set") +const LINK_MAP_SET_EVENT=event("miden::link_map::set") # Event emitted when an entry is fetched. -const.LINK_MAP_GET_EVENT=event("miden::link_map::get") +const LINK_MAP_GET_EVENT=event("miden::link_map::get") # LINK MAP PROCEDURES # ================================================================================================= @@ -198,7 +198,7 @@ const.LINK_MAP_GET_EVENT=event("miden::link_map::get") #! Panics if: #! - the host provides faulty advice. See panic sections of assert_entry_ptr_is_valid, #! update_entry, insert_at_head, insert_after_entry. -export.set +pub proc set emit.LINK_MAP_SET_EVENT adv_push.2 # => [operation, entry_ptr, map_ptr, KEY, VALUE0, VALUE1] @@ -265,7 +265,7 @@ end #! Panics if: #! - the host provides faulty advice. See panic sections of assert_entry_ptr_is_valid, #! get_existing_value, assert_absent_at_head, assert_absent_after_entry. -export.get +pub proc get emit.LINK_MAP_GET_EVENT adv_push.2 # => [get_operation, entry_ptr, map_ptr, KEY] @@ -318,7 +318,7 @@ end #! #! Inputs: [map_ptr] #! Outputs: [is_empty] -export.is_empty +pub proc is_empty mem_load eq.0 end @@ -328,7 +328,7 @@ end #! #! Inputs: [map_ptr] #! Outputs: [has_next, iter] -export.iter +pub proc iter exec.get_head # => [entry_ptr] @@ -349,7 +349,7 @@ end #! #! Inputs: [iter] #! Outputs: [KEY, VALUE0, VALUE1, has_next, next_iter] -export.next_key_double_value +pub proc next_key_double_value dup exec.next_key # => [KEY, has_next, next_entry_ptr, entry_ptr = iter] @@ -367,7 +367,7 @@ end #! #! Inputs: [iter] #! Outputs: [KEY, VALUE0, has_next, next_iter] -export.next_key_value +pub proc next_key_value dup exec.next_key # => [KEY, has_next, next_entry_ptr, entry_ptr = iter] @@ -385,7 +385,7 @@ end #! #! Inputs: [iter] #! Outputs: [KEY, has_next, next_iter] -export.next_key +pub proc next_key dup exec.get_next_entry_ptr # => [next_entry_ptr, entry_ptr = iter] @@ -406,7 +406,7 @@ end #! #! Panics if: #! - the KEY is not less than the key in the head of the map, unless the map is empty. -proc.insert_at_head +proc insert_at_head exec.memory::link_map_malloc # => [entry_ptr, map_ptr, KEY, VALUE0, VALUE1] @@ -465,7 +465,7 @@ end #! #! Panics if: #! - the key in the entry does not match the provided KEY. -proc.update_entry +proc update_entry dup movdn.5 # => [entry_ptr, KEY, entry_ptr, VALUE0, VALUE1] @@ -485,7 +485,7 @@ end #! - the KEY is not greater than the key in the prev entry. #! - the KEY is not less than the key in prev_entry.next_entry, unless the prev entry is the last #! one in the map. -proc.insert_after_entry +proc insert_after_entry movdn.5 # => [map_ptr, KEY, prev_entry_ptr, VALUE0, VALUE1] @@ -567,7 +567,7 @@ end #! #! Inputs: [entry_ptr, map_ptr, KEY, VALUE0, VALUE1] #! Outputs: [] -proc.insert_pair +proc insert_pair swap dup.1 # => [entry_ptr, map_ptr, entry_ptr, KEY, VALUE0, VALUE1] @@ -597,7 +597,7 @@ end #! #! Panics if: #! - the KEY is not less than the key in the head of the map, unless the map is empty. -proc.assert_absent_at_head +proc assert_absent_at_head dup exec.is_empty # => [is_empty, map_ptr, KEY] @@ -627,7 +627,7 @@ end #! - the map is empty. #! - the KEY is not greater than the key in the entry. #! - the KEY is not less than the key in entry.next_entry, unless the entry is the last one in the map. -proc.assert_absent_after_entry +proc assert_absent_after_entry swap exec.is_empty assertz.err=ERR_LINK_MAP_CANNOT_BE_EMPTY_ON_ABSENCE_AFTER_ENTRY # => [entry_ptr, KEY] @@ -666,7 +666,7 @@ end #! #! Panics if: #! - the key in the entry does not match KEY. -proc.get_existing_value +proc get_existing_value movdn.4 dup.4 # => [entry_ptr, KEY, entry_ptr] @@ -684,7 +684,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [has_next] -proc.has_next +proc has_next exec.get_next_entry_ptr eq.0 not end @@ -692,7 +692,7 @@ end #! #! Inputs: [entry_ptr, next_entry_ptr] #! Outputs: [] -proc.set_next_entry_ptr +proc set_next_entry_ptr add.NEXT_ENTRY_PTR_OFFSET mem_store # => [] end @@ -701,7 +701,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [next_entry_ptr] -proc.get_next_entry_ptr +proc get_next_entry_ptr add.NEXT_ENTRY_PTR_OFFSET mem_load # => [next_entry_ptr] end @@ -710,7 +710,7 @@ end #! #! Inputs: [entry_ptr, prev_entry_ptr] #! Outputs: [] -proc.set_prev_entry_ptr +proc set_prev_entry_ptr add.PREV_ENTRY_PTR_OFFSET mem_store # => [] end @@ -719,7 +719,7 @@ end #! #! Inputs: [entry_ptr, VALUE0, VALUE1] #! Outputs: [] -proc.set_value +proc set_value dup movdn.5 # => [entry_ptr, VALUE0, entry_ptr, VALUE1] @@ -734,7 +734,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [VALUE0, VALUE1] -proc.get_values +proc get_values dup exec.get_value1 # => [VALUE1, entry_ptr] @@ -746,7 +746,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [VALUE0] -proc.get_value0 +proc get_value0 padw movup.4 # => [entry_ptr, pad(4)] @@ -758,7 +758,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [VALUE1] -proc.get_value1 +proc get_value1 padw movup.4 # => [entry_ptr, pad(4)] @@ -770,7 +770,7 @@ end #! #! Inputs: [entry_ptr, KEY] #! Outputs: [] -proc.set_key +proc set_key add.KEY_OFFSET mem_storew_be dropw end @@ -778,7 +778,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [KEY] -proc.get_key +proc get_key padw movup.4 # => [entry_ptr, pad(4)] @@ -790,7 +790,7 @@ end #! #! Inputs: [map_ptr, entry_ptr] #! Outputs: [] -proc.set_head +proc set_head mem_store end @@ -798,7 +798,7 @@ end #! #! Inputs: [map_ptr] #! Outputs: [entry_ptr] -proc.get_head +proc get_head mem_load end @@ -806,7 +806,7 @@ end #! #! Inputs: [entry_ptr, map_ptr] #! Outputs: [] -proc.set_map_ptr +proc set_map_ptr mem_store end @@ -814,7 +814,7 @@ end #! #! Inputs: [entry_ptr] #! Outputs: [map_ptr] -proc.get_map_ptr +proc get_map_ptr mem_load end @@ -825,7 +825,7 @@ end #! #! Inputs: [entry_ptr, KEY] #! Outputs: [] -proc.assert_key_is_equal +proc assert_key_is_equal exec.get_key swapw # => [KEY, ENTRY_KEY] @@ -837,7 +837,7 @@ end #! #! Inputs: [entry_ptr, KEY] #! Outputs: [] -proc.assert_key_is_greater +proc assert_key_is_greater exec.get_key # => [ENTRY_KEY, KEY] @@ -848,7 +848,7 @@ end #! #! Inputs: [entry_ptr, KEY] #! Outputs: [] -proc.assert_key_is_less +proc assert_key_is_less exec.get_key # => [ENTRY_KEY, KEY] @@ -867,7 +867,7 @@ end #! - entry ptr is "link map entry"-aligned, i.e. entry_ptr % LINK_MAP_ENTRY_SIZE == 0. This #! works because every entry ptr is a multiple of LINK_MAP_ENTRY_SIZE. #! - entry's map ptr is equal to the given map_ptr. -export.assert_entry_ptr_is_valid +pub proc assert_entry_ptr_is_valid # Check entry pointer is in valid memory range. # ------------------------------------------------------------------------------------------------- diff --git a/crates/miden-lib/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm similarity index 79% rename from crates/miden-lib/asm/kernels/transaction/lib/memory.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 5223ca6e48..2d9e11ef30 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -1,20 +1,22 @@ -use.$kernel::constants -use.std::mem +use $kernel::constants::ACCOUNT_PROCEDURE_DATA_LENGTH +use $kernel::constants::MAX_ASSETS_PER_NOTE +use $kernel::constants::NOTE_MEM_SIZE +use miden::core::mem # 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 exceed 255" -const.ERR_ACCOUNT_IS_NOT_NATIVE="the active account is not native" +const ERR_ACCOUNT_IS_NOT_NATIVE="the active account is not native" -const.ERR_ACCOUNT_STACK_OVERFLOW="depth of the nested FPI calls exceeded 64" +const ERR_ACCOUNT_STACK_OVERFLOW="depth of the nested FPI calls exceeded 64" -const.ERR_ACCOUNT_STACK_UNDERFLOW="failed to end foreign context because the active account is the native account" +const ERR_ACCOUNT_STACK_UNDERFLOW="failed to end foreign context because the active account is the native account" -const.ERR_FOREIGN_ACCOUNT_CONTEXT_AGAINST_NATIVE_ACCOUNT="creation of a foreign context against the native account is forbidden" +const ERR_FOREIGN_ACCOUNT_CONTEXT_AGAINST_NATIVE_ACCOUNT="creation of a foreign context against the native account is forbidden" -const.ERR_LINK_MAP_MAX_ENTRIES_EXCEEDED="number of link map entries exceeds maximum" +const ERR_LINK_MAP_MAX_ENTRIES_EXCEEDED="number of link map entries exceeds maximum" # MEMORY ADDRESS CONSTANTS # ================================================================================================= @@ -23,134 +25,134 @@ const.ERR_LINK_MAP_MAX_ENTRIES_EXCEEDED="number of link map entries exceeds maxi # ------------------------------------------------------------------------------------------------- # The memory address at which a pointer to the currently active input note is stored. -const.ACTIVE_INPUT_NOTE_PTR=0 +const ACTIVE_INPUT_NOTE_PTR=0 # The memory address at which the number of output notes is stored. -const.NUM_OUTPUT_NOTES_PTR=4 +const NUM_OUTPUT_NOTES_PTR=4 # The memory address at which the input vault root is stored. -const.INPUT_VAULT_ROOT_PTR=8 +const INPUT_VAULT_ROOT_PTR=8 # The memory address at which the output vault root is stored. -const.OUTPUT_VAULT_ROOT_PTR=12 +const OUTPUT_VAULT_ROOT_PTR=12 # The memory address at which the dirty flag of the storage commitment of the native account is # stored. # # This binary flag specifies whether the commitment is outdated: it holds 1 if some changes were # made to the account storage since the last re-computation, and 0 otherwise. -const.NATIVE_ACCT_STORAGE_COMMITMENT_DIRTY_FLAG_PTR=16 +const NATIVE_ACCT_STORAGE_COMMITMENT_DIRTY_FLAG_PTR=16 # The memory address at which the absolute expiration block number is stored. -const.TX_EXPIRATION_BLOCK_NUM_PTR=20 +const TX_EXPIRATION_BLOCK_NUM_PTR=20 # The memory address at which the pointer to the account stack element containing the pointer to the # currently accessing account (active account) data is stored. -const.ACCOUNT_STACK_TOP_PTR=24 +const ACCOUNT_STACK_TOP_PTR=24 # Pointer to the first element on the account stack. -const.MIN_ACCOUNT_STACK_PTR=25 +const MIN_ACCOUNT_STACK_PTR=25 # Pointer to the last element on the account stack. -const.MAX_ACCOUNT_STACK_PTR=88 +const MAX_ACCOUNT_STACK_PTR=88 # GLOBAL INPUTS # ------------------------------------------------------------------------------------------------- # The memory address at which the global inputs section begins. -const.GLOBAL_INPUTS_SECTION_OFFSET=400 +const GLOBAL_INPUTS_SECTION_OFFSET=400 # The memory address at which the transaction reference block's commitment is stored. -const.BLOCK_COMMITMENT_PTR=400 +const BLOCK_COMMITMENT_PTR=400 # The memory address at which the native account ID is stored. -const.NATIVE_ACCT_ID_PTR=404 +const NATIVE_ACCT_ID_PTR=404 # The memory address at which the initial account commitment is stored. -const.INIT_ACCOUNT_COMMITMENT_PTR=408 +const INIT_ACCOUNT_COMMITMENT_PTR=408 # The memory address at which the initial nonce is stored. -const.INIT_NONCE_PTR=412 +const INIT_NONCE_PTR=412 # The memory address at which the initial vault root of the native account is stored. -const.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR=416 +const INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR=416 # The memory address at which the initial storage commitment of the native account is stored. -const.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR=420 +const INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR=420 # The memory address at which the input notes commitment is stored. -const.INPUT_NOTES_COMMITMENT_PTR=424 +const INPUT_NOTES_COMMITMENT_PTR=424 # The memory address at which the transaction script mast root is stored. -const.TX_SCRIPT_ROOT_PTR=428 +const TX_SCRIPT_ROOT_PTR=428 # The memory address at which the transaction script arguments are stored. -const.TX_SCRIPT_ARGS_PTR=432 +const TX_SCRIPT_ARGS_PTR=432 # The memory address at which the auth procedure arguments are stored. -const.AUTH_ARGS_PTR=436 +const AUTH_ARGS_PTR=436 # GLOBAL BLOCK DATA # ------------------------------------------------------------------------------------------------- # The memory address at which the block data section begins -const.BLOCK_DATA_SECTION_OFFSET=800 +const BLOCK_DATA_SECTION_OFFSET=800 # The memory address at which the previous block commitment is stored -const.PREV_BLOCK_COMMITMENT_PTR=800 +const PREV_BLOCK_COMMITMENT_PTR=800 # The memory address at which the chain commitment is stored -const.CHAIN_COMMITMENT_PTR=804 +const CHAIN_COMMITMENT_PTR=804 # The memory address at which the account root is stored -const.ACCT_DB_ROOT_PTR=808 +const ACCT_DB_ROOT_PTR=808 # The memory address at which the nullifier root is stored -const.NULLIFIER_ROOT_PTR=812 +const NULLIFIER_ROOT_PTR=812 # The memory address at which the tx commitment is stored -const.TX_COMMITMENT_PTR=816 +const TX_COMMITMENT_PTR=816 # The memory address at which the kernel commitment is stored -const.TX_KERNEL_COMMITMENT_PTR=820 +const TX_KERNEL_COMMITMENT_PTR=820 # The memory address at which the proof commitment is stored -const.PROOF_COMMITMENT_PTR=824 +const VALIDATOR_KEY_COMMITMENT_PTR=824 # The memory address at which the block metadata is stored [block_number, version, timestamp, 0] -const.BLOCK_METADATA_PTR=828 +const BLOCK_METADATA_PTR=828 # The memory address at which the fee parameters are stored. These occupy a double word. # [native_asset_id_suffix, native_asset_id_prefix, verification_base_fee, 0] # [0, 0, 0, 0] -const.FEE_PARAMETERS_PTR=832 +const FEE_PARAMETERS_PTR=832 # The memory address at which the verification base fee is stored. -const.VERIFICATION_BASE_FEE_PTR=FEE_PARAMETERS_PTR+2 +const VERIFICATION_BASE_FEE_PTR=FEE_PARAMETERS_PTR+2 # The memory address at which the note root is stored -const.NOTE_ROOT_PTR=840 +const NOTE_ROOT_PTR=840 # PARTIAL BLOCKCHAIN # ------------------------------------------------------------------------------------------------- # The memory address at which the chain data section begins -const.PARTIAL_BLOCKCHAIN_PTR=1200 +const PARTIAL_BLOCKCHAIN_PTR=1200 # The memory address at which the total number of leaves in the partial blockchain is stored -const.PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR=1200 +const PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR=1200 # The memory address at which the partial blockchain peaks are stored -const.PARTIAL_BLOCKCHAIN_PEAKS_PTR=1204 +const PARTIAL_BLOCKCHAIN_PEAKS_PTR=1204 # KERNEL DATA # ------------------------------------------------------------------------------------------------- # The memory address at which the number of the kernel procedures is stored. -const.NUM_KERNEL_PROCEDURES_PTR=1600 +const NUM_KERNEL_PROCEDURES_PTR=1600 # The memory address at which the hashes of kernel procedures begin. -const.KERNEL_PROCEDURES_PTR=1604 +const KERNEL_PROCEDURES_PTR=1604 # ACCOUNT DATA # ------------------------------------------------------------------------------------------------- @@ -158,90 +160,93 @@ const.KERNEL_PROCEDURES_PTR=1604 # The largest memory address which can be used to load the foreign account data. # It is computed as `2048 * 64 * 4` -- this is the memory address where the data block of the 64th # account starts. -const.MAX_FOREIGN_ACCOUNT_PTR=524288 +const MAX_FOREIGN_ACCOUNT_PTR=524288 # The memory address at which the native account data is stored. -const.NATIVE_ACCOUNT_DATA_PTR=8192 +const NATIVE_ACCOUNT_DATA_PTR=8192 # The length of the memory interval that the account data occupies. -const.ACCOUNT_DATA_LENGTH=8192 +const ACCOUNT_DATA_LENGTH=8192 # The offsets at which the account data is stored relative to the start of the account data segment. -const.ACCT_ID_AND_NONCE_OFFSET=0 -const.ACCT_NONCE_OFFSET=3 -const.ACCT_VAULT_ROOT_OFFSET=4 -const.ACCT_STORAGE_COMMITMENT_OFFSET=8 -const.ACCT_CODE_COMMITMENT_OFFSET=12 -const.ACCT_CORE_DATA_SECTION_END_OFFSET=16 -const.NUM_ACCT_PROCEDURES_OFFSET=28 -const.ACCT_PROCEDURES_SECTION_OFFSET=32 -const.ACCT_PROCEDURES_CALL_TRACKING_OFFSET=2084 -const.NUM_ACCT_STORAGE_SLOTS_OFFSET=2340 -const.ACCT_STORAGE_SLOTS_SECTION_OFFSET=2344 - -# Offset at which an exact copy of the account's storage slot section starting at -# ACCT_STORAGE_SLOTS_SECTION_OFFSET is kept. This keeps the initial values of the account storage -# slots accessible to enable computing a diff between the initial and current account storage slots -# for use in the account delta. This section is only present for the native account. -const.ACCT_INITIAL_STORAGE_SLOTS_SECTION_OFFSET=4384 +const ACCT_ID_AND_NONCE_OFFSET=0 +const ACCT_NONCE_OFFSET=3 +const ACCT_VAULT_ROOT_OFFSET=4 +const ACCT_STORAGE_COMMITMENT_OFFSET=8 +const ACCT_CODE_COMMITMENT_OFFSET=12 +const ACCT_CORE_DATA_SECTION_END_OFFSET=16 +const ACCT_NUM_PROCEDURES_OFFSET=28 +const ACCT_PROCEDURES_SECTION_OFFSET=32 +const ACCT_PROCEDURES_CALL_TRACKING_OFFSET=1060 +const ACCT_NUM_STORAGE_SLOTS_OFFSET=1316 +# Offset at which a copy of the initial account's storage slot section is kept. This section is +# only present for the native account. +const ACCT_INITIAL_STORAGE_SLOTS_SECTION_OFFSET=1320 + +# Offset at which the account's active storage slot section is kept. This section contains the +# current values of the account storage slots. +const ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET=2340 # NATIVE ACCOUNT DELTA # ------------------------------------------------------------------------------------------------- # The link map pointer at which the delta of the fungible asset vault is stored. -const.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR=532480 +const ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR=532480 # The link map pointer at which the delta of the non-fungible asset vault is stored. -const.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR=ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR+4 +const ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR=ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR+4 # The section of link map pointers where storage map deltas are stored. # This section is offset by `slot index` to get the link map ptr for the storage map # delta at that slot index. Slot indices that are not map slots are simply unused. -const.ACCOUNT_DELTA_STORAGE_MAP_SECTION=ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR+8 +const ACCOUNT_DELTA_STORAGE_MAP_SECTION=ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR+8 # INPUT NOTES DATA # ------------------------------------------------------------------------------------------------- # The memory address at which the input note section begins. -const.INPUT_NOTE_SECTION_OFFSET=4194304 +const INPUT_NOTE_SECTION_OFFSET=4194304 # The memory address at which the nullifier section of the input notes begins. -const.INPUT_NOTE_NULLIFIER_SECTION_PTR=4194308 +const INPUT_NOTE_NULLIFIER_SECTION_PTR=4194308 # The memory address at which the input note data section begins. -const.INPUT_NOTE_DATA_SECTION_OFFSET=4259840 +const INPUT_NOTE_DATA_SECTION_OFFSET=4259840 # The memory address at which the number of input notes is stored. -const.NUM_INPUT_NOTES_PTR=INPUT_NOTE_SECTION_OFFSET +const NUM_INPUT_NOTES_PTR=INPUT_NOTE_SECTION_OFFSET # The offsets at which data of an input note is stored relative to the start of its data segment -const.INPUT_NOTE_ID_OFFSET=0 -const.INPUT_NOTE_CORE_DATA_OFFSET=4 -const.INPUT_NOTE_SERIAL_NUM_OFFSET=4 -const.INPUT_NOTE_SCRIPT_ROOT_OFFSET=8 -const.INPUT_NOTE_INPUTS_COMMITMENT_OFFSET=12 -const.INPUT_NOTE_ASSETS_COMMITMENT_OFFSET=16 -const.INPUT_NOTE_RECIPIENT_OFFSET=20 -const.INPUT_NOTE_METADATA_OFFSET=24 -const.INPUT_NOTE_ARGS_OFFSET=28 -const.INPUT_NOTE_NUM_INPUTS_OFFSET=32 -const.INPUT_NOTE_NUM_ASSETS_OFFSET=36 -const.INPUT_NOTE_ASSETS_OFFSET=40 +const INPUT_NOTE_ID_OFFSET=0 +const INPUT_NOTE_CORE_DATA_OFFSET=4 +const INPUT_NOTE_SERIAL_NUM_OFFSET=4 +const INPUT_NOTE_SCRIPT_ROOT_OFFSET=8 +const INPUT_NOTE_INPUTS_COMMITMENT_OFFSET=12 +const INPUT_NOTE_ASSETS_COMMITMENT_OFFSET=16 +const INPUT_NOTE_RECIPIENT_OFFSET=20 +const INPUT_NOTE_METADATA_HEADER_OFFSET=24 +const INPUT_NOTE_ATTACHMENT_OFFSET=28 +const INPUT_NOTE_ARGS_OFFSET=32 +const INPUT_NOTE_NUM_INPUTS_OFFSET=36 +const INPUT_NOTE_NUM_ASSETS_OFFSET=40 +const INPUT_NOTE_ASSETS_OFFSET=44 # OUTPUT NOTES # ------------------------------------------------------------------------------------------------- # The memory address at which the output notes section begins. -const.OUTPUT_NOTE_SECTION_OFFSET=16777216 +const OUTPUT_NOTE_SECTION_OFFSET=16777216 # The offsets at which data of an output note is stored relative to the start of its data segment. -const.OUTPUT_NOTE_ID_OFFSET=0 -const.OUTPUT_NOTE_METADATA_OFFSET=4 -const.OUTPUT_NOTE_RECIPIENT_OFFSET=8 -const.OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET=12 -const.OUTPUT_NOTE_NUM_ASSETS_OFFSET=16 -const.OUTPUT_NOTE_DIRTY_FLAG_OFFSET=17 -const.OUTPUT_NOTE_ASSETS_OFFSET=20 +const OUTPUT_NOTE_ID_OFFSET=0 +const OUTPUT_NOTE_METADATA_HEADER_OFFSET=4 +const OUTPUT_NOTE_METADATA_ATTACHMENT_KIND_SCHEME_OFFSET=OUTPUT_NOTE_METADATA_HEADER_OFFSET + 3 +const OUTPUT_NOTE_ATTACHMENT_OFFSET=8 +const OUTPUT_NOTE_RECIPIENT_OFFSET=12 +const OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET=16 +const OUTPUT_NOTE_NUM_ASSETS_OFFSET=20 +const OUTPUT_NOTE_DIRTY_FLAG_OFFSET=21 +const OUTPUT_NOTE_ASSETS_OFFSET=24 # LINK MAP MEMORY # ------------------------------------------------------------------------------------------------- @@ -249,21 +254,21 @@ const.OUTPUT_NOTE_ASSETS_OFFSET=20 # The inclusive start of the link map dynamic memory region. # Chosen as a number greater than 2^25 such that all entry pointers are multiples of # LINK_MAP_ENTRY_SIZE. That enables a simpler check in assert_entry_ptr_is_valid. -const.LINK_MAP_REGION_START_PTR=33554448 +const LINK_MAP_REGION_START_PTR=33554448 # The non-inclusive end of the link map dynamic memory region. # This happens to be 2^26, but if it is changed, it should be chosen as a number such that # LINK_MAP_REGION_END_PTR - LINK_MAP_REGION_START_PTR is a multiple of LINK_MAP_ENTRY_SIZE, # because that enables checking whether a newly allocated entry pointer is at the end of the range # using equality rather than lt/gt in link_map_malloc. -const.LINK_MAP_REGION_END_PTR=67108864 +const LINK_MAP_REGION_END_PTR=67108864 # LINK_MAP_REGION_START_PTR + the currently used size stored at this pointer defines the next # entry pointer that will be allocated. -const.LINK_MAP_USED_MEMORY_SIZE=33554432 +const LINK_MAP_USED_MEMORY_SIZE=33554432 # The size of each map entry, i.e. four words. -const.LINK_MAP_ENTRY_SIZE=16 +const LINK_MAP_ENTRY_SIZE=16 # MEMORY PROCEDURES # ================================================================================================= @@ -278,7 +283,7 @@ const.LINK_MAP_ENTRY_SIZE=16 #! #! Where: #! - num_output_notes is the number of output notes created in this transaction so far. -export.get_num_output_notes +pub proc get_num_output_notes mem_load.NUM_OUTPUT_NOTES_PTR end @@ -289,7 +294,7 @@ end #! #! Where: #! - num_output_notes is the number of output notes. -export.set_num_output_notes +pub proc set_num_output_notes mem_store.NUM_OUTPUT_NOTES_PTR end @@ -300,7 +305,7 @@ end #! #! Where: #! - note_ptr is the memory address of the data segment for the active note. -export.get_active_input_note_ptr +pub proc get_active_input_note_ptr mem_load.ACTIVE_INPUT_NOTE_PTR end @@ -311,7 +316,7 @@ end #! #! Where: #! - note_ptr is the new memory address of the data segment for the input note. -export.set_active_input_note_ptr +pub proc set_active_input_note_ptr mem_store.ACTIVE_INPUT_NOTE_PTR end @@ -323,7 +328,7 @@ end #! Where: #! - input_vault_root_ptr is a pointer to the memory address at which the input vault root is #! stored. -export.get_input_vault_root_ptr +pub proc get_input_vault_root_ptr push.INPUT_VAULT_ROOT_PTR end @@ -334,7 +339,7 @@ end #! #! Where: #! - INPUT_VAULT_ROOT is the input vault root. -export.get_input_vault_root +pub proc get_input_vault_root padw mem_loadw_be.INPUT_VAULT_ROOT_PTR end @@ -345,7 +350,7 @@ end #! #! Where: #! - INPUT_VAULT_ROOT is the input vault root. -export.set_input_vault_root +pub proc set_input_vault_root mem_storew_be.INPUT_VAULT_ROOT_PTR end @@ -357,7 +362,7 @@ end #! Where: #! - output_vault_root_ptr is the pointer to the memory address at which the output vault root is #! stored. -export.get_output_vault_root_ptr +pub proc get_output_vault_root_ptr push.OUTPUT_VAULT_ROOT_PTR end @@ -368,7 +373,7 @@ end #! #! Where: #! - OUTPUT_VAULT_ROOT is the output vault root. -export.get_output_vault_root +pub proc get_output_vault_root padw mem_loadw_be.OUTPUT_VAULT_ROOT_PTR end @@ -379,7 +384,7 @@ end #! #! Where: #! - OUTPUT_VAULT_ROOT is the output vault root. -export.set_output_vault_root +pub proc set_output_vault_root mem_storew_be.OUTPUT_VAULT_ROOT_PTR end @@ -393,7 +398,7 @@ end #! #! Where: #! - BLOCK_COMMITMENT is the commitment of the transaction reference block. -export.set_block_commitment +pub proc set_block_commitment mem_storew_be.BLOCK_COMMITMENT_PTR end @@ -404,7 +409,7 @@ end #! #! Where: #! - BLOCK_COMMITMENT is the commitment of the transaction reference block. -export.get_block_commitment +pub proc get_block_commitment padw mem_loadw_be.BLOCK_COMMITMENT_PTR end @@ -415,7 +420,7 @@ end #! #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of the account ID. -export.set_global_account_id +pub proc set_global_account_id push.0.0 # => [0, 0, account_id_prefix, account_id_suffix] mem_storew_be.NATIVE_ACCT_ID_PTR @@ -430,7 +435,7 @@ end #! #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of the account ID. -export.get_global_account_id +pub proc get_global_account_id padw mem_loadw_be.NATIVE_ACCT_ID_PTR # => [0, 0, account_id_prefix, account_id_suffix] drop drop @@ -443,7 +448,7 @@ end #! #! Where: #! - INIT_ACCOUNT_COMMITMENT is the initial account commitment. -export.set_init_account_commitment +pub proc set_init_account_commitment mem_storew_be.INIT_ACCOUNT_COMMITMENT_PTR end @@ -454,7 +459,7 @@ end #! #! Where: #! - INIT_ACCOUNT_COMMITMENT is the initial account commitment. -export.get_init_account_commitment +pub proc get_init_account_commitment padw mem_loadw_be.INIT_ACCOUNT_COMMITMENT_PTR end @@ -465,7 +470,7 @@ end #! #! Where: #! - init_nonce is the initial account nonce. -export.set_init_nonce +pub proc set_init_nonce mem_store.INIT_NONCE_PTR end @@ -476,7 +481,7 @@ end #! #! Where: #! - init_nonce is the initial account nonce. -export.get_init_nonce +pub proc get_init_nonce mem_load.INIT_NONCE_PTR end @@ -487,7 +492,7 @@ end #! #! Where: #! - INIT_NATIVE_ACCOUNT_VAULT_ROOT is the initial vault root of the native account. -export.set_init_native_account_vault_root +pub proc set_init_native_account_vault_root mem_storew_be.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR end @@ -498,20 +503,20 @@ end #! #! Where: #! - INIT_NATIVE_ACCOUNT_VAULT_ROOT is the initial vault root of the native account. -export.get_init_native_account_vault_root +pub proc get_init_native_account_vault_root padw mem_loadw_be.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR end -#! Returns the memory address of the vault root of the native account at the beginning of the +#! Returns the memory address of the vault root of the native account at the beginning of the #! transaction. #! #! Inputs: [] #! Outputs: [native_account_initial_vault_root_ptr] #! #! Where: -#! - native_account_initial_vault_root_ptr is the memory pointer to the initial vault root of the +#! - native_account_initial_vault_root_ptr is the memory pointer to the initial vault root of the #! native account. -export.get_init_native_account_vault_root_ptr +pub proc get_init_native_account_vault_root_ptr push.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR end @@ -522,7 +527,7 @@ end #! #! Where: #! - INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT is the initial storage commitment of the native account. -export.set_init_account_storage_commitment +pub proc set_init_account_storage_commitment mem_storew_be.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR end @@ -533,7 +538,7 @@ end #! #! Where: #! - INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT is the initial storage commitment of the native account. -export.get_init_account_storage_commitment +pub proc get_init_account_storage_commitment padw mem_loadw_be.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR end @@ -546,7 +551,7 @@ end #! #! Where: #! - INPUT_NOTES_COMMITMENT is the input notes commitment. -export.get_input_notes_commitment +pub proc get_input_notes_commitment padw mem_loadw_be.INPUT_NOTES_COMMITMENT_PTR end @@ -557,7 +562,7 @@ end #! #! Where: #! - INPUT_NOTES_COMMITMENT is the notes' commitment. -export.set_nullifier_commitment +pub proc set_nullifier_commitment mem_storew_be.INPUT_NOTES_COMMITMENT_PTR end @@ -568,7 +573,7 @@ end #! #! Where: #! - tx_script_root_ptr is the pointer to the memory where transaction script root is stored. -export.get_tx_script_root_ptr +pub proc get_tx_script_root_ptr push.TX_SCRIPT_ROOT_PTR end @@ -579,7 +584,7 @@ end #! #! Where: #! - TX_SCRIPT_ROOT is the transaction script root. -export.set_tx_script_root +pub proc set_tx_script_root mem_storew_be.TX_SCRIPT_ROOT_PTR end @@ -591,7 +596,7 @@ end #! Where: #! - TX_SCRIPT_ARGS is the word of values which could be used directly or could be used to obtain #! some values associated with it from the advice map. -export.get_tx_script_args +pub proc get_tx_script_args padw mem_loadw_be.TX_SCRIPT_ARGS_PTR end @@ -603,7 +608,7 @@ end #! Where: #! - TX_SCRIPT_ARGS is the word of values which could be used directly or could be used to obtain #! some values associated with it from the advice map. -export.set_tx_script_args +pub proc set_tx_script_args mem_storew_be.TX_SCRIPT_ARGS_PTR end @@ -614,7 +619,7 @@ end #! #! Where: #! - AUTH_ARGS is the argument passed to the auth procedure. -export.get_auth_args +pub proc get_auth_args padw mem_loadw_be.AUTH_ARGS_PTR end @@ -625,7 +630,7 @@ end #! #! Where: #! - AUTH_ARGS is the argument passed to the auth procedure. -export.set_auth_args +pub proc set_auth_args mem_storew_be.AUTH_ARGS_PTR end @@ -639,7 +644,7 @@ end #! #! Where: #! - ptr is a pointer to the block data section. -export.get_block_data_ptr +pub proc get_block_data_ptr push.BLOCK_DATA_SECTION_OFFSET end @@ -650,7 +655,7 @@ end #! #! Where: #! - PREV_BLOCK_COMMITMENT_PTR is the block commitment of the transaction reference block. -export.get_prev_block_commitment +pub proc get_prev_block_commitment padw mem_loadw_be.PREV_BLOCK_COMMITMENT_PTR end @@ -661,7 +666,7 @@ end #! #! Where: #! - blk_num is the block number of the transaction reference block. -export.get_blk_num +pub proc get_blk_num mem_load.BLOCK_METADATA_PTR end @@ -672,8 +677,8 @@ end #! #! Where: #! - version is the protocol version of the transaction reference block. -const.BLOCK_METADATA_VERSION_PTR=BLOCK_METADATA_PTR+1 -export.get_blk_version +const BLOCK_METADATA_VERSION_PTR=BLOCK_METADATA_PTR+1 +pub proc get_blk_version mem_load.BLOCK_METADATA_VERSION_PTR end @@ -684,8 +689,8 @@ end #! #! Where: #! - timestamp is the timestamp of the reference block for this transaction. -const.BLOCK_METADATA_TIMESTAMP_PTR=BLOCK_METADATA_PTR+2 -export.get_blk_timestamp +const BLOCK_METADATA_TIMESTAMP_PTR=BLOCK_METADATA_PTR+2 +pub proc get_blk_timestamp mem_load.BLOCK_METADATA_TIMESTAMP_PTR end @@ -697,7 +702,7 @@ end #! Where: #! - native_asset_id_{prefix,suffix} are the prefix and suffix felts of the faucet ID that defines #! the native asset. -export.get_native_asset_id +pub proc get_native_asset_id padw mem_loadw_be.FEE_PARAMETERS_PTR drop drop # => [native_asset_id_prefix, native_asset_id_suffix] end @@ -710,7 +715,7 @@ end #! Where: #! - verification_base_fee is the base fee capturing the cost for the verification of a #! transaction. -export.get_verification_base_fee +pub proc get_verification_base_fee mem_load.VERIFICATION_BASE_FEE_PTR end @@ -721,7 +726,7 @@ end #! #! Where: #! - CHAIN_COMMITMENT is the chain commitment of the transaction reference block. -export.get_chain_commitment +pub proc get_chain_commitment padw mem_loadw_be.CHAIN_COMMITMENT_PTR end @@ -732,7 +737,7 @@ end #! #! Where: #! - ACCT_DB_ROOT is the account database root of the transaction reference block. -export.get_account_db_root +pub proc get_account_db_root padw mem_loadw_be.ACCT_DB_ROOT_PTR end @@ -743,7 +748,7 @@ end #! #! Where: #! - NULLIFIER_ROOT is the nullifier root of the transaction reference block. -export.get_nullifier_db_root +pub proc get_nullifier_db_root padw mem_loadw_be.NULLIFIER_ROOT_PTR end @@ -754,7 +759,7 @@ end #! #! Where: #! - TX_COMMITMENT is the tx commitment of the transaction reference block. -export.get_tx_commitment +pub proc get_tx_commitment padw mem_loadw_be.TX_COMMITMENT_PTR end @@ -765,19 +770,19 @@ end #! #! Where: #! - TX_KERNEL_COMMITMENT is the sequential hash of the kernel procedures. -export.get_tx_kernel_commitment +pub proc get_tx_kernel_commitment padw mem_loadw_be.TX_KERNEL_COMMITMENT_PTR end -#! Returns the proof commitment of the transaction reference block. +#! Returns the validator key commitment of the transaction reference block. #! #! Inputs: [] -#! Outputs: [PROOF_COMMITMENT] +#! Outputs: [VALIDATOR_KEY_COMMITMENT] #! #! Where: -#! - PROOF_COMMITMENT is the proof commitment of the transaction reference block. -export.get_proof_commitment - padw mem_loadw_be.PROOF_COMMITMENT_PTR +#! - VALIDATOR_KEY_COMMITMENT is the public key commitment of the transaction reference block. +pub proc get_validator_key_commitment + padw mem_loadw_be.VALIDATOR_KEY_COMMITMENT_PTR end #! Returns the note root of the transaction reference block. @@ -787,7 +792,7 @@ end #! #! Where: #! - NOTE_ROOT is the note root of the transaction reference block. -export.get_note_root +pub proc get_note_root padw mem_loadw_be.NOTE_ROOT_PTR end @@ -798,7 +803,7 @@ end #! #! Where: #! - NOTE_ROOT is the note root of the transaction reference block. -export.set_note_root +pub proc set_note_root mem_storew_be.NOTE_ROOT_PTR end @@ -812,7 +817,7 @@ end #! #! Where: #! - ptr is the pointer to the partial blockchain section. -export.get_partial_blockchain_ptr +pub proc get_partial_blockchain_ptr push.PARTIAL_BLOCKCHAIN_PTR end @@ -823,7 +828,7 @@ end #! #! Where: #! - num_leaves is the number of leaves in the partial blockchain. -export.set_partial_blockchain_num_leaves +pub proc set_partial_blockchain_num_leaves mem_store.PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR end @@ -834,7 +839,7 @@ end #! #! Where: #! - ptr is the pointer to the start of the partial blockchain peaks section. -export.get_partial_blockchain_peaks_ptr +pub proc get_partial_blockchain_peaks_ptr push.PARTIAL_BLOCKCHAIN_PEAKS_PTR end @@ -848,7 +853,7 @@ end #! #! Where: #! - ptr is the memory address at which the native account data is stored. -export.get_native_account_data_ptr +pub proc get_native_account_data_ptr push.NATIVE_ACCOUNT_DATA_PTR end @@ -859,7 +864,7 @@ end #! #! Where: #! - account_data_length is the length of the memory interval that the account data occupies. -export.get_account_data_length +pub proc get_account_data_length push.ACCOUNT_DATA_LENGTH end @@ -871,7 +876,7 @@ end #! Where: #! - max_foreign_account_ptr is the largest memory address which can be used to load the foreign #! account data. -export.get_max_foreign_account_ptr +pub proc get_max_foreign_account_ptr push.MAX_FOREIGN_ACCOUNT_PTR end @@ -879,7 +884,7 @@ end #! #! Inputs: [] #! Outputs: [] -export.set_active_account_data_ptr_to_native_account +pub proc set_active_account_data_ptr_to_native_account # store the native account data pointer into the first account stack element. push.NATIVE_ACCOUNT_DATA_PTR mem_store.MIN_ACCOUNT_STACK_PTR # => [native_acct_stack_ptr, account_stack_top_ptr] @@ -896,7 +901,7 @@ end #! #! Where: #! - active_account_data_ptr is the memory address at which the data of the active account begins. -export.get_active_account_data_ptr +pub proc get_active_account_data_ptr mem_load.ACCOUNT_STACK_TOP_PTR # => [account_stack_top_ptr] @@ -918,7 +923,7 @@ end #! Panics if: #! - the account stack is full, containing 64 accounts total. #! - the provided account data pointer is equal to the native account data pointer. -export.push_ptr_to_account_stack +pub proc push_ptr_to_account_stack # check that the account stack is not full mem_load.ACCOUNT_STACK_TOP_PTR dup # => [account_stack_top_ptr, account_stack_top_ptr, curr_account_data_ptr] @@ -947,7 +952,7 @@ end #! #! Panics if: #! - the account stack contains only native account. -export.pop_ptr_from_account_stack +pub proc pop_ptr_from_account_stack # check that the account stack always is at least of size 1, that is, it contains at least the # native account mem_load.ACCOUNT_STACK_TOP_PTR dup @@ -970,7 +975,7 @@ end #! #! Panics if: #! - the active account data pointer is not equal to native account data pointer (8192). -export.assert_native_account +pub proc assert_native_account exec.is_native_account assert.err=ERR_ACCOUNT_IS_NOT_NATIVE end @@ -979,7 +984,7 @@ end #! #! Inputs: [] #! Outputs: [is_native_account] -export.is_native_account +pub proc is_native_account exec.get_active_account_data_ptr # => [active_account_data_ptr] @@ -987,6 +992,17 @@ export.is_native_account # => [is_native_account] end +#! Returns 1 if the native account is new, i.e. its nonce is zero, 0 otherwise. +#! +#! Inputs: [] +#! Outputs: [is_new_account] +pub proc is_new_account + # the account is new if its nonce is zero + # use get_init_nonce so this procedure works correctly even after the account's nonce was + # incremented + exec.get_init_nonce eq.0 +end + #! Returns a pointer to the end of the core account data section. #! #! Inputs: [] @@ -994,7 +1010,7 @@ end #! #! Where: #! - ptr is the memory address at which the core account data ends. -export.get_core_account_data_end_ptr +pub proc get_core_account_data_end_ptr exec.get_active_account_data_ptr add.ACCT_CORE_DATA_SECTION_END_OFFSET end @@ -1007,7 +1023,7 @@ end #! #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. -export.get_account_id +pub proc get_account_id padw exec.get_active_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET mem_loadw_be # => [nonce, 0, account_id_prefix, account_id_suffix] drop drop @@ -1022,7 +1038,7 @@ end #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the native account #! of the transaction. -export.get_native_account_id +pub proc get_native_account_id padw push.NATIVE_ACCOUNT_DATA_PTR add.ACCT_ID_AND_NONCE_OFFSET mem_loadw_be # => [nonce, 0, account_id_prefix, account_id_suffix] drop drop @@ -1037,7 +1053,7 @@ end #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. #! - nonce is the nonce of the active account. -export.set_account_id_and_nonce +pub proc set_account_id_and_nonce exec.get_active_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET mem_storew_be end @@ -1049,7 +1065,7 @@ end #! #! Where: #! - nonce is the nonce of the active account. -export.get_account_nonce +pub proc get_account_nonce exec.get_active_account_data_ptr add.ACCT_NONCE_OFFSET mem_load end @@ -1061,7 +1077,7 @@ end #! #! Where: #! - nonce is the nonce of the native account of the transaction. -export.get_native_account_nonce +pub proc get_native_account_nonce push.NATIVE_ACCOUNT_DATA_PTR add.ACCT_NONCE_OFFSET mem_load end @@ -1073,7 +1089,7 @@ end #! #! Where: #! - nonce is the nonce of the active account. -export.set_account_nonce +pub proc set_account_nonce exec.get_active_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET padw # => [0, 0, 0, 0, account_id_and_nonce_ptr, new_nonce] dup.4 mem_loadw_be @@ -1091,7 +1107,7 @@ end #! #! Where: #! - account_vault_root_ptr is the memory pointer to the account asset vault root. -export.get_account_vault_root_ptr +pub proc get_account_vault_root_ptr exec.get_active_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET end @@ -1102,7 +1118,7 @@ end #! #! Where: #! - ACCT_VAULT_ROOT is the account asset vault root. -export.get_account_vault_root +pub proc get_account_vault_root padw exec.get_active_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET mem_loadw_be @@ -1115,7 +1131,7 @@ end #! #! Where: #! - ACCT_VAULT_ROOT is the account vault root to be set. -export.set_account_vault_root +pub proc set_account_vault_root exec.get_active_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET mem_storew_be end @@ -1131,8 +1147,8 @@ end #! #! Where: #! - account_initial_vault_root_ptr is the memory pointer to the initial vault root. -export.get_account_initial_vault_root_ptr - # For foreign account, use the regular vault root pointer since foreign accounts are read-only +pub proc get_account_initial_vault_root_ptr + # For foreign account, use the regular vault root pointer since foreign accounts are read-only # and initial == current exec.get_account_vault_root_ptr # => [account_vault_root_ptr] @@ -1159,7 +1175,7 @@ end #! #! Where: #! - CODE_COMMITMENT is the code commitment of the account. -export.get_account_code_commitment +pub proc get_account_code_commitment padw exec.get_active_account_data_ptr add.ACCT_CODE_COMMITMENT_OFFSET mem_loadw_be @@ -1172,7 +1188,7 @@ end #! #! Where: #! - CODE_COMMITMENT is the code commitment to be set. -export.set_account_code_commitment +pub proc set_account_code_commitment exec.get_active_account_data_ptr add.ACCT_CODE_COMMITMENT_OFFSET mem_storew_be end @@ -1184,7 +1200,7 @@ end #! #! Where: #! - tx_expiration_block_num is the number of the transaction expiration block. -export.set_expiration_block_num +pub proc set_expiration_block_num mem_store.TX_EXPIRATION_BLOCK_NUM_PTR end @@ -1195,7 +1211,7 @@ end #! #! Where: #! - tx_expiration_block_num is the number of the transaction expiration block. -export.get_expiration_block_num +pub proc get_expiration_block_num mem_load.TX_EXPIRATION_BLOCK_NUM_PTR end @@ -1206,8 +1222,8 @@ end #! #! Where: #! - num_procedures is the number of procedures contained in the account code. -export.get_num_account_procedures - exec.get_active_account_data_ptr add.NUM_ACCT_PROCEDURES_OFFSET +pub proc get_num_account_procedures + exec.get_active_account_data_ptr add.ACCT_NUM_PROCEDURES_OFFSET mem_load end @@ -1218,8 +1234,8 @@ end #! #! Where: #! - num_procedures is the number of procedures contained in the account code. -export.set_num_account_procedures - exec.get_active_account_data_ptr add.NUM_ACCT_PROCEDURES_OFFSET +pub proc set_num_account_procedures + exec.get_active_account_data_ptr add.ACCT_NUM_PROCEDURES_OFFSET mem_store end @@ -1230,7 +1246,7 @@ end #! #! Where: #! - account_procedures_section_ptr is the memory pointer to the account procedures section. -export.get_account_procedures_section_ptr +pub proc get_account_procedures_section_ptr exec.get_active_account_data_ptr add.ACCT_PROCEDURES_SECTION_OFFSET end @@ -1241,7 +1257,7 @@ end #! #! Where: #! - procedures_call_tracking_ptr is the memory pointer to the procedure call tracking section. -export.get_account_procedures_call_tracking_ptr +pub proc get_account_procedures_call_tracking_ptr exec.get_active_account_data_ptr add.ACCT_PROCEDURES_CALL_TRACKING_OFFSET end @@ -1253,8 +1269,8 @@ end #! Where: #! - proc_idx is the index of the account procedure. #! - proc_ptr is the memory pointer to the account procedure at the specified index. -export.get_account_procedure_ptr - mul.8 exec.get_account_procedures_section_ptr add +pub proc get_account_procedure_ptr + mul.ACCOUNT_PROCEDURE_DATA_LENGTH exec.get_account_procedures_section_ptr add end ### ACCOUNT STORAGE ################################################# @@ -1266,7 +1282,7 @@ end #! #! Where: #! - STORAGE_COMMITMENT is the account storage commitment. -export.get_account_storage_commitment +pub proc get_account_storage_commitment padw exec.get_active_account_data_ptr add.ACCT_STORAGE_COMMITMENT_OFFSET mem_loadw_be @@ -1279,7 +1295,7 @@ end #! #! Where: #! - STORAGE_COMMITMENT is the account storage commitment. -export.set_account_storage_commitment +pub proc set_account_storage_commitment exec.get_active_account_data_ptr add.ACCT_STORAGE_COMMITMENT_OFFSET mem_storew_be end @@ -1294,7 +1310,7 @@ end #! #! Where: #! - dirty_flag is the flag indicating whether the storage commitment is outdated. -export.set_native_account_storage_commitment_dirty_flag +pub proc set_native_account_storage_commitment_dirty_flag push.NATIVE_ACCT_STORAGE_COMMITMENT_DIRTY_FLAG_PTR mem_store # => [] end @@ -1309,7 +1325,7 @@ end #! Where: #! - should_recompute_storage_commitment is the flag indicating whether the storage commitment #! should be recomputed. -export.get_recompute_storage_commitment_flag +pub proc get_recompute_storage_commitment_flag # get the is_native_account flag exec.is_native_account # => [is_native_account] @@ -1330,8 +1346,8 @@ end #! #! Where: #! - num_storage_slots is the number of storage slots contained in the account storage. -export.get_num_storage_slots - exec.get_active_account_data_ptr add.NUM_ACCT_STORAGE_SLOTS_OFFSET +pub proc get_num_storage_slots + exec.get_active_account_data_ptr add.ACCT_NUM_STORAGE_SLOTS_OFFSET mem_load end @@ -1342,25 +1358,11 @@ end #! #! Where: #! - num_storage_slots is the number of storage slots contained in the account storage. -export.set_num_storage_slots - exec.get_active_account_data_ptr add.NUM_ACCT_STORAGE_SLOTS_OFFSET +pub proc set_num_storage_slots + exec.get_active_account_data_ptr add.ACCT_NUM_STORAGE_SLOTS_OFFSET mem_store end -#! Returns the type of the requested storage slot. -#! -#! Inputs: [account_storage_slots_section_ptr, index] -#! Outputs: [slot_type] -#! -#! Where: -#! - index is the location in memory of the storage slot. -#! - slot_type is the type of the storage slot. -export.get_storage_slot_type - # get storage slot type - swap mul.8 add add.4 mem_load - # => [slot_type] -end - #! Returns the memory pointer to the account storage slots section. #! #! Inputs: [] @@ -1368,8 +1370,8 @@ end #! #! Where: #! - storage_slots_section_ptr is the memory pointer to the account storage slots section. -export.get_account_storage_slots_section_ptr - exec.get_active_account_data_ptr add.ACCT_STORAGE_SLOTS_SECTION_OFFSET +pub proc get_account_active_storage_slots_section_ptr + exec.get_active_account_data_ptr add.ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET end #! Returns the memory pointer to the native account's storage slots section. @@ -1379,8 +1381,8 @@ end #! #! Where: #! - storage_slots_section_ptr is the memory pointer to the native account's storage slots section. -export.get_native_account_storage_slots_ptr - exec.get_native_account_data_ptr add.ACCT_STORAGE_SLOTS_SECTION_OFFSET +pub proc get_native_account_active_storage_slots_ptr + exec.get_native_account_data_ptr add.ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET end #! Returns the memory pointer to the initial storage slots of the native account. @@ -1390,7 +1392,7 @@ end #! #! Where: #! - account_initial_storage_slots_ptr is the memory pointer to the initial storage slot values. -export.get_native_account_initial_storage_slots_ptr +pub proc get_native_account_initial_storage_slots_ptr exec.get_native_account_data_ptr add.ACCT_INITIAL_STORAGE_SLOTS_SECTION_OFFSET end @@ -1405,10 +1407,10 @@ end #! #! Where: #! - account_initial_storage_slots_ptr is the memory pointer to the initial storage slot values. -export.get_account_initial_storage_slots_ptr - # For foreign account, use the regular storage slots pointer since foreign accounts are +pub proc get_account_initial_storage_slots_ptr + # For foreign account, use the regular storage slots pointer since foreign accounts are # read-only and initial == current - exec.get_account_storage_slots_section_ptr + exec.get_account_active_storage_slots_section_ptr # => [account_storage_slots_ptr] # For native account, use the initial storage slots pointer @@ -1433,7 +1435,7 @@ end #! #! Where: #! - account_delta_fungible_asset_ptr is the link map pointer to the fungible asset vault delta. -export.get_account_delta_fungible_asset_ptr +pub proc get_account_delta_fungible_asset_ptr push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR end @@ -1444,7 +1446,7 @@ end #! #! Where: #! - account_delta_non_fungible_asset_ptr is the link map pointer to the non-fungible asset vault delta. -export.get_account_delta_non_fungible_asset_ptr +pub proc get_account_delta_non_fungible_asset_ptr push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR end @@ -1456,7 +1458,7 @@ end #! Where: #! - account_delta_storage_map_ptr is the link map pointer to the storage map delta for the #! requested slot index. -export.get_account_delta_storage_map_ptr +pub proc get_account_delta_storage_map_ptr add.ACCOUNT_DELTA_STORAGE_MAP_SECTION end @@ -1464,14 +1466,14 @@ end #! #! Inputs: [] #! Outputs: [] -export.mem_copy_native_account_initial_storage_slots +pub proc mem_copy_native_account_initial_storage_slots exec.get_native_account_initial_storage_slots_ptr - exec.get_native_account_storage_slots_ptr - # => [storage_slots_section_ptr, initial_storage_slots_ptr] + exec.get_native_account_active_storage_slots_ptr + # => [active_storage_slots_ptr, initial_storage_slots_ptr] # each slot takes up two words exec.get_num_storage_slots mul.2 - # => [num_storage_slot_words, storage_slots_section_ptr, initial_storage_slots_ptr] + # => [num_storage_slot_words, active_storage_slots_ptr, initial_storage_slots_ptr] exec.mem::memcopy_words # => [] @@ -1487,7 +1489,7 @@ end #! #! Where: #! - num_input_notes is the total number of input notes consumed by this transaction. -export.get_num_input_notes +pub proc get_num_input_notes mem_load.NUM_INPUT_NOTES_PTR end @@ -1498,7 +1500,7 @@ end #! #! Where: #! - num_input_notes is the total number of input notes consumed by this transaction. -export.set_num_input_notes +pub proc set_num_input_notes mem_store.NUM_INPUT_NOTES_PTR end @@ -1511,8 +1513,8 @@ end #! Where: #! - idx is the index of the input note. #! - note_ptr is the memory address of the data segment for the input note with `idx`. -export.get_input_note_ptr - exec.constants::get_note_mem_size mul add.INPUT_NOTE_DATA_SECTION_OFFSET +pub proc get_input_note_ptr + push.NOTE_MEM_SIZE mul add.INPUT_NOTE_DATA_SECTION_OFFSET end #! Set the note id of the input note. @@ -1523,7 +1525,7 @@ end #! Where: #! - note_ptr is the input note's the memory address. #! - NOTE_ID is the note's id. -export.set_input_note_id +pub proc set_input_note_id mem_storew_be end @@ -1536,7 +1538,7 @@ end #! Where: #! - idx is the index of the input note. #! - nullifier_ptr is the memory address of the nullifier for note idx. -export.get_input_note_nullifier_ptr +pub proc get_input_note_nullifier_ptr mul.4 add.INPUT_NOTE_NULLIFIER_SECTION_PTR end @@ -1548,7 +1550,7 @@ end #! Where: #! - idx is the index of the input note. #! - nullifier is the nullifier of the input note. -export.get_input_note_nullifier +pub proc get_input_note_nullifier mul.4 padw movup.4 add.INPUT_NOTE_NULLIFIER_SECTION_PTR mem_loadw_be end @@ -1561,7 +1563,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - note_data_ptr is the memory address at which the input note core data begins. -export.get_input_note_core_ptr +pub proc get_input_note_core_ptr add.INPUT_NOTE_CORE_DATA_OFFSET end @@ -1573,7 +1575,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - SCRIPT_ROOT is the script root of the input note. -export.get_input_note_script_root +pub proc get_input_note_script_root padw movup.4 add.INPUT_NOTE_SCRIPT_ROOT_OFFSET mem_loadw_be @@ -1587,7 +1589,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - script_root_ptr is the memory address where script root of the input note is stored. -export.get_input_note_script_root_ptr +pub proc get_input_note_script_root_ptr add.INPUT_NOTE_SCRIPT_ROOT_OFFSET end @@ -1599,7 +1601,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - INPUTS_COMMITMENT is the inputs commitment of the input note. -export.get_input_note_inputs_commitment +pub proc get_input_note_inputs_commitment padw movup.4 add.INPUT_NOTE_INPUTS_COMMITMENT_OFFSET mem_loadw_be @@ -1608,27 +1610,54 @@ end #! Returns the metadata of an input note located at the specified memory address. #! #! Inputs: [note_ptr] -#! Outputs: [METADATA] +#! Outputs: [NOTE_METADATA_HEADER] #! #! Where: #! - note_ptr is the memory address at which the input note data begins. -#! - METADATA is the metadata of the input note. -export.get_input_note_metadata +#! - NOTE_METADATA_HEADER is the metadata header of the input note. +pub proc get_input_note_metadata_header padw - movup.4 add.INPUT_NOTE_METADATA_OFFSET + movup.4 add.INPUT_NOTE_METADATA_HEADER_OFFSET mem_loadw_be end #! Sets the metadata for an input note located at the specified memory address. #! -#! Inputs: [note_ptr, NOTE_METADATA] -#! Outputs: [NOTE_METADATA] +#! Inputs: [note_ptr, NOTE_METADATA_HEADER] +#! Outputs: [NOTE_METADATA_HEADER] #! #! Where: #! - note_ptr is the memory address at which the input note data begins. -#! - NOTE_METADATA is the metadata of the input note. -export.set_input_note_metadata - add.INPUT_NOTE_METADATA_OFFSET +#! - NOTE_METADATA_HEADER is the metadata header of the input note. +pub proc set_input_note_metadata_header + add.INPUT_NOTE_METADATA_HEADER_OFFSET + mem_storew_be +end + +#! Returns the attachment of an input note located at the specified memory address. +#! +#! Inputs: [note_ptr] +#! Outputs: [NOTE_ATTACHMENT] +#! +#! Where: +#! - note_ptr is the memory address at which the input note data begins. +#! - NOTE_ATTACHMENT is the attachment of the input note. +pub proc get_input_note_attachment + padw + movup.4 add.INPUT_NOTE_ATTACHMENT_OFFSET + mem_loadw_be +end + +#! Sets the attachment for an input note located at the specified memory address. +#! +#! Inputs: [note_ptr, NOTE_ATTACHMENT] +#! Outputs: [NOTE_ATTACHMENT] +#! +#! Where: +#! - note_ptr is the memory address at which the input note data begins. +#! - NOTE_ATTACHMENT is the attachment of the input note. +pub proc set_input_note_attachment + add.INPUT_NOTE_ATTACHMENT_OFFSET mem_storew_be end @@ -1640,7 +1669,7 @@ end #! Where: #! - note_ptr is the start memory address of the note. #! - NOTE_ARGS are the note's args. -export.get_input_note_args +pub proc get_input_note_args padw movup.4 add.INPUT_NOTE_ARGS_OFFSET mem_loadw_be @@ -1654,7 +1683,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - NOTE_ARGS are optional note args of the input note. -export.set_input_note_args +pub proc set_input_note_args add.INPUT_NOTE_ARGS_OFFSET mem_storew_be end @@ -1667,7 +1696,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - num_inputs is the number of inputs in in the input note. -export.get_input_note_num_inputs +pub proc get_input_note_num_inputs add.INPUT_NOTE_NUM_INPUTS_OFFSET mem_load end @@ -1680,7 +1709,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - num_inputs is the number of inputs in the input note. -export.set_input_note_num_inputs +pub proc set_input_note_num_inputs add.INPUT_NOTE_NUM_INPUTS_OFFSET mem_store end @@ -1693,7 +1722,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - num_assets is the number of assets in the input note. -export.get_input_note_num_assets +pub proc get_input_note_num_assets add.INPUT_NOTE_NUM_ASSETS_OFFSET mem_load end @@ -1706,7 +1735,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - num_assets is the number of assets in the input note. -export.set_input_note_num_assets +pub proc set_input_note_num_assets add.INPUT_NOTE_NUM_ASSETS_OFFSET mem_store end @@ -1720,7 +1749,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - assets_ptr is the memory address at which the assets segment for the input note begins. -export.get_input_note_assets_ptr +pub proc get_input_note_assets_ptr add.INPUT_NOTE_ASSETS_OFFSET end @@ -1732,7 +1761,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - RECIPIENT is the commitment to the note's script, inputs and the serial number. -export.get_input_note_recipient +pub proc get_input_note_recipient padw movup.4 add.INPUT_NOTE_RECIPIENT_OFFSET mem_loadw_be @@ -1746,7 +1775,7 @@ end #! Where: #! - note_ptr is the memory address at which the output note data begins. #! - RECIPIENT is the commitment to the note's script, inputs and the serial number. -export.set_input_note_recipient +pub proc set_input_note_recipient add.INPUT_NOTE_RECIPIENT_OFFSET mem_storew_be end @@ -1759,7 +1788,7 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - ASSET_COMMITMENT is the sequential hash of the padded assets of an input note. -export.get_input_note_assets_commitment +pub proc get_input_note_assets_commitment padw movup.4 add.INPUT_NOTE_ASSETS_COMMITMENT_OFFSET mem_loadw_be @@ -1773,42 +1802,12 @@ end #! Where: #! - note_ptr is the memory address at which the input note data begins. #! - SERIAL_NUMBER is the input note's serial number. -export.get_input_note_serial_num +pub proc get_input_note_serial_num padw movup.4 add.INPUT_NOTE_SERIAL_NUM_OFFSET mem_loadw_be end -#! Returns the sender for the input note located at the specified memory address. -#! -#! Inputs: [note_ptr] -#! Outputs: [sender_id_prefix, sender_id_suffix] -#! -#! Where: -#! - note_ptr is the memory address at which the input note data begins. -#! - sender is the sender for the input note. -export.get_input_note_sender - padw - movup.4 add.INPUT_NOTE_METADATA_OFFSET - mem_loadw_be - # => [aux, merged_tag_hint_payload, merged_sender_id_type_hint_tag, sender_id_prefix] - - drop drop - # => [merged_sender_id_type_hint_tag, sender_id_prefix] - - # extract suffix of sender from merged layout, which means clearing the least significant byte - u32split swap - # => [merged_lo, merged_hi, sender_id_prefix] - - # clear least significant byte - u32and.0xffffff00 swap - # => [sender_id_suffix_hi, sender_id_suffix_lo, sender_id_prefix] - - # reassemble the suffix by multiplying the high part with 2^32 and adding the lo part - mul.0x0100000000 add swap - # => [sender_id_prefix, sender_id_suffix] -end - # OUTPUT NOTES # ------------------------------------------------------------------------------------------------- @@ -1819,7 +1818,7 @@ end #! #! Where: #! - offset is the offset of the output note data segment. -export.get_output_note_data_offset +pub proc get_output_note_data_offset push.OUTPUT_NOTE_SECTION_OFFSET end @@ -1832,8 +1831,8 @@ end #! Where: #! - i is the index of the output note. #! - ptr is the memory address of the data segment for output note i. -export.get_output_note_ptr - exec.constants::get_note_mem_size mul add.OUTPUT_NOTE_SECTION_OFFSET +pub proc get_output_note_ptr + push.NOTE_MEM_SIZE mul add.OUTPUT_NOTE_SECTION_OFFSET end #! Returns the output note recipient. @@ -1844,7 +1843,7 @@ end #! Where: #! - note_ptr is the memory address at which the output note data begins. #! - RECIPIENT is the commitment to the note's script, inputs and the serial number. -export.get_output_note_recipient +pub proc get_output_note_recipient padw movup.4 add.OUTPUT_NOTE_RECIPIENT_OFFSET mem_loadw_be @@ -1858,7 +1857,7 @@ end #! Where: #! - note_ptr is the memory address at which the output note data begins. #! - RECIPIENT is the commitment to the note's script, inputs and the serial number. -export.set_output_note_recipient +pub proc set_output_note_recipient add.OUTPUT_NOTE_RECIPIENT_OFFSET mem_storew_be end @@ -1866,31 +1865,73 @@ end #! Returns the output note's metadata. #! #! Inputs: [note_ptr] -#! Outputs: [METADATA] +#! Outputs: [METADATA_HEADER] #! #! Where: -#! - METADATA is the note metadata. +#! - METADATA_HEADER is the note metadata header. #! - note_ptr is the memory address at which the output note data begins. -export.get_output_note_metadata +pub proc get_output_note_metadata_header padw # => [0, 0, 0, 0, note_ptr] - movup.4 add.OUTPUT_NOTE_METADATA_OFFSET + movup.4 add.OUTPUT_NOTE_METADATA_HEADER_OFFSET # => [(note_ptr + offset), 0, 0, 0, 0] mem_loadw_be - # => [METADATA] + # => [METADATA_HEADER] +end + +#! Sets the output note's metadata header. +#! +#! Inputs: [note_ptr, METADATA_HEADER] +#! Outputs: [METADATA_HEADER] +#! +#! Where: +#! - METADATA_HEADER is the note metadata header. +#! - note_ptr is the memory address at which the output note data begins. +pub proc set_output_note_metadata_header + add.OUTPUT_NOTE_METADATA_HEADER_OFFSET + mem_storew_be +end + +#! Sets the output note's attachment kind and scheme in the metadata header. +#! +#! Inputs: [note_ptr, attachment_kind_scheme] +#! Outputs: [] +#! +#! Where: +#! - attachment_kind_scheme is the type information of the attachment that will be overwritten. +#! - note_ptr is the memory address at which the output note data begins. +pub proc set_output_note_attachment_kind_scheme + add.OUTPUT_NOTE_METADATA_ATTACHMENT_KIND_SCHEME_OFFSET + mem_store +end + +#! Returns the output note's attachment. +#! +#! Inputs: [note_ptr] +#! Outputs: [ATTACHMENT] +#! +#! Where: +#! - ATTACHMENT is the note attachment. +#! - note_ptr is the memory address at which the output note data begins. +pub proc get_output_note_attachment + padw + movup.4 add.OUTPUT_NOTE_ATTACHMENT_OFFSET + mem_loadw_be + # => [ATTACHMENT] end -#! Sets the output note's metadata. +#! Sets the output note's attachment. #! -#! Inputs: [note_ptr, METADATA] -#! Outputs: [METADATA] +#! Inputs: [note_ptr, ATTACHMENT] +#! Outputs: [] #! #! Where: -#! - METADATA is the note metadata. +#! - ATTACHMENT is the note attachment. #! - note_ptr is the memory address at which the output note data begins. -export.set_output_note_metadata - add.OUTPUT_NOTE_METADATA_OFFSET +pub proc set_output_note_attachment + add.OUTPUT_NOTE_ATTACHMENT_OFFSET mem_storew_be + dropw end #! Returns the number of assets in the output note. @@ -1901,7 +1942,7 @@ end #! Where: #! - note_ptr is a pointer to the memory address at which the output note is stored. #! - num_assets is the number of assets in the output note. -export.get_output_note_num_assets +pub proc get_output_note_num_assets add.OUTPUT_NOTE_NUM_ASSETS_OFFSET mem_load end @@ -1916,12 +1957,12 @@ end #! #! Panics if: #! - the number of assets exceeds the maximum allowed number of assets per note. -export.set_output_note_num_assets +pub proc set_output_note_num_assets add.OUTPUT_NOTE_NUM_ASSETS_OFFSET # => [note_ptr + offset, num_assets] # check note number of assets limit - dup.1 exec.constants::get_max_assets_per_note lt assert.err=ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT + dup.1 push.MAX_ASSETS_PER_NOTE lt assert.err=ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT mem_store end @@ -1937,7 +1978,7 @@ end #! Where: #! - output_note_data_ptr is the memory address at which the output note data begins. #! - dirty_flag is the flag indicating whether the assets commitment is outdated. -export.get_output_note_dirty_flag +pub proc get_output_note_dirty_flag add.OUTPUT_NOTE_DIRTY_FLAG_OFFSET mem_load end @@ -1952,7 +1993,7 @@ end #! Where: #! - output_note_data_ptr is the memory address at which the output note data begins. #! - dirty_flag is the flag indicating whether the assets commitment is outdated. -export.set_output_note_dirty_flag +pub proc set_output_note_dirty_flag add.OUTPUT_NOTE_DIRTY_FLAG_OFFSET mem_store end @@ -1964,7 +2005,7 @@ end #! Where: #! - output_note_data_ptr is the memory address at which the output note data begins. #! - asset_data_ptr is the memory address at which the output note asset data begins. -export.get_output_note_asset_data_ptr +pub proc get_output_note_asset_data_ptr add.OUTPUT_NOTE_ASSETS_OFFSET end @@ -1976,7 +2017,7 @@ end #! Where: #! - output_note_data_ptr is the memory address at which the output note data begins. #! - ASSETS_COMMITMENT is the sequential hash of the padded assets of an output note. -export.get_output_note_assets_commitment +pub proc get_output_note_assets_commitment padw movup.4 add.OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET mem_loadw_be @@ -1991,7 +2032,7 @@ end #! Where: #! - output_note_data_ptr is the memory address at which the output note data begins. #! - ASSETS_COMMITMENT is the sequential hash of the padded assets of an output note. -export.set_output_note_assets_commitment +pub proc set_output_note_assets_commitment add.OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET mem_storew_be end @@ -2006,7 +2047,7 @@ end #! #! Where: #! - num_kernel_procedures is the number of the procedures of the selected kernel. -export.set_num_kernel_procedures +pub proc set_num_kernel_procedures mem_store.NUM_KERNEL_PROCEDURES_PTR end @@ -2017,7 +2058,7 @@ end #! #! Where: #! - num_kernel_procedures is the number of the procedures of the selected kernel. -export.get_num_kernel_procedures +pub proc get_num_kernel_procedures mem_load.NUM_KERNEL_PROCEDURES_PTR end @@ -2029,7 +2070,7 @@ end #! Where: #! - kernel_procedures_ptr is the memory address where the hashes of the kernel procedures are #! stored. -export.get_kernel_procedures_ptr +pub proc get_kernel_procedures_ptr push.KERNEL_PROCEDURES_PTR end @@ -2040,7 +2081,7 @@ end #! #! Inputs: [] #! Outputs: [start_ptr] -export.get_link_map_region_start_ptr +pub proc get_link_map_region_start_ptr push.LINK_MAP_REGION_START_PTR end @@ -2048,7 +2089,7 @@ end #! #! Inputs: [] #! Outputs: [end_ptr] -export.get_link_map_region_end_ptr +pub proc get_link_map_region_end_ptr push.LINK_MAP_REGION_END_PTR end @@ -2056,7 +2097,7 @@ end #! #! Inputs: [] #! Outputs: [entry_size] -export.get_link_map_entry_size +pub proc get_link_map_entry_size push.LINK_MAP_ENTRY_SIZE end @@ -2067,7 +2108,7 @@ end #! #! Panics if: #! - the allocation exceeds the maximum possible number of link map entries. -export.link_map_malloc +pub proc link_map_malloc # retrieve the current memory size mem_load.LINK_MAP_USED_MEMORY_SIZE dup # => [current_mem_size, current_mem_size] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm similarity index 74% rename from crates/miden-lib/asm/kernels/transaction/lib/note.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/note.masm index b884a3728f..581d6598d7 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/note.masm @@ -1,19 +1,19 @@ -use.std::crypto::hashes::rpo +use miden::core::crypto::hashes::rpo256 -use.$kernel::constants -use.$kernel::memory +use $kernel::constants::NOTE_MEM_SIZE +use $kernel::memory # 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 exceed 255" # 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=2040 # ACTIVE NOTE PROCEDURES # ================================================================================================= @@ -25,13 +25,13 @@ const.OUTPUT_NOTE_HASHING_MEM_DIFF=2040 #! #! Where: #! - active_input_note_ptr is the pointer to the next note to be processed. -export.increment_active_input_note_ptr +pub proc increment_active_input_note_ptr # get the active input note pointer exec.memory::get_active_input_note_ptr # => [orig_input_note_ptr] # increment the pointer - exec.constants::get_note_mem_size add + push.NOTE_MEM_SIZE add # => [active_input_note_ptr] # set the active input note pointer to the incremented value @@ -44,7 +44,7 @@ end #! #! Inputs: [] #! Outputs: [] -export.note_processing_teardown +pub proc note_processing_teardown # set the active input note pointer to 0 push.0 exec.memory::set_active_input_note_ptr # => [] @@ -60,7 +60,7 @@ end #! Where: #! - note_script_root_ptr is the memory address where note's script root is stored. #! - NOTE_ARGS is the note's arguments. -export.prepare_note +pub proc prepare_note padw padw push.0.0.0 # => [pad(11)] @@ -92,7 +92,7 @@ end #! Where: #! - note_data_ptr is a pointer to the data section of the output note. #! - ASSETS_COMMITMENT is the commitment of the assets of the output note located at note_data_ptr. -export.compute_output_note_assets_commitment +pub proc compute_output_note_assets_commitment # get the assets commitment dirty flag and decide whether we need to recompute the commitment dup exec.memory::get_output_note_dirty_flag # => [dirty_flag, note_data_ptr] @@ -128,7 +128,7 @@ export.compute_output_note_assets_commitment # read assets from memory. # if this is the last permutation of the loop and we have an odd number of assets then we # implicitly pad the last word of the hasher rate with zeros by reading from empty memory. - mem_stream hperm + mem_stream exec.rpo256::permute # => [PERM, PERM, PERM, asset_data_ptr, asset_counter, num_asset_pairs, note_data_ptr] # check if we should loop again @@ -138,7 +138,7 @@ export.compute_output_note_assets_commitment end # extract digest - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [ASSETS_COMMITMENT, asset_data_ptr, asset_counter, num_asset_pairs, note_data_ptr] # drop accessory variables from stack @@ -176,7 +176,7 @@ end #! Where: #! - note_data_ptr is a pointer to the data section of the output note. #! - NOTE_ID is the ID of the output note located at note_data_ptr. -proc.compute_output_note_id +proc compute_output_note_id # pad capacity elements of hasher padw # => [EMPTY_WORD, note_data_ptr] @@ -190,7 +190,7 @@ proc.compute_output_note_id # => [ASSETS_COMMITMENT, RECIPIENT, EMPTY_WORD, note_data_ptr] # compute output note commitment (which is also note ID) and extract digest - hperm exec.rpo::squeeze_digest + exec.rpo256::permute exec.rpo256::squeeze_digest # => [NOTE_ID, note_data_ptr] # save the output note commitment (note ID) to memory @@ -206,56 +206,66 @@ end #! #! Where: #! - OUTPUT_NOTES_COMMITMENT is the commitment to the notes output by the transaction. -export.compute_output_notes_commitment +pub proc compute_output_notes_commitment # get the number of output notes from memory exec.memory::get_num_output_notes - # => [num_notes, ...] + # => [num_notes] - # calculate the address at which we should stop looping - exec.memory::get_output_note_ptr - # => [end_ptr, ...] - - # compute pointer for first address - push.0 exec.memory::get_output_note_ptr - # => [first_note_ptr, end_ptr, ...] + # initialize the output note index at which to start the commitment computation + push.0 + # => [current_index = 0, num_notes] # prepare stack for hashing padw padw padw - # => [PERM, PERM, PERM, first_note_ptr, end_ptr, ...] + # => [PERM, PERM, PERM, current_index, num_notes] - # check if the number of output notes is greater then 0. Conditional for the while loop. - dup.13 dup.13 neq - # => [PERM, PERM, PERM, first_note_ptr, end_ptr, ...] + # starting looping if num_notes != 0 + dup.13 neq.0 + # => [should_loop, PERM, PERM, PERM, current_index, num_notes] # loop and hash output notes while.true + dup.12 exec.memory::get_output_note_ptr + # => [current_note_ptr, PERM, PERM, PERM, current_index, num_notes] + # compute and save output note ID to memory (this also computes the note's asset commitment) - dup.12 exec.compute_output_note_id - # => [NOTE_ID, PERM, PERM, PERM, note_ptr, end_ptr, ...] + dup exec.compute_output_note_id + # => [NOTE_ID, current_note_ptr, PERM, PERM, PERM, current_index, num_notes] + + dup.4 exec.memory::get_output_note_metadata_header + # => [NOTE_METADATA_HEADER, NOTE_ID, current_note_ptr, PERM, PERM, PERM, current_index, num_notes] + + movup.8 exec.memory::get_output_note_attachment + # => [NOTE_ATTACHMENT, NOTE_METADATA_HEADER, NOTE_ID, current_note_ptr, PERM, PERM, PERM, current_index, num_notes] + + # compute hash(NOTE_METADATA_HEADER || NOTE_ATTACHMENT) + exec.rpo256::merge + # => [NOTE_METADATA_COMMITMENT, NOTE_ID, current_note_ptr, PERM, PERM, PERM, current_index, num_notes] - # drop output note ID from stack (it will be read from memory by the next instruction) - dropw - # => [PERM, PERM, PERM, note_ptr, end_ptr, ...] + # replace rate words with note ID and metadata commitment + swapdw dropw dropw + # => [NOTE_METADATA_COMMITMENT, NOTE_ID, PERM, current_index, num_notes] - # permute over (note_id, note_metadata) - mem_stream hperm - # => [PERM, PERM, PERM, note_ptr + 8, end_ptr, ...] + # permute over (note_id, note_metadata_commitment) + exec.rpo256::permute + # => [PERM, PERM, PERM, current_index, num_notes] - # increment output note pointer - movup.12 add.OUTPUT_NOTE_HASHING_MEM_DIFF - # => [note_ptr + 2048, PERM, PERM, PERM, end_ptr, ...] + # increment current_index + movup.12 add.1 movdn.12 + # => [PERM, PERM, PERM, current_index + 1, num_notes] - # check if we should loop again - dup movdn.13 dup.14 neq - # => [should_loop, PERM, PERM, PERM, note_ptr + 512, end_ptr, ...] + # continue looping if current_index != num_notes + dup.13 dup.13 neq + # => [should_loop, PERM, PERM, PERM, current_index + 1, num_notes] end + # => [PERM, PERM, PERM, current_index + 1, num_notes] # extract digest - exec.rpo::squeeze_digest - # => [OUTPUT_NOTES_COMMITMENT, end_ptr, end_ptr, ...] + exec.rpo256::squeeze_digest + # => [OUTPUT_NOTES_COMMITMENT, current_index + 1, num_notes] # drop accessory variables from stack movup.4 drop movup.4 drop - # => [OUTPUT_NOTES_COMMITMENT, ...] + # => [OUTPUT_NOTES_COMMITMENT] end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm similarity index 62% rename from crates/miden-lib/asm/kernels/transaction/lib/output_note.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 902ea9768f..654e8ae6a3 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -1,119 +1,125 @@ -use.$kernel::account -use.$kernel::memory -use.$kernel::note -use.$kernel::asset -use.$kernel::constants -use.std::word +use $kernel::account +use $kernel::memory +use $kernel::note +use $kernel::asset +use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX +use miden::core::mem +use miden::core::word # CONSTANTS # ================================================================================================= # Constants for different note types -const.PUBLIC_NOTE=1 # 0b01 -const.PRIVATE_NOTE=2 # 0b10 -const.ENCRYPTED_NOTE=3 # 0b11 +const PUBLIC_NOTE=1 # 0b01 +const PRIVATE_NOTE=2 # 0b10 +const ENCRYPTED_NOTE=3 # 0b11 -# The note type must be PUBLIC, unless the high bits are `0b11`. (See the table below.) -const.LOCAL_ANY_PREFIX=3 # 0b11 +# Constants for note attachment kinds +const ATTACHMENT_KIND_NONE=0 +const ATTACHMENT_KIND_WORD=1 +const ATTACHMENT_KIND_ARRAY=2 + +# 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 + +#! The default attachment scheme, representing the absence of an attachment scheme. +const ATTACHMENT_SCHEME_NONE=0 # ERRORS # ================================================================================================= -const.ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT="number of output notes in the transaction exceeds the maximum limit of 1024" +const ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT="number of output notes in the transaction exceeds the maximum limit of 1024" + +const ERR_NOTE_INVALID_TYPE="invalid note type" + +const ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested output note index should be less than the total number of created output notes" -const.ERR_NOTE_INVALID_TYPE="invalid note type" +const ERR_OUTPUT_NOTE_INVALID_ATTACHMENT_SCHEMES="attachment scheme and attachment kind must fit into u32s" -const.ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested output note index should be less than the total number of created output notes" +const ERR_OUTPUT_NOTE_UNKNOWN_ATTACHMENT_KIND="attachment kind variant must be between 0 and 2" -const.ERR_NOTE_INVALID_INDEX="failed to find note at the given index; index must be within [0, num_of_notes]" +const ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_HAVE_ATTACHMENT_SCHEME_NONE="attachment kind none must have attachment scheme none" -const.ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807" +const ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_BE_EMPTY_WORD="attachment kind None requires ATTACHMENT to be set to an empty word" -const.ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="non-fungible asset that already exists in the note cannot be added again" +const ERR_NOTE_INVALID_INDEX="failed to find note at the given index; index must be within [0, num_of_notes]" -# The 2 highest bits in the u32 tag have the following meaning: -# -# | Prefix | Name | [`NoteExecutionMode`] | Target | Allowed [`NoteType`] | -# | :----: | :--------------------: | :-------------------: | :----------------------: | :------------------: | -# | `0b00` | `NetworkAccount` | Network | Network Account | [`NoteType::Public`] | -# | `0b01` | `NetworkUseCase` | Network | Use case | [`NoteType::Public`] | -# | `0b10` | `LocalPublicAny` | Local | Any | [`NoteType::Public`] | -# | `0b11` | `LocalAny` | Local | Any | Any | -# -# Execution: Is a hint for the network, to check if the note can be consumed by a network controlled -# account -# Target: Is a hint for the type of target. Use case means the note may be consumed by anyone, -# specific means there is a specific target for the note (the target may be a public key, a user -# that knows some secret, or a specific account ID) -# -# Only the note type from the above list is enforced. The other values are only hints intended as a -# best effort optimization strategy. A badly formatted note may 1. not be consumed because honest -# users won't see the note 2. generate slightly more load as extra validation is performed for the -# invalid tags. None of these scenarios have any significant impact. +const ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807" -const.ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX="invalid note type for the given note tag prefix" +const ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="non-fungible asset that already exists in the note cannot be added again" -const.ERR_NOTE_TAG_MUST_BE_U32="the note's tag must fit into a u32 so the 32 most significant bits must be zero" +const ERR_NOTE_TAG_MUST_BE_U32="the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero" # EVENTS # ================================================================================================= # Event emitted before a new note is created. -const.NOTE_BEFORE_CREATED_EVENT=event("miden::note::before_created") +const NOTE_BEFORE_CREATED_EVENT=event("miden::note::before_created") # Event emitted after a new note is created. -const.NOTE_AFTER_CREATED_EVENT=event("miden::note::after_created") +const NOTE_AFTER_CREATED_EVENT=event("miden::note::after_created") # Event emitted before an ASSET is added to a note -const.NOTE_BEFORE_ADD_ASSET_EVENT=event("miden::note::before_add_asset") +const NOTE_BEFORE_ADD_ASSET_EVENT=event("miden::note::before_add_asset") # Event emitted after an ASSET is added to a note -const.NOTE_AFTER_ADD_ASSET_EVENT=event("miden::note::after_add_asset") +const NOTE_AFTER_ADD_ASSET_EVENT=event("miden::note::after_add_asset") + +# Event emitted before an ATTACHMENT is added to a note +const NOTE_BEFORE_SET_ATTACHMENT_EVENT=event("miden::note::before_set_attachment") # OUTPUT NOTE PROCEDURES # ================================================================================================= #! Creates a new note and returns the index of the note. #! -#! Inputs: [tag, aux, note_type, execution_hint, RECIPIENT] +#! Inputs: [tag, note_type, RECIPIENT] #! Outputs: [note_idx] #! #! Where: #! - tag is the note tag which can be used by the recipient(s) to identify notes intended for them. -#! - aux is the arbitrary user-defined value. #! - note_type is the type of the note, which defines how the note is to be stored (e.g., on-chain #! or off-chain). -#! - execution_hint is the hint which specifies when a note is ready to be consumed. #! - RECIPIENT defines spend conditions for the note. #! - note_idx is the index of the created note. #! #! Panics if: -#! - the note_type is not valid. -#! - the note_tag is not an u32. -#! - the note_tag starts with anything but 0b11 and note_type is not public. +#! - the note_type is unknown. +#! - the note tag is not a u32. #! - the number of output notes exceeds the maximum limit of 1024. -export.create +pub proc create emit.NOTE_BEFORE_CREATED_EVENT + # => [tag, note_type, RECIPIENT] - exec.build_metadata - # => [NOTE_METADATA, RECIPIENT] + exec.build_metadata_header + # => [NOTE_METADATA_HEADER, RECIPIENT] # get the index for the next note to be created and increment counter exec.increment_num_output_notes dup movdn.9 - # => [note_idx, NOTE_METADATA, RECIPIENT, note_idx] + # => [note_idx, NOTE_METADATA_HEADER, RECIPIENT, note_idx] # get a pointer to the memory address at which the note will be stored exec.memory::get_output_note_ptr - # => [note_ptr, NOTE_METADATA, RECIPIENT, note_idx] + # => [note_ptr, NOTE_METADATA_HEADER, RECIPIENT, note_idx] movdn.4 - # => [NOTE_METADATA, note_ptr, RECIPIENT, note_idx] + # => [NOTE_METADATA_HEADER, note_ptr, RECIPIENT, note_idx] # emit event to signal that a new note is created emit.NOTE_AFTER_CREATED_EVENT # set the metadata for the output note dup.4 - # => [note_ptr, NOTE_METADATA, note_ptr, RECIPIENT, note_idx] - exec.memory::set_output_note_metadata dropw + # => [note_ptr, NOTE_METADATA_HEADER, note_ptr, RECIPIENT, note_idx] + + exec.memory::set_output_note_metadata_header dropw + # => [note_ptr, RECIPIENT, note_idx] + + # set the attachment value of a new note to an empty word + # note that the attachment kind is set to None by build_metadata_header + padw dup.4 + # => [note_ptr, EMPTY_WORD, note_ptr, RECIPIENT, note_idx] + + exec.memory::set_output_note_attachment # => [note_ptr, RECIPIENT, note_idx] # set the RECIPIENT for the output note @@ -132,7 +138,7 @@ end #! - note_index is the index of the output note whose assets info should be returned. #! - num_assets is the number of assets in the specified note. #! - ASSETS_COMMITMENT is a sequential hash of the assets in the specified note. -export.get_assets_info +pub proc get_assets_info # get the note data pointer based on the index of the requested note exec.memory::get_output_note_ptr # => [note_data_ptr] @@ -146,7 +152,7 @@ export.get_assets_info # => [ASSETS_COMMITMENT, note_data_ptr, num_assets] # next we should store the assets in the advice map using the computed assets commitment to be - # able to get the assets later (in the `miden::output_note::get_assets` procedure) + # able to get the assets later (in the `miden::protocol::output_note::get_assets` procedure) # get the start and the end pointers of the asset data # @@ -184,11 +190,12 @@ end #! - ASSET can be a fungible or non-fungible asset. #! #! Panics if: +#! - the note index points to a non-existent output note. #! - the ASSET is 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. -export.add_asset +pub proc add_asset # check if the note exists, it must be within [0, num_of_notes] dup exec.memory::get_num_output_notes lte assert.err=ERR_NOTE_INVALID_INDEX # => [note_idx, ASSET] @@ -238,11 +245,56 @@ export.add_asset # => [] end +#! Sets the attachment of the note specified by the index. +#! +#! Inputs: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +#! Outputs: [] +#! +#! Where: +#! - note_idx is the index of the note on which the attachment is set. +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - attachment_kind is the kind of the attachment content. +#! - ATTACHMENT is the attachment to be set. +#! +#! Panics if: +#! - the note index points to a non-existent output note. +#! - the attachment kind or scheme does not fit into a u32. +#! - the attachment kind is an unknown variant. +pub proc set_attachment + dup exec.memory::get_num_output_notes lte assert.err=ERR_NOTE_INVALID_INDEX + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + + exec.memory::get_output_note_ptr dup + # => [note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] + + dupw.1 + # => [ATTACHMENT, note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] + + dup.7 dup.7 + # => [attachment_scheme, attachment_kind, ATTACHMENT, note_ptr, note_ptr, + # attachment_scheme, attachment_kind, ATTACHMENT] + + exec.validate_attachment + # => [note_ptr, note_ptr, attachment_scheme, attachment_kind, ATTACHMENT] + + movdn.3 movdn.3 + # => [attachment_scheme, attachment_kind, note_ptr, note_ptr, ATTACHMENT] + + emit.NOTE_BEFORE_SET_ATTACHMENT_EVENT + # => [attachment_scheme, attachment_kind, note_ptr, note_ptr, ATTACHMENT] + + exec.set_attachment_kind_scheme + # => [note_ptr, ATTACHMENT] + + exec.memory::set_output_note_attachment + # => [] +end + #! Assert that the provided note index is less than the total number of output notes. #! #! Inputs: [note_index] #! Outputs: [note_index] -export.assert_note_index_in_bounds +pub proc assert_note_index_in_bounds # assert that the provided note index is less than the total number of notes dup exec.memory::get_num_output_notes # => [output_notes_num, note_index, note_index] @@ -255,108 +307,137 @@ end # HELPER PROCEDURES # ================================================================================================= -#! Builds the stack into the NOTE_METADATA word, encoding the note type and execution hint into a -#! single element. +#! Builds the provided inputs into the NOTE_METADATA_HEADER word. +#! +#! - The sender ID is set to the native account's ID. +#! - The attachment scheme is set to 0 (meaning none by convention) and the attachment content +#! type is set to None. +#! #! Note that this procedure is only exported so it can be tested. It should not be called from #! non-test code. #! -#! Inputs: [tag, aux, note_type, execution_hint] -#! Outputs: [NOTE_METADATA] +#! Inputs: [tag, note_type] +#! Outputs: [NOTE_METADATA_HEADER] #! #! Where: #! - tag is the note tag which can be used by the recipient(s) to identify notes intended for them. -#! - aux is the arbitrary user-defined value. #! - note_type is the type of the note, which defines how the note is to be stored (e.g., on-chain #! or off-chain). -#! - execution_hint is the hint which specifies when a note is ready to be consumed. -#! - NOTE_METADATA is the metadata associated with a note. -export.build_metadata - # Validate the note type. +#! - NOTE_METADATA_HEADER is the metadata associated with a note. +pub proc build_metadata_header + # Validate that note type is private or public. # -------------------------------------------------------------------------------------------- - # NOTE: encrypted notes are currently unsupported - dup.2 eq.PRIVATE_NOTE dup.3 eq.PUBLIC_NOTE or assert.err=ERR_NOTE_INVALID_TYPE - # => [tag, aux, note_type, execution_hint] + dup.1 eq.PRIVATE_NOTE dup.2 eq.PUBLIC_NOTE or assert.err=ERR_NOTE_INVALID_TYPE + # => [tag, note_type] - # copy data to validate the tag - dup.2 push.PUBLIC_NOTE dup.1 dup.3 - # => [tag, note_type, public_note, note_type, tag, aux, note_type, execution_hint] + # Validate the note tag fits into a u32. + # -------------------------------------------------------------------------------------------- u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 - # => [tag, note_type, public_note, note_type, tag, aux, note_type, execution_hint] + # => [tag, note_type] - # enforce the note type depending on the tag' bits - u32shr.30 eq.LOCAL_ANY_PREFIX cdrop - assert_eq.err=ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX - # => [tag, aux, note_type, execution_hint] - - # Split execution hint into its tag and payload parts as they are encoded in separate elements - # of the metadata. + # Merge note type and sender ID suffix. # -------------------------------------------------------------------------------------------- - # the execution_hint is laid out like this: [26 zero bits | payload (32 bits) | tag (6 bits)] - movup.3 - # => [execution_hint, tag, aux, note_type] - dup u32split drop - # => [execution_hint_lo, execution_hint, tag, aux, note_type] - - # mask out the lower 6 execution hint tag bits. - u32and.0x3f - # => [execution_hint_tag, execution_hint, tag, aux, note_type] + exec.account::get_id swap + # => [sender_id_suffix, sender_id_prefix, tag, note_type] - # compute the payload by subtracting the tag value so the lower 6 bits are zero - # note that this results in the following layout: [26 zero bits | payload (32 bits) | 6 zero bits] - swap - # => [execution_hint, execution_hint_tag, tag, aux, note_type] - dup.1 - # => [execution_hint_tag, execution_hint, execution_hint_tag, tag, aux, note_type] - sub - # => [execution_hint_payload, execution_hint_tag, tag, aux, 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 swap + # => [sender_id_prefix, sender_id_suffix_and_note_type, tag] - # Merge execution hint payload and note tag. + # Build metadata header. # -------------------------------------------------------------------------------------------- - # we need to move the payload to the upper 32 bits of the felt - # we only need to shift by 26 bits because the payload is already shifted left by 6 bits - # we shift the payload by multiplying with 2^26 - # this results in the lower 32 bits being zero which is where the note tag will be added - mul.0x04000000 - # => [execution_hint_payload, execution_hint_tag, tag, aux, note_type] - - # add the tag to the payload to produce the merged value - movup.2 add - # => [note_tag_hint_payload, execution_hint_tag, aux, note_type] + movup.2 + push.ATTACHMENT_DEFAULT_KIND_AND_SCHEME + # => [attachment_kind_scheme, tag, sender_id_prefix, sender_id_suffix_and_note_type] + # => [NOTE_METADATA_HEADER] +end - # Merge sender_id_suffix, note_type and execution_hint_tag. - # -------------------------------------------------------------------------------------------- +#! Validate the ATTACHMENT against the attachment kind. +#! +#! Inputs: [attachment_scheme, attachment_kind, ATTACHMENT] +#! Outputs: [] +#! +#! Where: +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - attachment_kind is the kind of the attachment content. +#! - ATTACHMENT is the attachment to validate. +#! +#! Panics if: +#! - the attachment kind or scheme does not fit into a u32. +#! - the attachment kind is an unknown variant. +#! - the attachment kind is None and the ATTACHMENT is not an empty word. +proc validate_attachment + u32assert2.err=ERR_OUTPUT_NOTE_INVALID_ATTACHMENT_SCHEMES + # => [attachment_scheme, attachment_kind, ATTACHMENT] - exec.account::get_id - # => [sender_id_prefix, sender_id_suffix, note_tag_hint_payload, execution_hint_tag, aux, note_type] + # assert that the attachment kind is valid + swap dup u32lte.ATTACHMENT_KIND_ARRAY + assert.err=ERR_OUTPUT_NOTE_UNKNOWN_ATTACHMENT_KIND + # => [attachment_kind, attachment_scheme, ATTACHMENT] - movup.5 - # => [note_type, sender_id_prefix, sender_id_suffix, note_tag_hint_payload, execution_hint_tag, aux] - # multiply by 2^6 to shift the two note_type bits left by 6 bits. - mul.0x40 - # => [shifted_note_type, sender_id_prefix, sender_id_suffix, note_tag_hint_payload, execution_hint_tag, aux] + eq.ATTACHMENT_KIND_NONE + # => [is_attachment_none, attachment_scheme, ATTACHMENT] - # merge execution_hint_tag into the note_type - # this produces an 8-bit value with the layout: [note_type (2 bits) | execution_hint_tag (6 bits)] - movup.4 add - # => [merged_note_type_execution_hint_tag, sender_id_prefix, sender_id_suffix, note_tag_hint_payload, aux] + if.true + eq.ATTACHMENT_SCHEME_NONE + assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_HAVE_ATTACHMENT_SCHEME_NONE + # => [ATTACHMENT] - # merge sender_id_suffix into this value - movup.2 add - # => [sender_id_suffix_type_and_hint_tag, sender_id_prefix, note_tag_hint_payload, aux] + padw assert_eqw.err=ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_BE_EMPTY_WORD + # => [] + else + drop dropw + # => [] + end + # => [] +end - # Rearrange elements to produce the final note metadata layout. - # -------------------------------------------------------------------------------------------- +#! Sets an output note's attachment kind and scheme in the note metadata header. +#! +#! WARNING: The attachment scheme and kind must be valid. +#! +#! Inputs: [attachment_scheme, attachment_kind, note_ptr] +#! Outputs: [] +#! +#! Where: +#! - attachment_scheme is the user-defined type of the attachment. +#! - attachment_kind is the kind of the attachment content. +#! - note_ptr is the memory address at which the output note data begins. +proc set_attachment_kind_scheme + exec.merge_attachment_kind_and_scheme + # => [attachment_kind_scheme, note_ptr] - swap movdn.3 - # => [sender_id_suffix_type_and_hint_tag, note_tag_hint_payload, aux, sender_id_prefix] swap - # => [note_tag_hint_payload, sender_id_suffix_type_and_hint_tag, aux, sender_id_prefix] - movup.2 - # => [NOTE_METADATA = [aux, note_tag_hint_payload, sender_id_suffix_type_and_hint_tag, sender_id_prefix]] + # => [note_ptr, attachment_kind_scheme] + + exec.memory::set_output_note_attachment_kind_scheme + # => [] +end + +#! Merges the attachment kind and scheme into a single felt with the following layout: +#! +#! [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] +#! +#! WARNING: The attachment scheme and kind must be valid. +#! +#! Inputs: [attachment_scheme, attachment_kind] +#! Outputs: [attachment_kind_scheme] +#! +#! Where: +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - attachment_kind is the kind of the attachment content. +#! - attachment_kind_scheme is the felt constructed from the inputs. +proc merge_attachment_kind_and_scheme + # shift the attachment_kind 32 bits to the left, which is the same as multiplying by 2^32 + # and set the lower bits to the attachment_scheme, which is done by adding the values together + swap mul.0x100000000 + add + # => [attachment_kind_scheme] end #! Increments the number of output notes by one. Returns the index of the next note to be created. @@ -366,13 +447,13 @@ end #! #! Where: #! - note_idx is the index of the next note to be created. -proc.increment_num_output_notes +proc increment_num_output_notes # get the current number of output notes exec.memory::get_num_output_notes # => [note_idx] # assert that there is space for a new note - dup exec.constants::get_max_num_output_notes lt + dup push.MAX_OUTPUT_NOTES_PER_TX lt assert.err=ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT # => [note_idx] @@ -397,7 +478,7 @@ end #! #! Panics if #! - the summed amounts exceed the maximum amount of fungible assets. -proc.add_fungible_asset +proc add_fungible_asset dup.4 exec.memory::get_output_note_asset_data_ptr # => [asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] @@ -487,7 +568,7 @@ end #! #! Panics if: #! - the non-fungible asset already exists in the note. -proc.add_non_fungible_asset +proc add_non_fungible_asset dup.4 exec.memory::get_output_note_asset_data_ptr # => [asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/prologue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm similarity index 85% rename from crates/miden-lib/asm/kernels/transaction/lib/prologue.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm index 0f4848deb0..bb0ce6e9d5 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm @@ -1,71 +1,77 @@ -use.std::mem -use.std::collections::mmr -use.std::crypto::hashes::rpo -use.std::word - -use.$kernel::account -use.$kernel::account_delta -use.$kernel::account_id -use.$kernel::asset_vault -use.$kernel::constants -use.$kernel::memory +use miden::core::mem +use miden::core::collections::mmr +use miden::core::crypto::hashes::rpo256 +use miden::core::word + +use $kernel::account +use $kernel::account_delta +use $kernel::account_id +use $kernel::asset_vault +use $kernel::constants::EMPTY_SMT_ROOT +use $kernel::constants::MAX_ASSETS_PER_NOTE +use $kernel::constants::MAX_INPUT_NOTES_PER_TX +use $kernel::constants::MAX_INPUTS_PER_NOTE +use $kernel::constants::NOTE_TREE_DEPTH +use $kernel::constants::STORAGE_SLOT_TYPE_MAP +use $kernel::constants::STORAGE_SLOT_TYPE_VALUE +use $kernel::memory # CONSTS # ================================================================================================= # Max U32 value, used for initializing the expiration block number -const.MAX_BLOCK_NUM=0xFFFFFFFF +const MAX_BLOCK_NUM=0xFFFFFFFF # EVENTS #================================================================================================= # Emission of an equivalent to `ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT`, use in `add_input_note_assets_to_vault` -const.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT=event("miden::account::vault_before_add_asset") +const ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT=event("miden::account::vault_before_add_asset") # ERRORS # ================================================================================================= -const.ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_COMMITMENT="the provided global inputs do not match the block commitment" +const ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_COMMITMENT="the provided global inputs do not match the block commitment" -const.ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_NUMBER_COMMITMENT="the provided global inputs do not match the block number commitment" +const ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_NUMBER_COMMITMENT="the provided global inputs do not match the block number commitment" -const.ERR_PROLOGUE_NATIVE_ASSET_ID_IS_NOT_FUNGIBLE="native asset account ID in reference block is not of type fungible faucet" +const ERR_PROLOGUE_NATIVE_ASSET_ID_IS_NOT_FUNGIBLE="native asset account ID in reference block is not of type fungible faucet" -const.ERR_PROLOGUE_VERIFICATION_BASE_FEE_MUST_BE_U32="verification base fee must fit into a u32" +const ERR_PROLOGUE_VERIFICATION_BASE_FEE_MUST_BE_U32="verification base fee must fit into a u32" -const.ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY="new account must have an empty vault" +const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY="new account must have an empty vault" -const.ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY="reserved slot for new fungible faucet is not empty" +const ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY="reserved slot for new fungible faucet is not empty" -const.ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE="reserved slot for new fungible faucet has an invalid type" +const ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE="reserved slot for new fungible faucet has an invalid type" -const.ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPTY_SMT="reserved slot for non-fungible faucet is not a valid empty SMT" +const ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPTY_SMT="reserved slot for non-fungible faucet is not a valid empty SMT" -const.ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE="reserved slot for new non-fungible faucet has an invalid type" +const ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE="reserved slot for new non-fungible faucet has an invalid type" -const.ERR_PROLOGUE_PROVIDED_ACCOUNT_DATA_DOES_NOT_MATCH_ON_CHAIN_COMMITMENT="account data provided does not match the commitment recorded on-chain" +const ERR_PROLOGUE_PROVIDED_ACCOUNT_DATA_DOES_NOT_MATCH_ON_CHAIN_COMMITMENT="account data provided does not match the commitment recorded on-chain" -const.ERR_PROLOGUE_EXISTING_ACCOUNT_MUST_HAVE_NON_ZERO_NONCE="existing accounts must have a non-zero nonce" +const ERR_PROLOGUE_EXISTING_ACCOUNT_MUST_HAVE_NON_ZERO_NONCE="existing accounts must have a non-zero nonce" -const.ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER="account IDs provided via global inputs and advice provider do not match" +const ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER="account IDs provided via global inputs and advice provider do not match" -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_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 256" -const.ERR_PROLOGUE_PROVIDED_INPUT_ASSETS_INFO_DOES_NOT_MATCH_ITS_COMMITMENT="provided info about assets of an input does not match its commitment" +const ERR_PROLOGUE_PROVIDED_INPUT_ASSETS_INFO_DOES_NOT_MATCH_ITS_COMMITMENT="provided info about assets of an input does not match its commitment" -const.ERR_PROLOGUE_NUMBER_OF_INPUT_NOTES_EXCEEDS_LIMIT="number of input notes exceeds the kernel's maximum limit of 1024" +const ERR_PROLOGUE_NUMBER_OF_INPUT_NOTES_EXCEEDS_LIMIT="number of input notes exceeds the kernel's maximum limit of 1024" -const.ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH="note commitment computed from the input note data does not match given note commitment" +const ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH="note commitment computed from the input note data does not match given note commitment" -const.ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO="new account must have a zero nonce" +const ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO="new account must have a zero nonce" -const.ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT="number of note inputs exceeded the maximum limit of 128" +const ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT="number of note inputs exceeded the maximum limit of 1024" -const.ERR_PROLOGUE_NOTE_AUTHENTICATION_FAILED="failed to authenticate note inclusion in block" +const ERR_PROLOGUE_NOTE_AUTHENTICATION_FAILED="failed to authenticate note inclusion in block" -const.ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH="sequential hash over kernel procedures does not match kernel commitment from block" +const ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH="sequential hash over kernel procedures does not match kernel commitment from block" # PUBLIC INPUTS # ================================================================================================= @@ -89,7 +95,7 @@ const.ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH="sequential hash over ke #! accounts. #! - INPUT_NOTES_COMMITMENT is the commitment to the input notes. See the #! `tx_get_input_notes_commitment` kernel procedure for details. -proc.process_global_inputs +proc process_global_inputs exec.memory::set_block_commitment dropw exec.memory::set_init_account_commitment dropw exec.memory::set_nullifier_commitment dropw @@ -114,7 +120,7 @@ end #! Where: #! - TX_KERNEL_COMMITMENT is the sequential hash of the kernel procedures. #! - [KERNEL_PROCEDURE_ROOTS] is the array of the kernel procedure roots. -proc.process_kernel_data +proc process_kernel_data # load the transaction kernel commitment from memory exec.memory::get_tx_kernel_commitment # OS => [TX_KERNEL_COMMITMENT] @@ -147,7 +153,7 @@ proc.process_kernel_data # AS => [] # extract the resulting hash - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # OS => [SEQ_KERNEL_PROC_HASH, kernel_procs_ptr', TX_KERNEL_COMMITMENT] # AS => [] @@ -171,7 +177,7 @@ end #! NULLIFIER_ROOT, #! TX_COMMITMENT, #! TX_KERNEL_COMMITMENT -#! PROOF_COMMITMENT, +#! VALIDATOR_KEY_COMMITMENT, #! [block_num, version, timestamp, 0], #! [native_asset_id_suffix, native_asset_id_prefix, verification_base_fee, 0], #! [0, 0, 0, 0], @@ -188,7 +194,7 @@ end #! consumed. #! - TX_COMMITMENT is a commitment to the set of transaction IDs which affected accounts in the block. #! - TX_KERNEL_COMMITMENT is the sequential hash of the kernel procedures. -#! - PROOF_COMMITMENT is the commitment of the block's STARK proof attesting to the correct state transition. +#! - VALIDATOR_KEY_COMMITMENT is the commitment to the validator's public key. #! - block_num is the reference block number. #! - version is the current protocol version. #! - timestamp is the current timestamp. @@ -197,19 +203,19 @@ end #! - verification_base_fee is the base fee capturing the cost for the verification of a #! transaction. #! - NOTE_ROOT is the root of the tree with all notes created in the block. -proc.process_block_data +proc process_block_data exec.memory::get_block_data_ptr # => [block_data_ptr, block_num] # read block data and compute its sub commitment # see `Advice stack` above for details. padw padw padw - adv_pipe hperm - adv_pipe hperm - adv_pipe hperm - adv_pipe hperm - adv_pipe hperm - exec.rpo::squeeze_digest + adv_pipe exec.rpo256::permute + adv_pipe exec.rpo256::permute + adv_pipe exec.rpo256::permute + adv_pipe exec.rpo256::permute + adv_pipe exec.rpo256::permute + exec.rpo256::squeeze_digest # => [SUB_COMMITMENT, block_data_ptr', block_num] # store the note root in memory @@ -218,7 +224,7 @@ proc.process_block_data # => [NOTE_ROOT, SUB_COMMITMENT, block_data_ptr', block_num] # merge the note root with the sub commitment to get the block commitment - hmerge + exec.rpo256::merge # => [BLOCK_COMMITMENT, block_data_ptr', block_num] # assert that the block commitment matches the commitment in global inputs @@ -258,7 +264,7 @@ end #! - PARTIAL_BLOCKCHAIN_COMMITMENT is the sequential hash of the padded MMR peaks. #! - num_blocks is the number of blocks in the MMR. #! - PEAK_1 .. PEAK_N are the MMR peaks. -proc.process_chain_data +proc process_chain_data exec.memory::get_partial_blockchain_ptr dup # => [partial_blockchain_ptr, partial_blockchain_ptr] @@ -276,6 +282,10 @@ end # ACCOUNT DATA # ================================================================================================= +#! This should only be necessary whenever the slots of an account "change", which is essentially +#! the case when it is created, or when slots are added or removed (currently unimplemented). +#! So, it should be sufficient to do this here instead of in save_account_storage_data. +#! #! Validates that the account the transaction is being executed against satisfies the criteria #! for a new account. #! @@ -287,7 +297,7 @@ end #! #! Inputs: [] #! Outputs: [] -proc.validate_new_account +proc validate_new_account # Assert the account ID of the account is valid exec.memory::get_account_id exec.account_id::validate # => [] @@ -303,7 +313,7 @@ proc.validate_new_account # => [ACCT_VAULT_ROOT] # push empty vault root onto stack - exec.constants::get_empty_smt_root + push.EMPTY_SMT_ROOT # => [EMPTY_VAULT_ROOT, ACCT_VAULT_ROOT] assert_eqw.err=ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY @@ -318,40 +328,32 @@ proc.validate_new_account # process conditional logic depending on whether the account is a faucet if.true # get the faucet reserved slot - exec.account::get_faucet_storage_data_slot exec.account::get_item - # => [FAUCET_RESERVED_SLOT, acct_id_prefix] + exec.account::get_faucet_sysdata_slot_id exec.account::get_typed_item + # => [FAUCET_RESERVED_SLOT, slot_type, acct_id_prefix] # check if the account is a fungible faucet - movup.4 exec.account_id::is_fungible_faucet - # => [is_fungible_faucet, FAUCET_RESERVED_SLOT] + movup.5 exec.account_id::is_fungible_faucet + # => [is_fungible_faucet, FAUCET_RESERVED_SLOT, slot_type] if.true # assert the fungible faucet reserved slot is initialized correctly (EMPTY_WORD) exec.word::eqz not assertz.err=ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY - # => [] - - # get the faucet reserved storage data slot type - exec.account::get_faucet_storage_data_slot exec.account::get_storage_slot_type # => [slot_type] # assert the fungible faucet reserved slot type == value - exec.constants::get_storage_slot_type_value eq + push.STORAGE_SLOT_TYPE_VALUE eq assert.err=ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE # => [] else # assert the non-fungible faucet reserved slot is initialized correctly (root of # empty SMT) - exec.constants::get_empty_smt_root + push.EMPTY_SMT_ROOT assert_eqw.err=ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPTY_SMT - # => [] - - # get the faucet reserved storage data slot type - exec.account::get_faucet_storage_data_slot exec.account::get_storage_slot_type # => [slot_type] # assert the non-fungible faucet reserved slot type == map - exec.constants::get_storage_slot_type_map eq + push.STORAGE_SLOT_TYPE_MAP eq assert.err=ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE # => [] end @@ -366,9 +368,9 @@ proc.validate_new_account exec.account::validate_seed # => [] - # Assert the provided procedures offsets and sizes satisfy storage requirements + # Assert the storage slots are sorted and unique. # --------------------------------------------------------------------------------------------- - exec.account::validate_procedure_metadata + exec.account::validate_storage # => [] end @@ -401,7 +403,7 @@ end #! - ACCOUNT_VAULT_ROOT is the account's vault root. #! - ACCOUNT_STORAGE_COMMITMENT is the account's storage commitment. #! - ACCOUNT_CODE_COMMITMENT is the account's code commitment. -proc.process_account_data +proc process_account_data # Initialize the active account data pointer in the bookkeeping section with the native offset # (2048) exec.memory::set_active_account_data_ptr_to_native_account @@ -414,9 +416,9 @@ proc.process_account_data # read account details and compute its digest. See `Advice stack` above for details. padw padw padw - adv_pipe hperm - adv_pipe hperm - exec.rpo::squeeze_digest + adv_pipe exec.rpo256::permute + adv_pipe exec.rpo256::permute + exec.rpo256::squeeze_digest # => [ACCOUNT_COMMITMENT, acct_data_ptr'] movup.4 drop @@ -517,12 +519,13 @@ end #! Advice stack: [] #! #! Where: -#! - NOTE_COMMITMENT is the input note's commitment computed as `hash(NOTE_ID || NOTE_METADATA)`. +#! - NOTE_COMMITMENT is the input note's commitment computed as `hash(NOTE_ID || NOTE_METADATA_COMMITMENT)`. #! - block_num is the leaf position in the MMR chain of the block which created the input note. #! - BLOCK_SUB_COMMITMENT is the sub_commitment of the block which created the input note. #! - NOTE_ROOT is the merkle root of the notes tree containing the input note. #! - note_index is the input note's position in the notes tree. -proc.authenticate_note.8 +@locals(8) +proc authenticate_note # Load the BLOCK_COMMITMENT from the PARTIAL_BLOCKCHAIN # --------------------------------------------------------------------------------------------- @@ -540,7 +543,7 @@ proc.authenticate_note.8 # read data from advice and compute hash(BLOCK_SUB_COMMITMENT || NOTE_ROOT) padw padw padw - adv_pipe hperm + adv_pipe exec.rpo256::permute # => [PERM, COMPUTED_BLOCK_COMMITMENT, PERM, mem_ptr', BLOCK_COMMITMENT, NOTE_COMMITMENT] dropw @@ -563,7 +566,7 @@ proc.authenticate_note.8 # => [NOTE_COMMITMENT, note_index, NOTE_ROOT] # get the depth of the note tree - exec.constants::get_note_tree_depth movdn.4 + push.NOTE_TREE_DEPTH movdn.4 # => [NOTE_COMMITMENT, depth, note_index, NOTE_ROOT] # verify the note commitment @@ -596,48 +599,54 @@ end #! - ASSETS_COMMITMENT is the sequential hash of the padded note's assets. #! - NULLIFIER is the result of #! `hash(SERIAL_NUMBER || SCRIPT_ROOT || INPUTS_COMMITMENT || ASSETS_COMMITMENT)`. -proc.process_input_note_details +proc process_input_note_details exec.memory::get_input_note_core_ptr # => [note_data_ptr] # read input note's data and compute its digest. See `Advice stack` above for details. padw padw padw - adv_pipe hperm - adv_pipe hperm - exec.rpo::squeeze_digest + adv_pipe exec.rpo256::permute + adv_pipe exec.rpo256::permute + exec.rpo256::squeeze_digest # => [NULLIFIER, note_data_ptr + 16] movup.4 drop # => [NULLIFIER] end -#! Copies the note's metadata and args from the advice stack to memory. +#! Copies the note's metadata header and attachment as well as note args from the advice stack to +#! memory. #! #! Notes: #! - The note's ARGS are not authenticated, these are optional arguments the user can provide when #! consuming the note. #! - The note's metadata is authenticated, so the data is returned in the stack. The value is used -#! to compute the NOTE_COMMITMENT as `hash(NOTE_ID || NOTE_METADATA)`, which is the leaf value of the -#! note's tree in the contained in the block header. The NOTE_COMMITMENT is either verified by this -#! kernel, or delayed to be verified by another kernel (e.g. block or batch kernels). +#! to compute the NOTE_COMMITMENT as `hash(NOTE_ID || NOTE_METADATA_COMMITMENT)`, where +#! `NOTE_METADATA_COMMITMENT` is `hash(NOTE_METADATA_HEADER || NOTE_METADATA_ATTACHMENT)`. The +#! NOTE_COMMITMENT is the leaf value of the block note tree contained in the block header and is +#! either verified by this kernel, or delayed to be verified by another kernel (e.g. batch or +#! block kernels). #! #! Inputs: #! Operand stack: [note_ptr] -#! Advice stack: [NOTE_ARGS, NOTE_METADATA] +#! Advice stack: [NOTE_ARGS, NOTE_METADATA_HEADER, NOTE_ATTACHMENT] #! Outputs: -#! Operand stack: [NOTE_METADATA] +#! Operand stack: [NOTE_ATTACHMENT, NOTE_METADATA_HEADER] #! Advice stack: [] #! #! Where: #! - note_ptr is the memory location for the input note. #! - NOTE_ARGS are the user arguments passed to the note. -#! - NOTE_METADATA is the note's metadata. -proc.process_note_args_and_metadata +#! - NOTE_METADATA_HEADER is the note's metadata. +proc process_note_args_and_metadata padw adv_loadw dup.4 exec.memory::set_input_note_args dropw # => [note_ptr] - padw adv_loadw movup.4 exec.memory::set_input_note_metadata - # => [NOTE_METADATA] + padw adv_loadw dup.4 exec.memory::set_input_note_metadata_header + # => [NOTE_METADATA_HEADER, note_ptr] + + padw adv_loadw movup.8 exec.memory::set_input_note_attachment + # => [NOTE_ATTACHMENT, NOTE_METADATA_HEADER] end #! Checks that the number of note inputs is within limit and stores it to memory. @@ -652,13 +661,13 @@ end #! Where: #! - note_ptr is the memory location for the input note. #! - inputs_len is the note's input count. -proc.process_note_inputs_length +proc process_note_inputs_length # move the inputs length from the advice stack to the operand stack adv_push.1 # => [inputs_len, note_ptr] # validate the input length - dup exec.::$kernel::util::note::get_max_inputs_per_note lte + dup push.MAX_INPUTS_PER_NOTE lte assert.err=ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT # => [inputs_len, note_ptr] @@ -680,14 +689,14 @@ end #! - note_ptr is the memory location for the input note. #! - assets_count is the note's assets count. #! - ASSET_0, ..., ASSET_N are the padded note's assets. -proc.process_note_assets +proc process_note_assets # verify and save the assets count # --------------------------------------------------------------------------------------------- adv_push.1 # => [assets_count, note_ptr] - dup exec.constants::get_max_assets_per_note lte + dup push.MAX_ASSETS_PER_NOTE lte assert.err=ERR_PROLOGUE_NUMBER_OF_NOTE_ASSETS_EXCEEDS_LIMIT # => [assets_count, note_ptr] @@ -726,7 +735,7 @@ proc.process_note_assets # loop and read assets from the advice provider while.true # read data and compute its digest. See `Advice stack` above for details. - adv_pipe hperm + adv_pipe exec.rpo256::permute # => [PERM, PERM, PERM, assets_ptr+8, note_ptr, counter, rounded_num_assets] # update counter @@ -739,7 +748,7 @@ proc.process_note_assets end # => [PERM, PERM, PERM, assets_ptr+8n, note_ptr, counter+2n, rounded_num_assets] - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [ASSET_COMMITMENT_COMPUTED, assets_ptr+8n, note_ptr, counter+2n, rounded_num_assets] swapw drop movdn.2 drop drop @@ -758,7 +767,7 @@ end #! #! Where: #! - note_ptr is the memory location for the input note. -proc.add_input_note_assets_to_vault +proc add_input_note_assets_to_vault # prepare the stack # --------------------------------------------------------------------------------------------- @@ -785,16 +794,8 @@ proc.add_input_note_assets_to_vault padw dup.5 mem_loadw_be # => [ASSET, input_vault_root_ptr, assets_start_ptr, assets_end_ptr, input_vault_root_ptr] - # TODO: Because the input vault is a copy of the account vault, to mutate the input vault, - # asset witnesses for the account vault must be requested. - # This is a temporary solution. We should avoid loading assets one by one here and instead - # pre-load all relevant merkle paths for the note assets before tx execution. - # - # This emitted event is equivalent to ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT. - - emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT - # => [ASSET, input_vault_root_ptr, assets_start_ptr, assets_end_ptr, input_vault_root_ptr] - + # the witnesses for the note assets should be added prior to transaction execution and so + # there should be no need to fetch them lazily via an event. exec.asset_vault::add_asset dropw # => [assets_start_ptr, assets_end_ptr, input_vault_root_ptr] @@ -818,17 +819,17 @@ end #! Where: #! - note_ptr is the memory location for the input note. #! - NOTE_ID is the note's id, i.e. `hash(RECIPIENT || ASSET_COMMITMENT)`. -proc.compute_input_note_id +proc compute_input_note_id # compute SERIAL_COMMITMENT: hash(SERIAL_NUMBER || EMPTY_WORD) - dup exec.memory::get_input_note_serial_num padw hmerge + dup exec.memory::get_input_note_serial_num padw exec.rpo256::merge # => [SERIAL_COMMITMENT, note_ptr] # compute MERGE_SCRIPT: hash(SERIAL_COMMITMENT || SCRIPT_ROOT) - dup.4 exec.memory::get_input_note_script_root hmerge + dup.4 exec.memory::get_input_note_script_root exec.rpo256::merge # => [MERGE_SCRIPT, note_ptr] # compute RECIPIENT: hash(MERGE_SCRIPT || INPUT_COMMITMENT) - dup.4 exec.memory::get_input_note_inputs_commitment hmerge + dup.4 exec.memory::get_input_note_inputs_commitment exec.rpo256::merge # => [RECIPIENT, note_ptr] # store the recipient in memory @@ -836,7 +837,7 @@ proc.compute_input_note_id # => [RECIPIENT, note_ptr] # compute NOTE_ID: hash(RECIPIENT || ASSET_COMMITMENT) - movup.4 exec.memory::get_input_note_assets_commitment hmerge + movup.4 exec.memory::get_input_note_assets_commitment exec.rpo256::merge # => [NOTE_ID] end @@ -854,8 +855,9 @@ end #! SCRIPT_ROOT, #! INPUTS_COMMITMENT, #! ASSETS_COMMITMENT, -#! ARGS, -#! NOTE_METADATA, +#! NOTE_ARGS, +#! NOTE_METADATA_HEADER, +#! NOTE_ATTACHMENT, #! assets_count, #! ASSET_0, ..., ASSET_N, #! is_authenticated, @@ -877,8 +879,9 @@ end #! - SCRIPT_ROOT is the note's script root. #! - INPUTS_COMMITMENT is the sequential hash of the padded note's inputs. #! - ASSETS_COMMITMENT is the sequential hash of the padded note's assets. -#! - NOTE_METADATA is the note's metadata. -#! - ARGS is the user arguments passed to the note. +#! - NOTE_METADATA_HEADER is the note's metadata header. +#! - NOTE_ATTACHMENT is the note's attachment. +#! - NOTE_ARGS are the user arguments passed to the note. #! - assets_count is the note's assets count. #! - ASSET_0, ..., ASSET_N are the padded note's assets. #! - is_authenticated is the boolean indicating if the note contains an authentication proof. @@ -886,7 +889,7 @@ end #! - block_num is the note's creation block number. #! - BLOCK_SUB_COMMITMENT is the block's sub_commitment for which the note was created. #! - NOTE_ROOT is the merkle root of the note's tree. -proc.process_input_note +proc process_input_note # note details # --------------------------------------------------------------------------------------------- @@ -907,39 +910,43 @@ proc.process_input_note # => [note_ptr, NULLIFIER, HASHER_CAPACITY] dup exec.process_note_args_and_metadata - # => [NOTE_METADATA, note_ptr, NULLIFIER, HASHER_CAPACITY] + # => [NOTE_ATTACHMENT, NOTE_METADATA_HEADER, note_ptr, NULLIFIER, HASHER_CAPACITY] + + # compute hash(NOTE_METADATA_HEADER || NOTE_ATTACHMENT) + exec.rpo256::merge + # => [NOTE_METADATA_COMMITMENT, note_ptr, NULLIFIER, HASHER_CAPACITY] movup.4 - # => [note_ptr, NOTE_METADATA, NULLIFIER, HASHER_CAPACITY] + # => [note_ptr, NOTE_METADATA_COMMITMENT, NULLIFIER, HASHER_CAPACITY] # note inputs len # --------------------------------------------------------------------------------------------- exec.process_note_inputs_length - # => [note_ptr, NOTE_METADATA, NULLIFIER, HASHER_CAPACITY] + # => [note_ptr, NOTE_METADATA_COMMITMENT, NULLIFIER, HASHER_CAPACITY] # note assets # --------------------------------------------------------------------------------------------- dup exec.process_note_assets dup exec.add_input_note_assets_to_vault - # => [note_ptr, NOTE_METADATA, NULLIFIER, HASHER_CAPACITY] + # => [note_ptr, NOTE_METADATA_COMMITMENT, NULLIFIER, HASHER_CAPACITY] # note id # --------------------------------------------------------------------------------------------- dup exec.compute_input_note_id - # => [NOTE_ID, note_ptr, NOTE_METADATA, NULLIFIER, HASHER_CAPACITY] + # => [NOTE_ID, note_ptr, NOTE_METADATA_COMMITMENT, NULLIFIER, HASHER_CAPACITY] # save note id to memory movup.4 exec.memory::set_input_note_id - # => [NOTE_ID, NOTE_METADATA, NULLIFIER, HASHER_CAPACITY] + # => [NOTE_ID, NOTE_METADATA_COMMITMENT, NULLIFIER, HASHER_CAPACITY] # note authentication # --------------------------------------------------------------------------------------------- - # NOTE_COMMITMENT: `hash(NOTE_ID || NOTE_METADATA)` - swapw hmerge + # NOTE_COMMITMENT: `hash(NOTE_ID || NOTE_METADATA_COMMITMENT)` + swapw exec.rpo256::merge # => [NOTE_COMMITMENT, NULLIFIER, HASHER_CAPACITY] adv_push.1 @@ -957,7 +964,7 @@ proc.process_input_note # => [EMPTY_WORD_OR_NOTE_COMMITMENT, NULLIFIER, HASHER_CAPACITY] # update the input note commitment - hperm + exec.rpo256::permute # => [PERM, PERM, PERM] end @@ -981,7 +988,7 @@ end #! - num_notes is the number of input notes. #! - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`. #! - NOTE_DATA is the input notes' details, for format see `prologue::process_input_note`. -proc.process_input_notes_data +proc process_input_notes_data # get the number of input notes from the advice stack adv_push.1 # => [num_notes] @@ -989,7 +996,7 @@ proc.process_input_notes_data # assert the number of input notes is within limits; since max number of input notes is # expected to be smaller than 2^32, we can use a more efficient u32 comparison dup - exec.constants::get_max_num_input_notes + push.MAX_INPUT_NOTES_PER_TX u32assert2.err=ERR_PROLOGUE_NUMBER_OF_INPUT_NOTES_EXCEEDS_LIMIT u32lte assert.err=ERR_PROLOGUE_NUMBER_OF_INPUT_NOTES_EXCEEDS_LIMIT # => [num_notes] @@ -1048,7 +1055,7 @@ proc.process_input_notes_data # => [has_more_notes, PERM, PERM, PERM, idx+1, num_notes] end - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [INPUT_NOTES_COMMITMENT, idx+1, num_notes] # assert the input notes and the commitment matches @@ -1082,7 +1089,7 @@ end #! - TX_SCRIPT_ROOT is the transaction's script root. #! - TX_SCRIPT_ARGS is the word of values which could be used directly or could be used to obtain #! some values associated with it from the advice map. -proc.process_tx_script_data +proc process_tx_script_data # read the transaction script root from the advice stack padw adv_loadw # => [TX_SCRIPT_ROOT] @@ -1113,7 +1120,7 @@ end #! #! Where: #! - AUTH_ARGS is the argument passed to the auth procedure. -proc.process_auth_procedure_data +proc process_auth_procedure_data # read the auth procedure args from the advice stack padw adv_loadw # => [AUTH_ARGS] @@ -1146,7 +1153,7 @@ end #! NULLIFIER_ROOT, #! TX_COMMITMENT, #! TX_KERNEL_COMMITMENT -#! PROOF_COMMITMENT, +#! VALIDATOR_KEY_COMMITMENT, #! [block_num, version, timestamp, 0], #! NOTE_ROOT, #! kernel_version @@ -1185,7 +1192,7 @@ end #! - NULLIFIER_ROOT is the root of the tree with nullifiers of all notes that have ever been #! consumed. #! - TX_COMMITMENT is a commitment to the set of transaction IDs which affected accounts in the block. -#! - PROOF_COMMITMENT is the commitment of the block's STARK proof attesting to the correct state transition. +#! - VALIDATOR_KEY_COMMITMENT is the commitment to the validator's public key. #! - block_num is the reference block number. #! - version is the current protocol version. #! - timestamp is the current timestamp. @@ -1208,7 +1215,7 @@ end #! - data provided by the advice provider does not match global inputs. #! - the account data is invalid. #! - any of the input notes do note exist in the note db. -export.prepare_transaction +pub proc prepare_transaction exec.process_global_inputs # => [block_num] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/tx.masm b/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm similarity index 86% rename from crates/miden-lib/asm/kernels/transaction/lib/tx.masm rename to crates/miden-protocol/asm/kernels/transaction/lib/tx.masm index e7e137c885..df4c0e9d42 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/tx.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm @@ -1,19 +1,19 @@ -use.$kernel::memory -use.$kernel::note +use $kernel::memory +use $kernel::note # CONSTANTS # ================================================================================================= # Max value for U16, used as the upper limit for expiration block delta -const.EXPIRY_UPPER_LIMIT=0xFFFF+1 +const EXPIRY_UPPER_LIMIT=0xFFFF+1 # Max U32 value, used for initializing the expiration block number -const.MAX_BLOCK_NUM=0xFFFFFFFF +const MAX_BLOCK_NUM=0xFFFFFFFF # ERRORS # ================================================================================================= -const.ERR_TX_INVALID_EXPIRATION_DELTA="transaction expiration block delta must be within 0x1 and 0xFFFF" +const ERR_TX_INVALID_EXPIRATION_DELTA="transaction expiration block delta must be within 0x1 and 0xFFFF" # PROCEDURES # ================================================================================================= @@ -25,7 +25,7 @@ const.ERR_TX_INVALID_EXPIRATION_DELTA="transaction expiration block delta must b #! #! Where: #! - BLOCK_COMMITMENT is the commitment of the transaction reference block. -export.memory::get_block_commitment +pub use memory::get_block_commitment #! Returns the block number of the transaction reference block. #! @@ -34,7 +34,7 @@ export.memory::get_block_commitment #! #! Where: #! - num is the transaction reference block number. -export.memory::get_blk_num->get_block_number +pub use memory::get_blk_num->get_block_number #! Returns the block timestamp of the reference block for this transaction. #! @@ -43,7 +43,7 @@ export.memory::get_blk_num->get_block_number #! #! Where: #! - timestamp is the timestamp of the reference block for this transaction. -export.memory::get_blk_timestamp->get_block_timestamp +pub use memory::get_blk_timestamp->get_block_timestamp #! Returns the input notes commitment hash. #! @@ -54,7 +54,7 @@ export.memory::get_blk_timestamp->get_block_timestamp #! #! Where: #! - INPUT_NOTES_COMMITMENT is the input notes commitment hash. -export.memory::get_input_notes_commitment +pub use memory::get_input_notes_commitment #! Returns the output notes commitment hash. This is computed as a sequential hash of #! (note_id, note_metadata) tuples over all output notes. @@ -64,7 +64,7 @@ export.memory::get_input_notes_commitment #! #! Where: #! - OUTPUT_NOTES_COMMITMENT is the output notes commitment. -export.note::compute_output_notes_commitment->get_output_notes_commitment +pub use note::compute_output_notes_commitment->get_output_notes_commitment #! Returns the total number of input notes consumed by this transaction. #! @@ -73,7 +73,7 @@ export.note::compute_output_notes_commitment->get_output_notes_commitment #! #! Where: #! - num_input_notes is the total number of input notes consumed by this transaction. -export.memory::get_num_input_notes +pub use memory::get_num_input_notes #! Returns the current number of output notes created in this transaction. #! @@ -82,7 +82,7 @@ export.memory::get_num_input_notes #! #! Where: #! - num_output_notes is the number of output notes created in this transaction so far. -export.memory::get_num_output_notes +pub use memory::get_num_output_notes #! Updates the transaction expiration block delta. #! @@ -95,7 +95,7 @@ export.memory::get_num_output_notes #! #! Where: #! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). -export.update_expiration_block_delta +pub proc update_expiration_block_delta # Ensure block_height_delta is between 1 and 0xFFFF (inclusive) dup neq.0 assert.err=ERR_TX_INVALID_EXPIRATION_DELTA # => [block_height_delta] @@ -130,7 +130,7 @@ end #! #! Where: #! - block_height_delta is the stored expiration time delta (1 to 0xFFFF). -export.get_expiration_delta +pub proc get_expiration_delta exec.memory::get_expiration_block_num # => [stored_expiration_block_num] diff --git a/crates/miden-lib/asm/kernels/transaction/main.masm b/crates/miden-protocol/asm/kernels/transaction/main.masm similarity index 88% rename from crates/miden-lib/asm/kernels/transaction/main.masm rename to crates/miden-protocol/asm/kernels/transaction/main.masm index 959f882d83..61a0763a06 100644 --- a/crates/miden-lib/asm/kernels/transaction/main.masm +++ b/crates/miden-protocol/asm/kernels/transaction/main.masm @@ -1,37 +1,37 @@ -use.std::word +use miden::core::word -use.$kernel::epilogue -use.$kernel::memory -use.$kernel::note -use.$kernel::prologue +use $kernel::epilogue +use $kernel::memory +use $kernel::note +use $kernel::prologue # EVENTS # ================================================================================================= # Event emitted to signal that an execution of the transaction prologue has started. -const.PROLOGUE_START_EVENT=event("miden::tx::prologue_start") +const PROLOGUE_START_EVENT=event("miden::tx::prologue_start") # Event emitted to signal that an execution of the transaction prologue has ended. -const.PROLOGUE_END_EVENT=event("miden::tx::prologue_end") +const PROLOGUE_END_EVENT=event("miden::tx::prologue_end") # Event emitted to signal that the notes processing has started. -const.NOTES_PROCESSING_START_EVENT=event("miden::tx::notes_processing_start") +const NOTES_PROCESSING_START_EVENT=event("miden::tx::notes_processing_start") # Event emitted to signal that the notes processing has ended. -const.NOTES_PROCESSING_END_EVENT=event("miden::tx::notes_processing_end") +const NOTES_PROCESSING_END_EVENT=event("miden::tx::notes_processing_end") # Event emitted to signal that the note consuming has started. -const.NOTE_EXECUTION_START_EVENT=event("miden::tx::note_execution_start") +const NOTE_EXECUTION_START_EVENT=event("miden::tx::note_execution_start") # Event emitted to signal that the note consuming has ended. -const.NOTE_EXECUTION_END_EVENT=event("miden::tx::note_execution_end") +const NOTE_EXECUTION_END_EVENT=event("miden::tx::note_execution_end") # Event emitted to signal that the transaction script processing has started. -const.TX_SCRIPT_PROCESSING_START_EVENT=event("miden::tx::tx_script_processing_start") +const TX_SCRIPT_PROCESSING_START_EVENT=event("miden::tx::tx_script_processing_start") # Event emitted to signal that the transaction script processing has ended. -const.TX_SCRIPT_PROCESSING_END_EVENT=event("miden::tx::tx_script_processing_end") +const TX_SCRIPT_PROCESSING_END_EVENT=event("miden::tx::tx_script_processing_end") # Event emitted to signal that an execution of the transaction epilogue has started. -const.EPILOGUE_START_EVENT=event("miden::tx::epilogue_start") +const EPILOGUE_START_EVENT=event("miden::tx::epilogue_start") # Event emitted to signal that an execution of the transaction epilogue has ended. -const.EPILOGUE_END_EVENT=event("miden::tx::epilogue_end") +const EPILOGUE_END_EVENT=event("miden::tx::epilogue_end") # MAIN # ================================================================================================= @@ -66,7 +66,8 @@ const.EPILOGUE_END_EVENT=event("miden::tx::epilogue_end") #! - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account #! delta commitment. #! - FEE_ASSET is the fungible asset used as the transaction fee. -proc.main.1 +@locals(1) +proc main # Prologue # --------------------------------------------------------------------------------------------- diff --git a/crates/miden-lib/asm/kernels/transaction/tx_script_main.masm b/crates/miden-protocol/asm/kernels/transaction/tx_script_main.masm similarity index 93% rename from crates/miden-lib/asm/kernels/transaction/tx_script_main.masm rename to crates/miden-protocol/asm/kernels/transaction/tx_script_main.masm index 79c99f8549..b0b12ea8cf 100644 --- a/crates/miden-lib/asm/kernels/transaction/tx_script_main.masm +++ b/crates/miden-protocol/asm/kernels/transaction/tx_script_main.masm @@ -1,12 +1,12 @@ -use.std::word +use miden::core::word -use.$kernel::memory -use.$kernel::prologue +use $kernel::memory +use $kernel::prologue # ERRORS # ================================================================================================= -const.ERR_TX_TRANSACTION_SCRIPT_IS_MISSING="the transaction script is missing" +const ERR_TX_TRANSACTION_SCRIPT_IS_MISSING="the transaction script is missing" # MAIN # ================================================================================================= @@ -34,7 +34,7 @@ const.ERR_TX_TRANSACTION_SCRIPT_IS_MISSING="the transaction script is missing" #! - account_id is the account that the transaction is being executed against. #! - INITIAL_ACCOUNT_COMMITMENT is the account state prior to the transaction, EMPTY_WORD for new accounts. #! - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`. -proc.main +proc main # Prologue # --------------------------------------------------------------------------------------------- diff --git a/crates/miden-lib/asm/miden/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm similarity index 85% rename from crates/miden-lib/asm/miden/active_account.masm rename to crates/miden-protocol/asm/protocol/active_account.masm index 576e6b672c..9f69e30cc1 100644 --- a/crates/miden-lib/asm/miden/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -1,4 +1,4 @@ -use.miden::kernel_proc_offsets +use miden::protocol::kernel_proc_offsets # ACTIVE ACCOUNT PROCEDURES # ================================================================================================= @@ -15,7 +15,7 @@ use.miden::kernel_proc_offsets #! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. #! #! Invocation: exec -export.get_id +pub proc get_id # pad the stack padw padw padw push.0.0 # => [pad(14)] @@ -48,7 +48,7 @@ end #! - nonce is the active account's nonce. #! #! Invocation: exec -export.get_nonce +pub proc get_nonce # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -76,7 +76,7 @@ end #! - INIT_COMMITMENT is the initial account commitment. #! #! Invocation: exec -export.get_initial_commitment +pub proc get_initial_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -101,7 +101,7 @@ end #! - ACCOUNT_COMMITMENT is the commitment of the account data. #! #! Invocation: exec -export.compute_commitment +pub proc compute_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -139,7 +139,7 @@ end #! - CODE_COMMITMENT is the commitment of the account code. #! #! Invocation: exec -export.get_code_commitment +pub proc get_code_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -169,7 +169,7 @@ end #! - INIT_STORAGE_COMMITMENT is the initial account storage commitment. #! #! Invocation: exec -export.get_initial_storage_commitment +pub proc get_initial_storage_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -203,7 +203,7 @@ end #! - STORAGE_COMMITMENT is the commitment of the account storage. #! #! Invocation: exec -export.compute_storage_commitment +pub proc compute_storage_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -228,7 +228,7 @@ end #! - INIT_VAULT_ROOT is the initial account vault root. #! #! Invocation: exec -export.get_initial_vault_root +pub proc get_initial_vault_root # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -253,7 +253,7 @@ end #! - VAULT_ROOT is the root of the account vault. #! #! Invocation: exec -export.get_vault_root +pub proc get_vault_root # pad the stack for syscall invocation padw padw padw push.0.0.0 # => [pad(15)] @@ -272,29 +272,30 @@ end # STORAGE # ------------------------------------------------------------------------------------------------- -#! Gets an item from the active account storage. Panics if the index is out of bounds. +#! Gets an item from the active account storage. #! -#! Inputs: [index] +#! Inputs: [slot_id_prefix, slot_id_suffix] #! Outputs: [VALUE] #! #! Where: -#! - index is the index of the item to get. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value of the item. #! #! Panics if: -#! - the index of the requested item is out of bounds. +#! - a slot with the provided slot ID does not exist in account storage. #! #! Invocation: exec -export.get_item - push.0.0 movup.2 - # => [index, 0, 0] +pub proc get_item + push.0 movdn.2 + # => [slot_id_prefix, slot_id_suffix, 0] exec.kernel_proc_offsets::account_get_item_offset - # => [offset, index, 0, 0] + # => [offset, slot_id_prefix, slot_id_suffix, 0] # pad the stack padw swapw padw padw swapdw - # => [offset, index, pad(14)] + # => [offset, slot_id_prefix, slot_id_suffix, pad(13)] syscall.exec_kernel_proc # => [VALUE, pad(12)] @@ -307,27 +308,28 @@ end #! Gets the initial item from the active account storage slot as it was at the beginning of the #! transaction. #! -#! Inputs: [index] +#! Inputs: [slot_id_prefix, slot_id_suffix] #! Outputs: [INIT_VALUE] #! #! Where: -#! - index is the index of the item to get. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - INIT_VALUE is the initial value of the item at the beginning of the transaction. #! #! Panics if: -#! - the index of the requested item is out of bounds. +#! - a slot with the provided slot ID does not exist in account storage. #! #! Invocation: exec -export.get_initial_item - push.0.0 movup.2 - # => [index, 0, 0] +pub proc get_initial_item + push.0 movdn.2 + # => [slot_id_prefix, slot_id_suffix, 0] exec.kernel_proc_offsets::account_get_initial_item_offset - # => [offset, index, 0, 0] + # => [offset, slot_id_prefix, slot_id_suffix, 0] # pad the stack padw swapw padw padw swapdw - # => [offset, index, pad(14)] + # => [offset, slot_id_prefix, slot_id_suffix, pad(13)] syscall.exec_kernel_proc # => [INIT_VALUE, pad(12)] @@ -339,26 +341,28 @@ end #! Gets a map item from the active account storage. #! -#! Inputs: [index, KEY] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY] #! Outputs: [VALUE] #! #! Where: -#! - index is the index of the map where the KEY VALUE should be read. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - the slot must point to the root of the storage map. #! - KEY is the key of the item to get. #! - VALUE is the value of the item. #! #! Panics if: -#! - the index for the map is out of bounds, meaning > 255. +#! - a slot with the provided slot ID does not exist in account storage. #! - the slot item at index is not a map. #! #! Invocation: exec -export.get_map_item +pub proc get_map_item exec.kernel_proc_offsets::account_get_map_item_offset - # => [offset, index, KEY] + # => [offset, slot_id_prefix, slot_id_suffix, KEY] # pad the stack - push.0.0 movdn.7 movdn.7 padw padw swapdw - # => [offset, index, KEY, pad(10)] + push.0 movdn.7 padw padw swapdw + # => [0, offset, slot_id_prefix, slot_id_suffix, KEY, pad(9)] syscall.exec_kernel_proc # => [VALUE, pad(12)] @@ -371,26 +375,26 @@ end #! Gets the initial VALUE from the active account storage map as it was at the beginning of the #! transaction. #! -#! Inputs: [index, KEY] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY] #! Outputs: [INIT_VALUE] #! #! Where: -#! - index is the index of the map where the KEY VALUE should be read. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - KEY is the key of the item to get. #! - INIT_VALUE is the initial value of the item at the beginning of the transaction. #! #! Panics if: -#! - the index for the map is out of bounds, meaning > 255. +#! - a slot with the provided slot ID does not exist in account storage. #! - the slot item at index is not a map. #! #! Invocation: exec -export.get_initial_map_item +pub proc get_initial_map_item exec.kernel_proc_offsets::account_get_initial_map_item_offset - # => [offset, index, KEY] + # => [offset, slot_id_prefix, slot_id_suffix, KEY] - # pad the stack - push.0.0 movdn.7 movdn.7 padw padw swapdw - # => [offset, index, KEY, pad(10)] + push.0 movdn.7 padw padw swapdw + # => [0, offset, slot_id_prefix, slot_id_suffix, KEY, pad(9)] syscall.exec_kernel_proc # => [INIT_VALUE, pad(12)] @@ -418,7 +422,7 @@ end #! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: exec -export.get_balance +pub proc get_balance exec.kernel_proc_offsets::account_get_balance_offset # => [offset, faucet_id_prefix, faucet_id_suffix] @@ -449,7 +453,7 @@ end #! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: exec -export.get_initial_balance +pub proc get_initial_balance exec.kernel_proc_offsets::account_get_initial_balance_offset # => [offset, faucet_id_prefix, faucet_id_suffix] @@ -479,7 +483,7 @@ end #! - the ASSET is a fungible asset. #! #! Invocation: exec -export.has_non_fungible_asset +pub proc has_non_fungible_asset exec.kernel_proc_offsets::account_has_non_fungible_asset_offset # => [offset, ASSET] @@ -504,7 +508,7 @@ end #! - num_procedures is the number of procedures in the active account. #! #! Invocation: exec -export.get_num_procedures +pub proc get_num_procedures # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -533,7 +537,7 @@ end #! - the procedure index is out of bounds. #! #! Invocation: exec -export.get_procedure_root +pub proc get_procedure_root # => [index] push.0.0 movup.2 @@ -566,7 +570,7 @@ end #! available on the active account. #! #! Invocation: exec -export.has_procedure +pub proc has_procedure exec.kernel_proc_offsets::account_has_procedure_offset # => [offset, PROC_ROOT] diff --git a/crates/miden-lib/asm/miden/active_note.masm b/crates/miden-protocol/asm/protocol/active_note.masm similarity index 67% rename from crates/miden-lib/asm/miden/active_note.masm rename to crates/miden-protocol/asm/protocol/active_note.masm index 69d2e6753b..77865a19bc 100644 --- a/crates/miden-lib/asm/miden/active_note.masm +++ b/crates/miden-protocol/asm/protocol/active_note.masm @@ -1,15 +1,15 @@ -use.std::mem +use miden::core::crypto::hashes::rpo256 +use miden::core::mem -use.miden::kernel_proc_offsets -use.miden::note -use.miden::contracts::wallets::basic->wallet +use miden::protocol::kernel_proc_offsets +use miden::protocol::note # ERRORS # ================================================================================================= -const.ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT="note data does not match the commitment" +const ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT="note data does not match the commitment" -const.ERR_NOTE_INVALID_NUMBER_OF_INPUTS="the specified number of note inputs does not match the actual number" +const ERR_NOTE_INVALID_NUMBER_OF_INPUTS="the specified number of note inputs does not match the actual number" # ACTIVE NOTE PROCEDURES # ================================================================================================= @@ -30,7 +30,7 @@ const.ERR_NOTE_INVALID_NUMBER_OF_INPUTS="the specified number of note inputs doe #! - no note is currently active. #! #! Invocation: exec -export.get_assets +pub proc get_assets # pad the stack padw padw padw push.0.0 # => [pad(14), dest_ptr] @@ -66,7 +66,7 @@ end #! - no note is currently active. #! #! Invocation: exec -export.get_recipient +pub proc get_recipient # pad the stack padw padw padw push.0.0 # => [pad(14)] @@ -96,14 +96,14 @@ end #! #! Where: #! - dest_ptr is the memory address to write the note inputs. -#! - NOTE_INPUTS_COMMITMENT is the sequential hash of the padded note's inputs. +#! - NOTE_INPUTS_COMMITMENT is the commitment to the note's inputs. #! - INPUTS is the data corresponding to the note's inputs. #! #! Panics if: #! - no note is currently active. #! #! Invocation: exec -export.get_inputs +pub proc get_inputs # pad the stack padw padw padw push.0.0 # => [pad(14), dest_ptr] @@ -131,16 +131,17 @@ end #! Returns the metadata of the active note. #! #! Inputs: [] -#! Outputs: [METADATA] +#! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER] #! #! Where: -#! - METADATA is the metadata of the active note. +#! - METADATA_HEADER is the metadata header of the specified input note. +#! - NOTE_ATTACHMENT is the attachment of the specified input note. #! #! Panics if: #! - no note is currently active. #! #! Invocation: exec -export.get_metadata +pub proc get_metadata # pad the stack padw padw padw push.0.0 # => [pad(14)] @@ -153,11 +154,11 @@ export.get_metadata # => [offset, is_active_note = 1, pad(14)] syscall.exec_kernel_proc - # => [METADATA, pad(12)] + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] # clean the stack - swapdw dropw dropw swapw dropw - # => [METADATA] + swapdw dropw dropw + # => [NOTE_ATTACHMENT, METADATA_HEADER] end #! Returns the sender of the active note. @@ -172,27 +173,13 @@ end #! - no note is currently active. #! #! Invocation: exec -export.get_sender - # pad the stack - padw padw padw push.0.0 - # => [pad(14)] - - # push the flag indicating that we want to request metadata from the active note - push.1 - # => [is_active_note = 1, pad(14)] +pub proc get_sender + # get metadata and drop attachment + exec.get_metadata dropw + # => [METADATA_HEADER] - exec.kernel_proc_offsets::input_note_get_metadata_offset - # => [offset, is_active_note = 1, pad(14)] - - syscall.exec_kernel_proc - # => [METADATA, pad(12)] - - # extract the sender ID from the metadata word + # extract the sender ID from the metadata header exec.note::extract_sender_from_metadata - # => [sender_id_prefix, sender_id_suffix, pad(12)] - - # clean the stack - swapw dropw swapw dropw movdn.5 movdn.5 dropw # => [sender_id_prefix, sender_id_suffix] end @@ -208,7 +195,7 @@ end #! - no note is currently active. #! #! Invocation: exec -export.get_serial_number +pub proc get_serial_number # pad the stack padw padw padw push.0.0 # => [pad(14)] @@ -240,7 +227,7 @@ end #! - no note is currently active. #! #! Invocation: exec -export.get_script_root +pub proc get_script_root # pad the stack padw padw padw push.0.0 # => [pad(14)] @@ -260,63 +247,6 @@ export.get_script_root # => [SCRIPT_ROOT] end -#! Adds all assets from the active note to the native account's vault. -#! -#! Inputs: [] -#! Outputs: [] -export.add_assets_to_account.1024 - # write assets to local memory starting at offset 0 - # we have allocated 4 * MAX_ASSETS_PER_NOTE number of locals so all assets should fit - # since the asset memory will be overwritten, we don't have to initialize the locals to zero - locaddr.0 exec.get_assets - # => [num_of_assets, ptr = 0] - - # compute the pointer at which we should stop iterating - mul.4 dup.1 add - # => [end_ptr, ptr] - - # pad the stack and move the pointer to the top - padw movup.5 - # => [ptr, EMPTY_WORD, end_ptr] - - # loop if the amount of assets is non-zero - dup dup.6 neq - # => [should_loop, ptr, EMPTY_WORD, end_ptr] - - while.true - # => [ptr, EMPTY_WORD, end_ptr] - - # save the pointer so that we can use it later - dup movdn.5 - # => [ptr, EMPTY_WORD, ptr, end_ptr] - - # load the asset - mem_loadw_be - # => [ASSET, ptr, end_ptr] - - # pad the stack before call - padw swapw padw padw swapdw - # => [ASSET, pad(12), ptr, end_ptr] - - # add asset to the account - call.wallet::receive_asset - # => [pad(16), ptr, end_ptr] - - # clean the stack after call - dropw dropw dropw - # => [EMPTY_WORD, ptr, end_ptr] - - # increment the pointer and continue looping if ptr != end_ptr - movup.4 add.4 dup dup.6 neq - # => [should_loop, ptr+4, EMPTY_WORD, end_ptr] - end - # => [ptr', EMPTY_WORD, end_ptr] - - # clear the stack - drop dropw drop - # => [] -end - # HELPER PROCEDURES # ================================================================================================= @@ -330,9 +260,13 @@ end #! } #! Outputs: #! Operand stack: [num_inputs, dest_ptr] -proc.write_inputs_to_memory +proc write_inputs_to_memory # load the inputs from the advice map to the advice stack - adv.push_mapvaln + # we pad the number of inputs to the next multiple of 8 so that we can use the + # `pipe_double_words_to_memory` instruction. The padded zeros don't affect the commitment + # computation. + + adv.push_mapvaln.8 # OS => [NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] # AS => [advice_num_inputs, [INPUT_VALUES]] @@ -341,12 +275,6 @@ proc.write_inputs_to_memory # OS => [num_inputs, advice_num_inputs, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] # AS => [[INPUT_VALUES]] - # Validate the note inputs length. Round up the number of inputs to the next multiple of 8: that - # value should be equal to the length obtained from the `adv.push_mapvaln` procedure. - u32divmod.8 neq.0 add mul.8 - # OS => [rounded_up_num_inputs, advice_num_inputs, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] - # AS => [[INPUT_VALUES]] - assert_eq.err=ERR_NOTE_INVALID_NUMBER_OF_INPUTS # OS => [NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] # AS => [[INPUT_VALUES]] @@ -361,13 +289,45 @@ proc.write_inputs_to_memory # OS => [even_num_words, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] # AS => [[INPUT_VALUES]] - # prepare the stack for the `pipe_preimage_to_memory` procedure - dup.6 swap - # OS => [even_num_words, dest_ptr, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + # compute the end pointer for writing the padded inputs (even_num_words * 4 elements) + dup.6 swap mul.4 add + # OS => [end_ptr, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + # AS => [[INPUT_VALUES]] + + # prepare the stack for the `pipe_double_words_to_memory` procedure. + # + # To match `rpo256::hash_elements` (used for NOTE_INPUTS_COMMITMENT), we set the first capacity + # element to `num_inputs % 8`. + dup.6 dup.6 + # OS => [num_inputs, write_ptr, end_ptr, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + # AS => [[INPUT_VALUES]] + + u32divmod.8 swap drop + # OS => [num_inputs_mod_8, write_ptr, end_ptr, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + # AS => [[INPUT_VALUES]] + + push.0.0.0 + # OS => [A, write_ptr, end_ptr, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr], where A = [0, 0, 0, num_inputs_mod_8] + # AS => [[INPUT_VALUES]] + + padw padw + # OS => [PAD, PAD, A, write_ptr, end_ptr, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] # AS => [[INPUT_VALUES]] # write the inputs from the advice stack into memory - exec.mem::pipe_preimage_to_memory drop - # OS => [num_inputs, dest_ptr] + exec.mem::pipe_double_words_to_memory + # OS => [PERM, PERM, PERM, end_ptr', NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] # AS => [] + + # extract the computed commitment from the hasher state + exec.rpo256::squeeze_digest + # OS => [COMPUTED_COMMITMENT, end_ptr', NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + + # drop end_ptr' + movup.4 drop + # OS => [COMPUTED_COMMITMENT, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + + # validate that the inputs written to memory match the inputs commitment + assert_eqw.err=ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT + # => [num_inputs, dest_ptr] end diff --git a/crates/miden-lib/asm/miden/asset.masm b/crates/miden-protocol/asm/protocol/asset.masm similarity index 86% rename from crates/miden-lib/asm/miden/asset.masm rename to crates/miden-protocol/asm/protocol/asset.masm index 6c6d8f8d6b..e8da408a8a 100644 --- a/crates/miden-lib/asm/miden/asset.masm +++ b/crates/miden-protocol/asm/protocol/asset.masm @@ -1,13 +1,13 @@ -use.miden::account_id +use miden::protocol::account_id # ERRORS # ================================================================================================= -const.ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the fungible asset because the provided faucet id is not from a fungible faucet" +const ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the fungible asset because the provided faucet id is not from a fungible faucet" -const.ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT="fungible asset build operation called with amount that exceeds the maximum allowed asset amount" +const ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT="fungible asset build operation called with amount that exceeds the maximum allowed asset amount" -const.ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet" +const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet" # PROCEDURES # ================================================================================================= @@ -24,7 +24,7 @@ const.ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! - ASSET is the built fungible asset. #! #! Invocation: exec -export.build_fungible_asset +pub proc build_fungible_asset # assert the faucet is a fungible faucet dup exec.account_id::is_fungible_faucet assert.err=ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID # => [faucet_id_prefix, faucet_id_suffix, amount] @@ -51,7 +51,7 @@ end #! - ASSET is the built non-fungible asset. #! #! Invocation: exec -export.build_non_fungible_asset +pub proc build_non_fungible_asset # assert the faucet is a non-fungible faucet dup exec.account_id::is_non_fungible_faucet assert.err=ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID @@ -69,4 +69,4 @@ end #! Outputs: [fungible_asset_max_amount] #! #! fungible_asset_max_amount is the maximum amount of a fungible asset. -export.::miden::util::asset::get_fungible_asset_max_amount +pub use ::miden::protocol::util::asset::get_fungible_asset_max_amount diff --git a/crates/miden-lib/asm/miden/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm similarity index 94% rename from crates/miden-lib/asm/miden/faucet.masm rename to crates/miden-protocol/asm/protocol/faucet.masm index 1332f2c3ca..f3afe38e9b 100644 --- a/crates/miden-lib/asm/miden/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -1,6 +1,6 @@ -use.miden::asset -use.miden::active_account -use.miden::kernel_proc_offsets +use miden::protocol::asset +use miden::protocol::active_account +use miden::protocol::kernel_proc_offsets #! Creates a fungible asset for the faucet the transaction is being executed against. #! @@ -15,7 +15,7 @@ use.miden::kernel_proc_offsets #! - the active account is not a fungible faucet. #! #! Invocation: exec -export.create_fungible_asset +pub proc create_fungible_asset # fetch the id of the faucet the transaction is being executed against. exec.active_account::get_id # => [id_prefix, id_suffix, amount] @@ -38,7 +38,7 @@ end #! - the active account is not a non-fungible faucet. #! #! Invocation: exec -export.create_non_fungible_asset +pub proc create_non_fungible_asset # get the id of the faucet the transaction is being executed against exec.active_account::get_id swap drop # => [faucet_id_prefix, DATA_HASH] @@ -66,7 +66,7 @@ end #! - for non-fungible faucets if the non-fungible asset being minted already exists. #! #! Invocation: exec -export.mint +pub proc mint exec.kernel_proc_offsets::faucet_mint_asset_offset # => [offset, ASSET] @@ -101,7 +101,7 @@ end #! provided as input to the transaction via a note or the accounts vault. #! #! Invocation: exec -export.burn +pub proc burn exec.kernel_proc_offsets::faucet_burn_asset_offset # => [offset, ASSET] @@ -130,7 +130,7 @@ end #! - the transaction is not being executed against a fungible faucet. #! #! Invocation: exec -export.get_total_issuance +pub proc get_total_issuance # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -161,7 +161,7 @@ end #! - the ASSET is not associated with the faucet the transaction is being executed against. #! #! Invocation: exec -export.is_non_fungible_asset_issued +pub proc is_non_fungible_asset_issued exec.kernel_proc_offsets::faucet_is_non_fungible_asset_issued_offset # => [offset, ASSET] diff --git a/crates/miden-lib/asm/miden/input_note.masm b/crates/miden-protocol/asm/protocol/input_note.masm similarity index 88% rename from crates/miden-lib/asm/miden/input_note.masm rename to crates/miden-protocol/asm/protocol/input_note.masm index 228899e11e..a08d5a5dd2 100644 --- a/crates/miden-lib/asm/miden/input_note.masm +++ b/crates/miden-protocol/asm/protocol/input_note.masm @@ -1,5 +1,5 @@ -use.miden::kernel_proc_offsets -use.miden::note +use miden::protocol::kernel_proc_offsets +use miden::protocol::note # PROCEDURES # ================================================================================================= @@ -20,7 +20,7 @@ use.miden::note #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_assets_info +pub proc get_assets_info # start padding the stack push.0 swap # => [note_index, 0] @@ -71,7 +71,7 @@ end #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_assets +pub proc get_assets # get the assets commitment and assets number dup.1 exec.get_assets_info # => [ASSETS_COMMITMENT, num_assets, dest_ptr, note_index] @@ -94,7 +94,7 @@ end #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_recipient +pub proc get_recipient # start padding the stack push.0 swap # => [note_index, 0] @@ -122,17 +122,18 @@ end #! Returns the metadata of the input note with the specified index. #! #! Inputs: [note_index] -#! Outputs: [METADATA] +#! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER] #! #! Where: #! - note_index is the index of the input note whose metadata should be returned. -#! - METADATA is the metadata of the input note. +#! - METADATA_HEADER is the metadata header of the specified input note. +#! - NOTE_ATTACHMENT is the attachment of the specified input note. #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_metadata +pub proc get_metadata # start padding the stack push.0 swap # => [note_index, 0] @@ -150,11 +151,11 @@ export.get_metadata # => [offset, is_active_note = 0, note_index, pad(13)] syscall.exec_kernel_proc - # => [METADATA, pad(12)] + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] # clean the stack - swapdw dropw dropw swapw dropw - # => [METADATA] + swapdw dropw dropw + # => [NOTE_ATTACHMENT, METADATA_HEADER] end #! Returns the sender of the input note with the specified index. @@ -170,32 +171,13 @@ end #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_sender - # start padding the stack - push.0 swap - # => [note_index, 0] - - # push the flag indicating that we want to request metadata from the note with the specified - # index - push.0 - # => [is_active_note = 0, note_index, 0] - - exec.kernel_proc_offsets::input_note_get_metadata_offset - # => [offset, is_active_note = 0, note_index, 0] - - # pad the stack - padw swapw padw padw swapdw - # => [offset, is_active_note = 0, note_index, pad(13)] - - syscall.exec_kernel_proc - # => [METADATA, pad(12)] +pub proc get_sender + # get metadata and drop attachment + exec.get_metadata dropw + # => [METADATA_HEADER] - # extract the sender ID from the metadata word + # extract the sender ID from the metadata header exec.note::extract_sender_from_metadata - # => [sender_id_prefix, sender_id_suffix, pad(12)] - - # clean the stack - swapw dropw swapw dropw movdn.5 movdn.5 dropw # => [sender_id_prefix, sender_id_suffix] end @@ -213,7 +195,7 @@ end #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_inputs_info +pub proc get_inputs_info # start padding the stack push.0 swap # => [note_index, 0] @@ -254,7 +236,7 @@ end #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_script_root +pub proc get_script_root # start padding the stack push.0 swap # => [note_index, 0] @@ -292,7 +274,7 @@ end #! - the note index is greater or equal to the total number of input notes. #! #! Invocation: exec -export.get_serial_number +pub proc get_serial_number # start padding the stack push.0 swap # => [note_index, 0] diff --git a/crates/miden-lib/asm/miden/kernel_proc_offsets.masm b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm similarity index 78% rename from crates/miden-lib/asm/miden/kernel_proc_offsets.masm rename to crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm index 3f7215e494..ae698f2df7 100644 --- a/crates/miden-lib/asm/miden/kernel_proc_offsets.masm +++ b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm @@ -4,92 +4,93 @@ ### Account ##################################### # Entire account commitment -const.ACCOUNT_GET_INITIAL_COMMITMENT_OFFSET=0 -const.ACCOUNT_COMPUTE_COMMITMENT_OFFSET=1 +const ACCOUNT_GET_INITIAL_COMMITMENT_OFFSET=0 +const ACCOUNT_COMPUTE_COMMITMENT_OFFSET=1 # ID -const.ACCOUNT_GET_ID_OFFSET=2 +const ACCOUNT_GET_ID_OFFSET=2 # Nonce -const.ACCOUNT_GET_NONCE_OFFSET=3 # accessor -const.ACCOUNT_INCR_NONCE_OFFSET=4 # mutator +const ACCOUNT_GET_NONCE_OFFSET=3 # accessor +const ACCOUNT_INCR_NONCE_OFFSET=4 # mutator # Code -const.ACCOUNT_GET_CODE_COMMITMENT_OFFSET=5 +const ACCOUNT_GET_CODE_COMMITMENT_OFFSET=5 # Storage -const.ACCOUNT_GET_INITIAL_STORAGE_COMMITMENT_OFFSET=6 -const.ACCOUNT_COMPUTE_STORAGE_COMMITMENT_OFFSET=7 -const.ACCOUNT_GET_ITEM_OFFSET=8 -const.ACCOUNT_GET_INITIAL_ITEM_OFFSET=9 -const.ACCOUNT_SET_ITEM_OFFSET=10 -const.ACCOUNT_GET_MAP_ITEM_OFFSET=11 -const.ACCOUNT_GET_INITIAL_MAP_ITEM_OFFSET=12 -const.ACCOUNT_SET_MAP_ITEM_OFFSET=13 +const ACCOUNT_GET_INITIAL_STORAGE_COMMITMENT_OFFSET=6 +const ACCOUNT_COMPUTE_STORAGE_COMMITMENT_OFFSET=7 +const ACCOUNT_GET_ITEM_OFFSET=8 +const ACCOUNT_GET_INITIAL_ITEM_OFFSET=9 +const ACCOUNT_SET_ITEM_OFFSET=10 +const ACCOUNT_GET_MAP_ITEM_OFFSET=11 +const ACCOUNT_GET_INITIAL_MAP_ITEM_OFFSET=12 +const ACCOUNT_SET_MAP_ITEM_OFFSET=13 # Vault -const.ACCOUNT_GET_INITIAL_VAULT_ROOT_OFFSET=14 -const.ACCOUNT_GET_VAULT_ROOT_OFFSET=15 -const.ACCOUNT_ADD_ASSET_OFFSET=16 -const.ACCOUNT_REMOVE_ASSET_OFFSET=17 -const.ACCOUNT_GET_BALANCE_OFFSET=18 -const.ACCOUNT_GET_INITIAL_BALANCE_OFFSET=19 -const.ACCOUNT_HAS_NON_FUNGIBLE_ASSET_OFFSET=20 +const ACCOUNT_GET_INITIAL_VAULT_ROOT_OFFSET=14 +const ACCOUNT_GET_VAULT_ROOT_OFFSET=15 +const ACCOUNT_ADD_ASSET_OFFSET=16 +const ACCOUNT_REMOVE_ASSET_OFFSET=17 +const ACCOUNT_GET_BALANCE_OFFSET=18 +const ACCOUNT_GET_INITIAL_BALANCE_OFFSET=19 +const ACCOUNT_HAS_NON_FUNGIBLE_ASSET_OFFSET=20 # Delta -const.ACCOUNT_COMPUTE_DELTA_COMMITMENT_OFFSET=21 +const ACCOUNT_COMPUTE_DELTA_COMMITMENT_OFFSET=21 # Procedure introspection -const.ACCOUNT_GET_NUM_PROCEDURES_OFFSET=22 -const.ACCOUNT_GET_PROCEDURE_ROOT_OFFSET=23 -const.ACCOUNT_WAS_PROCEDURE_CALLED_OFFSET=24 -const.ACCOUNT_HAS_PROCEDURE_OFFSET=25 +const ACCOUNT_GET_NUM_PROCEDURES_OFFSET=22 +const ACCOUNT_GET_PROCEDURE_ROOT_OFFSET=23 +const ACCOUNT_WAS_PROCEDURE_CALLED_OFFSET=24 +const ACCOUNT_HAS_PROCEDURE_OFFSET=25 ### Faucet ###################################### -const.FAUCET_MINT_ASSET_OFFSET=26 -const.FAUCET_BURN_ASSET_OFFSET=27 -const.FAUCET_GET_TOTAL_FUNGIBLE_ASSET_ISSUANCE_OFFSET=28 -const.FAUCET_IS_NON_FUNGIBLE_ASSET_ISSUED_OFFSET=29 +const FAUCET_MINT_ASSET_OFFSET=26 +const FAUCET_BURN_ASSET_OFFSET=27 +const FAUCET_GET_TOTAL_FUNGIBLE_ASSET_ISSUANCE_OFFSET=28 +const FAUCET_IS_NON_FUNGIBLE_ASSET_ISSUED_OFFSET=29 ### Note ######################################## # input notes -const.INPUT_NOTE_GET_METADATA_OFFSET=30 -const.INPUT_NOTE_GET_ASSETS_INFO_OFFSET=31 -const.INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET=32 -const.INPUT_NOTE_GET_INPUTS_INFO_OFFSET=33 -const.INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET=34 -const.INPUT_NOTE_GET_RECIPIENT_OFFSET=35 +const INPUT_NOTE_GET_METADATA_OFFSET=30 +const INPUT_NOTE_GET_ASSETS_INFO_OFFSET=31 +const INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET=32 +const INPUT_NOTE_GET_INPUTS_INFO_OFFSET=33 +const INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET=34 +const INPUT_NOTE_GET_RECIPIENT_OFFSET=35 # output notes -const.OUTPUT_NOTE_CREATE_OFFSET=36 -const.OUTPUT_NOTE_GET_METADATA_OFFSET=37 -const.OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=38 -const.OUTPUT_NOTE_GET_RECIPIENT_OFFSET=39 -const.OUTPUT_NOTE_ADD_ASSET_OFFSET=40 +const OUTPUT_NOTE_CREATE_OFFSET=36 +const OUTPUT_NOTE_GET_METADATA_OFFSET=37 +const OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=38 +const OUTPUT_NOTE_GET_RECIPIENT_OFFSET=39 +const OUTPUT_NOTE_ADD_ASSET_OFFSET=40 +const OUTPUT_NOTE_SET_ATTACHMENT_OFFSET=41 ### Tx ########################################## # input notes -const.TX_GET_NUM_INPUT_NOTES_OFFSET=41 -const.TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=42 +const TX_GET_NUM_INPUT_NOTES_OFFSET=42 +const TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=43 # output notes -const.TX_GET_NUM_OUTPUT_NOTES_OFFSET=43 -const.TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=44 +const TX_GET_NUM_OUTPUT_NOTES_OFFSET=44 +const TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=45 # block info -const.TX_GET_BLOCK_COMMITMENT_OFFSET=45 -const.TX_GET_BLOCK_NUMBER_OFFSET=46 -const.TX_GET_BLOCK_TIMESTAMP_OFFSET=47 +const TX_GET_BLOCK_COMMITMENT_OFFSET=46 +const TX_GET_BLOCK_NUMBER_OFFSET=47 +const TX_GET_BLOCK_TIMESTAMP_OFFSET=48 # foreign context -const.TX_START_FOREIGN_CONTEXT_OFFSET=48 -const.TX_END_FOREIGN_CONTEXT_OFFSET=49 +const TX_START_FOREIGN_CONTEXT_OFFSET=49 +const TX_END_FOREIGN_CONTEXT_OFFSET=50 # expiration data -const.TX_GET_EXPIRATION_DELTA_OFFSET=50 # accessor -const.TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=51 # mutator +const TX_GET_EXPIRATION_DELTA_OFFSET=51 # accessor +const TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=52 # mutator # ACCESSORS # ------------------------------------------------------------------------------------------------- @@ -104,7 +105,7 @@ const.TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=51 # mutator #! Where: #! - proc_offset is the offset of the `account_get_initial_commitment` kernel procedure required to #! get the address where this procedure is stored. -export.account_get_initial_commitment_offset +pub proc account_get_initial_commitment_offset push.ACCOUNT_GET_INITIAL_COMMITMENT_OFFSET end @@ -116,7 +117,7 @@ end #! Where: #! - proc_offset is the offset of the `account_compute_commitment` kernel procedure required to get #! the address where this procedure is stored. -export.account_compute_commitment_offset +pub proc account_compute_commitment_offset push.ACCOUNT_COMPUTE_COMMITMENT_OFFSET end @@ -128,7 +129,7 @@ end #! Where: #! - proc_offset is the offset of the `account_compute_delta_commitment` kernel procedure required #! to get the address where this procedure is stored. -export.account_compute_delta_commitment_offset +pub proc account_compute_delta_commitment_offset push.ACCOUNT_COMPUTE_DELTA_COMMITMENT_OFFSET end @@ -140,7 +141,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_id` kernel procedure required to get the address #! where this procedure is stored. -export.account_get_id_offset +pub proc account_get_id_offset push.ACCOUNT_GET_ID_OFFSET end @@ -152,7 +153,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_nonce` kernel procedure required to get the #! address where this procedure is stored. -export.account_get_nonce_offset +pub proc account_get_nonce_offset push.ACCOUNT_GET_NONCE_OFFSET end @@ -164,7 +165,7 @@ end #! Where: #! - proc_offset is the offset of the `account_incr_nonce` kernel procedure required to get the #! address where this procedure is stored. -export.account_incr_nonce_offset +pub proc account_incr_nonce_offset push.ACCOUNT_INCR_NONCE_OFFSET end @@ -176,7 +177,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_code_commitment` kernel procedure required to get #! the address where this procedure is stored. -export.account_get_code_commitment_offset +pub proc account_get_code_commitment_offset push.ACCOUNT_GET_CODE_COMMITMENT_OFFSET end @@ -188,7 +189,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_initial_storage_commitment` kernel procedure #! required to get the address where this procedure is stored. -export.account_get_initial_storage_commitment_offset +pub proc account_get_initial_storage_commitment_offset push.ACCOUNT_GET_INITIAL_STORAGE_COMMITMENT_OFFSET end @@ -200,7 +201,7 @@ end #! Where: #! - proc_offset is the offset of the `account_compute_storage_commitment` kernel procedure required #! to get the address where this procedure is stored. -export.account_compute_storage_commitment_offset +pub proc account_compute_storage_commitment_offset push.ACCOUNT_COMPUTE_STORAGE_COMMITMENT_OFFSET end @@ -212,7 +213,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_item` kernel procedure required to get the #! address where this procedure is stored. -export.account_get_item_offset +pub proc account_get_item_offset push.ACCOUNT_GET_ITEM_OFFSET end @@ -224,7 +225,7 @@ end #! Where: #! - proc_offset is the offset of the `account_set_item` kernel procedure required to get the #! address where this procedure is stored. -export.account_set_item_offset +pub proc account_set_item_offset push.ACCOUNT_SET_ITEM_OFFSET end @@ -236,7 +237,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_map_item` kernel procedure required to get the #! address where this procedure is stored. -export.account_get_map_item_offset +pub proc account_get_map_item_offset push.ACCOUNT_GET_MAP_ITEM_OFFSET end @@ -248,7 +249,7 @@ end #! Where: #! - proc_offset is the offset of the `account_set_map_item` kernel procedure required to get the #! address where this procedure is stored. -export.account_set_map_item_offset +pub proc account_set_map_item_offset push.ACCOUNT_SET_MAP_ITEM_OFFSET end @@ -260,7 +261,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_initial_item` kernel procedure required to get #! the address where this procedure is stored. -export.account_get_initial_item_offset +pub proc account_get_initial_item_offset push.ACCOUNT_GET_INITIAL_ITEM_OFFSET end @@ -272,7 +273,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_initial_map_item` kernel procedure required to #! get the address where this procedure is stored. -export.account_get_initial_map_item_offset +pub proc account_get_initial_map_item_offset push.ACCOUNT_GET_INITIAL_MAP_ITEM_OFFSET end @@ -284,7 +285,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_initial_vault_root` kernel procedure required #! to get the address where this procedure is stored. -export.account_get_initial_vault_root_offset +pub proc account_get_initial_vault_root_offset push.ACCOUNT_GET_INITIAL_VAULT_ROOT_OFFSET end @@ -296,7 +297,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_vault_root` kernel procedure required to #! get the address where this procedure is stored. -export.account_get_vault_root_offset +pub proc account_get_vault_root_offset push.ACCOUNT_GET_VAULT_ROOT_OFFSET end @@ -308,7 +309,7 @@ end #! Where: #! - proc_offset is the offset of the `account_add_asset` kernel procedure required to get the #! address where this procedure is stored. -export.account_add_asset_offset +pub proc account_add_asset_offset push.ACCOUNT_ADD_ASSET_OFFSET end @@ -320,7 +321,7 @@ end #! Where: #! - proc_offset is the offset of the `account_remove_asset` kernel procedure required to get the #! address where this procedure is stored. -export.account_remove_asset_offset +pub proc account_remove_asset_offset push.ACCOUNT_REMOVE_ASSET_OFFSET end @@ -332,7 +333,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_balance` kernel procedure required to get the #! address where this procedure is stored. -export.account_get_balance_offset +pub proc account_get_balance_offset push.ACCOUNT_GET_BALANCE_OFFSET end @@ -344,7 +345,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_initial_balance` kernel procedure required to get #! the address where this procedure is stored. -export.account_get_initial_balance_offset +pub proc account_get_initial_balance_offset push.ACCOUNT_GET_INITIAL_BALANCE_OFFSET end @@ -356,7 +357,7 @@ end #! Where: #! - proc_offset is the offset of the `account_has_non_fungible_asset` kernel procedure required to #! get the address where this procedure is stored. -export.account_has_non_fungible_asset_offset +pub proc account_has_non_fungible_asset_offset push.ACCOUNT_HAS_NON_FUNGIBLE_ASSET_OFFSET end @@ -368,7 +369,7 @@ end #! Where: #! - proc_offset is the offset of the `account_was_procedure_called` kernel procedure required to #! get the address where this procedure is stored. -export.account_was_procedure_called_offset +pub proc account_was_procedure_called_offset push.ACCOUNT_WAS_PROCEDURE_CALLED_OFFSET end @@ -380,7 +381,7 @@ end #! Where: #! - proc_offset is the offset of the `account_has_procedure` kernel procedure required to get the #! address where this procedure is stored. -export.account_has_procedure_offset +pub proc account_has_procedure_offset push.ACCOUNT_HAS_PROCEDURE_OFFSET end @@ -392,7 +393,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_num_procedures` kernel procedure required to #! get the address where this procedure is stored. -export.account_get_num_procedures_offset +pub proc account_get_num_procedures_offset push.ACCOUNT_GET_NUM_PROCEDURES_OFFSET end @@ -404,7 +405,7 @@ end #! Where: #! - proc_offset is the offset of the `account_get_procedure_root` kernel procedure required to #! get the address where this procedure is stored. -export.account_get_procedure_root_offset +pub proc account_get_procedure_root_offset push.ACCOUNT_GET_PROCEDURE_ROOT_OFFSET end @@ -418,7 +419,7 @@ end #! Where: #! - proc_offset is the offset of the `faucet_mint_asset` kernel procedure required to get the #! address where this procedure is stored. -export.faucet_mint_asset_offset +pub proc faucet_mint_asset_offset push.FAUCET_MINT_ASSET_OFFSET end @@ -430,7 +431,7 @@ end #! Where: #! - proc_offset is the offset of the `faucet_burn_asset` kernel procedure required to get the #! address where this procedure is stored. -export.faucet_burn_asset_offset +pub proc faucet_burn_asset_offset push.FAUCET_BURN_ASSET_OFFSET end @@ -442,7 +443,7 @@ end #! Where: #! - proc_offset is the offset of the `faucet_get_total_fungible_asset_issuance` kernel procedure #! required to get the address where this procedure is stored. -export.faucet_get_total_fungible_asset_issuance_offset +pub proc faucet_get_total_fungible_asset_issuance_offset push.FAUCET_GET_TOTAL_FUNGIBLE_ASSET_ISSUANCE_OFFSET end @@ -454,7 +455,7 @@ end #! Where: #! - proc_offset is the offset of the `faucet_is_non_fungible_asset_issued` kernel procedure #! required to get the address where this procedure is stored. -export.faucet_is_non_fungible_asset_issued_offset +pub proc faucet_is_non_fungible_asset_issued_offset push.FAUCET_IS_NON_FUNGIBLE_ASSET_ISSUED_OFFSET end @@ -468,7 +469,7 @@ end #! Where: #! - proc_offset is the offset of the `output_note_create` kernel procedure required to get the #! address where this procedure is stored. -export.output_note_create_offset +pub proc output_note_create_offset push.OUTPUT_NOTE_CREATE_OFFSET end @@ -480,10 +481,23 @@ end #! Where: #! - proc_offset is the offset of the `output_note_add_asset` kernel procedure required to get the #! address where this procedure is stored. -export.output_note_add_asset_offset +pub proc output_note_add_asset_offset push.OUTPUT_NOTE_ADD_ASSET_OFFSET end +#! Returns the offset of the `output_note_set_attachment` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! Where: +#! - proc_offset is the offset of the `output_note_set_attachment` kernel procedure required to get +#! the address where this procedure is stored. +pub proc output_note_set_attachment_offset + push.OUTPUT_NOTE_SET_ATTACHMENT_OFFSET +end + + #! Returns the offset of the `output_note_get_assets_info` kernel procedure. #! #! Inputs: [] @@ -492,7 +506,7 @@ end #! Where: #! - proc_offset is the offset of the `output_note_get_assets_info` kernel procedure required to get #! the address where this procedure is stored. -export.output_note_get_assets_info_offset +pub proc output_note_get_assets_info_offset push.OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET end @@ -504,7 +518,7 @@ end #! Where: #! - proc_offset is the offset of the `output_note_get_recipient` kernel procedure required to get #! the address where this procedure is stored. -export.output_note_get_recipient_offset +pub proc output_note_get_recipient_offset push.OUTPUT_NOTE_GET_RECIPIENT_OFFSET end @@ -516,7 +530,7 @@ end #! Where: #! - proc_offset is the offset of the `output_note_get_metadata` kernel procedure required to get #! the address where this procedure is stored. -export.output_note_get_metadata_offset +pub proc output_note_get_metadata_offset push.OUTPUT_NOTE_GET_METADATA_OFFSET end @@ -530,7 +544,7 @@ end #! Where: #! - proc_offset is the offset of the `input_note_get_assets_info` kernel procedure required to get #! the address where this procedure is stored. -export.input_note_get_assets_info_offset +pub proc input_note_get_assets_info_offset push.INPUT_NOTE_GET_ASSETS_INFO_OFFSET end @@ -542,7 +556,7 @@ end #! Where: #! - proc_offset is the offset of the `input_note_get_recipient` kernel procedure required to get #! the address where this procedure is stored. -export.input_note_get_recipient_offset +pub proc input_note_get_recipient_offset push.INPUT_NOTE_GET_RECIPIENT_OFFSET end @@ -554,7 +568,7 @@ end #! Where: #! - proc_offset is the offset of the `input_note_get_metadata` kernel procedure required to get #! the address where this procedure is stored. -export.input_note_get_metadata_offset +pub proc input_note_get_metadata_offset push.INPUT_NOTE_GET_METADATA_OFFSET end @@ -566,7 +580,7 @@ end #! Where: #! - proc_offset is the offset of the `input_note_get_serial_number` kernel procedure required to #! get the address where this procedure is stored. -export.input_note_get_serial_number_offset +pub proc input_note_get_serial_number_offset push.INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET end @@ -578,7 +592,7 @@ end #! Where: #! - proc_offset is the offset of the `input_note_get_inputs_info` kernel procedure required to get #! the address where this procedure is stored. -export.input_note_get_inputs_info_offset +pub proc input_note_get_inputs_info_offset push.INPUT_NOTE_GET_INPUTS_INFO_OFFSET end @@ -590,7 +604,7 @@ end #! Where: #! - proc_offset is the offset of the `input_note_get_script_root` kernel procedure required to get #! the address where this procedure is stored. -export.input_note_get_script_root_offset +pub proc input_note_get_script_root_offset push.INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET end @@ -604,7 +618,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_input_notes_commitment` kernel procedure required to #! get the address where this procedure is stored. -export.tx_get_input_notes_commitment_offset +pub proc tx_get_input_notes_commitment_offset push.TX_GET_INPUT_NOTES_COMMITMENT_OFFSET end @@ -616,7 +630,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_output_notes_commitment` kernel procedure required to #! get the address where this procedure is stored. -export.tx_get_output_notes_commitment_offset +pub proc tx_get_output_notes_commitment_offset push.TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET end @@ -628,7 +642,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_num_input_notes` kernel procedure required to get the #! address where this procedure is stored. -export.tx_get_num_input_notes_offset +pub proc tx_get_num_input_notes_offset push.TX_GET_NUM_INPUT_NOTES_OFFSET end @@ -640,7 +654,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_num_output_notes` kernel procedure required to get the #! address where this procedure is stored. -export.tx_get_num_output_notes_offset +pub proc tx_get_num_output_notes_offset push.TX_GET_NUM_OUTPUT_NOTES_OFFSET end @@ -652,7 +666,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_block_commitment` kernel procedure required to get the #! address where this procedure is stored. -export.tx_get_block_commitment_offset +pub proc tx_get_block_commitment_offset push.TX_GET_BLOCK_COMMITMENT_OFFSET end @@ -664,7 +678,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_block_number` kernel procedure required to get the #! address where this procedure is stored. -export.tx_get_block_number_offset +pub proc tx_get_block_number_offset push.TX_GET_BLOCK_NUMBER_OFFSET end @@ -676,7 +690,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_block_timestamp` kernel procedure required to get the #! address where this procedure is stored. -export.tx_get_block_timestamp_offset +pub proc tx_get_block_timestamp_offset push.TX_GET_BLOCK_TIMESTAMP_OFFSET end @@ -688,7 +702,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_start_foreign_context` kernel procedure required to get #! the address where this procedure is stored. -export.tx_start_foreign_context_offset +pub proc tx_start_foreign_context_offset push.TX_START_FOREIGN_CONTEXT_OFFSET end @@ -700,7 +714,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_end_foreign_context` kernel procedure required to get the #! address where this procedure is stored. -export.tx_end_foreign_context_offset +pub proc tx_end_foreign_context_offset push.TX_END_FOREIGN_CONTEXT_OFFSET end @@ -712,7 +726,7 @@ end #! Where: #! - proc_offset is the offset of the `tx_update_expiration_block_delta` kernel procedure required #! to get the address where this procedure is stored. -export.tx_update_expiration_block_delta_offset +pub proc tx_update_expiration_block_delta_offset push.TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET end @@ -724,6 +738,6 @@ end #! Where: #! - proc_offset is the offset of the `tx_get_expiration_delta` kernel procedure required to get the #! address where this procedure is stored. -export.tx_get_expiration_delta_offset +pub proc tx_get_expiration_delta_offset push.TX_GET_EXPIRATION_DELTA_OFFSET end diff --git a/crates/miden-lib/asm/miden/native_account.masm b/crates/miden-protocol/asm/protocol/native_account.masm similarity index 79% rename from crates/miden-lib/asm/miden/native_account.masm rename to crates/miden-protocol/asm/protocol/native_account.masm index f09f638c0d..197f52ba25 100644 --- a/crates/miden-lib/asm/miden/native_account.masm +++ b/crates/miden-protocol/asm/protocol/native_account.masm @@ -1,4 +1,4 @@ -use.miden::kernel_proc_offsets +use miden::protocol::kernel_proc_offsets # NATIVE ACCOUNT PROCEDURES # ================================================================================================= @@ -16,7 +16,7 @@ use.miden::kernel_proc_offsets #! transaction. #! #! Invocation: exec -export.get_id +pub proc get_id # pad the stack padw padw padw push.0.0 # => [pad(14)] @@ -52,7 +52,7 @@ end #! - the nonce has already been incremented. #! #! Invocation: exec -export.incr_nonce +pub proc incr_nonce # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -93,7 +93,7 @@ end #! #! Panics if: #! - the vault or storage delta is not empty but the nonce increment is zero. -export.compute_delta_commitment +pub proc compute_delta_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -114,25 +114,28 @@ end #! Sets an item in the native account storage. #! -#! Inputs: [index, VALUE] +#! Inputs: [slot_id_prefix, slot_id_suffix, VALUE] #! Outputs: [OLD_VALUE] #! #! Where: -#! - index is the index of the item to set. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. #! - VALUE is the value to set. #! - OLD_VALUE is the previous value of the item. #! #! Panics if: -#! - the index of the item is out of bounds. +#! - a slot with the provided slot ID does not exist in account storage. +#! - the invocation of this procedure does not originate from the native account. +#! - the native account is a faucet and the provided slot ID points to the reserved faucet storage slot. #! #! Invocation: exec -export.set_item +pub proc set_item exec.kernel_proc_offsets::account_set_item_offset - # => [offset, index, VALUE] + # => [offset, slot_id_prefix, slot_id_suffix, VALUE] # pad the stack - push.0.0 movdn.7 movdn.7 padw padw swapdw - # => [offset, index, VALUE, pad(10)] + push.0 movdn.7 padw padw swapdw + # => [offset, slot_id_prefix, slot_id_suffix, VALUE, pad(9)] syscall.exec_kernel_proc # => [OLD_VALUE, pad(12)] @@ -144,35 +147,44 @@ end #! Sets a map item in the native account storage. #! -#! Inputs: [index, KEY, VALUE] -#! Outputs: [OLD_MAP_ROOT, OLD_MAP_VALUE] +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY, VALUE] +#! Outputs: [OLD_VALUE] #! #! Where: -#! - index is the index of the map where the KEY VALUE should be set. +#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - the slot must point to the root of the storage map. #! - KEY is the key to set at VALUE. #! - VALUE is the value to set at KEY. -#! - OLD_MAP_ROOT is the old map root. -#! - OLD_MAP_VALUE is the old value at KEY. +#! - OLD_VALUE is the old value at KEY. #! #! Panics if: -#! - the index for the map is out of bounds, meaning > 255. -#! - the slot item at index is not a map. +#! - a slot with the provided slot ID does not exist in account storage. +#! - the requested storage slot type is not map. +#! - the procedure is called from a non-account context. +#! - the invocation of this procedure does not originate from the native account. #! #! Invocation: exec -export.set_map_item +pub proc set_map_item exec.kernel_proc_offsets::account_set_map_item_offset - # => [offset, index, KEY, VALUE] + # => [offset, slot_id_prefix, slot_id_suffix, KEY, VALUE] # pad the stack - push.0.0 movdn.11 movdn.11 padw movdnw.3 - # => [offset, index, KEY, VALUE, pad(6)] + push.0 padw + # => [pad(4), 0, offset, slot_id_prefix, slot_id_suffix, KEY, VALUE] + + movdnw.3 + # => [0, offset, slot_id_prefix, slot_id_suffix, KEY, VALUE, pad(4)] + + movdn.11 + # => [offset, slot_id_prefix, slot_id_suffix, KEY, VALUE, pad(5)] syscall.exec_kernel_proc - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] + # => [OLD_VALUE, pad(12)] - # clean the stack - swapdw dropw dropw - # => [OLD_MAP_ROOT, OLD_MAP_VALUE] + # drop pad(12) + movdnw.3 dropw dropw dropw + # => [OLD_VALUE] end # VAULT @@ -195,7 +207,7 @@ end #! - the vault already contains the same non-fungible asset. #! #! Invocation: exec -export.add_asset +pub proc add_asset exec.kernel_proc_offsets::account_add_asset_offset # => [offset, ASSET] @@ -225,7 +237,7 @@ end #! - the non-fungible asset is not found in the vault. #! #! Invocation: exec -export.remove_asset +pub proc remove_asset exec.kernel_proc_offsets::account_remove_asset_offset # => [offset, ASSET] @@ -258,7 +270,7 @@ end #! - was_called is 1 if the procedure was called, 0 otherwise. #! #! Invocation: exec -export.was_procedure_called +pub proc was_procedure_called exec.kernel_proc_offsets::account_was_procedure_called_offset # => [offset, PROC_ROOT] diff --git a/crates/miden-lib/asm/miden/note.masm b/crates/miden-protocol/asm/protocol/note.masm similarity index 72% rename from crates/miden-lib/asm/miden/note.masm rename to crates/miden-protocol/asm/protocol/note.masm index c4e05c4c67..1fe0195e71 100644 --- a/crates/miden-lib/asm/miden/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -1,11 +1,15 @@ -use.miden::account_id -use.std::crypto::hashes::rpo -use.std::mem +use miden::protocol::account_id +use miden::core::crypto::hashes::rpo256 +use miden::core::math::u64 +use miden::core::mem + +# Re-export the max inputs per note constant. +pub use ::miden::protocol::util::note::MAX_INPUTS_PER_NOTE # ERRORS # ================================================================================================= -const.ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT="number of note inputs exceeded the maximum limit of 128" +const ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT="number of note inputs exceeded the maximum limit of 1024" # NOTE UTILITY PROCEDURES # ================================================================================================= @@ -15,8 +19,6 @@ const.ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT="number of note inputs exceede #! This procedure checks that the provided number of note inputs is within limits and then computes #! the commitment. #! -#! Notice that the note inputs are padded with zeros in case their number is not a multiple of 8. -#! #! If the number of note inputs is 0, procedure returns the empty word: [0, 0, 0, 0]. #! #! Inputs: [inputs_ptr, num_inputs] @@ -28,33 +30,20 @@ const.ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT="number of note inputs exceede #! #! Panics if: #! - inputs_ptr is not word-aligned (i.e., is not a multiple of 4). -#! - num_inputs is greater than 128. +#! - num_inputs is greater than 1024. #! #! Invocation: exec -export.compute_inputs_commitment - # check that number of inputs is less than 128 - dup.1 push.128 u32assert2.err=ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT +pub proc compute_inputs_commitment + # check that number of inputs is less than or equal to MAX_INPUTS_PER_NOTE + dup.1 push.MAX_INPUTS_PER_NOTE u32assert2.err=ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT u32lte assert.err=ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT # => [inputs_ptr, num_inputs] - # push 1 as the pad_inputs flag: we should pad the stack while computing the note inputs - # commitment - push.1 movdn.2 - # => [inputs_ptr, num_inputs, pad_inputs_flag] - - exec.rpo::prepare_hasher_state - exec.rpo::hash_memory_with_state + # compute the inputs commitment (over the unpadded values) + exec.rpo256::hash_elements # => [INPUTS_COMMITMENT] end -#! Returns the max allowed number of input values per note. -#! -#! Stack: [] -#! Output: [max_inputs_per_note] -#! -#! - max_inputs_per_note is the max inputs per note. -export.::miden::util::note::get_max_inputs_per_note - #! Writes the assets data stored in the advice map to the memory specified by the provided #! destination pointer. #! @@ -65,7 +54,7 @@ export.::miden::util::note::get_max_inputs_per_note #! } #! Outputs: #! Operand stack: [num_assets, dest_ptr] -export.write_assets_to_memory +pub proc write_assets_to_memory # load the asset data from the advice map to the advice stack adv.push_mapval # OS => [ASSETS_COMMITMENT, num_assets, dest_ptr] @@ -119,10 +108,11 @@ end #! #! Panics if: #! - inputs_ptr is not word-aligned (i.e., is not a multiple of 4). -#! - num_inputs is greater than 128. +#! - num_inputs is greater than 1024. #! #! Invocation: exec -export.build_recipient.1 +@locals(1) +pub proc build_recipient dup.1 dup.1 # => [inputs_ptr, num_inputs, inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] @@ -142,7 +132,7 @@ export.build_recipient.1 # INPUTS_COMMITMENT, inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] # compute the advice map key for num_inputs by hashing the inputs commitment - hash + exec.rpo256::hash # => [hash(INPUTS_COMMITMENT), num_inputs_start_ptr, num_inputs_end_ptr, # INPUTS_COMMITMENT, inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] @@ -168,13 +158,13 @@ export.build_recipient.1 movdnw.2 # => [SERIAL_NUM, SCRIPT_ROOT, INPUTS_COMMITMENT] - padw adv.insert_hdword hmerge + padw adv.insert_hdword exec.rpo256::merge # => [SERIAL_HASH, SCRIPT_ROOT, INPUTS_COMMITMENT] - swapw adv.insert_hdword hmerge + swapw adv.insert_hdword exec.rpo256::merge # => [SERIAL_SCRIPT_HASH, INPUTS_COMMITMENT] - swapw adv.insert_hdword hmerge + swapw adv.insert_hdword exec.rpo256::merge # => [RECIPIENT] end @@ -190,32 +180,32 @@ end #! - RECIPIENT is the recipient of the note. #! #! Invocation: exec -export.build_recipient_hash - padw hmerge +pub proc build_recipient_hash + padw exec.rpo256::merge # => [SERIAL_NUM_HASH, SCRIPT_ROOT, INPUT_COMMITMENT] - swapw hmerge + swapw exec.rpo256::merge # => [MERGE_SCRIPT, INPUT_COMMITMENT] - swapw hmerge + swapw exec.rpo256::merge # [RECIPIENT] end -#! Extracts the sender ID from the provided metadata word. +#! Extracts the sender ID from the provided metadata header. #! -#! Inputs: [METADATA] +#! Inputs: [METADATA_HEADER] #! Outputs: [sender_id_prefix, sender_id_suffix] #! #! Where: -#! - METADATA is the metadata of some note. +#! - METADATA_HEADER is the metadata of a note. #! - sender_{prefix,suffix} are the prefix and suffix felts of the sender ID of the note which #! metadata was provided. -export.extract_sender_from_metadata - # => [aux, merged_tag_hint_payload, merged_sender_id_type_hint_tag, sender_id_prefix] +pub proc extract_sender_from_metadata + # => [attachment_kind_scheme, tag, sender_id_prefix, sender_id_suffix_and_note_type] - # drop aux felt and the felt containing tag, execution hint and payload - drop drop - # => [merged_sender_id_type_hint_tag, sender_id_prefix] + # drop attachment kind, attachment scheme and tag + drop drop swap + # => [sender_id_suffix_and_note_type, sender_id_prefix] # extract suffix of sender from merged layout, which means clearing the least significant byte exec.account_id::shape_suffix @@ -225,3 +215,34 @@ export.extract_sender_from_metadata swap # => [sender_id_prefix, sender_id_suffix] end + +#! Computes the tag for a network note for a given network account such that it is +#! picked up by the network transaction builder. +#! +#! This procedure implements the same logic as in Rust in NoteTag::from_network_account_id(). +#! Note: This procedure does not check if the account id is a network account id. +#! +#! Inputs: [account_id_prefix, account_id_suffix] +#! Outputs: [network_account_tag] +#! +#! Where: +#! - account_id_prefix, account_id_suffix is the account id to compute the note tag for. +#! - network_account_tag is the computed network note tag. +#! +#! Invocation: exec +pub proc build_note_tag_for_network_account + swap drop + # => [account_id_prefix] + + u32split + # => [a_hi, a_lo] + + push.34 + # => [b, a_hi, a_lo] + + exec.u64::shr + # => [c_hi, c_lo] + + drop + # => [network_account_tag] +end diff --git a/crates/miden-lib/asm/miden/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm similarity index 54% rename from crates/miden-lib/asm/miden/output_note.masm rename to crates/miden-protocol/asm/protocol/output_note.masm index 39c900edfc..d8898bea68 100644 --- a/crates/miden-lib/asm/miden/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -1,31 +1,45 @@ -use.miden::kernel_proc_offsets -use.miden::note +use miden::protocol::kernel_proc_offsets +use miden::protocol::note + +# CONSTANTS +# ================================================================================================= + +# Constants for note attachment kinds +pub const ATTACHMENT_KIND_NONE=0 +pub const ATTACHMENT_KIND_WORD=1 +pub const ATTACHMENT_KIND_ARRAY=2 # PROCEDURES # ================================================================================================= #! Creates a new note and returns the index of the note. #! -#! Inputs: [tag, aux, note_type, execution_hint, RECIPIENT] +#! Inputs: [tag, note_type, RECIPIENT] #! Outputs: [note_idx] #! #! Where: #! - tag is the tag to be included in the note. -#! - aux is the auxiliary metadata to be included in the note. #! - note_type is the storage type of the note. -#! - execution_hint is the note's execution hint. #! - RECIPIENT is the recipient of the note. #! - note_idx is the index of the created note. #! +#! Panics if: +#! - the note_type is unknown. +#! - the note tag is not a u32. +#! - the number of output notes exceeds the maximum limit of 1024. +#! #! Invocation: exec -export.create +pub proc create # pad the stack before the syscall to prevent accidental modification of the deeper stack # elements - padw padw swapdw movup.8 drop - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] + push.0 movdn.6 push.0 + # => [0, tag, note_type, RECIPIENT, 0] + + padw padw swapdw drop + # => [tag, note_type, RECIPIENT, pad(9)] exec.kernel_proc_offsets::output_note_create_offset - # => [offset, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] + # => [offset, tag, note_type, RECIPIENT, pad(9)] syscall.exec_kernel_proc # => [note_idx, pad(15)] @@ -51,7 +65,7 @@ end #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: exec -export.get_assets_info +pub proc get_assets_info # start padding the stack push.0.0 movup.2 # => [note_index, 0, 0] @@ -97,7 +111,7 @@ end #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: exec -export.get_assets +pub proc get_assets # get the assets commitment and assets number dup.1 exec.get_assets_info # => [ASSETS_COMMITMENT, num_assets, dest_ptr, note_index] @@ -117,7 +131,7 @@ end #! - ASSET can be a fungible or non-fungible asset. #! #! Invocation: exec -export.add_asset +pub proc add_asset movup.4 exec.kernel_proc_offsets::output_note_add_asset_offset # => [offset, note_idx, ASSET] @@ -134,6 +148,106 @@ export.add_asset # => [] end +#! Sets the attachment of the note specified by the index. +#! +#! If attachment_kind == Array, there must be an advice map entry for ATTACHMENT (see below). +#! +#! Inputs: +#! Operand Stack: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] +#! Advice map: { +#! ATTACHMENT?: [[ATTACHMENT_ELEMENTS]], +#! } +#! Outputs: [] +#! +#! Where: +#! - note_idx is the index of the note on which the attachment is set. +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - attachment_kind is the kind of the attachment content. +#! - ATTACHMENT is the attachment to be set. +#! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment (only +#! needed if attachment_kind == Array). +#! +#! Panics if: +#! - the procedure is called when the active account is not the native one. +#! - the note index points to a non-existent output note. +#! - the attachment kind or scheme does not fit into a u32. +#! - the attachment kind is an unknown variant. +#! +#! Invocation: exec +pub proc set_attachment + exec.kernel_proc_offsets::output_note_set_attachment_offset + # => [offset, note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + + # pad the stack before the syscall + padw padw swapdw + # => [offset, note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(8)] + + syscall.exec_kernel_proc + # => [pad(16)] + + # remove excess PADs from the stack + dropw dropw dropw dropw + # => [] +end + +#! Sets the attachment of the note specified by the note index to the provided word. +#! +#! This overwrites any previously set attachment. +#! +#! Inputs: [note_idx, attachment_scheme, ATTACHMENT] +#! Outputs: [] +#! +#! Where: +#! - note_idx is the index of the note on which the attachment is set. +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - ATTACHMENT is the raw attachment to set. +#! +#! Panics if: +#! - the procedure is called when the active account is not the native one. +#! - the note index points to a non-existent output note. +#! - the attachment_scheme does not fit into a u32. +#! +#! Invocation: exec +pub proc set_word_attachment + push.ATTACHMENT_KIND_WORD movdn.2 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + + exec.set_attachment + # => [] +end + +#! Sets the attachment of the note specified by the note index to the provided ATTACHMENT which +#! commits to an array of felts. +#! +#! This overwrites any previously set attachment. +#! +#! Inputs: +#! Operand Stack: [note_idx, attachment_scheme, ATTACHMENT] +#! Advice map: { +#! ATTACHMENT: [[ATTACHMENT_ELEMENTS]], +#! } +#! Outputs: [] +#! +#! Where: +#! - note_idx is the index of the note on which the attachment is set. +#! - attachment_scheme is the user-defined scheme of the attachment. +#! - ATTACHMENT is the commitment of the set of elements that form the note attachment. +#! - ATTACHMENT_ELEMENTS are the elements for which ATTACHMENT is the sequential commitment. +#! +#! Panics if: +#! - the procedure is called when the active account is not the native one. +#! - the note index points to a non-existent output note. +#! - the attachment_scheme does not fit into a u32. +#! +#! Invocation: exec +pub proc set_array_attachment + push.ATTACHMENT_KIND_ARRAY movdn.2 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + + exec.set_attachment + # => [] +end + #! Returns the recipient of the output note with the specified index. #! #! Inputs: [note_index] @@ -147,7 +261,7 @@ end #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: exec -export.get_recipient +pub proc get_recipient # start padding the stack push.0.0 movup.2 # => [note_index, 0, 0] @@ -170,17 +284,18 @@ end #! Returns the metadata of the output note with the specified index. #! #! Inputs: [note_index] -#! Outputs: [METADATA] +#! Outputs: [NOTE_ATTACHMENT, METADATA_HEADER] #! #! Where: #! - note_index is the index of the output note whose metadata should be returned. -#! - METADATA is the metadata of the output note. +#! - METADATA_HEADER is the metadata header of the specified output note. +#! - NOTE_ATTACHMENT is the attachment of the specified output note. #! #! Panics if: #! - the note index is greater or equal to the total number of output notes. #! #! Invocation: exec -export.get_metadata +pub proc get_metadata # start padding the stack push.0.0 movup.2 # => [note_index, 0, 0] @@ -193,9 +308,9 @@ export.get_metadata # => [offset, note_index, pad(14)] syscall.exec_kernel_proc - # => [METADATA, pad(12)] + # => [NOTE_ATTACHMENT, METADATA_HEADER, pad(8)] # clean the stack - swapdw dropw dropw swapw dropw - # => [METADATA] + swapdw dropw dropw + # => [NOTE_ATTACHMENT, METADATA_HEADER] end diff --git a/crates/miden-lib/asm/miden/tx.masm b/crates/miden-protocol/asm/protocol/tx.masm similarity index 96% rename from crates/miden-lib/asm/miden/tx.masm rename to crates/miden-protocol/asm/protocol/tx.masm index a6890e1fe4..b55b354ebb 100644 --- a/crates/miden-lib/asm/miden/tx.masm +++ b/crates/miden-protocol/asm/protocol/tx.masm @@ -1,4 +1,4 @@ -use.miden::kernel_proc_offsets +use miden::protocol::kernel_proc_offsets #! Returns the block number of the transaction reference block. #! @@ -9,7 +9,7 @@ use.miden::kernel_proc_offsets #! - num is the transaction reference block number. #! #! Invocation: exec -export.get_block_number +pub proc get_block_number # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -34,7 +34,7 @@ end #! - BLOCK_COMMITMENT is the commitment to the reference block of the transaction. #! #! Invocation: exec -export.get_block_commitment +pub proc get_block_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -77,7 +77,7 @@ end #! Where: #! - timestamp is the timestamp of the reference block for this transaction. The underlying value is #! of type u32, so u32 operations can be safely used on it. -export.get_block_timestamp +pub proc get_block_timestamp # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -104,7 +104,7 @@ end #! - INPUT_NOTES_COMMITMENT is the input notes commitment hash. #! #! Invocation: exec -export.get_input_notes_commitment +pub proc get_input_notes_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -130,7 +130,7 @@ end #! - OUTPUT_NOTES_COMMITMENT is the output notes commitment. #! #! Invocation: exec -export.get_output_notes_commitment +pub proc get_output_notes_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -155,7 +155,7 @@ end #! - num_input_notes is the total number of input notes consumed by this transaction. #! #! Invocation: exec -export.get_num_input_notes +pub proc get_num_input_notes # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -183,7 +183,7 @@ end #! - num_output_notes is the number of output notes created in this transaction so far. #! #! Invocation: exec -export.get_num_output_notes +pub proc get_num_output_notes # pad the stack padw padw padw push.0.0.0 # => [pad(15)] @@ -215,7 +215,8 @@ end #! moment of the foreign procedure execution (n = 16 - mem_addr_size - foreign_inputs_len). #! #! Invocation: exec -export.execute_foreign_procedure.4 +@locals(4) +pub proc execute_foreign_procedure # get the start_foreign_context procedure offset push.0 movup.2 movup.2 exec.kernel_proc_offsets::tx_start_foreign_context_offset # => [offset, foreign_account_id_prefix, foreign_account_id_suffix, 0, FOREIGN_PROC_ROOT, , pad(n)] @@ -268,7 +269,7 @@ end #! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). #! #! Annotation hint: is not used anywhere -export.update_expiration_block_delta +pub proc update_expiration_block_delta exec.kernel_proc_offsets::tx_update_expiration_block_delta_offset # => [offset, expiration_delta, ...] @@ -291,7 +292,7 @@ end #! - block_height_delta is the stored expiration time delta (1 to 0xFFFF). #! #! Annotation hint: is not used anywhere -export.get_expiration_block_delta +pub proc get_expiration_block_delta # pad the stack padw padw padw push.0.0.0 # => [pad(15)] diff --git a/crates/miden-lib/asm/shared_modules/account_id.masm b/crates/miden-protocol/asm/shared_modules/account_id.masm similarity index 88% rename from crates/miden-lib/asm/shared_modules/account_id.masm rename to crates/miden-protocol/asm/shared_modules/account_id.masm index 27ab23b767..4dcf7d4e11 100644 --- a/crates/miden-lib/asm/shared_modules/account_id.masm +++ b/crates/miden-protocol/asm/shared_modules/account_id.masm @@ -1,52 +1,50 @@ # ERRORS # ================================================================================================= -const.ERR_ACCOUNT_ID_UNKNOWN_VERSION="unknown version in account ID" +const ERR_ACCOUNT_ID_UNKNOWN_VERSION="unknown version in account ID" -const.ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO="most significant bit of the account ID suffix must be zero" +const ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO="most significant bit of the account ID suffix must be zero" -const.ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE="unknown account storage mode in account ID" +const ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE="unknown account storage mode in account ID" -const.ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO="least significant byte of the account ID suffix must be zero" +const ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO="least significant byte of the account ID suffix must be zero" -const.ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS="provided storage slot index is out of bounds" - -const.ERR_ACCOUNT_ID_NON_PUBLIC_NETWORK_ACCOUNT="the account ID must have storage mode public if the network flag is set" +const ERR_ACCOUNT_ID_NON_PUBLIC_NETWORK_ACCOUNT="the account ID must have storage mode public if the network flag is set" # CONSTANTS # ================================================================================================= # Bit pattern for a faucet account, after the account type mask has been applied. -const.FAUCET_ACCOUNT=0x20 # 0b10_0000 +const FAUCET_ACCOUNT=0x20 # 0b10_0000 # Bit pattern for an account w/ updatable code, after the account type mask has been applied. -const.REGULAR_ACCOUNT_UPDATABLE_CODE=0x10 # 0b01_0000 +const REGULAR_ACCOUNT_UPDATABLE_CODE=0x10 # 0b01_0000 # Bit pattern for an account w/ immutable code, after the account type mask has been applied. -const.REGULAR_ACCOUNT_IMMUTABLE_CODE=0 # 0b00_0000 +const REGULAR_ACCOUNT_IMMUTABLE_CODE=0 # 0b00_0000 # Bit pattern for a fungible faucet w/ immutable code, after the account type mask has been applied. -const.FUNGIBLE_FAUCET_ACCOUNT=0x20 # 0b10_0000 +const FUNGIBLE_FAUCET_ACCOUNT=0x20 # 0b10_0000 # Bit pattern for a non-fungible faucet w/ immutable code, after the account type mask has been # applied. -const.NON_FUNGIBLE_FAUCET_ACCOUNT=0x30 # 0b11_0000 +const NON_FUNGIBLE_FAUCET_ACCOUNT=0x30 # 0b11_0000 # Given the least significant 32 bits of an account id's prefix, this mask defines the bits used # to determine the account type. -const.ACCOUNT_ID_TYPE_MASK_U32=0x30 # 0b11_0000 +const ACCOUNT_ID_TYPE_MASK_U32=0x30 # 0b11_0000 # Given the least significant 32 bits of an account id's prefix, this mask defines the bits used # to determine the account version. -const.ACCOUNT_VERSION_MASK_U32=0x0f # 0b1111 +const ACCOUNT_VERSION_MASK_U32=0x0f # 0b1111 # Given the least significant 32 bits of an account ID's prefix, this mask defines the bits used # to determine the account storage mode. -const.ACCOUNT_ID_STORAGE_MODE_MASK_U32=0xC0 # 0b1100_0000 +const ACCOUNT_ID_STORAGE_MODE_MASK_U32=0xC0 # 0b1100_0000 # Given the least significant 32 bits of an account ID's first felt with the storage mode mask # applied, this value defines the non-existent, invalid storage mode. -const.ACCOUNT_ID_STORAGE_MODE_INVALID_U32=0xc0 # 0b1100_0000 +const ACCOUNT_ID_STORAGE_MODE_INVALID_U32=0xc0 # 0b1100_0000 # PROCEDURES # ================================================================================================= @@ -59,7 +57,7 @@ const.ACCOUNT_ID_STORAGE_MODE_INVALID_U32=0xc0 # 0b1100_0000 #! Where: #! - acct_id_prefix is the prefix of the account ID. #! - is_fungible_faucet is a boolean indicating whether the account is a fungible faucet. -export.is_fungible_faucet +pub proc is_fungible_faucet exec.id_type eq.FUNGIBLE_FAUCET_ACCOUNT # => [is_fungible_faucet] end @@ -72,7 +70,7 @@ end #! Where: #! - acct_id_prefix is the prefix of the account ID. #! - is_non_fungible_faucet is a boolean indicating whether the account is a non-fungible faucet. -export.is_non_fungible_faucet +pub proc is_non_fungible_faucet exec.id_type eq.NON_FUNGIBLE_FAUCET_ACCOUNT # => [is_non_fungible_faucet] end @@ -87,7 +85,7 @@ end #! - other_acct_id_{prefix,suffix} are the prefix and suffix felts of the other account ID to #! compare against. #! - is_id_equal is a boolean indicating whether the account IDs are equal. -export.is_equal +pub proc is_equal movup.2 eq # => [is_prefix_equal, acct_id_suffix, other_acct_id_suffix] movdn.2 eq @@ -104,7 +102,7 @@ end #! Where: #! - acct_id_prefix is the prefix of the account ID. #! - is_faucet is a boolean indicating whether the account is a faucet. -export.is_faucet +pub proc is_faucet u32split drop u32and.FAUCET_ACCOUNT neq.0 # => [is_faucet] end @@ -118,7 +116,7 @@ end #! - acct_id_prefix is the prefix of the account ID. #! - is_updatable_account is a boolean indicating whether the account is a regular updatable #! account. -export.is_updatable_account +pub proc is_updatable_account exec.id_type eq.REGULAR_ACCOUNT_UPDATABLE_CODE # => [is_updatable_account] end @@ -132,7 +130,7 @@ end #! - acct_id_prefix is the prefix of the account ID. #! - is_immutable_account is a boolean indicating whether the account is a regular immutable #! account. -export.is_immutable_account +pub proc is_immutable_account exec.id_type eq.REGULAR_ACCOUNT_IMMUTABLE_CODE # => [is_immutable_account] end @@ -151,7 +149,7 @@ end #! - account_id_prefix does not contain either the public, network or private storage mode. #! - account_id_suffix does not have its most significant bit set to zero. #! - account_id_suffix does not have its lower 8 bits set to zero. -export.validate +pub proc validate # Validate version in prefix. For now only version 0 is supported. # --------------------------------------------------------------------------------------------- @@ -203,7 +201,7 @@ end #! - seed_digest_suffix is the suffix of the digest that should be shaped into the suffix #! of an account ID. #! - account_id_suffix is the suffix of an account ID. -export.shape_suffix +pub proc shape_suffix u32split swap # => [seed_digest_suffix_lo, seed_digest_suffix_hi] @@ -227,7 +225,7 @@ end #! Where: #! - account_id_prefix is the prefix of an account ID. #! - id_version is the version number of the ID. -proc.id_version +proc id_version # extract the lower 32 bits u32split drop # => [account_id_prefix_lo] @@ -251,7 +249,7 @@ end #! Where: #! - acct_id_prefix is the prefix of the account ID. #! - acct_type is the account type. -proc.id_type +proc id_type u32split drop u32and.ACCOUNT_ID_TYPE_MASK_U32 # => [acct_type] end diff --git a/crates/miden-lib/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm similarity index 88% rename from crates/miden-lib/asm/shared_utils/util/asset.masm rename to crates/miden-protocol/asm/shared_utils/util/asset.masm index 9d16b342f2..225ed0366d 100644 --- a/crates/miden-lib/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -4,7 +4,7 @@ # Specifies the maximum amount a fungible asset can represent. # # This is 2^63 - 2^31. See account_delta.masm for more details. -const.FUNGIBLE_ASSET_MAX_AMOUNT=0x7fffffff80000000 +const FUNGIBLE_ASSET_MAX_AMOUNT=0x7fffffff80000000 # PROCEDURES # ================================================================================================= @@ -16,7 +16,7 @@ const.FUNGIBLE_ASSET_MAX_AMOUNT=0x7fffffff80000000 #! #! Where: #! - fungible_asset_max_amount is the maximum amount of a fungible asset. -export.get_fungible_asset_max_amount +pub proc get_fungible_asset_max_amount push.FUNGIBLE_ASSET_MAX_AMOUNT # => [fungible_asset_max_amount] end @@ -31,7 +31,7 @@ end #! Where: #! - ASSET is the fungible asset from which to extract the balance. #! - balance is the amount of the fungible asset. -export.get_balance_from_fungible_asset +pub proc get_balance_from_fungible_asset drop drop drop # => [balance] end diff --git a/crates/miden-protocol/asm/shared_utils/util/note.masm b/crates/miden-protocol/asm/shared_utils/util/note.masm new file mode 100644 index 0000000000..8c502a8f71 --- /dev/null +++ b/crates/miden-protocol/asm/shared_utils/util/note.masm @@ -0,0 +1,5 @@ +# CONSTANTS +# ================================================================================================= + +# The maximum number of input values associated with a single note. +pub const MAX_INPUTS_PER_NOTE = 1024 diff --git a/crates/miden-objects/benches/account_seed.rs b/crates/miden-protocol/benches/account_seed.rs similarity index 90% rename from crates/miden-objects/benches/account_seed.rs rename to crates/miden-protocol/benches/account_seed.rs index 8e37e84228..77f6c107a2 100644 --- a/crates/miden-objects/benches/account_seed.rs +++ b/crates/miden-protocol/benches/account_seed.rs @@ -1,8 +1,8 @@ use std::time::Duration; use criterion::{Criterion, criterion_group, criterion_main}; -use miden_objects::Word; -use miden_objects::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; +use miden_protocol::Word; +use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use rand::{Rng, SeedableRng}; /// Running this benchmark with --no-default-features will use the single-threaded account seed @@ -13,7 +13,7 @@ use rand::{Rng, SeedableRng}; /// To produce a flamegraph, run with the `--profile-time` argument. /// /// ```sh -/// cargo bench -p miden-objects --no-default-features -- --profile-time 10 +/// cargo bench -p miden-protocol --no-default-features -- --profile-time 10 /// ``` /// /// The flamegraph will be saved as `target/criterion/grind-seed/Grind regular public account diff --git a/crates/miden-protocol/build.rs b/crates/miden-protocol/build.rs new file mode 100644 index 0000000000..a15237e295 --- /dev/null +++ b/crates/miden-protocol/build.rs @@ -0,0 +1,815 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::env; +use std::path::Path; +use std::sync::Arc; + +use fs_err as fs; +use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr, miette}; +use miden_assembly::{Assembler, DefaultSourceManager, KernelLibrary, Library}; +use regex::Regex; +use walkdir::WalkDir; + +// 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_PROTOCOL_DIR: &str = "protocol"; + +const SHARED_UTILS_DIR: &str = "shared_utils"; +const SHARED_MODULES_DIR: &str = "shared_modules"; +const ASM_TX_KERNEL_DIR: &str = "kernels/transaction"; +const KERNEL_PROCEDURES_RS_FILE: &str = "src/transaction/kernel/procedures.rs"; + +const PROTOCOL_LIB_NAMESPACE: &str = "miden::protocol"; + +const TX_KERNEL_ERRORS_FILE: &str = "src/errors/tx_kernel.rs"; +const PROTOCOL_LIB_ERRORS_FILE: &str = "src/errors/protocol.rs"; + +const TX_KERNEL_ERRORS_ARRAY_NAME: &str = "TX_KERNEL_ERRORS"; +const PROTOCOL_LIB_ERRORS_ARRAY_NAME: &str = "PROTOCOL_LIB_ERRORS"; + +const TX_KERNEL_ERROR_CATEGORIES: [&str; 14] = [ + "KERNEL", + "PROLOGUE", + "EPILOGUE", + "TX", + "NOTE", + "ACCOUNT", + "FOREIGN_ACCOUNT", + "FAUCET", + "FUNGIBLE_ASSET", + "NON_FUNGIBLE_ASSET", + "VAULT", + "LINK_MAP", + "INPUT_NOTE", + "OUTPUT_NOTE", +]; + +// PRE-PROCESSING +// ================================================================================================ + +/// Read and parse the contents from `./asm`. +/// - Compiles the contents of asm/protocol directory into a Protocol library file (.masl) under +/// miden::protocol namespace. +/// - Compiles the contents of asm/kernels into the transaction kernel library. +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"); + + // 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); + + // copy the shared modules to the kernel and protocol library folders + copy_shared_modules(&source_dir)?; + + // set target directory to {OUT_DIR}/assets + let target_dir = Path::new(&build_dir).join(ASSETS_DIR); + + // compile transaction kernel + let mut assembler = + compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?; + + // compile protocol library + let protocol_lib = compile_protocol_lib(&source_dir, &target_dir, assembler.clone())?; + assembler.link_dynamic_library(protocol_lib)?; + + generate_error_constants(&source_dir)?; + + generate_event_constants(&source_dir, &target_dir)?; + + Ok(()) +} + +// COMPILE TRANSACTION KERNEL +// ================================================================================================ + +/// Reads the transaction kernel MASM source from the `source_dir`, compiles it, saves the results +/// to the `target_dir`, and returns an [Assembler] instantiated with the compiled kernel. +/// +/// Additionally it compiles the transaction script executor program, see the +/// [compile_tx_script_main] procedure for details. +/// +/// `source_dir` is expected to have the following structure: +/// +/// - {source_dir}/api.masm -> defines exported procedures from the transaction kernel. +/// - {source_dir}/main.masm -> defines the executable program of the transaction kernel. +/// - {source_dir}/tx_script_main -> defines the executable program of the arbitrary transaction +/// script. +/// - {source_dir}/lib -> contains common modules used by both api.masm and main.masm. +/// +/// The compiled files are written as follows: +/// +/// - {target_dir}/tx_kernel.masl -> contains kernel library compiled from api.masm. +/// - {target_dir}/tx_kernel.masb -> contains the executable compiled from main.masm. +/// - {target_dir}/tx_script_main.masb -> contains the executable compiled from +/// tx_script_main.masm. +/// - src/transaction/procedures/kernel_v0.rs -> contains the kernel procedures table. +fn compile_tx_kernel(source_dir: &Path, target_dir: &Path) -> Result { + let shared_utils_path = std::path::Path::new(ASM_DIR).join(SHARED_UTILS_DIR); + let kernel_path = miden_assembly::Path::kernel_path(); + + let mut assembler = build_assembler(None)?; + // add the shared util modules to the kernel lib under the ::$kernel::util namespace + assembler.compile_and_statically_link_from_dir(&shared_utils_path, kernel_path)?; + + // assemble the kernel library and write it to the "tx_kernel.masl" file + let kernel_lib = assembler + .assemble_kernel_from_dir(source_dir.join("api.masm"), Some(source_dir.join("lib")))?; + + // generate kernel `procedures.rs` file + generate_kernel_proc_hash_file(kernel_lib.clone())?; + + let output_file = target_dir.join("tx_kernel").with_extension(Library::LIBRARY_EXTENSION); + kernel_lib.write_to_file(output_file).into_diagnostic()?; + + let assembler = build_assembler(Some(kernel_lib))?; + + // assemble the kernel program and write it to the "tx_kernel.masb" file + let mut main_assembler = assembler.clone(); + // add the shared util modules to the kernel lib under the ::$kernel::util namespace + main_assembler.compile_and_statically_link_from_dir(&shared_utils_path, kernel_path)?; + main_assembler.compile_and_statically_link_from_dir(source_dir.join("lib"), kernel_path)?; + + let main_file_path = source_dir.join("main.masm"); + let kernel_main = main_assembler.clone().assemble_program(main_file_path)?; + + let masb_file_path = target_dir.join("tx_kernel.masb"); + kernel_main.write_to_file(masb_file_path).into_diagnostic()?; + + // compile the transaction script main program + compile_tx_script_main(source_dir, target_dir, main_assembler)?; + + #[cfg(any(feature = "testing", test))] + { + let mut kernel_lib_assembler = assembler.clone(); + // Build kernel as a library and save it to file. + // This is needed in test assemblers to access individual procedures which would otherwise + // be hidden when using KernelLibrary (api.masm) + + // add the shared util modules to the kernel lib under the ::$kernel::util namespace + kernel_lib_assembler + .compile_and_statically_link_from_dir(&shared_utils_path, kernel_path)?; + + let test_lib = kernel_lib_assembler + .assemble_library_from_dir(source_dir.join("lib"), kernel_path) + .unwrap(); + + let masb_file_path = + target_dir.join("kernel_library").with_extension(Library::LIBRARY_EXTENSION); + test_lib.write_to_file(masb_file_path).into_diagnostic()?; + } + + Ok(assembler) +} + +/// Reads the transaction script executor MASM source from the `source_dir/tx_script_main.masm`, +/// compiles it and saves the results to the `target_dir` as a `tx_script_main.masb` binary file. +fn compile_tx_script_main( + source_dir: &Path, + target_dir: &Path, + main_assembler: Assembler, +) -> Result<()> { + // assemble the transaction script executor program and write it to the "tx_script_main.masb" + // file. + let tx_script_main_file_path = source_dir.join("tx_script_main.masm"); + let tx_script_main = main_assembler.assemble_program(tx_script_main_file_path)?; + + let masb_file_path = target_dir.join("tx_script_main.masb"); + tx_script_main.write_to_file(masb_file_path).into_diagnostic() +} + +/// Generates kernel `procedures.rs` file based on the kernel library +fn generate_kernel_proc_hash_file(kernel: KernelLibrary) -> Result<()> { + // Because the kernel Rust file will be stored under ./src, this should be a no-op if we can't + // write there + if !BUILD_GENERATED_FILES_IN_SRC { + return Ok(()); + } + + let (_, module_info, _) = kernel.into_parts(); + + let to_exclude = BTreeSet::from_iter(["exec_kernel_proc"]); + let offsets_filename = + Path::new(ASM_DIR).join(ASM_PROTOCOL_DIR).join("kernel_proc_offsets.masm"); + let offsets = parse_proc_offsets(&offsets_filename)?; + + let generated_procs: BTreeMap = module_info + .procedures() + .filter(|(_, proc_info)| !to_exclude.contains::(proc_info.name.as_ref())) + .map(|(_, proc_info)| { + let name = proc_info.name.to_string(); + + let Some(&offset) = offsets.get(&name) else { + panic!("Offset constant for function `{name}` not found in `{offsets_filename:?}`"); + }; + + (offset, format!(" // {name}\n word!(\"{}\"),", proc_info.digest)) + }) + .collect(); + + let proc_count = generated_procs.len(); + let generated_procs: String = generated_procs.into_iter().enumerate().map(|(index, (offset, txt))| { + if index != offset { + panic!("Offset constants in the file `{offsets_filename:?}` are not contiguous (missing offset: {index})"); + } + + txt + }).collect::>().join("\n"); + + fs::write( + KERNEL_PROCEDURES_RS_FILE, + format!( + r#"// This file is generated by build.rs, do not modify + +use crate::{{Word, word}}; + +// KERNEL PROCEDURES +// ================================================================================================ + +/// Hashes of all dynamically executed kernel procedures. +pub const KERNEL_PROCEDURES: [Word; {proc_count}] = [ +{generated_procs} +]; +"#, + ), + ) + .into_diagnostic() +} + +fn parse_proc_offsets(filename: impl AsRef) -> Result> { + let regex: Regex = Regex::new(r"^const\s*(?P\w+)_OFFSET\s*=\s*(?P\d+)").unwrap(); + let mut result = BTreeMap::new(); + for line in fs::read_to_string(filename).into_diagnostic()?.lines() { + if let Some(captures) = regex.captures(line) { + result.insert( + captures["name"].to_string().to_lowercase(), + captures["offset"].parse().into_diagnostic()?, + ); + } + } + + Ok(result) +} + +// COMPILE PROTOCOL LIB +// ================================================================================================ + +/// Reads the MASM files from "{source_dir}/protocol" directory, compiles them into a Miden assembly +/// library, saves the library into "{target_dir}/protocol.masl", and returns the compiled library. +fn compile_protocol_lib( + source_dir: &Path, + target_dir: &Path, + mut assembler: Assembler, +) -> Result { + let source_dir = source_dir.join(ASM_PROTOCOL_DIR); + let shared_path = Path::new(ASM_DIR).join(SHARED_UTILS_DIR); + + // add the shared modules to the protocol lib under the miden::protocol::util namespace + // note that this module is not publicly exported, it is only available for linking the library + // itself + assembler.compile_and_statically_link_from_dir(&shared_path, PROTOCOL_LIB_NAMESPACE)?; + + let protocol_lib = assembler.assemble_library_from_dir(source_dir, PROTOCOL_LIB_NAMESPACE)?; + + let output_file = target_dir.join("protocol").with_extension(Library::LIBRARY_EXTENSION); + protocol_lib.write_to_file(output_file).into_diagnostic()?; + + Ok(protocol_lib) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Returns a new [Assembler] loaded with miden-core-lib and the specified kernel, if provided. +fn build_assembler(kernel: Option) -> Result { + kernel + .map(|kernel| Assembler::with_kernel(Arc::new(DefaultSourceManager::default()), kernel)) + .unwrap_or_default() + .with_dynamic_library(miden_core_lib::CoreLibrary::default()) +} + +/// Copies the content of the build `shared_modules` folder to the `lib` and `protocol` build +/// folders. This is required to include the shared modules as APIs of the `kernel` and `protocol` +/// libraries. +/// +/// This is done to make it possible to import the modules in the `shared_modules` folder directly, +/// i.e. "use $kernel::account_id". +fn copy_shared_modules>(source_dir: T) -> Result<()> { + // source is expected to be an `OUT_DIR/asm` folder + let shared_modules_dir = source_dir.as_ref().join(SHARED_MODULES_DIR); + + for module_path in shared::get_masm_files(shared_modules_dir).unwrap() { + let module_name = module_path.file_name().unwrap(); + + // copy to kernel lib + let kernel_lib_folder = source_dir.as_ref().join(ASM_TX_KERNEL_DIR).join("lib"); + fs::copy(&module_path, kernel_lib_folder.join(module_name)).into_diagnostic()?; + + // copy to protocol lib + let protocol_lib_folder = source_dir.as_ref().join(ASM_PROTOCOL_DIR); + fs::copy(&module_path, protocol_lib_folder.join(module_name)).into_diagnostic()?; + } + + Ok(()) +} + +// ERROR CONSTANTS FILE GENERATION +// ================================================================================================ + +/// Reads all MASM files from the `asm_source_dir` and extracts its error constants and their +/// associated error message and generates a Rust file for each category of errors. +/// For example: +/// +/// ```text +/// const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY="new account must have an empty vault" +/// ``` +/// +/// would generate a Rust file for transaction kernel errors (since the error belongs to that +/// category, identified by the category extracted from `ERR_`) with - roughly - the +/// following content: +/// +/// ```rust +/// pub const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY: MasmError = +/// MasmError::from_static_str("new account must have an empty vault"); +/// ``` +/// +/// and add the constant to the error constants array. +/// +/// 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(()); + } + + // Transaction kernel errors + // ------------------------------------------ + + let tx_kernel_dir = asm_source_dir.join(ASM_TX_KERNEL_DIR); + let errors = shared::extract_all_masm_errors(&tx_kernel_dir) + .context("failed to extract all masm errors")?; + validate_tx_kernel_category(&errors)?; + + shared::generate_error_file( + shared::ErrorModule { + file_name: TX_KERNEL_ERRORS_FILE, + array_name: TX_KERNEL_ERRORS_ARRAY_NAME, + is_crate_local: true, + }, + errors, + )?; + + // Miden protocol library errors + // ------------------------------------------ + + let protocol_dir = asm_source_dir.join(ASM_PROTOCOL_DIR); + let errors = shared::extract_all_masm_errors(&protocol_dir) + .context("failed to extract all masm errors")?; + + shared::generate_error_file( + shared::ErrorModule { + file_name: PROTOCOL_LIB_ERRORS_FILE, + array_name: PROTOCOL_LIB_ERRORS_ARRAY_NAME, + is_crate_local: true, + }, + errors, + )?; + + Ok(()) +} + +/// Validates that all error names in the provided slice start with a known tx kernel error +/// category. +fn validate_tx_kernel_category(errors: &[shared::NamedError]) -> Result<()> { + for error in errors { + if !TX_KERNEL_ERROR_CATEGORIES + .iter() + .any(|known_category| error.name.starts_with(known_category)) + { + return Err(miette::miette!( + "error `{}` does not start with a known tx kernel error category", + error.name + )); + } + } + + Ok(()) +} + +// EVENT CONSTANTS FILE GENERATION +// ================================================================================================ + +/// Reads all MASM files from the `asm_source_dir` and extracts event definitions, +/// then generates the transaction_events.rs file with constants. +fn generate_event_constants(asm_source_dir: &Path, target_dir: &Path) -> Result<()> { + // Extract all event definitions from MASM files + let mut events = extract_all_event_definitions(asm_source_dir)?; + + // Add two additional events we want in `TransactionEventId` that do not appear in kernel or + // protocol lib modules. + events.insert("miden::auth::request".to_owned(), "AUTH_REQUEST".to_owned()); + events.insert("miden::auth::unauthorized".to_owned(), "AUTH_UNAUTHORIZED".to_owned()); + + // Generate the events file in OUT_DIR + let event_file_content = generate_event_file_content(&events).into_diagnostic()?; + let event_file_path = target_dir.join("transaction_events.rs"); + fs::write(event_file_path, event_file_content).into_diagnostic()?; + + Ok(()) +} + +/// Extract all `const X=event("x")` definitions from all MASM files +fn extract_all_event_definitions(asm_source_dir: &Path) -> Result> { + // collect mappings event path to const variable name, we want a unique mapping + // which we use to generate the constants and enum variant names + let mut events = BTreeMap::new(); + + // Walk all MASM files + for entry in WalkDir::new(asm_source_dir) { + let entry = entry.into_diagnostic()?; + if !shared::is_masm_file(entry.path()).into_diagnostic()? { + continue; + } + let file_contents = fs::read_to_string(entry.path()).into_diagnostic()?; + extract_event_definitions_from_file(&mut events, &file_contents, entry.path())?; + } + + Ok(events) +} + +/// Extract event definitions from a single MASM file in form of `const ${X} = event("${x::path}")`. +fn extract_event_definitions_from_file( + events: &mut BTreeMap, + file_contents: &str, + file_path: &Path, +) -> Result<()> { + let regex = Regex::new(r#"const\s*(\w+)\s*=\s*event\("([^"]+)"\)"#).unwrap(); + + for capture in regex.captures_iter(file_contents) { + let const_name = capture.get(1).expect("const name should be captured"); + let event_path = capture.get(2).expect("event path should be captured"); + + let event_path = event_path.as_str(); + let const_name = const_name.as_str(); + + let const_name_wo_suffix = + if let Some((const_name_wo_suffix, _)) = const_name.rsplit_once("_EVENT") { + const_name_wo_suffix.to_string() + } else { + const_name.to_owned() + }; + + if !event_path.starts_with("miden::") { + return Err(miette::miette!("unhandled `event_path={event_path}`")); + } + + // Check for duplicates with different definitions + if let Some(existing_const_name) = events.get(event_path) { + if existing_const_name != &const_name_wo_suffix { + println!( + "cargo:warning=Duplicate event definition found {event_path} with different definitions names: + '{existing_const_name}' vs '{const_name}' in {}", + file_path.display() + ); + } + } else { + events.insert(event_path.to_owned(), const_name_wo_suffix.to_owned()); + } + } + + Ok(()) +} + +/// Generate the content of the transaction_events.rs file +fn generate_event_file_content( + events: &BTreeMap, +) -> std::result::Result { + use std::fmt::Write; + + let mut output = String::new(); + + writeln!(&mut output, "// This file is generated by build.rs, do not modify")?; + writeln!(&mut output)?; + + // Generate constants + // + // Note: If we ever encounter two constants `const X`, that are both named `X` we will error + // when attempting to generate the rust code. Currently this is a side-effect, but we + // want to error out as early as possible: + // TODO: make the error out at build-time to be able to present better error hints + for (event_path, event_name) in events { + let value = miden_core::EventId::from_name(event_path).as_felt().as_int(); + debug_assert!(!event_name.is_empty()); + writeln!(&mut output, "const {}: u64 = {};", event_name, value)?; + } + + { + writeln!(&mut output)?; + + writeln!(&mut output)?; + + writeln!( + &mut output, + r###" +use alloc::collections::BTreeMap; + +pub(crate) static EVENT_NAME_LUT: ::miden_utils_sync::LazyLock> = + ::miden_utils_sync::LazyLock::new(|| {{ + BTreeMap::from_iter([ +"### + )?; + + for (event_path, const_name) in events { + writeln!(&mut output, " ({}, \"{}\"),", const_name, event_path)?; + } + + writeln!( + &mut output, + r###" ]) +}});"### + )?; + } + + Ok(output) +} + +/// This module should be kept in sync with the copy in miden-standards' build.rs. +mod shared { + use std::collections::BTreeMap; + use std::fmt::Write; + use std::io::{self}; + use std::path::{Path, PathBuf}; + + use fs_err as fs; + use miden_assembly::Report; + use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; + 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. + /// + /// All non-MASM files are skipped. + pub fn get_masm_files>(dir_path: P) -> Result> { + let mut files = Vec::new(); + + let path = dir_path.as_ref(); + if path.is_dir() { + for entry in WalkDir::new(path) { + let entry = entry.into_diagnostic()?; + let file_path = entry.path().to_path_buf(); + if is_masm_file(&file_path).into_diagnostic()? { + files.push(file_path); + } + } + } else { + println!("cargo:warn=The specified path is not a directory."); + } + + Ok(files) + } + + /// Returns true if the provided path resolves to a file with `.masm` extension. + /// + /// # Errors + /// Returns an error if the path could not be converted to a UTF-8 string. + pub fn is_masm_file(path: &Path) -> io::Result { + if let Some(extension) = path.extension() { + let extension = extension + .to_str() + .ok_or_else(|| io::Error::other("invalid UTF-8 filename"))? + .to_lowercase(); + Ok(extension == "masm") + } else { + Ok(false) + } + } + + /// Extract all masm errors from the given path and returns a map by error category. + pub fn extract_all_masm_errors(asm_source_dir: &Path) -> Result> { + // We use a BTree here to order the errors by their categories which is the first part after + // the ERR_ prefix and to allow for the same error to be defined multiple times in + // different files (as long as the constant name and error messages match). + let mut errors = BTreeMap::new(); + + // Walk all files of the kernel source directory. + for entry in WalkDir::new(asm_source_dir) { + let entry = entry.into_diagnostic()?; + if !is_masm_file(entry.path()).into_diagnostic()? { + continue; + } + let file_contents = std::fs::read_to_string(entry.path()).into_diagnostic()?; + extract_masm_errors(&mut errors, &file_contents)?; + } + + let errors = errors + .into_iter() + .map(|(error_name, error)| NamedError { name: error_name, message: error.message }) + .collect(); + + Ok(errors) + } + + /// Extracts the errors from a single masm file and inserts them into the provided map. + pub fn extract_masm_errors( + errors: &mut BTreeMap, + file_contents: &str, + ) -> Result<()> { + let regex = Regex::new(r#"const\s*ERR_(?.*)\s*=\s*"(?.*)""#).unwrap(); + + for capture in regex.captures_iter(file_contents) { + let error_name = capture + .name("name") + .expect("error name should be captured") + .as_str() + .trim() + .to_owned(); + let error_message = capture + .name("message") + .expect("error code should be captured") + .as_str() + .trim() + .to_owned(); + + if let Some(ExtractedError { message: existing_error_message, .. }) = + errors.get(&error_name) + && existing_error_message != &error_message + { + return Err(Report::msg(format!( + "Transaction kernel error constant ERR_{error_name} is already defined elsewhere but its error message is different" + ))); + } + + // Enforce the "no trailing punctuation" rule from the Rust error guidelines on MASM + // errors. + if error_message.ends_with(".") { + return Err(Report::msg(format!( + "Error messages should not end with a period: `ERR_{error_name}: {error_message}`" + ))); + } + + errors.insert(error_name, ExtractedError { message: error_message }); + } + + Ok(()) + } + + pub fn is_new_error_category<'a>( + last_error: &mut Option<&'a str>, + current_error: &'a str, + ) -> bool { + let is_new = match last_error { + Some(last_err) => { + let last_category = + last_err.split("_").next().expect("there should be at least one entry"); + let new_category = + current_error.split("_").next().expect("there should be at least one entry"); + last_category != new_category + }, + None => false, + }; + + last_error.replace(current_error); + + is_new + } + + /// Generates the content of an error file for the given category and the set of errors and + /// writes it to the category's file. + pub fn generate_error_file(module: ErrorModule, errors: Vec) -> Result<()> { + let mut output = String::new(); + + if module.is_crate_local { + writeln!(output, "use crate::errors::MasmError;\n").unwrap(); + } else { + writeln!(output, "use miden_protocol::errors::MasmError;\n").unwrap(); + } + + writeln!( + output, + "// 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, ...). +" + ) + .unwrap(); + + writeln!( + output, + "// {} +// ================================================================================================ +", + module.array_name.replace("_", " ") + ) + .unwrap(); + + let mut last_error = None; + for named_error in errors.iter() { + let NamedError { name, message } = named_error; + + // Group errors into blocks separate by newlines. + if is_new_error_category(&mut last_error, name) { + writeln!(output).into_diagnostic()?; + } + + writeln!(output, "/// Error Message: \"{message}\"").into_diagnostic()?; + writeln!( + output, + r#"pub const ERR_{name}: MasmError = MasmError::from_static_str("{message}");"# + ) + .into_diagnostic()?; + } + + std::fs::write(module.file_name, output).into_diagnostic()?; + + Ok(()) + } + + pub type ErrorName = String; + + #[derive(Debug, Clone)] + pub struct ExtractedError { + pub message: String, + } + + #[derive(Debug, Clone)] + pub struct NamedError { + pub name: ErrorName, + pub message: String, + } + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct ErrorModule { + pub file_name: &'static str, + pub array_name: &'static str, + pub is_crate_local: bool, + } +} diff --git a/crates/miden-lib/masm_doc_comment_fmt.md b/crates/miden-protocol/masm_doc_comment_fmt.md similarity index 100% rename from crates/miden-lib/masm_doc_comment_fmt.md rename to crates/miden-protocol/masm_doc_comment_fmt.md diff --git a/crates/miden-objects/src/account/account_id/account_type.rs b/crates/miden-protocol/src/account/account_id/account_type.rs similarity index 100% rename from crates/miden-objects/src/account/account_id/account_type.rs rename to crates/miden-protocol/src/account/account_id/account_type.rs diff --git a/crates/miden-objects/src/account/account_id/id_prefix.rs b/crates/miden-protocol/src/account/account_id/id_prefix.rs similarity index 98% rename from crates/miden-objects/src/account/account_id/id_prefix.rs rename to crates/miden-protocol/src/account/account_id/id_prefix.rs index 97fa2552b4..46207bcc85 100644 --- a/crates/miden-objects/src/account/account_id/id_prefix.rs +++ b/crates/miden-protocol/src/account/account_id/id_prefix.rs @@ -51,9 +51,9 @@ impl AccountIdPrefix { /// /// Panics if the prefix does not contain a known account ID version. /// - /// If debug_assertions are enabled (e.g. in debug mode), this function panics if the given - /// felt is invalid according to the constraints in the - /// [`AccountId`](crate::account::AccountId) documentation. + /// If `debug_assertions` are enabled (e.g. in debug mode), this function panics if the given + /// felt is invalid according to the constraints in the [`AccountId`](crate::account::AccountId) + /// documentation. pub fn new_unchecked(prefix: Felt) -> Self { // The prefix contains the metadata. // If we add more versions in the future, we may need to generalize this. diff --git a/crates/miden-objects/src/account/account_id/id_version.rs b/crates/miden-protocol/src/account/account_id/id_version.rs similarity index 100% rename from crates/miden-objects/src/account/account_id/id_version.rs rename to crates/miden-protocol/src/account/account_id/id_version.rs diff --git a/crates/miden-objects/src/account/account_id/mod.rs b/crates/miden-protocol/src/account/account_id/mod.rs similarity index 89% rename from crates/miden-objects/src/account/account_id/mod.rs rename to crates/miden-protocol/src/account/account_id/mod.rs index 2c94bb9850..03a575fbf8 100644 --- a/crates/miden-objects/src/account/account_id/mod.rs +++ b/crates/miden-protocol/src/account/account_id/mod.rs @@ -23,9 +23,9 @@ use miden_core::utils::{ByteReader, Deserializable, Serializable}; use miden_crypto::utils::hex_to_bytes; use miden_processor::DeserializationError; +use crate::Word; use crate::address::NetworkId; -use crate::errors::AccountIdError; -use crate::{AccountError, Word}; +use crate::errors::{AccountError, AccountIdError}; /// The identifier of an [`Account`](crate::account::Account). /// @@ -317,6 +317,29 @@ impl AccountId { .map(|(network_id, account_id)| (network_id, AccountId::V0(account_id))) } + /// Parses a string into an [`AccountId`]. + /// + /// This function supports parsing from both hex (`0x...`) and bech32 formats. + /// + /// # Returns + /// + /// Returns a tuple of the parsed [`AccountId`] and an optional [`NetworkId`]. + /// - For hex strings: `NetworkId` is `None`. + /// - For bech32 strings: `NetworkId` is `Some(...)`. + /// + /// # Errors + /// + /// Returns an error if the string cannot be parsed as either hex or bech32 format. + pub fn parse(s: &str) -> Result<(Self, Option), AccountIdError> { + if let Some(hex_digits) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + // Normalize to lowercase "0x" prefix for from_hex + let normalized = format!("0x{hex_digits}"); + Self::from_hex(&normalized).map(|id| (id, None)) + } else { + Self::from_bech32(s).map(|(network_id, id)| (id, Some(network_id))) + } + } + /// Decodes the data from the bech32 byte iterator into an [`AccountId`]. pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result { AccountIdV0::from_bech32_byte_iter(byte_iter).map(AccountId::V0) @@ -651,4 +674,61 @@ mod tests { AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. }) ); } + + #[test] + fn parse_hex_string() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let hex_string = account_id.to_hex(); + + let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap(); + + assert_eq!(parsed_id, account_id); + assert!(network_id.is_none()); + } + + #[test] + fn parse_hex_string_uppercase() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + // Keep "0x" prefix lowercase, only uppercase the hex digits + let hex_string = account_id.to_hex(); + let hex_string = format!("0x{}", hex_string[2..].to_uppercase()); + + let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap(); + + assert_eq!(parsed_id, account_id); + assert!(network_id.is_none()); + } + + #[test] + fn parse_hex_string_uppercase_prefix() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + // Use "0X" prefix (uppercase X) with uppercase hex digits + let hex_string = account_id.to_hex(); + let hex_string = format!("0X{}", hex_string[2..].to_uppercase()); + + let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap(); + + assert_eq!(parsed_id, account_id); + assert!(network_id.is_none()); + } + + #[test] + fn parse_bech32_string() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let bech32_string = account_id.to_bech32(NetworkId::Mainnet); + + let (parsed_id, parsed_network_id) = AccountId::parse(&bech32_string).unwrap(); + + assert_eq!(parsed_id, account_id); + assert_eq!(parsed_network_id, Some(NetworkId::Mainnet)); + } + + #[test] + fn parse_invalid_string() { + let error = AccountId::parse("invalid_string").unwrap_err(); + assert_matches!(error, AccountIdError::Bech32DecodeError(_)); + + let error = AccountId::parse("0xinvalid").unwrap_err(); + assert_matches!(error, AccountIdError::AccountIdHexParseError(_)); + } } diff --git a/crates/miden-objects/src/account/account_id/seed.rs b/crates/miden-protocol/src/account/account_id/seed.rs similarity index 97% rename from crates/miden-objects/src/account/account_id/seed.rs rename to crates/miden-protocol/src/account/account_id/seed.rs index c472695dfb..8ad1be02a7 100644 --- a/crates/miden-objects/src/account/account_id/seed.rs +++ b/crates/miden-protocol/src/account/account_id/seed.rs @@ -3,7 +3,8 @@ use alloc::vec::Vec; use crate::account::account_id::AccountIdVersion; use crate::account::account_id::v0::{compute_digest, validate_prefix}; use crate::account::{AccountStorageMode, AccountType}; -use crate::{AccountError, Felt, Word}; +use crate::errors::AccountError; +use crate::{Felt, Word}; /// Finds and returns a seed suitable for creating an account ID for the specified account type /// using the provided initial seed as a starting point. diff --git a/crates/miden-objects/src/account/account_id/storage_mode.rs b/crates/miden-protocol/src/account/account_id/storage_mode.rs similarity index 100% rename from crates/miden-objects/src/account/account_id/storage_mode.rs rename to crates/miden-protocol/src/account/account_id/storage_mode.rs diff --git a/crates/miden-objects/src/account/account_id/v0/mod.rs b/crates/miden-protocol/src/account/account_id/v0/mod.rs similarity index 99% rename from crates/miden-objects/src/account/account_id/v0/mod.rs rename to crates/miden-protocol/src/account/account_id/v0/mod.rs index c14095819d..34ad3ebbb9 100644 --- a/crates/miden-objects/src/account/account_id/v0/mod.rs +++ b/crates/miden-protocol/src/account/account_id/v0/mod.rs @@ -19,9 +19,9 @@ use crate::account::account_id::account_type::{ use crate::account::account_id::storage_mode::{NETWORK, PRIVATE, PUBLIC}; use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; use crate::address::AddressType; -use crate::errors::{AccountIdError, Bech32Error}; +use crate::errors::{AccountError, AccountIdError, Bech32Error}; use crate::utils::{ByteReader, Deserializable, DeserializationError, Serializable}; -use crate::{AccountError, EMPTY_WORD, Felt, Hasher, Word}; +use crate::{EMPTY_WORD, Felt, Hasher, Word}; // ACCOUNT ID VERSION 0 // ================================================================================================ diff --git a/crates/miden-objects/src/account/account_id/v0/prefix.rs b/crates/miden-protocol/src/account/account_id/v0/prefix.rs similarity index 100% rename from crates/miden-objects/src/account/account_id/v0/prefix.rs rename to crates/miden-protocol/src/account/account_id/v0/prefix.rs diff --git a/crates/miden-objects/src/account/auth.rs b/crates/miden-protocol/src/account/auth.rs similarity index 79% rename from crates/miden-objects/src/account/auth.rs rename to crates/miden-protocol/src/account/auth.rs index ea1c7f550c..22a747898b 100644 --- a/crates/miden-objects/src/account/auth.rs +++ b/crates/miden-protocol/src/account/auth.rs @@ -2,7 +2,8 @@ use alloc::vec::Vec; use rand::{CryptoRng, Rng}; -use crate::crypto::dsa::{ecdsa_k256_keccak, rpo_falcon512}; +use crate::crypto::dsa::{ecdsa_k256_keccak, falcon512_rpo}; +use crate::errors::AuthSchemeError; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -10,13 +11,13 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{AuthSchemeError, Felt, Hasher, Word}; +use crate::{Felt, Hasher, Word}; // AUTH SCHEME // ================================================================================================ /// Identifier of signature schemes use for transaction authentication -const RPO_FALCON_512: u8 = 0; +const FALCON_512_RPO: u8 = 0; const ECDSA_K256_KECCAK: u8 = 1; /// Defines standard authentication schemes (i.e., signature schemes) available in the Miden @@ -25,12 +26,12 @@ const ECDSA_K256_KECCAK: u8 = 1; #[non_exhaustive] #[repr(u8)] pub enum AuthScheme { - /// A deterministic RPO Falcon512 signature scheme. + /// A deterministic Falcon512 signature scheme. /// /// This version differs from the reference Falcon512 implementation in its use of the RPO /// algebraic hash function in its hash-to-point algorithm to make signatures very efficient /// to verify inside Miden VM. - RpoFalcon512 = RPO_FALCON_512, + Falcon512Rpo = FALCON_512_RPO, /// ECDSA signature scheme over secp256k1 curve using Keccak to hash the messages when signing. EcdsaK256Keccak = ECDSA_K256_KECCAK, @@ -46,7 +47,7 @@ impl AuthScheme { impl core::fmt::Display for AuthScheme { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::RpoFalcon512 => f.write_str("RpoFalcon512"), + Self::Falcon512Rpo => f.write_str("Falcon512Rpo"), Self::EcdsaK256Keccak => f.write_str("EcdsaK256Keccak"), } } @@ -57,7 +58,7 @@ impl TryFrom for AuthScheme { fn try_from(value: u8) -> Result { match value { - RPO_FALCON_512 => Ok(Self::RpoFalcon512), + FALCON_512_RPO => Ok(Self::Falcon512Rpo), ECDSA_K256_KECCAK => Ok(Self::EcdsaK256Keccak), value => Err(AuthSchemeError::InvalidAuthSchemeIdentifier(value)), } @@ -78,7 +79,7 @@ impl Serializable for AuthScheme { impl Deserializable for AuthScheme { fn read_from(source: &mut R) -> Result { match source.read_u8()? { - RPO_FALCON_512 => Ok(Self::RpoFalcon512), + FALCON_512_RPO => Ok(Self::Falcon512Rpo), ECDSA_K256_KECCAK => Ok(Self::EcdsaK256Keccak), value => Err(DeserializationError::InvalidValue(format!( "auth scheme identifier `{value}` is not valid" @@ -95,20 +96,20 @@ impl Deserializable for AuthScheme { #[non_exhaustive] #[repr(u8)] pub enum AuthSecretKey { - RpoFalcon512(rpo_falcon512::SecretKey) = RPO_FALCON_512, + Falcon512Rpo(falcon512_rpo::SecretKey) = FALCON_512_RPO, EcdsaK256Keccak(ecdsa_k256_keccak::SecretKey) = ECDSA_K256_KECCAK, } impl AuthSecretKey { - /// Generates an RpoFalcon512 secret key from the OS-provided randomness. + /// Generates an Falcon512Rpo secret key from the OS-provided randomness. #[cfg(feature = "std")] - pub fn new_rpo_falcon512() -> Self { - Self::RpoFalcon512(rpo_falcon512::SecretKey::new()) + pub fn new_falcon512_rpo() -> Self { + Self::Falcon512Rpo(falcon512_rpo::SecretKey::new()) } - /// Generates an RpoFalcon512 secrete key using the provided random number generator. - pub fn new_rpo_falcon512_with_rng(rng: &mut R) -> Self { - Self::RpoFalcon512(rpo_falcon512::SecretKey::with_rng(rng)) + /// Generates an Falcon512Rpo secrete key using the provided random number generator. + pub fn new_falcon512_rpo_with_rng(rng: &mut R) -> Self { + Self::Falcon512Rpo(falcon512_rpo::SecretKey::with_rng(rng)) } /// Generates an EcdsaK256Keccak secret key from the OS-provided randomness. @@ -125,7 +126,7 @@ impl AuthSecretKey { /// Returns the authentication scheme of this secret key. pub fn auth_scheme(&self) -> AuthScheme { match self { - AuthSecretKey::RpoFalcon512(_) => AuthScheme::RpoFalcon512, + AuthSecretKey::Falcon512Rpo(_) => AuthScheme::Falcon512Rpo, AuthSecretKey::EcdsaK256Keccak(_) => AuthScheme::EcdsaK256Keccak, } } @@ -133,7 +134,7 @@ impl AuthSecretKey { /// Returns a public key associated with this secret key. pub fn public_key(&self) -> PublicKey { match self { - AuthSecretKey::RpoFalcon512(key) => PublicKey::RpoFalcon512(key.public_key()), + AuthSecretKey::Falcon512Rpo(key) => PublicKey::Falcon512Rpo(key.public_key()), AuthSecretKey::EcdsaK256Keccak(key) => PublicKey::EcdsaK256Keccak(key.public_key()), } } @@ -141,7 +142,7 @@ impl AuthSecretKey { /// Signs the provided message with this secret key. pub fn sign(&self, message: Word) -> Signature { match self { - AuthSecretKey::RpoFalcon512(key) => Signature::RpoFalcon512(key.sign(message)), + AuthSecretKey::Falcon512Rpo(key) => Signature::Falcon512Rpo(key.sign(message)), AuthSecretKey::EcdsaK256Keccak(key) => Signature::EcdsaK256Keccak(key.sign(message)), } } @@ -151,7 +152,7 @@ impl Serializable for AuthSecretKey { fn write_into(&self, target: &mut W) { self.auth_scheme().write_into(target); match self { - AuthSecretKey::RpoFalcon512(key) => key.write_into(target), + AuthSecretKey::Falcon512Rpo(key) => key.write_into(target), AuthSecretKey::EcdsaK256Keccak(key) => key.write_into(target), } } @@ -160,9 +161,9 @@ impl Serializable for AuthSecretKey { impl Deserializable for AuthSecretKey { fn read_from(source: &mut R) -> Result { match source.read::()? { - AuthScheme::RpoFalcon512 => { - let secret_key = rpo_falcon512::SecretKey::read_from(source)?; - Ok(AuthSecretKey::RpoFalcon512(secret_key)) + AuthScheme::Falcon512Rpo => { + let secret_key = falcon512_rpo::SecretKey::read_from(source)?; + Ok(AuthSecretKey::Falcon512Rpo(secret_key)) }, AuthScheme::EcdsaK256Keccak => { let secret_key = ecdsa_k256_keccak::SecretKey::read_from(source)?; @@ -185,8 +186,8 @@ impl core::fmt::Display for PublicKeyCommitment { } } -impl From for PublicKeyCommitment { - fn from(value: rpo_falcon512::PublicKey) -> Self { +impl From for PublicKeyCommitment { + fn from(value: falcon512_rpo::PublicKey) -> Self { Self(value.to_commitment()) } } @@ -207,7 +208,7 @@ impl From for PublicKeyCommitment { #[derive(Clone, Debug)] #[non_exhaustive] pub enum PublicKey { - RpoFalcon512(rpo_falcon512::PublicKey), + Falcon512Rpo(falcon512_rpo::PublicKey), EcdsaK256Keccak(ecdsa_k256_keccak::PublicKey), } @@ -215,7 +216,7 @@ impl PublicKey { /// Returns the authentication scheme of this public key. pub fn auth_scheme(&self) -> AuthScheme { match self { - PublicKey::RpoFalcon512(_) => AuthScheme::RpoFalcon512, + PublicKey::Falcon512Rpo(_) => AuthScheme::Falcon512Rpo, PublicKey::EcdsaK256Keccak(_) => AuthScheme::EcdsaK256Keccak, } } @@ -223,7 +224,7 @@ impl PublicKey { /// Returns a commitment to this public key. pub fn to_commitment(&self) -> PublicKeyCommitment { match self { - PublicKey::RpoFalcon512(key) => key.to_commitment().into(), + PublicKey::Falcon512Rpo(key) => key.to_commitment().into(), PublicKey::EcdsaK256Keccak(key) => key.to_commitment().into(), } } @@ -231,7 +232,7 @@ impl PublicKey { /// Verifies the provided signature against the provided message and this public key. pub fn verify(&self, message: Word, signature: Signature) -> bool { match (self, signature) { - (PublicKey::RpoFalcon512(key), Signature::RpoFalcon512(sig)) => { + (PublicKey::Falcon512Rpo(key), Signature::Falcon512Rpo(sig)) => { key.verify(message, &sig) }, (PublicKey::EcdsaK256Keccak(key), Signature::EcdsaK256Keccak(sig)) => { @@ -246,7 +247,7 @@ impl Serializable for PublicKey { fn write_into(&self, target: &mut W) { self.auth_scheme().write_into(target); match self { - PublicKey::RpoFalcon512(pub_key) => pub_key.write_into(target), + PublicKey::Falcon512Rpo(pub_key) => pub_key.write_into(target), PublicKey::EcdsaK256Keccak(pub_key) => pub_key.write_into(target), } } @@ -255,9 +256,9 @@ impl Serializable for PublicKey { impl Deserializable for PublicKey { fn read_from(source: &mut R) -> Result { match source.read::()? { - AuthScheme::RpoFalcon512 => { - let pub_key = rpo_falcon512::PublicKey::read_from(source)?; - Ok(PublicKey::RpoFalcon512(pub_key)) + AuthScheme::Falcon512Rpo => { + let pub_key = falcon512_rpo::PublicKey::read_from(source)?; + Ok(PublicKey::Falcon512Rpo(pub_key)) }, AuthScheme::EcdsaK256Keccak => { let pub_key = ecdsa_k256_keccak::PublicKey::read_from(source)?; @@ -276,9 +277,9 @@ impl Deserializable for PublicKey { /// convert the native signature into a vector of field elements that can be loaded into the advice /// provider. To prepare the signature, use the provided `to_prepared_signature` method: /// ```rust,no_run -/// use miden_objects::account::auth::Signature; -/// use miden_objects::crypto::dsa::rpo_falcon512::SecretKey; -/// use miden_objects::{Felt, Word}; +/// use miden_protocol::account::auth::Signature; +/// use miden_protocol::crypto::dsa::falcon512_rpo::SecretKey; +/// use miden_protocol::{Felt, Word}; /// /// let secret_key = SecretKey::new(); /// let message = Word::default(); @@ -288,7 +289,7 @@ impl Deserializable for PublicKey { #[derive(Clone, Debug)] #[repr(u8)] pub enum Signature { - RpoFalcon512(rpo_falcon512::Signature) = RPO_FALCON_512, + Falcon512Rpo(falcon512_rpo::Signature) = FALCON_512_RPO, EcdsaK256Keccak(ecdsa_k256_keccak::Signature) = ECDSA_K256_KECCAK, } @@ -296,7 +297,7 @@ impl Signature { /// Returns the authentication scheme of this signature. pub fn auth_scheme(&self) -> AuthScheme { match self { - Signature::RpoFalcon512(_) => AuthScheme::RpoFalcon512, + Signature::Falcon512Rpo(_) => AuthScheme::Falcon512Rpo, Signature::EcdsaK256Keccak(_) => AuthScheme::EcdsaK256Keccak, } } @@ -310,9 +311,12 @@ impl Signature { // TODO: the `expect()` should be changed to an error; but that will be a part of a bigger // refactoring let mut result = match self { - Signature::RpoFalcon512(sig) => prepare_rpo_falcon512_signature(sig), - Signature::EcdsaK256Keccak(sig) => miden_stdlib::prepare_ecdsa_signature(msg, sig) - .expect("inferring public key from signature and message should succeed"), + Signature::Falcon512Rpo(sig) => prepare_falcon512_rpo_signature(sig), + Signature::EcdsaK256Keccak(sig) => { + let pk = ecdsa_k256_keccak::PublicKey::recover_from(msg, sig) + .expect("inferring public key from signature and message should succeed"); + miden_core_lib::dsa::ecdsa_k256_keccak::encode_signature(&pk, sig) + }, }; // reverse the signature data so that when it is pushed onto the advice stack, the first @@ -322,9 +326,9 @@ impl Signature { } } -impl From for Signature { - fn from(signature: rpo_falcon512::Signature) -> Self { - Signature::RpoFalcon512(signature) +impl From for Signature { + fn from(signature: falcon512_rpo::Signature) -> Self { + Signature::Falcon512Rpo(signature) } } @@ -332,7 +336,7 @@ impl Serializable for Signature { fn write_into(&self, target: &mut W) { self.auth_scheme().write_into(target); match self { - Signature::RpoFalcon512(signature) => signature.write_into(target), + Signature::Falcon512Rpo(signature) => signature.write_into(target), Signature::EcdsaK256Keccak(signature) => signature.write_into(target), } } @@ -341,9 +345,9 @@ impl Serializable for Signature { impl Deserializable for Signature { fn read_from(source: &mut R) -> Result { match source.read::()? { - AuthScheme::RpoFalcon512 => { - let signature = rpo_falcon512::Signature::read_from(source)?; - Ok(Signature::RpoFalcon512(signature)) + AuthScheme::Falcon512Rpo => { + let signature = falcon512_rpo::Signature::read_from(source)?; + Ok(Signature::Falcon512Rpo(signature)) }, AuthScheme::EcdsaK256Keccak => { let signature = ecdsa_k256_keccak::Signature::read_from(source)?; @@ -356,7 +360,7 @@ impl Deserializable for Signature { // SIGNATURE PREPARATION // ================================================================================================ -/// Converts a Falcon [rpo_falcon512::Signature] to a vector of values to be pushed onto the +/// Converts a Falcon [falcon512_rpo::Signature] to a vector of values to be pushed onto the /// advice stack. The values are the ones required for a Falcon signature verification inside the VM /// and they are: /// @@ -367,8 +371,8 @@ impl Deserializable for Signature { /// 4. The product of the above two polynomials `pi` in the ring of polynomials with coefficients in /// the Miden field. /// 5. The nonce represented as 8 field elements. -fn prepare_rpo_falcon512_signature(sig: &rpo_falcon512::Signature) -> Vec { - use rpo_falcon512::Polynomial; +fn prepare_falcon512_rpo_signature(sig: &falcon512_rpo::Signature) -> Vec { + use falcon512_rpo::Polynomial; // The signature is composed of a nonce and a polynomial s2 // The nonce is represented as 8 field elements. diff --git a/crates/miden-objects/src/account/builder/mod.rs b/crates/miden-protocol/src/account/builder/mod.rs similarity index 84% rename from crates/miden-objects/src/account/builder/mod.rs rename to crates/miden-protocol/src/account/builder/mod.rs index e5d19206be..478a2013d3 100644 --- a/crates/miden-objects/src/account/builder/mod.rs +++ b/crates/miden-protocol/src/account/builder/mod.rs @@ -3,6 +3,7 @@ use alloc::vec::Vec; use miden_core::FieldElement; +use crate::account::component::StorageSchema; use crate::account::{ Account, AccountCode, @@ -15,7 +16,8 @@ use crate::account::{ AccountType, }; use crate::asset::AssetVault; -use crate::{AccountError, Felt, Word}; +use crate::errors::AccountError; +use crate::{Felt, Word}; /// A convenient builder for an [`Account`] allowing for safe construction of an account by /// combining multiple [`AccountComponent`]s. @@ -126,6 +128,16 @@ impl AccountBuilder { self } + /// Returns an iterator of storage schemas attached to the builder's components, if any. + /// + /// Components constructed without metadata will not contribute a schema. + pub fn storage_schemas(&self) -> impl Iterator + '_ { + self.auth_component + .iter() + .chain(self.components.iter()) + .filter_map(|component| component.storage_schema()) + } + /// Builds the common parts of testing and non-testing code. fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> { #[cfg(any(feature = "testing", test))] @@ -144,13 +156,13 @@ impl AccountBuilder { let mut components = vec![auth_component]; components.append(&mut self.components); - let (code, storage) = Account::initialize_from_components(self.account_type, &components) + let (code, storage) = Account::initialize_from_components(self.account_type, components) .map_err(|err| { - AccountError::BuildError( - "account components failed to build".into(), - Some(Box::new(err)), - ) - })?; + AccountError::BuildError( + "account components failed to build".into(), + Some(Box::new(err)), + ) + })?; Ok((vault, code, storage)) } @@ -209,14 +221,14 @@ impl AccountBuilder { self.init_seed, self.id_version, code.commitment(), - storage.commitment(), + storage.to_commitment(), )?; let account_id = AccountId::new( seed, AccountIdVersion::Version0, code.commitment(), - storage.commitment(), + storage.to_commitment(), ) .expect("get_account_seed should provide a suitable seed"); @@ -291,16 +303,16 @@ mod tests { use miden_processor::MastNodeExt; use super::*; - use crate::account::StorageSlot; + use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName}; use crate::testing::noop_auth_component::NoopAuthComponent; const CUSTOM_CODE1: &str = " - export.foo + pub proc foo push.2.2 add eq.4 end "; const CUSTOM_CODE2: &str = " - export.bar + pub proc bar push.4.4 add eq.8 end "; @@ -316,6 +328,19 @@ mod tests { .expect("code should be valid") }); + static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("custom::component1::slot0") + .expect("storage slot name should be valid") + }); + static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock = LazyLock::new(|| { + StorageSlotName::new("custom::component2::slot0") + .expect("storage slot name should be valid") + }); + static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock = LazyLock::new(|| { + StorageSlotName::new("custom::component2::slot1") + .expect("storage slot name should be valid") + }); + struct CustomComponent1 { slot0: u64, } @@ -324,9 +349,12 @@ mod tests { let mut value = Word::empty(); value[0] = Felt::new(custom.slot0); - AccountComponent::new(CUSTOM_LIBRARY1.clone(), vec![StorageSlot::Value(value)]) - .expect("component should be valid") - .with_supports_all_types() + AccountComponent::new( + CUSTOM_LIBRARY1.clone(), + vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)], + ) + .expect("component should be valid") + .with_supports_all_types() } } @@ -343,7 +371,10 @@ mod tests { AccountComponent::new( CUSTOM_LIBRARY2.clone(), - vec![StorageSlot::Value(value0), StorageSlot::Value(value1)], + vec![ + StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0), + StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1), + ], ) .expect("component should be valid") .with_supports_all_types() @@ -373,7 +404,7 @@ mod tests { account.seed().unwrap(), AccountIdVersion::Version0, account.code.commitment(), - account.storage.commitment(), + account.storage.to_commitment(), ) .unwrap(); assert_eq!(account.id(), computed_id); @@ -382,40 +413,25 @@ mod tests { assert_eq!(account.code.procedure_roots().count(), 3); let foo_root = CUSTOM_LIBRARY1.mast_forest() - [CUSTOM_LIBRARY1.get_export_node_id(&CUSTOM_LIBRARY1.exports().next().unwrap().name)] + [CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap().path())] .digest(); let bar_root = CUSTOM_LIBRARY2.mast_forest() - [CUSTOM_LIBRARY2.get_export_node_id(&CUSTOM_LIBRARY2.exports().next().unwrap().name)] + [CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap().path())] .digest(); - let foo_procedure_info = &account - .code() - .procedures() - .iter() - .find(|info| info.mast_root() == &foo_root) - .unwrap(); - assert_eq!(foo_procedure_info.storage_offset(), 0); - assert_eq!(foo_procedure_info.storage_size(), 1); - - let bar_procedure_info = &account - .code() - .procedures() - .iter() - .find(|info| info.mast_root() == &bar_root) - .unwrap(); - assert_eq!(bar_procedure_info.storage_offset(), 1); - assert_eq!(bar_procedure_info.storage_size(), 2); + assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root))); + assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root))); assert_eq!( - account.storage().get_item(0).unwrap(), + account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(), [Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into() ); assert_eq!( - account.storage().get_item(1).unwrap(), + account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(), [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into() ); assert_eq!( - account.storage().get_item(2).unwrap(), + account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(), [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into() ); } diff --git a/crates/miden-objects/src/account/code/header.rs b/crates/miden-protocol/src/account/code/header.rs similarity index 100% rename from crates/miden-objects/src/account/code/header.rs rename to crates/miden-protocol/src/account/code/header.rs diff --git a/crates/miden-objects/src/account/code/mod.rs b/crates/miden-protocol/src/account/code/mod.rs similarity index 63% rename from crates/miden-objects/src/account/code/mod.rs rename to crates/miden-protocol/src/account/code/mod.rs index d72690d689..e079b0746c 100644 --- a/crates/miden-objects/src/account/code/mod.rs +++ b/crates/miden-protocol/src/account/code/mod.rs @@ -1,4 +1,3 @@ -use alloc::collections::BTreeSet; use alloc::sync::Arc; use alloc::vec::Vec; @@ -16,41 +15,49 @@ use super::{ Serializable, }; use crate::Word; -use crate::account::{AccountComponent, AccountType}; +use crate::account::AccountComponent; +#[cfg(any(feature = "testing", test))] +use crate::account::AccountType; pub mod procedure; -use procedure::{AccountProcedureInfo, PrintableProcedure}; +use procedure::{AccountProcedureRoot, PrintableProcedure}; // ACCOUNT CODE // ================================================================================================ -/// A public interface of an account. +/// The public interface of an account. /// -/// Account's public interface consists of a set of account procedures, each procedure being a -/// Miden VM program. Thus, MAST root of each procedure commits to the underlying program. +/// An account's public interface consists of a set of account procedures, each of which is +/// identified and committed to by a MAST root. They are represented by [`AccountProcedureRoot`]. /// -/// Each exported procedure is associated with a storage offset and a storage size. +/// The set of procedures has an arbitrary order, i.e. they are not sorted. The only exception is +/// the authentication procedure of the account, which is always at index 0. This procedure is +/// automatically called at the end of a transaction to validate an account's state transition. /// -/// We commit to the entire account interface by building a sequential hash of all procedure MAST -/// roots and associated storage_offset's. Specifically, each procedure contributes exactly 8 field -/// elements to the sequence of elements to be hashed. These elements are defined as follows: +/// The code commits to the entire account interface by building a sequential hash of all procedure +/// MAST roots. Specifically, each procedure contributes exactly 4 field elements to the sequence of +/// elements to be hashed. Each procedure is represented by its MAST root: /// /// ```text -/// [PROCEDURE_MAST_ROOT, storage_offset, 0, 0, storage_size] +/// [PROCEDURE_MAST_ROOT] /// ``` #[derive(Debug, Clone)] pub struct AccountCode { mast: Arc, - procedures: Vec, + procedures: Vec, commitment: Word, } impl AccountCode { - /// The maximum number of account interface procedures. - pub const MAX_NUM_PROCEDURES: usize = 256; + // CONSTANTS + // -------------------------------------------------------------------------------------------- + /// The minimum number of account interface procedures (one auth and at least one non-auth). pub const MIN_NUM_PROCEDURES: usize = 2; + /// The maximum number of account interface procedures. + pub const MAX_NUM_PROCEDURES: usize = 256; + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -63,7 +70,7 @@ impl AccountCode { account_type: AccountType, ) -> Result { super::validate_components_support_account_type(components, account_type)?; - Self::from_components_unchecked(components, account_type) + Self::from_components_unchecked(components) } /// Creates a new [`AccountCode`] from the provided components' libraries. @@ -85,13 +92,12 @@ impl AccountCode { /// - [`MastForest::merge`] fails on all libraries. pub(super) fn from_components_unchecked( components: &[AccountComponent], - account_type: AccountType, ) -> Result { let (merged_mast_forest, _) = MastForest::merge(components.iter().map(|component| component.mast_forest())) .map_err(AccountError::AccountComponentMastForestMergeError)?; - let mut builder = ProcedureInfoBuilder::new(account_type); + let mut builder = AccountProcedureBuilder::new(); let mut components_iter = components.iter(); let first_component = @@ -119,16 +125,15 @@ impl AccountCode { Self::read_from_bytes(bytes).map_err(AccountError::AccountCodeDeserializationError) } - /// Returns a new definition of an account's interface instantiated from the provided - /// [MastForest] and a list of [AccountProcedureInfo]s. + /// 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 1 or greater than 256. - /// - If some any of the provided procedures does not have a corresponding root in the provided - /// MAST forest. - pub fn from_parts(mast: Arc, procedures: Vec) -> Self { - assert!(!procedures.is_empty(), "no account procedures"); + /// - 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 { @@ -151,8 +156,8 @@ impl AccountCode { self.mast.clone() } - /// Returns a reference to the account procedures. - pub fn procedures(&self) -> &[AccountProcedureInfo] { + /// Returns a reference to the account procedure roots. + pub fn procedures(&self) -> &[AccountProcedureRoot] { &self.procedures } @@ -171,29 +176,19 @@ impl AccountCode { self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root) } - /// Returns information about the procedure at the specified index. - /// - /// # Panics - /// Panics if the provided index is out of bounds. - pub fn get_procedure_by_index(&self, index: usize) -> &AccountProcedureInfo { - &self.procedures[index] - } - - /// Returns the procedure index for the procedure with the specified MAST root or None if such - /// procedure is not defined in this [AccountCode]. - pub fn get_procedure_index_by_root(&self, root: Word) -> Option { - self.procedures - .iter() - .map(|procedure| procedure.mast_root()) - .position(|r| r == &root) + /// Returns the procedure root at the specified index. + pub fn get(&self, index: usize) -> Option<&AccountProcedureRoot> { + self.procedures.get(index) } - /// Converts procedure information in this [AccountCode] into a vector of field elements. + /// Converts the procedure root in this [`AccountCode`] into a vector of field elements. + /// + /// This is done by first converting each procedure into 4 field elements as follows: /// - /// This is done by first converting each procedure into 8 field elements as follows: /// ```text - /// [PROCEDURE_MAST_ROOT, storage_offset, storage_size, 0, 0] + /// [PROCEDURE_MAST_ROOT] /// ``` + /// /// And then concatenating the resulting elements into a single vector. pub fn as_elements(&self) -> Vec { procedures_as_elements(self.procedures()) @@ -202,12 +197,13 @@ impl AccountCode { /// Returns an iterator of printable representations for all procedures in this account code. /// /// # Returns + /// /// An iterator yielding [`PrintableProcedure`] instances for all procedures in this account /// code. pub fn printable_procedures(&self) -> impl Iterator { self.procedures() .iter() - .filter_map(move |procedure_info| self.printable_procedure(procedure_info).ok()) + .filter_map(move |proc_root| self.printable_procedure(proc_root).ok()) } // HELPER FUNCTIONS @@ -219,14 +215,14 @@ impl AccountCode { /// Returns an error if no procedure with the specified root exists in this account code. fn printable_procedure( &self, - proc_info: &AccountProcedureInfo, + proc_root: &AccountProcedureRoot, ) -> Result { let node_id = self .mast - .find_procedure_root(*proc_info.mast_root()) + .find_procedure_root(*proc_root.mast_root()) .expect("procedure root should be present in the mast forest"); - Ok(PrintableProcedure::new(self.mast.clone(), *proc_info, node_id)) + Ok(PrintableProcedure::new(self.mast.clone(), *proc_root, node_id)) } } @@ -260,7 +256,7 @@ impl Eq for AccountCode {} impl Serializable for AccountCode { fn write_into(&self, target: &mut W) { self.mast.write_into(target); - // since the number of procedures is guaranteed to be between 1 and 256, we can store the + // since the number of procedures is guaranteed to be between 2 and 256, we can store the // number as a single byte - but we do have to subtract 1 to store 256 as 255. target.write_u8((self.procedures.len() - 1) as u8); target.write_many(self.procedures()); @@ -287,7 +283,7 @@ impl Deserializable for AccountCode { fn read_from(source: &mut R) -> Result { let module = Arc::new(MastForest::read_from(source)?); let num_procedures = (source.read_u8()? as usize) + 1; - let procedures = source.read_many::(num_procedures)?; + let procedures = source.read_many::(num_procedures)?; Ok(Self::from_parts(module, procedures)) } @@ -307,13 +303,7 @@ impl PrettyPrint for AccountCode { 0, indent( 4, - text(format!("proc.{}", printable_procedure.mast_root())) - + nl() - + text(format!( - "storage.{}.{}", - printable_procedure.storage_offset(), - printable_procedure.storage_size() - )) + text(format!("proc {}", printable_procedure.mast_root())) + nl() + printable_procedure.render(), ) + nl() @@ -330,28 +320,25 @@ impl PrettyPrint for AccountCode { // ACCOUNT PROCEDURE BUILDER // ================================================================================================ -struct ProcedureInfoBuilder { - procedures: Vec, - proc_root_set: BTreeSet, - storage_offset: u8, +/// A helper type for building the set of account procedures from account components. +/// +/// In particular, this ensures that the auth procedure ends up at index 0. +struct AccountProcedureBuilder { + procedures: Vec, } -impl ProcedureInfoBuilder { - fn new(account_type: AccountType) -> Self { - let storage_offset = if account_type.is_faucet() { 1 } else { 0 }; - - Self { - procedures: Vec::new(), - proc_root_set: BTreeSet::new(), - storage_offset, - } +impl AccountProcedureBuilder { + fn new() -> Self { + Self { procedures: Vec::new() } } + /// This method must be called before add_component is called. fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> { let mut auth_proc_count = 0; for (proc_root, is_auth) in component.get_procedures() { - self.add_procedure(proc_root, component.storage_size())?; + self.add_procedure(proc_root); + if is_auth { let auth_proc_idx = self.procedures.len() - 1; self.procedures.swap(0, auth_proc_idx); @@ -365,10 +352,6 @@ impl ProcedureInfoBuilder { return Err(AccountError::AccountComponentMultipleAuthProcedures); } - self.storage_offset = self.storage_offset.checked_add(component.storage_size()).expect( - "account procedure info constructor should return an error if the addition overflows", - ); - Ok(()) } @@ -377,47 +360,22 @@ impl ProcedureInfoBuilder { if is_auth { return Err(AccountError::AccountCodeMultipleAuthComponents); } - self.add_procedure(proc_mast_root, component.storage_size())?; + self.add_procedure(proc_mast_root); } - self.storage_offset = self.storage_offset.checked_add(component.storage_size()).expect( - "account procedure info constructor should return an error if the addition overflows", - ); - Ok(()) } - fn add_procedure( - &mut self, - proc_mast_root: Word, - component_storage_size: u8, - ) -> Result<(), AccountError> { - // We cannot support procedures from multiple components with the same MAST root - // since storage offsets/sizes are set per MAST root. Setting them again for - // procedures where the offset has already been inserted would cause that - // procedure of the earlier component to write to the wrong slot. - if !self.proc_root_set.insert(proc_mast_root) { - return Err(AccountError::AccountComponentDuplicateProcedureRoot(proc_mast_root)); + fn add_procedure(&mut self, proc_mast_root: Word) { + // Allow procedures with the same MAST root from different components, but only add them + // once. + let proc_root = AccountProcedureRoot::from_raw(proc_mast_root); + if !self.procedures.contains(&proc_root) { + self.procedures.push(proc_root); } - - // Components that do not access storage need to have offset and size set to 0. - let (storage_offset, storage_size) = if component_storage_size == 0 { - (0, 0) - } else { - (self.storage_offset, component_storage_size) - }; - - // Note: Offset and size are validated in `AccountProcedureInfo::new`. - self.procedures.push(AccountProcedureInfo::new( - proc_mast_root, - storage_offset, - storage_size, - )?); - - Ok(()) } - fn build(self) -> Result, AccountError> { + fn build(self) -> Result, AccountError> { if self.procedures.len() < AccountCode::MIN_NUM_PROCEDURES { Err(AccountError::AccountCodeNoProcedures) } else if self.procedures.len() > AccountCode::MAX_NUM_PROCEDURES { @@ -432,14 +390,14 @@ impl ProcedureInfoBuilder { // ================================================================================================ /// Computes the commitment to the given procedures -pub(crate) fn build_procedure_commitment(procedures: &[AccountProcedureInfo]) -> Word { +pub(crate) 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: &[AccountProcedureInfo]) -> Vec { - procedures.iter().flat_map(|procedure| <[Felt; 8]>::from(*procedure)).collect() +pub(crate) fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec { + procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect() } // TESTS @@ -450,12 +408,11 @@ mod tests { use assert_matches::assert_matches; use miden_assembly::Assembler; - use miden_core::Word; use super::{AccountCode, Deserializable, Serializable}; - use crate::AccountError; use crate::account::code::build_procedure_commitment; - use crate::account::{AccountComponent, AccountType, StorageSlot}; + use crate::account::{AccountComponent, AccountType}; + use crate::errors::AccountError; use crate::testing::account_code::CODE; use crate::testing::noop_auth_component::NoopAuthComponent; @@ -474,43 +431,6 @@ mod tests { assert_eq!(procedure_root, code.commitment()) } - #[test] - fn test_account_code_procedure_offset_out_of_bounds() { - let code1 = "export.foo add end"; - let library1 = Assembler::default().assemble_library([code1]).unwrap(); - let code2 = "export.bar sub end"; - let library2 = Assembler::default().assemble_library([code2]).unwrap(); - - let auth_component: AccountComponent = NoopAuthComponent.into(); - - let component1 = - AccountComponent::new(library1, vec![StorageSlot::Value(Word::empty()); 250]) - .unwrap() - .with_supports_all_types(); - let mut component2 = - AccountComponent::new(library2, vec![StorageSlot::Value(Word::empty()); 5]) - .unwrap() - .with_supports_all_types(); - - // This is fine as the offset+size for component 2 is <= 255. - AccountCode::from_components( - &[auth_component.clone(), component1.clone(), component2.clone()], - AccountType::RegularAccountUpdatableCode, - ) - .unwrap(); - - // Push one more slot so offset+size exceeds 255. - component2.storage_slots.push(StorageSlot::Value(Word::empty())); - - let err = AccountCode::from_components( - &[auth_component, component1, component2], - AccountType::RegularAccountUpdatableCode, - ) - .unwrap_err(); - - assert_matches!(err, AccountError::StorageOffsetPlusSizeOutOfBounds(256)) - } - #[test] fn test_account_code_only_auth_component() { let err = AccountCode::from_components( @@ -524,9 +444,8 @@ mod tests { #[test] fn test_account_code_no_auth_component() { - let component = AccountComponent::compile(CODE, Assembler::default(), vec![]) - .unwrap() - .with_supports_all_types(); + let library = Assembler::default().assemble_library([CODE]).unwrap(); + let component = AccountComponent::new(library, vec![]).unwrap().with_supports_all_types(); let err = AccountCode::from_components(&[component], AccountType::RegularAccountUpdatableCode) @@ -551,21 +470,17 @@ mod tests { use miden_assembly::Assembler; let code_with_multiple_auth = " - use.miden::account - - export.auth_basic + pub proc auth_basic push.1 drop end - export.auth_secondary + pub proc auth_secondary push.0 drop end "; - let component = - AccountComponent::compile(code_with_multiple_auth, Assembler::default(), vec![]) - .unwrap() - .with_supports_all_types(); + let library = Assembler::default().assemble_library([code_with_multiple_auth]).unwrap(); + let component = AccountComponent::new(library, vec![]).unwrap().with_supports_all_types(); let err = AccountCode::from_components(&[component], AccountType::RegularAccountUpdatableCode) diff --git a/crates/miden-protocol/src/account/code/procedure.rs b/crates/miden-protocol/src/account/code/procedure.rs new file mode 100644 index 0000000000..fbef026007 --- /dev/null +++ b/crates/miden-protocol/src/account/code/procedure.rs @@ -0,0 +1,123 @@ +use alloc::string::String; +use alloc::sync::Arc; + +use miden_core::mast::MastForest; +use miden_core::prettier::PrettyPrint; +use miden_processor::{MastNode, MastNodeExt, MastNodeId}; +use miden_protocol_macros::WordWrapper; + +use super::Felt; +use crate::Word; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; + +// ACCOUNT PROCEDURE ROOT +// ================================================================================================ + +/// The MAST root of a public procedure in an account's interface. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, WordWrapper)] +pub struct AccountProcedureRoot(Word); + +impl AccountProcedureRoot { + /// The number of field elements that represent an [`AccountProcedureRoot`] in kernel memory. + pub const NUM_ELEMENTS: usize = 4; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the procedure's mast root. + pub fn mast_root(&self) -> &Word { + &self.0 + } +} + +impl From for Word { + fn from(root: AccountProcedureRoot) -> Self { + *root.mast_root() + } +} + +impl Serializable for AccountProcedureRoot { + 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 AccountProcedureRoot { + fn read_from(source: &mut R) -> Result { + let mast_root: Word = source.read()?; + Ok(Self::from_raw(mast_root)) + } +} + +// PRINTABLE PROCEDURE +// ================================================================================================ + +/// A printable representation of a single account procedure. +#[derive(Debug, Clone)] +pub struct PrintableProcedure { + mast: Arc, + procedure_root: AccountProcedureRoot, + entrypoint: MastNodeId, +} + +impl PrintableProcedure { + /// Creates a new PrintableProcedure instance from its components. + pub(crate) fn new( + mast: Arc, + procedure_root: AccountProcedureRoot, + entrypoint: MastNodeId, + ) -> Self { + Self { mast, procedure_root, entrypoint } + } + + fn entrypoint(&self) -> &MastNode { + &self.mast[self.entrypoint] + } + + pub(crate) fn mast_root(&self) -> &Word { + self.procedure_root.mast_root() + } +} + +impl PrettyPrint for PrintableProcedure { + fn render(&self) -> miden_core::prettier::Document { + use miden_core::prettier::*; + + indent( + 4, + const_text("begin") + nl() + self.entrypoint().to_pretty_print(&self.mast).render(), + ) + nl() + + const_text("end") + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + + use miden_crypto::utils::{Deserializable, Serializable}; + + use crate::account::{AccountCode, AccountProcedureRoot}; + + #[test] + fn test_serde_account_procedure() { + let account_code = AccountCode::mock(); + + let serialized = account_code.procedures()[0].to_bytes(); + let deserialized = AccountProcedureRoot::read_from_bytes(&serialized).unwrap(); + + assert_eq!(account_code.procedures()[0], deserialized); + } +} diff --git a/crates/miden-protocol/src/account/component/code.rs b/crates/miden-protocol/src/account/component/code.rs new file mode 100644 index 0000000000..2ddcdfca53 --- /dev/null +++ b/crates/miden-protocol/src/account/component/code.rs @@ -0,0 +1,47 @@ +use miden_assembly::Library; +use miden_processor::MastForest; + +// ACCOUNT COMPONENT CODE +// ================================================================================================ + +/// A [`Library`] that has been assembled for use as component code. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountComponentCode(Library); + +impl AccountComponentCode { + /// Returns a reference to the underlying [`Library`] + pub fn as_library(&self) -> &Library { + &self.0 + } + + /// Returns a reference to the code's [`MastForest`] + pub fn mast_forest(&self) -> &MastForest { + self.0.mast_forest().as_ref() + } + + /// Consumes `self` and returns the underlying [`Library`] + pub fn into_library(self) -> Library { + self.0 + } +} + +impl AsRef for AccountComponentCode { + fn as_ref(&self) -> &Library { + self.as_library() + } +} + +// CONVERSIONS +// ================================================================================================ + +impl From for AccountComponentCode { + fn from(value: Library) -> Self { + Self(value) + } +} + +impl From for Library { + fn from(value: AccountComponentCode) -> Self { + value.into_library() + } +} diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs new file mode 100644 index 0000000000..a0f58ae9cd --- /dev/null +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -0,0 +1,223 @@ +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::{String, ToString}; +use core::str::FromStr; + +use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; +use miden_mast_package::{Package, SectionId}; +use miden_processor::DeserializationError; +use semver::Version; + +use super::{AccountType, SchemaRequirement, StorageSchema, StorageValueName}; +use crate::errors::AccountError; + +// ACCOUNT COMPONENT METADATA +// ================================================================================================ + +/// Represents the full component metadata configuration. +/// +/// An account component metadata describes the component alongside its storage layout. +/// The storage layout can declare typed values which must be provided at instantiation time via +/// [InitStorageData](`super::storage::InitStorageData`). These can appear either at the slot level +/// (a singular word slot) or inside composed words as typed fields. +/// +/// When the `std` feature is enabled, this struct allows for serialization and deserialization to +/// and from a TOML file. +/// +/// # Guarantees +/// +/// - The metadata's storage schema does not contain duplicate slot names. +/// - The schema cannot contain protocol-reserved slot names. +/// - Each init-time value name uniquely identifies a single value. The expected init-time metadata +/// can be retrieved with [AccountComponentMetadata::schema_requirements()], which returns a map +/// from keys to [SchemaRequirement] (which indicates the expected value type and optional +/// defaults). +/// +/// # Example +/// +/// ``` +/// use std::collections::{BTreeMap, BTreeSet}; +/// +/// use miden_protocol::account::StorageSlotName; +/// use miden_protocol::account::component::{ +/// AccountComponentMetadata, +/// FeltSchema, +/// InitStorageData, +/// SchemaTypeId, +/// StorageSchema, +/// StorageSlotSchema, +/// StorageValueName, +/// ValueSlotSchema, +/// WordSchema, +/// WordValue, +/// }; +/// use semver::Version; +/// +/// let slot_name = StorageSlotName::new("demo::test_value")?; +/// +/// let word = WordSchema::new_value([ +/// FeltSchema::new_void(), +/// FeltSchema::new_void(), +/// FeltSchema::new_void(), +/// FeltSchema::new_typed(SchemaTypeId::native_felt(), "foo"), +/// ]); +/// +/// let storage_schema = StorageSchema::new([( +/// slot_name.clone(), +/// StorageSlotSchema::Value(ValueSlotSchema::new(Some("demo slot".into()), word)), +/// )])?; +/// +/// let metadata = AccountComponentMetadata::new( +/// "test name".into(), +/// "description of the component".into(), +/// Version::parse("0.1.0")?, +/// BTreeSet::new(), +/// storage_schema, +/// ); +/// +/// // Init value keys are derived from slot name: `demo::test_value.foo`. +/// let value_name = StorageValueName::from_slot_name_with_suffix(&slot_name, "foo")?; +/// let mut init_storage_data = InitStorageData::default(); +/// init_storage_data.set_value(value_name, WordValue::Atomic("300".into()))?; +/// +/// let storage_slots = metadata.storage_schema().build_storage_slots(&init_storage_data)?; +/// assert_eq!(storage_slots.len(), 1); +/// # Ok::<(), Box>(()) +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))] +pub struct AccountComponentMetadata { + /// The human-readable name of the component. + name: String, + + /// A brief description of what this component is and how it works. + description: String, + + /// The version of the component using semantic versioning. + /// This can be used to track and manage component upgrades. + version: Version, + + /// A set of supported target account types for this component. + supported_types: BTreeSet, + + /// Storage schema defining the component's storage layout, defaults, and init-supplied values. + #[cfg_attr(feature = "std", serde(rename = "storage"))] + storage_schema: StorageSchema, +} + +impl AccountComponentMetadata { + /// Create a new [AccountComponentMetadata]. + pub fn new( + name: String, + description: String, + version: Version, + targets: BTreeSet, + storage_schema: StorageSchema, + ) -> Self { + Self { + name, + description, + version, + supported_types: targets, + storage_schema, + } + } + + /// Returns the init-time values requirements for this schema. + /// + /// These values are used for initializing storage slot values or storage map entries. For a + /// full example, refer to the docs for [AccountComponentMetadata]. + /// + /// Types for returned init values are inferred based on their location in the storage layout. + pub fn schema_requirements(&self) -> BTreeMap { + self.storage_schema.schema_requirements().expect("storage schema is validated") + } + + /// Returns the name of the account component. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the description of the account component. + pub fn description(&self) -> &str { + &self.description + } + + /// Returns the semantic version of the account component. + pub fn version(&self) -> &Version { + &self.version + } + + /// Returns the account types supported by the component. + pub fn supported_types(&self) -> &BTreeSet { + &self.supported_types + } + + /// Returns the storage schema of the component. + pub fn storage_schema(&self) -> &StorageSchema { + &self.storage_schema + } +} + +impl TryFrom<&Package> for AccountComponentMetadata { + type Error = AccountError; + + fn try_from(package: &Package) -> Result { + package + .sections + .iter() + .find_map(|section| { + (section.id == SectionId::ACCOUNT_COMPONENT_METADATA).then(|| { + AccountComponentMetadata::read_from_bytes(§ion.data).map_err(|err| { + AccountError::other_with_source( + "failed to deserialize account component metadata", + err, + ) + }) + }) + }) + .transpose()? + .ok_or_else(|| { + AccountError::other( + "package does not contain account component metadata section - packages without explicit metadata may be intended for other purposes (e.g., note scripts, transaction scripts)", + ) + }) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AccountComponentMetadata { + fn write_into(&self, target: &mut W) { + self.name.write_into(target); + self.description.write_into(target); + self.version.to_string().write_into(target); + self.supported_types.write_into(target); + self.storage_schema.write_into(target); + } +} + +impl Deserializable for AccountComponentMetadata { + fn read_from(source: &mut R) -> Result { + let name = String::read_from(source)?; + let description = String::read_from(source)?; + if !description.is_ascii() { + return Err(DeserializationError::InvalidValue( + "description must contain only ASCII characters".to_string(), + )); + } + let version = semver::Version::from_str(&String::read_from(source)?) + .map_err(|err: semver::Error| DeserializationError::InvalidValue(err.to_string()))?; + let supported_types = BTreeSet::::read_from(source)?; + let storage_schema = StorageSchema::read_from(source)?; + + Ok(Self { + name, + description, + version, + supported_types, + storage_schema, + }) + } +} diff --git a/crates/miden-objects/src/account/component/mod.rs b/crates/miden-protocol/src/account/component/mod.rs similarity index 62% rename from crates/miden-objects/src/account/component/mod.rs rename to crates/miden-protocol/src/account/component/mod.rs index 22874bc06e..b77d2c2e0e 100644 --- a/crates/miden-objects/src/account/component/mod.rs +++ b/crates/miden-protocol/src/account/component/mod.rs @@ -1,55 +1,27 @@ use alloc::collections::BTreeSet; use alloc::vec::Vec; -use miden_assembly::ast::QualifiedProcedureName; -use miden_assembly::{Assembler, Library, Parse}; -use miden_core::utils::Deserializable; -use miden_mast_package::{Package, SectionId}; -use miden_processor::MastForest; +use miden_mast_package::{MastArtifact, Package}; -mod template; -pub use template::*; +mod metadata; +pub use metadata::*; -use crate::account::{AccountType, StorageSlot}; -use crate::{AccountError, Word}; +pub mod storage; +pub use storage::*; -// IMPLEMENTATIONS -// ================================================================================================ +mod code; +pub use code::AccountComponentCode; -impl TryFrom for AccountComponentTemplate { - type Error = AccountError; - - fn try_from(package: Package) -> Result { - let library = package.unwrap_library().as_ref().clone(); - - // Look for account component metadata in sections - let metadata = package - .sections - .iter() - .find_map(|section| { - (section.id == SectionId::ACCOUNT_COMPONENT_METADATA).then(|| { - AccountComponentMetadata::read_from_bytes(§ion.data) - .map_err(|err| { - AccountError::other_with_source( - "failed to deserialize account component metadata", - err, - ) - }) - }) - }) - .transpose()? - .ok_or_else(|| { - AccountError::other( - "package does not contain account component metadata section - packages without explicit metadata may be intended for other purposes (e.g., note scripts, transaction scripts)", - ) - })?; +use crate::account::{AccountType, StorageSlot}; +use crate::assembly::Path; +use crate::errors::AccountError; +use crate::{MastForest, Word}; - Ok(AccountComponentTemplate::new(metadata, library)) - } -} +// ACCOUNT COMPONENT +// ================================================================================================ -/// An [`AccountComponent`] defines a [`Library`] of code and the initial value and types of -/// the [`StorageSlot`]s it accesses. +/// An [`AccountComponent`] defines a [`Library`](miden_assembly::Library) of code and the initial +/// value and types of the [`StorageSlot`]s it accesses. /// /// One or more components can be used to built [`AccountCode`](crate::account::AccountCode) and /// [`AccountStorage`](crate::account::AccountStorage). @@ -65,8 +37,9 @@ impl TryFrom for AccountComponentTemplate { /// is forced to explicitly define what it supports. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountComponent { - pub(super) library: Library, + pub(super) code: AccountComponentCode, pub(super) storage_slots: Vec, + pub(super) metadata: Option, pub(super) supported_types: BTreeSet, } @@ -88,90 +61,94 @@ impl AccountComponent { /// /// Returns an error if: /// - The number of given [`StorageSlot`]s exceeds 255. - pub fn new(code: Library, storage_slots: Vec) -> Result { + pub fn new( + code: impl Into, + storage_slots: Vec, + ) -> Result { // Check that we have less than 256 storage slots. u8::try_from(storage_slots.len()) .map_err(|_| AccountError::StorageTooManySlots(storage_slots.len() as u64))?; Ok(Self { - library: code, + code: code.into(), storage_slots, + metadata: None, supported_types: BTreeSet::new(), }) } - /// Returns a new [`AccountComponent`] whose library is compiled from the provided `source_code` - /// using the specified `assembler` and with the given `storage_slots`. - /// - /// All procedures exported from the provided code will become members of the account's public - /// interface when added to an [`AccountCode`](crate::account::AccountCode). + /// Creates an [`AccountComponent`] from a [`Package`] using [`InitStorageData`]. /// - /// # Errors + /// This method provides type safety by leveraging the component's metadata to validate + /// storage initialization data. The package must contain explicit account component metadata. /// - /// Returns an error if: - /// - the compilation of the provided source code fails. - /// - The number of storage slots exceeds 255. - pub fn compile( - source_code: impl Parse, - assembler: Assembler, - storage_slots: Vec, - ) -> Result { - let library = assembler - .assemble_library([source_code]) - .map_err(AccountError::AccountComponentAssemblyError)?; - - Self::new(library, storage_slots) - } - - /// Instantiates an [AccountComponent] from the [AccountComponentTemplate]. + /// # Arguments /// - /// The template's component metadata might contain placeholders, which can be replaced by - /// mapping storage placeholders to values through the `init_storage_data` parameter. + /// * `package` - The package containing the [`Library`](miden_assembly::Library) and account + /// component metadata + /// * `init_storage_data` - The initialization data for storage slots /// /// # Errors /// - /// - If any of the component's storage entries cannot be transformed into a valid storage slot. - /// This could be because the metadata is invalid, or storage values were not provided (or - /// they are not of a valid type) - pub fn from_template( - template: &AccountComponentTemplate, + /// Returns an error if: + /// - The package does not contain a library artifact + /// - The package does not contain account component metadata + /// - The metadata cannot be deserialized from the package + /// - The storage initialization fails due to invalid or missing data + /// - The component creation fails + pub fn from_package( + package: &Package, init_storage_data: &InitStorageData, - ) -> Result { - let mut storage_slots = vec![]; - for storage_entry in template.metadata().storage_entries() { - let entry_storage_slots = storage_entry - .try_build_storage_slots(init_storage_data) - .map_err(AccountError::AccountComponentTemplateInstantiationError)?; - storage_slots.extend(entry_storage_slots); - } + ) -> Result { + let metadata = AccountComponentMetadata::try_from(package)?; + let library = match &package.mast { + MastArtifact::Library(library) => library.as_ref().clone(), + MastArtifact::Executable(_) => { + return Err(AccountError::other( + "expected Package to contain a library, but got an executable", + )); + }, + }; - Ok(AccountComponent::new(template.library().clone(), storage_slots)? - .with_supported_types(template.metadata().supported_types().clone())) + let component_code = AccountComponentCode::from(library); + Self::from_library(&component_code, &metadata, init_storage_data) } - /// Creates an [`AccountComponent`] from a [`Package`] using [`InitStorageData`]. + /// Creates an [`AccountComponent`] from an [`AccountComponentCode`] and + /// [`AccountComponentMetadata`]. /// /// This method provides type safety by leveraging the component's metadata to validate - /// storage initialization data. The package must contain explicit account component metadata. + /// the passed storage initialization data ([`InitStorageData`]). /// /// # Arguments /// - /// * `package` - The package containing the library and account component metadata + /// * `library` - The component's assembled code + /// * `account_component_metadata` - The component's metadata, which describes the storage + /// layout /// * `init_storage_data` - The initialization data for storage slots /// /// # Errors /// /// Returns an error if: + /// - The package does not contain a library artifact /// - The package does not contain account component metadata - /// - The package cannot be converted to an [`AccountComponentTemplate`] + /// - The metadata cannot be deserialized from the package /// - The storage initialization fails due to invalid or missing data /// - The component creation fails - pub fn from_package_with_init_data( - package: &Package, + pub fn from_library( + library: &AccountComponentCode, + account_component_metadata: &AccountComponentMetadata, init_storage_data: &InitStorageData, ) -> Result { - let template = AccountComponentTemplate::try_from(package.clone())?; - Self::from_template(&template, init_storage_data) + let storage_slots = account_component_metadata + .storage_schema() + .build_storage_slots(init_storage_data) + .map_err(|err| { + AccountError::other_with_source("failed to instantiate account component", err) + })?; + + Ok(AccountComponent::new(library.clone(), storage_slots)? + .with_metadata(account_component_metadata.clone())) } // ACCESSORS @@ -183,14 +160,14 @@ impl AccountComponent { .expect("storage slots len should fit in u8 per the constructor") } - /// Returns a reference to the underlying [`Library`] of this component. - pub fn library(&self) -> &Library { - &self.library + /// Returns a reference to the underlying [`AccountComponentCode`] of this component. + pub fn component_code(&self) -> &AccountComponentCode { + &self.code } /// Returns a reference to the underlying [`MastForest`] of this component. pub fn mast_forest(&self) -> &MastForest { - self.library.mast_forest().as_ref() + self.code.mast_forest() } /// Returns a slice of the underlying [`StorageSlot`]s of this component. @@ -198,6 +175,16 @@ impl AccountComponent { self.storage_slots.as_slice() } + /// Returns the component metadata, if any. + pub fn metadata(&self) -> Option<&AccountComponentMetadata> { + self.metadata.as_ref() + } + + /// Returns the storage schema associated with this component, if any. + pub fn storage_schema(&self) -> Option<&StorageSchema> { + self.metadata.as_ref().map(AccountComponentMetadata::storage_schema) + } + /// Returns a reference to the supported [`AccountType`]s. pub fn supported_types(&self) -> &BTreeSet { &self.supported_types @@ -211,7 +198,7 @@ impl AccountComponent { /// Returns a vector of tuples (digest, is_auth) for all procedures in this component. pub fn get_procedures(&self) -> Vec<(Word, bool)> { let mut procedures = Vec::new(); - for module in self.library.module_infos() { + for module in self.code.as_library().module_infos() { for (_, procedure_info) in module.procedures() { let is_auth = procedure_info.name.starts_with("auth_"); procedures.push((procedure_info.digest, is_auth)); @@ -220,13 +207,10 @@ impl AccountComponent { procedures } - /// Returns the digest of the procedure with the specified name, or `None` if it was not found + /// Returns the digest of the procedure with the specified path, or `None` if it was not found /// in this component's library or its library path is malformed. - pub fn get_procedure_root_by_name( - &self, - proc_name: impl TryInto, - ) -> Option { - self.library.get_procedure_root_by_name(proc_name) + pub fn get_procedure_root_by_path(&self, proc_name: impl AsRef) -> Option { + self.code.as_library().get_procedure_root_by_path(proc_name) } // MUTATORS @@ -250,6 +234,13 @@ impl AccountComponent { self } + /// Attaches metadata to this component for downstream schema commitments and introspection. + pub fn with_metadata(mut self, metadata: AccountComponentMetadata) -> Self { + self.supported_types = metadata.supported_types().clone(); + self.metadata = Some(metadata); + self + } + /// Sets the [`AccountType`]s supported by this component to all account types. pub fn with_supports_all_types(mut self) -> Self { self.supported_types.extend([ @@ -262,9 +253,9 @@ impl AccountComponent { } } -impl From for Library { +impl From for AccountComponentCode { fn from(component: AccountComponent) -> Self { - component.library + component.code } } @@ -276,14 +267,21 @@ mod tests { use miden_assembly::Assembler; use miden_core::utils::Serializable; - use miden_mast_package::{MastArtifact, Package, PackageManifest, Section}; + use miden_mast_package::{ + MastArtifact, + Package, + PackageKind, + PackageManifest, + Section, + SectionId, + }; use semver::Version; use super::*; use crate::testing::account_code::CODE; #[test] - fn test_try_from_package_for_template() { + fn test_extract_metadata_from_package() { // Create a simple library for testing let library = Assembler::default().assemble_library([CODE]).unwrap(); @@ -293,16 +291,15 @@ mod tests { "A test component".to_string(), Version::new(1, 0, 0), BTreeSet::from_iter([AccountType::RegularAccountImmutableCode]), - vec![], - ) - .unwrap(); + StorageSchema::default(), + ); let metadata_bytes = metadata.to_bytes(); let package_with_metadata = Package { name: "test_package".to_string(), mast: MastArtifact::Library(Arc::new(library.clone())), manifest: PackageManifest::new(None), - + kind: PackageKind::AccountComponent, sections: vec![Section::new( SectionId::ACCOUNT_COMPONENT_METADATA, metadata_bytes.clone(), @@ -311,11 +308,11 @@ mod tests { description: None, }; - let template = AccountComponentTemplate::try_from(package_with_metadata).unwrap(); - assert_eq!(template.metadata().name(), "test_component"); + let extracted_metadata = + AccountComponentMetadata::try_from(&package_with_metadata).unwrap(); + assert_eq!(extracted_metadata.name(), "test_component"); assert!( - template - .metadata() + extracted_metadata .supported_types() .contains(&AccountType::RegularAccountImmutableCode) ); @@ -325,21 +322,23 @@ mod tests { name: "test_package_no_metadata".to_string(), mast: MastArtifact::Library(Arc::new(library)), manifest: PackageManifest::new(None), + kind: PackageKind::AccountComponent, sections: vec![], // No metadata section version: Default::default(), description: None, }; - let result = AccountComponentTemplate::try_from(package_without_metadata); + let result = AccountComponentMetadata::try_from(&package_without_metadata); assert!(result.is_err()); let error_msg = result.unwrap_err().to_string(); assert!(error_msg.contains("package does not contain account component metadata")); } #[test] - fn test_from_package_with_init_data() { + fn test_from_library_with_init_data() { // Create a simple library for testing let library = Assembler::default().assemble_library([CODE]).unwrap(); + let component_code = AccountComponentCode::from(library.clone()); // Create metadata for the component let metadata = AccountComponentMetadata::new( @@ -350,28 +349,14 @@ mod tests { AccountType::RegularAccountImmutableCode, AccountType::RegularAccountUpdatableCode, ]), - vec![], - ) - .unwrap(); - - // Create a package with metadata - let package = Package { - name: "test_package_init_data".to_string(), - mast: MastArtifact::Library(Arc::new(library.clone())), - manifest: PackageManifest::new(None), - sections: vec![Section::new( - SectionId::ACCOUNT_COMPONENT_METADATA, - metadata.to_bytes(), - )], - version: Default::default(), - description: None, - }; + StorageSchema::default(), + ); // Test with empty init data - this tests the complete workflow: - // Package -> AccountComponentTemplate -> AccountComponent + // Library + Metadata -> AccountComponent let init_data = InitStorageData::default(); let component = - AccountComponent::from_package_with_init_data(&package, &init_data).unwrap(); + AccountComponent::from_library(&component_code, &metadata, &init_data).unwrap(); // Verify the component was created correctly assert_eq!(component.storage_size(), 0); @@ -383,14 +368,14 @@ mod tests { let package_without_metadata = Package { name: "test_package_no_metadata".to_string(), mast: MastArtifact::Library(Arc::new(library)), + kind: PackageKind::AccountComponent, manifest: PackageManifest::new(None), sections: vec![], // No metadata section version: Default::default(), description: None, }; - let result = - AccountComponent::from_package_with_init_data(&package_without_metadata, &init_data); + let result = AccountComponent::from_package(&package_without_metadata, &init_data); assert!(result.is_err()); let error_msg = result.unwrap_err().to_string(); assert!(error_msg.contains("package does not contain account component metadata")); diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs new file mode 100644 index 0000000000..999f552a3c --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -0,0 +1,262 @@ +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use thiserror::Error; + +use super::StorageValueName; +use crate::account::StorageSlotName; +use crate::{Felt, FieldElement, Word}; + +/// A word value provided via [`InitStorageData`]. +/// +/// This is used for defining specific values in relation to a component's schema, where each value +/// is supplied as either a fully-typed word, an atomic string (e.g. `"0x1234"`, `"16"`, `"BTC"`), +/// or an array of 4 field elements. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WordValue { + /// A fully-typed word value. + FullyTyped(Word), + /// Represents a single word value, given by a single string input. + Atomic(String), + /// Represents a word through four string-encoded field elements. + Elements([String; 4]), +} + +impl From for WordValue { + fn from(value: Word) -> Self { + WordValue::FullyTyped(value) + } +} + +impl From for WordValue { + fn from(value: String) -> Self { + WordValue::Atomic(value) + } +} + +impl From<&str> for WordValue { + fn from(value: &str) -> Self { + WordValue::Atomic(String::from(value)) + } +} + +// CONVERSIONS +// ==================================================================================================== + +impl From for WordValue { + /// Converts a [`Felt`] to a [`WordValue`] as a Word in the form `[0, 0, 0, felt]`. + fn from(value: Felt) -> Self { + WordValue::FullyTyped(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, value])) + } +} + +impl From<[Felt; 4]> for WordValue { + fn from(value: [Felt; 4]) -> Self { + WordValue::FullyTyped(Word::from(value)) + } +} + +// INIT STORAGE DATA +// ==================================================================================================== + +/// Represents the data required to initialize storage entries when instantiating an +/// [AccountComponent](crate::account::AccountComponent) from component metadata (either provided +/// directly or extracted from a package). +/// +/// An [`InitStorageData`] can be created from a TOML string when the `std` feature flag is set. +#[derive(Clone, Debug, Default)] +pub struct InitStorageData { + /// A mapping of storage value names to their init values. + value_entries: BTreeMap, + /// A mapping of storage map slot names to their init key/value entries. + map_entries: BTreeMap>, +} + +impl InitStorageData { + /// Creates a new instance of [InitStorageData], validating that there are no conflicting + /// entries. + /// + /// # Errors + /// + /// Returns an error if: + /// - A slot has both value entries and map entries + /// - A slot has both a slot-level value and field values + pub fn new( + value_entries: BTreeMap, + map_entries: BTreeMap>, + ) -> Result { + // Check for conflicts between value entries and map entries + for slot_name in map_entries.keys() { + if value_entries.keys().any(|v| v.slot_name() == slot_name) { + return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); + } + } + + // Check for conflicts between slot-level values and field values + for value_name in value_entries.keys() { + if value_name.field_name().is_none() { + // This is a slot-level value; check if there are field entries for this slot + let has_field_entries = value_entries.keys().any(|other| { + other.slot_name() == value_name.slot_name() && other.field_name().is_some() + }); + if has_field_entries { + return Err(InitStorageDataError::ConflictingEntries( + value_name.slot_name().as_str().into(), + )); + } + } + } + + Ok(InitStorageData { value_entries, map_entries }) + } + + /// Returns a reference to the underlying init values map. + pub fn values(&self) -> &BTreeMap { + &self.value_entries + } + + /// Returns a reference to the underlying init map entries. + pub fn maps(&self) -> &BTreeMap> { + &self.map_entries + } + + /// Returns a reference to the stored init value for the given name. + pub fn value_entry(&self, name: &StorageValueName) -> Option<&WordValue> { + self.value_entries.get(name) + } + + /// Returns a reference to the stored init value for a full slot name. + pub fn slot_value_entry(&self, slot_name: &StorageSlotName) -> Option<&WordValue> { + let name = StorageValueName::from_slot_name(slot_name); + self.value_entries.get(&name) + } + + /// Returns the map entries associated with the given storage map slot name, if any. + pub fn map_entries(&self, slot_name: &StorageSlotName) -> Option<&Vec<(WordValue, WordValue)>> { + self.map_entries.get(slot_name) + } + + /// Returns true if any init value entry targets the given slot name. + pub fn has_value_entries_for_slot(&self, slot_name: &StorageSlotName) -> bool { + self.value_entries.keys().any(|name| name.slot_name() == slot_name) + } + + /// Returns true if any init value entry targets a field of the given slot name. + pub fn has_field_entries_for_slot(&self, slot_name: &StorageSlotName) -> bool { + self.value_entries + .keys() + .any(|name| name.slot_name() == slot_name && name.field_name().is_some()) + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Inserts a value entry, returning an error on duplicate or conflicting keys. + /// + /// The value can be any type that implements `Into`, e.g.: + /// + /// - `Word`: a fully-typed word value + /// - `[Felt; 4]`: converted to a Word + /// - `Felt`: converted to `[0, 0, 0, felt]` + /// - `String` or `&str`: a parseable string value + /// - `WordValue`: a word value (fully typed, atomic, or elements) + pub fn insert_value( + &mut self, + name: StorageValueName, + value: impl Into, + ) -> Result<(), InitStorageDataError> { + if self.value_entries.contains_key(&name) { + return Err(InitStorageDataError::DuplicateKey(name.to_string())); + } + if self.map_entries.contains_key(name.slot_name()) { + return Err(InitStorageDataError::ConflictingEntries(name.slot_name().as_str().into())); + } + self.value_entries.insert(name, value.into()); + Ok(()) + } + + /// Sets a value entry, overriding any existing entry for the name. + /// + /// Returns an error if the [`StorageValueName`] has been used for a map slot. + pub fn set_value( + &mut self, + name: StorageValueName, + value: impl Into, + ) -> Result<(), InitStorageDataError> { + if self.map_entries.contains_key(name.slot_name()) { + return Err(InitStorageDataError::ConflictingEntries(name.slot_name().as_str().into())); + } + self.value_entries.insert(name, value.into()); + Ok(()) + } + + /// Inserts a single map entry, returning an error on duplicate or conflicting keys. + /// + /// See [`Self::insert_value`] for examples of supported types for `key` and `value`. + pub fn insert_map_entry( + &mut self, + slot_name: StorageSlotName, + key: impl Into, + value: impl Into, + ) -> Result<(), InitStorageDataError> { + if self.has_value_entries_for_slot(&slot_name) { + return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); + } + + let key = key.into(); + if let Some(entries) = self.map_entries.get(&slot_name) + && entries.iter().any(|(existing_key, _)| existing_key == &key) + { + return Err(InitStorageDataError::DuplicateKey(format!( + "{}[{key:?}]", + slot_name.as_str() + ))); + } + + self.map_entries.entry(slot_name).or_default().push((key, value.into())); + Ok(()) + } + + /// Sets map entries for the slot, replacing any existing entries. + /// + /// Returns an error if there are conflicting value entries. + pub fn set_map_values( + &mut self, + slot_name: StorageSlotName, + entries: Vec<(WordValue, WordValue)>, + ) -> Result<(), InitStorageDataError> { + if self.has_value_entries_for_slot(&slot_name) { + return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into())); + } + self.map_entries.insert(slot_name, entries); + Ok(()) + } + + /// Merges another [`InitStorageData`] into this one, overwriting value entries and appending + /// map entries. + pub fn merge_with(&mut self, other: InitStorageData) { + self.value_entries.extend(other.value_entries); + for (slot_name, entries) in other.map_entries { + self.map_entries.entry(slot_name).or_default().extend(entries); + } + } + + /// Merges another [`InitStorageData`] into this one, overwriting value entries and appending + /// map entries. + pub fn merge_from(&mut self, other: InitStorageData) { + self.merge_with(other); + } +} + +// ERRORS +// ==================================================================================================== + +/// Error returned when creating [`InitStorageData`] with invalid entries. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum InitStorageDataError { + #[error("duplicate init key `{0}`")] + DuplicateKey(String), + #[error("conflicting init entries for `{0}`")] + ConflictingEntries(String), +} diff --git a/crates/miden-protocol/src/account/component/storage/mod.rs b/crates/miden-protocol/src/account/component/storage/mod.rs new file mode 100644 index 0000000000..06b24bd768 --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/mod.rs @@ -0,0 +1,14 @@ +mod schema; +pub use schema::*; + +mod value_name; +pub use value_name::{StorageValueName, StorageValueNameError}; + +mod type_registry; +pub use type_registry::{SchemaRequirement, SchemaTypeError, SchemaTypeId}; + +mod init_storage_data; +pub use init_storage_data::{InitStorageData, InitStorageDataError, WordValue}; + +#[cfg(feature = "std")] +pub mod toml; diff --git a/crates/miden-protocol/src/account/component/storage/schema.rs b/crates/miden-protocol/src/account/component/storage/schema.rs new file mode 100644 index 0000000000..710e70af94 --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/schema.rs @@ -0,0 +1,1231 @@ +use alloc::boxed::Box; +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; +use miden_processor::DeserializationError; + +use super::type_registry::{SCHEMA_TYPE_REGISTRY, SchemaRequirement, SchemaTypeId}; +use super::{InitStorageData, StorageValueName, WordValue}; +use crate::account::storage::is_reserved_slot_name; +use crate::account::{StorageMap, StorageSlot, StorageSlotName}; +use crate::crypto::utils::bytes_to_elements_with_padding; +use crate::errors::AccountComponentTemplateError; +use crate::{Felt, FieldElement, Hasher, Word}; + +// STORAGE SCHEMA +// ================================================================================================ + +/// Describes the storage schema of an account component in terms of its named storage slots. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct StorageSchema { + slots: BTreeMap, +} + +impl StorageSchema { + /// Creates a new [`StorageSchema`]. + /// + /// # Errors + /// - If `fields` contains duplicate slot names. + /// - If `fields` contains the protocol-reserved faucet metadata slot name. + /// - If any slot schema is invalid. + /// - If multiple schema fields map to the same init value name. + pub fn new( + slots: impl IntoIterator, + ) -> Result { + let mut map = BTreeMap::new(); + for (slot_name, schema) in slots { + if map.insert(slot_name.clone(), schema).is_some() { + return Err(AccountComponentTemplateError::DuplicateSlotName(slot_name)); + } + } + + let schema = Self { slots: map }; + schema.validate()?; + Ok(schema) + } + + /// Returns an iterator over `(slot_name, schema)` pairs in slot-id order. + pub fn iter(&self) -> impl Iterator { + self.slots.iter() + } + + /// Returns a reference to the underlying slots map. + pub fn slots(&self) -> &BTreeMap { + &self.slots + } + + /// Builds the initial [`StorageSlot`]s for this schema using the provided initialization data. + pub fn build_storage_slots( + &self, + init_storage_data: &InitStorageData, + ) -> Result, AccountComponentTemplateError> { + self.slots + .iter() + .map(|(slot_name, schema)| schema.try_build_storage_slot(slot_name, init_storage_data)) + .collect() + } + + /// Returns a commitment to this storage schema definition. + /// + /// The commitment is computed over the serialized schema and does not include defaults. + pub fn commitment(&self) -> Word { + let mut bytes = Vec::new(); + self.write_into_with_optional_defaults(&mut bytes, false); + let elements = bytes_to_elements_with_padding(&bytes); + Hasher::hash_elements(&elements) + } + + /// Returns init-value requirements for the entire schema. + /// + /// The returned map includes both required values (no `default_value`) and optional values + /// (with `default_value`), and excludes map entries. + pub fn schema_requirements( + &self, + ) -> Result, AccountComponentTemplateError> { + let mut requirements = BTreeMap::new(); + for (slot_name, schema) in self.slots.iter() { + schema.collect_init_value_requirements(slot_name, &mut requirements)?; + } + Ok(requirements) + } + + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write_u16(self.slots.len() as u16); + for (slot_name, schema) in self.slots.iter() { + target.write(slot_name); + schema.write_into_with_optional_defaults(target, include_defaults); + } + } + + /// Validates schema-level invariants across all slots. + fn validate(&self) -> Result<(), AccountComponentTemplateError> { + let mut init_values = BTreeMap::new(); + + for (slot_name, schema) in self.slots.iter() { + if is_reserved_slot_name(slot_name) { + return Err(AccountComponentTemplateError::ReservedSlotName(slot_name.clone())); + } + + schema.validate()?; + schema.collect_init_value_requirements(slot_name, &mut init_values)?; + } + + Ok(()) + } +} + +impl Serializable for StorageSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + +impl Deserializable for StorageSchema { + fn read_from(source: &mut R) -> Result { + let num_entries = source.read_u16()? as usize; + let mut fields = BTreeMap::new(); + + for _ in 0..num_entries { + let slot_name = StorageSlotName::read_from(source)?; + let schema = StorageSlotSchema::read_from(source)?; + + if fields.insert(slot_name.clone(), schema).is_some() { + return Err(DeserializationError::InvalidValue(format!( + "duplicate slot name in storage schema: {slot_name}", + ))); + } + } + + let schema = StorageSchema::new(fields) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + Ok(schema) + } +} + +fn validate_description_ascii(description: &str) -> Result<(), AccountComponentTemplateError> { + if description.is_ascii() { + Ok(()) + } else { + Err(AccountComponentTemplateError::InvalidSchema( + "description must contain only ASCII characters".to_string(), + )) + } +} + +// STORAGE SLOT SCHEMA +// ================================================================================================ + +/// Describes the schema for a storage slot. +/// Can describe either a value slot, or a map slot. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StorageSlotSchema { + Value(ValueSlotSchema), + Map(MapSlotSchema), +} + +impl StorageSlotSchema { + fn collect_init_value_requirements( + &self, + slot_name: &StorageSlotName, + requirements: &mut BTreeMap, + ) -> Result<(), AccountComponentTemplateError> { + let slot_name = StorageValueName::from_slot_name(slot_name); + match self { + StorageSlotSchema::Value(slot) => { + slot.collect_init_value_requirements(slot_name, requirements) + }, + StorageSlotSchema::Map(_) => Ok(()), + } + } + + /// Builds a [`StorageSlot`] for the specified `slot_name` using the provided initialization + /// data. + pub fn try_build_storage_slot( + &self, + slot_name: &StorageSlotName, + init_storage_data: &InitStorageData, + ) -> Result { + match self { + StorageSlotSchema::Value(slot) => { + let word = slot.try_build_word(init_storage_data, slot_name)?; + Ok(StorageSlot::with_value(slot_name.clone(), word)) + }, + StorageSlotSchema::Map(slot) => { + let storage_map = slot.try_build_map(init_storage_data, slot_name)?; + Ok(StorageSlot::with_map(slot_name.clone(), storage_map)) + }, + } + } + + /// Validates this slot schema's internal invariants. + pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { + match self { + StorageSlotSchema::Value(slot) => slot.validate()?, + StorageSlotSchema::Map(slot) => slot.validate()?, + } + + Ok(()) + } + + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + match self { + StorageSlotSchema::Value(slot) => { + target.write_u8(0u8); + slot.write_into_with_optional_defaults(target, include_defaults); + }, + StorageSlotSchema::Map(slot) => { + target.write_u8(1u8); + slot.write_into_with_optional_defaults(target, include_defaults); + }, + } + } +} + +impl Serializable for StorageSlotSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + +impl Deserializable for StorageSlotSchema { + fn read_from(source: &mut R) -> Result { + let variant_tag = source.read_u8()?; + match variant_tag { + 0 => Ok(StorageSlotSchema::Value(ValueSlotSchema::read_from(source)?)), + 1 => Ok(StorageSlotSchema::Map(MapSlotSchema::read_from(source)?)), + _ => Err(DeserializationError::InvalidValue(format!( + "unknown variant tag '{variant_tag}' for StorageSlotSchema" + ))), + } + } +} + +// WORDS +// ================================================================================================ + +/// Defines how a word slot is described within the component's storage schema. +/// +/// Each word schema can either describe a whole-word typed value supplied at instantiation time +/// (`Simple`) or a composite word that explicitly defines each felt element (`Composite`). +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum WordSchema { + /// A whole-word typed value supplied at instantiation time. + Simple { + r#type: SchemaTypeId, + default_value: Option, + }, + /// A composed word that may mix defaults and typed fields. + Composite { value: [FeltSchema; 4] }, +} + +impl WordSchema { + pub fn new_simple(r#type: SchemaTypeId) -> Self { + WordSchema::Simple { r#type, default_value: None } + } + + pub fn new_simple_with_default(r#type: SchemaTypeId, default_value: Word) -> Self { + WordSchema::Simple { + r#type, + default_value: Some(default_value), + } + } + + pub fn new_value(value: impl Into<[FeltSchema; 4]>) -> Self { + WordSchema::Composite { value: value.into() } + } + + pub fn value(&self) -> Option<&[FeltSchema; 4]> { + match self { + WordSchema::Composite { value } => Some(value), + WordSchema::Simple { .. } => None, + } + } + + /// Returns the schema type identifier associated with whole-word init-supplied values. + pub fn word_type(&self) -> SchemaTypeId { + match self { + WordSchema::Simple { r#type, .. } => r#type.clone(), + WordSchema::Composite { .. } => SchemaTypeId::native_word(), + } + } + + fn collect_init_value_requirements( + &self, + value_name: StorageValueName, + description: Option, + requirements: &mut BTreeMap, + ) -> Result<(), AccountComponentTemplateError> { + match self { + WordSchema::Simple { r#type, default_value } => { + if *r#type == SchemaTypeId::void() { + return Ok(()); + } + + let default_value = default_value.map(|word| { + SCHEMA_TYPE_REGISTRY.display_word(r#type, word).value().to_string() + }); + + if requirements + .insert( + value_name.clone(), + SchemaRequirement { + description, + r#type: r#type.clone(), + default_value, + }, + ) + .is_some() + { + return Err(AccountComponentTemplateError::DuplicateInitValueName(value_name)); + } + + Ok(()) + }, + WordSchema::Composite { value } => { + for felt in value.iter() { + felt.collect_init_value_requirements(value_name.clone(), requirements)?; + } + Ok(()) + }, + } + } + + /// Validates the word schema type, defaults, and inner felts (if any). + fn validate(&self) -> Result<(), AccountComponentTemplateError> { + let type_exists = SCHEMA_TYPE_REGISTRY.contains_word_type(&self.word_type()); + if !type_exists { + return Err(AccountComponentTemplateError::InvalidType( + self.word_type().to_string(), + "Word".into(), + )); + } + + if let WordSchema::Simple { + r#type, + default_value: Some(default_value), + } = self + { + SCHEMA_TYPE_REGISTRY + .validate_word_value(r#type, *default_value) + .map_err(AccountComponentTemplateError::StorageValueParsingError)?; + } + + if let Some(felts) = self.value() { + for felt in felts { + felt.validate()?; + } + } + + Ok(()) + } + + /// Builds a [`Word`] from the provided initialization data according to this schema. + /// + /// For simple schemas, expects a direct slot value (not map or field entries). + /// For composite schemas, either parses a single value or builds the word from individual + /// felt entries. + pub(crate) fn try_build_word( + &self, + init_storage_data: &InitStorageData, + slot_name: &StorageSlotName, + ) -> Result { + let slot_prefix = StorageValueName::from_slot_name(slot_name); + let slot_value = init_storage_data.slot_value_entry(slot_name); + let has_fields = init_storage_data.has_field_entries_for_slot(slot_name); + + if init_storage_data.map_entries(slot_name).is_some() { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a value, got a map".into(), + )); + } + + match self { + WordSchema::Simple { r#type, default_value } => { + if has_fields { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a value, got field entries".into(), + )); + } + match slot_value { + Some(value) => parse_storage_value_with_schema(self, value, &slot_prefix), + None => { + if *r#type == SchemaTypeId::void() { + Ok(Word::empty()) + } else { + default_value.as_ref().copied().ok_or_else(|| { + AccountComponentTemplateError::InitValueNotProvided(slot_prefix) + }) + } + }, + } + }, + WordSchema::Composite { value } => { + if let Some(value) = slot_value { + if has_fields { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a single value, got both value and field entries".into(), + )); + } + return parse_storage_value_with_schema(self, value, &slot_prefix); + } + + let mut result = [Felt::ZERO; 4]; + for (index, felt_schema) in value.iter().enumerate() { + result[index] = felt_schema.try_build_felt(init_storage_data, slot_name)?; + } + Ok(Word::from(result)) + }, + } + } + + pub(crate) fn validate_word_value( + &self, + slot_prefix: &StorageValueName, + label: &str, + word: Word, + ) -> Result<(), AccountComponentTemplateError> { + match self { + WordSchema::Simple { r#type, .. } => { + SCHEMA_TYPE_REGISTRY.validate_word_value(r#type, word).map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("{label} does not match `{}`: {err}", r#type), + ) + }) + }, + WordSchema::Composite { value } => { + for (index, felt_schema) in value.iter().enumerate() { + let felt_type = felt_schema.felt_type(); + SCHEMA_TYPE_REGISTRY.validate_felt_value(&felt_type, word[index]).map_err( + |err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("{label}[{index}] does not match `{felt_type}`: {err}"), + ) + }, + )?; + } + + Ok(()) + }, + } + } + + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + match self { + WordSchema::Simple { r#type, default_value } => { + target.write_u8(0); + target.write(r#type); + let default_value = if include_defaults { *default_value } else { None }; + target.write(default_value); + }, + WordSchema::Composite { value } => { + target.write_u8(1); + for felt in value.iter() { + felt.write_into_with_optional_defaults(target, include_defaults); + } + }, + } + } +} + +impl Serializable for WordSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + +impl Deserializable for WordSchema { + fn read_from(source: &mut R) -> Result { + let tag = source.read_u8()?; + match tag { + 0 => { + let r#type = SchemaTypeId::read_from(source)?; + let default_value = Option::::read_from(source)?; + Ok(WordSchema::Simple { r#type, default_value }) + }, + 1 => { + let value = <[FeltSchema; 4]>::read_from(source)?; + Ok(WordSchema::Composite { value }) + }, + other => Err(DeserializationError::InvalidValue(format!( + "unknown tag '{other}' for WordSchema" + ))), + } + } +} + +impl From<[FeltSchema; 4]> for WordSchema { + fn from(value: [FeltSchema; 4]) -> Self { + WordSchema::new_value(value) + } +} + +impl From<[Felt; 4]> for WordSchema { + fn from(value: [Felt; 4]) -> Self { + WordSchema::new_simple_with_default(SchemaTypeId::native_word(), Word::from(value)) + } +} + +// FELT SCHEMA +// ================================================================================================ + +/// Supported element schema descriptors for a component's storage entries. +/// +/// Each felt element in a composed word slot is typed, can have an optional default value, and can +/// optionally be named to allow overriding at instantiation time. +/// +/// To avoid non-overridable constants, unnamed elements are allowed only when `type = "void"`, +/// which always evaluates to `0` and does not require init data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FeltSchema { + name: Option, + description: Option, + r#type: SchemaTypeId, + default_value: Option, +} + +impl FeltSchema { + /// Creates a new required typed felt field. + pub fn new_typed(r#type: SchemaTypeId, name: impl Into) -> Self { + FeltSchema { + name: Some(name.into()), + description: None, + r#type, + default_value: None, + } + } + + /// Creates a new typed felt field with a default value. + pub fn new_typed_with_default( + r#type: SchemaTypeId, + name: impl Into, + default_value: Felt, + ) -> Self { + FeltSchema { + name: Some(name.into()), + description: None, + r#type, + default_value: Some(default_value), + } + } + + /// Creates an unnamed `void` felt element. + pub fn new_void() -> Self { + FeltSchema { + name: None, + description: None, + r#type: SchemaTypeId::void(), + default_value: None, + } + } + + /// Sets the description of the [`FeltSchema`] and returns `self`. + pub fn with_description(self, description: impl Into) -> Self { + FeltSchema { + description: Some(description.into()), + ..self + } + } + + /// Returns the felt type. + pub fn felt_type(&self) -> SchemaTypeId { + self.r#type.clone() + } + + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + pub fn description(&self) -> Option<&String> { + self.description.as_ref() + } + + pub fn default_value(&self) -> Option { + self.default_value + } + + fn collect_init_value_requirements( + &self, + slot_prefix: StorageValueName, + requirements: &mut BTreeMap, + ) -> Result<(), AccountComponentTemplateError> { + if self.r#type == SchemaTypeId::void() { + return Ok(()); + } + + let Some(name) = self.name.as_deref() else { + return Err(AccountComponentTemplateError::InvalidSchema( + "non-void felt elements must be named".into(), + )); + }; + let value_name = + StorageValueName::from_slot_name_with_suffix(slot_prefix.slot_name(), name) + .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?; + + let default_value = self + .default_value + .map(|felt| SCHEMA_TYPE_REGISTRY.display_felt(&self.r#type, felt)); + + if requirements + .insert( + value_name.clone(), + SchemaRequirement { + description: self.description.clone(), + r#type: self.r#type.clone(), + default_value, + }, + ) + .is_some() + { + return Err(AccountComponentTemplateError::DuplicateInitValueName(value_name)); + } + + Ok(()) + } + + /// Attempts to convert the [`FeltSchema`] into a [`Felt`]. + /// + /// If the schema variant is typed, the value is retrieved from `init_storage_data`, + /// identified by its key. Otherwise, the returned value is just the inner element. + pub(crate) fn try_build_felt( + &self, + init_storage_data: &InitStorageData, + slot_name: &StorageSlotName, + ) -> Result { + let value_name = match self.name.as_deref() { + Some(name) => Some( + StorageValueName::from_slot_name_with_suffix(slot_name, name) + .map_err(|err| AccountComponentTemplateError::InvalidSchema(err.to_string()))?, + ), + None => None, + }; + + if let Some(value_name) = value_name.clone() + && let Some(raw_value) = init_storage_data.value_entry(&value_name) + { + match raw_value { + WordValue::Atomic(raw) => { + let felt = SCHEMA_TYPE_REGISTRY + .try_parse_felt(&self.r#type, raw) + .map_err(AccountComponentTemplateError::StorageValueParsingError)?; + return Ok(felt); + }, + WordValue::Elements(_) => { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + value_name, + "expected an atomic value, got a 4-element array".into(), + )); + }, + WordValue::FullyTyped(_) => { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + value_name, + "expected an atomic value, got a word".into(), + )); + }, + } + } + + if self.r#type == SchemaTypeId::void() { + return Ok(Felt::ZERO); + } + + if let Some(default_value) = self.default_value { + return Ok(default_value); + } + + let Some(value_name) = value_name else { + return Err(AccountComponentTemplateError::InvalidSchema( + "non-void felt elements must be named".into(), + )); + }; + + Err(AccountComponentTemplateError::InitValueNotProvided(value_name)) + } + + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write(&self.name); + target.write(&self.description); + target.write(&self.r#type); + let default_value = if include_defaults { self.default_value } else { None }; + target.write(default_value); + } + + /// Validates the felt type, naming rules, and default value (if any). + fn validate(&self) -> Result<(), AccountComponentTemplateError> { + if let Some(description) = self.description.as_deref() { + validate_description_ascii(description)?; + } + + let type_exists = SCHEMA_TYPE_REGISTRY.contains_felt_type(&self.felt_type()); + if !type_exists { + return Err(AccountComponentTemplateError::InvalidType( + self.felt_type().to_string(), + "Felt".into(), + )); + } + + if self.r#type == SchemaTypeId::void() { + if self.name.is_some() { + return Err(AccountComponentTemplateError::InvalidSchema( + "void felt elements must be unnamed".into(), + )); + } + if self.default_value.is_some() { + return Err(AccountComponentTemplateError::InvalidSchema( + "void felt elements cannot define `default-value`".into(), + )); + } + return Ok(()); + } + + if self.name.is_none() { + return Err(AccountComponentTemplateError::InvalidSchema( + "non-void felt elements must be named".into(), + )); + } + + if let Some(value) = self.default_value { + SCHEMA_TYPE_REGISTRY + .validate_felt_value(&self.felt_type(), value) + .map_err(AccountComponentTemplateError::StorageValueParsingError)?; + } + Ok(()) + } +} + +impl Serializable for FeltSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + +impl Deserializable for FeltSchema { + fn read_from(source: &mut R) -> Result { + let name = Option::::read_from(source)?; + let description = Option::::read_from(source)?; + let r#type = SchemaTypeId::read_from(source)?; + let default_value = Option::::read_from(source)?; + Ok(FeltSchema { name, description, r#type, default_value }) + } +} + +/// Describes the schema for a storage value slot. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValueSlotSchema { + description: Option, + word: WordSchema, +} + +impl ValueSlotSchema { + pub fn new(description: Option, word: WordSchema) -> Self { + Self { description, word } + } + + pub fn description(&self) -> Option<&String> { + self.description.as_ref() + } + + pub fn word(&self) -> &WordSchema { + &self.word + } + + fn collect_init_value_requirements( + &self, + value_name: StorageValueName, + requirements: &mut BTreeMap, + ) -> Result<(), AccountComponentTemplateError> { + self.word.collect_init_value_requirements( + value_name, + self.description.clone(), + requirements, + ) + } + + /// Builds a [Word] from the provided initialization data using the inner word schema. + pub fn try_build_word( + &self, + init_storage_data: &InitStorageData, + slot_name: &StorageSlotName, + ) -> Result { + self.word.try_build_word(init_storage_data, slot_name) + } + + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write(&self.description); + self.word.write_into_with_optional_defaults(target, include_defaults); + } + + /// Validates the slot's word schema. + pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { + if let Some(description) = self.description.as_deref() { + validate_description_ascii(description)?; + } + self.word.validate()?; + Ok(()) + } +} + +impl Serializable for ValueSlotSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + +impl Deserializable for ValueSlotSchema { + fn read_from(source: &mut R) -> Result { + let description = Option::::read_from(source)?; + let word = WordSchema::read_from(source)?; + Ok(ValueSlotSchema::new(description, word)) + } +} + +/// Describes the schema for a storage map slot. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapSlotSchema { + description: Option, + default_values: Option>, + key_schema: WordSchema, + value_schema: WordSchema, +} + +impl MapSlotSchema { + pub fn new( + description: Option, + default_values: Option>, + key_schema: WordSchema, + value_schema: WordSchema, + ) -> Self { + Self { + description, + default_values, + key_schema, + value_schema, + } + } + + pub fn description(&self) -> Option<&String> { + self.description.as_ref() + } + + /// Builds a [`StorageMap`] from the provided initialization data. + /// + /// Merges any default values with entries from the init data, validating that the data + /// contains map entries (not a direct value or field entries). + pub fn try_build_map( + &self, + init_storage_data: &InitStorageData, + slot_name: &StorageSlotName, + ) -> Result { + let mut entries = self.default_values.clone().unwrap_or_default(); + let slot_prefix = StorageValueName::from_slot_name(slot_name); + + if init_storage_data.slot_value_entry(slot_name).is_some() { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a map, got a value".into(), + )); + } + if init_storage_data.has_field_entries_for_slot(slot_name) { + return Err(AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix, + "expected a map, got field entries".into(), + )); + } + if let Some(init_entries) = init_storage_data.map_entries(slot_name) { + let mut parsed_entries = Vec::with_capacity(init_entries.len()); + for (raw_key, raw_value) in init_entries.iter() { + let key = parse_storage_value_with_schema(&self.key_schema, raw_key, &slot_prefix)?; + let value = + parse_storage_value_with_schema(&self.value_schema, raw_value, &slot_prefix)?; + + parsed_entries.push((key, value)); + } + + for (key, value) in parsed_entries.iter() { + entries.insert(*key, *value); + } + } + + if entries.is_empty() { + return Ok(StorageMap::new()); + } + + StorageMap::with_entries(entries) + .map_err(|err| AccountComponentTemplateError::StorageMapHasDuplicateKeys(Box::new(err))) + } + + pub fn key_schema(&self) -> &WordSchema { + &self.key_schema + } + + pub fn value_schema(&self) -> &WordSchema { + &self.value_schema + } + + pub fn default_values(&self) -> Option> { + self.default_values.clone() + } + + /// Serializes the schema, optionally ignoring the default values (used for committing to a + /// schema definition). + fn write_into_with_optional_defaults( + &self, + target: &mut W, + include_defaults: bool, + ) { + target.write(&self.description); + let default_values = if include_defaults { + self.default_values.clone() + } else { + None + }; + target.write(&default_values); + self.key_schema.write_into_with_optional_defaults(target, include_defaults); + self.value_schema.write_into_with_optional_defaults(target, include_defaults); + } + + /// Validates key/value word schemas for this map slot. + fn validate(&self) -> Result<(), AccountComponentTemplateError> { + if let Some(description) = self.description.as_deref() { + validate_description_ascii(description)?; + } + self.key_schema.validate()?; + self.value_schema.validate()?; + Ok(()) + } +} + +pub(super) fn parse_storage_value_with_schema( + schema: &WordSchema, + raw_value: &WordValue, + slot_prefix: &StorageValueName, +) -> Result { + let word = match (schema, raw_value) { + (_, WordValue::FullyTyped(word)) => *word, + (WordSchema::Simple { r#type, .. }, raw_value) => { + parse_simple_word_value(r#type, raw_value, slot_prefix)? + }, + (WordSchema::Composite { value }, WordValue::Elements(elements)) => { + parse_composite_elements(value, elements, slot_prefix)? + }, + (WordSchema::Composite { .. }, WordValue::Atomic(value)) => SCHEMA_TYPE_REGISTRY + .try_parse_word(&SchemaTypeId::native_word(), value) + .map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value as `word`: {err}"), + ) + })?, + }; + + schema.validate_word_value(slot_prefix, "value", word)?; + Ok(word) +} + +fn parse_simple_word_value( + schema_type: &SchemaTypeId, + raw_value: &WordValue, + slot_prefix: &StorageValueName, +) -> Result { + match raw_value { + WordValue::Atomic(value) => { + SCHEMA_TYPE_REGISTRY.try_parse_word(schema_type, value).map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value as `{}`: {err}", schema_type), + ) + }) + }, + WordValue::Elements(elements) => { + let felts: Vec = elements + .iter() + .map(|element| { + SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaTypeId::native_felt(), element) + }) + .collect::>() + .map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value element as `felt`: {err}"), + ) + })?; + let felts: [Felt; 4] = felts.try_into().expect("length is 4"); + Ok(Word::from(felts)) + }, + WordValue::FullyTyped(word) => Ok(*word), + } +} + +fn parse_composite_elements( + schema: &[FeltSchema; 4], + elements: &[String; 4], + slot_prefix: &StorageValueName, +) -> Result { + let mut felts = [Felt::ZERO; 4]; + for (index, felt_schema) in schema.iter().enumerate() { + let felt_type = felt_schema.felt_type(); + felts[index] = + SCHEMA_TYPE_REGISTRY + .try_parse_felt(&felt_type, &elements[index]) + .map_err(|err| { + AccountComponentTemplateError::InvalidInitStorageValue( + slot_prefix.clone(), + format!("failed to parse value[{index}] as `{felt_type}`: {err}"), + ) + })?; + } + Ok(Word::from(felts)) +} + +impl Serializable for MapSlotSchema { + fn write_into(&self, target: &mut W) { + self.write_into_with_optional_defaults(target, true); + } +} + +impl Deserializable for MapSlotSchema { + fn read_from(source: &mut R) -> Result { + let description = Option::::read_from(source)?; + let default_values = Option::>::read_from(source)?; + let key_schema = WordSchema::read_from(source)?; + let value_schema = WordSchema::read_from(source)?; + Ok(MapSlotSchema::new(description, default_values, key_schema, value_schema)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + + use super::*; + + #[test] + fn map_slot_schema_default_values_returns_map() { + let word_schema = WordSchema::new_simple(SchemaTypeId::native_word()); + let mut default_values = BTreeMap::new(); + default_values.insert( + Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), + Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]), + ); + let slot = MapSlotSchema::new( + Some("static map".into()), + Some(default_values), + word_schema.clone(), + word_schema, + ); + + let mut expected = BTreeMap::new(); + expected.insert( + Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), + Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]), + ); + + assert_eq!(slot.default_values(), Some(expected)); + } + + #[test] + fn value_slot_schema_exposes_felt_schema_types() { + let felt_values = [ + FeltSchema::new_typed(SchemaTypeId::u8(), "a"), + FeltSchema::new_typed(SchemaTypeId::u16(), "b"), + FeltSchema::new_typed(SchemaTypeId::u32(), "c"), + FeltSchema::new_typed(SchemaTypeId::new("felt").unwrap(), "d"), + ]; + + let slot = ValueSlotSchema::new(None, WordSchema::new_value(felt_values)); + let WordSchema::Composite { value } = slot.word() else { + panic!("expected composite word schema"); + }; + + assert_eq!(value[0].felt_type(), SchemaTypeId::u8()); + assert_eq!(value[1].felt_type(), SchemaTypeId::u16()); + assert_eq!(value[2].felt_type(), SchemaTypeId::u32()); + assert_eq!(value[3].felt_type(), SchemaTypeId::new("felt").unwrap()); + } + + #[test] + fn map_slot_schema_key_and_value_types() { + let key_schema = WordSchema::new_simple(SchemaTypeId::new("sampling::Key").unwrap()); + + let value_schema = WordSchema::new_value([ + FeltSchema::new_typed(SchemaTypeId::native_felt(), "a"), + FeltSchema::new_typed(SchemaTypeId::native_felt(), "b"), + FeltSchema::new_typed(SchemaTypeId::native_felt(), "c"), + FeltSchema::new_typed(SchemaTypeId::native_felt(), "d"), + ]); + + let slot = MapSlotSchema::new(None, None, key_schema, value_schema); + + assert_eq!( + slot.key_schema(), + &WordSchema::new_simple(SchemaTypeId::new("sampling::Key").unwrap()) + ); + + let WordSchema::Composite { value } = slot.value_schema() else { + panic!("expected composite word schema for map values"); + }; + for felt in value.iter() { + assert_eq!(felt.felt_type(), SchemaTypeId::native_felt()); + } + } + + #[test] + fn value_slot_schema_accepts_typed_word_init_value() { + let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::native_word())); + let slot_name: StorageSlotName = "demo::slot".parse().unwrap(); + + let expected = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + let mut init_data = InitStorageData::default(); + init_data + .set_value(StorageValueName::from_slot_name(&slot_name), expected) + .unwrap(); + + let built = slot.try_build_word(&init_data, &slot_name).unwrap(); + assert_eq!(built, expected); + } + + #[test] + fn value_slot_schema_accepts_felt_typed_word_init_value() { + let slot = ValueSlotSchema::new(None, WordSchema::new_simple(SchemaTypeId::u8())); + let slot_name: StorageSlotName = "demo::u8_word".parse().unwrap(); + + let mut init_data = InitStorageData::default(); + init_data.set_value(StorageValueName::from_slot_name(&slot_name), "6").unwrap(); + + let built = slot.try_build_word(&init_data, &slot_name).unwrap(); + assert_eq!(built, Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(6)])); + } + + #[test] + fn value_slot_schema_accepts_typed_felt_init_value_in_composed_word() { + let word = WordSchema::new_value([ + FeltSchema::new_typed(SchemaTypeId::u8(), "a"), + FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "b", Felt::new(2)), + FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "c", Felt::new(3)), + FeltSchema::new_typed_with_default(SchemaTypeId::native_felt(), "d", Felt::new(4)), + ]); + let slot = ValueSlotSchema::new(None, word); + let slot_name: StorageSlotName = "demo::slot".parse().unwrap(); + + let mut init_data = InitStorageData::default(); + init_data + .set_value(StorageValueName::from_slot_name_with_suffix(&slot_name, "a").unwrap(), "1") + .unwrap(); + + let built = slot.try_build_word(&init_data, &slot_name).unwrap(); + assert_eq!(built, Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)])); + } + + #[test] + fn map_slot_schema_accepts_typed_map_init_value() { + let word_schema = WordSchema::new_simple(SchemaTypeId::native_word()); + let slot = MapSlotSchema::new(None, None, word_schema.clone(), word_schema); + let slot_name: StorageSlotName = "demo::map".parse().unwrap(); + + let entries = vec![( + WordValue::Elements(["1".into(), "0".into(), "0".into(), "0".into()]), + WordValue::Elements(["10".into(), "11".into(), "12".into(), "13".into()]), + )]; + let mut init_data = InitStorageData::default(); + init_data.set_map_values(slot_name.clone(), entries.clone()).unwrap(); + + let built = slot.try_build_map(&init_data, &slot_name).unwrap(); + let expected = StorageMap::with_entries([( + Word::from([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]), + Word::from([Felt::new(10), Felt::new(11), Felt::new(12), Felt::new(13)]), + )]) + .unwrap(); + assert_eq!(built, expected); + } + + #[test] + fn map_slot_schema_missing_init_value_defaults_to_empty_map() { + let word_schema = WordSchema::new_simple(SchemaTypeId::native_word()); + let slot = MapSlotSchema::new(None, None, word_schema.clone(), word_schema); + let built = slot + .try_build_map(&InitStorageData::default(), &"demo::map".parse().unwrap()) + .unwrap(); + assert_eq!(built, StorageMap::new()); + } +} diff --git a/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs new file mode 100644 index 0000000000..cf681dc852 --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/toml/init_storage_data.rs @@ -0,0 +1,121 @@ +use alloc::string::{String, ToString}; + +use serde::Deserialize; +use thiserror::Error; + +use super::super::{ + InitStorageData, + InitStorageDataError as CoreInitStorageDataError, + StorageValueName, + StorageValueNameError, + WordValue, +}; +use super::RawMapEntrySchema; + +impl InitStorageData { + /// Creates an instance of [`InitStorageData`] from a TOML string. + /// + /// # Supported formats + /// + /// ```toml + /// # Value entry (string) + /// "slot::name" = "0x1234" + /// + /// # Value entry (4-element word) + /// "slot::name" = ["0", "0", "0", "100"] + /// + /// # Nested table (flattened to slot::name.field) + /// ["slot::name"] + /// field = "value" + /// + /// # Map entries + /// "slot::map" = [ + /// { key = "0x01", value = "0x10" }, + /// ] + /// ``` + pub fn from_toml(toml_str: &str) -> Result { + let table: toml::Table = toml::from_str(toml_str)?; + let mut data = InitStorageData::default(); + + for (key, value) in table { + let name: StorageValueName = + key.parse().map_err(InitStorageDataError::InvalidStorageValueName)?; + + match value { + // ["slot::name"] + // field = "value" + toml::Value::Table(nested) => { + if nested.is_empty() { + return Err(InitStorageDataError::EmptyTable(name.to_string())); + } + if name.field_name().is_some() { + return Err(InitStorageDataError::ExcessiveNesting(name.to_string())); + } + for (field, field_value) in nested { + let field_name = + StorageValueName::from_slot_name_with_suffix(name.slot_name(), &field) + .map_err(InitStorageDataError::InvalidStorageValueName)?; + let word = WordValue::deserialize(field_value).map_err(|_| { + InitStorageDataError::InvalidValue(field_name.to_string()) + })?; + data.insert_value(field_name, word)?; + } + }, + // "slot::name" = [{ key = "...", value = "..." }, ...] + toml::Value::Array(items) + if items.iter().all(|v| matches!(v, toml::Value::Table(_))) => + { + if name.field_name().is_some() { + return Err(InitStorageDataError::InvalidMapEntryKey(name.to_string())); + } + for item in items { + // Try deserializing as map entry + let entry: RawMapEntrySchema = RawMapEntrySchema::deserialize(item) + .map_err(|e| { + InitStorageDataError::InvalidMapEntrySchema(e.to_string()) + })?; + + data.insert_map_entry(name.slot_name().clone(), entry.key, entry.value)?; + } + }, + // "slot::name" = "value" or "slot::name" = ["a", "b", "c", "d"] + other => { + let word = WordValue::deserialize(other) + .map_err(|_| InitStorageDataError::InvalidValue(name.to_string()))?; + data.insert_value(name, word)?; + }, + } + } + + Ok(data) + } +} + +#[derive(Debug, Error)] +pub enum InitStorageDataError { + #[error("failed to parse TOML: {0}")] + InvalidToml(#[from] toml::de::Error), + + #[error("empty table encountered for key `{0}`")] + EmptyTable(String), + + #[error(transparent)] + InvalidData(#[from] CoreInitStorageDataError), + + #[error("invalid map entry key `{0}`: map entries must target a slot name")] + InvalidMapEntryKey(String), + + #[error("excessive nesting for key `{0}`: only one level of table nesting is allowed")] + ExcessiveNesting(String), + + #[error( + "invalid input for `{0}`: expected a string, a 4-element string array, or a map entry list" + )] + InvalidValue(String), + + #[error("invalid storage value name")] + InvalidStorageValueName(#[source] StorageValueNameError), + + #[error("invalid map entry: {0}")] + InvalidMapEntrySchema(String), +} diff --git a/crates/miden-protocol/src/account/component/storage/toml/mod.rs b/crates/miden-protocol/src/account/component/storage/toml/mod.rs new file mode 100644 index 0000000000..9d40674cb0 --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/toml/mod.rs @@ -0,0 +1,504 @@ +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use miden_core::{Felt, Word}; +use semver::Version; +use serde::de::Error as _; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use super::super::{ + FeltSchema, + MapSlotSchema, + StorageSchema, + StorageSlotSchema, + StorageValueName, + ValueSlotSchema, + WordSchema, + WordValue, +}; +use crate::account::component::storage::type_registry::SCHEMA_TYPE_REGISTRY; +use crate::account::component::{AccountComponentMetadata, SchemaTypeId}; +use crate::account::{AccountType, StorageSlotName}; +use crate::errors::AccountComponentTemplateError; + +mod init_storage_data; +mod serde_impls; + +#[cfg(test)] +mod tests; + +// ACCOUNT COMPONENT METADATA TOML FROM/TO +// ================================================================================================ + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct RawAccountComponentMetadata { + name: String, + description: String, + version: Version, + supported_types: BTreeSet, + #[serde(rename = "storage")] + #[serde(default)] + storage: RawStorageSchema, +} + +impl AccountComponentMetadata { + /// Deserializes `toml_string` and validates the resulting [AccountComponentMetadata] + /// + /// # Errors + /// + /// - If deserialization fails + /// - If the schema specifies storage slots with duplicates. + /// - If the schema contains invalid slot definitions. + pub fn from_toml(toml_string: &str) -> Result { + let raw: RawAccountComponentMetadata = toml::from_str(toml_string) + .map_err(AccountComponentTemplateError::TomlDeserializationError)?; + + if !raw.description.is_ascii() { + return Err(AccountComponentTemplateError::InvalidSchema( + "description must contain only ASCII characters".to_string(), + )); + } + + let RawStorageSchema { slots } = raw.storage; + let mut fields = Vec::with_capacity(slots.len()); + + for slot in slots { + fields.push(slot.try_into_slot_schema()?); + } + + let storage_schema = StorageSchema::new(fields)?; + Ok(Self::new( + raw.name, + raw.description, + raw.version, + raw.supported_types, + storage_schema, + )) + } + + /// Serializes the account component metadata into a TOML string. + pub fn to_toml(&self) -> Result { + let toml = + toml::to_string(self).map_err(AccountComponentTemplateError::TomlSerializationError)?; + Ok(toml) + } +} + +// ACCOUNT STORAGE SCHEMA SERIALIZATION +// ================================================================================================ + +/// Raw TOML storage schema: +/// +/// - `[[storage.slots]]` for both value and map slots. +/// +/// Slot kind is inferred by the shape of the `type` field: +/// - `type = "..."` or `type = [ ... ]` => value slot +/// - `type = { ... }` => map slot +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct RawStorageSchema { + #[serde(default)] + slots: Vec, +} + +/// Storage slot type descriptor. +/// +/// This field accepts either: +/// - a type identifier (e.g. `"word"`, `"u16"`, `"miden::standards::auth::falcon512_rpo::pub_key"`) +/// for simple word slots, +/// - an array of 4 [`FeltSchema`] descriptors for composite word slots, or +/// - a table `{ key = ..., value = ... }` for map slots. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +enum RawSlotType { + Word(RawWordType), + Map(RawMapType), +} + +/// A word type descriptor. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +enum RawWordType { + TypeIdentifier(SchemaTypeId), + FeltSchemaArray(Vec), +} + +/// A map type descriptor. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct RawMapType { + key: RawWordType, + value: RawWordType, +} + +// ACCOUNT STORAGE SCHEMA SERDE +// ================================================================================================ + +impl Serialize for StorageSchema { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let slots = self + .slots() + .iter() + .map(|(slot_name, schema)| RawStorageSlotSchema::from_slot(slot_name, schema)) + .collect(); + + RawStorageSchema { slots }.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for StorageSchema { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // First, look at the raw representation + let raw = RawStorageSchema::deserialize(deserializer)?; + let mut fields = Vec::with_capacity(raw.slots.len()); + + for slot in raw.slots { + let (slot_name, schema) = slot.try_into_slot_schema().map_err(D::Error::custom)?; + fields.push((slot_name, schema)); + } + + StorageSchema::new(fields).map_err(D::Error::custom) + } +} + +// ACCOUNT STORAGE SCHEMA SERDE HELPERS +// ================================================================================================ + +/// Raw storage slot schemas contain the raw representation that can get deserialized from TOML. +/// Specifically, it expresses the different combination of fields that expose the different types +/// of slots. +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct RawStorageSlotSchema { + /// The name of the storage slot, in `StorageSlotName` format (e.g. + /// `my_project::module::slot`). + name: String, + #[serde(default)] + description: Option, + /// Slot type descriptor. + /// + /// - If `type = { ... }`, this is a map slot. + /// - If `type = [ ... ]`, this is a composite word slot whose schema is described by 4 + /// [`FeltSchema`] descriptors. + /// - Otherwise, if `type = "..."`, this is a simple word slot whose value is supplied at + /// instantiation time unless `default-value` is set (or the type is `void`). + #[serde(rename = "type")] + r#type: RawSlotType, + /// The (overridable) default value for a simple word slot. + #[serde(default)] + default_value: Option, + /// Default map entries. + /// + /// These entries must be fully-specified values. If the map should be populated at + /// instantiation time, omit `default-values` and provide entries via init storage data. + #[serde(default)] + default_values: Option>, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct RawMapEntrySchema { + key: WordValue, + value: WordValue, +} + +impl RawStorageSlotSchema { + // SERIALIZATION + // -------------------------------------------------------------------------------------------- + + fn from_slot(slot_name: &StorageSlotName, schema: &StorageSlotSchema) -> Self { + match schema { + StorageSlotSchema::Value(schema) => Self::from_value_slot(slot_name, schema), + StorageSlotSchema::Map(schema) => Self::from_map_slot(slot_name, schema), + } + } + + fn from_value_slot(slot_name: &StorageSlotName, schema: &ValueSlotSchema) -> Self { + let word = schema.word(); + let (r#type, default_value) = match word { + WordSchema::Simple { r#type, default_value } => ( + RawSlotType::Word(RawWordType::TypeIdentifier(r#type.clone())), + default_value.map(|word| WordValue::from_word(r#type, word)), + ), + WordSchema::Composite { value } => { + (RawSlotType::Word(RawWordType::FeltSchemaArray(value.to_vec())), None) + }, + }; + + Self { + name: slot_name.as_str().to_string(), + description: schema.description().cloned(), + r#type, + default_value, + default_values: None, + } + } + + fn from_map_slot(slot_name: &StorageSlotName, schema: &MapSlotSchema) -> Self { + let default_values = schema.default_values().map(|default_values| { + default_values + .into_iter() + .map(|(key, value)| RawMapEntrySchema { + key: WordValue::from_word(&schema.key_schema().word_type(), key), + value: WordValue::from_word(&schema.value_schema().word_type(), value), + }) + .collect() + }); + + let key_type = match schema.key_schema() { + WordSchema::Simple { r#type, .. } => RawWordType::TypeIdentifier(r#type.clone()), + WordSchema::Composite { value } => RawWordType::FeltSchemaArray(value.to_vec()), + }; + + let value_type = match schema.value_schema() { + WordSchema::Simple { r#type, .. } => RawWordType::TypeIdentifier(r#type.clone()), + WordSchema::Composite { value } => RawWordType::FeltSchemaArray(value.to_vec()), + }; + + Self { + name: slot_name.as_str().to_string(), + description: schema.description().cloned(), + r#type: RawSlotType::Map(RawMapType { key: key_type, value: value_type }), + default_value: None, + default_values, + } + } + + // DESERIALIZATION + // -------------------------------------------------------------------------------------------- + + /// Converts the raw representation into a tuple of the storage slot name and its schema. + fn try_into_slot_schema( + self, + ) -> Result<(StorageSlotName, StorageSlotSchema), AccountComponentTemplateError> { + let RawStorageSlotSchema { + name, + description, + r#type, + default_value, + default_values, + } = self; + + let slot_name_raw = name; + let slot_name = StorageSlotName::new(slot_name_raw.clone()).map_err(|err| { + AccountComponentTemplateError::InvalidSchema(format!( + "invalid storage slot name `{slot_name_raw}`: {err}" + )) + })?; + + let description = + description.and_then(|d| if d.trim().is_empty() { None } else { Some(d) }); + + let slot_prefix = StorageValueName::from_slot_name(&slot_name); + + if default_value.is_some() && default_values.is_some() { + return Err(AccountComponentTemplateError::InvalidSchema( + "storage slot schema cannot define both `default-value` and `default-values`" + .into(), + )); + } + + match r#type { + RawSlotType::Map(map_type) => { + if default_value.is_some() { + return Err(AccountComponentTemplateError::InvalidSchema( + "map slots cannot define `default-value`".into(), + )); + } + + let RawMapType { key: key_type, value: value_type } = map_type; + let key_schema = Self::parse_word_schema(key_type, "`type.key`")?; + let value_schema = Self::parse_word_schema(value_type, "`type.value`")?; + + let default_values = default_values + .map(|entries| { + Self::parse_default_map_entries( + entries, + &key_schema, + &value_schema, + &slot_prefix, + ) + }) + .transpose()?; + + Ok(( + slot_name, + StorageSlotSchema::Map(MapSlotSchema::new( + description, + default_values, + key_schema, + value_schema, + )), + )) + }, + + RawSlotType::Word(word_type) => { + if default_values.is_some() { + return Err(AccountComponentTemplateError::InvalidSchema( + "`default-values` can be specified only for map slots (use `type = { ... }`)" + .into(), + )); + } + + match word_type { + RawWordType::TypeIdentifier(r#type) => { + if r#type.as_str() == "map" { + return Err(AccountComponentTemplateError::InvalidSchema( + "value slots cannot use `type = \"map\"`; use `type = { key = , value = }` instead" + .into(), + )); + } + + let word = default_value + .as_ref() + .map(|default_value| { + default_value.try_parse_as_typed_word( + &r#type, + &slot_prefix, + "default value", + ) + }) + .transpose()?; + + let word_schema = match word { + Some(word) => WordSchema::new_simple_with_default(r#type, word), + None => WordSchema::new_simple(r#type), + }; + + Ok(( + slot_name, + StorageSlotSchema::Value(ValueSlotSchema::new( + description, + word_schema, + )), + )) + }, + + RawWordType::FeltSchemaArray(elements) => { + if default_value.is_some() { + return Err(AccountComponentTemplateError::InvalidSchema( + "composite word slots cannot define `default-value`".into(), + )); + } + + let elements = Self::parse_felt_schema_array(elements, "word slot `type`")?; + Ok(( + slot_name, + StorageSlotSchema::Value(ValueSlotSchema::new( + description, + WordSchema::new_value(elements), + )), + )) + }, + } + }, + } + } + + fn parse_word_schema( + raw: RawWordType, + label: &str, + ) -> Result { + match raw { + RawWordType::TypeIdentifier(r#type) => Ok(WordSchema::new_simple(r#type)), + RawWordType::FeltSchemaArray(elements) => { + let elements = Self::parse_felt_schema_array(elements, label)?; + Ok(WordSchema::new_value(elements)) + }, + } + } + + fn parse_felt_schema_array( + elements: Vec, + label: &str, + ) -> Result<[FeltSchema; 4], AccountComponentTemplateError> { + if elements.len() != 4 { + return Err(AccountComponentTemplateError::InvalidSchema(format!( + "{label} must be an array of 4 elements, got {}", + elements.len() + ))); + } + Ok(elements.try_into().expect("length is 4")) + } + + fn parse_default_map_entries( + entries: Vec, + key_schema: &WordSchema, + value_schema: &WordSchema, + slot_prefix: &StorageValueName, + ) -> Result, AccountComponentTemplateError> { + let mut map = BTreeMap::new(); + + let parse = |schema: &WordSchema, raw: &WordValue, label: &str| { + super::schema::parse_storage_value_with_schema(schema, raw, slot_prefix).map_err( + |err| { + AccountComponentTemplateError::InvalidSchema(format!( + "invalid map `{label}`: {err}" + )) + }, + ) + }; + + for (index, entry) in entries.into_iter().enumerate() { + let key_label = format!("default-values[{index}].key"); + let value_label = format!("default-values[{index}].value"); + + let key = parse(key_schema, &entry.key, &key_label)?; + let value = parse(value_schema, &entry.value, &value_label)?; + + if map.insert(key, value).is_some() { + return Err(AccountComponentTemplateError::InvalidSchema(format!( + "map storage slot `default-values[{index}]` contains a duplicate key" + ))); + } + } + + Ok(map) + } +} + +impl WordValue { + pub(super) fn try_parse_as_typed_word( + &self, + schema_type: &SchemaTypeId, + slot_prefix: &StorageValueName, + label: &str, + ) -> Result { + let word = match self { + WordValue::FullyTyped(word) => *word, + WordValue::Atomic(value) => SCHEMA_TYPE_REGISTRY + .try_parse_word(schema_type, value) + .map_err(AccountComponentTemplateError::StorageValueParsingError)?, + WordValue::Elements(elements) => { + let felts = elements + .iter() + .map(|element| { + SCHEMA_TYPE_REGISTRY.try_parse_felt(&SchemaTypeId::native_felt(), element) + }) + .collect::, _>>() + .map_err(AccountComponentTemplateError::StorageValueParsingError)?; + let felts: [Felt; 4] = felts.try_into().expect("length is 4"); + Word::from(felts) + }, + }; + + WordSchema::new_simple(schema_type.clone()).validate_word_value( + slot_prefix, + label, + word, + )?; + Ok(word) + } + + pub(super) fn from_word(schema_type: &SchemaTypeId, word: Word) -> Self { + WordValue::Atomic(SCHEMA_TYPE_REGISTRY.display_word(schema_type, word).value().to_string()) + } +} diff --git a/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs b/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs new file mode 100644 index 0000000000..3dc8f9033c --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/toml/serde_impls.rs @@ -0,0 +1,157 @@ +use alloc::string::{String, ToString}; + +use serde::de::Error as _; +use serde::ser::{Error as SerError, SerializeStruct}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use super::super::type_registry::SCHEMA_TYPE_REGISTRY; +use super::super::{FeltSchema, SchemaTypeId, WordValue}; + +// FELT SCHEMA SERIALIZATION +// ================================================================================================ + +impl Serialize for FeltSchema { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.felt_type() == SchemaTypeId::void() { + let mut state = serializer.serialize_struct("FeltSchema", 2)?; + state.serialize_field("type", &SchemaTypeId::void())?; + if let Some(description) = self.description() { + state.serialize_field("description", description)?; + } + return state.end(); + } + + let name = self.name().ok_or_else(|| { + SerError::custom("invalid FeltSchema: non-void elements must have a name") + })?; + + let mut state = serializer.serialize_struct("FeltSchema", 4)?; + state.serialize_field("name", name)?; + if let Some(description) = self.description() { + state.serialize_field("description", description)?; + } + if self.felt_type() != SchemaTypeId::native_felt() { + state.serialize_field("type", &self.felt_type())?; + } + if let Some(default_value) = self.default_value() { + state.serialize_field( + "default-value", + &SCHEMA_TYPE_REGISTRY.display_felt(&self.felt_type(), default_value), + )?; + } + state.end() + } +} + +impl<'de> Deserialize<'de> for FeltSchema { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + struct RawFeltSchema { + #[serde(default)] + name: Option, + #[serde(default)] + description: Option, + #[serde(default, rename = "default-value")] + default_value: Option, + #[serde(default, rename = "type")] + r#type: Option, + } + + let raw = RawFeltSchema::deserialize(deserializer)?; + + let felt_type = raw.r#type.unwrap_or_else(SchemaTypeId::native_felt); + + let description = raw.description.and_then(|description| { + if description.trim().is_empty() { + None + } else { + Some(description) + } + }); + + if felt_type == SchemaTypeId::void() { + if raw.name.is_some() { + return Err(D::Error::custom("`type = \"void\"` elements must omit `name`")); + } + if raw.default_value.is_some() { + return Err(D::Error::custom( + "`type = \"void\"` elements cannot define `default-value`", + )); + } + + let schema = FeltSchema::new_void(); + return Ok(match description { + Some(description) => schema.with_description(description), + None => schema, + }); + } + + let Some(name) = raw.name else { + return Err(D::Error::custom("non-void elements must define `name`")); + }; + + let default_value = raw + .default_value + .map(|default_value| { + SCHEMA_TYPE_REGISTRY.try_parse_felt(&felt_type, &default_value).map_err(|err| { + D::Error::custom(format!( + "failed to parse {felt_type} as Felt for `default-value`: {err}" + )) + }) + }) + .transpose()?; + + let schema = match default_value { + Some(default_value) => { + FeltSchema::new_typed_with_default(felt_type, name, default_value) + }, + None => FeltSchema::new_typed(felt_type, name), + }; + Ok(match description { + Some(description) => schema.with_description(description), + None => schema, + }) + } +} + +// WORD VALUE SERIALIZATION +// ================================================================================================ + +impl Serialize for WordValue { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + WordValue::Atomic(value) => serializer.serialize_str(value), + WordValue::Elements(elements) => elements.serialize(serializer), + WordValue::FullyTyped(word) => serializer.serialize_str(&word.to_string()), + } + } +} + +impl<'de> Deserialize<'de> for WordValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum RawWordValue { + Atomic(String), + Elements([String; 4]), + } + + match RawWordValue::deserialize(deserializer)? { + RawWordValue::Atomic(value) => Ok(WordValue::Atomic(value)), + RawWordValue::Elements(elements) => Ok(WordValue::Elements(elements)), + } + } +} diff --git a/crates/miden-protocol/src/account/component/storage/toml/tests.rs b/crates/miden-protocol/src/account/component/storage/toml/tests.rs new file mode 100644 index 0000000000..dd6ead16cf --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/toml/tests.rs @@ -0,0 +1,962 @@ +use alloc::string::ToString; +use core::error::Error; + +use miden_air::FieldElement; +use miden_core::{Felt, Word}; + +use crate::account::component::toml::init_storage_data::InitStorageDataError; +use crate::account::component::{ + AccountComponentMetadata, + InitStorageData, + InitStorageDataError as CoreInitStorageDataError, + SchemaTypeId, + StorageSlotSchema, + StorageValueName, + StorageValueNameError, + WordSchema, + WordValue, +}; +use crate::account::{AccountStorage, StorageSlotContent, StorageSlotName}; +use crate::asset::TokenSymbol; +use crate::errors::AccountComponentTemplateError; + +#[test] +fn from_toml_str_with_nested_table_and_flattened() { + let toml_table = r#" + ["demo::token_metadata"] + max_supply = "1000000000" + symbol = "ETH" + decimals = "9" + "#; + + let toml_inline = r#" + "demo::token_metadata.max_supply" = "1000000000" + "demo::token_metadata.symbol" = "ETH" + "demo::token_metadata.decimals" = "9" + "#; + + let storage_table = InitStorageData::from_toml(toml_table).unwrap(); + let storage_inline = InitStorageData::from_toml(toml_inline).unwrap(); + + assert_eq!(storage_table.values(), storage_inline.values()); + assert_eq!(storage_table.maps(), storage_inline.maps()); +} + +#[test] +fn empty_table_is_rejected() { + let toml_str = r#" + ["demo::empty_table"] + + ["demo::valid_table"] + value = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::EmptyTable(key)) if key == "demo::empty_table" + ); +} + +#[test] +fn invalid_storage_value_name_is_rejected() { + // Nested table fields are flattened to `slot.field` and thus must be valid field segments. + let toml_str = r#" + ["demo::valid_token_metadata"] + max_supply = "1000000000" + + "demo::another_valid_token_metadata.supply" = "1000000000" + + ["demo::invalid_token_metadata"] + "bad.field" = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidStorageValueName( + StorageValueNameError::InvalidCharacter { part, character } + )) if part == "bad.field" && character == '.' + ); +} + +#[test] +fn from_toml_str_with_deeply_nested_tables_is_rejected() { + let toml_str = r#" + ["demo::token_metadata"] + [ "demo::token_metadata".nested ] + value = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidValue(_)) + ); +} + +#[test] +fn from_toml_str_excessive_key_nesting_rejected() { + let toml_str = r#" + ["demo::token_metadata.nested"] + value = "42" + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::ExcessiveNesting(_)) + ); +} + +#[test] +fn from_toml_rejects_non_string_atomics() { + let toml_str = r#" + "demo::foo" = 42 + "#; + + let result = InitStorageData::from_toml(toml_str); + assert_matches::assert_matches!(result.unwrap_err(), InitStorageDataError::InvalidValue(_)); +} + +#[test] +fn test_error_on_array() { + let toml_str = r#" + "demo::token_metadata.v" = ["1", "2", "3", "4", "5"] + "#; + + let err = InitStorageData::from_toml(toml_str).unwrap_err(); + assert_matches::assert_matches!(&err, InitStorageDataError::InvalidValue(key) if key == "demo::token_metadata.v"); +} + +#[test] +fn parse_map_entries_from_array() { + let toml_str = r#" + "demo::my_map" = [ + { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000010" }, + { key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = ["1", "2", "3", "4"] } + ] + "#; + + let storage = InitStorageData::from_toml(toml_str).expect("Failed to parse map entries"); + let map_name: StorageSlotName = "demo::my_map".parse().unwrap(); + let entries = storage.map_entries(&map_name).expect("map entries missing"); + assert_eq!(entries.len(), 2); + + assert_matches::assert_matches!( + &entries[0].0, + WordValue::Atomic(v) + if v == "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + assert_matches::assert_matches!( + &entries[0].1, + WordValue::Atomic(v) + if v == "0x0000000000000000000000000000000000000000000000000000000000000010" + ); + assert_matches::assert_matches!( + &entries[1].1, + WordValue::Elements(elements) + if elements == &[ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ] + ); +} + +#[test] +fn map_entries_reject_field_key() { + let toml_str = r#" + "demo::my_map.entry" = [ + { key = "0x1", value = "0x2" } + ] + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidMapEntryKey(_)) + ); +} + +#[test] +fn map_entries_reject_invalid_schema() { + // Missing required `value` field in the entry table should fail schema deserialization. + let toml_str = r#" + "demo::my_map" = [ + { key = "0x1" } + ] + "#; + + assert_matches::assert_matches!( + InitStorageData::from_toml(toml_str), + Err(InitStorageDataError::InvalidMapEntrySchema(_)) + ); +} + +#[test] +fn error_on_empty_subtable() { + let toml_str = r#" + ["demo::token_metadata"] + max_supply = {} + "#; + + let result = InitStorageData::from_toml(toml_str); + assert_matches::assert_matches!( + result.unwrap_err(), + InitStorageDataError::InvalidValue(key) if key == "demo::token_metadata.max_supply" + ); +} + +#[test] +fn error_on_duplicate_keys() { + let toml_str = r#" + "demo::token_metadata.max_supply" = "1000000000" + "demo::token_metadata.max_supply" = "500000000" + "#; + + let result = InitStorageData::from_toml(toml_str).unwrap_err(); + // TOML does not support duplicate keys + assert_matches::assert_matches!(result, InitStorageDataError::InvalidToml(_)); + assert!(result.source().unwrap().to_string().contains("duplicate")); +} + +#[test] +fn error_on_duplicate_keys_after_flattening() { + // `"slot.field"` should collide with `["slot"] field`. + let toml_str = r#" + "demo::token_metadata.max_supply" = "1" + + ["demo::token_metadata"] + max_supply = "2" + "#; + + let err = InitStorageData::from_toml(toml_str).unwrap_err(); + assert_matches::assert_matches!( + err, + InitStorageDataError::InvalidData(CoreInitStorageDataError::DuplicateKey(key)) + if key == "demo::token_metadata.max_supply" + ); +} + +#[test] +fn metadata_from_toml_parses_named_storage_schema() { + let toml_str = r#" + name = "Test Component" + description = "Test description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::test_value" + description = "a demo slot" + type = "word" + + [[storage.slots]] + name = "demo::my_map" + type = { key = "word", value = "word" } + "#; + + let metadata = AccountComponentMetadata::from_toml(toml_str).unwrap(); + let requirements = metadata.schema_requirements(); + + assert!(requirements.contains_key(&"demo::test_value".parse::().unwrap())); + assert!(!requirements.contains_key(&"demo::my_map".parse::().unwrap())); +} + +#[test] +fn metadata_from_toml_rejects_non_ascii_component_description() { + let toml_str = r#" + name = "Test Component" + description = "Invalid \u00e9" + version = "0.1.0" + supported-types = [] + "#; + + assert_matches::assert_matches!( + AccountComponentMetadata::from_toml(toml_str), + Err(AccountComponentTemplateError::InvalidSchema(_)) + ); +} + +#[test] +fn metadata_from_toml_rejects_non_ascii_slot_description() { + let toml_str = r#" + name = "Test Component" + description = "Test description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::test_value" + description = "Invalid \u00e9" + type = "word" + "#; + + assert_matches::assert_matches!( + AccountComponentMetadata::from_toml(toml_str), + Err(AccountComponentTemplateError::InvalidSchema(_)) + ); +} + +#[test] +fn metadata_schema_commitment_ignores_defaults_and_ordering() { + let toml_a = r#" + name = "Commitment Test" + description = "Schema commitments are equal regardless of defaults and ordering" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::first" + type = "word" + default-value = "0x1" + + [[storage.slots]] + name = "demo::map" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x1", value = "0x10" }, + ] + + [[storage.slots]] + name = "demo::composed" + type = [ + { name = "a", type = "u8", description = "field a", default-value = "1" }, + { name = "b", description = "field b", default-value = "2" }, + { name = "c", type = "u16", description = "field c", default-value = "3" }, + { type = "void", description = "padding" }, + ] + "#; + + let toml_b = r#" + name = "Commitment Test" + description = "" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::map" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x2", value = "0x20" }, + ] + + [[storage.slots]] + name = "demo::composed" + type = [ + { name = "a", type = "u8", description = "field a", default-value = "9" }, + { name = "b", description = "field b", default-value = "8" }, + { name = "c", type = "u16", description = "field c", default-value = "7" }, + { type = "void", description = "padding" }, + ] + + [[storage.slots]] + name = "demo::first" + type = "word" + default-value = "0x9" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); + + assert_ne!(metadata_a.storage_schema(), metadata_b.storage_schema()); + assert_eq!( + metadata_a.storage_schema().commitment(), + metadata_b.storage_schema().commitment() + ); +} + +#[test] +fn metadata_schema_commitment_includes_descriptions() { + let toml_a = r#" + name = "Commitment Test" + description = "Component description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::value" + description = "slot description a" + type = "word" + "#; + + let toml_bad_description = r#" + name = "Commitment Test" + description = "Component description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::value" + description = "incorrect description" + type = "word" + "#; + + let toml_bad_name = r#" + name = "Commitment Test" + description = "Component description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::bad_value" + description = "slot description a" + type = "word" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_bad_description = + AccountComponentMetadata::from_toml(toml_bad_description).unwrap(); + let metadata_bad_slot_name = AccountComponentMetadata::from_toml(toml_bad_name).unwrap(); + + assert_ne!( + metadata_a.storage_schema().commitment(), + metadata_bad_description.storage_schema().commitment() + ); + + assert_ne!( + metadata_a.storage_schema().commitment(), + metadata_bad_slot_name.storage_schema().commitment() + ); +} + +#[test] +fn metadata_from_toml_rejects_typed_fields_in_static_map_values() { + let toml_str = r#" + name = "Test Component" + description = "Test description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::my_map" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x1", value = { type = "word" } }, + ] + "#; + + assert_matches::assert_matches!( + AccountComponentMetadata::from_toml(toml_str), + Err(AccountComponentTemplateError::TomlDeserializationError(_)) + ); +} + +#[test] +fn metadata_from_toml_rejects_reserved_slot_names() { + let reserved_slot = AccountStorage::faucet_sysdata_slot().as_str(); + + let toml_str = format!( + r#" + name = "Test Component" + description = "Test description" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "{reserved_slot}" + type = "word" + "# + ); + + assert_matches::assert_matches!( + AccountComponentMetadata::from_toml(&toml_str), + Err(AccountComponentTemplateError::ReservedSlotName(_)) + ); +} + +#[test] +fn metadata_toml_round_trip_value_and_map_slots() { + let toml_str = r#" + name = "round trip" + description = "test round-trip" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::single_value" + description = "single word slot" + type = "word" + default-value = "0x1" + + [[storage.slots]] + name = "demo::statemap" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x000000000000ed5d", value = "0x10" }, + ] + "#; + + let original = + AccountComponentMetadata::from_toml(toml_str).expect("original metadata should parse"); + let round_trip_toml = original.to_toml().expect("serialize to toml"); + let round_trip = + AccountComponentMetadata::from_toml(&round_trip_toml).expect("round-trip parse"); + + assert_eq!(original, round_trip); +} + +#[test] +fn metadata_toml_round_trip_composed_slot_with_typed_fields() { + let toml_str = r#" + name = "round trip typed fields" + description = "test composed slot typed fields" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::composed" + description = "composed word with typed fields" + type = [ + { name = "a", type = "u16" }, + { name = "b", default-value = "0x2" }, + { name = "c", default-value = "0x3" }, + { name = "d", default-value = "0x4" }, + ] + "#; + + let original = + AccountComponentMetadata::from_toml(toml_str).expect("original metadata should parse"); + + let mut requirements = original.schema_requirements(); + assert_eq!( + requirements + .remove(&"demo::composed.a".parse::().unwrap()) + .unwrap() + .r#type, + SchemaTypeId::u16() + ); + + let round_trip_toml = original.to_toml().expect("serialize to toml"); + let round_trip = + AccountComponentMetadata::from_toml(&round_trip_toml).expect("round-trip parse"); + + assert_eq!(original, round_trip); +} + +#[test] +fn metadata_toml_round_trip_typed_slots() { + let toml_str = r#" + name = "typed components" + description = "test typed slots" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::typed_value" + type = "word" + + [[storage.slots]] + name = "demo::typed_map" + type = { key = "miden::standards::auth::falcon512_rpo::pub_key", value = "miden::standards::auth::falcon512_rpo::pub_key" } + "#; + + let metadata = + AccountComponentMetadata::from_toml(toml_str).expect("typed metadata should parse"); + let schema = metadata.storage_schema(); + + let value_slot = schema + .slots() + .get(&StorageSlotName::new("demo::typed_value").unwrap()) + .expect("value slot missing"); + let value_slot = match value_slot { + StorageSlotSchema::Value(slot) => slot, + _ => panic!("expected value slot"), + }; + + let typed_value = SchemaTypeId::native_word(); + assert_eq!(value_slot.word(), &WordSchema::new_simple(typed_value.clone())); + + let map_slot = schema + .slots() + .get(&StorageSlotName::new("demo::typed_map").unwrap()) + .expect("map slot missing"); + let map_slot = match map_slot { + StorageSlotSchema::Map(slot) => slot, + _ => panic!("expected map slot"), + }; + + let pub_key_type = SchemaTypeId::new("miden::standards::auth::falcon512_rpo::pub_key").unwrap(); + assert_eq!(map_slot.key_schema(), &WordSchema::new_simple(pub_key_type.clone())); + assert_eq!(map_slot.value_schema(), &WordSchema::new_simple(pub_key_type)); + + let mut requirements = metadata.schema_requirements(); + assert_eq!( + requirements + .remove(&"demo::typed_value".parse::().unwrap()) + .unwrap() + .r#type, + typed_value + ); + assert!(!requirements.contains_key(&"demo::typed_map".parse::().unwrap())); + + let round_trip = metadata.to_toml().expect("serialize"); + let parsed: toml::Value = toml::from_str(&round_trip).unwrap(); + let storage = parsed.get("storage").unwrap().as_table().unwrap(); + let storage_slots = storage.get("slots").unwrap().as_array().unwrap(); + + let typed_value_entry = storage_slots + .iter() + .find(|entry| entry.get("name").unwrap().as_str().unwrap() == "demo::typed_value") + .unwrap(); + assert_eq!(typed_value_entry.get("type").unwrap().as_str().unwrap(), "word"); + + let typed_map_entry = storage_slots + .iter() + .find(|entry| entry.get("name").unwrap().as_str().unwrap() == "demo::typed_map") + .unwrap(); + let map_type = typed_map_entry.get("type").unwrap().as_table().unwrap(); + assert_eq!( + map_type.get("key").unwrap().as_str().unwrap(), + "miden::standards::auth::falcon512_rpo::pub_key" + ); + assert_eq!( + map_type.get("value").unwrap().as_str().unwrap(), + "miden::standards::auth::falcon512_rpo::pub_key" + ); +} + +#[test] +fn extensive_schema_metadata_and_init_toml_example() { + let metadata_toml = r#" + name = "Extensive Example" + description = "Exercises composite slots, simple typed slots, static maps, optional init maps, and map typing." + version = "0.1.0" + supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"] + + # composed slot schema expressed via `type = [...]` + [[storage.slots]] + name = "demo::token_metadata" + description = "Token metadata: max_supply, symbol, decimals, reserved." + type = [ + { type = "u32", name = "max_supply", description = "Maximum supply (base units)" }, + { type = "miden::standards::fungible_faucets::metadata::token_symbol", name = "symbol", default-value = "TST" }, + { type = "u8", name = "decimals", description = "Token decimals" }, + { type = "void" }, + ] + + # simple word-typed slot (must be passed at instantiation) + [[storage.slots]] + name = "demo::owner_pub_key" + description = "Owner public key" + type = "miden::standards::auth::falcon512_rpo::pub_key" + + # simple felt-typed word slot (parsed as felt, stored as [0,0,0,]) + [[storage.slots]] + name = "demo::protocol_version" + description = "Protocol version stored as u8 in the last felt" + type = "u8" + + # word slot with an overridable default + [[storage.slots]] + name = "demo::static_word" + description = "A fully specified word slot" + type = "word" + default-value = ["0x1", "0x2", "0x3", "0x4"] + + # Word slot with explicit `type = "word"` + [[storage.slots]] + name = "demo::legacy_word" + type = "word" + default-value = "0x123" + + # Static map defaults (fully concrete key/value words) + [[storage.slots]] + name = "demo::static_map" + description = "Static map with default entries" + type = { key = "word", value = "word" } + default-values = [ + { key = "0x1", value = "0x10" }, + { key = ["0", "0", "0", "2"], value = ["0", "0", "0", "32"] }, + ] + + # Word/word map (explicit key/value types). + [[storage.slots]] + name = "demo::default_typed_map" + description = "Defaults to key/value type => word/word" + type = { key = "word", value = "word" } + + # init-populated map with key/value types + [[storage.slots]] + name = "demo::typed_map_new" + type.key = [ + { type = "felt", name = "prefix" }, + { type = "felt", name = "suffix" }, + { type = "void" }, + { type = "void" }, + ] + type.value = "u16" + "#; + + let metadata = AccountComponentMetadata::from_toml(metadata_toml).unwrap(); + + // TOML round-trips + let round_trip_toml = metadata.to_toml().unwrap(); + let round_trip = AccountComponentMetadata::from_toml(&round_trip_toml).unwrap(); + assert_eq!(metadata, round_trip); + + // map typing is always explicit + let default_map_name = StorageSlotName::new("demo::default_typed_map").unwrap(); + let StorageSlotSchema::Map(default_map) = + metadata.storage_schema().slots().get(&default_map_name).unwrap() + else { + panic!("expected map slot schema"); + }; + assert_eq!(default_map.key_schema(), &WordSchema::new_simple(SchemaTypeId::native_word())); + assert_eq!(default_map.value_schema(), &WordSchema::new_simple(SchemaTypeId::native_word())); + + // `type.key`/`type.value` parse as schema/type descriptors (not literal words). + let typed_map_new_name = StorageSlotName::new("demo::typed_map_new").unwrap(); + let StorageSlotSchema::Map(typed_map_new) = + metadata.storage_schema().slots().get(&typed_map_new_name).unwrap() + else { + panic!("expected map slot schema"); + }; + assert_eq!(typed_map_new.value_schema(), &WordSchema::new_simple(SchemaTypeId::u16())); + assert!(matches!(typed_map_new.key_schema(), WordSchema::Composite { .. })); + + // used storage slots + let requirements = metadata.schema_requirements(); + assert!(requirements.contains_key(&"demo::owner_pub_key".parse::().unwrap())); + assert!( + requirements.contains_key(&"demo::protocol_version".parse::().unwrap()) + ); + assert!( + requirements + .contains_key(&"demo::token_metadata.max_supply".parse::().unwrap()) + ); + assert!( + requirements + .contains_key(&"demo::token_metadata.decimals".parse::().unwrap()) + ); + let symbol_requirement = requirements + .get(&"demo::token_metadata.symbol".parse::().unwrap()) + .expect("symbol should be reported with a default value"); + assert_eq!( + symbol_requirement.r#type, + SchemaTypeId::new("miden::standards::fungible_faucets::metadata::token_symbol").unwrap() + ); + assert_eq!(symbol_requirement.default_value.as_deref(), Some("TST")); + assert!( + !requirements.contains_key(&"demo::typed_map_new".parse::().unwrap()) + ); + assert!(!requirements.contains_key(&"demo::static_map".parse::().unwrap())); + + // Build storage without providing optional defaulted fields. + let init_toml_defaults = r#" + "demo::owner_pub_key" = "0x1234" + "demo::protocol_version" = "7" + + "demo::token_metadata.max_supply" = "1000000" + "demo::token_metadata.decimals" = "6" + "#; + let init_defaults = InitStorageData::from_toml(init_toml_defaults).unwrap(); + let slots = metadata.storage_schema().build_storage_slots(&init_defaults).unwrap(); + + let token_metadata_name = StorageSlotName::new("demo::token_metadata").unwrap(); + let token_metadata_slot = slots.iter().find(|s| s.name() == &token_metadata_name).unwrap(); + let StorageSlotContent::Value(token_metadata_word) = token_metadata_slot.content() else { + panic!("expected value slot for token_metadata"); + }; + let symbol_felt: Felt = TokenSymbol::new("TST").unwrap().into(); + let expected_token_metadata = + Word::from([Felt::from(1_000_000u32), symbol_felt, Felt::from(6u8), Felt::ZERO]); + assert_eq!(token_metadata_word, &expected_token_metadata); + + let owner_pub_key_name = StorageSlotName::new("demo::owner_pub_key").unwrap(); + let owner_pub_key_slot = slots.iter().find(|s| s.name() == &owner_pub_key_name).unwrap(); + let StorageSlotContent::Value(owner_pub_key_word) = owner_pub_key_slot.content() else { + panic!("expected value slot for owner_pub_key"); + }; + let expected_pub_key = + Word::parse("0x0000000000000000000000000000000000000000000000000000000000001234").unwrap(); + assert_eq!(owner_pub_key_word, &expected_pub_key); + + let protocol_version_name = StorageSlotName::new("demo::protocol_version").unwrap(); + let protocol_version_slot = slots.iter().find(|s| s.name() == &protocol_version_name).unwrap(); + let StorageSlotContent::Value(protocol_version_word) = protocol_version_slot.content() else { + panic!("expected value slot for protocol_version"); + }; + assert_eq!( + protocol_version_word, + &Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(7u8)]) + ); + + let static_word_name = StorageSlotName::new("demo::static_word").unwrap(); + let static_word_slot = slots.iter().find(|s| s.name() == &static_word_name).unwrap(); + let StorageSlotContent::Value(static_word) = static_word_slot.content() else { + panic!("expected value slot for static_word"); + }; + assert_eq!( + static_word, + &Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]) + ); + + let legacy_word_name = StorageSlotName::new("demo::legacy_word").unwrap(); + let legacy_word_slot = slots.iter().find(|s| s.name() == &legacy_word_name).unwrap(); + let StorageSlotContent::Value(legacy_word) = legacy_word_slot.content() else { + panic!("expected value slot for legacy_word"); + }; + assert_eq!(legacy_word, &Word::parse("0x123").unwrap()); + + let static_map_name = StorageSlotName::new("demo::static_map").unwrap(); + let static_map_slot = slots.iter().find(|s| s.name() == &static_map_name).unwrap(); + let StorageSlotContent::Map(static_map) = static_map_slot.content() else { + panic!("expected map slot for static_map"); + }; + assert_eq!(static_map.num_entries(), 2); + assert_eq!(static_map.get(&Word::parse("0x1").unwrap()), Word::parse("0x10").unwrap()); + assert_eq!( + static_map.get(&Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(2)])), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(32)]) + ); + + let typed_map_new_slot = slots.iter().find(|s| s.name() == &typed_map_new_name).unwrap(); + let StorageSlotContent::Map(typed_map_new_contents) = typed_map_new_slot.content() else { + panic!("expected map slot for typed_map_new"); + }; + assert_eq!(typed_map_new_contents.num_entries(), 0); + + // Provide init-populated multiple map entries and rebuild. + let init_toml_with_overrides = r#" + "demo::owner_pub_key" = "0x1234" + "demo::protocol_version" = "7" + "demo::legacy_word" = "0x456" + + "demo::typed_map_new" = [ + { key = ["1", "2", "0", "0"], value = "16" }, + { key = ["3", "4", "0", "0"], value = "32" } + ] + + "demo::static_map" = [ + { key = "0x1", value = "0x99" }, # overrides default + { key = "0x3", value = "0x30" } # adds a new key + ] + + ["demo::token_metadata"] + max_supply = "1000000" + decimals = "6" + symbol = "BTC" + "#; + let init_with_overrides = InitStorageData::from_toml(init_toml_with_overrides).unwrap(); + let parsed_entries = init_with_overrides + .map_entries(&"demo::typed_map_new".parse::().unwrap()) + .expect("demo::typed_map_new map entries missing"); + assert_eq!(parsed_entries.len(), 2); + let slots_with_maps = + metadata.storage_schema().build_storage_slots(&init_with_overrides).unwrap(); + + let typed_map_new_slot = + slots_with_maps.iter().find(|s| s.name() == &typed_map_new_name).unwrap(); + let StorageSlotContent::Map(typed_map_new_contents) = typed_map_new_slot.content() else { + panic!("expected map slot for typed_map_new"); + }; + assert_eq!(typed_map_new_contents.num_entries(), 2); + + let key1 = Word::from([Felt::new(1), Felt::new(2), Felt::ZERO, Felt::ZERO]); + assert_eq!( + typed_map_new_contents.get(&key1), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(16)]) + ); + + let token_metadata_slot = + slots_with_maps.iter().find(|s| s.name() == &token_metadata_name).unwrap(); + let StorageSlotContent::Value(token_metadata_word) = token_metadata_slot.content() else { + panic!("expected value slot for token_metadata"); + }; + let symbol_felt: Felt = TokenSymbol::new("BTC").unwrap().into(); + let expected_token_metadata_overridden = + Word::from([Felt::from(1_000_000u32), symbol_felt, Felt::from(6u8), Felt::ZERO]); + assert_eq!(token_metadata_word, &expected_token_metadata_overridden); + + let legacy_word_slot = slots_with_maps.iter().find(|s| s.name() == &legacy_word_name).unwrap(); + let StorageSlotContent::Value(legacy_word) = legacy_word_slot.content() else { + panic!("expected value slot for legacy_word"); + }; + assert_eq!(legacy_word, &Word::parse("0x456").unwrap()); + + let static_map_slot = slots_with_maps.iter().find(|s| s.name() == &static_map_name).unwrap(); + let StorageSlotContent::Map(static_map) = static_map_slot.content() else { + panic!("expected map slot for static_map"); + }; + assert_eq!(static_map.num_entries(), 3); + assert_eq!(static_map.get(&Word::parse("0x1").unwrap()), Word::parse("0x99").unwrap()); + assert_eq!( + static_map.get(&Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(2)])), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(32)]) + ); + assert_eq!(static_map.get(&Word::parse("0x3").unwrap()), Word::parse("0x30").unwrap()); +} + +#[test] +fn typed_map_init_entries_are_validated() { + let metadata_toml = r#" + name = "typed map validation" + description = "validates init-provided map entries against type.key/type.value" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::typed_map" + type.key = [ + { name = "prefix" }, + { name = "suffix" }, + { type = "void" }, + { type = "void" } + ] + type.value = "u16" + "#; + + let metadata = AccountComponentMetadata::from_toml(metadata_toml).unwrap(); + + // Key schema requires the last 2 elements to be `void` (0). This also tests parsing composite + // keys against a schema + let init_toml = r#" + "demo::typed_map" = [ + { key = ["1", "2", "3", "0"], value = ["0", "0", "0", "1"] } + ] + "#; + let init_data = InitStorageData::from_toml(init_toml).unwrap(); + + assert_matches::assert_matches!( + metadata.storage_schema().build_storage_slots(&init_data), + Err(AccountComponentTemplateError::InvalidInitStorageValue(name, msg)) + if &name.to_string() == "demo::typed_map" && msg.contains("void") + ); +} + +#[test] +fn typed_map_supports_non_numeric_value_types() { + let metadata_toml = r#" + name = "typed map token_symbol" + description = "parses typed map values using slot-level type.value" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "demo::symbol_map" + type.key = "word" + type.value = "miden::standards::fungible_faucets::metadata::token_symbol" + "#; + + let metadata = AccountComponentMetadata::from_toml(metadata_toml).unwrap(); + + let init_toml = r#" + "demo::symbol_map" = [ + { key = "0x1", value = "BTC" } + ] + "#; + let init_data = InitStorageData::from_toml(init_toml).unwrap(); + + let slots = metadata.storage_schema().build_storage_slots(&init_data).unwrap(); + let slot_name = StorageSlotName::new("demo::symbol_map").unwrap(); + let slot = slots.iter().find(|s| s.name() == &slot_name).unwrap(); + + let StorageSlotContent::Map(map) = slot.content() else { + panic!("expected map slot"); + }; + + assert_eq!(map.num_entries(), 1); + + let key = Word::parse("0x1").unwrap(); + let symbol_felt: Felt = TokenSymbol::new("BTC").unwrap().into(); + let expected_value = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, symbol_felt]); + assert_eq!(map.get(&key), expected_value); +} diff --git a/crates/miden-protocol/src/account/component/storage/type_registry.rs b/crates/miden-protocol/src/account/component/storage/type_registry.rs new file mode 100644 index 0000000000..45512f5e95 --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/type_registry.rs @@ -0,0 +1,703 @@ +use alloc::boxed::Box; +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use core::error::Error; +use core::fmt::{self, Display}; + +use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; +use miden_core::{Felt, FieldElement, Word}; +use miden_crypto::dsa::{ecdsa_k256_keccak, falcon512_rpo}; +use miden_processor::DeserializationError; +use thiserror::Error; + +use crate::asset::TokenSymbol; +use crate::utils::sync::LazyLock; + +/// A global registry for schema type converters. +/// +/// It is used during component instantiation to convert init-provided values (typically provided +/// as strings) into their respective storage values. +pub static SCHEMA_TYPE_REGISTRY: LazyLock = LazyLock::new(|| { + let mut registry = SchemaTypeRegistry::new(); + registry.register_felt_type::(); + 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::(); + registry.register_word_type::(); + registry +}); + +// SCHEMA TYPE ERROR +// ================================================================================================ + +/// Errors that can occur when parsing or converting schema types. +/// +/// This enum covers various failure cases including parsing errors, conversion errors, +/// unsupported conversions, and cases where a required type is not found in the registry. +#[derive(Debug, Error)] +pub enum SchemaTypeError { + #[error("conversion error: {0}")] + ConversionError(String), + #[error("felt type ` {0}` not found in the type registry")] + FeltTypeNotFound(SchemaTypeId), + #[error("invalid type name `{0}`: {1}")] + InvalidTypeName(String, String), + #[error("failed to parse input `{input}` as `{schema_type}`")] + ParseError { + input: String, + schema_type: SchemaTypeId, + source: Box, + }, + #[error("word type ` {0}` not found in the type registry")] + WordTypeNotFound(SchemaTypeId), +} + +impl SchemaTypeError { + /// Creates a [`SchemaTypeError::ParseError`]. + pub fn parse( + input: impl Into, + schema_type: SchemaTypeId, + source: impl Error + Send + Sync + 'static, + ) -> Self { + SchemaTypeError::ParseError { + input: input.into(), + schema_type, + source: Box::new(source), + } + } +} + +// SCHEMA TYPE +// ================================================================================================ + +/// A newtype wrapper around a `String`, representing a schema type identifier. +/// +/// A valid schema identifier is a name in the style of Rust namespaces, composed of one or more +/// non-empty segments separated by `::`. Each segment can contain only ASCII alphanumerics or `_`. +/// +/// Some examples: +/// - `u32` +/// - `felt` +/// - `miden::standards::auth::falcon512_rpo::pub_key` +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] +#[cfg_attr(feature = "std", serde(transparent))] +pub struct SchemaTypeId(String); + +impl SchemaTypeId { + /// Creates a new [`SchemaTypeId`] from a `String`. + /// + /// The name must follow a Rust-style namespace format, consisting of one or more segments + /// (non-empty, and alphanumerical) separated by double-colon (`::`) delimiters. + /// + /// # Errors + /// + /// - If the identifier is empty. + /// - If any segment is empty or contains something other than alphanumerical + /// characters/underscores. + pub fn new(s: impl Into) -> Result { + let s = s.into(); + if s.is_empty() { + return Err(SchemaTypeError::InvalidTypeName( + s.clone(), + "schema type identifier is empty".to_string(), + )); + } + for segment in s.split("::") { + if segment.is_empty() { + return Err(SchemaTypeError::InvalidTypeName( + s.clone(), + "empty segment in schema type identifier".to_string(), + )); + } + if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return Err(SchemaTypeError::InvalidTypeName( + s.clone(), + format!("segment '{segment}' contains invalid characters"), + )); + } + } + Ok(Self(s)) + } + + /// Returns the schema type identifier for the `void` type. + /// + /// The `void` type always parses to `0` and is intended to model reserved or padding felts. + pub fn void() -> SchemaTypeId { + SchemaTypeId::new("void").expect("type is well formed") + } + + /// Returns the schema type identifier for the native [`Felt`] type. + pub fn native_felt() -> SchemaTypeId { + SchemaTypeId::new("felt").expect("type is well formed") + } + + /// Returns the schema type identifier for the native [`Word`] type. + pub fn native_word() -> SchemaTypeId { + SchemaTypeId::new("word").expect("type is well formed") + } + + /// Returns the schema type identifier for the native `u8` type. + pub fn u8() -> SchemaTypeId { + SchemaTypeId::new("u8").expect("type is well formed") + } + + /// Returns the schema type identifier for the native `u16` type. + pub fn u16() -> SchemaTypeId { + SchemaTypeId::new("u16").expect("type is well formed") + } + + /// Returns the schema type identifier for the native `u32` type. + pub fn u32() -> SchemaTypeId { + SchemaTypeId::new("u32").expect("type is well formed") + } + + /// Returns a reference to the inner string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for SchemaTypeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl Serializable for SchemaTypeId { + fn write_into(&self, target: &mut W) { + target.write(self.0.clone()) + } +} + +impl Deserializable for SchemaTypeId { + fn read_from(source: &mut R) -> Result { + let id: String = source.read()?; + + SchemaTypeId::new(id).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// SCHEMA REQUIREMENT +// ================================================================================================ + +/// Describes the expected type and additional metadata for an init-provided storage value. +/// +/// A schema requirement specifies the expected type identifier for an init value, along with +/// optional description and default value metadata. +/// +/// The `default_value` (when present) is the canonical string representation for this type, and +/// can be used directly in [`InitStorageData`](super::InitStorageData). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SchemaRequirement { + /// The expected type identifier. + pub r#type: SchemaTypeId, + /// An optional description providing additional context. + pub description: Option, + /// An optional default value, which can be overridden at component instantiation time. + pub default_value: Option, +} + +// SCHEMA TYPE TRAITS +// ================================================================================================ + +/// Trait for converting a string into a single `Felt`. +pub trait FeltType: Send + Sync { + /// Returns the type identifier. + fn type_name() -> SchemaTypeId + where + Self: Sized; + + /// Parses the input string into a `Felt`. + fn parse_str(input: &str) -> Result + where + Self: Sized; + + /// Displays a `Felt` in a canonical string representation for this type. + fn display_felt(value: Felt) -> Result + where + Self: Sized; +} + +/// Trait for converting a string into a single `Word`. +pub trait WordType: Send + Sync { + /// Returns the type identifier. + fn type_name() -> SchemaTypeId + where + Self: Sized; + + /// Parses the input string into a `Word`. + fn parse_str(input: &str) -> Result + where + Self: Sized; + + /// Displays a `Word` in a canonical string representation for this type. + fn display_word(value: Word) -> Result + where + Self: Sized; +} + +impl WordType for T +where + T: FeltType, +{ + fn type_name() -> SchemaTypeId { + ::type_name() + } + + fn parse_str(input: &str) -> Result { + let felt = ::parse_str(input)?; + Ok(Word::from([Felt::new(0), Felt::new(0), Felt::new(0), felt])) + } + + fn display_word(value: Word) -> Result { + if value[0] != Felt::new(0) || value[1] != Felt::new(0) || value[2] != Felt::new(0) { + return Err(SchemaTypeError::ConversionError(format!( + "expected a word of the form [0, 0, 0, ] for type `{}`", + Self::type_name() + ))); + } + ::display_felt(value[3]) + } +} + +// FELT IMPLS FOR NATIVE TYPES +// ================================================================================================ + +/// A felt type that represents irrelevant elements in a storage schema definition. +struct Void; + +impl FeltType for Void { + fn type_name() -> SchemaTypeId { + SchemaTypeId::void() + } + + fn parse_str(input: &str) -> Result { + let parsed = ::parse_str(input)?; + if parsed != Felt::new(0) { + return Err(SchemaTypeError::ConversionError("void values must be zero".to_string())); + } + Ok(Felt::new(0)) + } + + fn display_felt(value: Felt) -> Result { + if value != Felt::new(0) { + return Err(SchemaTypeError::ConversionError("void values must be zero".to_string())); + } + Ok("0".into()) + } +} + +impl FeltType for u8 { + fn type_name() -> SchemaTypeId { + SchemaTypeId::u8() + } + + fn parse_str(input: &str) -> Result { + let native: u8 = input.parse().map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Ok(Felt::from(native)) + } + + fn display_felt(value: Felt) -> Result { + let native = u8::try_from(value.as_int()).map_err(|_| { + SchemaTypeError::ConversionError(format!("value `{}` is out of range for u8", value)) + })?; + Ok(native.to_string()) + } +} + +impl FeltType for u16 { + fn type_name() -> SchemaTypeId { + SchemaTypeId::u16() + } + + fn parse_str(input: &str) -> Result { + let native: u16 = input.parse().map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Ok(Felt::from(native)) + } + + fn display_felt(value: Felt) -> Result { + let native = u16::try_from(value.as_int()).map_err(|_| { + SchemaTypeError::ConversionError(format!("value `{}` is out of range for u16", value)) + })?; + Ok(native.to_string()) + } +} + +impl FeltType for u32 { + fn type_name() -> SchemaTypeId { + SchemaTypeId::u32() + } + + fn parse_str(input: &str) -> Result { + let native: u32 = input.parse().map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Ok(Felt::from(native)) + } + + fn display_felt(value: Felt) -> Result { + let native = u32::try_from(value.as_int()).map_err(|_| { + SchemaTypeError::ConversionError(format!("value `{}` is out of range for u32", value)) + })?; + Ok(native.to_string()) + } +} + +impl FeltType for Felt { + fn type_name() -> SchemaTypeId { + SchemaTypeId::new("felt").expect("type is well formed") + } + + fn parse_str(input: &str) -> Result { + let n = if let Some(hex) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) { + u64::from_str_radix(hex, 16) + } else { + input.parse::() + } + .map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Felt::try_from(n).map_err(|_| SchemaTypeError::ConversionError(input.to_string())) + } + + fn display_felt(value: Felt) -> Result { + Ok(format!("0x{:x}", value.as_int())) + } +} + +impl FeltType for TokenSymbol { + fn type_name() -> SchemaTypeId { + SchemaTypeId::new("miden::standards::fungible_faucets::metadata::token_symbol") + .expect("type is well formed") + } + fn parse_str(input: &str) -> Result { + let token = TokenSymbol::new(input).map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Ok(Felt::from(token)) + } + + fn display_felt(value: Felt) -> Result { + let token = TokenSymbol::try_from(value).map_err(|err| { + SchemaTypeError::ConversionError(format!( + "invalid token_symbol value `{}`: {err}", + value.as_int() + )) + })?; + token.to_string().map_err(|err| { + SchemaTypeError::ConversionError(format!( + "failed to display token_symbol value `{}`: {err}", + value.as_int() + )) + }) + } +} + +// WORD IMPLS FOR NATIVE TYPES +// ================================================================================================ + +#[derive(Debug, Error)] +#[error("error parsing word: {0}")] +struct WordParseError(String); + +/// Pads a hex string to 64 characters (excluding the 0x prefix). +/// +/// If the input starts with "0x" and has fewer than 64 hex characters after the prefix, +/// it will be left-padded with zeros. Otherwise, returns the input unchanged. +fn pad_hex_string(input: &str) -> String { + if input.starts_with("0x") && input.len() < 66 { + // 66 = "0x" + 64 hex chars + let hex_part = &input[2..]; + let padding = "0".repeat(64 - hex_part.len()); + format!("0x{}{}", padding, hex_part) + } else { + input.to_string() + } +} + +impl WordType for Word { + fn type_name() -> SchemaTypeId { + SchemaTypeId::native_word() + } + fn parse_str(input: &str) -> Result { + Word::parse(input).map_err(|err| { + SchemaTypeError::parse( + input.to_string(), + Self::type_name(), + WordParseError(err.to_string()), + ) + }) + } + + fn display_word(value: Word) -> Result { + Ok(value.to_string()) + } +} + +impl WordType for falcon512_rpo::PublicKey { + fn type_name() -> SchemaTypeId { + SchemaTypeId::new("miden::standards::auth::falcon512_rpo::pub_key") + .expect("type is well formed") + } + fn parse_str(input: &str) -> Result { + let padded_input = pad_hex_string(input); + + Word::try_from(padded_input.as_str()).map_err(|err| { + SchemaTypeError::parse( + input.to_string(), // Use original input in error + Self::type_name(), + WordParseError(err.to_string()), + ) + }) + } + + fn display_word(value: Word) -> Result { + Ok(value.to_string()) + } +} + +impl WordType for ecdsa_k256_keccak::PublicKey { + fn type_name() -> SchemaTypeId { + SchemaTypeId::new("miden::standards::auth::ecdsa_k256_keccak::pub_key") + .expect("type is well formed") + } + fn parse_str(input: &str) -> Result { + let padded_input = pad_hex_string(input); + + Word::try_from(padded_input.as_str()).map_err(|err| { + SchemaTypeError::parse( + input.to_string(), + Self::type_name(), + WordParseError(err.to_string()), + ) + }) + } + + fn display_word(value: Word) -> Result { + Ok(value.to_string()) + } +} + +// TYPE ALIASES FOR CONVERTER CLOSURES +// ================================================================================================ + +/// Type alias for a function that converts a string into a [`Felt`] value. +type FeltFromStrConverter = fn(&str) -> Result; + +/// Type alias for a function that converts a string into a [`Word`]. +type WordFromStrConverter = fn(&str) -> Result; + +/// Type alias for a function that converts a [`Felt`] into a canonical string representation. +type FeltTypeDisplayer = fn(Felt) -> Result; + +/// Type alias for a function that converts a [`Word`] into a canonical string representation. +type WordTypeDisplayer = fn(Word) -> Result; + +/// Result of a word display conversion. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WordDisplay { + Word(String), + Felt(String), + Hex(String), +} + +impl WordDisplay { + pub fn value(&self) -> &str { + match self { + WordDisplay::Word(v) => v, + WordDisplay::Felt(v) => v, + WordDisplay::Hex(v) => v, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TypeKind { + Word, + Felt, +} + +// SCHEMA TYPE REGISTRY +// ================================================================================================ + +/// Registry for schema type converters. +/// +/// This registry maintains mappings from type identifiers (as strings) to conversion functions for +/// [`Felt`] and [`Word`] types. It is used to dynamically parse init-provided inputs into their +/// corresponding storage values. +#[derive(Clone, Debug, Default)] +pub struct SchemaTypeRegistry { + felt: BTreeMap, + word: BTreeMap, + felt_display: BTreeMap, + word_display: BTreeMap, +} + +impl SchemaTypeRegistry { + /// Creates a new, empty [`SchemaTypeRegistry`]. + /// + /// The registry is initially empty and conversion functions can be registered using the + /// `register_*_type` methods. + pub fn new() -> Self { + Self::default() + } + + /// Registers a `FeltType` converter, to interpret a string as a [`Felt``]. + pub fn register_felt_type(&mut self) { + let key = ::type_name(); + self.felt.insert(key.clone(), T::parse_str); + self.felt_display.insert(key, T::display_felt); + } + + /// Registers a `WordType` converter, to interpret a string as a [`Word`]. + pub fn register_word_type(&mut self) { + let key = ::type_name(); + self.word.insert(key.clone(), T::parse_str); + self.word_display.insert(key, T::display_word); + } + + /// Attempts to parse a string into a `Felt` using the registered converter for the given type + /// name. + /// + /// # Arguments + /// + /// - type_name: A string that acts as the type identifier. + /// - value: The string input that should be parsed. + /// + /// # Errors + /// + /// - If the type is not registered or if the conversion fails. + pub fn try_parse_felt( + &self, + type_name: &SchemaTypeId, + value: &str, + ) -> Result { + let converter = self + .felt + .get(type_name) + .ok_or(SchemaTypeError::FeltTypeNotFound(type_name.clone()))?; + converter(value) + } + + /// Validates that the given [`Felt`] conforms to the specified schema type. + pub fn validate_felt_value( + &self, + type_name: &SchemaTypeId, + felt: Felt, + ) -> Result<(), SchemaTypeError> { + let display = self + .felt_display + .get(type_name) + .ok_or(SchemaTypeError::FeltTypeNotFound(type_name.clone()))?; + display(felt).map(|_| ()) + } + + // VALUE VALIDATION HELPERS + // ============================================================================================ + + /// Validates that the given [`Word`] conforms to the specified schema type. + pub fn validate_word_value( + &self, + type_name: &SchemaTypeId, + word: Word, + ) -> Result<(), SchemaTypeError> { + match self.type_kind(type_name) { + TypeKind::Word => Ok(()), + TypeKind::Felt => { + // Felt types stored as words must have the form [0, 0, 0, ] + if word[0] != Felt::ZERO || word[1] != Felt::ZERO || word[2] != Felt::ZERO { + return Err(SchemaTypeError::ConversionError(format!( + "expected a word of the form [0, 0, 0, ] for type `{type_name}`" + ))); + } + self.validate_felt_value(type_name, word[3]) + }, + } + } + + /// Converts a [`Felt`] into a canonical string representation for the given schema type. + /// + /// This is intended for serializing schemas to TOML (e.g. default values). + #[allow(dead_code)] + pub fn display_felt(&self, type_name: &SchemaTypeId, felt: Felt) -> String { + self.felt_display + .get(type_name) + .and_then(|display| display(felt).ok()) + .unwrap_or_else(|| format!("0x{:x}", felt.as_int())) + } + + /// Converts a [`Word`] into a canonical string representation and reports how it was produced. + pub fn display_word(&self, type_name: &SchemaTypeId, word: Word) -> WordDisplay { + if let Some(display) = self.word_display.get(type_name) { + let value = display(word).unwrap_or_else(|_| word.to_string()); + return WordDisplay::Word(value); + } + + // Treat any registered felt type as a word type by zero-padding the remaining felts. + if self.contains_felt_type(type_name) { + let value = self.display_felt(type_name, word[3]); + return WordDisplay::Felt(value); + } + + WordDisplay::Hex(word.to_hex()) + } + + /// Attempts to parse a string into a `Word` using the registered converter for the given type + /// name. + /// + /// # Arguments + /// + /// - type_name: A string that acts as the type identifier. + /// - value: The string input that should be parsed. + /// + /// # Errors + /// + /// - If the type is not registered or if the conversion fails. + pub fn try_parse_word( + &self, + type_name: &SchemaTypeId, + value: &str, + ) -> Result { + if let Some(converter) = self.word.get(type_name) { + return converter(value); + } + + // Treat any registered felt type as a word type by zero-padding the remaining felts. + if let Some(converter) = self.felt.get(type_name) { + let felt = converter(value)?; + return Ok(Word::from([Felt::new(0), Felt::new(0), Felt::new(0), felt])); + } + + Err(SchemaTypeError::WordTypeNotFound(type_name.clone())) + } + + /// Returns `true` if a `FeltType` is registered for the given type. + pub fn contains_felt_type(&self, type_name: &SchemaTypeId) -> bool { + self.felt.contains_key(type_name) + } + + fn type_kind(&self, type_name: &SchemaTypeId) -> TypeKind { + if self.contains_felt_type(type_name) { + TypeKind::Felt + } else { + TypeKind::Word + } + } + + /// Returns `true` if a `WordType` is registered for the given type. + /// + /// This also returns `true` for any registered felt type (as those can be embedded into a word + /// with zero-padding). + pub fn contains_word_type(&self, type_name: &SchemaTypeId) -> bool { + self.word.contains_key(type_name) || self.felt.contains_key(type_name) + } +} diff --git a/crates/miden-protocol/src/account/component/storage/value_name.rs b/crates/miden-protocol/src/account/component/storage/value_name.rs new file mode 100644 index 0000000000..302916d9de --- /dev/null +++ b/crates/miden-protocol/src/account/component/storage/value_name.rs @@ -0,0 +1,210 @@ +use alloc::string::{String, ToString}; +use core::cmp::Ordering; +use core::fmt::{self, Display}; +use core::str::FromStr; + +use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; +use miden_processor::DeserializationError; +use thiserror::Error; + +use crate::account::StorageSlotName; +use crate::errors::StorageSlotNameError; + +/// A simple wrapper type around a string key that identifies init-provided values. +/// +/// A storage value name is a string that identifies values supplied during component +/// instantiation (via [`InitStorageData`](super::InitStorageData)). +/// +/// Each name is either a storage slot name, or a storage slot name with a suffixed identifier for +/// composite types (where the suffix identifies the inner type). +#[derive(Clone, Debug)] +#[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] +#[cfg_attr(feature = "std", serde(try_from = "String", into = "String"))] +pub struct StorageValueName { + slot_name: StorageSlotName, + element_field: Option, +} + +impl StorageValueName { + /// Creates a [`StorageValueName`] for the given storage slot. + pub fn from_slot_name(slot_name: &StorageSlotName) -> Self { + StorageValueName { + slot_name: slot_name.clone(), + element_field: None, + } + } + + /// Creates a [`StorageValueName`] for the given storage slot and field suffix. + /// + /// A suffixed slot name is used to identify a specific field element's type in a schema + /// (e.g., `miden::contracts::fungible_faucets::token_metadata.max_supply` can specify the + /// `max_supply` element in the `token_metadata` storage slot) + pub fn from_slot_name_with_suffix( + slot_name: &StorageSlotName, + suffix: &str, + ) -> Result { + Self::validate_field_segment(suffix)?; + Ok(StorageValueName { + slot_name: slot_name.clone(), + element_field: Some(suffix.to_string()), + }) + } + + /// Returns the storage slot name prefix of this value name. + pub fn slot_name(&self) -> &StorageSlotName { + &self.slot_name + } + + /// Returns the optional field suffix of this value name. + pub fn field_name(&self) -> Option<&str> { + self.element_field.as_deref() + } + + fn validate_field_segment(segment: &str) -> Result<(), StorageValueNameError> { + if segment.is_empty() { + return Err(StorageValueNameError::EmptySuffix); + } + + if let Some(offending_char) = + segment.chars().find(|&c| !(c.is_ascii_alphanumeric() || c == '_' || c == '-')) + { + return Err(StorageValueNameError::InvalidCharacter { + part: segment.to_string(), + character: offending_char, + }); + } + + Ok(()) + } +} + +impl PartialEq for StorageValueName { + fn eq(&self, other: &Self) -> bool { + self.slot_name.as_str() == other.slot_name.as_str() + && self.element_field.as_deref() == other.element_field.as_deref() + } +} + +impl Eq for StorageValueName {} + +impl PartialOrd for StorageValueName { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for StorageValueName { + fn cmp(&self, other: &Self) -> Ordering { + let slot_cmp = self.slot_name.as_str().cmp(other.slot_name.as_str()); + if slot_cmp != Ordering::Equal { + return slot_cmp; + } + + match (self.element_field.as_deref(), other.element_field.as_deref()) { + (None, None) => Ordering::Equal, + + // "" is a prefix of ".", so it sorts first. + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + + (Some(a), Some(b)) => a.cmp(b), + } + } +} + +impl FromStr for StorageValueName { + type Err = StorageValueNameError; + + fn from_str(value: &str) -> Result { + if value.is_empty() { + return Err(StorageValueNameError::EmptySuffix); + } + + // `StorageValueName` represents: + // - a storage slot name (`StorageSlotName`), or + // - a fully-qualified storage slot field key (`named::slot.field`). + let (slot, field) = match value.split_once('.') { + Some((slot, field)) => { + Self::validate_field_segment(field)?; + + if slot.is_empty() || field.is_empty() { + return Err(StorageValueNameError::EmptySuffix); + } + + (slot, Some(field)) + }, + None => (value, None), + }; + + let slot_name = + StorageSlotName::new(slot).map_err(StorageValueNameError::InvalidSlotName)?; + let field = match field { + Some(field) => { + Self::validate_field_segment(field)?; + Some(field.to_string()) + }, + None => None, + }; + + Ok(Self { slot_name, element_field: field }) + } +} + +impl TryFrom for StorageValueName { + type Error = StorageValueNameError; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +impl From for String { + fn from(value: StorageValueName) -> Self { + value.to_string() + } +} + +impl From<&StorageSlotName> for StorageValueName { + fn from(value: &StorageSlotName) -> Self { + StorageValueName::from_slot_name(value) + } +} + +impl Display for StorageValueName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.element_field { + None => f.write_str(self.slot_name.as_str()), + Some(field) => { + f.write_str(self.slot_name.as_str())?; + f.write_str(".")?; + f.write_str(field) + }, + } + } +} + +impl Serializable for StorageValueName { + fn write_into(&self, target: &mut W) { + let key = self.to_string(); + target.write(&key); + } +} + +impl Deserializable for StorageValueName { + fn read_from(source: &mut R) -> Result { + let key: String = source.read()?; + key.parse().map_err(|err: StorageValueNameError| { + DeserializationError::InvalidValue(err.to_string()) + }) + } +} + +#[derive(Debug, Error)] +pub enum StorageValueNameError { + #[error("key suffix is empty")] + EmptySuffix, + #[error("key segment '{part}' contains invalid character '{character}'")] + InvalidCharacter { part: String, character: char }, + #[error("invalid storage slot name")] + InvalidSlotName(#[source] StorageSlotNameError), +} diff --git a/crates/miden-objects/src/account/delta/mod.rs b/crates/miden-protocol/src/account/delta/mod.rs similarity index 93% rename from crates/miden-objects/src/account/delta/mod.rs rename to crates/miden-protocol/src/account/delta/mod.rs index a51c58edd6..1b8d13d1f5 100644 --- a/crates/miden-objects/src/account/delta/mod.rs +++ b/crates/miden-protocol/src/account/delta/mod.rs @@ -11,11 +11,12 @@ use crate::account::{ }; use crate::asset::AssetVault; use crate::crypto::SequentialCommit; +use crate::errors::{AccountDeltaError, AccountError}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{AccountDeltaError, AccountError, Felt, Word, ZERO}; +use crate::{Felt, Word, ZERO}; mod storage; -pub use storage::{AccountStorageDelta, StorageMapDelta}; +pub use storage::{AccountStorageDelta, StorageMapDelta, StorageSlotDelta}; mod vault; pub use vault::{ @@ -205,15 +206,19 @@ impl AccountDelta { /// assets since `faucet_id_prefix` is at the same position in the layout for both assets, /// and, by design, it is never the same for fungible and non-fungible assets. /// - Append `[hash0, hash1, hash2, faucet_id_prefix]`, i.e. the non-fungible asset. - /// - Storage Slots - for each slot **whose value has changed**, depending on the slot type: + /// - Storage Slots are sorted by slot ID and are iterated in this order. For each slot **whose + /// value has changed**, depending on the slot type: /// - Value Slot - /// - Append `[[domain = 2, slot_idx, 0, 0], NEW_VALUE]` where NEW_VALUE is the new value of - /// the slot and slot_idx is the index of the slot. + /// - Append `[[domain = 2, 0, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where + /// `NEW_VALUE` is the new value of the slot and `slot_id_{suffix, prefix}` is the + /// identifier of the slot. /// - Map Slot /// - For each key-value pair, sorted by key, whose new value is different from the previous /// value in the map: /// - Append `[KEY, NEW_VALUE]`. - /// - Append `[[domain = 3, slot_idx, num_changed_entries, 0], 0, 0, 0, 0]`. + /// - Append `[[domain = 3, num_changed_entries, slot_id_suffix, slot_id_prefix], 0, 0, 0, + /// 0]`, where `slot_id_{suffix, prefix}` are the slot identifiers and + /// `num_changed_entries` is the number of changed key-value pairs in the map. /// - For partial state deltas, the map header must only be included if /// `num_changed_entries` is not zero. /// - For full state deltas, the map header must always be included. @@ -252,10 +257,9 @@ impl AccountDelta { /// a value slot being set to EMPTY_WORD and its value being unchanged. /// - Storage map slots: /// - Map slots append a header which summarizes the changes in the slot, in particular the - /// slot index and number of changed entries. Since only changed slots are included, the - /// number of changed entries is never zero. + /// slot ID and number of changed entries. /// - Two distinct storage map slots use the same domain but are disambiguated due to - /// inclusion of the slot index. + /// inclusion of the slot ID. /// /// ### Domain Separators /// @@ -265,7 +269,7 @@ impl AccountDelta { /// [ /// ID_AND_NONCE, EMPTY_WORD, /// [/* no fungible asset delta */], - /// [[domain = 1, was_added = 1, 0, 0], NON_FUNGIBLE_ASSET], + /// [[domain = 1, was_added = 0, 0, 0], NON_FUNGIBLE_ASSET], /// [/* no storage delta */] /// ] /// ``` @@ -275,7 +279,7 @@ impl AccountDelta { /// ID_AND_NONCE, EMPTY_WORD, /// [/* no fungible asset delta */], /// [/* no non-fungible asset delta */], - /// [[domain = 2, slot_idx = 1, 0, 0], NEW_VALUE] + /// [[domain = 2, 0, slot_id_suffix = 0, slot_id_prefix = 0], NEW_VALUE] /// ] /// ``` /// @@ -293,8 +297,8 @@ impl AccountDelta { /// ID_AND_NONCE, EMPTY_WORD, /// [/* no fungible asset delta */], /// [/* no non-fungible asset delta */], - /// [domain = 3, slot_idx = 0, num_changed_entries = 0, 0, 0, 0, 0, 0] - /// [domain = 3, slot_idx = 1, num_changed_entries = 0, 0, 0, 0, 0, 0] + /// [domain = 3, num_changed_entries = 0, slot_id_suffix = 20, slot_id_prefix = 21, 0, 0, 0, 0] + /// [domain = 3, num_changed_entries = 0, slot_id_suffix = 42, slot_id_prefix = 43, 0, 0, 0, 0] /// ] /// ``` /// @@ -304,7 +308,7 @@ impl AccountDelta { /// [/* no fungible asset delta */], /// [/* no non-fungible asset delta */], /// [KEY0, VALUE0], - /// [domain = 3, slot_idx = 1, num_changed_entries = 1, 0, 0, 0, 0, 0] + /// [domain = 3, num_changed_entries = 1, slot_id_suffix = 42, slot_id_prefix = 43, 0, 0, 0, 0] /// ] /// ``` /// @@ -356,11 +360,10 @@ impl TryFrom<&AccountDelta> for Account { // this to create an empty account and use `Account::apply_delta` instead. // For now, we need to create the initial storage of the account with the same slot types. let mut empty_storage_slots = Vec::new(); - for slot_idx in 0..u8::MAX { - let slot = match delta.storage().slot_type(slot_idx) { - Some(StorageSlotType::Value) => StorageSlot::empty_value(), - Some(StorageSlotType::Map) => StorageSlot::empty_map(), - None => break, + for (slot_name, slot_delta) in delta.storage().slots() { + let slot = match slot_delta.slot_type() { + StorageSlotType::Value => StorageSlot::with_empty_value(slot_name.clone()), + StorageSlotType::Map => StorageSlot::with_empty_map(slot_name.clone()), }; empty_storage_slots.push(slot); } @@ -597,6 +600,7 @@ mod tests { AccountStorageMode, AccountType, StorageMapDelta, + StorageSlotName, }; use crate::asset::{ Asset, @@ -605,12 +609,13 @@ mod tests { NonFungibleAsset, NonFungibleAssetDetails, }; + use crate::errors::AccountDeltaError; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, AccountIdBuilder, }; - use crate::{AccountDeltaError, ONE, Word, ZERO}; + use crate::{ONE, Word, ZERO}; #[test] fn account_delta_nonce_validation() { @@ -623,7 +628,7 @@ mod tests { AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap(); // non-empty delta - let storage_delta = AccountStorageDelta::from_iters([1], [], []); + let storage_delta = AccountStorageDelta::from_iters([StorageSlotName::mock(1)], [], []); assert_matches!( AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO) @@ -671,10 +676,13 @@ mod tests { assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint()); let storage_delta = AccountStorageDelta::from_iters( - [1], - [(2, Word::from([1, 1, 1, 1u32])), (3, Word::from([1, 1, 0, 1u32]))], + [StorageSlotName::mock(1)], + [ + (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])), + (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])), + ], [( - 4, + StorageSlotName::mock(4), StorageMapDelta::from_iters( [Word::from([1, 1, 1, 0u32]), Word::from([0, 1, 1, 1u32])], [(Word::from([1, 1, 1, 1u32]), Word::from([1, 1, 1, 1u32]))], diff --git a/crates/miden-protocol/src/account/delta/storage.rs b/crates/miden-protocol/src/account/delta/storage.rs new file mode 100644 index 0000000000..bd600ac0e0 --- /dev/null +++ b/crates/miden-protocol/src/account/delta/storage.rs @@ -0,0 +1,806 @@ +use alloc::collections::BTreeMap; +use alloc::collections::btree_map::Entry; +use alloc::vec::Vec; + +use super::{ + AccountDeltaError, + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, + Word, +}; +use crate::account::{StorageMap, StorageSlotContent, StorageSlotName, StorageSlotType}; +use crate::{EMPTY_WORD, Felt, LexicographicWord, ZERO}; + +// ACCOUNT STORAGE DELTA +// ================================================================================================ + +/// The [`AccountStorageDelta`] stores the differences between two states of account storage. +/// +/// The delta consists of a map from [`StorageSlotName`] to [`StorageSlotDelta`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AccountStorageDelta { + /// The updates to the slots of the account. + deltas: BTreeMap, +} + +impl AccountStorageDelta { + /// Creates a new, empty storage delta. + pub fn new() -> Self { + Self { deltas: BTreeMap::new() } + } + + /// Creates a new storage delta from the provided slot deltas. + pub fn from_raw(deltas: BTreeMap) -> Self { + Self { deltas } + } + + /// Returns the delta for the provided slot name, or `None` if no delta exists. + pub fn get(&self, slot_name: &StorageSlotName) -> Option<&StorageSlotDelta> { + self.deltas.get(slot_name) + } + + /// Returns an iterator over the slot deltas. + pub(crate) fn slots(&self) -> impl Iterator { + self.deltas.iter() + } + + /// Returns an iterator over the updated values in this storage delta. + pub fn values(&self) -> impl Iterator { + self.deltas.iter().filter_map(|(slot_name, slot_delta)| match slot_delta { + StorageSlotDelta::Value(word) => Some((slot_name, word)), + StorageSlotDelta::Map(_) => None, + }) + } + + /// Returns an iterator over the updated maps in this storage delta. + pub fn maps(&self) -> impl Iterator { + self.deltas.iter().filter_map(|(slot_name, slot_delta)| match slot_delta { + StorageSlotDelta::Value(_) => None, + StorageSlotDelta::Map(map_delta) => Some((slot_name, map_delta)), + }) + } + + /// Returns true if storage delta contains no updates. + pub fn is_empty(&self) -> bool { + self.deltas.is_empty() + } + + /// Tracks a slot change. + /// + /// This does not (and cannot) validate that the slot name _exists_ or that it points to a + /// _value_ slot in the corresponding account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the slot name points to an existing slot that is not of type value. + pub fn set_item( + &mut self, + slot_name: StorageSlotName, + new_slot_value: Word, + ) -> Result<(), AccountDeltaError> { + if !self.deltas.get(&slot_name).map(StorageSlotDelta::is_value).unwrap_or(true) { + return Err(AccountDeltaError::StorageSlotUsedAsDifferentTypes(slot_name)); + } + + self.deltas.insert(slot_name, StorageSlotDelta::Value(new_slot_value)); + + Ok(()) + } + + /// Tracks a map item change. + /// + /// This does not (and cannot) validate that the slot name _exists_ or that it points to a + /// _map_ slot in the corresponding account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the slot name points to an existing slot that is not of type map. + pub fn set_map_item( + &mut self, + slot_name: StorageSlotName, + key: Word, + new_value: Word, + ) -> Result<(), AccountDeltaError> { + match self + .deltas + .entry(slot_name.clone()) + .or_insert(StorageSlotDelta::Map(StorageMapDelta::default())) + { + StorageSlotDelta::Value(_) => { + return Err(AccountDeltaError::StorageSlotUsedAsDifferentTypes(slot_name)); + }, + StorageSlotDelta::Map(storage_map_delta) => { + storage_map_delta.insert(key, new_value); + }, + }; + + Ok(()) + } + + /// Inserts an empty storage map delta for the provided slot name. + /// + /// This is useful for full state deltas to represent an empty map in the delta. + /// + /// This overwrites the existing slot delta, if any. + pub fn insert_empty_map_delta(&mut self, slot_name: StorageSlotName) { + self.deltas.insert(slot_name, StorageSlotDelta::with_empty_map()); + } + + /// Merges another delta into this one, overwriting any existing values. + pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> { + for (slot_name, slot_delta) in other.deltas { + match self.deltas.entry(slot_name.clone()) { + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(slot_delta); + }, + Entry::Occupied(mut occupied_entry) => { + occupied_entry.get_mut().merge(slot_delta).ok_or_else(|| { + AccountDeltaError::StorageSlotUsedAsDifferentTypes(slot_name) + })?; + }, + } + } + + Ok(()) + } + + /// Returns an iterator of all the cleared storage slots. + fn cleared_values(&self) -> impl Iterator { + self.values().filter_map( + |(slot_name, slot_value)| { + if slot_value.is_empty() { Some(slot_name) } else { None } + }, + ) + } + + /// Returns an iterator of all the updated storage slots. + fn updated_values(&self) -> impl Iterator { + self.values().filter_map(|(slot_name, slot_value)| { + if !slot_value.is_empty() { + Some((slot_name, slot_value)) + } else { + None + } + }) + } + + /// Appends the storage slots delta to the given `elements` from which the delta commitment will + /// be computed. + pub(super) fn append_delta_elements(&self, elements: &mut Vec) { + const DOMAIN_VALUE: Felt = Felt::new(2); + const DOMAIN_MAP: Felt = Felt::new(3); + + for (slot_name, slot_delta) in self.deltas.iter() { + let slot_id = slot_name.id(); + + match slot_delta { + StorageSlotDelta::Value(new_value) => { + elements.extend_from_slice(&[ + DOMAIN_VALUE, + ZERO, + slot_id.suffix(), + slot_id.prefix(), + ]); + elements.extend_from_slice(new_value.as_elements()); + }, + StorageSlotDelta::Map(map_delta) => { + for (key, value) in map_delta.entries() { + elements.extend_from_slice(key.inner().as_elements()); + elements.extend_from_slice(value.as_elements()); + } + + let num_changed_entries = Felt::try_from(map_delta.num_entries()).expect( + "number of changed entries should not exceed max representable felt", + ); + + elements.extend_from_slice(&[ + DOMAIN_MAP, + num_changed_entries, + slot_id.suffix(), + slot_id.prefix(), + ]); + elements.extend_from_slice(EMPTY_WORD.as_elements()); + }, + } + } + } + + /// Consumes self and returns the underlying map of the storage delta. + pub fn into_map(self) -> BTreeMap { + self.deltas + } +} + +impl Default for AccountStorageDelta { + fn default() -> Self { + Self::new() + } +} + +impl Serializable for AccountStorageDelta { + fn write_into(&self, target: &mut W) { + let num_cleared_values = self.cleared_values().count(); + let num_cleared_values = + u8::try_from(num_cleared_values).expect("number of slots should fit in u8"); + let cleared_values = self.cleared_values(); + + let num_updated_values = self.updated_values().count(); + let num_updated_values = + u8::try_from(num_updated_values).expect("number of slots should fit in u8"); + let updated_values = self.updated_values(); + + let num_maps = self.maps().count(); + let num_maps = u8::try_from(num_maps).expect("number of slots should fit in u8"); + let maps = self.maps(); + + target.write_u8(num_cleared_values); + target.write_many(cleared_values); + + target.write_u8(num_updated_values); + target.write_many(updated_values); + + target.write_u8(num_maps); + target.write_many(maps); + } + + fn get_size_hint(&self) -> usize { + let u8_size = 0u8.get_size_hint(); + + let mut storage_map_delta_size = 0; + for (slot_name, storage_map_delta) in self.maps() { + // The serialized size of each entry is the combination of slot (key) and the delta + // (value). + storage_map_delta_size += slot_name.get_size_hint() + storage_map_delta.get_size_hint(); + } + + // Length Prefixes + u8_size * 3 + + // Cleared Values + self.cleared_values().fold(0, |acc, slot_name| acc + slot_name.get_size_hint()) + + // Updated Values + self.updated_values().fold(0, |acc, (slot_name, slot_value)| { + acc + slot_name.get_size_hint() + slot_value.get_size_hint() + }) + + // Storage Map Delta + storage_map_delta_size + } +} + +impl Deserializable for AccountStorageDelta { + fn read_from(source: &mut R) -> Result { + let mut deltas = BTreeMap::new(); + + let num_cleared_values = source.read_u8()?; + for _ in 0..num_cleared_values { + let cleared_value: StorageSlotName = source.read()?; + deltas.insert(cleared_value, StorageSlotDelta::with_empty_value()); + } + + let num_updated_values = source.read_u8()?; + for _ in 0..num_updated_values { + let (updated_slot, updated_value) = source.read()?; + deltas.insert(updated_slot, StorageSlotDelta::Value(updated_value)); + } + + let num_maps = source.read_u8()? as usize; + deltas.extend( + source + .read_many::<(StorageSlotName, StorageMapDelta)>(num_maps)? + .into_iter() + .map(|(slot_name, map_delta)| (slot_name, StorageSlotDelta::Map(map_delta))), + ); + + Ok(Self::from_raw(deltas)) + } +} + +// STORAGE SLOT DELTA +// ================================================================================================ + +/// The delta of a single storage slot. +/// +/// - [`StorageSlotDelta::Value`] contains the value to which a value slot is updated. +/// - [`StorageSlotDelta::Map`] contains the [`StorageMapDelta`] which contains the key-value pairs +/// that were updated in a map slot. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StorageSlotDelta { + Value(Word), + Map(StorageMapDelta), +} + +impl StorageSlotDelta { + // CONSTANTS + // ---------------------------------------------------------------------------------------- + + /// The type byte for value slot deltas. + const VALUE: u8 = 0; + + /// The type byte for map slot deltas. + const MAP: u8 = 1; + + // CONSTRUCTORS + // ---------------------------------------------------------------------------------------- + + /// Returns a new [`StorageSlotDelta::Value`] with an empty value. + pub fn with_empty_value() -> Self { + Self::Value(Word::empty()) + } + + /// Returns a new [`StorageSlotDelta::Map`] with an empty map delta. + pub fn with_empty_map() -> Self { + Self::Map(StorageMapDelta::default()) + } + + // ACCESSORS + // ---------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotType`] of this slot delta. + pub fn slot_type(&self) -> StorageSlotType { + match self { + StorageSlotDelta::Value(_) => StorageSlotType::Value, + StorageSlotDelta::Map(_) => StorageSlotType::Map, + } + } + + /// Returns `true` if the slot delta is of type [`StorageSlotDelta::Value`], `false` otherwise. + pub fn is_value(&self) -> bool { + matches!(self, Self::Value(_)) + } + + /// Returns `true` if the slot delta is of type [`StorageSlotDelta::Map`], `false` otherwise. + pub fn is_map(&self) -> bool { + matches!(self, Self::Map(_)) + } + + // MUTATORS + // ---------------------------------------------------------------------------------------- + + /// Unwraps a value slot delta into a [`Word`]. + /// + /// # Panics + /// + /// Panics if: + /// - `self` is not of type [`StorageSlotDelta::Value`]. + pub fn unwrap_value(self) -> Word { + match self { + StorageSlotDelta::Value(value) => value, + StorageSlotDelta::Map(_) => panic!("called unwrap_value on a map slot delta"), + } + } + + /// Unwraps a map slot delta into a [`StorageMapDelta`]. + /// + /// # Panics + /// + /// Panics if: + /// - `self` is not of type [`StorageSlotDelta::Map`]. + pub fn unwrap_map(self) -> StorageMapDelta { + match self { + StorageSlotDelta::Value(_) => panic!("called unwrap_map on a value slot delta"), + StorageSlotDelta::Map(map_delta) => map_delta, + } + } + + /// Merges `other` into `self`. + /// + /// # Errors + /// + /// Returns `None` if: + /// - merging failed due to a slot type mismatch. + #[must_use] + fn merge(&mut self, other: Self) -> Option<()> { + match (self, other) { + (StorageSlotDelta::Value(current_value), StorageSlotDelta::Value(new_value)) => { + *current_value = new_value; + }, + (StorageSlotDelta::Map(current_map_delta), StorageSlotDelta::Map(new_map_delta)) => { + current_map_delta.merge(new_map_delta); + }, + (..) => { + return None; + }, + } + + Some(()) + } +} + +impl From for StorageSlotDelta { + fn from(content: StorageSlotContent) -> Self { + match content { + StorageSlotContent::Value(word) => StorageSlotDelta::Value(word), + StorageSlotContent::Map(storage_map) => { + StorageSlotDelta::Map(StorageMapDelta::from(storage_map)) + }, + } + } +} + +impl Serializable for StorageSlotDelta { + fn write_into(&self, target: &mut W) { + match self { + StorageSlotDelta::Value(value) => { + target.write_u8(Self::VALUE); + target.write(value); + }, + StorageSlotDelta::Map(storage_map_delta) => { + target.write_u8(Self::MAP); + target.write(storage_map_delta); + }, + } + } +} + +impl Deserializable for StorageSlotDelta { + fn read_from(source: &mut R) -> Result { + match source.read_u8()? { + Self::VALUE => { + let value = source.read()?; + Ok(Self::Value(value)) + }, + Self::MAP => { + let map_delta = source.read()?; + Ok(Self::Map(map_delta)) + }, + other => Err(DeserializationError::InvalidValue(format!( + "unknown storage slot delta variant {other}" + ))), + } + } +} + +// STORAGE MAP DELTA +// ================================================================================================ + +/// [StorageMapDelta] stores the differences between two states of account storage maps. +/// +/// The differences are represented as leaf updates: a map of updated item key ([Word]) to +/// value ([Word]). For cleared items the value is [EMPTY_WORD]. +/// +/// The [`LexicographicWord`] wrapper is necessary to order the keys in the same way as the +/// in-kernel account delta which uses a link map. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct StorageMapDelta(BTreeMap); + +impl StorageMapDelta { + /// Creates a new storage map delta from the provided leaves. + pub fn new(map: BTreeMap) -> Self { + Self(map) + } + + /// Returns the number of changed entries in this map delta. + pub fn num_entries(&self) -> usize { + self.0.len() + } + + /// Returns a reference to the updated entries in this storage map delta. + /// + /// Note that the returned key is the raw map key. + pub fn entries(&self) -> &BTreeMap { + &self.0 + } + + /// Inserts an item into the storage map delta. + pub fn insert(&mut self, raw_key: Word, value: Word) { + self.0.insert(LexicographicWord::new(raw_key), value); + } + + /// Returns true if storage map delta contains no updates. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Merge `other` into this delta, giving precedence to `other`. + pub fn merge(&mut self, other: Self) { + // Aggregate the changes into a map such that `other` overwrites self. + self.0.extend(other.0); + } + + /// Returns a mutable reference to the underlying map. + pub fn as_map_mut(&mut self) -> &mut BTreeMap { + &mut self.0 + } + + /// Returns an iterator of all the cleared keys in the storage map. + fn cleared_keys(&self) -> impl Iterator + '_ { + self.0.iter().filter(|&(_, value)| value.is_empty()).map(|(key, _)| key.inner()) + } + + /// Returns an iterator of all the updated entries in the storage map. + fn updated_entries(&self) -> impl Iterator + '_ { + self.0.iter().filter_map(|(key, value)| { + if !value.is_empty() { + Some((key.inner(), value)) + } else { + None + } + }) + } +} + +#[cfg(any(feature = "testing", test))] +impl StorageMapDelta { + /// Creates a new [StorageMapDelta] from the provided iterators. + pub fn from_iters( + cleared_leaves: impl IntoIterator, + updated_leaves: impl IntoIterator, + ) -> Self { + Self(BTreeMap::from_iter( + cleared_leaves + .into_iter() + .map(|key| (LexicographicWord::new(key), EMPTY_WORD)) + .chain( + updated_leaves + .into_iter() + .map(|(key, value)| (LexicographicWord::new(key), value)), + ), + )) + } + + /// Consumes self and returns the underlying map. + pub fn into_map(self) -> BTreeMap { + self.0 + } +} + +/// Converts a [StorageMap] into a [StorageMapDelta] for initial delta construction. +impl From for StorageMapDelta { + fn from(map: StorageMap) -> Self { + StorageMapDelta::new( + map.into_entries() + .into_iter() + .map(|(key, value)| (LexicographicWord::new(key), value)) + .collect(), + ) + } +} + +impl Serializable for StorageMapDelta { + fn write_into(&self, target: &mut W) { + let cleared: Vec<&Word> = self.cleared_keys().collect(); + let updated: Vec<(&Word, &Word)> = self.updated_entries().collect(); + + target.write_usize(cleared.len()); + target.write_many(cleared.iter()); + + target.write_usize(updated.len()); + target.write_many(updated.iter()); + } + + fn get_size_hint(&self) -> usize { + let word_size = EMPTY_WORD.get_size_hint(); + + let cleared_keys_count = self.cleared_keys().count(); + let updated_entries_count = self.updated_entries().count(); + + // Cleared Keys + cleared_keys_count.get_size_hint() + + cleared_keys_count * Word::SERIALIZED_SIZE + + + // Updated Entries + updated_entries_count.get_size_hint() + + updated_entries_count * (Word::SERIALIZED_SIZE + word_size) + } +} + +impl Deserializable for StorageMapDelta { + fn read_from(source: &mut R) -> Result { + let mut map = BTreeMap::new(); + + let cleared_count = source.read_usize()?; + for _ in 0..cleared_count { + let cleared_key = source.read()?; + map.insert(LexicographicWord::new(cleared_key), EMPTY_WORD); + } + + let updated_count = source.read_usize()?; + for _ in 0..updated_count { + let (updated_key, updated_value) = source.read()?; + map.insert(LexicographicWord::new(updated_key), updated_value); + } + + Ok(Self::new(map)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use anyhow::Context; + use assert_matches::assert_matches; + + use super::{AccountStorageDelta, Deserializable, Serializable}; + use crate::account::{StorageMapDelta, StorageSlotDelta, StorageSlotName}; + use crate::errors::AccountDeltaError; + use crate::{ONE, Word}; + + #[test] + fn account_storage_delta_returns_err_on_slot_type_mismatch() { + let value_slot_name = StorageSlotName::mock(1); + let map_slot_name = StorageSlotName::mock(2); + + let mut delta = AccountStorageDelta::from_iters( + [value_slot_name.clone()], + [], + [(map_slot_name.clone(), StorageMapDelta::default())], + ); + + let err = delta + .set_map_item(value_slot_name.clone(), Word::empty(), Word::empty()) + .unwrap_err(); + assert_matches!(err, AccountDeltaError::StorageSlotUsedAsDifferentTypes(slot_name) => { + assert_eq!(value_slot_name, slot_name) + }); + + let err = delta.set_item(map_slot_name.clone(), Word::empty()).unwrap_err(); + assert_matches!(err, AccountDeltaError::StorageSlotUsedAsDifferentTypes(slot_name) => { + assert_eq!(map_slot_name, slot_name) + }); + } + + #[test] + fn test_is_empty() { + let storage_delta = AccountStorageDelta::new(); + assert!(storage_delta.is_empty()); + + let storage_delta = AccountStorageDelta::from_iters([StorageSlotName::mock(1)], [], []); + assert!(!storage_delta.is_empty()); + + let storage_delta = AccountStorageDelta::from_iters( + [], + [(StorageSlotName::mock(2), Word::from([ONE, ONE, ONE, ONE]))], + [], + ); + assert!(!storage_delta.is_empty()); + + let storage_delta = AccountStorageDelta::from_iters( + [], + [], + [(StorageSlotName::mock(3), StorageMapDelta::default())], + ); + assert!(!storage_delta.is_empty()); + } + + #[test] + fn test_serde_account_storage_delta() { + let storage_delta = AccountStorageDelta::new(); + let serialized = storage_delta.to_bytes(); + let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_delta); + + let storage_delta = AccountStorageDelta::from_iters([StorageSlotName::mock(1)], [], []); + let serialized = storage_delta.to_bytes(); + let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_delta); + + let storage_delta = AccountStorageDelta::from_iters( + [], + [(StorageSlotName::mock(2), Word::from([ONE, ONE, ONE, ONE]))], + [], + ); + let serialized = storage_delta.to_bytes(); + let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_delta); + + let storage_delta = AccountStorageDelta::from_iters( + [], + [], + [(StorageSlotName::mock(3), StorageMapDelta::default())], + ); + let serialized = storage_delta.to_bytes(); + let deserialized = AccountStorageDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_delta); + } + + #[test] + fn test_serde_storage_map_delta() { + let storage_map_delta = StorageMapDelta::default(); + let serialized = storage_map_delta.to_bytes(); + let deserialized = StorageMapDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_map_delta); + + let storage_map_delta = StorageMapDelta::from_iters([Word::from([ONE, ONE, ONE, ONE])], []); + let serialized = storage_map_delta.to_bytes(); + let deserialized = StorageMapDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_map_delta); + + let storage_map_delta = + StorageMapDelta::from_iters([], [(Word::empty(), Word::from([ONE, ONE, ONE, ONE]))]); + let serialized = storage_map_delta.to_bytes(); + let deserialized = StorageMapDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, storage_map_delta); + } + + #[test] + fn test_serde_storage_slot_value_delta() { + let slot_delta = StorageSlotDelta::with_empty_value(); + let serialized = slot_delta.to_bytes(); + let deserialized = StorageSlotDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, slot_delta); + + let slot_delta = StorageSlotDelta::Value(Word::from([1, 2, 3, 4u32])); + let serialized = slot_delta.to_bytes(); + let deserialized = StorageSlotDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, slot_delta); + } + + #[test] + fn test_serde_storage_slot_map_delta() { + let slot_delta = StorageSlotDelta::with_empty_map(); + let serialized = slot_delta.to_bytes(); + let deserialized = StorageSlotDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, slot_delta); + + let map_delta = StorageMapDelta::from_iters( + [Word::from([1, 2, 3, 4u32])], + [(Word::from([5, 6, 7, 8u32]), Word::from([3, 4, 5, 6u32]))], + ); + let slot_delta = StorageSlotDelta::Map(map_delta); + let serialized = slot_delta.to_bytes(); + let deserialized = StorageSlotDelta::read_from_bytes(&serialized).unwrap(); + assert_eq!(deserialized, slot_delta); + } + + #[rstest::rstest] + #[case::some_some(Some(1), Some(2), Some(2))] + #[case::none_some(None, Some(2), Some(2))] + #[case::some_none(Some(1), None, None)] + #[test] + fn merge_items( + #[case] x: Option, + #[case] y: Option, + #[case] expected: Option, + ) -> anyhow::Result<()> { + /// Creates a delta containing the item as an update if Some, else with the item cleared. + fn create_delta(item: Option) -> AccountStorageDelta { + let slot_name = StorageSlotName::mock(123); + let item = item.map(|x| (slot_name.clone(), Word::from([x, 0, 0, 0]))); + + AccountStorageDelta::new() + .add_cleared_items(item.is_none().then_some(slot_name.clone())) + .add_updated_values(item) + } + + let mut delta_x = create_delta(x); + let delta_y = create_delta(y); + let expected = create_delta(expected); + + delta_x.merge(delta_y).context("failed to merge deltas")?; + + assert_eq!(delta_x, expected); + + Ok(()) + } + + #[rstest::rstest] + #[case::some_some(Some(1), Some(2), Some(2))] + #[case::none_some(None, Some(2), Some(2))] + #[case::some_none(Some(1), None, None)] + #[test] + fn merge_maps(#[case] x: Option, #[case] y: Option, #[case] expected: Option) { + fn create_delta(value: Option) -> StorageMapDelta { + let key = Word::from([10u32, 0, 0, 0]); + match value { + Some(value) => { + StorageMapDelta::from_iters([], [(key, Word::from([value, 0, 0, 0]))]) + }, + None => StorageMapDelta::from_iters([key], []), + } + } + + let mut delta_x = create_delta(x); + let delta_y = create_delta(y); + let expected = create_delta(expected); + + delta_x.merge(delta_y); + + assert_eq!(delta_x, expected); + } +} diff --git a/crates/miden-objects/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs similarity index 100% rename from crates/miden-objects/src/account/delta/vault.rs rename to crates/miden-protocol/src/account/delta/vault.rs diff --git a/crates/miden-objects/src/account/file.rs b/crates/miden-protocol/src/account/file.rs similarity index 97% rename from crates/miden-objects/src/account/file.rs rename to crates/miden-protocol/src/account/file.rs index f882f261e6..4b6f5d4287 100644 --- a/crates/miden-objects/src/account/file.rs +++ b/crates/miden-protocol/src/account/file.rs @@ -119,8 +119,8 @@ mod tests { let storage = AccountStorage::new(vec![]).unwrap(); let nonce = Felt::new(1); let account = Account::new_existing(id, vault, storage, code, nonce); - let auth_secret_key = AuthSecretKey::new_rpo_falcon512(); - let auth_secret_key_2 = AuthSecretKey::new_rpo_falcon512(); + let auth_secret_key = AuthSecretKey::new_falcon512_rpo(); + let auth_secret_key_2 = AuthSecretKey::new_falcon512_rpo(); AccountFile::new(account, vec![auth_secret_key, auth_secret_key_2]) } diff --git a/crates/miden-objects/src/account/header.rs b/crates/miden-protocol/src/account/header.rs similarity index 72% rename from crates/miden-objects/src/account/header.rs rename to crates/miden-protocol/src/account/header.rs index b46e6f2209..a725394a2f 100644 --- a/crates/miden-objects/src/account/header.rs +++ b/crates/miden-protocol/src/account/header.rs @@ -1,8 +1,20 @@ use alloc::vec::Vec; use super::{Account, AccountId, Felt, PartialAccount, ZERO, hash_account}; -use crate::Word; +use crate::errors::AccountError; +use crate::transaction::memory::{ + ACCT_CODE_COMMITMENT_OFFSET, + ACCT_DATA_MEM_SIZE, + ACCT_ID_AND_NONCE_OFFSET, + ACCT_ID_PREFIX_IDX, + ACCT_ID_SUFFIX_IDX, + ACCT_NONCE_IDX, + ACCT_STORAGE_COMMITMENT_OFFSET, + ACCT_VAULT_ROOT_OFFSET, + MemoryOffset, +}; use crate::utils::serde::{Deserializable, Serializable}; +use crate::{WORD_SIZE, Word, WordError}; // ACCOUNT HEADER // ================================================================================================ @@ -45,6 +57,33 @@ impl AccountHeader { } } + /// Parses the account header data returned by the VM into individual account component + /// commitments. Returns a tuple of account ID, vault root, storage commitment, code + /// commitment, and nonce. + pub(crate) fn try_from_elements(elements: &[Felt]) -> Result { + if elements.len() != ACCT_DATA_MEM_SIZE { + return Err(AccountError::HeaderDataIncorrectLength { + actual: elements.len(), + expected: ACCT_DATA_MEM_SIZE, + }); + } + + let id = AccountId::try_from([ + elements[ACCT_ID_AND_NONCE_OFFSET as usize + ACCT_ID_PREFIX_IDX], + elements[ACCT_ID_AND_NONCE_OFFSET as usize + ACCT_ID_SUFFIX_IDX], + ]) + .map_err(AccountError::FinalAccountHeaderIdParsingFailed)?; + let nonce = elements[ACCT_ID_AND_NONCE_OFFSET as usize + ACCT_NONCE_IDX]; + let vault_root = parse_word(elements, ACCT_VAULT_ROOT_OFFSET) + .expect("we should have sliced off exactly 4 bytes"); + let storage_commitment = parse_word(elements, ACCT_STORAGE_COMMITMENT_OFFSET) + .expect("we should have sliced off exactly 4 bytes"); + let code_commitment = parse_word(elements, ACCT_CODE_COMMITMENT_OFFSET) + .expect("we should have sliced off exactly 4 bytes"); + + Ok(AccountHeader::new(id, nonce, vault_root, storage_commitment, code_commitment)) + } + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -141,7 +180,7 @@ impl From<&Account> for AccountHeader { id: account.id(), nonce: account.nonce(), vault_root: account.vault().root(), - storage_commitment: account.storage().commitment(), + storage_commitment: account.storage().to_commitment(), code_commitment: account.code().commitment(), } } @@ -177,6 +216,14 @@ impl Deserializable for AccountHeader { } } +// HELPER FUNCTIONS +// ================================================================================================ + +/// Creates a new `Word` instance from the slice of `Felt`s using provided offset. +fn parse_word(data: &[Felt], offset: MemoryOffset) -> Result { + Word::try_from(&data[offset as usize..offset as usize + WORD_SIZE]) +} + // TESTS // ================================================================================================ @@ -187,7 +234,7 @@ mod tests { use super::AccountHeader; use crate::Word; - use crate::account::StorageSlot; + use crate::account::StorageSlotContent; use crate::account::tests::build_account; use crate::asset::FungibleAsset; @@ -196,7 +243,7 @@ mod tests { let init_nonce = Felt::new(1); let asset_0 = FungibleAsset::mock(99); let word = Word::from([1, 2, 3, 4u32]); - let storage_slot = StorageSlot::Value(word); + let storage_slot = StorageSlotContent::Value(word); let account = build_account(vec![asset_0], init_nonce, vec![storage_slot]); let account_header: AccountHeader = account.into(); diff --git a/crates/miden-objects/src/account/mod.rs b/crates/miden-protocol/src/account/mod.rs similarity index 83% rename from crates/miden-objects/src/account/mod.rs rename to crates/miden-protocol/src/account/mod.rs index 0918e43ce8..c521c5e46f 100644 --- a/crates/miden-objects/src/account/mod.rs +++ b/crates/miden-protocol/src/account/mod.rs @@ -1,9 +1,8 @@ -use alloc::collections::BTreeMap; use alloc::string::ToString; - -use miden_core::LexicographicWord; +use alloc::vec::Vec; use crate::asset::{Asset, AssetVault}; +use crate::errors::AccountError; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -11,7 +10,7 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{AccountError, Felt, Hasher, Word, ZERO}; +use crate::{Felt, Hasher, Word, ZERO}; mod account_id; pub use account_id::{ @@ -31,25 +30,10 @@ pub use builder::AccountBuilder; pub mod code; pub use code::AccountCode; -pub use code::procedure::AccountProcedureInfo; +pub use code::procedure::AccountProcedureRoot; pub mod component; -pub use component::{ - AccountComponent, - AccountComponentMetadata, - AccountComponentTemplate, - FeltRepresentation, - InitStorageData, - MapEntry, - MapRepresentation, - PlaceholderTypeRequirement, - StorageEntry, - StorageValueName, - StorageValueNameError, - TemplateType, - TemplateTypeError, - WordRepresentation, -}; +pub use component::{AccountComponent, AccountComponentCode, AccountComponentMetadata}; pub mod delta; pub use delta::{ @@ -60,18 +44,22 @@ pub use delta::{ NonFungibleAssetDelta, NonFungibleDeltaAction, StorageMapDelta, + StorageSlotDelta, }; -mod storage; +pub mod storage; pub use storage::{ AccountStorage, AccountStorageHeader, PartialStorage, PartialStorageMap, - SlotName, StorageMap, StorageMapWitness, StorageSlot, + StorageSlotContent, + StorageSlotHeader, + StorageSlotId, + StorageSlotName, StorageSlotType, }; @@ -136,7 +124,7 @@ impl Account { nonce: Felt, seed: Option, ) -> Result { - validate_account_seed(id, code.commitment(), storage.commitment(), seed, nonce)?; + validate_account_seed(id, code.commitment(), storage.to_commitment(), seed, nonce)?; Ok(Self::new_unchecked(id, vault, storage, code, nonce, seed)) } @@ -161,31 +149,23 @@ impl Account { /// Creates an account's [`AccountCode`] and [`AccountStorage`] from the provided components. /// /// This merges all libraries of the components into a single - /// [`MastForest`](miden_processor::MastForest) to produce the [`AccountCode`]. For each - /// procedure in the resulting forest, the storage offset and size are set so that the - /// procedure can only access the storage slots of the component in which it was defined and - /// each component's storage offset is the total number of slots in the previous components. - /// To illustrate, given two components with one and two storage slots respectively: - /// - /// - RpoFalcon512 Component: Component slot 0 stores the public key. - /// - Custom Component: Component slot 0 stores a custom [`StorageSlot::Value`] and component - /// slot 1 stores a custom [`StorageSlot::Map`]. + /// [`MastForest`](miden_processor::MastForest) to produce the [`AccountCode`]. /// - /// When combined, their assigned slots in the [`AccountStorage`] would be: - /// - /// - The RpoFalcon512 Component has offset 0 and size 1: Account slot 0 stores the public key. - /// - The Custom Component has offset 1 and size 2: Account slot 1 stores the value and account - /// slot 2 stores the map. + /// The storage slots of all components are merged into a single [`AccountStorage`], where the + /// slots are sorted by their [`StorageSlotName`]. /// /// The resulting commitments from code and storage can then be used to construct an /// [`AccountId`]. Finally, a new account can then be instantiated from those parts using /// [`Account::new`]. /// - /// If the account type is faucet the reserved slot (slot 0) will be initialized. - /// - For Fungible Faucets the value is [`StorageSlot::empty_value`]. - /// - For Non-Fungible Faucets the value is [`StorageSlot::empty_map`]. + /// If the account type is faucet the reserved slot ([`AccountStorage::faucet_metadata_slot`]) + /// will be initialized as follows: + /// - For [`AccountType::FungibleFaucet`] the value is set to + /// [`StorageSlotContent::empty_value`]. + /// - For [`AccountType::NonFungibleFaucet`] the value is set to + /// [`StorageSlotContent::empty_map`]. /// - /// If the storage needs to be initialized with certain values in that slot, those can be added + /// If the storage needs to be initialized with certain values in that slot, those must be added /// after construction with the standard set methods for items and maps. /// /// # Errors @@ -201,11 +181,11 @@ impl Account { /// - [`MastForest::merge`](miden_processor::MastForest::merge) fails on all libraries. pub(super) fn initialize_from_components( account_type: AccountType, - components: &[AccountComponent], + components: Vec, ) -> Result<(AccountCode, AccountStorage), AccountError> { - validate_components_support_account_type(components, account_type)?; + validate_components_support_account_type(&components, account_type)?; - let code = AccountCode::from_components_unchecked(components, account_type)?; + let code = AccountCode::from_components_unchecked(&components)?; let storage = AccountStorage::from_components(components, account_type)?; Ok((code, storage)) @@ -232,7 +212,7 @@ impl Account { self.id, self.nonce, self.vault.root(), - self.storage.commitment(), + self.storage.to_commitment(), self.code.commitment(), ) } @@ -436,28 +416,13 @@ impl TryFrom for AccountDelta { return Err(AccountError::DeltaFromAccountWithSeed); } - let mut value_slots = BTreeMap::new(); - let mut map_slots = BTreeMap::new(); - - for (slot_idx, slot) in (0..u8::MAX).zip(storage.into_slots().into_iter()) { - match slot { - StorageSlot::Value(word) => { - value_slots.insert(slot_idx, word); - }, - StorageSlot::Map(storage_map) => { - let map_delta = StorageMapDelta::new( - storage_map - .into_entries() - .into_iter() - .map(|(key, value)| (LexicographicWord::from(key), value)) - .collect(), - ); - map_slots.insert(slot_idx, map_delta); - }, - } - } - let storage_delta = AccountStorageDelta::from_parts(value_slots, map_slots) - .expect("value and map slots from account storage should not overlap"); + let slot_deltas = storage + .into_slots() + .into_iter() + .map(StorageSlot::into_parts) + .map(|(slot_name, slot_content)| (slot_name, StorageSlotDelta::from(slot_content))) + .collect(); + let storage_delta = AccountStorageDelta::from_raw(slot_deltas); let mut fungible_delta = FungibleAssetDelta::default(); let mut non_fungible_delta = NonFungibleAssetDelta::default(); @@ -628,7 +593,6 @@ mod tests { AccountStorageDelta, AccountVaultDelta, }; - use crate::AccountError; use crate::account::AccountStorageMode::Network; use crate::account::{ Account, @@ -640,22 +604,24 @@ mod tests { StorageMap, StorageMapDelta, StorageSlot, + StorageSlotContent, + StorageSlotName, }; use crate::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; + use crate::errors::AccountError; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, }; use crate::testing::add_component::AddComponent; use crate::testing::noop_auth_component::NoopAuthComponent; - use crate::testing::storage::AccountStorageDeltaBuilder; #[test] fn test_serde_account() { let init_nonce = Felt::new(1); let asset_0 = FungibleAsset::mock(99); let word = Word::from([1, 2, 3, 4u32]); - let storage_slot = StorageSlot::Value(word); + let storage_slot = StorageSlotContent::Value(word); let account = build_account(vec![asset_0], init_nonce, vec![storage_slot]); let serialized = account.to_bytes(); @@ -669,11 +635,9 @@ mod tests { let nonce_delta = Felt::new(2); let asset_0 = FungibleAsset::mock(15); let asset_1 = NonFungibleAsset::mock(&[5, 5, 5]); - let storage_delta = AccountStorageDeltaBuilder::new() - .add_cleared_items([0]) - .add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))]) - .build() - .unwrap(); + let storage_delta = AccountStorageDelta::new() + .add_cleared_items([StorageSlotName::mock(0)]) + .add_updated_values([(StorageSlotName::mock(1), Word::from([1, 2, 3, 4u32]))]); let account_delta = build_account_delta( account_id, vec![asset_1], @@ -696,8 +660,8 @@ mod tests { let asset_1 = NonFungibleAsset::mock(&[1, 2, 3]); // build storage slots - let storage_slot_value_0 = StorageSlot::Value(Word::from([1, 2, 3, 4u32])); - let storage_slot_value_1 = StorageSlot::Value(Word::from([5, 6, 7, 8u32])); + let storage_slot_value_0 = StorageSlotContent::Value(Word::from([1, 2, 3, 4u32])); + let storage_slot_value_1 = StorageSlotContent::Value(Word::from([5, 6, 7, 8u32])); let mut storage_map = StorageMap::with_entries([ ( Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), @@ -714,7 +678,7 @@ mod tests { ), ]) .unwrap(); - let storage_slot_map = StorageSlot::Map(storage_map.clone()); + let storage_slot_map = StorageSlotContent::Map(storage_map.clone()); let mut account = build_account( vec![asset_0], @@ -734,12 +698,10 @@ mod tests { // build account delta let final_nonce = Felt::new(2); - let storage_delta = AccountStorageDeltaBuilder::new() - .add_cleared_items([0]) - .add_updated_values([(1, Word::from([1, 2, 3, 4u32]))]) - .add_updated_maps([(2, updated_map)]) - .build() - .unwrap(); + let storage_delta = AccountStorageDelta::new() + .add_cleared_items([StorageSlotName::mock(0)]) + .add_updated_values([(StorageSlotName::mock(1), Word::from([1, 2, 3, 4u32]))]) + .add_updated_maps([(StorageSlotName::mock(2), updated_map)]); let account_delta = build_account_delta( account_id, vec![asset_1], @@ -755,9 +717,9 @@ mod tests { vec![asset_1], final_nonce, vec![ - StorageSlot::Value(Word::empty()), - StorageSlot::Value(Word::from([1, 2, 3, 4u32])), - StorageSlot::Map(storage_map), + StorageSlotContent::Value(Word::empty()), + StorageSlotContent::Value(Word::from([1, 2, 3, 4u32])), + StorageSlotContent::Map(storage_map), ], ); @@ -773,14 +735,12 @@ mod tests { let init_nonce = Felt::new(1); let asset = FungibleAsset::mock(110); let mut account = - build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::empty())]); + build_account(vec![asset], init_nonce, vec![StorageSlotContent::Value(Word::empty())]); // build account delta - let storage_delta = AccountStorageDeltaBuilder::new() - .add_cleared_items([0]) - .add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))]) - .build() - .unwrap(); + let storage_delta = AccountStorageDelta::new() + .add_cleared_items([StorageSlotName::mock(0)]) + .add_updated_values([(StorageSlotName::mock(1), Word::from([1, 2, 3, 4u32]))]); let account_delta = build_account_delta(account_id, vec![], vec![asset], init_nonce, storage_delta); @@ -796,15 +756,13 @@ mod tests { let init_nonce = Felt::new(2); let asset = FungibleAsset::mock(100); let mut account = - build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::empty())]); + build_account(vec![asset], init_nonce, vec![StorageSlotContent::Value(Word::empty())]); // build account delta let final_nonce = Felt::new(1); - let storage_delta = AccountStorageDeltaBuilder::new() - .add_cleared_items([0]) - .add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))]) - .build() - .unwrap(); + let storage_delta = AccountStorageDelta::new() + .add_cleared_items([StorageSlotName::mock(0)]) + .add_updated_values([(StorageSlotName::mock(1), Word::from([1, 2, 3, 4u32]))]); let account_delta = build_account_delta(account_id, vec![], vec![asset], final_nonce, storage_delta); @@ -818,7 +776,7 @@ mod tests { let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap(); let init_nonce = Felt::new(1); let word = Word::from([1, 2, 3, 4u32]); - let storage_slot = StorageSlot::Value(word); + let storage_slot = StorageSlotContent::Value(word); let mut account = build_account(vec![], init_nonce, vec![storage_slot]); // build account delta @@ -846,12 +804,22 @@ mod tests { AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta).unwrap() } - pub fn build_account(assets: Vec, nonce: Felt, slots: Vec) -> Account { + pub fn build_account( + assets: Vec, + nonce: Felt, + slots: Vec, + ) -> Account { let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); let code = AccountCode::mock(); let vault = AssetVault::new(&assets).unwrap(); + let slots = slots + .into_iter() + .enumerate() + .map(|(idx, slot)| StorageSlot::new(StorageSlotName::mock(idx), slot)) + .collect(); + let storage = AccountStorage::new(slots).unwrap(); Account::new_existing(id, vault, storage, code, nonce) @@ -861,7 +829,7 @@ mod tests { /// account type returns an error. #[test] fn test_account_unsupported_component_type() { - let code1 = "export.foo add end"; + let code1 = "pub proc foo add end"; let library1 = Assembler::default().assemble_library([code1]).unwrap(); // This component support all account types except the regular account with updatable code. @@ -873,7 +841,7 @@ mod tests { let err = Account::initialize_from_components( AccountType::RegularAccountUpdatableCode, - &[component1], + vec![component1], ) .unwrap_err(); @@ -886,28 +854,6 @@ mod tests { )) } - /// Two components who export a procedure with the same MAST root should fail to convert into - /// code and storage. - #[test] - fn test_account_duplicate_exported_mast_root() { - let code1 = "export.foo add eq.1 end"; - let code2 = "export.bar add eq.1 end"; - - let library1 = Assembler::default().assemble_library([code1]).unwrap(); - let library2 = Assembler::default().assemble_library([code2]).unwrap(); - - let component1 = AccountComponent::new(library1, vec![]).unwrap().with_supports_all_types(); - let component2 = AccountComponent::new(library2, vec![]).unwrap().with_supports_all_types(); - - let err = Account::initialize_from_components( - AccountType::RegularAccountUpdatableCode, - &[NoopAuthComponent.into(), component1, component2], - ) - .unwrap_err(); - - assert_matches!(err, AccountError::AccountComponentDuplicateProcedureRoot(_)) - } - /// Tests all cases of account ID seed validation. #[test] fn seed_validation() -> anyhow::Result<()> { @@ -924,7 +870,7 @@ mod tests { Network, AccountIdVersion::Version0, code.commitment(), - storage.commitment(), + storage.to_commitment(), )?; // Set nonce to 1 so the account is considered existing and provide the seed. diff --git a/crates/miden-objects/src/account/partial.rs b/crates/miden-protocol/src/account/partial.rs similarity index 99% rename from crates/miden-objects/src/account/partial.rs rename to crates/miden-protocol/src/account/partial.rs index fd1161f07e..f7ae354dae 100644 --- a/crates/miden-objects/src/account/partial.rs +++ b/crates/miden-protocol/src/account/partial.rs @@ -4,10 +4,11 @@ use miden_core::utils::{Deserializable, Serializable}; use miden_core::{Felt, ZERO}; use super::{Account, AccountCode, AccountId, PartialStorage}; +use crate::Word; use crate::account::{hash_account, validate_account_seed}; use crate::asset::PartialVault; +use crate::errors::AccountError; use crate::utils::serde::DeserializationError; -use crate::{AccountError, Word}; /// A partial representation of an account. /// diff --git a/crates/miden-protocol/src/account/storage/header.rs b/crates/miden-protocol/src/account/storage/header.rs new file mode 100644 index 0000000000..359e6ff49e --- /dev/null +++ b/crates/miden-protocol/src/account/storage/header.rs @@ -0,0 +1,513 @@ +use alloc::collections::BTreeMap; +use alloc::format; +use alloc::string::ToString; +use alloc::vec::Vec; + +use super::map::EMPTY_STORAGE_MAP_ROOT; +use super::{AccountStorage, Felt, StorageSlotType, Word}; +use crate::account::{StorageSlot, StorageSlotId, StorageSlotName}; +use crate::crypto::SequentialCommit; +use crate::errors::AccountError; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; +use crate::{FieldElement, ZERO}; + +// ACCOUNT STORAGE HEADER +// ================================================================================================ + +/// The header of an [`AccountStorage`], storing only the slot name, slot type and value of each +/// storage slot. +/// +/// The stored value differs based on the slot type: +/// - [`StorageSlotType::Value`]: The value of the slot itself. +/// - [`StorageSlotType::Map`]: The root of the SMT that represents the storage map. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountStorageHeader { + slots: Vec, +} + +impl AccountStorageHeader { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + + /// Returns a new instance of account storage header initialized with the provided slots. + /// + /// # Errors + /// + /// Returns an error if: + /// - The number of provided slots is greater than [`AccountStorage::MAX_NUM_STORAGE_SLOTS`]. + /// - The slots are not sorted by [`StorageSlotId`]. + /// - There are multiple storage slots with the same [`StorageSlotName`]. + pub fn new(slots: Vec) -> Result { + if slots.len() > AccountStorage::MAX_NUM_STORAGE_SLOTS { + return Err(AccountError::StorageTooManySlots(slots.len() as u64)); + } + + if !slots.is_sorted_by_key(|slot| slot.id()) { + return Err(AccountError::UnsortedStorageSlots); + } + + // Check for slot name uniqueness by checking each neighboring slot's IDs. This is + // sufficient because the slots are sorted. + for slots in slots.windows(2) { + if slots[0].id() == slots[1].id() { + return Err(AccountError::DuplicateStorageSlotName(slots[0].name().clone())); + } + } + + Ok(Self { slots }) + } + + /// Returns a new instance of account storage header initialized with the provided slot tuples. + /// + /// This is a convenience method that converts tuples to [`StorageSlotHeader`]s. + /// + /// # Errors + /// + /// Returns an error if: + /// - The number of provided slots is greater than [`AccountStorage::MAX_NUM_STORAGE_SLOTS`]. + /// - The slots are not sorted by [`StorageSlotId`]. + #[cfg(any(feature = "testing", test))] + pub fn from_tuples( + slots: Vec<(StorageSlotName, StorageSlotType, Word)>, + ) -> Result { + let slots = slots + .into_iter() + .map(|(name, slot_type, value)| StorageSlotHeader::new(name, slot_type, value)) + .collect(); + + Self::new(slots) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns an iterator over the storage header slots. + pub fn slots(&self) -> impl Iterator { + self.slots.iter() + } + + /// Returns an iterator over the storage header map slots. + pub fn map_slot_roots(&self) -> impl Iterator + '_ { + self.slots.iter().filter_map(|slot| match slot.slot_type() { + StorageSlotType::Value => None, + StorageSlotType::Map => Some(slot.value()), + }) + } + + /// Returns the number of slots contained in the storage header. + pub fn num_slots(&self) -> u8 { + // SAFETY: The constructors of this type ensure this value fits in a u8. + self.slots.len() as u8 + } + + /// Returns the storage slot header for the slot with the given name. + /// + /// Returns `None` if a slot with the provided name does not exist. + pub fn find_slot_header_by_name( + &self, + slot_name: &StorageSlotName, + ) -> Option<&StorageSlotHeader> { + self.find_slot_header_by_id(slot_name.id()) + } + + /// Returns the storage slot header for the slot with the given ID. + /// + /// Returns `None` if a slot with the provided slot ID does not exist. + pub fn find_slot_header_by_id(&self, slot_id: StorageSlotId) -> Option<&StorageSlotHeader> { + self.slots.iter().find(|slot| slot.id() == slot_id) + } + + /// Indicates whether the slot with the given `name` is a map slot. + /// + /// # Errors + /// + /// Returns an error if: + /// - a slot with the provided name does not exist. + pub fn is_map_slot(&self, name: &StorageSlotName) -> Result { + match self + .find_slot_header_by_name(name) + .ok_or(AccountError::StorageSlotNameNotFound { slot_name: name.clone() })? + .slot_type() + { + StorageSlotType::Map => Ok(true), + StorageSlotType::Value => Ok(false), + } + } + + /// Converts storage slots of this account storage header into a vector of field elements. + /// + /// This is done by first converting each storage slot into exactly 8 elements as follows: + /// + /// ```text + /// [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE] + /// ``` + /// + /// And then concatenating the resulting elements into a single vector. + pub fn to_elements(&self) -> Vec { + ::to_elements(self) + } + + /// Reconstructs an [`AccountStorageHeader`] from field elements with provided slot names. + /// + /// The elements are expected to be groups of 8 elements per slot: + /// `[[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]` + pub fn try_from_elements( + elements: &[Felt], + slot_names: &BTreeMap, + ) -> Result { + if !elements.len().is_multiple_of(StorageSlot::NUM_ELEMENTS) { + return Err(AccountError::other( + "storage header elements length must be divisible by 8", + )); + } + + let mut slots = Vec::new(); + for chunk in elements.chunks_exact(StorageSlot::NUM_ELEMENTS) { + // Parse slot type from second element. + let slot_type_felt = chunk[1]; + let slot_type = slot_type_felt.try_into()?; + + // Parse slot ID from third and fourth elements. + let slot_id_suffix = chunk[2]; + let slot_id_prefix = chunk[3]; + let parsed_slot_id = StorageSlotId::new(slot_id_suffix, slot_id_prefix); + + // Retrieve slot name from the map. + let slot_name = slot_names.get(&parsed_slot_id).cloned().ok_or(AccountError::other( + format!("slot name not found for slot ID {}", parsed_slot_id), + ))?; + + // Parse slot value from last 4 elements. + let slot_value = Word::new([chunk[4], chunk[5], chunk[6], chunk[7]]); + + let slot_header = StorageSlotHeader::new(slot_name, slot_type, slot_value); + slots.push(slot_header); + } + + // Sort slots by ID. + slots.sort_by_key(|slot| slot.id()); + + Self::new(slots) + } + + /// Returns the commitment to the [`AccountStorage`] this header represents. + pub fn to_commitment(&self) -> Word { + ::to_commitment(self) + } +} + +impl From<&AccountStorage> for AccountStorageHeader { + fn from(value: &AccountStorage) -> Self { + value.to_header() + } +} + +// SEQUENTIAL COMMIT +// ================================================================================================ + +impl SequentialCommit for AccountStorageHeader { + type Commitment = Word; + + fn to_elements(&self) -> Vec { + self.slots().flat_map(|slot| slot.to_elements()).collect() + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AccountStorageHeader { + fn write_into(&self, target: &mut W) { + let len = self.slots.len() as u8; + target.write_u8(len); + target.write_many(self.slots()) + } +} + +impl Deserializable for AccountStorageHeader { + fn read_from(source: &mut R) -> Result { + let len = source.read_u8()?; + let slots: Vec = source.read_many(len as usize)?; + Self::new(slots).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// STORAGE SLOT HEADER +// ================================================================================================ + +/// The header of a [`StorageSlot`], storing only the slot name (or ID), slot type and value of the +/// slot. +/// +/// The stored value differs based on the slot type: +/// - [`StorageSlotType::Value`]: The value of the slot itself. +/// - [`StorageSlotType::Map`]: The root of the SMT that represents the storage map. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StorageSlotHeader { + name: StorageSlotName, + r#type: StorageSlotType, + value: Word, +} + +impl StorageSlotHeader { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Returns a new instance of storage slot header. + pub fn new(name: StorageSlotName, r#type: StorageSlotType, value: Word) -> Self { + Self { name, r#type, value } + } + + /// Returns a new instance of storage slot header with an empty value slot. + pub fn with_empty_value(name: StorageSlotName) -> StorageSlotHeader { + StorageSlotHeader::new(name, StorageSlotType::Value, Word::default()) + } + + /// Returns a new instance of storage slot header with an empty map slot. + pub fn with_empty_map(name: StorageSlotName) -> StorageSlotHeader { + StorageSlotHeader::new(name, StorageSlotType::Map, EMPTY_STORAGE_MAP_ROOT) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the slot name. + pub fn name(&self) -> &StorageSlotName { + &self.name + } + + /// Returns the slot ID. + pub fn id(&self) -> StorageSlotId { + self.name.id() + } + + /// Returns the slot type. + pub fn slot_type(&self) -> StorageSlotType { + self.r#type + } + + /// Returns the slot value. + pub fn value(&self) -> Word { + self.value + } + + /// Returns this storage slot header as field elements. + /// + /// This is done by converting this storage slot into 8 field elements as follows: + /// ```text + /// [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE] + /// ``` + pub(crate) fn to_elements(&self) -> [Felt; StorageSlot::NUM_ELEMENTS] { + let id = self.id(); + let mut elements = [ZERO; StorageSlot::NUM_ELEMENTS]; + elements[0..4].copy_from_slice(&[ + Felt::ZERO, + self.r#type.as_felt(), + id.suffix(), + id.prefix(), + ]); + elements[4..8].copy_from_slice(self.value.as_elements()); + elements + } +} + +impl From<&StorageSlot> for StorageSlotHeader { + fn from(slot: &StorageSlot) -> Self { + StorageSlotHeader::new(slot.name().clone(), slot.slot_type(), slot.value()) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for StorageSlotHeader { + fn write_into(&self, target: &mut W) { + self.name.write_into(target); + self.r#type.write_into(target); + self.value.write_into(target); + } +} + +impl Deserializable for StorageSlotHeader { + fn read_from(source: &mut R) -> Result { + let name = StorageSlotName::read_from(source)?; + let slot_type = StorageSlotType::read_from(source)?; + let value = Word::read_from(source)?; + Ok(Self::new(name, slot_type, value)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + use alloc::string::ToString; + + use miden_core::Felt; + use miden_core::utils::{Deserializable, Serializable}; + + use super::AccountStorageHeader; + use crate::Word; + use crate::account::{AccountStorage, StorageSlotHeader, StorageSlotName, StorageSlotType}; + use crate::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1}; + + #[test] + fn test_from_account_storage() { + let storage_map = AccountStorage::mock_map(); + + // create new storage header from AccountStorage + let mut slots = vec![ + (MOCK_VALUE_SLOT0.clone(), StorageSlotType::Value, Word::from([1, 2, 3, 4u32])), + ( + MOCK_VALUE_SLOT1.clone(), + StorageSlotType::Value, + Word::from([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]), + ), + (MOCK_MAP_SLOT.clone(), StorageSlotType::Map, storage_map.root()), + ]; + slots.sort_unstable_by_key(|(slot_name, ..)| slot_name.id()); + + let expected_header = AccountStorageHeader::from_tuples(slots).unwrap(); + let account_storage = AccountStorage::mock(); + + assert_eq!(expected_header, AccountStorageHeader::from(&account_storage)) + } + + #[test] + fn test_serde_account_storage_header() { + // create new storage header + let storage = AccountStorage::mock(); + let storage_header = AccountStorageHeader::from(&storage); + + // serde storage header + let bytes = storage_header.to_bytes(); + let deserialized = AccountStorageHeader::read_from_bytes(&bytes).unwrap(); + + // assert deserialized == storage header + assert_eq!(storage_header, deserialized); + } + + #[test] + fn test_to_elements_from_elements_empty() { + // Construct empty header. + let empty_header = AccountStorageHeader::new(vec![]).unwrap(); + let empty_elements = empty_header.to_elements(); + + // Call from_elements. + let empty_slot_names = BTreeMap::new(); + let reconstructed_empty = + AccountStorageHeader::try_from_elements(&empty_elements, &empty_slot_names).unwrap(); + assert_eq!(empty_header, reconstructed_empty); + } + + #[test] + fn test_to_elements_from_elements_single_slot() { + // Construct single slot header. + let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap(); + let slot1 = StorageSlotHeader::new( + slot_name1, + StorageSlotType::Value, + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + ); + + let single_slot_header = AccountStorageHeader::new(vec![slot1.clone()]).unwrap(); + let single_elements = single_slot_header.to_elements(); + + // Call from_elements. + let slot_names = BTreeMap::from([(slot1.id(), slot1.name().clone())]); + let reconstructed_single = + AccountStorageHeader::try_from_elements(&single_elements, &slot_names).unwrap(); + + assert_eq!(single_slot_header, reconstructed_single); + } + + #[test] + fn test_to_elements_from_elements_multiple_slot() { + // Construct multi slot header. + let slot_name2 = StorageSlotName::new("test::map::slot2".to_string()).unwrap(); + let slot_name3 = StorageSlotName::new("test::value::slot3".to_string()).unwrap(); + + let slot2 = StorageSlotHeader::new( + slot_name2, + StorageSlotType::Map, + Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]), + ); + let slot3 = StorageSlotHeader::new( + slot_name3, + StorageSlotType::Value, + Word::new([Felt::new(9), Felt::new(10), Felt::new(11), Felt::new(12)]), + ); + + let mut slots = vec![slot2, slot3]; + slots.sort_by_key(|slot| slot.id()); + let multi_slot_header = AccountStorageHeader::new(slots.clone()).unwrap(); + let multi_elements = multi_slot_header.to_elements(); + + // Call from_elements. + let slot_names = BTreeMap::from([ + (slots[0].id(), slots[0].name.clone()), + (slots[1].id(), slots[1].name.clone()), + ]); + let reconstructed_multi = + AccountStorageHeader::try_from_elements(&multi_elements, &slot_names).unwrap(); + + assert_eq!(multi_slot_header, reconstructed_multi); + } + + #[test] + fn test_from_elements_errors() { + // Test with invalid length (not divisible by 8). + let invalid_elements = vec![Felt::new(1), Felt::new(2), Felt::new(3)]; + let empty_slot_names = BTreeMap::new(); + assert!( + AccountStorageHeader::try_from_elements(&invalid_elements, &empty_slot_names).is_err() + ); + + // Test with invalid slot type. + let mut invalid_type_elements = vec![crate::ZERO; 8]; + invalid_type_elements[1] = Felt::new(5); // Invalid slot type. + assert!( + AccountStorageHeader::try_from_elements(&invalid_type_elements, &empty_slot_names) + .is_err() + ); + } + + #[test] + fn test_from_elements_with_slot_names() { + use alloc::collections::BTreeMap; + + // Create original slot with known name. + let slot_name1 = StorageSlotName::new("test::value::slot1".to_string()).unwrap(); + let slot1 = StorageSlotHeader::new( + slot_name1.clone(), + StorageSlotType::Value, + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + ); + + // Serialize the single slot to elements + let elements = slot1.to_elements(); + + // Create slot names map using the slot's ID + let mut slot_names = BTreeMap::new(); + slot_names.insert(slot1.id(), slot_name1.clone()); + + // Test from_elements with provided slot names on raw slot elements. + let reconstructed_header = + AccountStorageHeader::try_from_elements(&elements, &slot_names).unwrap(); + + // Verify that the original slot names are preserved. + assert_eq!(reconstructed_header.slots().count(), 1); + let reconstructed_slot = reconstructed_header.slots().next().unwrap(); + + assert_eq!(slot_name1.as_str(), reconstructed_slot.name().as_str()); + assert_eq!(slot1.slot_type(), reconstructed_slot.slot_type()); + assert_eq!(slot1.value(), reconstructed_slot.value()); + } +} diff --git a/crates/miden-objects/src/account/storage/map/mod.rs b/crates/miden-protocol/src/account/storage/map/mod.rs similarity index 95% rename from crates/miden-objects/src/account/storage/map/mod.rs rename to crates/miden-protocol/src/account/storage/map/mod.rs index 4b262f88ed..46d489a91c 100644 --- a/crates/miden-objects/src/account/storage/map/mod.rs +++ b/crates/miden-protocol/src/account/storage/map/mod.rs @@ -4,10 +4,11 @@ use miden_core::EMPTY_WORD; use miden_crypto::merkle::EmptySubtreeRoots; use super::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, Word}; +use crate::Hasher; use crate::account::StorageMapDelta; -use crate::crypto::merkle::{InnerNodeInfo, LeafIndex, SMT_DEPTH, Smt, SmtLeaf}; -use crate::errors::StorageMapError; -use crate::{AccountError, Felt, Hasher}; +use crate::crypto::merkle::InnerNodeInfo; +use crate::crypto::merkle::smt::{LeafIndex, SMT_DEPTH, Smt, SmtLeaf}; +use crate::errors::{AccountError, StorageMapError}; mod partial; pub use partial::PartialStorageMap; @@ -203,16 +204,22 @@ impl StorageMap { self.entries } + // UTILITY FUNCTIONS + // -------------------------------------------------------------------------------------------- + /// Hashes the given key to get the key of the SMT. pub fn hash_key(raw_key: Word) -> Word { Hasher::hash_elements(raw_key.as_elements()) } - // TODO: Replace with https://github.com/0xMiden/crypto/issues/515 once implemented. + /// Returns leaf index of a raw map key. + pub fn map_key_to_leaf_index(raw_key: Word) -> LeafIndex { + Self::hash_key(raw_key).into() + } + /// Returns the leaf index of a map key. - pub fn hashed_map_key_to_leaf_index(hashed_map_key: Word) -> Felt { - // The third element in an SMT key is the index. - hashed_map_key[3] + pub fn hashed_map_key_to_leaf_index(hashed_map_key: Word) -> LeafIndex { + hashed_map_key.into() } } diff --git a/crates/miden-objects/src/account/storage/map/partial.rs b/crates/miden-protocol/src/account/storage/map/partial.rs similarity index 98% rename from crates/miden-objects/src/account/storage/map/partial.rs rename to crates/miden-protocol/src/account/storage/map/partial.rs index 92fd97e868..55f1f408f7 100644 --- a/crates/miden-objects/src/account/storage/map/partial.rs +++ b/crates/miden-protocol/src/account/storage/map/partial.rs @@ -2,15 +2,8 @@ use alloc::collections::BTreeMap; use miden_core::utils::{Deserializable, Serializable}; use miden_crypto::Word; -use miden_crypto::merkle::{ - InnerNodeInfo, - LeafIndex, - MerkleError, - PartialSmt, - SMT_DEPTH, - SmtLeaf, - SmtProof, -}; +use miden_crypto::merkle::smt::{LeafIndex, PartialSmt, SMT_DEPTH, SmtLeaf, SmtProof}; +use miden_crypto::merkle::{InnerNodeInfo, MerkleError}; use crate::account::{StorageMap, StorageMapWitness}; use crate::utils::serde::{ByteReader, DeserializationError}; diff --git a/crates/miden-objects/src/account/storage/map/witness.rs b/crates/miden-protocol/src/account/storage/map/witness.rs similarity index 93% rename from crates/miden-objects/src/account/storage/map/witness.rs rename to crates/miden-protocol/src/account/storage/map/witness.rs index 401d8345a4..f70a8359af 100644 --- a/crates/miden-objects/src/account/storage/map/witness.rs +++ b/crates/miden-protocol/src/account/storage/map/witness.rs @@ -1,6 +1,7 @@ use alloc::collections::BTreeMap; -use miden_crypto::merkle::{InnerNodeInfo, SmtProof}; +use miden_crypto::merkle::InnerNodeInfo; +use miden_crypto::merkle::smt::SmtProof; use crate::Word; use crate::account::StorageMap; @@ -15,9 +16,9 @@ use crate::errors::StorageMapError; /// This type guarantees that the raw key-value pairs it contains are all present in the /// contained SMT proof. Note that the inverse is not necessarily true. The proof may contain more /// entries than the map because to prove inclusion of a given raw key A an -/// [`SmtLeaf::Multiple`](miden_crypto::merkle::SmtLeaf::Multiple) may be present that contains both -/// keys hash(A) and hash(B). However, B may not be present in the key-value pairs and this is a -/// valid state. +/// [`SmtLeaf::Multiple`](miden_crypto::merkle::smt::SmtLeaf::Multiple) may be present that contains +/// both keys hash(A) and hash(B). However, B may not be present in the key-value pairs and this is +/// a valid state. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StorageMapWitness { proof: SmtProof, diff --git a/crates/miden-protocol/src/account/storage/mod.rs b/crates/miden-protocol/src/account/storage/mod.rs new file mode 100644 index 0000000000..a2e2f44c21 --- /dev/null +++ b/crates/miden-protocol/src/account/storage/mod.rs @@ -0,0 +1,475 @@ +use alloc::string::ToString; +use alloc::vec::Vec; + +use super::{ + AccountError, + AccountStorageDelta, + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Felt, + Serializable, + Word, +}; +use crate::account::{AccountComponent, AccountType}; +use crate::crypto::SequentialCommit; +use crate::utils::sync::LazyLock; + +mod slot; +pub use slot::{StorageSlot, StorageSlotContent, StorageSlotId, StorageSlotName, StorageSlotType}; + +mod map; +pub use map::{PartialStorageMap, StorageMap, StorageMapWitness}; + +mod header; +pub use header::{AccountStorageHeader, StorageSlotHeader}; + +mod partial; +pub use partial::PartialStorage; + +static FAUCET_SYSDATA_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::protocol::faucet::sysdata") + .expect("storage slot name should be valid") +}); + +static RESERVED_SLOT_NAMES: LazyLock> = + LazyLock::new(|| vec![FAUCET_SYSDATA_SLOT_NAME.clone()]); + +/// Returns `true` if the provided slot name is reserved by the protocol. +pub fn is_reserved_slot_name(slot_name: &StorageSlotName) -> bool { + RESERVED_SLOT_NAMES.iter().any(|reserved| reserved.id() == slot_name.id()) +} + +// ACCOUNT STORAGE +// ================================================================================================ + +/// Account storage is composed of a variable number of name-addressable [`StorageSlot`]s up to +/// 255 slots in total. +/// +/// Each slot consists of a [`StorageSlotName`] and [`StorageSlotContent`] which defines its size +/// and structure. Currently, the following content types are supported: +/// - [`StorageSlotContent::Value`]: contains a single [`Word`] of data (i.e., 32 bytes). +/// - [`StorageSlotContent::Map`]: contains a [`StorageMap`] which is a key-value map where both +/// keys and values are [Word]s. The value of a storage slot containing a map is the commitment to +/// the underlying map. +/// +/// Slots are sorted by [`StorageSlotName`] (or [`StorageSlotId`] equivalently). This order is +/// necessary to: +/// - Simplify lookups of slots in the transaction kernel (using `std::collections::sorted_array` +/// from the miden core library) +/// - Allow the [`AccountStorageDelta`] to work only with slot names instead of slot indices. +/// - Make it simple to check for duplicates by iterating the slots and checking that no two +/// adjacent items have the same slot name. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct AccountStorage { + slots: Vec, +} + +impl AccountStorage { + /// The maximum number of storage slots allowed in an account storage. + pub const MAX_NUM_STORAGE_SLOTS: usize = 255; + + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + + /// Returns a new instance of account storage initialized with the provided storage slots. + /// + /// This function sorts the slots by [`StorageSlotName`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - The number of [`StorageSlot`]s exceeds 255. + /// - There are multiple storage slots with the same [`StorageSlotName`]. + pub fn new(mut slots: Vec) -> Result { + let num_slots = slots.len(); + + if num_slots > Self::MAX_NUM_STORAGE_SLOTS { + return Err(AccountError::StorageTooManySlots(num_slots as u64)); + } + + // Unstable sort is fine because we require all names to be unique. + slots.sort_unstable(); + + // Check for slot name uniqueness by checking each neighboring slot's IDs. This is + // sufficient because the slots are sorted. + for slots in slots.windows(2) { + if slots[0].id() == slots[1].id() { + return Err(AccountError::DuplicateStorageSlotName(slots[0].name().clone())); + } + } + + Ok(Self { slots }) + } + + /// Creates an [`AccountStorage`] from the provided components' storage slots. + /// + /// If the account type is faucet the reserved slot (slot 0) will be initialized. + /// - For Fungible Faucets the value is [`StorageSlot::empty_value`]. + /// - For Non-Fungible Faucets the value is [`StorageSlot::empty_map`]. + /// + /// If the storage needs to be initialized with certain values in that slot, those can be added + /// after construction with the standard set methods for items and maps. + /// + /// # Errors + /// + /// Returns an error if: + /// - The number of [`StorageSlot`]s of all components exceeds 255. + /// - Any component accesses [`AccountStorage::faucet_sysdata_slot`]. + pub(super) fn from_components( + components: Vec, + account_type: AccountType, + ) -> Result { + let mut storage_slots = match account_type { + AccountType::FungibleFaucet => { + vec![StorageSlot::with_empty_value(Self::faucet_sysdata_slot().clone())] + }, + AccountType::NonFungibleFaucet => { + vec![StorageSlot::with_empty_map(Self::faucet_sysdata_slot().clone())] + }, + _ => vec![], + }; + + for component_slot in components.into_iter().flat_map(|component| { + let AccountComponent { storage_slots, .. } = component; + storage_slots.into_iter() + }) { + if is_reserved_slot_name(component_slot.name()) { + return Err(AccountError::StorageSlotNameMustNotBeFaucetSysdata); + } + + storage_slots.push(component_slot); + } + + Self::new(storage_slots) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] of the faucet's protocol system data. + pub fn faucet_sysdata_slot() -> &'static StorageSlotName { + &FAUCET_SYSDATA_SLOT_NAME + } + + /// Converts storage slots of this account storage into a vector of field elements. + /// + /// Each storage slot is represented by exactly 8 elements: + /// + /// ```text + /// [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE] + /// ``` + pub fn to_elements(&self) -> Vec { + ::to_elements(self) + } + + /// Returns the commitment to the [`AccountStorage`]. + pub fn to_commitment(&self) -> Word { + ::to_commitment(self) + } + + /// Returns the number of slots in the account's storage. + pub fn num_slots(&self) -> u8 { + // SAFETY: The constructors of account storage ensure that the number of slots fits into a + // u8. + self.slots.len() as u8 + } + + /// Returns a reference to the storage slots. + pub fn slots(&self) -> &[StorageSlot] { + &self.slots + } + + /// Consumes self and returns the storage slots of the account storage. + pub fn into_slots(self) -> Vec { + self.slots + } + + /// Returns an [AccountStorageHeader] for this account storage. + pub fn to_header(&self) -> AccountStorageHeader { + AccountStorageHeader::new(self.slots.iter().map(StorageSlotHeader::from).collect()) + .expect("slots should be valid as ensured by AccountStorage") + } + + /// Returns a reference to the storage slot with the provided name, if it exists, `None` + /// otherwise. + pub fn get(&self, slot_name: &StorageSlotName) -> Option<&StorageSlot> { + self.slots.iter().find(|slot| slot.name().id() == slot_name.id()) + } + + /// Returns a mutable reference to the storage slot with the provided name, if it exists, `None` + /// otherwise. + fn get_mut(&mut self, slot_name: &StorageSlotName) -> Option<&mut StorageSlot> { + self.slots.iter_mut().find(|slot| slot.name().id() == slot_name.id()) + } + + /// Returns an item from the storage slot with the given name. + /// + /// # Errors + /// + /// Returns an error if: + /// - A slot with the provided name does not exist. + pub fn get_item(&self, slot_name: &StorageSlotName) -> Result { + self.get(slot_name) + .map(|slot| slot.content().value()) + .ok_or_else(|| AccountError::StorageSlotNameNotFound { slot_name: slot_name.clone() }) + } + + /// Returns a map item from the map in the storage slot with the given name. + /// + /// # Errors + /// + /// Returns an error if: + /// - A slot with the provided name does not exist. + /// - If the [`StorageSlot`] is not [`StorageSlotType::Map`]. + pub fn get_map_item( + &self, + slot_name: &StorageSlotName, + key: Word, + ) -> Result { + self.get(slot_name) + .ok_or_else(|| AccountError::StorageSlotNameNotFound { slot_name: slot_name.clone() }) + .and_then(|slot| match slot.content() { + StorageSlotContent::Map(map) => Ok(map.get(&key)), + _ => Err(AccountError::StorageSlotNotMap(slot_name.clone())), + }) + } + + // STATE MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Applies the provided delta to this account storage. + /// + /// # Errors + /// + /// Returns an error if: + /// - The updates violate storage constraints. + pub(super) fn apply_delta(&mut self, delta: &AccountStorageDelta) -> Result<(), AccountError> { + // Update storage values + for (slot_name, &value) in delta.values() { + self.set_item(slot_name, value)?; + } + + // Update storage maps + for (slot_name, map_delta) in delta.maps() { + let slot = self + .get_mut(slot_name) + .ok_or(AccountError::StorageSlotNameNotFound { slot_name: slot_name.clone() })?; + + let storage_map = match slot.content_mut() { + StorageSlotContent::Map(map) => map, + _ => return Err(AccountError::StorageSlotNotMap(slot_name.clone())), + }; + + storage_map.apply_delta(map_delta)?; + } + + Ok(()) + } + + /// Updates the value of the storage slot with the given name. + /// + /// This method should be used only to update value slots. For updating values + /// in storage maps, please see [`AccountStorage::set_map_item`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - A slot with the provided name does not exist. + /// - The [`StorageSlot`] is not [`StorageSlotType::Value`]. + pub fn set_item( + &mut self, + slot_name: &StorageSlotName, + value: Word, + ) -> Result { + let slot = self.get_mut(slot_name).ok_or_else(|| { + AccountError::StorageSlotNameNotFound { slot_name: slot_name.clone() } + })?; + + let StorageSlotContent::Value(old_value) = slot.content() else { + return Err(AccountError::StorageSlotNotValue(slot_name.clone())); + }; + let old_value = *old_value; + + let mut new_slot = StorageSlotContent::Value(value); + core::mem::swap(slot.content_mut(), &mut new_slot); + + Ok(old_value) + } + + /// Updates the value of a key-value pair of a storage map with the given name. + /// + /// This method should be used only to update storage maps. For updating values + /// in storage slots, please see [AccountStorage::set_item()]. + /// + /// # Errors + /// + /// Returns an error if: + /// - A slot with the provided name does not exist. + /// - If the [`StorageSlot`] is not [`StorageSlotType::Map`]. + pub fn set_map_item( + &mut self, + slot_name: &StorageSlotName, + raw_key: Word, + value: Word, + ) -> Result<(Word, Word), AccountError> { + let slot = self.get_mut(slot_name).ok_or_else(|| { + AccountError::StorageSlotNameNotFound { slot_name: slot_name.clone() } + })?; + + let StorageSlotContent::Map(storage_map) = slot.content_mut() else { + return Err(AccountError::StorageSlotNotMap(slot_name.clone())); + }; + + let old_root = storage_map.root(); + + let old_value = storage_map.insert(raw_key, value)?; + + Ok((old_root, old_value)) + } +} + +// ITERATORS +// ================================================================================================ + +impl IntoIterator for AccountStorage { + type Item = StorageSlot; + type IntoIter = alloc::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.slots.into_iter() + } +} + +// SEQUENTIAL COMMIT +// ================================================================================================ + +impl SequentialCommit for AccountStorage { + type Commitment = Word; + + fn to_elements(&self) -> Vec { + self.slots() + .iter() + .flat_map(|slot| { + StorageSlotHeader::new( + slot.name().clone(), + slot.content().slot_type(), + slot.content().value(), + ) + .to_elements() + }) + .collect() + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AccountStorage { + fn write_into(&self, target: &mut W) { + target.write_u8(self.slots().len() as u8); + target.write_many(self.slots()); + } + + fn get_size_hint(&self) -> usize { + // Size of the serialized slot length. + let u8_size = 0u8.get_size_hint(); + let mut size = u8_size; + + for slot in self.slots() { + size += slot.get_size_hint(); + } + + size + } +} + +impl Deserializable for AccountStorage { + fn read_from(source: &mut R) -> Result { + let num_slots = source.read_u8()? as usize; + let slots = source.read_many::(num_slots)?; + + Self::new(slots).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::{AccountStorage, Deserializable, Serializable}; + use crate::account::{AccountStorageHeader, StorageSlot, StorageSlotHeader, StorageSlotName}; + use crate::errors::AccountError; + + #[test] + fn test_serde_account_storage() -> anyhow::Result<()> { + // empty storage + let storage = AccountStorage::new(vec![]).unwrap(); + let bytes = storage.to_bytes(); + assert_eq!(storage, AccountStorage::read_from_bytes(&bytes).unwrap()); + + // storage with values for default types + let storage = AccountStorage::new(vec![ + StorageSlot::with_empty_value(StorageSlotName::new("miden::test::value")?), + StorageSlot::with_empty_map(StorageSlotName::new("miden::test::map")?), + ]) + .unwrap(); + let bytes = storage.to_bytes(); + assert_eq!(storage, AccountStorage::read_from_bytes(&bytes).unwrap()); + + Ok(()) + } + + #[test] + fn test_get_slot_by_name() -> anyhow::Result<()> { + let counter_slot = StorageSlotName::new("miden::test::counter")?; + let map_slot = StorageSlotName::new("miden::test::map")?; + + let slots = vec![ + StorageSlot::with_empty_value(counter_slot.clone()), + StorageSlot::with_empty_map(map_slot.clone()), + ]; + let storage = AccountStorage::new(slots.clone())?; + + assert_eq!(storage.get(&counter_slot).unwrap(), &slots[0]); + assert_eq!(storage.get(&map_slot).unwrap(), &slots[1]); + + Ok(()) + } + + #[test] + fn test_account_storage_and_header_fail_on_duplicate_slot_name() -> anyhow::Result<()> { + let slot_name0 = StorageSlotName::mock(0); + let slot_name1 = StorageSlotName::mock(1); + let slot_name2 = StorageSlotName::mock(2); + + let mut slots = vec![ + StorageSlot::with_empty_value(slot_name0.clone()), + StorageSlot::with_empty_value(slot_name1.clone()), + StorageSlot::with_empty_map(slot_name0.clone()), + StorageSlot::with_empty_value(slot_name2.clone()), + ]; + + // Set up a test where the slots we pass are not already sorted + // This ensures the duplicate is correctly found + let err = AccountStorage::new(slots.clone()).unwrap_err(); + + assert_matches!(err, AccountError::DuplicateStorageSlotName(name) => { + assert_eq!(name, slot_name0); + }); + + slots.sort_unstable(); + let err = AccountStorageHeader::new(slots.iter().map(StorageSlotHeader::from).collect()) + .unwrap_err(); + + assert_matches!(err, AccountError::DuplicateStorageSlotName(name) => { + assert_eq!(name, slot_name0); + }); + + Ok(()) + } +} diff --git a/crates/miden-objects/src/account/storage/partial.rs b/crates/miden-protocol/src/account/storage/partial.rs similarity index 87% rename from crates/miden-objects/src/account/storage/partial.rs rename to crates/miden-protocol/src/account/storage/partial.rs index 53c90828a7..41e2500bac 100644 --- a/crates/miden-objects/src/account/storage/partial.rs +++ b/crates/miden-protocol/src/account/storage/partial.rs @@ -2,11 +2,12 @@ use alloc::collections::{BTreeMap, BTreeSet}; use miden_core::utils::{Deserializable, Serializable}; use miden_crypto::Word; -use miden_crypto::merkle::{InnerNodeInfo, SmtLeaf}; +use miden_crypto::merkle::InnerNodeInfo; +use miden_crypto::merkle::smt::SmtLeaf; -use super::{AccountStorage, AccountStorageHeader, StorageSlot}; -use crate::AccountError; +use super::{AccountStorage, AccountStorageHeader, StorageSlotContent}; use crate::account::PartialStorageMap; +use crate::errors::AccountError; /// A partial representation of an account storage, containing only a subset of the storage data. /// @@ -47,7 +48,7 @@ impl PartialStorage { maps.insert(smt.root(), smt); } - let commitment = storage_header.compute_commitment(); + let commitment = storage_header.to_commitment(); Ok(Self { commitment, header: storage_header, maps }) } @@ -57,11 +58,11 @@ impl PartialStorage { /// in all map slots of the account storage. pub fn new_full(account_storage: AccountStorage) -> Self { let header: AccountStorageHeader = account_storage.to_header(); - let commitment = header.compute_commitment(); + let commitment = header.to_commitment(); let mut maps = BTreeMap::new(); for slot in account_storage { - if let StorageSlot::Map(storage_map) = slot { + if let StorageSlotContent::Map(storage_map) = slot.into_parts().1 { let partial_map = PartialStorageMap::new_full(storage_map); maps.insert(partial_map.root(), partial_map); } @@ -76,11 +77,11 @@ impl PartialStorage { /// [`PartialStorageMap`] represents the correct root. pub fn new_minimal(account_storage: &AccountStorage) -> Self { let header: AccountStorageHeader = account_storage.to_header(); - let commitment = header.compute_commitment(); + let commitment = header.to_commitment(); let mut maps = BTreeMap::new(); for slot in account_storage.slots() { - if let StorageSlot::Map(storage_map) = slot { + if let StorageSlotContent::Map(storage_map) = slot.content() { let partial_map = PartialStorageMap::new_minimal(storage_map); maps.insert(partial_map.root(), partial_map); } @@ -145,7 +146,7 @@ impl Deserializable for PartialStorage { let header: AccountStorageHeader = source.read()?; let map_smts: BTreeMap = source.read()?; - let commitment = header.compute_commitment(); + let commitment = header.to_commitment(); Ok(PartialStorage { header, maps: map_smts, commitment }) } @@ -163,6 +164,7 @@ mod tests { PartialStorageMap, StorageMap, StorageSlot, + StorageSlotName, }; #[test] @@ -175,7 +177,11 @@ mod tests { map_1.insert(map_key_present, Word::try_from([5u64, 4, 3, 2])?).unwrap(); assert_eq!(map_1.get(&map_key_present), [5u64, 4, 3, 2].try_into()?); - let storage = AccountStorage::new(vec![StorageSlot::Map(map_1.clone())]).unwrap(); + let slot_name = StorageSlotName::new("miden::test_map")?; + + let storage = + AccountStorage::new(vec![StorageSlot::with_map(slot_name.clone(), map_1.clone())]) + .unwrap(); // Create partial storage with validation of one map key let storage_header = AccountStorageHeader::from(&storage); @@ -185,7 +191,8 @@ mod tests { PartialStorage::new(storage_header, [PartialStorageMap::with_witnesses([witness])?]) .context("creating partial storage")?; - let retrieved_map = partial_storage.maps.get(&partial_storage.header.slot(0)?.1).unwrap(); + let slot_header = partial_storage.header.find_slot_header_by_name(&slot_name).unwrap(); + let retrieved_map = partial_storage.maps.get(&slot_header.value()).unwrap(); assert!(retrieved_map.open(&map_key_absent).is_err()); assert!(retrieved_map.open(&map_key_present).is_ok()); Ok(()) diff --git a/crates/miden-protocol/src/account/storage/slot/mod.rs b/crates/miden-protocol/src/account/storage/slot/mod.rs new file mode 100644 index 0000000000..711ae48bed --- /dev/null +++ b/crates/miden-protocol/src/account/storage/slot/mod.rs @@ -0,0 +1,14 @@ +mod slot_name; +pub use slot_name::StorageSlotName; + +mod slot_id; +pub use slot_id::StorageSlotId; + +mod r#type; +pub use r#type::StorageSlotType; + +mod storage_slot; +pub use storage_slot::StorageSlot; + +mod slot_content; +pub use slot_content::StorageSlotContent; diff --git a/crates/miden-objects/src/account/storage/slot/mod.rs b/crates/miden-protocol/src/account/storage/slot/slot_content.rs similarity index 62% rename from crates/miden-objects/src/account/storage/slot/mod.rs rename to crates/miden-protocol/src/account/storage/slot/slot_content.rs index 9f6389c38b..746391de54 100644 --- a/crates/miden-objects/src/account/storage/slot/mod.rs +++ b/crates/miden-protocol/src/account/storage/slot/slot_content.rs @@ -2,65 +2,57 @@ use miden_core::EMPTY_WORD; use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use miden_processor::DeserializationError; -use super::map::EMPTY_STORAGE_MAP_ROOT; -use super::{StorageMap, Word}; +use crate::account::StorageSlotType; +use crate::account::storage::map::EMPTY_STORAGE_MAP_ROOT; +use crate::account::storage::{StorageMap, Word}; -mod slot_name; -pub use slot_name::SlotName; - -mod r#type; -pub use r#type::StorageSlotType; - -// STORAGE SLOT +// STORAGE SLOT CONTENT // ================================================================================================ -/// An object representing the contents of an account's storage slot. +/// Represents the contents of a [`StorageSlot`](super::StorageSlot). /// -/// An account storage slot can be of two types: +/// The content of a storage slot can be of two types: /// - A simple value which contains a single word (4 field elements or ~32 bytes). /// - A key value map where both keys and values are words. The capacity of such storage slot is /// theoretically unlimited. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum StorageSlot { +pub enum StorageSlotContent { Value(Word), Map(StorageMap), } -impl StorageSlot { - /// The number of field elements needed to represent a [StorageSlot] in kernel memory. - pub const NUM_ELEMENTS_PER_STORAGE_SLOT: usize = 8; - - /// Returns true if this storage slot has a value equal the default of it's type +impl StorageSlotContent { + /// Returns true if this storage slot has a value equal to the default of its type. pub fn is_default(&self) -> bool { match self { - StorageSlot::Value(value) => *value == EMPTY_WORD, - StorageSlot::Map(map) => map.root() == EMPTY_STORAGE_MAP_ROOT, + StorageSlotContent::Value(value) => *value == EMPTY_WORD, + StorageSlotContent::Map(map) => map.root() == EMPTY_STORAGE_MAP_ROOT, } } /// Returns the empty [Word] for a storage slot of this type pub fn default_word(&self) -> Word { match self { - StorageSlot::Value(_) => EMPTY_WORD, - StorageSlot::Map(_) => EMPTY_STORAGE_MAP_ROOT, + StorageSlotContent::Value(_) => EMPTY_WORD, + StorageSlotContent::Map(_) => EMPTY_STORAGE_MAP_ROOT, } } - /// Returns a [`StorageSlot::Value`] with an empty word. + /// Returns a [`StorageSlotContent::Value`] with an empty word. pub fn empty_value() -> Self { - StorageSlot::Value(EMPTY_WORD) + StorageSlotContent::Value(EMPTY_WORD) } - /// Returns an empty [`StorageSlot::Map`]. + /// Returns an empty [`StorageSlotContent::Map`]. pub fn empty_map() -> Self { - StorageSlot::Map(StorageMap::new()) + StorageSlotContent::Map(StorageMap::new()) } /// Returns this storage slot value as a [Word] /// /// Returns: - /// - For [StorageSlot::Value] the value - /// - For [StorageSlot::Map] the root of the [StorageMap] + /// - For [`StorageSlotContent::Value`] the value. + /// - For [`StorageSlotContent::Map`] the root of the [StorageMap]. pub fn value(&self) -> Word { match self { Self::Value(value) => *value, @@ -71,8 +63,8 @@ impl StorageSlot { /// Returns the type of this storage slot pub fn slot_type(&self) -> StorageSlotType { match self { - StorageSlot::Value(_) => StorageSlotType::Value, - StorageSlot::Map(_) => StorageSlotType::Map, + StorageSlotContent::Value(_) => StorageSlotType::Value, + StorageSlotContent::Map(_) => StorageSlotType::Map, } } } @@ -80,7 +72,7 @@ impl StorageSlot { // SERIALIZATION // ================================================================================================ -impl Serializable for StorageSlot { +impl Serializable for StorageSlotContent { fn write_into(&self, target: &mut W) { target.write(self.slot_type()); @@ -94,26 +86,26 @@ impl Serializable for StorageSlot { let mut size = self.slot_type().get_size_hint(); size += match self { - StorageSlot::Value(word) => word.get_size_hint(), - StorageSlot::Map(storage_map) => storage_map.get_size_hint(), + StorageSlotContent::Value(word) => word.get_size_hint(), + StorageSlotContent::Map(storage_map) => storage_map.get_size_hint(), }; size } } -impl Deserializable for StorageSlot { +impl Deserializable for StorageSlotContent { fn read_from(source: &mut R) -> Result { let storage_slot_type = source.read::()?; match storage_slot_type { StorageSlotType::Value => { let word = source.read::()?; - Ok(StorageSlot::Value(word)) + Ok(StorageSlotContent::Value(word)) }, StorageSlotType::Map => { let map = source.read::()?; - Ok(StorageSlot::Map(map)) + Ok(StorageSlotContent::Map(map)) }, } } @@ -129,7 +121,7 @@ mod tests { use crate::account::AccountStorage; #[test] - fn test_serde_account_storage_slot() { + fn test_serde_storage_slot_content() { let storage = AccountStorage::mock(); let serialized = storage.to_bytes(); let deserialized = AccountStorage::read_from_bytes(&serialized).unwrap(); diff --git a/crates/miden-protocol/src/account/storage/slot/slot_id.rs b/crates/miden-protocol/src/account/storage/slot/slot_id.rs new file mode 100644 index 0000000000..b22b46b2dd --- /dev/null +++ b/crates/miden-protocol/src/account/storage/slot/slot_id.rs @@ -0,0 +1,131 @@ +use core::cmp::Ordering; +use core::fmt::Display; +use core::hash::Hash; + +use miden_core::utils::hash_string_to_word; + +use crate::Felt; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; + +/// The partial hash of a [`StorageSlotName`](super::StorageSlotName). +/// +/// The ID of a slot are the first (`suffix`) and second (`prefix`) field elements of the +/// blake3-hashed slot name. +/// +/// The slot ID is used to uniquely identify a storage slot and is used to sort slots in account +/// storage. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StorageSlotId { + suffix: Felt, + prefix: Felt, +} + +impl StorageSlotId { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`StorageSlotId`] from the provided felts. + pub fn new(suffix: Felt, prefix: Felt) -> Self { + Self { suffix, prefix } + } + + /// Computes the [`StorageSlotId`] from a slot name. + /// + /// The provided `name`'s validity is **not** checked. + pub(super) fn from_str(name: &str) -> StorageSlotId { + let hashed_word = hash_string_to_word(name); + let suffix = hashed_word[0]; + let prefix = hashed_word[1]; + StorageSlotId::new(suffix, prefix) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the suffix of the [`StorageSlotId`]. + pub fn suffix(&self) -> Felt { + self.suffix + } + + /// Returns the prefix of the [`StorageSlotId`]. + pub fn prefix(&self) -> Felt { + self.prefix + } + + /// Returns the [`StorageSlotId`]'s felts encoded into a u128. + fn as_u128(&self) -> u128 { + let mut le_bytes = [0_u8; 16]; + le_bytes[..8].copy_from_slice(&self.suffix().as_int().to_le_bytes()); + le_bytes[8..].copy_from_slice(&self.prefix().as_int().to_le_bytes()); + u128::from_le_bytes(le_bytes) + } +} + +impl Ord for StorageSlotId { + fn cmp(&self, other: &Self) -> Ordering { + match self.prefix.as_int().cmp(&other.prefix.as_int()) { + ord @ Ordering::Less | ord @ Ordering::Greater => ord, + Ordering::Equal => self.suffix.as_int().cmp(&other.suffix.as_int()), + } + } +} + +impl PartialOrd for StorageSlotId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Hash for StorageSlotId { + fn hash(&self, state: &mut H) { + self.suffix.inner().hash(state); + self.prefix.inner().hash(state); + } +} + +impl Display for StorageSlotId { + /// Returns a big-endian, hex-encoded string of length 34, including the `0x` prefix. + /// + /// This means it encodes 16 bytes. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_fmt(format_args!("0x{:032x}", self.as_u128())) + } +} + +impl Serializable for StorageSlotId { + fn write_into(&self, target: &mut W) { + self.suffix.write_into(target); + self.prefix.write_into(target); + } +} + +impl Deserializable for StorageSlotId { + fn read_from(source: &mut R) -> Result { + let suffix = Felt::read_from(source)?; + let prefix = Felt::read_from(source)?; + Ok(StorageSlotId::new(suffix, prefix)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_slot_id_as_u128() { + let suffix = 5; + let prefix = 3; + let slot_id = StorageSlotId::new(Felt::from(suffix as u32), Felt::from(prefix as u32)); + assert_eq!(slot_id.as_u128(), (prefix << 64) + suffix); + assert_eq!(format!("{slot_id}"), "0x00000000000000030000000000000005"); + } +} diff --git a/crates/miden-protocol/src/account/storage/slot/slot_name.rs b/crates/miden-protocol/src/account/storage/slot/slot_name.rs new file mode 100644 index 0000000000..27d0c7ce45 --- /dev/null +++ b/crates/miden-protocol/src/account/storage/slot/slot_name.rs @@ -0,0 +1,427 @@ +use alloc::string::{String, ToString}; +use alloc::sync::Arc; +use core::fmt::Display; +use core::str::FromStr; + +use crate::account::storage::slot::StorageSlotId; +use crate::errors::StorageSlotNameError; +use crate::utils::serde::{ByteWriter, Deserializable, DeserializationError, Serializable}; + +/// The name of an account storage slot. +/// +/// A typical slot name looks like this: +/// +/// ```text +/// miden::standards::fungible_faucets::metadata +/// ``` +/// +/// The double-colon (`::`) serves as a separator and the strings in between the separators are +/// called components. +/// +/// It is generally recommended that slot names have at least three components and follow this +/// structure: +/// +/// ```text +/// project_name::component_name::slot_name +/// ``` +/// +/// ## Requirements +/// +/// For a string to be a valid slot name it needs to satisfy the following criteria: +/// - Its length must be less than 255. +/// - It needs to have at least 2 components. +/// - Each component must consist of at least one character. +/// - Each component must only consist of the characters `a` to `z`, `A` to `Z`, `0` to `9` or `_` +/// (underscore). +/// - Each component must not start with an underscore. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StorageSlotName { + name: Arc, + id: StorageSlotId, +} + +impl StorageSlotName { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The minimum number of components that a slot name must contain. + pub(crate) const MIN_NUM_COMPONENTS: usize = 2; + + /// The maximum number of characters in a slot name. + pub(crate) const MAX_LENGTH: usize = u8::MAX as usize; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Constructs a new [`StorageSlotName`] from a string. + /// + /// # Errors + /// + /// Returns an error if: + /// - the slot name is invalid (see the type-level docs for the requirements). + pub fn new(name: impl Into>) -> Result { + let name: Arc = name.into(); + Self::validate(&name)?; + let id = StorageSlotId::from_str(&name); + Ok(Self { name, id }) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the slot name as a string slice. + pub fn as_str(&self) -> &str { + &self.name + } + + /// Returns the slot name as a string slice. + // allow is_empty to be missing because it would always return false since slot names are + // enforced to have a length greater than zero, so it does not have much use. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> u8 { + // SAFETY: Slot name validation should enforce length fits into a u8. + debug_assert!(self.name.len() <= Self::MAX_LENGTH); + self.name.len() as u8 + } + + /// Returns the [`StorageSlotId`] derived from the slot name. + pub fn id(&self) -> StorageSlotId { + self.id + } + + // HELPERS + // -------------------------------------------------------------------------------------------- + + /// Validates a slot name. + /// + /// This checks that components are separated by double colons, that each component contains + /// only valid characters and that the name is not empty or starts or ends with a colon. + /// + /// We must check the validity of a slot name against the raw bytes of the UTF-8 string because + /// typical character APIs are not available in a const version. We can do this because any byte + /// in a UTF-8 string that is an ASCII character never represents anything other than such a + /// character, even though UTF-8 can contain multibyte sequences: + /// + /// > UTF-8, the object of this memo, has a one-octet encoding unit. It uses all bits of an + /// > octet, but has the quality of preserving the full US-ASCII range: US-ASCII characters + /// > are encoded in one octet having the normal US-ASCII value, and any octet with such a value + /// > can only stand for a US-ASCII character, and nothing else. + /// > https://www.rfc-editor.org/rfc/rfc3629 + const fn validate(name: &str) -> Result<(), StorageSlotNameError> { + let bytes = name.as_bytes(); + let mut idx = 0; + let mut num_components = 0; + + if bytes.is_empty() { + return Err(StorageSlotNameError::TooShort); + } + + if bytes.len() > Self::MAX_LENGTH { + return Err(StorageSlotNameError::TooLong); + } + + // Slot names must not start with a colon or underscore. + // SAFETY: We just checked that we're not dealing with an empty slice. + if bytes[0] == b':' { + return Err(StorageSlotNameError::UnexpectedColon); + } else if bytes[0] == b'_' { + return Err(StorageSlotNameError::UnexpectedUnderscore); + } + + while idx < bytes.len() { + let byte = bytes[idx]; + + let is_colon = byte == b':'; + + if is_colon { + // A colon must always be followed by another colon. In other words, we + // expect a double colon. + if (idx + 1) < bytes.len() { + if bytes[idx + 1] != b':' { + return Err(StorageSlotNameError::UnexpectedColon); + } + } else { + return Err(StorageSlotNameError::UnexpectedColon); + } + + // A component cannot end with a colon, so this allows us to validate the start of a + // component: It must not start with a colon or an underscore. + if (idx + 2) < bytes.len() { + if bytes[idx + 2] == b':' { + return Err(StorageSlotNameError::UnexpectedColon); + } else if bytes[idx + 2] == b'_' { + return Err(StorageSlotNameError::UnexpectedUnderscore); + } + } else { + return Err(StorageSlotNameError::UnexpectedColon); + } + + // Advance past the double colon. + idx += 2; + + // A double colon completes a slot name component. + num_components += 1; + } else if Self::is_valid_char(byte) { + idx += 1; + } else { + return Err(StorageSlotNameError::InvalidCharacter); + } + } + + // The last component is not counted as part of the loop because no double colon follows. + num_components += 1; + + if num_components < Self::MIN_NUM_COMPONENTS { + return Err(StorageSlotNameError::TooShort); + } + + Ok(()) + } + + /// Returns `true` if the given byte is a valid slot name character, `false` otherwise. + const fn is_valid_char(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' + } +} + +impl Ord for StorageSlotName { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.id().cmp(&other.id()) + } +} + +impl PartialOrd for StorageSlotName { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Display for StorageSlotName { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for StorageSlotName { + type Err = StorageSlotNameError; + + fn from_str(string: &str) -> Result { + StorageSlotName::new(string) + } +} + +impl From for String { + fn from(slot_name: StorageSlotName) -> Self { + slot_name.name.to_string() + } +} + +impl Serializable for StorageSlotName { + fn write_into(&self, target: &mut W) { + target.write_u8(self.len()); + target.write_many(self.as_str().as_bytes()) + } + + fn get_size_hint(&self) -> usize { + // Slot name length + slot name bytes + 1 + self.as_str().len() + } +} + +impl Deserializable for StorageSlotName { + fn read_from( + source: &mut R, + ) -> Result { + let len = source.read_u8()?; + let name = source.read_many(len as usize)?; + String::from_utf8(name) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + .and_then(|name| { + Self::new(name).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + }) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use std::borrow::ToOwned; + + use assert_matches::assert_matches; + + use super::*; + + // A string containing all allowed characters of a slot name. + const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + + // Invalid colon or underscore tests + // -------------------------------------------------------------------------------------------- + + #[test] + fn slot_name_fails_on_invalid_colon_placement() { + // Single colon. + assert_matches!( + StorageSlotName::new(":").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new("0::1:").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new(":0::1").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new("0::1:2").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + + // Double colon (placed invalidly). + assert_matches!( + StorageSlotName::new("::").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new("1::2::").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new("::1::2").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + + // Triple colon. + assert_matches!( + StorageSlotName::new(":::").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new("1::2:::").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new(":::1::2").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + assert_matches!( + StorageSlotName::new("1::2:::3").unwrap_err(), + StorageSlotNameError::UnexpectedColon + ); + } + + #[test] + fn slot_name_fails_on_invalid_underscore_placement() { + assert_matches!( + StorageSlotName::new("_one::two").unwrap_err(), + StorageSlotNameError::UnexpectedUnderscore + ); + assert_matches!( + StorageSlotName::new("one::_two").unwrap_err(), + StorageSlotNameError::UnexpectedUnderscore + ); + } + + // Length validation tests + // -------------------------------------------------------------------------------------------- + + #[test] + fn slot_name_fails_on_empty_string() { + assert_matches!(StorageSlotName::new("").unwrap_err(), StorageSlotNameError::TooShort); + } + + #[test] + fn slot_name_fails_on_single_component() { + assert_matches!( + StorageSlotName::new("single_component").unwrap_err(), + StorageSlotNameError::TooShort + ); + } + + #[test] + fn slot_name_fails_on_string_whose_length_exceeds_max_length() { + let mut string = get_max_length_slot_name(); + string.push('a'); + assert_matches!(StorageSlotName::new(string).unwrap_err(), StorageSlotNameError::TooLong); + } + + // Alphabet validation tests + // -------------------------------------------------------------------------------------------- + + #[test] + fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> { + let name = format!("{FULL_ALPHABET}::second"); + let slot_name = StorageSlotName::new(name.clone())?; + assert_eq!(slot_name.as_str(), name); + + Ok(()) + } + + #[test] + fn slot_name_fails_on_invalid_character() { + assert_matches!( + StorageSlotName::new("na#me::second").unwrap_err(), + StorageSlotNameError::InvalidCharacter + ); + assert_matches!( + StorageSlotName::new("first_entry::secönd").unwrap_err(), + StorageSlotNameError::InvalidCharacter + ); + assert_matches!( + StorageSlotName::new("first::sec::th!rd").unwrap_err(), + StorageSlotNameError::InvalidCharacter + ); + } + + // Valid slot name tests + // -------------------------------------------------------------------------------------------- + + #[test] + fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> { + StorageSlotName::new("miden::component")?; + Ok(()) + } + + #[test] + fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> { + StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?; + Ok(()) + } + + #[test] + fn slot_name_with_max_length_is_valid() -> anyhow::Result<()> { + StorageSlotName::new(get_max_length_slot_name())?; + Ok(()) + } + + // Serialization tests + // -------------------------------------------------------------------------------------------- + + #[test] + fn serde_slot_name() -> anyhow::Result<()> { + let slot_name = StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?; + assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?); + Ok(()) + } + + #[test] + fn serde_max_length_slot_name() -> anyhow::Result<()> { + let slot_name = StorageSlotName::new(get_max_length_slot_name())?; + assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?); + Ok(()) + } + + // Test helpers + // -------------------------------------------------------------------------------------------- + + fn get_max_length_slot_name() -> String { + const MIDEN_STR: &str = "miden::"; + let remainder = ['a'; StorageSlotName::MAX_LENGTH - MIDEN_STR.len()]; + let mut string = MIDEN_STR.to_owned(); + string.extend(remainder); + assert_eq!(string.len(), StorageSlotName::MAX_LENGTH); + string + } +} diff --git a/crates/miden-protocol/src/account/storage/slot/storage_slot.rs b/crates/miden-protocol/src/account/storage/slot/storage_slot.rs new file mode 100644 index 0000000000..9fce734847 --- /dev/null +++ b/crates/miden-protocol/src/account/storage/slot/storage_slot.rs @@ -0,0 +1,139 @@ +use crate::Word; +use crate::account::storage::slot::StorageSlotId; +use crate::account::{StorageMap, StorageSlotContent, StorageSlotName, StorageSlotType}; + +/// An individual storage slot in [`AccountStorage`](crate::account::AccountStorage). +/// +/// This consists of a [`StorageSlotName`] that uniquely identifies the slot and its +/// [`StorageSlotContent`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StorageSlot { + /// The name of the storage slot. + name: StorageSlotName, + /// The content of the storage slot. + content: StorageSlotContent, +} + +impl StorageSlot { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The number of field elements that represent a [`StorageSlot`] in kernel memory. + pub const NUM_ELEMENTS: usize = 8; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`StorageSlot`] with the given [`StorageSlotName`] and + /// [`StorageSlotContent`]. + pub fn new(name: StorageSlotName, content: StorageSlotContent) -> Self { + Self { name, content } + } + + /// Creates a new [`StorageSlot`] with the given [`StorageSlotName`] and the `value` + /// wrapped into a [`StorageSlotContent::Value`]. + pub fn with_value(name: StorageSlotName, value: Word) -> Self { + Self::new(name, StorageSlotContent::Value(value)) + } + + /// Creates a new [`StorageSlot`] with the given [`StorageSlotName`] and + /// [`StorageSlotContent::empty_value`]. + pub fn with_empty_value(name: StorageSlotName) -> Self { + Self::new(name, StorageSlotContent::empty_value()) + } + + /// Creates a new [`StorageSlot`] with the given [`StorageSlotName`] and the `map` wrapped + /// into a [`StorageSlotContent::Map`] + pub fn with_map(name: StorageSlotName, map: StorageMap) -> Self { + Self::new(name, StorageSlotContent::Map(map)) + } + + /// Creates a new [`StorageSlot`] with the given [`StorageSlotName`] and + /// [`StorageSlotContent::empty_map`]. + pub fn with_empty_map(name: StorageSlotName) -> Self { + Self::new(name, StorageSlotContent::empty_map()) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] by which the [`StorageSlot`] is identified. + pub fn name(&self) -> &StorageSlotName { + &self.name + } + + /// Returns the [`StorageSlotId`] by which the [`StorageSlot`] is identified. + pub fn id(&self) -> StorageSlotId { + self.name.id() + } + + /// Returns this storage slot value as a [Word] + /// + /// Returns: + /// - For [`StorageSlotContent::Value`] the value. + /// - For [`StorageSlotContent::Map`] the root of the [StorageMap]. + pub fn value(&self) -> Word { + self.content().value() + } + + /// Returns a reference to the [`StorageSlotContent`] contained in this [`StorageSlot`]. + pub fn content(&self) -> &StorageSlotContent { + &self.content + } + + /// Returns the [`StorageSlotType`] of this [`StorageSlot`]. + pub fn slot_type(&self) -> StorageSlotType { + self.content.slot_type() + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Returns a mutable reference to the [`StorageSlotContent`] contained in this + /// [`StorageSlot`]. + pub fn content_mut(&mut self) -> &mut StorageSlotContent { + &mut self.content + } + + /// Consumes self and returns the underlying parts. + pub fn into_parts(self) -> (StorageSlotName, StorageSlotContent) { + (self.name, self.content) + } +} + +impl Ord for StorageSlot { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.name().cmp(&other.name) + } +} + +impl PartialOrd for StorageSlot { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl crate::utils::serde::Serializable for StorageSlot { + fn write_into(&self, target: &mut W) { + target.write(&self.name); + target.write(&self.content); + } + + fn get_size_hint(&self) -> usize { + self.name.get_size_hint() + self.content().get_size_hint() + } +} + +impl crate::utils::serde::Deserializable for StorageSlot { + fn read_from( + source: &mut R, + ) -> Result { + let name: StorageSlotName = source.read()?; + let content: StorageSlotContent = source.read()?; + + Ok(Self::new(name, content)) + } +} diff --git a/crates/miden-objects/src/account/storage/slot/type.rs b/crates/miden-protocol/src/account/storage/slot/type.rs similarity index 59% rename from crates/miden-objects/src/account/storage/slot/type.rs rename to crates/miden-protocol/src/account/storage/slot/type.rs index bb5d9f2645..25bd5a3a60 100644 --- a/crates/miden-objects/src/account/storage/slot/type.rs +++ b/crates/miden-protocol/src/account/storage/slot/type.rs @@ -1,5 +1,10 @@ -use alloc::string::{String, ToString}; +use alloc::string::ToString; +use core::fmt::Display; +use miden_core::{ONE, ZERO}; + +use crate::Felt; +use crate::errors::AccountError; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -7,27 +12,26 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{Felt, Word}; // STORAGE SLOT TYPE // ================================================================================================ -/// An object that represents the type of a storage slot. +/// The type of a storage slot. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] pub enum StorageSlotType { /// Represents a slot that contains a value. - Value, + Value = Self::VALUE_TYPE, /// Represents a slot that contains a commitment to a map with key-value pairs. - Map, + Map = Self::MAP_TYPE, } impl StorageSlotType { - /// Returns storage slot type as a [Word] - pub fn as_word(&self) -> Word { - match self { - StorageSlotType::Value => Word::empty(), - StorageSlotType::Map => Word::from([1, 0, 0, 0u32]), - } + const VALUE_TYPE: u8 = 0; + const MAP_TYPE: u8 = 1; + + pub fn as_felt(&self) -> Felt { + Felt::from(*self as u8) } /// Returns `true` if the slot is a value slot, `false` otherwise. @@ -41,16 +45,37 @@ impl StorageSlotType { } } +impl TryFrom for StorageSlotType { + type Error = AccountError; + + fn try_from(value: u8) -> Result { + match value { + Self::VALUE_TYPE => Ok(StorageSlotType::Value), + Self::MAP_TYPE => Ok(StorageSlotType::Map), + _ => Err(AccountError::other(format!("unsupported storage slot type {value}"))), + } + } +} + impl TryFrom for StorageSlotType { - type Error = String; + type Error = AccountError; fn try_from(value: Felt) -> Result { - let value = value.as_int(); + if value == ZERO { + Ok(StorageSlotType::Value) + } else if value == ONE { + Ok(StorageSlotType::Map) + } else { + Err(AccountError::other("invalid storage slot type")) + } + } +} - match value { - 0 => Ok(StorageSlotType::Value), - 1 => Ok(StorageSlotType::Map), - _ => Err("No storage slot type exists for this field element.".to_string()), +impl Display for StorageSlotType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + StorageSlotType::Value => f.write_str("value"), + StorageSlotType::Map => f.write_str("map"), } } } @@ -77,8 +102,8 @@ impl Deserializable for StorageSlotType { let storage_slot_type = source.read_u8()?; match storage_slot_type { - 0 => Ok(Self::Value), - 1 => Ok(Self::Map), + Self::VALUE_TYPE => Ok(Self::Value), + Self::MAP_TYPE => Ok(Self::Map), _ => Err(DeserializationError::InvalidValue(storage_slot_type.to_string())), } } @@ -92,6 +117,7 @@ mod tests { use miden_core::utils::{Deserializable, Serializable}; use crate::account::StorageSlotType; + use crate::{Felt, FieldElement}; #[test] fn test_serde_account_storage_slot_type() { @@ -104,4 +130,15 @@ mod tests { assert_eq!(type_0, deserialized_0); assert_eq!(type_1, deserialized_1); } + + #[test] + fn test_storage_slot_type_from_felt() { + let felt = Felt::ZERO; + let slot_type = StorageSlotType::try_from(felt).unwrap(); + assert_eq!(slot_type, StorageSlotType::Value); + + let felt = Felt::ONE; + let slot_type = StorageSlotType::try_from(felt).unwrap(); + assert_eq!(slot_type, StorageSlotType::Map); + } } diff --git a/crates/miden-objects/src/address/address_id.rs b/crates/miden-protocol/src/address/address_id.rs similarity index 92% rename from crates/miden-objects/src/address/address_id.rs rename to crates/miden-protocol/src/address/address_id.rs index 2974e66ac3..e6cdbdddeb 100644 --- a/crates/miden-objects/src/address/address_id.rs +++ b/crates/miden-protocol/src/address/address_id.rs @@ -4,10 +4,9 @@ use bech32::Bech32m; use bech32::primitives::decode::CheckedHrpstring; use miden_processor::DeserializationError; -use crate::AddressError; use crate::account::{AccountId, AccountStorageMode}; use crate::address::{AddressType, NetworkId}; -use crate::errors::Bech32Error; +use crate::errors::{AddressError, Bech32Error}; use crate::note::NoteTag; use crate::utils::serde::{ByteWriter, Deserializable, Serializable}; @@ -31,14 +30,15 @@ impl AddressId { /// Returns the default tag length of the ID. /// /// This is guaranteed to be in range `0..=30` (e.g. the maximum of - /// [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]). + /// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and + /// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]). pub fn default_note_tag_len(&self) -> u8 { match self { AddressId::AccountId(id) => { if id.storage_mode() == AccountStorageMode::Network { - NoteTag::DEFAULT_NETWORK_TAG_LENGTH + NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH } else { - NoteTag::DEFAULT_LOCAL_TAG_LENGTH + NoteTag::DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH } }, } diff --git a/crates/miden-objects/src/address/interface.rs b/crates/miden-protocol/src/address/interface.rs similarity index 97% rename from crates/miden-objects/src/address/interface.rs rename to crates/miden-protocol/src/address/interface.rs index 562feb2585..9884ebdc00 100644 --- a/crates/miden-objects/src/address/interface.rs +++ b/crates/miden-protocol/src/address/interface.rs @@ -1,6 +1,6 @@ use core::fmt::{self, Display, Formatter}; -use crate::AddressError; +use crate::errors::AddressError; /// The account interface of an [`Address`](super::Address). /// diff --git a/crates/miden-objects/src/address/mod.rs b/crates/miden-protocol/src/address/mod.rs similarity index 96% rename from crates/miden-objects/src/address/mod.rs rename to crates/miden-protocol/src/address/mod.rs index 142dd9e8b8..91c9438569 100644 --- a/crates/miden-objects/src/address/mod.rs +++ b/crates/miden-protocol/src/address/mod.rs @@ -16,9 +16,9 @@ pub use interface::AddressInterface; use miden_processor::DeserializationError; pub use network_id::{CustomNetworkId, NetworkId}; -use crate::AddressError; use crate::account::AccountStorageMode; use crate::crypto::ies::SealingKey; +use crate::errors::AddressError; use crate::note::NoteTag; use crate::utils::serde::{ByteWriter, Deserializable, Serializable}; @@ -85,8 +85,8 @@ impl Address { /// # Errors /// /// Returns an error if: - /// - The tag length routing parameter is not [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`] for - /// network accounts. + /// - The tag length routing parameter is not + /// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`] for network accounts. pub fn with_routing_parameters( mut self, routing_params: RoutingParameters, @@ -95,7 +95,7 @@ impl Address { match self.id { AddressId::AccountId(account_id) => { if account_id.storage_mode() == AccountStorageMode::Network - && tag_len != NoteTag::DEFAULT_NETWORK_TAG_LENGTH + && tag_len != NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH { return Err(AddressError::CustomTagLengthNotAllowedForNetworkAccounts( tag_len, @@ -126,7 +126,8 @@ impl Address { /// Returns the preferred tag length. /// /// This is guaranteed to be in range `0..=30` (e.g. the maximum of - /// [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]). + /// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and + /// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]). pub fn note_tag_len(&self) -> u8 { self.routing_params .as_ref() @@ -143,8 +144,8 @@ impl Address { match id.storage_mode() { AccountStorageMode::Network => NoteTag::from_network_account_id(id), AccountStorageMode::Private | AccountStorageMode::Public => { - NoteTag::from_local_account_id(id, note_tag_len) - .expect("address should validate that tag len does not exceed MAX_LOCAL_TAG_LENGTH bits") + NoteTag::with_custom_account_target(id, note_tag_len) + .expect("address should validate that tag len does not exceed MAX_ACCOUNT_TARGET_TAG_LENGTH bits") } } }, @@ -245,10 +246,9 @@ mod tests { use bech32::{Bech32, Bech32m, NoChecksum}; use super::*; - use crate::AccountIdError; use crate::account::{AccountId, AccountType}; use crate::address::CustomNetworkId; - use crate::errors::Bech32Error; + use crate::errors::{AccountIdError, Bech32Error}; use crate::testing::account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, AccountIdBuilder}; /// Tests that an account ID address can be encoded and decoded. @@ -302,7 +302,7 @@ mod tests { // Encode/Decode with routing parameters should be valid. address = address.with_routing_parameters( RoutingParameters::new(AddressInterface::BasicWallet) - .with_note_tag_len(NoteTag::DEFAULT_NETWORK_TAG_LENGTH)?, + .with_note_tag_len(NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH)?, )?; let bech32_string = address.encode(network_id.clone()); @@ -431,7 +431,7 @@ mod tests { let account_id = AccountIdBuilder::new().account_type(account_type).build_with_rng(rng); let address = Address::new(account_id).with_routing_parameters( RoutingParameters::new(AddressInterface::BasicWallet) - .with_note_tag_len(NoteTag::DEFAULT_NETWORK_TAG_LENGTH)?, + .with_note_tag_len(NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH)?, )?; let serialized = address.to_bytes(); @@ -445,7 +445,7 @@ mod tests { /// Tests that an address with encryption key can be created and used. #[test] fn address_with_encryption_key() -> anyhow::Result<()> { - use crate::crypto::dsa::eddsa_25519::SecretKey; + use crate::crypto::dsa::eddsa_25519_sha512::SecretKey; use crate::crypto::ies::{SealingKey, UnsealingKey}; let rng = &mut rand::rng(); @@ -484,7 +484,7 @@ mod tests { /// Tests that an address with encryption key can be encoded/decoded. #[test] fn address_encryption_key_encode_decode() -> anyhow::Result<()> { - use crate::crypto::dsa::eddsa_25519::SecretKey; + use crate::crypto::dsa::eddsa_25519_sha512::SecretKey; let rng = &mut rand::rng(); // Use a local account type (RegularAccountImmutableCode) instead of network diff --git a/crates/miden-objects/src/address/network_id.rs b/crates/miden-protocol/src/address/network_id.rs similarity index 100% rename from crates/miden-objects/src/address/network_id.rs rename to crates/miden-protocol/src/address/network_id.rs diff --git a/crates/miden-objects/src/address/routing_parameters.rs b/crates/miden-protocol/src/address/routing_parameters.rs similarity index 95% rename from crates/miden-objects/src/address/routing_parameters.rs rename to crates/miden-protocol/src/address/routing_parameters.rs index 5d173f9ee6..0fb5ec6923 100644 --- a/crates/miden-objects/src/address/routing_parameters.rs +++ b/crates/miden-protocol/src/address/routing_parameters.rs @@ -5,11 +5,10 @@ use alloc::vec::Vec; use bech32::primitives::decode::CheckedHrpstring; use bech32::{Bech32m, Hrp}; -use crate::AddressError; use crate::address::AddressInterface; -use crate::crypto::dsa::{ecdsa_k256_keccak, eddsa_25519}; +use crate::crypto::dsa::{ecdsa_k256_keccak, eddsa_25519_sha512}; use crate::crypto::ies::SealingKey; -use crate::errors::Bech32Error; +use crate::errors::{AddressError, Bech32Error}; use crate::note::NoteTag; use crate::utils::serde::{ ByteReader, @@ -87,16 +86,16 @@ impl RoutingParameters { /// The tag length determines how many bits of the address ID are encoded into [`NoteTag`]s of /// notes targeted to this address. This lets the receiver choose their level of privacy. A /// higher tag length makes the address ID more uniquely identifiable and reduces privacy, - /// while a shorter length increases privacy at the cost of matching more notes - /// published onchain. + /// while a shorter length increases privacy at the cost of matching more notes published + /// onchain. /// /// # Errors /// /// Returns an error if: - /// - The tag length exceeds the maximum of [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and - /// [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]. + /// - The tag length exceeds the maximum of [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and + /// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]. pub fn with_note_tag_len(mut self, note_tag_len: u8) -> Result { - if note_tag_len > NoteTag::MAX_LOCAL_TAG_LENGTH { + if note_tag_len > NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH { return Err(AddressError::TagLengthTooLarge(note_tag_len)); } @@ -110,7 +109,8 @@ impl RoutingParameters { /// Returns the note tag length preference. /// /// This is guaranteed to be in range `0..=30` (e.g. the maximum of - /// [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]). + /// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and + /// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]). pub fn note_tag_len(&self) -> Option { self.note_tag_len } @@ -363,7 +363,7 @@ fn decode_encryption_key( fn read_x25519_pub_key( byte_iter: &mut impl ExactSizeIterator, -) -> Result { +) -> Result { if byte_iter.len() < X25519_PUBLIC_KEY_LENGTH { return Err(AddressError::decode_error(format!( "expected {} bytes to decode X25519 public key", @@ -371,7 +371,7 @@ fn read_x25519_pub_key( ))); } let key_bytes: [u8; X25519_PUBLIC_KEY_LENGTH] = read_byte_array(byte_iter); - eddsa_25519::PublicKey::read_from_bytes(&key_bytes).map_err(|err| { + eddsa_25519_sha512::PublicKey::read_from_bytes(&key_bytes).map_err(|err| { AddressError::decode_error_with_source("failed to decode X25519 public key", err) }) } @@ -467,11 +467,11 @@ mod tests { // Test case 4: Explicit tag length set to max let params_tag_max = RoutingParameters::new(AddressInterface::BasicWallet) - .with_note_tag_len(NoteTag::MAX_LOCAL_TAG_LENGTH)?; + .with_note_tag_len(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)?; let encoded = params_tag_max.encode_to_string(); let decoded = RoutingParameters::decode(encoded)?; assert_eq!(params_tag_max, decoded); - assert_eq!(decoded.note_tag_len(), Some(NoteTag::MAX_LOCAL_TAG_LENGTH)); + assert_eq!(decoded.note_tag_len(), Some(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)); Ok(()) } @@ -504,11 +504,11 @@ mod tests { // Test case 4: Explicit tag length set to max let params_tag_max = RoutingParameters::new(AddressInterface::BasicWallet) - .with_note_tag_len(NoteTag::MAX_LOCAL_TAG_LENGTH)?; + .with_note_tag_len(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)?; let serialized = params_tag_max.to_bytes(); let deserialized = RoutingParameters::read_from_bytes(&serialized)?; assert_eq!(params_tag_max, deserialized); - assert_eq!(deserialized.note_tag_len(), Some(NoteTag::MAX_LOCAL_TAG_LENGTH)); + assert_eq!(deserialized.note_tag_len(), Some(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)); Ok(()) } @@ -538,7 +538,7 @@ mod tests { // Test X25519XChaCha20Poly1305 { - use crate::crypto::dsa::eddsa_25519::SecretKey; + use crate::crypto::dsa::eddsa_25519_sha512::SecretKey; let secret_key = SecretKey::with_rng(&mut rand::rng()); let public_key = secret_key.public_key(); let encryption_key = SealingKey::X25519XChaCha20Poly1305(public_key); @@ -556,7 +556,7 @@ mod tests { // Test X25519AeadRpo { - use crate::crypto::dsa::eddsa_25519::SecretKey; + use crate::crypto::dsa::eddsa_25519_sha512::SecretKey; let secret_key = SecretKey::with_rng(&mut rand::rng()); let public_key = secret_key.public_key(); let encryption_key = SealingKey::X25519AeadRpo(public_key); diff --git a/crates/miden-objects/src/address/type.rs b/crates/miden-protocol/src/address/type.rs similarity index 95% rename from crates/miden-objects/src/address/type.rs rename to crates/miden-protocol/src/address/type.rs index 188755e28d..e3ee4f8fed 100644 --- a/crates/miden-objects/src/address/type.rs +++ b/crates/miden-protocol/src/address/type.rs @@ -1,5 +1,4 @@ -use crate::AddressError; -use crate::errors::Bech32Error; +use crate::errors::{AddressError, Bech32Error}; /// The type of an [`Address`](super::Address) in Miden. /// diff --git a/crates/miden-objects/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs similarity index 100% rename from crates/miden-objects/src/asset/fungible.rs rename to crates/miden-protocol/src/asset/fungible.rs diff --git a/crates/miden-objects/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs similarity index 99% rename from crates/miden-objects/src/asset/mod.rs rename to crates/miden-protocol/src/asset/mod.rs index 3c3fc7d73c..4d14998289 100644 --- a/crates/miden-objects/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -1,4 +1,5 @@ use super::account::AccountType; +use super::errors::{AssetError, TokenSymbolError}; use super::utils::serde::{ ByteReader, ByteWriter, @@ -6,7 +7,7 @@ use super::utils::serde::{ DeserializationError, Serializable, }; -use super::{AssetError, Felt, Hasher, TokenSymbolError, Word, ZERO}; +use super::{Felt, Hasher, Word, ZERO}; use crate::account::AccountIdPrefix; mod fungible; diff --git a/crates/miden-objects/src/asset/nonfungible.rs b/crates/miden-protocol/src/asset/nonfungible.rs similarity index 99% rename from crates/miden-objects/src/asset/nonfungible.rs rename to crates/miden-protocol/src/asset/nonfungible.rs index b0fa5f5ddd..d48b26602b 100644 --- a/crates/miden-objects/src/asset/nonfungible.rs +++ b/crates/miden-protocol/src/asset/nonfungible.rs @@ -83,7 +83,7 @@ impl NonFungibleAsset { /// Creates a new [NonFungibleAsset] without checking its validity. /// /// # Safety - /// This function required that the provided value is a valid word representation of a + /// This function requires that the provided value is a valid word encoding of a /// [NonFungibleAsset]. pub unsafe fn new_unchecked(value: Word) -> NonFungibleAsset { NonFungibleAsset(value) diff --git a/crates/miden-objects/src/asset/token_symbol.rs b/crates/miden-protocol/src/asset/token_symbol.rs similarity index 72% rename from crates/miden-objects/src/asset/token_symbol.rs rename to crates/miden-protocol/src/asset/token_symbol.rs index 656a9c2ff5..a1132fd396 100644 --- a/crates/miden-objects/src/asset/token_symbol.rs +++ b/crates/miden-protocol/src/asset/token_symbol.rs @@ -1,4 +1,4 @@ -use alloc::string::{String, ToString}; +use alloc::string::String; use super::{Felt, TokenSymbolError}; @@ -20,8 +20,33 @@ impl TokenSymbol { /// This value encodes the "ZZZZZZ" token symbol. pub const MAX_ENCODED_VALUE: u64 = 8031810156; + /// Constructs a new [`TokenSymbol`] from a static string. + /// + /// This function is `const` and can be used to define token symbols as constants, e.g.: + /// + /// ```rust + /// # use miden_protocol::asset::TokenSymbol; + /// const TOKEN: TokenSymbol = TokenSymbol::from_static_str("ETH"); + /// ``` + /// + /// This is convenient because using a string that is not a valid token symbol fails to + /// compile. + /// + /// # Panics + /// + /// Panics if: + /// - The length of the provided string is less than 1 or greater than 6. + /// - The provided token string contains characters that are not uppercase ASCII. + pub const fn from_static_str(symbol: &'static str) -> Self { + match encode_symbol_to_felt(symbol) { + Ok(felt) => Self(felt), + // We cannot format the error in a const context. + Err(_) => panic!("invalid token symbol"), + } + } + /// Creates a new [`TokenSymbol`] instance from the provided token name string. - /// + /// /// # Errors /// Returns an error if: /// - The length of the provided string is less than 1 or greater than 6. @@ -89,22 +114,31 @@ impl TryFrom for TokenSymbol { /// Returns an error if: /// - The length of the provided string is less than 1 or greater than 6. /// - The provided token string contains characters that are not uppercase ASCII. -fn encode_symbol_to_felt(s: &str) -> Result { - if s.is_empty() || s.len() > TokenSymbol::MAX_SYMBOL_LENGTH { - return Err(TokenSymbolError::InvalidLength(s.len())); - } else if s.chars().any(|c| !c.is_ascii_uppercase()) { - return Err(TokenSymbolError::InvalidCharacter(s.to_string())); +const fn encode_symbol_to_felt(s: &str) -> Result { + let bytes = s.as_bytes(); + let len = bytes.len(); + + if len == 0 || len > TokenSymbol::MAX_SYMBOL_LENGTH { + return Err(TokenSymbolError::InvalidLength(len)); } - let mut encoded_value = 0; - for char in s.chars() { - let digit = char as u64 - b'A' as u64; - debug_assert!(digit < TokenSymbol::ALPHABET_LENGTH); + let mut encoded_value: u64 = 0; + let mut idx = 0; + + while idx < len { + let byte = bytes[idx]; + + if !byte.is_ascii_uppercase() { + return Err(TokenSymbolError::InvalidCharacter); + } + + let digit = (byte - b'A') as u64; encoded_value = encoded_value * TokenSymbol::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 * TokenSymbol::ALPHABET_LENGTH + s.len() as u64; + encoded_value = encoded_value * TokenSymbol::ALPHABET_LENGTH + len as u64; Ok(Felt::new(encoded_value)) } @@ -198,7 +232,7 @@ mod test { let symbol = "$$$"; let felt = encode_symbol_to_felt(symbol); - assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidCharacter(s) if s == *"$$$"); + assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidCharacter); let symbol = "ABCDEF"; let token_symbol = TokenSymbol::try_from(symbol); @@ -231,4 +265,58 @@ mod test { let token_symbol = TokenSymbol::try_from("ZZZZZZ").unwrap(); assert_eq!(Felt::from(token_symbol).as_int(), TokenSymbol::MAX_ENCODED_VALUE); } + + // Const function tests + // -------------------------------------------------------------------------------------------- + + const _TOKEN0: TokenSymbol = TokenSymbol::from_static_str("A"); + const _TOKEN1: TokenSymbol = TokenSymbol::from_static_str("ETH"); + const _TOKEN2: TokenSymbol = TokenSymbol::from_static_str("MIDEN"); + const _TOKEN3: TokenSymbol = TokenSymbol::from_static_str("ZZZZZZ"); + + #[test] + fn test_from_static_str_matches_new() { + // Test that from_static_str produces the same result as new + let symbols = ["A", "BC", "ETH", "MIDEN", "ZZZZZZ"]; + for symbol in symbols { + let from_new = TokenSymbol::new(symbol).unwrap(); + let from_static = TokenSymbol::from_static_str(symbol); + assert_eq!( + Felt::from(from_new), + Felt::from(from_static), + "Mismatch for symbol: {}", + symbol + ); + } + } + + #[test] + #[should_panic(expected = "invalid token symbol")] + fn token_symbol_panics_on_empty_string() { + TokenSymbol::from_static_str(""); + } + + #[test] + #[should_panic(expected = "invalid token symbol")] + fn token_symbol_panics_on_too_long_string() { + TokenSymbol::from_static_str("ABCDEFG"); + } + + #[test] + #[should_panic(expected = "invalid token symbol")] + fn token_symbol_panics_on_lowercase() { + TokenSymbol::from_static_str("eth"); + } + + #[test] + #[should_panic(expected = "invalid token symbol")] + fn token_symbol_panics_on_invalid_character() { + TokenSymbol::from_static_str("ET$"); + } + + #[test] + #[should_panic(expected = "invalid token symbol")] + fn token_symbol_panics_on_number() { + TokenSymbol::from_static_str("ETH1"); + } } diff --git a/crates/miden-objects/src/asset/vault/asset_witness.rs b/crates/miden-protocol/src/asset/vault/asset_witness.rs similarity index 70% rename from crates/miden-objects/src/asset/vault/asset_witness.rs rename to crates/miden-protocol/src/asset/vault/asset_witness.rs index 41154d23c8..503b468d41 100644 --- a/crates/miden-objects/src/asset/vault/asset_witness.rs +++ b/crates/miden-protocol/src/asset/vault/asset_witness.rs @@ -1,8 +1,12 @@ -use miden_crypto::merkle::{InnerNodeInfo, SmtLeaf, SmtProof}; +use alloc::string::ToString; + +use miden_crypto::merkle::InnerNodeInfo; +use miden_crypto::merkle::smt::{SmtLeaf, SmtProof}; use super::vault_key::AssetVaultKey; -use crate::AssetError; use crate::asset::Asset; +use crate::errors::AssetError; +use crate::utils::serde::{Deserializable, DeserializationError, Serializable}; /// A witness of an asset in an [`AssetVault`](super::AssetVault). /// @@ -46,6 +50,12 @@ impl AssetWitness { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- + /// Returns `true` if this [`AssetWitness`] authenticates the provided [`AssetVaultKey`], i.e. + /// if its leaf index matches, `false` otherwise. + pub fn authenticates_asset_vault_key(&self, vault_key: AssetVaultKey) -> bool { + self.0.leaf().index() == vault_key.to_leaf_index() + } + /// Searches for an [`Asset`] in the witness with the given `vault_key`. pub fn find(&self, vault_key: AssetVaultKey) -> Option { self.assets().find(|asset| asset.vault_key() == vault_key) @@ -82,17 +92,36 @@ impl From for SmtProof { } } +impl Serializable for AssetWitness { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for AssetWitness { + fn read_from( + source: &mut R, + ) -> Result { + let proof = SmtProof::read_from(source)?; + Self::new(proof).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + // TESTS // ================================================================================================ #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_crypto::merkle::Smt; + use miden_crypto::merkle::smt::Smt; use super::*; use crate::Word; - use crate::asset::{FungibleAsset, NonFungibleAsset}; + use crate::asset::{AssetVault, FungibleAsset, NonFungibleAsset}; + use crate::testing::account_id::{ + ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, + }; /// Tests that constructing an asset witness fails if any asset in the smt proof is invalid. #[test] @@ -128,4 +157,20 @@ mod tests { Ok(()) } + + #[test] + fn asset_witness_authenticates_asset_vault_key() -> anyhow::Result<()> { + let fungible_asset0 = + FungibleAsset::new(ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET.try_into()?, 200)?; + let fungible_asset1 = + FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?; + + let vault = AssetVault::new(&[fungible_asset0.into()])?; + let witness0 = vault.open(fungible_asset0.vault_key()); + + assert!(witness0.authenticates_asset_vault_key(fungible_asset0.vault_key())); + assert!(!witness0.authenticates_asset_vault_key(fungible_asset1.vault_key())); + + Ok(()) + } } diff --git a/crates/miden-objects/src/asset/vault/mod.rs b/crates/miden-protocol/src/asset/vault/mod.rs similarity index 99% rename from crates/miden-objects/src/asset/vault/mod.rs rename to crates/miden-protocol/src/asset/vault/mod.rs index 88b57e0dcf..94bb7ab81a 100644 --- a/crates/miden-objects/src/asset/vault/mod.rs +++ b/crates/miden-protocol/src/asset/vault/mod.rs @@ -14,9 +14,10 @@ use super::{ NonFungibleAsset, Serializable, }; +use crate::Word; use crate::account::{AccountId, AccountVaultDelta, NonFungibleDeltaAction}; -use crate::crypto::merkle::Smt; -use crate::{AssetVaultError, Word}; +use crate::crypto::merkle::smt::Smt; +use crate::errors::AssetVaultError; mod partial; pub use partial::PartialVault; diff --git a/crates/miden-objects/src/asset/vault/partial.rs b/crates/miden-protocol/src/asset/vault/partial.rs similarity index 98% rename from crates/miden-objects/src/asset/vault/partial.rs rename to crates/miden-protocol/src/asset/vault/partial.rs index b303842cff..1427a8902c 100644 --- a/crates/miden-objects/src/asset/vault/partial.rs +++ b/crates/miden-protocol/src/asset/vault/partial.rs @@ -1,6 +1,7 @@ use alloc::string::ToString; -use miden_crypto::merkle::{InnerNodeInfo, MerkleError, PartialSmt, SmtLeaf, SmtProof}; +use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf, SmtProof}; +use miden_crypto::merkle::{InnerNodeInfo, MerkleError}; use super::{AssetVault, AssetVaultKey}; use crate::Word; @@ -191,7 +192,7 @@ impl Deserializable for PartialVault { #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_crypto::merkle::Smt; + use miden_crypto::merkle::smt::Smt; use super::*; use crate::asset::FungibleAsset; diff --git a/crates/miden-objects/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs similarity index 99% rename from crates/miden-objects/src/asset/vault/vault_key.rs rename to crates/miden-protocol/src/asset/vault/vault_key.rs index c68cf7be10..2cff63d04d 100644 --- a/crates/miden-objects/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -1,6 +1,6 @@ use core::fmt; -use miden_crypto::merkle::LeafIndex; +use miden_crypto::merkle::smt::LeafIndex; use miden_processor::SMT_DEPTH; use crate::Word; diff --git a/crates/miden-objects/src/batch/account_update.rs b/crates/miden-protocol/src/batch/account_update.rs similarity index 100% rename from crates/miden-objects/src/batch/account_update.rs rename to crates/miden-protocol/src/batch/account_update.rs diff --git a/crates/miden-objects/src/batch/batch_id.rs b/crates/miden-protocol/src/batch/batch_id.rs similarity index 85% rename from crates/miden-objects/src/batch/batch_id.rs rename to crates/miden-protocol/src/batch/batch_id.rs index 45d3cbc97a..bcbb50e16a 100644 --- a/crates/miden-objects/src/batch/batch_id.rs +++ b/crates/miden-protocol/src/batch/batch_id.rs @@ -1,6 +1,8 @@ use alloc::string::String; use alloc::vec::Vec; +use miden_protocol_macros::WordWrapper; + use crate::account::AccountId; use crate::transaction::{ProvenTransaction, TransactionId}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; @@ -15,7 +17,7 @@ use crate::{Felt, Hasher, Word, ZERO}; /// This is a sequential hash of the tuple `(TRANSACTION_ID || [account_id_prefix, /// account_id_suffix, 0, 0])` of all transactions and the accounts their executed against in the /// batch. -#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)] +#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd, Hash, WordWrapper)] pub struct BatchId(Word); impl BatchId { @@ -38,21 +40,6 @@ impl BatchId { Self(Hasher::hash_elements(&elements)) } - - /// Returns the elements representation of this batch ID. - pub fn as_elements(&self) -> &[Felt] { - self.0.as_elements() - } - - /// Returns the byte representation of this batch ID. - 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() - } } impl core::fmt::Display for BatchId { diff --git a/crates/miden-objects/src/batch/input_output_note_tracker.rs b/crates/miden-protocol/src/batch/input_output_note_tracker.rs similarity index 99% rename from crates/miden-objects/src/batch/input_output_note_tracker.rs rename to crates/miden-protocol/src/batch/input_output_note_tracker.rs index d18d3fe92e..296cf021e6 100644 --- a/crates/miden-objects/src/batch/input_output_note_tracker.rs +++ b/crates/miden-protocol/src/batch/input_output_note_tracker.rs @@ -1,10 +1,11 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; +use crate::Word; use crate::batch::{BatchId, ProvenBatch}; use crate::block::{BlockHeader, BlockNumber}; use crate::crypto::merkle::MerkleError; -use crate::errors::ProposedBatchError; +use crate::errors::{ProposedBatchError, ProposedBlockError}; use crate::note::{NoteHeader, NoteId, NoteInclusionProof, Nullifier}; use crate::transaction::{ InputNoteCommitment, @@ -13,7 +14,6 @@ use crate::transaction::{ ProvenTransaction, TransactionId, }; -use crate::{ProposedBlockError, Word}; type BatchInputNotes = Vec; type BlockInputNotes = Vec; diff --git a/crates/miden-objects/src/batch/mod.rs b/crates/miden-protocol/src/batch/mod.rs similarity index 100% rename from crates/miden-objects/src/batch/mod.rs rename to crates/miden-protocol/src/batch/mod.rs diff --git a/crates/miden-objects/src/batch/note_tree.rs b/crates/miden-protocol/src/batch/note_tree.rs similarity index 95% rename from crates/miden-objects/src/batch/note_tree.rs rename to crates/miden-protocol/src/batch/note_tree.rs index 23ba6c7608..7897856389 100644 --- a/crates/miden-objects/src/batch/note_tree.rs +++ b/crates/miden-protocol/src/batch/note_tree.rs @@ -1,13 +1,14 @@ use alloc::vec::Vec; -use crate::crypto::merkle::{LeafIndex, MerkleError, SimpleSmt}; +use crate::crypto::merkle::MerkleError; +use crate::crypto::merkle::smt::{LeafIndex, SimpleSmt}; use crate::note::{NoteId, NoteMetadata, compute_note_commitment}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; use crate::{BATCH_NOTE_TREE_DEPTH, EMPTY_WORD, Word}; /// Wrapper over [SimpleSmt] for batch note tree. /// -/// Value of each leaf is computed as: `hash(note_id || note_metadata)`. +/// Value of each leaf is computed as: `hash(note_id || note_metadata_commitment)`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BatchNoteTree(SimpleSmt); diff --git a/crates/miden-objects/src/batch/ordered_batches.rs b/crates/miden-protocol/src/batch/ordered_batches.rs similarity index 97% rename from crates/miden-objects/src/batch/ordered_batches.rs rename to crates/miden-protocol/src/batch/ordered_batches.rs index f1d80593dc..00b04fcf9a 100644 --- a/crates/miden-objects/src/batch/ordered_batches.rs +++ b/crates/miden-protocol/src/batch/ordered_batches.rs @@ -17,7 +17,7 @@ pub struct OrderedBatches(Vec); impl OrderedBatches { /// Creates a new set of ordered batches from the provided vector. - pub(crate) fn new(batches: Vec) -> Self { + pub fn new(batches: Vec) -> Self { Self(batches) } diff --git a/crates/miden-objects/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs similarity index 99% rename from crates/miden-objects/src/batch/proposed_batch.rs rename to crates/miden-protocol/src/batch/proposed_batch.rs index d24364ef43..5d5cb0f97b 100644 --- a/crates/miden-objects/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -29,7 +29,7 @@ use crate::{MAX_ACCOUNTS_PER_BATCH, MAX_INPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_ pub struct ProposedBatch { /// The transactions of this batch. transactions: Vec>, - /// The header is boxed as it has a large stack size. + /// The header of the reference block that this batch is proposed for. reference_block_header: BlockHeader, /// The partial blockchain used to authenticate: /// - all unauthenticated notes that can be authenticated, @@ -425,7 +425,7 @@ impl Deserializable for ProposedBatch { #[cfg(test)] mod tests { use anyhow::Context; - use miden_crypto::merkle::{Mmr, PartialMmr}; + use miden_crypto::merkle::mmr::{Mmr, PartialMmr}; use miden_verifier::ExecutionProof; use winter_rand_utils::rand_value; diff --git a/crates/miden-objects/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs similarity index 100% rename from crates/miden-objects/src/batch/proven_batch.rs rename to crates/miden-protocol/src/batch/proven_batch.rs diff --git a/crates/miden-protocol/src/block/account_tree/backend.rs b/crates/miden-protocol/src/block/account_tree/backend.rs new file mode 100644 index 0000000000..78dc989786 --- /dev/null +++ b/crates/miden-protocol/src/block/account_tree/backend.rs @@ -0,0 +1,239 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; + +use super::{AccountId, AccountIdPrefix, AccountTree, AccountTreeError, account_id_to_smt_key}; +use crate::Word; +use crate::crypto::merkle::MerkleError; +#[cfg(feature = "std")] +use crate::crypto::merkle::smt::{LargeSmt, LargeSmtError, SmtStorage}; +use crate::crypto::merkle::smt::{LeafIndex, MutationSet, SMT_DEPTH, Smt, SmtLeaf, SmtProof}; + +// ACCOUNT TREE BACKEND +// ================================================================================================ + +/// This trait abstracts over different SMT backends (e.g., `Smt` and `LargeSmt`) to allow +/// the `AccountTree` to work with either implementation transparently. +/// +/// Implementors must provide `Default` for creating empty instances. Users should +/// instantiate the backend directly (potentially with entries) and then pass it to +/// [`AccountTree::new`]. +pub trait AccountTreeBackend: Sized { + type Error: core::error::Error + Send + 'static; + + /// Returns the number of leaves in the SMT. + fn num_leaves(&self) -> usize; + + /// Returns all leaves in the SMT as an iterator over leaf index and leaf pairs. + fn leaves<'a>(&'a self) -> Box, SmtLeaf)>>; + + /// Opens the leaf at the given key, returning a Merkle proof. + fn open(&self, key: &Word) -> SmtProof; + + /// Applies the given mutation set to the SMT. + fn apply_mutations( + &mut self, + set: MutationSet, + ) -> Result<(), Self::Error>; + + /// Applies the given mutation set to the SMT and returns the reverse mutation set. + /// + /// The reverse mutation set can be used to revert the changes made by this operation. + fn apply_mutations_with_reversion( + &mut self, + set: MutationSet, + ) -> Result, Self::Error>; + + /// Computes the mutation set required to apply the given updates to the SMT. + fn compute_mutations( + &self, + updates: Vec<(Word, Word)>, + ) -> Result, Self::Error>; + + /// Inserts a key-value pair into the SMT, returning the previous value at that key. + fn insert(&mut self, key: Word, value: Word) -> Result; + + /// Returns the value associated with the given key. + fn get_value(&self, key: &Word) -> Word; + + /// Returns the leaf at the given key. + fn get_leaf(&self, key: &Word) -> SmtLeaf; + + /// Returns the root of the SMT. + fn root(&self) -> Word; +} + +// BACKEND IMPLEMENTATION FOR SMT +// ================================================================================================ + +impl AccountTreeBackend for Smt { + type Error = MerkleError; + + fn num_leaves(&self) -> usize { + Smt::num_leaves(self) + } + + fn leaves<'a>(&'a self) -> Box, SmtLeaf)>> { + Box::new(Smt::leaves(self).map(|(idx, leaf)| (idx, leaf.clone()))) + } + + fn open(&self, key: &Word) -> SmtProof { + Smt::open(self, key) + } + + fn apply_mutations( + &mut self, + set: MutationSet, + ) -> Result<(), Self::Error> { + Smt::apply_mutations(self, set) + } + + fn apply_mutations_with_reversion( + &mut self, + set: MutationSet, + ) -> Result, Self::Error> { + Smt::apply_mutations_with_reversion(self, set) + } + + fn compute_mutations( + &self, + updates: Vec<(Word, Word)>, + ) -> Result, Self::Error> { + Smt::compute_mutations(self, updates) + } + + fn insert(&mut self, key: Word, value: Word) -> Result { + Smt::insert(self, key, value) + } + + fn get_value(&self, key: &Word) -> Word { + Smt::get_value(self, key) + } + + fn get_leaf(&self, key: &Word) -> SmtLeaf { + Smt::get_leaf(self, key) + } + + fn root(&self) -> Word { + Smt::root(self) + } +} + +// BACKEND IMPLEMENTATION FOR LARGE SMT +// ================================================================================================ + +#[cfg(feature = "std")] +impl AccountTreeBackend for LargeSmt +where + Backend: SmtStorage, +{ + type Error = MerkleError; + + fn num_leaves(&self) -> usize { + // LargeSmt::num_leaves returns Result + // We'll unwrap or return 0 on error + LargeSmt::num_leaves(self).map_err(large_smt_error_to_merkle_error).unwrap_or(0) + } + + fn leaves<'a>(&'a self) -> Box, SmtLeaf)>> { + Box::new(LargeSmt::leaves(self).expect("Only IO can error out here")) + } + + fn open(&self, key: &Word) -> SmtProof { + LargeSmt::open(self, key) + } + + fn apply_mutations( + &mut self, + set: MutationSet, + ) -> Result<(), Self::Error> { + LargeSmt::apply_mutations(self, set).map_err(large_smt_error_to_merkle_error) + } + + fn apply_mutations_with_reversion( + &mut self, + set: MutationSet, + ) -> Result, Self::Error> { + LargeSmt::apply_mutations_with_reversion(self, set).map_err(large_smt_error_to_merkle_error) + } + + fn compute_mutations( + &self, + updates: Vec<(Word, Word)>, + ) -> Result, Self::Error> { + LargeSmt::compute_mutations(self, updates).map_err(large_smt_error_to_merkle_error) + } + + fn insert(&mut self, key: Word, value: Word) -> Result { + LargeSmt::insert(self, key, value) + } + + fn get_value(&self, key: &Word) -> Word { + LargeSmt::get_value(self, key) + } + + fn get_leaf(&self, key: &Word) -> SmtLeaf { + LargeSmt::get_leaf(self, key) + } + + fn root(&self) -> Word { + LargeSmt::root(self) + } +} + +// CONVENIENCE METHODS +// ================================================================================================ + +impl AccountTree { + /// Creates a new [`AccountTree`] with the provided entries. + /// + /// This is a convenience method for testing that creates an SMT backend with the provided + /// entries and wraps it in an AccountTree. It validates that the entries don't contain + /// duplicate prefixes. + /// + /// # Errors + /// + /// Returns an error if: + /// - The provided entries contain duplicate account ID prefixes + /// - The backend fails to create the SMT with the entries + pub fn with_entries( + entries: impl IntoIterator, + ) -> Result + where + I: ExactSizeIterator, + { + // Create the SMT with the entries + let smt = Smt::with_entries( + entries + .into_iter() + .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), + ) + .map_err(|err| { + let MerkleError::DuplicateValuesForIndex(leaf_idx) = err else { + unreachable!("the only error returned by Smt::with_entries is of this type"); + }; + + // SAFETY: Since we only inserted account IDs into the SMT, it is guaranteed that + // the leaf_idx is a valid Felt as well as a valid account ID prefix. + AccountTreeError::DuplicateStateCommitments { + prefix: AccountIdPrefix::new_unchecked( + crate::Felt::try_from(leaf_idx).expect("leaf index should be a valid felt"), + ), + } + })?; + + AccountTree::new(smt) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +#[cfg(feature = "std")] +fn large_smt_error_to_merkle_error(err: LargeSmtError) -> MerkleError { + match err { + LargeSmtError::Storage(storage_err) => { + panic!("Storage error encountered: {:?}", storage_err) + }, + LargeSmtError::Merkle(merkle_err) => merkle_err, + } +} diff --git a/crates/miden-objects/src/block/account_tree.rs b/crates/miden-protocol/src/block/account_tree/mod.rs similarity index 73% rename from crates/miden-objects/src/block/account_tree.rs rename to crates/miden-protocol/src/block/account_tree/mod.rs index 0aa6655f9c..2bff2b5f44 100644 --- a/crates/miden-objects/src/block/account_tree.rs +++ b/crates/miden-protocol/src/block/account_tree/mod.rs @@ -1,15 +1,23 @@ -use alloc::boxed::Box; use alloc::string::ToString; use alloc::vec::Vec; -use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_crypto::merkle::{LeafIndex, MerkleError, MutationSet, Smt, SmtLeaf, SmtProof}; -use miden_processor::{DeserializationError, SMT_DEPTH}; +use miden_crypto::merkle::smt::LeafIndex; use crate::Word; use crate::account::{AccountId, AccountIdPrefix}; -use crate::block::AccountWitness; +use crate::crypto::merkle::MerkleError; +use crate::crypto::merkle::smt::{MutationSet, SMT_DEPTH, Smt, SmtLeaf}; use crate::errors::AccountTreeError; +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +mod partial; +pub use partial::PartialAccountTree; + +mod witness; +pub use witness::AccountWitness; + +mod backend; +pub use backend::AccountTreeBackend; // FREE HELPER FUNCTIONS // ================================================================================================ @@ -33,190 +41,20 @@ pub fn account_id_to_smt_key(account_id: AccountId) -> Word { /// /// # Panics /// -/// Panics if the key does not represent a valid account ID. This should never happen -/// when used with keys from account trees, as the tree only stores valid IDs. +/// Panics if the key does not represent a valid account ID. This should never happen when used +/// with keys from account trees, as the tree only stores valid IDs. pub fn smt_key_to_account_id(key: Word) -> AccountId { AccountId::try_from([key[KEY_PREFIX_IDX], key[KEY_SUFFIX_IDX]]) .expect("account tree should only contain valid IDs") } -// ACCOUNT TREE BACKEND TRAIT -// ================================================================================================ - -/// This trait abstracts over different SMT backends (e.g., `Smt` and `LargeSmt`) to allow -/// the `AccountTree` to work with either implementation transparently. -/// -/// Implementors must provide `Default` for creating empty instances. Users should -/// instantiate the backend directly (potentially with entries) and then pass it to -/// [`AccountTree::new`]. -pub trait AccountTreeBackend: Sized { - type Error: core::error::Error + Send + 'static; - - /// Returns the number of leaves in the SMT. - fn num_leaves(&self) -> usize; - - /// Returns all leaves in the SMT as an iterator over leaf index and leaf pairs. - fn leaves<'a>(&'a self) -> Box, SmtLeaf)>>; - - /// Opens the leaf at the given key, returning a Merkle proof. - fn open(&self, key: &Word) -> SmtProof; - - /// Applies the given mutation set to the SMT. - fn apply_mutations( - &mut self, - set: MutationSet, - ) -> Result<(), Self::Error>; - - /// Applies the given mutation set to the SMT and returns the reverse mutation set. - /// - /// The reverse mutation set can be used to revert the changes made by this operation. - fn apply_mutations_with_reversion( - &mut self, - set: MutationSet, - ) -> Result, Self::Error>; - - /// Computes the mutation set required to apply the given updates to the SMT. - fn compute_mutations( - &self, - updates: Vec<(Word, Word)>, - ) -> Result, Self::Error>; - - /// Inserts a key-value pair into the SMT, returning the previous value at that key. - fn insert(&mut self, key: Word, value: Word) -> Result; - - /// Returns the value associated with the given key. - fn get_value(&self, key: &Word) -> Word; - - /// Returns the leaf at the given key. - fn get_leaf(&self, key: &Word) -> SmtLeaf; - - /// Returns the root of the SMT. - fn root(&self) -> Word; +/// Converts an AccountId to an SMT leaf index for use with MerkleStore operations. +pub fn account_id_to_smt_index(account_id: AccountId) -> LeafIndex { + account_id_to_smt_key(account_id).into() } -impl AccountTreeBackend for Smt { - type Error = MerkleError; - - fn num_leaves(&self) -> usize { - Smt::num_leaves(self) - } - - fn leaves<'a>(&'a self) -> Box, SmtLeaf)>> { - Box::new(Smt::leaves(self).map(|(idx, leaf)| (idx, leaf.clone()))) - } - - fn open(&self, key: &Word) -> SmtProof { - Smt::open(self, key) - } - - fn apply_mutations( - &mut self, - set: MutationSet, - ) -> Result<(), Self::Error> { - Smt::apply_mutations(self, set) - } - - fn apply_mutations_with_reversion( - &mut self, - set: MutationSet, - ) -> Result, Self::Error> { - Smt::apply_mutations_with_reversion(self, set) - } - - fn compute_mutations( - &self, - updates: Vec<(Word, Word)>, - ) -> Result, Self::Error> { - Smt::compute_mutations(self, updates) - } - - fn insert(&mut self, key: Word, value: Word) -> Result { - Smt::insert(self, key, value) - } - - fn get_value(&self, key: &Word) -> Word { - Smt::get_value(self, key) - } - - fn get_leaf(&self, key: &Word) -> SmtLeaf { - Smt::get_leaf(self, key) - } - - fn root(&self) -> Word { - Smt::root(self) - } -} - -#[cfg(feature = "std")] -use miden_crypto::merkle::{LargeSmt, LargeSmtError, SmtStorage}; -#[cfg(feature = "std")] -fn large_smt_error_to_merkle_error(err: LargeSmtError) -> MerkleError { - match err { - LargeSmtError::Storage(storage_err) => { - panic!("Storage error encountered: {:?}", storage_err) - }, - LargeSmtError::Merkle(merkle_err) => merkle_err, - } -} - -#[cfg(feature = "std")] -impl AccountTreeBackend for LargeSmt -where - Backend: SmtStorage, -{ - type Error = MerkleError; - - fn num_leaves(&self) -> usize { - // LargeSmt::num_leaves returns Result - // We'll unwrap or return 0 on error - LargeSmt::num_leaves(self).map_err(large_smt_error_to_merkle_error).unwrap_or(0) - } - - fn leaves<'a>(&'a self) -> Box, SmtLeaf)>> { - Box::new(LargeSmt::leaves(self).expect("Only IO can error out here")) - } - - fn open(&self, key: &Word) -> SmtProof { - LargeSmt::open(self, key) - } - - fn apply_mutations( - &mut self, - set: MutationSet, - ) -> Result<(), Self::Error> { - LargeSmt::apply_mutations(self, set).map_err(large_smt_error_to_merkle_error) - } - - fn apply_mutations_with_reversion( - &mut self, - set: MutationSet, - ) -> Result, Self::Error> { - LargeSmt::apply_mutations_with_reversion(self, set).map_err(large_smt_error_to_merkle_error) - } - - fn compute_mutations( - &self, - updates: Vec<(Word, Word)>, - ) -> Result, Self::Error> { - LargeSmt::compute_mutations(self, updates).map_err(large_smt_error_to_merkle_error) - } - - fn insert(&mut self, key: Word, value: Word) -> Result { - LargeSmt::insert(self, key, value) - } - - fn get_value(&self, key: &Word) -> Word { - LargeSmt::get_value(self, key) - } - - fn get_leaf(&self, key: &Word) -> SmtLeaf { - LargeSmt::get_leaf(self, key) - } - - fn root(&self) -> Word { - LargeSmt::root(self).map_err(large_smt_error_to_merkle_error).unwrap() - } -} +// ACCOUNT TREE +// ================================================================================================ /// The sparse merkle tree of all accounts in the blockchain. /// @@ -276,13 +114,13 @@ where }, SmtLeaf::Single((key, _)) => { // Single entry is good - verify it's a valid account ID - Self::smt_key_to_id(key); + smt_key_to_account_id(key); }, SmtLeaf::Multiple(entries) => { // Multiple entries means duplicate prefixes // Extract one of the keys to identify the duplicate prefix if let Some((key, _)) = entries.first() { - let account_id = Self::smt_key_to_id(*key); + let account_id = smt_key_to_account_id(*key); return Err(AccountTreeError::DuplicateIdPrefix { duplicate_prefix: account_id.prefix(), }); @@ -318,9 +156,9 @@ where /// /// # Panics /// - /// Panics if the SMT backend fails to open the leaf (only possible with [`LargeSmt`] backend). + /// Panics if the SMT backend fails to open the leaf (only possible with `LargeSmt` backend). pub fn open(&self, account_id: AccountId) -> AccountWitness { - let key = Self::id_to_smt_key(account_id); + let key = account_id_to_smt_key(account_id); let proof = self.smt.open(&key); AccountWitness::from_smt_proof(account_id, proof) @@ -328,7 +166,7 @@ where /// Returns the current state commitment of the given account ID. pub fn get(&self, account_id: AccountId) -> Word { - let key = Self::id_to_smt_key(account_id); + let key = account_id_to_smt_key(account_id); self.smt.get_value(&key) } @@ -396,7 +234,7 @@ where .compute_mutations(Vec::from_iter( account_commitments .into_iter() - .map(|(id, commitment)| (Self::id_to_smt_key(id), commitment)), + .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), )) .map_err(AccountTreeError::ComputeMutations)?; @@ -410,7 +248,7 @@ where // valid. If it does not match, then we would insert a duplicate. if existing_key != *id_key { return Err(AccountTreeError::DuplicateIdPrefix { - duplicate_prefix: Self::smt_key_to_id(*id_key).prefix(), + duplicate_prefix: smt_key_to_account_id(*id_key).prefix(), }); } }, @@ -443,7 +281,7 @@ where account_id: AccountId, state_commitment: Word, ) -> Result { - let key = Self::id_to_smt_key(account_id); + let key = account_id_to_smt_key(account_id); // SAFETY: account tree should not contain multi-entry leaves and so the maximum number // of entries per leaf should never be exceeded. let prev_value = self.smt.insert(key, state_commitment) @@ -498,17 +336,6 @@ where // HELPERS // -------------------------------------------------------------------------------------------- - /// Returns the SMT key of the given account ID. - pub(super) fn id_to_smt_key(account_id: AccountId) -> Word { - // We construct this in such a way that we're forced to use the constants, so that when - // they're updated, the other usages of the constants are also updated. - let mut key = Word::empty(); - key[Self::KEY_SUFFIX_IDX] = account_id.suffix(); - key[Self::KEY_PREFIX_IDX] = account_id.prefix().as_felt(); - - key - } - /// Returns the SMT key of the given account ID prefix. fn id_prefix_to_smt_key(account_id: AccountIdPrefix) -> Word { // We construct this in such a way that we're forced to use the constants, so that when @@ -518,63 +345,6 @@ where key } - - /// Returns the [`AccountId`] recovered from the given SMT key. - /// - /// # Panics - /// - /// Panics if: - /// - the key is not a valid account ID. This should not happen when used on keys from (partial) - /// account tree. - pub(super) fn smt_key_to_id(key: Word) -> AccountId { - AccountId::try_from([key[Self::KEY_PREFIX_IDX], key[Self::KEY_SUFFIX_IDX]]) - .expect("account tree should only contain valid IDs") - } -} - -// CONVENIENCE METHODS -// ================================================================================================ - -impl AccountTree { - /// Creates a new [`AccountTree`] with the provided entries. - /// - /// This is a convenience method for testing that creates an SMT backend with the provided - /// entries and wraps it in an AccountTree. It validates that the entries don't contain - /// duplicate prefixes. - /// - /// # Errors - /// - /// Returns an error if: - /// - The provided entries contain duplicate account ID prefixes - /// - The backend fails to create the SMT with the entries - pub fn with_entries( - entries: impl IntoIterator, - ) -> Result - where - I: ExactSizeIterator, - { - // Create the SMT with the entries - let smt = Smt::with_entries( - entries - .into_iter() - .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), - ) - .map_err(|err| { - let MerkleError::DuplicateValuesForIndex(leaf_idx) = err else { - unreachable!("the only error returned by Smt::with_entries is of this type"); - }; - - // SAFETY: Since we only inserted account IDs into the SMT, it is guaranteed that - // the leaf_idx is a valid Felt as well as a valid account ID prefix. - AccountTreeError::DuplicateStateCommitments { - prefix: AccountIdPrefix::new_unchecked( - crate::Felt::try_from(leaf_idx).expect("leaf index should be a valid felt"), - ), - } - })?; - - AccountTree::new(smt) - } } // SERIALIZATION @@ -816,7 +586,7 @@ pub(super) mod tests { #[cfg(feature = "std")] #[test] fn large_smt_backend_basic_operations() { - use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; // Create test data let id0 = AccountIdBuilder::new().build_with_seed([5; 32]); @@ -862,7 +632,7 @@ pub(super) mod tests { #[cfg(feature = "std")] #[test] fn large_smt_backend_duplicate_prefix_check() { - use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; let [(id0, commitment0), (id1, commitment1)] = setup_duplicate_prefix_ids(); @@ -883,7 +653,7 @@ pub(super) mod tests { #[cfg(feature = "std")] #[test] fn large_smt_backend_apply_mutations() { - use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; let id0 = AccountIdBuilder::new().build_with_seed([5; 32]); let id1 = AccountIdBuilder::new().build_with_seed([6; 32]); @@ -916,7 +686,7 @@ pub(super) mod tests { #[cfg(feature = "std")] #[test] fn large_smt_backend_same_root_as_regular_smt() { - use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; let id0 = AccountIdBuilder::new().build_with_seed([5; 32]); let id1 = AccountIdBuilder::new().build_with_seed([6; 32]); diff --git a/crates/miden-objects/src/block/partial_account_tree.rs b/crates/miden-protocol/src/block/account_tree/partial.rs similarity index 98% rename from crates/miden-objects/src/block/partial_account_tree.rs rename to crates/miden-protocol/src/block/account_tree/partial.rs index 34cfd20c01..af84f725ae 100644 --- a/crates/miden-objects/src/block/partial_account_tree.rs +++ b/crates/miden-protocol/src/block/account_tree/partial.rs @@ -1,10 +1,8 @@ -use miden_crypto::merkle::SmtLeaf; +use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf}; +use super::{AccountWitness, account_id_to_smt_key}; use crate::Word; use crate::account::AccountId; -use crate::block::AccountWitness; -use crate::block::account_tree::account_id_to_smt_key; -use crate::crypto::merkle::PartialSmt; use crate::errors::AccountTreeError; /// The partial sparse merkle tree containing the state commitments of accounts in the chain. @@ -192,7 +190,7 @@ impl PartialAccountTree { #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_crypto::merkle::Smt; + use miden_crypto::merkle::smt::Smt; use super::*; use crate::block::account_tree::AccountTree; diff --git a/crates/miden-objects/src/block/account_witness.rs b/crates/miden-protocol/src/block/account_tree/witness.rs similarity index 93% rename from crates/miden-objects/src/block/account_witness.rs rename to crates/miden-protocol/src/block/account_tree/witness.rs index ca654f90cb..9c5c81d745 100644 --- a/crates/miden-objects/src/block/account_witness.rs +++ b/crates/miden-protocol/src/block/account_tree/witness.rs @@ -1,28 +1,21 @@ use alloc::string::ToString; -use miden_crypto::merkle::{ - InnerNodeInfo, - LeafIndex, - SMT_DEPTH, - SmtLeaf, - SmtProof, - SmtProofError, - SparseMerklePath, -}; +use miden_crypto::merkle::smt::{LeafIndex, SMT_DEPTH, SmtLeaf, SmtProof, SmtProofError}; +use miden_crypto::merkle::{InnerNodeInfo, SparseMerklePath}; +use crate::Word; use crate::account::AccountId; use crate::block::account_tree::{account_id_to_smt_key, smt_key_to_account_id}; +use crate::errors::AccountTreeError; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{AccountTreeError, Word}; // ACCOUNT WITNESS // ================================================================================================ -/// A specialized version of an [`SmtProof`] for use in -/// [`AccountTree`](super::account_tree::AccountTree) and -/// [`PartialAccountTree`](crate::block::PartialAccountTree). It proves the inclusion of an account +/// A specialized version of an [`SmtProof`] for use in [`AccountTree`](super::AccountTree) and +/// [`PartialAccountTree`](super::PartialAccountTree). It proves the inclusion of an account /// ID at a certain state (i.e. [`Account::commitment`](crate::account::Account::commitment)) in the -/// [`AccountTree`](super::account_tree::AccountTree). +/// [`AccountTree`](super::AccountTree). /// /// By construction the witness can only represent the equivalent of an [`SmtLeaf`] with zero or one /// entries, which guarantees that the account ID prefix it represents is unique in the tree. diff --git a/crates/miden-objects/src/block/account_update_witness.rs b/crates/miden-protocol/src/block/account_update_witness.rs similarity index 98% rename from crates/miden-objects/src/block/account_update_witness.rs rename to crates/miden-protocol/src/block/account_update_witness.rs index 9d8c9a485c..359784d3b7 100644 --- a/crates/miden-objects/src/block/account_update_witness.rs +++ b/crates/miden-protocol/src/block/account_update_witness.rs @@ -1,6 +1,6 @@ use crate::Word; use crate::account::delta::AccountUpdateDetails; -use crate::block::AccountWitness; +use crate::block::account_tree::AccountWitness; use crate::utils::serde::{ ByteReader, ByteWriter, diff --git a/crates/miden-objects/src/block/block_account_update.rs b/crates/miden-protocol/src/block/block_account_update.rs similarity index 100% rename from crates/miden-objects/src/block/block_account_update.rs rename to crates/miden-protocol/src/block/block_account_update.rs diff --git a/crates/miden-protocol/src/block/block_body.rs b/crates/miden-protocol/src/block/block_body.rs new file mode 100644 index 0000000000..53b86741cb --- /dev/null +++ b/crates/miden-protocol/src/block/block_body.rs @@ -0,0 +1,202 @@ +use alloc::vec::Vec; + +use miden_core::Word; +use miden_core::utils::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; + +use crate::block::{ + BlockAccountUpdate, + BlockNoteIndex, + BlockNoteTree, + OutputNoteBatch, + ProposedBlock, +}; +use crate::note::Nullifier; +use crate::transaction::{OrderedTransactionHeaders, OutputNote}; + +// BLOCK BODY +// ================================================================================================ + +/// Body of a block in the chain which contains data pertaining to all relevant state changes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockBody { + /// Account updates for the block. + updated_accounts: Vec, + + /// Note batches created by the transactions in this block. + output_note_batches: Vec, + + /// Nullifiers created by the transactions in this block through the consumption of notes. + created_nullifiers: Vec, + + /// The aggregated and flattened transaction headers of all batches in the order in which they + /// appeared in the proposed block. + transactions: OrderedTransactionHeaders, +} + +impl BlockBody { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`BlockBody`] without performing any validation. + /// + /// # Warning + /// + /// This does not validate any of the guarantees of this type. It should only be used internally + /// (in miden-lib) or in tests. + pub fn new_unchecked( + updated_accounts: Vec, + output_note_batches: Vec, + created_nullifiers: Vec, + transactions: OrderedTransactionHeaders, + ) -> Self { + Self { + updated_accounts, + output_note_batches, + created_nullifiers, + transactions, + } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the slice of [`BlockAccountUpdate`]s for all accounts updated in the block. + pub fn updated_accounts(&self) -> &[BlockAccountUpdate] { + &self.updated_accounts + } + + /// Returns the slice of [`OutputNoteBatch`]es for all output notes created in the block. + pub fn output_note_batches(&self) -> &[OutputNoteBatch] { + &self.output_note_batches + } + + /// Returns a reference to the slice of nullifiers for all notes consumed in the block. + pub fn created_nullifiers(&self) -> &[Nullifier] { + &self.created_nullifiers + } + + /// Returns the [`OrderedTransactionHeaders`] of all transactions included in this block. + pub fn transactions(&self) -> &OrderedTransactionHeaders { + &self.transactions + } + + /// Returns the commitment of all transactions included in this block. + pub fn transaction_commitment(&self) -> Word { + self.transactions.commitment() + } + + /// Returns an iterator over all [`OutputNote`]s created in this block. + /// + /// Each note is accompanied by a corresponding index specifying where the note is located + /// in the block's [`BlockNoteTree`]. + pub fn output_notes(&self) -> impl Iterator { + self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| { + notes.iter().map(move |(note_idx_in_batch, note)| { + ( + // SAFETY: The block body contains at most the max allowed number of + // batches and each batch is guaranteed to contain + // at most the max allowed number of output notes. + BlockNoteIndex::new(batch_idx, *note_idx_in_batch) + .expect("max batches in block and max notes in batches should be enforced"), + note, + ) + }) + }) + } + + /// Computes the [`BlockNoteTree`] containing all [`OutputNote`]s created in this block. + pub fn compute_block_note_tree(&self) -> BlockNoteTree { + let entries = self + .output_notes() + .map(|(note_index, note)| (note_index, note.id(), note.metadata())); + + // SAFETY: We only construct block bodies that: + // - do not contain duplicates + // - contain at most the max allowed number of batches and each batch is guaranteed to + // contain at most the max allowed number of output notes. + BlockNoteTree::with_entries(entries) + .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum") + } + + // DESTRUCTURING + // -------------------------------------------------------------------------------------------- + + /// Consumes the block body and returns its parts. + pub fn into_parts( + self, + ) -> ( + Vec, + Vec, + Vec, + OrderedTransactionHeaders, + ) { + ( + self.updated_accounts, + self.output_note_batches, + self.created_nullifiers, + self.transactions, + ) + } +} + +impl From for BlockBody { + fn from(block: ProposedBlock) -> Self { + // Split the proposed block into its constituent parts. + let (batches, account_updated_witnesses, output_note_batches, created_nullifiers, ..) = + block.into_parts(); + + // Transform the account update witnesses into block account updates. + let updated_accounts = account_updated_witnesses + .into_iter() + .map(|(account_id, update_witness)| { + let ( + _initial_state_commitment, + final_state_commitment, + // Note that compute_account_root took out this value so it should not be used. + _initial_state_proof, + details, + ) = update_witness.into_parts(); + BlockAccountUpdate::new(account_id, final_state_commitment, details) + }) + .collect(); + let created_nullifiers = created_nullifiers.keys().copied().collect::>(); + // Aggregate the verified transactions of all batches. + let transactions = batches.into_transactions(); + Self { + updated_accounts, + output_note_batches, + created_nullifiers, + transactions, + } + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for BlockBody { + fn write_into(&self, target: &mut W) { + self.updated_accounts.write_into(target); + self.output_note_batches.write_into(target); + self.created_nullifiers.write_into(target); + self.transactions.write_into(target); + } +} + +impl Deserializable for BlockBody { + fn read_from(source: &mut R) -> Result { + let block = Self { + updated_accounts: Vec::read_from(source)?, + output_note_batches: Vec::read_from(source)?, + created_nullifiers: Vec::read_from(source)?, + transactions: OrderedTransactionHeaders::read_from(source)?, + }; + Ok(block) + } +} diff --git a/crates/miden-objects/src/block/block_inputs.rs b/crates/miden-protocol/src/block/block_inputs.rs similarity index 76% rename from crates/miden-objects/src/block/block_inputs.rs rename to crates/miden-protocol/src/block/block_inputs.rs index f21bd95006..e67f4e0bba 100644 --- a/crates/miden-objects/src/block/block_inputs.rs +++ b/crates/miden-protocol/src/block/block_inputs.rs @@ -1,9 +1,12 @@ use alloc::collections::BTreeMap; use crate::account::AccountId; -use crate::block::{AccountWitness, BlockHeader, NullifierWitness}; +use crate::block::BlockHeader; +use crate::block::account_tree::AccountWitness; +use crate::block::nullifier_tree::NullifierWitness; use crate::note::{NoteId, NoteInclusionProof, Nullifier}; use crate::transaction::PartialBlockchain; +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; // BLOCK INPUTS // ================================================================================================ @@ -128,3 +131,35 @@ impl BlockInputs { &mut self.account_witnesses } } + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for BlockInputs { + fn write_into(&self, target: &mut W) { + self.prev_block_header.write_into(target); + self.partial_blockchain.write_into(target); + self.account_witnesses.write_into(target); + self.nullifier_witnesses.write_into(target); + self.unauthenticated_note_proofs.write_into(target); + } +} + +impl Deserializable for BlockInputs { + fn read_from(source: &mut R) -> Result { + let prev_block_header = BlockHeader::read_from(source)?; + let partial_blockchain = PartialBlockchain::read_from(source)?; + let account_witnesses = BTreeMap::::read_from(source)?; + let nullifier_witnesses = BTreeMap::::read_from(source)?; + let unauthenticated_note_proofs = + BTreeMap::::read_from(source)?; + + Ok(Self::new( + prev_block_header, + partial_blockchain, + account_witnesses, + nullifier_witnesses, + unauthenticated_note_proofs, + )) + } +} diff --git a/crates/miden-objects/src/block/block_number.rs b/crates/miden-protocol/src/block/block_number.rs similarity index 100% rename from crates/miden-objects/src/block/block_number.rs rename to crates/miden-protocol/src/block/block_number.rs diff --git a/crates/miden-protocol/src/block/block_proof.rs b/crates/miden-protocol/src/block/block_proof.rs new file mode 100644 index 0000000000..710a77cb03 --- /dev/null +++ b/crates/miden-protocol/src/block/block_proof.rs @@ -0,0 +1,33 @@ +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +/// Represents a proof of a block in the chain. +/// +/// NOTE: Block proving is not yet implemented. This is a placeholder struct. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockProof {} + +impl BlockProof { + /// Creates a dummy `BlockProof` for testing purposes only. + #[cfg(any(test, feature = "testing"))] + pub fn new_dummy() -> Self { + Self {} + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for BlockProof { + fn write_into(&self, _target: &mut W) { + // TODO: Implement serialization for BlockProof when fields exist. + } +} + +impl Deserializable for BlockProof { + fn read_from(_source: &mut R) -> Result { + // TODO: Implement deserialization for BlockProof when fields exist. + let block = Self {}; + + Ok(block) + } +} diff --git a/crates/miden-objects/src/block/blockchain.rs b/crates/miden-protocol/src/block/blockchain.rs similarity index 98% rename from crates/miden-objects/src/block/blockchain.rs rename to crates/miden-protocol/src/block/blockchain.rs index 58046a630a..a70159fbd0 100644 --- a/crates/miden-objects/src/block/blockchain.rs +++ b/crates/miden-protocol/src/block/blockchain.rs @@ -1,7 +1,7 @@ use alloc::collections::BTreeSet; use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_crypto::merkle::{Forest, Mmr, MmrError, MmrPeaks, MmrProof, PartialMmr}; +use miden_crypto::merkle::mmr::{Forest, Mmr, MmrError, MmrPeaks, MmrProof, PartialMmr}; use miden_processor::DeserializationError; use crate::Word; diff --git a/crates/miden-objects/src/block/header.rs b/crates/miden-protocol/src/block/header.rs similarity index 86% rename from crates/miden-objects/src/block/header.rs rename to crates/miden-protocol/src/block/header.rs index 9c8f28b15d..33b5f0f8dd 100644 --- a/crates/miden-objects/src/block/header.rs +++ b/crates/miden-protocol/src/block/header.rs @@ -3,6 +3,8 @@ use alloc::vec::Vec; use crate::account::{AccountId, AccountType}; use crate::block::BlockNumber; +use crate::crypto::dsa::ecdsa_k256_keccak::PublicKey; +use crate::errors::FeeError; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -10,10 +12,13 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{FeeError, Felt, Hasher, Word, ZERO}; +use crate::{Felt, Hasher, Word, ZERO}; -/// The header of a block. It contains metadata about the block, commitments to the current -/// state of the chain and the hash of the proof that attests to the integrity of the chain. +// BLOCK HEADER +// ================================================================================================ + +/// The header of a block. It contains metadata about the block, commitments to the current state of +/// the chain and the hash of the proof that attests to the integrity of the chain. /// /// A block header includes the following fields: /// @@ -27,8 +32,7 @@ use crate::{FeeError, Felt, Hasher, Word, ZERO}; /// - `tx_commitment` is a commitment to the set of transaction IDs which affected accounts in the /// block. /// - `tx_kernel_commitment` a commitment to all transaction kernels supported by this block. -/// - `proof_commitment` is the commitment of the block's STARK proof attesting to the correct state -/// transition. +/// - `validator_key` is the public key of the validator that is expected to sign the block. /// - `fee_parameters` are the parameters defining the base fees and the native asset, see /// [`FeeParameters`] for more details. /// - `timestamp` is the time when the block was created, in seconds since UNIX epoch. Current @@ -46,7 +50,7 @@ pub struct BlockHeader { note_root: Word, tx_commitment: Word, tx_kernel_commitment: Word, - proof_commitment: Word, + validator_key: PublicKey, fee_parameters: FeeParameters, timestamp: u32, sub_commitment: Word, @@ -66,11 +70,11 @@ impl BlockHeader { note_root: Word, tx_commitment: Word, tx_kernel_commitment: Word, - proof_commitment: Word, + validator_key: PublicKey, fee_parameters: FeeParameters, timestamp: u32, ) -> Self { - // compute block sub commitment + // Compute block sub commitment. let sub_commitment = Self::compute_sub_commitment( version, prev_block_commitment, @@ -79,7 +83,7 @@ impl BlockHeader { nullifier_root, tx_commitment, tx_kernel_commitment, - proof_commitment, + &validator_key, &fee_parameters, timestamp, block_num, @@ -101,7 +105,7 @@ impl BlockHeader { note_root, tx_commitment, tx_kernel_commitment, - proof_commitment, + validator_key, fee_parameters, timestamp, sub_commitment, @@ -169,6 +173,11 @@ impl BlockHeader { self.note_root } + /// Returns the public key of the block's validator. + pub fn validator_key(&self) -> &PublicKey { + &self.validator_key + } + /// Returns the commitment to all transactions in this block. /// /// The commitment is computed as sequential hash of (`transaction_id`, `account_id`) tuples. @@ -186,11 +195,6 @@ impl BlockHeader { self.tx_kernel_commitment } - /// Returns the proof commitment. - pub fn proof_commitment(&self) -> Word { - self.proof_commitment - } - /// Returns a reference to the [`FeeParameters`] in this header. pub fn fee_parameters(&self) -> &FeeParameters { &self.fee_parameters @@ -213,7 +217,7 @@ impl BlockHeader { /// /// The sub commitment is computed as a sequential hash of the following fields: /// `prev_block_commitment`, `chain_commitment`, `account_root`, `nullifier_root`, `note_root`, - /// `tx_commitment`, `tx_kernel_commitment`, `proof_commitment`, `version`, `timestamp`, + /// `tx_commitment`, `tx_kernel_commitment`, `validator_key_commitment`, `version`, `timestamp`, /// `block_num`, `native_asset_id`, `verification_base_fee` (all fields except the `note_root`). #[allow(clippy::too_many_arguments)] fn compute_sub_commitment( @@ -224,7 +228,7 @@ impl BlockHeader { nullifier_root: Word, tx_commitment: Word, tx_kernel_commitment: Word, - proof_commitment: Word, + validator_key: &PublicKey, fee_parameters: &FeeParameters, timestamp: u32, block_num: BlockNumber, @@ -236,7 +240,7 @@ impl BlockHeader { elements.extend_from_slice(nullifier_root.as_elements()); elements.extend_from_slice(tx_commitment.as_elements()); elements.extend_from_slice(tx_kernel_commitment.as_elements()); - elements.extend_from_slice(proof_commitment.as_elements()); + elements.extend(validator_key.to_commitment()); elements.extend([block_num.into(), version.into(), timestamp.into(), ZERO]); elements.extend([ fee_parameters.native_asset_id().suffix(), @@ -254,18 +258,36 @@ impl BlockHeader { impl Serializable for BlockHeader { fn write_into(&self, target: &mut W) { - self.version.write_into(target); - self.prev_block_commitment.write_into(target); - self.block_num.write_into(target); - self.chain_commitment.write_into(target); - self.account_root.write_into(target); - self.nullifier_root.write_into(target); - self.note_root.write_into(target); - self.tx_commitment.write_into(target); - self.tx_kernel_commitment.write_into(target); - self.proof_commitment.write_into(target); - self.fee_parameters.write_into(target); - self.timestamp.write_into(target); + let Self { + version, + prev_block_commitment, + block_num, + chain_commitment, + account_root, + nullifier_root, + note_root, + tx_commitment, + tx_kernel_commitment, + validator_key, + fee_parameters, + timestamp, + // Don't serialize sub commitment and commitment as they can be derived. + sub_commitment: _, + commitment: _, + } = self; + + version.write_into(target); + prev_block_commitment.write_into(target); + block_num.write_into(target); + chain_commitment.write_into(target); + account_root.write_into(target); + nullifier_root.write_into(target); + note_root.write_into(target); + tx_commitment.write_into(target); + tx_kernel_commitment.write_into(target); + validator_key.write_into(target); + fee_parameters.write_into(target); + timestamp.write_into(target); } } @@ -280,7 +302,7 @@ impl Deserializable for BlockHeader { let note_root = source.read()?; let tx_commitment = source.read()?; let tx_kernel_commitment = source.read()?; - let proof_commitment = source.read()?; + let validator_key = source.read()?; let fee_parameters = source.read()?; let timestamp = source.read()?; @@ -294,7 +316,7 @@ impl Deserializable for BlockHeader { note_root, tx_commitment, tx_kernel_commitment, - proof_commitment, + validator_key, fee_parameters, timestamp, )) @@ -370,6 +392,7 @@ impl Deserializable for FeeParameters { .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } + // TESTS // ================================================================================================ diff --git a/crates/miden-objects/src/block/mod.rs b/crates/miden-protocol/src/block/mod.rs similarity index 75% rename from crates/miden-objects/src/block/mod.rs rename to crates/miden-protocol/src/block/mod.rs index 083551363f..e829aacdb6 100644 --- a/crates/miden-objects/src/block/mod.rs +++ b/crates/miden-protocol/src/block/mod.rs @@ -1,38 +1,30 @@ mod header; pub use header::{BlockHeader, FeeParameters}; +mod block_body; +pub use block_body::BlockBody; + mod block_number; pub use block_number::BlockNumber; +mod block_proof; +pub use block_proof::BlockProof; + mod proposed_block; pub use proposed_block::ProposedBlock; mod proven_block; pub use proven_block::ProvenBlock; -mod nullifier_witness; -pub use nullifier_witness::NullifierWitness; - -mod partial_account_tree; -pub use partial_account_tree::PartialAccountTree; - pub mod account_tree; - -mod nullifier_tree; -pub use nullifier_tree::NullifierTree; +pub mod nullifier_tree; mod blockchain; pub use blockchain::Blockchain; -mod partial_nullifier_tree; -pub use partial_nullifier_tree::PartialNullifierTree; - mod block_account_update; pub use block_account_update::BlockAccountUpdate; -mod account_witness; -pub use account_witness::AccountWitness; - mod account_update_witness; pub use account_update_witness::AccountUpdateWitness; @@ -42,6 +34,9 @@ pub use block_inputs::BlockInputs; mod note_tree; pub use note_tree::{BlockNoteIndex, BlockNoteTree}; +mod signer; +pub use signer::BlockSigner; + /// The set of notes created in a transaction batch with their index in the batch. /// /// The index is included as some notes may be erased at the block level that were part of the diff --git a/crates/miden-objects/src/block/note_tree.rs b/crates/miden-protocol/src/block/note_tree.rs similarity index 96% rename from crates/miden-objects/src/block/note_tree.rs rename to crates/miden-protocol/src/block/note_tree.rs index 5e6ee6d215..497aab12ba 100644 --- a/crates/miden-objects/src/block/note_tree.rs +++ b/crates/miden-protocol/src/block/note_tree.rs @@ -3,7 +3,8 @@ use alloc::string::ToString; use miden_crypto::merkle::SparseMerklePath; use crate::batch::BatchNoteTree; -use crate::crypto::merkle::{LeafIndex, MerkleError, SimpleSmt}; +use crate::crypto::merkle::MerkleError; +use crate::crypto::merkle::smt::{LeafIndex, SimpleSmt}; use crate::note::{NoteId, NoteMetadata, compute_note_commitment}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; use crate::{ @@ -28,18 +29,18 @@ impl BlockNoteTree { /// /// Entry format: (note_index, note_id, note_metadata). /// - /// Value of each leaf is computed as: `hash(note_id || note_metadata)`. + /// Value of each leaf is computed as: `hash(note_id || note_metadata_commitment)`. /// All leaves omitted from the entries list are set to [crate::EMPTY_WORD]. /// /// # Errors /// Returns an error if: /// - The number of entries exceeds the maximum notes tree capacity, that is 2^16. /// - The provided entries contain multiple values for the same key. - pub fn with_entries( - entries: impl IntoIterator, + pub fn with_entries<'metadata>( + entries: impl IntoIterator, ) -> Result { let leaves = entries.into_iter().map(|(index, note_id, metadata)| { - (index.leaf_index_value() as u64, compute_note_commitment(note_id, &metadata)) + (index.leaf_index_value() as u64, compute_note_commitment(note_id, metadata)) }); SimpleSmt::with_leaves(leaves).map(Self) @@ -176,7 +177,7 @@ impl Deserializable for BlockNoteTree { #[cfg(test)] mod tests { - use miden_crypto::merkle::SimpleSmt; + use miden_crypto::merkle::smt::SimpleSmt; use miden_crypto::utils::{Deserializable, Serializable}; use super::BlockNoteTree; diff --git a/crates/miden-protocol/src/block/nullifier_tree/backend.rs b/crates/miden-protocol/src/block/nullifier_tree/backend.rs new file mode 100644 index 0000000000..603258ea0a --- /dev/null +++ b/crates/miden-protocol/src/block/nullifier_tree/backend.rs @@ -0,0 +1,235 @@ +use alloc::boxed::Box; + +use super::{BlockNumber, Nullifier, NullifierBlock, NullifierTree, NullifierTreeError}; +use crate::Word; +use crate::crypto::merkle::MerkleError; +#[cfg(feature = "std")] +use crate::crypto::merkle::smt::{LargeSmt, LargeSmtError, SmtStorage}; +use crate::crypto::merkle::smt::{MutationSet, SMT_DEPTH, Smt, SmtProof}; + +// NULLIFIER TREE BACKEND +// ================================================================================================ + +/// This trait abstracts over different SMT backends (e.g., `Smt` and `LargeSmt`) to allow +/// the `NullifierTree` to work with either implementation transparently. +/// +/// Users should instantiate the backend directly (potentially with entries) and then +/// pass it to [`NullifierTree::new_unchecked`]. +/// +/// # Invariants +/// +/// Assumes the provided SMT upholds the guarantees of the [`NullifierTree`]. Specifically: +/// - Nullifiers are only spent once and their block numbers do not change. +/// - Nullifier leaf values must be valid according to [`NullifierBlock`]. +pub trait NullifierTreeBackend: Sized { + type Error: core::error::Error + Send + 'static; + + /// Returns the number of entries in the SMT. + fn num_entries(&self) -> usize; + + /// Returns all entries in the SMT as an iterator over key-value pairs. + fn entries(&self) -> Box + '_>; + + /// Opens the leaf at the given key, returning a Merkle proof. + fn open(&self, key: &Word) -> SmtProof; + + /// Applies the given mutation set to the SMT. + fn apply_mutations( + &mut self, + set: MutationSet, + ) -> Result<(), Self::Error>; + + /// Computes the mutation set required to apply the given updates to the SMT. + fn compute_mutations( + &self, + updates: impl IntoIterator, + ) -> Result, Self::Error>; + + /// Inserts a key-value pair into the SMT, returning the previous value at that key. + fn insert(&mut self, key: Word, value: NullifierBlock) -> Result; + + /// Returns the value associated with the given key. + fn get_value(&self, key: &Word) -> NullifierBlock; + + /// Returns the root of the SMT. + fn root(&self) -> Word; +} + +// BACKEND IMPLEMENTATION FOR SMT +// ================================================================================================ + +impl NullifierTreeBackend for Smt { + type Error = MerkleError; + + fn num_entries(&self) -> usize { + Smt::num_entries(self) + } + + fn entries(&self) -> Box + '_> { + Box::new(Smt::entries(self).map(|(k, v)| (*k, *v))) + } + + fn open(&self, key: &Word) -> SmtProof { + Smt::open(self, key) + } + + fn apply_mutations( + &mut self, + set: MutationSet, + ) -> Result<(), Self::Error> { + Smt::apply_mutations(self, set) + } + + fn compute_mutations( + &self, + updates: impl IntoIterator, + ) -> Result, Self::Error> { + Smt::compute_mutations(self, updates) + } + + fn insert(&mut self, key: Word, value: NullifierBlock) -> Result { + Smt::insert(self, key, value.into()).map(|word| { + NullifierBlock::try_from(word).expect("SMT should only store valid NullifierBlocks") + }) + } + + fn get_value(&self, key: &Word) -> NullifierBlock { + NullifierBlock::new(Smt::get_value(self, key)) + .expect("SMT should only store valid NullifierBlocks") + } + + fn root(&self) -> Word { + Smt::root(self) + } +} + +// NULLIFIER TREE BACKEND FOR LARGE SMT +// ================================================================================================ + +#[cfg(feature = "std")] +impl NullifierTreeBackend for LargeSmt +where + Backend: SmtStorage, +{ + type Error = MerkleError; + + fn num_entries(&self) -> usize { + // SAFETY: We panic on storage errors here as they represent unrecoverable I/O failures. + // This maintains API compatibility with the non-fallible Smt::num_entries(). + // See issue #2010 for future improvements to error handling. + LargeSmt::num_entries(self) + .map_err(large_smt_error_to_merkle_error) + .expect("Storage I/O error accessing num_entries") + } + + fn entries(&self) -> Box + '_> { + // SAFETY: We expect here as only I/O errors can occur. Storage failures are considered + // unrecoverable at this layer. See issue #2010 for future error handling improvements. + Box::new(LargeSmt::entries(self).expect("Storage I/O error accessing entries")) + } + + fn open(&self, key: &Word) -> SmtProof { + LargeSmt::open(self, key) + } + + fn apply_mutations( + &mut self, + set: MutationSet, + ) -> Result<(), Self::Error> { + LargeSmt::apply_mutations(self, set).map_err(large_smt_error_to_merkle_error) + } + + fn compute_mutations( + &self, + updates: impl IntoIterator, + ) -> Result, Self::Error> { + LargeSmt::compute_mutations(self, updates).map_err(large_smt_error_to_merkle_error) + } + + fn insert(&mut self, key: Word, value: NullifierBlock) -> Result { + LargeSmt::insert(self, key, value.into()).map(|word| { + NullifierBlock::try_from(word).expect("SMT should only store valid NullifierBlocks") + }) + } + + fn get_value(&self, key: &Word) -> NullifierBlock { + LargeSmt::get_value(self, key) + .try_into() + .expect("unable to create NullifierBlock") + } + + fn root(&self) -> Word { + LargeSmt::root(self) + } +} + +// CONVENIENCE METHODS +// ================================================================================================ + +impl NullifierTree { + /// Creates a new nullifier tree from the provided entries. + /// + /// This is a convenience method that creates an SMT backend with the provided entries and + /// wraps it in a NullifierTree. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided entries contain multiple block numbers for the same nullifier. + pub fn with_entries( + entries: impl IntoIterator, + ) -> Result { + let leaves = entries.into_iter().map(|(nullifier, block_num)| { + (nullifier.as_word(), NullifierBlock::from(block_num).into()) + }); + + let smt = Smt::with_entries(leaves) + .map_err(NullifierTreeError::DuplicateNullifierBlockNumbers)?; + + Ok(Self::new_unchecked(smt)) + } +} + +#[cfg(feature = "std")] +impl NullifierTree> +where + Backend: SmtStorage, +{ + /// Creates a new nullifier tree from the provided entries using the given storage backend + /// + /// This is a convenience method that creates an SMT on the provided storage backend using the + /// provided entries and wraps it in a NullifierTree. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided entries contain multiple block numbers for the same nullifier. + /// - a storage error is encountered. + pub fn with_storage_from_entries( + storage: Backend, + entries: impl IntoIterator, + ) -> Result { + let leaves = entries.into_iter().map(|(nullifier, block_num)| { + (nullifier.as_word(), NullifierBlock::from(block_num).into()) + }); + + let smt = LargeSmt::::with_entries(storage, leaves) + .map_err(large_smt_error_to_merkle_error) + .map_err(NullifierTreeError::DuplicateNullifierBlockNumbers)?; + + Ok(Self::new_unchecked(smt)) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +#[cfg(feature = "std")] +pub(super) fn large_smt_error_to_merkle_error(err: LargeSmtError) -> MerkleError { + match err { + LargeSmtError::Storage(storage_err) => { + panic!("Storage error encountered: {:?}", storage_err) + }, + LargeSmtError::Merkle(merkle_err) => merkle_err, + } +} diff --git a/crates/miden-protocol/src/block/nullifier_tree/mod.rs b/crates/miden-protocol/src/block/nullifier_tree/mod.rs new file mode 100644 index 0000000000..18332812ea --- /dev/null +++ b/crates/miden-protocol/src/block/nullifier_tree/mod.rs @@ -0,0 +1,544 @@ +use alloc::string::ToString; +use alloc::vec::Vec; + +use crate::block::BlockNumber; +use crate::crypto::merkle::MerkleError; +use crate::crypto::merkle::smt::{MutationSet, SMT_DEPTH, Smt}; +use crate::errors::NullifierTreeError; +use crate::note::Nullifier; +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; +use crate::{Felt, FieldElement, Word}; + +mod backend; +pub use backend::NullifierTreeBackend; + +mod witness; +pub use witness::NullifierWitness; + +mod partial; +pub use partial::PartialNullifierTree; + +// NULLIFIER TREE +// ================================================================================================ + +/// The sparse merkle tree of all nullifiers in the blockchain. +/// +/// A nullifier can only ever be spent once and its value in the tree is the block number at which +/// it was spent. +/// +/// The tree guarantees that once a nullifier has been inserted into the tree, its block number does +/// not change. Note that inserting the nullifier multiple times with the same block number is +/// valid. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NullifierTree { + smt: Backend, +} + +impl Default for NullifierTree +where + Backend: Default, +{ + fn default() -> Self { + Self { smt: Default::default() } + } +} + +impl NullifierTree +where + Backend: NullifierTreeBackend, +{ + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The depth of the nullifier tree. + pub const DEPTH: u8 = SMT_DEPTH; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new `NullifierTree` from its inner representation. + /// + /// # Invariants + /// + /// See the documentation on [`NullifierTreeBackend`] trait documentation. + pub fn new_unchecked(backend: Backend) -> Self { + NullifierTree { smt: backend } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the root of the nullifier SMT. + pub fn root(&self) -> Word { + self.smt.root() + } + + /// Returns the number of spent nullifiers in this tree. + pub fn num_nullifiers(&self) -> usize { + self.smt.num_entries() + } + + /// Returns an iterator over the nullifiers and their block numbers in the tree. + pub fn entries(&self) -> impl Iterator { + self.smt.entries().map(|(nullifier, value)| { + ( + Nullifier::from_raw(nullifier), + NullifierBlock::new(value) + .expect("SMT should only store valid NullifierBlocks") + .into(), + ) + }) + } + + /// Returns a [`NullifierWitness`] of the leaf associated with the `nullifier`. + /// + /// Conceptually, such a witness is a Merkle path to the leaf, as well as the leaf itself. + /// + /// This witness is a proof of the current block number of the given nullifier. If that block + /// number is zero, it proves that the nullifier is unspent. + pub fn open(&self, nullifier: &Nullifier) -> NullifierWitness { + NullifierWitness::new(self.smt.open(&nullifier.as_word())) + } + + /// Returns the block number for the given nullifier or `None` if the nullifier wasn't spent + /// yet. + pub fn get_block_num(&self, nullifier: &Nullifier) -> Option { + let nullifier_block = self.smt.get_value(&nullifier.as_word()); + if nullifier_block.is_unspent() { + return None; + } + + Some(nullifier_block.into()) + } + + /// Computes a mutation set resulting from inserting the provided nullifiers into this nullifier + /// tree. + /// + /// # Errors + /// + /// Returns an error if: + /// - a nullifier in the provided iterator was already spent. + pub fn compute_mutations( + &self, + nullifiers: impl IntoIterator, + ) -> Result + where + I: Iterator + Clone, + { + let nullifiers = nullifiers.into_iter(); + for (nullifier, _) in nullifiers.clone() { + if self.get_block_num(&nullifier).is_some() { + return Err(NullifierTreeError::NullifierAlreadySpent(nullifier)); + } + } + + let mutation_set = self + .smt + .compute_mutations( + nullifiers + .into_iter() + .map(|(nullifier, block_num)| { + (nullifier.as_word(), NullifierBlock::from(block_num).into()) + }) + .collect::>(), + ) + .map_err(NullifierTreeError::ComputeMutations)?; + + Ok(NullifierMutationSet::new(mutation_set)) + } + + // PUBLIC MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Marks the given nullifier as spent at the given block number. + /// + /// # Errors + /// + /// Returns an error if: + /// - the nullifier was already spent. + pub fn mark_spent( + &mut self, + nullifier: Nullifier, + block_num: BlockNumber, + ) -> Result<(), NullifierTreeError> { + let prev_nullifier_value = self + .smt + .insert(nullifier.as_word(), NullifierBlock::from(block_num)) + .map_err(NullifierTreeError::MaxLeafEntriesExceeded)?; + + if prev_nullifier_value.is_spent() { + Err(NullifierTreeError::NullifierAlreadySpent(nullifier)) + } else { + Ok(()) + } + } + + /// Applies mutations to the nullifier tree. + /// + /// # Errors + /// + /// Returns an error if: + /// - `mutations` was computed on a tree with a different root than this one. + pub fn apply_mutations( + &mut self, + mutations: NullifierMutationSet, + ) -> Result<(), NullifierTreeError> { + self.smt + .apply_mutations(mutations.into_mutation_set()) + .map_err(NullifierTreeError::TreeRootConflict) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for NullifierTree { + fn write_into(&self, target: &mut W) { + self.entries().collect::>().write_into(target); + } +} + +impl Deserializable for NullifierTree { + fn read_from(source: &mut R) -> Result { + let entries = Vec::<(Nullifier, BlockNumber)>::read_from(source)?; + Self::with_entries(entries) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// NULLIFIER MUTATION SET +// ================================================================================================ + +/// A newtype wrapper around a [`MutationSet`] for use in the [`NullifierTree`]. +/// +/// It guarantees that applying the contained mutations will result in a nullifier tree where +/// nullifier's block numbers are not updated (except if they were unspent before), ensuring that +/// nullifiers are only spent once. +/// +/// It is returned by and used in methods on the [`NullifierTree`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NullifierMutationSet { + mutation_set: MutationSet, +} + +impl NullifierMutationSet { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NullifierMutationSet`] from the provided raw mutation set. + fn new(mutation_set: MutationSet) -> Self { + Self { mutation_set } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the underlying [`MutationSet`]. + pub fn as_mutation_set(&self) -> &MutationSet { + &self.mutation_set + } + + // PUBLIC MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Consumes self and returns the underlying [`MutationSet`]. + pub fn into_mutation_set(self) -> MutationSet { + self.mutation_set + } +} + +// NULLIFIER BLOCK +// ================================================================================================ + +/// The [`BlockNumber`] at which a [`Nullifier`] was consumed. +/// +/// Since there are no nullifiers in the genesis block the [`BlockNumber::GENESIS`] is used to +/// signal an unconsumed nullifier. +/// +/// This type can be converted to a [`Word`] which is laid out like this: +/// +/// ```text +/// [block_num, 0, 0, 0] +/// ``` +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct NullifierBlock(BlockNumber); + +impl NullifierBlock { + pub const UNSPENT: NullifierBlock = NullifierBlock(BlockNumber::GENESIS); + + /// Returns a new [NullifierBlock] constructed from the provided word. + /// + /// # Errors + /// Returns an error if: + /// - The 0th element in the word is not a valid [BlockNumber]. + /// - Any of the remaining elements is non-zero. + pub fn new(word: Word) -> Result { + let block_num = u32::try_from(word[0].as_int()) + .map(BlockNumber::from) + .map_err(|_| NullifierTreeError::InvalidNullifierBlockNumber(word))?; + + if word[1..4].iter().any(|felt| *felt != Felt::ZERO) { + return Err(NullifierTreeError::InvalidNullifierBlockNumber(word)); + } + + Ok(NullifierBlock(block_num)) + } + + /// Returns true if the nullifier has already been spent. + pub fn is_spent(&self) -> bool { + !self.is_unspent() + } + + /// Returns true if the nullifier has not yet been spent. + pub fn is_unspent(&self) -> bool { + self == &Self::UNSPENT + } +} + +impl From for NullifierBlock { + fn from(block_num: BlockNumber) -> Self { + Self(block_num) + } +} + +impl From for BlockNumber { + fn from(value: NullifierBlock) -> BlockNumber { + value.0 + } +} + +impl From for Word { + fn from(value: NullifierBlock) -> Word { + Word::from([Felt::from(value.0), Felt::ZERO, Felt::ZERO, Felt::ZERO]) + } +} + +impl TryFrom for NullifierBlock { + type Error = NullifierTreeError; + + fn try_from(value: Word) -> Result { + Self::new(value) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::NullifierTree; + use crate::Word; + use crate::block::BlockNumber; + use crate::block::nullifier_tree::NullifierBlock; + use crate::errors::NullifierTreeError; + use crate::note::Nullifier; + + #[test] + fn leaf_value_encode_decode() { + let block_num = BlockNumber::from(0xffff_ffff_u32); + let nullifier_block = NullifierBlock::from(block_num); + let block_num_recovered = nullifier_block.into(); + assert_eq!(block_num, block_num_recovered); + } + + #[test] + fn leaf_value_encoding() { + let block_num = BlockNumber::from(123); + let nullifier_value = NullifierBlock::from(block_num); + assert_eq!( + nullifier_value, + NullifierBlock::new(Word::from([block_num.as_u32(), 0, 0, 0u32])).unwrap() + ); + } + + #[test] + fn leaf_value_decoding() { + let block_num = 123; + let nullifier_value = NullifierBlock::new(Word::from([block_num, 0, 0, 0u32])).unwrap(); + let decoded_block_num: BlockNumber = nullifier_value.into(); + + assert_eq!(decoded_block_num, block_num.into()); + } + + #[test] + fn apply_mutations() { + let nullifier1 = Nullifier::dummy(1); + let nullifier2 = Nullifier::dummy(2); + let nullifier3 = Nullifier::dummy(3); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + let block3 = BlockNumber::from(3); + + let mut tree = NullifierTree::with_entries([(nullifier1, block1)]).unwrap(); + + // Check that passing nullifier2 twice with different values will use the last value. + let mutations = tree + .compute_mutations([(nullifier2, block1), (nullifier3, block3), (nullifier2, block2)]) + .unwrap(); + + tree.apply_mutations(mutations).unwrap(); + + assert_eq!(tree.num_nullifiers(), 3); + assert_eq!(tree.get_block_num(&nullifier1).unwrap(), block1); + assert_eq!(tree.get_block_num(&nullifier2).unwrap(), block2); + assert_eq!(tree.get_block_num(&nullifier3).unwrap(), block3); + } + + #[test] + fn nullifier_already_spent() { + let nullifier1 = Nullifier::dummy(1); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + + let mut tree = NullifierTree::with_entries([(nullifier1, block1)]).unwrap(); + + // Attempt to insert nullifier 1 again at _the same_ block number. + let err = tree.clone().compute_mutations([(nullifier1, block1)]).unwrap_err(); + assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); + + let err = tree.clone().mark_spent(nullifier1, block1).unwrap_err(); + assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); + + // Attempt to insert nullifier 1 again at a different block number. + let err = tree.clone().compute_mutations([(nullifier1, block2)]).unwrap_err(); + assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); + + let err = tree.mark_spent(nullifier1, block2).unwrap_err(); + assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_basic_operations() { + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; + + // Create test data + let nullifier1 = Nullifier::dummy(1); + let nullifier2 = Nullifier::dummy(2); + let nullifier3 = Nullifier::dummy(3); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + let block3 = BlockNumber::from(3); + + // Create NullifierTree with LargeSmt backend + let mut tree = NullifierTree::new_unchecked( + LargeSmt::with_entries( + MemoryStorage::default(), + [ + (nullifier1.as_word(), NullifierBlock::from(block1).into()), + (nullifier2.as_word(), NullifierBlock::from(block2).into()), + ], + ) + .unwrap(), + ); + + // Test basic operations + assert_eq!(tree.num_nullifiers(), 2); + assert_eq!(tree.get_block_num(&nullifier1).unwrap(), block1); + assert_eq!(tree.get_block_num(&nullifier2).unwrap(), block2); + + // Test opening + let _witness1 = tree.open(&nullifier1); + + // Test mutations + tree.mark_spent(nullifier3, block3).unwrap(); + assert_eq!(tree.num_nullifiers(), 3); + assert_eq!(tree.get_block_num(&nullifier3).unwrap(), block3); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_nullifier_already_spent() { + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; + + let nullifier1 = Nullifier::dummy(1); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + + let mut tree = NullifierTree::new_unchecked( + LargeSmt::with_entries( + MemoryStorage::default(), + [(nullifier1.as_word(), NullifierBlock::from(block1).into())], + ) + .unwrap(), + ); + + assert_eq!(tree.get_block_num(&nullifier1).unwrap(), block1); + + let err = tree.mark_spent(nullifier1, block2).unwrap_err(); + assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_apply_mutations() { + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; + + let nullifier1 = Nullifier::dummy(1); + let nullifier2 = Nullifier::dummy(2); + let nullifier3 = Nullifier::dummy(3); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + let block3 = BlockNumber::from(3); + + let mut tree = LargeSmt::with_entries( + MemoryStorage::default(), + [(nullifier1.as_word(), NullifierBlock::from(block1).into())], + ) + .map(NullifierTree::new_unchecked) + .unwrap(); + + let mutations = + tree.compute_mutations([(nullifier2, block2), (nullifier3, block3)]).unwrap(); + + tree.apply_mutations(mutations).unwrap(); + + assert_eq!(tree.num_nullifiers(), 3); + assert_eq!(tree.get_block_num(&nullifier1).unwrap(), block1); + assert_eq!(tree.get_block_num(&nullifier2).unwrap(), block2); + assert_eq!(tree.get_block_num(&nullifier3).unwrap(), block3); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_same_root_as_regular_smt() { + use miden_crypto::merkle::smt::{LargeSmt, MemoryStorage}; + + let nullifier1 = Nullifier::dummy(1); + let nullifier2 = Nullifier::dummy(2); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + + // Create tree with LargeSmt backend + let large_tree = LargeSmt::with_entries( + MemoryStorage::default(), + [ + (nullifier1.as_word(), NullifierBlock::from(block1).into()), + (nullifier2.as_word(), NullifierBlock::from(block2).into()), + ], + ) + .map(NullifierTree::new_unchecked) + .unwrap(); + + // Create tree with regular Smt backend + let regular_tree = + NullifierTree::with_entries([(nullifier1, block1), (nullifier2, block2)]).unwrap(); + + // Both should have the same root + assert_eq!(large_tree.root(), regular_tree.root()); + + // Both should have the same nullifier entries + let large_entries: std::collections::BTreeMap<_, _> = large_tree.entries().collect(); + let regular_entries: std::collections::BTreeMap<_, _> = regular_tree.entries().collect(); + + assert_eq!(large_entries, regular_entries); + } +} diff --git a/crates/miden-objects/src/block/partial_nullifier_tree.rs b/crates/miden-protocol/src/block/nullifier_tree/partial.rs similarity index 85% rename from crates/miden-objects/src/block/partial_nullifier_tree.rs rename to crates/miden-protocol/src/block/nullifier_tree/partial.rs index d32d425976..0bcb90c80d 100644 --- a/crates/miden-objects/src/block/partial_nullifier_tree.rs +++ b/crates/miden-protocol/src/block/nullifier_tree/partial.rs @@ -1,9 +1,13 @@ +use super::{NullifierBlock, NullifierWitness}; use crate::Word; -use crate::block::{BlockNumber, NullifierTree, NullifierWitness}; -use crate::crypto::merkle::PartialSmt; +use crate::block::BlockNumber; +use crate::crypto::merkle::smt::PartialSmt; use crate::errors::NullifierTreeError; use crate::note::Nullifier; +// PARTIAL NULLIFIER TREE +// ================================================================================================ + /// The partial sparse merkle tree containing the nullifiers of consumed notes. /// /// A nullifier can only ever be spent once and its value in the tree is the block number at which @@ -36,8 +40,15 @@ impl PartialNullifierTree { .map(Self) } - /// Adds the given nullifier witness to the partial tree and tracks it. Once a nullifier has - /// been added to the tree, it can be marked as spent using [`Self::mark_spent`]. + /// Returns the root of the tree. + pub fn root(&self) -> Word { + self.0.root() + } + + /// Adds the given nullifier witness to the partial tree and tracks it. + /// + /// Once a nullifier has been added to the tree, it can be marked as spent using + /// [`Self::mark_spent`]. /// /// # Errors /// @@ -49,7 +60,7 @@ impl PartialNullifierTree { self.0.add_path(leaf, path).map_err(NullifierTreeError::TreeRootConflict) } - /// Marks the given nullifiers as spent at the given block number. + /// Marks the given nullifier as spent at the given block number. /// /// # Errors /// @@ -59,51 +70,51 @@ impl PartialNullifierTree { /// [`NullifierWitness`] was not added to the tree previously. pub fn mark_spent( &mut self, - nullifiers: impl IntoIterator, + nullifier: Nullifier, block_num: BlockNumber, ) -> Result<(), NullifierTreeError> { - for nullifier in nullifiers { - self.mark_spent_single(nullifier, block_num)?; - } - - Ok(()) - } + let prev_nullifier_value: NullifierBlock = self + .0 + .insert(nullifier.as_word(), NullifierBlock::from(block_num).into()) + .map_err(|source| NullifierTreeError::UntrackedNullifier { nullifier, source })? + .try_into()?; - /// Returns the root of the tree. - pub fn root(&self) -> Word { - self.0.root() + if prev_nullifier_value.is_spent() { + Err(NullifierTreeError::NullifierAlreadySpent(nullifier)) + } else { + Ok(()) + } } - /// Marks the given nullifier as spent at the given block number. + /// Marks the given nullifiers as spent at the given block number. /// /// # Errors /// /// See [`Self::mark_spent`] for the possible error conditions. - fn mark_spent_single( + pub fn mark_spent_all( &mut self, - nullifier: Nullifier, + nullifiers: impl IntoIterator, block_num: BlockNumber, ) -> Result<(), NullifierTreeError> { - let prev_nullifier_value = self - .0 - .insert(nullifier.as_word(), NullifierTree::block_num_to_leaf_value(block_num)) - .map_err(|source| NullifierTreeError::UntrackedNullifier { nullifier, source })?; - - if prev_nullifier_value != NullifierTree::UNSPENT_NULLIFIER { - Err(NullifierTreeError::NullifierAlreadySpent(nullifier)) - } else { - Ok(()) + for nullifier in nullifiers { + self.mark_spent(nullifier, block_num)?; } + + Ok(()) } } +// TESTS +// ================================================================================================ + #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_crypto::merkle::Smt; + use miden_crypto::merkle::smt::Smt; use winter_rand_utils::rand_value; use super::*; + use crate::block::nullifier_tree::NullifierTree; use crate::{EMPTY_WORD, Word}; /// Test that using a stale nullifier witness together with a current one results in a different @@ -151,7 +162,7 @@ mod tests { let mut partial_tree = PartialNullifierTree::with_witnesses([witness]).unwrap(); // Attempt to insert nullifier 1 again at a different block number. - let err = partial_tree.mark_spent([nullifier1], block2).unwrap_err(); + let err = partial_tree.mark_spent_all([nullifier1], block2).unwrap_err(); assert_matches!(err, NullifierTreeError::NullifierAlreadySpent(nullifier) if nullifier == nullifier1); } @@ -180,7 +191,7 @@ mod tests { // Insert a new value into partial and full tree and assert the root is the same. tree.mark_spent(nullifier3, block3).unwrap(); - partial_tree.mark_spent([nullifier3], block3).unwrap(); + partial_tree.mark_spent(nullifier3, block3).unwrap(); assert_eq!(tree.root(), partial_tree.root()); } diff --git a/crates/miden-objects/src/block/nullifier_witness.rs b/crates/miden-protocol/src/block/nullifier_tree/witness.rs similarity index 96% rename from crates/miden-objects/src/block/nullifier_witness.rs rename to crates/miden-protocol/src/block/nullifier_tree/witness.rs index 019e5763dc..1b7df6db38 100644 --- a/crates/miden-objects/src/block/nullifier_witness.rs +++ b/crates/miden-protocol/src/block/nullifier_tree/witness.rs @@ -1,4 +1,4 @@ -use crate::crypto::merkle::SmtProof; +use crate::crypto::merkle::smt::SmtProof; use crate::utils::serde::{ ByteReader, ByteWriter, diff --git a/crates/miden-objects/src/block/proposed_block.rs b/crates/miden-protocol/src/block/proposed_block.rs similarity index 76% rename from crates/miden-objects/src/block/proposed_block.rs rename to crates/miden-protocol/src/block/proposed_block.rs index 529a91dfa5..129f94912b 100644 --- a/crates/miden-objects/src/block/proposed_block.rs +++ b/crates/miden-protocol/src/block/proposed_block.rs @@ -11,18 +11,27 @@ use crate::batch::{ OrderedBatches, ProvenBatch, }; +use crate::block::account_tree::{AccountWitness, PartialAccountTree}; use crate::block::block_inputs::BlockInputs; +use crate::block::nullifier_tree::{NullifierWitness, PartialNullifierTree}; use crate::block::{ AccountUpdateWitness, - AccountWitness, + BlockBody, BlockHeader, + BlockNoteIndex, + BlockNoteTree, BlockNumber, - NullifierWitness, OutputNoteBatch, }; use crate::errors::ProposedBlockError; use crate::note::{NoteId, Nullifier}; -use crate::transaction::{InputNoteCommitment, OutputNote, PartialBlockchain, TransactionHeader}; +use crate::transaction::{ + InputNoteCommitment, + OutputNote, + PartialBlockchain, + TransactionHeader, + TransactionKernel, +}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -267,14 +276,6 @@ impl ProposedBlock { // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns an iterator over all transactions in the block. - pub fn transactions(&self) -> impl Iterator { - self.batches - .as_slice() - .iter() - .flat_map(|batch| batch.transactions().as_slice().iter()) - } - /// Returns the block number of this proposed block. pub fn block_num(&self) -> BlockNumber { // The chain length is the length at the state of the previous block header, so we have to @@ -282,16 +283,6 @@ impl ProposedBlock { self.partial_blockchain().chain_length() + 1 } - /// Returns a reference to the slice of batches in this block. - pub fn batches(&self) -> &OrderedBatches { - &self.batches - } - - /// Returns the map of nullifiers to their proofs from the proposed block. - pub fn created_nullifiers(&self) -> &BTreeMap { - &self.created_nullifiers - } - /// Returns a reference to the previous block header that this block builds on top of. pub fn prev_block_header(&self) -> &BlockHeader { &self.prev_block_header @@ -302,24 +293,229 @@ impl ProposedBlock { &self.partial_blockchain } + /// Returns a reference to the slice of transaction batches in this block. + pub fn batches(&self) -> &OrderedBatches { + &self.batches + } + + /// Returns an iterator over all transactions in the block. + pub fn transactions(&self) -> impl Iterator { + self.batches + .as_slice() + .iter() + .flat_map(|batch| batch.transactions().as_slice().iter()) + } + + /// Returns the map of nullifiers to their proofs from the proposed block. + pub fn created_nullifiers(&self) -> &BTreeMap { + &self.created_nullifiers + } + /// Returns a reference to the slice of accounts updated in this block. pub fn updated_accounts(&self) -> &[(AccountId, AccountUpdateWitness)] { &self.account_updated_witnesses } + /// Returns a slice of the [`OutputNoteBatch`] of each batch in this block. + pub fn output_note_batches(&self) -> &[OutputNoteBatch] { + &self.output_note_batches + } + /// Returns the timestamp of this block. pub fn timestamp(&self) -> u32 { self.timestamp } - /// Returns a slice of the [`OutputNoteBatch`] of each batch in this block. - pub fn output_note_batches(&self) -> &[OutputNoteBatch] { - &self.output_note_batches + // COMMITMENT COMPUTATIONS + // -------------------------------------------------------------------------------------------- + + /// Computes the new account tree root after the given updates. + pub fn compute_account_root(&self) -> Result { + // If no accounts were updated, the account tree root is unchanged. + if self.account_updated_witnesses.is_empty() { + return Ok(self.prev_block_header.account_root()); + } + + // First reconstruct the current account tree from the provided merkle paths. + // If a witness points to a leaf where multiple account IDs share the same prefix, this will + // return an error. + let mut partial_account_tree = PartialAccountTree::with_witnesses( + self.account_updated_witnesses + .iter() + .map(|(_, update_witness)| update_witness.to_witness()), + ) + .map_err(|source| ProposedBlockError::AccountWitnessTracking { source })?; + + // Check the account tree root in the previous block header matches the reconstructed tree's + // root. + if self.prev_block_header.account_root() != partial_account_tree.root() { + return Err(ProposedBlockError::StaleAccountTreeRoot { + prev_block_account_root: self.prev_block_header.account_root(), + stale_account_root: partial_account_tree.root(), + }); + } + + // Second, update the account tree by inserting the new final account state commitments to + // compute the new root of the account tree. + // If an account ID's prefix already exists in the tree, this will return an error. + // Note that we have inserted all witnesses that we want to update into the partial account + // tree, so we should not run into the untracked key error. + partial_account_tree + .upsert_state_commitments(self.account_updated_witnesses.iter().map( + |(account_id, update_witness)| { + (*account_id, update_witness.final_state_commitment()) + }, + )) + .map_err(|source| ProposedBlockError::AccountIdPrefixDuplicate { source })?; + + Ok(partial_account_tree.root()) + } + + /// Computes the new nullifier root by inserting the nullifier witnesses into a partial + /// nullifier tree and marking each nullifier as spent in the given block number. + pub fn compute_nullifier_root(&self) -> Result { + // If no nullifiers were created, the nullifier tree root is unchanged. + if self.created_nullifiers.is_empty() { + return Ok(self.prev_block_header.nullifier_root()); + } + + // First, reconstruct the current nullifier tree with the merkle paths of the nullifiers we + // want to update. + // Due to the guarantees of ProposedBlock we can safely assume that each nullifier is mapped + // to its corresponding nullifier witness, so we don't have to check again whether + // they match. + let mut partial_nullifier_tree = + PartialNullifierTree::with_witnesses(self.created_nullifiers().values().cloned()) + .map_err(ProposedBlockError::NullifierWitnessRootMismatch)?; + + // Check the nullifier tree root in the previous block header matches the reconstructed + // tree's root. + if self.prev_block_header.nullifier_root() != partial_nullifier_tree.root() { + return Err(ProposedBlockError::StaleNullifierTreeRoot { + prev_block_nullifier_root: self.prev_block_header.nullifier_root(), + stale_nullifier_root: partial_nullifier_tree.root(), + }); + } + + // Second, mark each nullifier as spent in the tree. Note that checking whether each + // nullifier is unspent is checked as part of constructing the proposed block. + + // SAFETY: As mentioned above, we can safely assume that each nullifier's witness was + // added and every nullifier should be tracked by the partial tree and + // therefore updatable. + partial_nullifier_tree + .mark_spent_all(self.created_nullifiers.keys().copied(), self.block_num()) + .expect("nullifiers' merkle path should have been added to the partial tree and the nullifiers should be unspent"); + + Ok(partial_nullifier_tree.root()) + } + + /// Compute the block note tree from the output note batches. + pub fn compute_block_note_tree(&self) -> BlockNoteTree { + let output_notes_iter = + self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| { + notes.iter().map(move |(note_idx_in_batch, note)| { + ( + // SAFETY: The proposed block contains at most the max allowed number of + // batches and each batch is guaranteed to contain at most + // the max allowed number of output notes. + BlockNoteIndex::new(batch_idx, *note_idx_in_batch).expect( + "max batches in block and max notes in batches should be enforced", + ), + note.id(), + note.metadata(), + ) + }) + }); + + // SAFETY: We only construct proposed blocks that: + // - do not contain duplicates + // - contain at most the max allowed number of batches and each batch is guaranteed to + // contain at most the max allowed number of output notes. + BlockNoteTree::with_entries(output_notes_iter) + .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum") + } + + /// Adds the commitment of the previous block header to the partial blockchain to compute the + /// new chain commitment. + pub fn compute_chain_commitment(&self) -> Word { + let mut partial_blockchain = self.partial_blockchain.clone(); + // SAFETY: This does not panic as long as the block header we're adding is the next one in + // the chain which is validated as part of constructing a `ProposedBlock`. + partial_blockchain.add_block(&self.prev_block_header, true); + partial_blockchain.peaks().hash_peaks() } // STATE MUTATORS // -------------------------------------------------------------------------------------------- + /// Builds a [`BlockHeader`] and [`BlockBody`] by computing the following from the state + /// updates encapsulated by the provided [`ProposedBlock`]: + /// - the account root; + /// - the nullifier root; + /// - the note root; + /// - the transaction commitment; and + /// - the chain commitment. + /// + /// The returned block header contains the same validator public key as the previous block, as + /// provided by the proposed block. + pub fn into_header_and_body(self) -> Result<(BlockHeader, BlockBody), ProposedBlockError> { + // Get fields from the proposed block before it is consumed. + let block_num = self.block_num(); + let timestamp = self.timestamp(); + let prev_block_header = self.prev_block_header().clone(); + + // Insert the state commitments of updated accounts into the account tree to compute its new + // root. + let new_account_root = self.compute_account_root()?; + + // Insert the created nullifiers into the nullifier tree to compute its new root. + let new_nullifier_root = self.compute_nullifier_root()?; + + // Compute the root of the block note tree. + let note_tree = self.compute_block_note_tree(); + let note_root = note_tree.root(); + + // Insert the previous block header into the block partial blockchain to get the new chain + // commitment. + // TODO: Consider avoiding the partial blockchain clone by constructing `BlockBody` from its + // raw parts, which does not require the partial blockchain. + let new_chain_commitment = self.compute_chain_commitment(); + + // Construct the block body from the proposed block. + let body = BlockBody::from(self); + + // Construct the header. + let tx_commitment = body.transaction_commitment(); + let prev_block_commitment = prev_block_header.commitment(); + + // For now we copy the parameters of the previous header, which means the parameters set on + // the genesis block will be passed through. Eventually, the contained base fees will be + // updated based on the demand in the currently proposed block. + let fee_parameters = prev_block_header.fee_parameters().clone(); + + // Currently undefined and reserved for future use. + // See miden-base/1155. + let version = 0; + let tx_kernel_commitment = TransactionKernel.to_commitment(); + let header = BlockHeader::new( + version, + prev_block_commitment, + block_num, + new_chain_commitment, + new_account_root, + new_nullifier_root, + note_root, + tx_commitment, + tx_kernel_commitment, + prev_block_header.validator_key().clone(), + fee_parameters, + timestamp, + ); + + Ok((header, body)) + } + /// Consumes self and returns the non-[`Copy`] parts of the block. #[allow(clippy::type_complexity)] pub fn into_parts( @@ -373,6 +569,7 @@ impl Deserializable for ProposedBlock { Ok(block) } } + // HELPER FUNCTIONS // ================================================================================================ diff --git a/crates/miden-protocol/src/block/proven_block.rs b/crates/miden-protocol/src/block/proven_block.rs new file mode 100644 index 0000000000..8e1c23c6c8 --- /dev/null +++ b/crates/miden-protocol/src/block/proven_block.rs @@ -0,0 +1,96 @@ +use miden_crypto::dsa::ecdsa_k256_keccak::Signature; + +use crate::MIN_PROOF_SECURITY_LEVEL; +use crate::block::{BlockBody, BlockHeader, BlockProof}; +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +// PROVEN BLOCK +// ================================================================================================ + +/// Represents a block in the Miden blockchain that has been signed and proven. +/// +/// Blocks transition through proposed, signed, and proven states. This struct represents the final, +/// proven state of a block. +/// +/// Proven blocks are the final, canonical blocks in the chain. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProvenBlock { + /// The header of the proven block. + header: BlockHeader, + + /// The body of the proven block. + body: BlockBody, + + /// The validator's signature over the block header. + signature: Signature, + + /// The proof of the block. + proof: BlockProof, +} + +impl ProvenBlock { + /// Returns a new [`ProvenBlock`] instantiated from the provided components. + /// + /// # Warning + /// + /// This constructor does not do any validation, so passing incorrect values may lead to later + /// panics. + pub fn new_unchecked( + header: BlockHeader, + body: BlockBody, + signature: Signature, + proof: BlockProof, + ) -> Self { + Self { header, signature, body, proof } + } + + /// Returns the proof security level of the block. + pub fn proof_security_level(&self) -> u32 { + MIN_PROOF_SECURITY_LEVEL + } + + /// Returns the header of the block. + pub fn header(&self) -> &BlockHeader { + &self.header + } + + /// Returns the validator's signature over the block header. + pub fn signature(&self) -> &Signature { + &self.signature + } + + /// Returns the body of the block. + pub fn body(&self) -> &BlockBody { + &self.body + } + + /// Returns the proof of the block. + pub fn proof(&self) -> &BlockProof { + &self.proof + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for ProvenBlock { + fn write_into(&self, target: &mut W) { + self.header.write_into(target); + self.body.write_into(target); + self.signature.write_into(target); + self.proof.write_into(target); + } +} + +impl Deserializable for ProvenBlock { + fn read_from(source: &mut R) -> Result { + let block = Self { + header: BlockHeader::read_from(source)?, + body: BlockBody::read_from(source)?, + signature: Signature::read_from(source)?, + proof: BlockProof::read_from(source)?, + }; + + Ok(block) + } +} diff --git a/crates/miden-protocol/src/block/signer.rs b/crates/miden-protocol/src/block/signer.rs new file mode 100644 index 0000000000..2007e58146 --- /dev/null +++ b/crates/miden-protocol/src/block/signer.rs @@ -0,0 +1,28 @@ +use crate::block::BlockHeader; +use crate::crypto::dsa::ecdsa_k256_keccak as ecdsa; +use crate::crypto::dsa::ecdsa_k256_keccak::SecretKey; + +// BLOCK SIGNER +// ================================================================================================ + +/// Trait which abstracts the signing of block headers with ECDSA signatures. +/// +/// Production-level implementations will involve some sort of secure remote backend. The trait also +/// allows for testing with local and ephemeral signers. +pub trait BlockSigner { + fn sign(&self, header: &BlockHeader) -> ecdsa::Signature; + fn public_key(&self) -> ecdsa::PublicKey; +} + +// SECRET KEY BLOCK SIGNER +// ================================================================================================ + +impl BlockSigner for SecretKey { + fn sign(&self, header: &BlockHeader) -> ecdsa::Signature { + self.sign(header.commitment()) + } + + fn public_key(&self) -> ecdsa::PublicKey { + self.public_key() + } +} diff --git a/crates/miden-objects/src/constants.rs b/crates/miden-protocol/src/constants.rs similarity index 95% rename from crates/miden-objects/src/constants.rs rename to crates/miden-protocol/src/constants.rs index 4d57250a40..bda8e339cc 100644 --- a/crates/miden-objects/src/constants.rs +++ b/crates/miden-protocol/src/constants.rs @@ -11,9 +11,8 @@ pub const MAX_ASSETS_PER_NOTE: usize = 255; /// The maximum number of inputs that can accompany a single note. /// -/// The value is set to 128 so that it can be represented using as a single byte while being -/// evenly divisible by 8. -pub const MAX_INPUTS_PER_NOTE: usize = 128; +/// The value is set to 1024 so that it is evenly divisible by 8. +pub const MAX_INPUTS_PER_NOTE: usize = 1024; /// The maximum number of notes that can be consumed by a single transaction. pub const MAX_INPUT_NOTES_PER_TX: usize = 1024; diff --git a/crates/miden-lib/src/errors/masm_error.rs b/crates/miden-protocol/src/errors/masm_error.rs similarity index 90% rename from crates/miden-lib/src/errors/masm_error.rs rename to crates/miden-protocol/src/errors/masm_error.rs index 3f3606151d..43c55f8797 100644 --- a/crates/miden-lib/src/errors/masm_error.rs +++ b/crates/miden-protocol/src/errors/masm_error.rs @@ -1,6 +1,6 @@ use alloc::borrow::Cow; -use miden_objects::Felt; +use crate::Felt; /// A convenience wrapper around an error extracted from Miden Assembly source files. pub struct MasmError { @@ -27,7 +27,7 @@ impl MasmError { /// Returns the code of this error. pub fn code(&self) -> Felt { - miden_objects::assembly::mast::error_code_from_msg(&self.message) + crate::assembly::mast::error_code_from_msg(&self.message) } } diff --git a/crates/miden-objects/src/errors.rs b/crates/miden-protocol/src/errors/mod.rs similarity index 84% rename from crates/miden-objects/src/errors.rs rename to crates/miden-protocol/src/errors/mod.rs index 3a806326a7..19236a647f 100644 --- a/crates/miden-objects/src/errors.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -5,9 +5,10 @@ use core::error::Error; use miden_assembly::Report; use miden_assembly::diagnostics::reporting::PrintDiagnostic; -use miden_core::Felt; use miden_core::mast::MastForestError; -use miden_crypto::merkle::MmrError; +use miden_core::{EventId, Felt}; +use miden_crypto::merkle::mmr::MmrError; +use miden_crypto::merkle::smt::{SmtLeafError, SmtProofError}; use miden_crypto::utils::HexParseError; use miden_processor::DeserializationError; use thiserror::Error; @@ -17,22 +18,31 @@ use super::asset::{FungibleAsset, NonFungibleAsset, TokenSymbol}; use super::crypto::merkle::MerkleError; use super::note::NoteId; use super::{MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, Word}; +use crate::account::component::{SchemaTypeError, StorageValueName, StorageValueNameError}; use crate::account::{ AccountCode, AccountIdPrefix, AccountStorage, AccountType, - SlotName, - StorageValueName, - StorageValueNameError, - TemplateTypeError, + StorageSlotId, + // StorageValueName, + // StorageValueNameError, + // TemplateTypeError, + StorageSlotName, }; use crate::address::AddressType; use crate::asset::AssetVaultKey; use crate::batch::BatchId; use crate::block::BlockNumber; -use crate::note::{NoteAssets, NoteExecutionHint, NoteTag, NoteType, Nullifier}; -use crate::transaction::TransactionId; +use crate::note::{ + NoteAssets, + NoteAttachmentArray, + NoteExecutionHint, + NoteTag, + NoteType, + Nullifier, +}; +use crate::transaction::{TransactionEventId, TransactionId}; use crate::{ ACCOUNT_UPDATE_MAX_SIZE, MAX_ACCOUNTS_PER_BATCH, @@ -42,37 +52,50 @@ use crate::{ MAX_OUTPUT_NOTES_PER_TX, }; +#[cfg(any(feature = "testing", test))] +mod masm_error; +#[cfg(any(feature = "testing", test))] +pub use masm_error::MasmError; + +/// The errors from the MASM code of the transaction kernel. +#[cfg(any(feature = "testing", test))] +#[rustfmt::skip] +pub mod tx_kernel; + +/// The errors from the MASM code of the Miden protocol library. +#[cfg(any(feature = "testing", test))] +#[rustfmt::skip] +pub mod protocol; + // ACCOUNT COMPONENT TEMPLATE ERROR // ================================================================================================ #[derive(Debug, Error)] pub enum AccountComponentTemplateError { #[error("storage slot name `{0}` is duplicate")] - DuplicateEntryNames(StorageValueName), - #[error("storage placeholder name `{0}` is duplicate")] - DuplicatePlaceholderName(StorageValueName), - #[error("slot {0} is defined multiple times")] - DuplicateSlot(u8), + DuplicateSlotName(StorageSlotName), + #[error("storage init value name `{0}` is duplicate")] + DuplicateInitValueName(StorageValueName), #[error("storage value name is incorrect: {0}")] IncorrectStorageValueName(#[source] StorageValueNameError), + #[error("invalid storage schema: {0}")] + InvalidSchema(String), #[error("type `{0}` is not valid for `{1}` slots")] InvalidType(String, String), #[error("error deserializing component metadata: {0}")] MetadataDeserializationError(String), - #[error("multi-slot entry should contain as many values as storage slot indices")] - MultiSlotArityMismatch, - #[error("multi-slot entry slot range should occupy more than one storage slot")] - MultiSlotSpansOneSlot, - #[error("component storage slots are not contiguous ({0} is followed by {1})")] - NonContiguousSlots(u8, u8), - #[error("storage value for placeholder `{0}` was not provided in the init storage data")] - PlaceholderValueNotProvided(StorageValueName), - #[error("error converting value into expected type: ")] - StorageValueParsingError(#[source] TemplateTypeError), + #[error("init storage value `{0}` was not provided")] + InitValueNotProvided(StorageValueName), + #[error("invalid init storage value for `{0}`: {1}")] + InvalidInitStorageValue(StorageValueName, String), + #[error( + "account component storage schema cannot contain a slot with name `{0}` as it is reserved by the protocol" + )] + ReservedSlotName(StorageSlotName), + #[error("error converting value into expected type: {0}")] + StorageValueParsingError(#[source] SchemaTypeError), #[error("storage map contains duplicate keys")] StorageMapHasDuplicateKeys(#[source] Box), - #[error("component storage slots have to start at 0, but they start at {0}")] - StorageSlotsDoNotStartAtZero(u8), #[cfg(feature = "std")] #[error("error trying to deserialize from toml")] TomlDeserializationError(#[source] toml::de::Error), @@ -96,20 +119,12 @@ pub enum AccountError { AccountCodeNoProcedures, #[error("account code contains {0} procedures but it may contain at most {max} procedures", max = AccountCode::MAX_NUM_PROCEDURES)] AccountCodeTooManyProcedures(usize), - #[error("account procedure {0}'s storage offset {1} does not fit into u8")] - AccountCodeProcedureStorageOffsetTooLarge(Word, Felt), - #[error("account procedure {0}'s storage size {1} does not fit into u8")] - AccountCodeProcedureStorageSizeTooLarge(Word, Felt), - #[error("account procedure {0}'s final two elements must be Felt::ZERO")] - AccountCodeProcedureInvalidPadding(Word), #[error("failed to assemble account component:\n{}", PrintDiagnostic::new(.0))] AccountComponentAssemblyError(Report), #[error("failed to merge components into one account code mast forest")] AccountComponentMastForestMergeError(#[source] MastForestError), - #[error("procedure with MAST root {0} is present in multiple account components")] - AccountComponentDuplicateProcedureRoot(Word), - #[error("failed to create account component")] - AccountComponentTemplateInstantiationError(#[source] AccountComponentTemplateError), + // #[error("failed to create account component")] + // AccountComponentTemplateInstantiationError(#[source] AccountComponentTemplateError), #[error("account component contains multiple authentication procedures")] AccountComponentMultipleAuthProcedures, #[error("failed to update asset vault")] @@ -144,22 +159,25 @@ pub enum AccountError { SeedConvertsToInvalidAccountId(#[source] AccountIdError), #[error("storage map root {0} not found in the account storage")] StorageMapRootNotFound(Word), - #[error("storage slot at index {0} is not of type map")] - StorageSlotNotMap(u8), - #[error("storage slot at index {0} is not of type value")] - StorageSlotNotValue(u8), - #[error("storage slot index is {index} but the slots length is {slots_len}")] - StorageIndexOutOfBounds { slots_len: u8, index: u8 }, - #[error("number of storage slots is {0} but max possible number is {max}", max = AccountStorage::MAX_NUM_STORAGE_SLOTS)] - StorageTooManySlots(u64), - #[error("procedure storage offset + size is {0} which exceeds the maximum value of {max}", - max = AccountStorage::MAX_NUM_STORAGE_SLOTS - )] - StorageOffsetPlusSizeOutOfBounds(u16), + #[error("storage slot {0} is not of type map")] + StorageSlotNotMap(StorageSlotName), + #[error("storage slot {0} is not of type value")] + StorageSlotNotValue(StorageSlotName), + #[error("storage slot name {0} is assigned to more than one slot")] + DuplicateStorageSlotName(StorageSlotName), #[error( - "procedure which does not access storage (storage size = 0) has non-zero storage offset" + "account storage cannot contain a user-provided slot with name {} as it is reserved by the protocol", + AccountStorage::faucet_sysdata_slot() )] - PureProcedureWithStorageOffset, + StorageSlotNameMustNotBeFaucetSysdata, + #[error("storage does not contain a slot with name {slot_name}")] + StorageSlotNameNotFound { slot_name: StorageSlotName }, + #[error("storage does not contain a slot with ID {slot_id}")] + StorageSlotIdNotFound { slot_id: StorageSlotId }, + #[error("storage slots must be sorted by slot ID")] + UnsortedStorageSlots, + #[error("number of storage slots is {0} but max possible number is {max}", max = AccountStorage::MAX_NUM_STORAGE_SLOTS)] + StorageTooManySlots(u64), #[error( "account component at index {component_index} is incompatible with account of type {account_type}" )] @@ -235,8 +253,8 @@ pub enum AccountIdError { // ================================================================================================ #[derive(Debug, Error)] -pub enum SlotNameError { - #[error("slot names must only contain characters a..z, A..Z, 0..9 or underscore")] +pub enum StorageSlotNameError { + #[error("slot name must only contain characters a..z, A..Z, 0..9, double colon or underscore")] InvalidCharacter, #[error("slot names must be separated by double colons")] UnexpectedColon, @@ -244,9 +262,11 @@ pub enum SlotNameError { UnexpectedUnderscore, #[error( "slot names must contain at least {} components separated by double colons", - SlotName::MIN_NUM_COMPONENTS + StorageSlotName::MIN_NUM_COMPONENTS )] TooShort, + #[error("slot names must contain at most {} characters", StorageSlotName::MAX_LENGTH)] + TooLong, } // ACCOUNT TREE ERROR @@ -282,11 +302,11 @@ pub enum AccountTreeError { #[derive(Debug, Error)] pub enum AddressError { #[error("tag length {0} should be {expected} bits for network accounts", - expected = NoteTag::DEFAULT_NETWORK_TAG_LENGTH + expected = NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH )] CustomTagLengthNotAllowedForNetworkAccounts(u8), #[error("tag length {0} is too large, must be less than or equal to {max}", - max = NoteTag::MAX_LOCAL_TAG_LENGTH + max = NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH )] TagLengthTooLarge(u8), #[error("unknown address interface `{0}`")] @@ -357,12 +377,8 @@ pub enum NetworkIdError { #[derive(Debug, Error)] pub enum AccountDeltaError { - #[error( - "storage slot index {slot_index} is greater than or equal to the number of slots {num_slots}" - )] - StorageSlotIndexOutOfBounds { slot_index: u8, num_slots: u8 }, - #[error("storage slot {0} was updated as a value and as a map")] - StorageSlotUsedAsDifferentTypes(u8), + #[error("storage slot {0} was used as different slot types")] + StorageSlotUsedAsDifferentTypes(StorageSlotName), #[error("non fungible vault can neither be added nor removed twice")] DuplicateNonFungibleVaultUpdate(NonFungibleAsset), #[error( @@ -483,8 +499,8 @@ pub enum TokenSymbolError { ValueTooLarge(u64), #[error("token symbol should have length between 1 and 6 characters, but {0} was provided")] InvalidLength(usize), - #[error("token symbol `{0}` contains characters that are not uppercase ASCII")] - InvalidCharacter(String), + #[error("token symbol contains a character that is not uppercase ASCII")] + InvalidCharacter, #[error("token symbol data left after decoding the specified number of characters")] DataNotFullyDecoded, } @@ -532,7 +548,7 @@ pub enum PartialAssetVaultError { #[derive(Debug, Error)] pub enum NoteError { - #[error("note tag length {0} exceeds the maximum of {max}", max = NoteTag::MAX_LOCAL_TAG_LENGTH)] + #[error("note tag length {0} exceeds the maximum of {max}", max = NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)] NoteTagLengthTooLarge(u8), #[error("duplicate fungible asset from issuer {0} in note")] DuplicateFungibleAsset(AccountId), @@ -544,8 +560,6 @@ pub enum NoteError { AddFungibleAssetBalanceError(#[source] AssetError), #[error("note sender is not a valid account ID")] NoteSenderInvalidAccountId(#[source] AccountIdError), - #[error("note tag use case {0} must be less than 2^{exp}", exp = NoteTag::MAX_USE_CASE_ID_EXPONENT)] - NoteTagUseCaseTooLarge(u16), #[error( "note execution hint tag {0} must be in range {from}..={to}", from = NoteExecutionHint::NONE_TAG, @@ -579,6 +593,15 @@ pub enum NoteError { TooManyInputs(usize), #[error("note tag requires a public note but the note is of type {0}")] PublicNoteRequired(NoteType), + #[error( + "note attachment cannot commit to more than {} elements", + NoteAttachmentArray::MAX_NUM_ELEMENTS + )] + NoteAttachmentArraySizeExceeded(usize), + #[error("unknown note attachment kind {0}")] + UnknownNoteAttachmentKind(u8), + #[error("note attachment of kind None must have attachment scheme None")] + AttachmentKindNoneMustHaveAttachmentSchemeNone, #[error("{error_msg}")] Other { error_msg: Box, @@ -686,6 +709,41 @@ pub enum TransactionInputError { TooManyInputNotes(usize), } +// TRANSACTION INPUTS EXTRACTION ERROR +// =============================================================================================== + +#[derive(Debug, Error)] +pub enum TransactionInputsExtractionError { + #[error("specified foreign account id matches the transaction input's account id")] + AccountNotForeign, + #[error("foreign account data not found in advice map for account {0}")] + ForeignAccountNotFound(AccountId), + #[error("foreign account code not found for account {0}")] + ForeignAccountCodeNotFound(AccountId), + #[error("storage header data not found in advice map for account {0}")] + StorageHeaderNotFound(AccountId), + #[error("failed to handle account data")] + AccountError(#[from] AccountError), + #[error("failed to handle merkle data")] + MerkleError(#[from] MerkleError), + #[error("failed to handle account tree data")] + AccountTreeError(#[from] AccountTreeError), + #[error("missing vault root from Merkle store")] + MissingVaultRoot, + #[error("missing storage map root from Merkle store")] + MissingMapRoot, + #[error("failed to construct SMT proof")] + SmtProofError(#[from] SmtProofError), + #[error("failed to construct asset witness")] + AssetError(#[from] AssetError), + #[error("failed to handle storage map data")] + StorageMapError(#[from] StorageMapError), + #[error("failed to convert elements to leaf index: {0}")] + LeafConversionError(String), + #[error("failed to construct SMT leaf")] + SmtLeafError(#[from] SmtLeafError), +} + // TRANSACTION OUTPUT ERROR // =============================================================================================== @@ -713,6 +771,28 @@ pub enum TransactionOutputError { AccountUpdateCommitment(Box), } +// TRANSACTION EVENT PARSING ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum TransactionEventError { + #[error("event id {0} is not a valid transaction event")] + InvalidTransactionEvent(EventId, Option<&'static str>), + #[error("event id {0} is not a transaction kernel event")] + NotTransactionEvent(EventId, Option<&'static str>), + #[error("event id {0} can only be emitted from the root context")] + NotRootContext(TransactionEventId), +} + +// TRANSACTION TRACE PARSING ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum TransactionTraceParsingError { + #[error("trace id {0} is an unknown transaction kernel trace")] + UnknownTransactionTrace(u32), +} + // PROVEN TRANSACTION ERROR // ================================================================================================ @@ -1022,6 +1102,31 @@ pub enum ProposedBlockError { account_id: AccountId, source: Box, }, + + #[error("failed to track account witness")] + AccountWitnessTracking { source: AccountTreeError }, + + #[error( + "account tree root of the previous block header is {prev_block_account_root} but the root of the partial tree computed from account witnesses is {stale_account_root}, indicating that the witnesses are stale" + )] + StaleAccountTreeRoot { + prev_block_account_root: Word, + stale_account_root: Word, + }, + + #[error("account ID prefix already exists in the tree")] + AccountIdPrefixDuplicate { source: AccountTreeError }, + + #[error( + "nullifier tree root of the previous block header is {prev_block_nullifier_root} but the root of the partial tree computed from nullifier witnesses is {stale_nullifier_root}, indicating that the witnesses are stale" + )] + StaleNullifierTreeRoot { + prev_block_nullifier_root: Word, + stale_nullifier_root: Word, + }, + + #[error("nullifier witness has a different root than the current nullifier tree root")] + NullifierWitnessRootMismatch(NullifierTreeError), } // FEE ERROR @@ -1058,8 +1163,11 @@ pub enum NullifierTreeError { #[error("new tree root after nullifier witness insertion does not match previous tree root")] TreeRootConflict(#[source] MerkleError), - #[error("failed to compute nulifier tree mutations")] + #[error("failed to compute nullifier tree mutations")] ComputeMutations(#[source] MerkleError), + + #[error("invalid nullifier block number")] + InvalidNullifierBlockNumber(Word), } // AUTH SCHEME ERROR diff --git a/crates/miden-protocol/src/errors/protocol.rs b/crates/miden-protocol/src/errors/protocol.rs new file mode 100644 index 0000000000..73b7085d33 --- /dev/null +++ b/crates/miden-protocol/src/errors/protocol.rs @@ -0,0 +1,37 @@ +use crate::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, ...). + +// PROTOCOL LIB ERRORS +// ================================================================================================ + +/// Error Message: "the account ID must have storage mode public if the network flag is set" +pub const ERR_ACCOUNT_ID_NON_PUBLIC_NETWORK_ACCOUNT: MasmError = MasmError::from_static_str("the account ID must have storage mode public if the network flag is set"); +/// Error Message: "least significant byte of the account ID suffix must be zero" +pub const ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO: MasmError = MasmError::from_static_str("least significant byte of the account ID suffix must be zero"); +/// Error Message: "most significant bit of the account ID suffix must be zero" +pub const ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO: MasmError = MasmError::from_static_str("most significant bit of the account ID suffix must be zero"); +/// Error Message: "unknown account storage mode in account ID" +pub const ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE: MasmError = MasmError::from_static_str("unknown account storage mode in account ID"); +/// Error Message: "unknown version in account ID" +pub const ERR_ACCOUNT_ID_UNKNOWN_VERSION: MasmError = MasmError::from_static_str("unknown version in account ID"); + +/// Error Message: "fungible asset build operation called with amount that exceeds the maximum allowed asset amount" +pub const ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT: MasmError = MasmError::from_static_str("fungible asset build operation called with amount that exceeds the maximum allowed asset amount"); +/// Error Message: "failed to build the fungible asset because the provided faucet id is not from a fungible faucet" +pub const ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID: MasmError = MasmError::from_static_str("failed to build the fungible asset because the provided faucet id is not from a fungible faucet"); + +/// Error Message: "failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet" +pub const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID: MasmError = MasmError::from_static_str("failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet"); + +/// Error Message: "note data does not match the commitment" +pub const ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT: MasmError = MasmError::from_static_str("note data does not match the commitment"); +/// Error Message: "the specified number of note inputs does not match the actual number" +pub const ERR_NOTE_INVALID_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("the specified number of note inputs does not match the actual number"); + +/// Error Message: "number of note inputs exceeded the maximum limit of 1024" +pub const ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("number of note inputs exceeded the maximum limit of 1024"); diff --git a/crates/miden-lib/src/errors/tx_kernel_errors.rs b/crates/miden-protocol/src/errors/tx_kernel.rs similarity index 87% rename from crates/miden-lib/src/errors/tx_kernel_errors.rs rename to crates/miden-protocol/src/errors/tx_kernel.rs index 3022640235..d267df0216 100644 --- a/crates/miden-lib/src/errors/tx_kernel_errors.rs +++ b/crates/miden-protocol/src/errors/tx_kernel.rs @@ -1,11 +1,10 @@ use crate::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 `miden-lib/asm` directory. +// 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, Prologue, -// Non-Fungible-Asset, ...). +// 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, ...). // TX KERNEL ERRORS // ================================================================================================ @@ -26,20 +25,20 @@ pub const ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO: MasmError = M pub const ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE: MasmError = MasmError::from_static_str("unknown account storage mode in account ID"); /// Error Message: "unknown version in account ID" pub const ERR_ACCOUNT_ID_UNKNOWN_VERSION: MasmError = MasmError::from_static_str("unknown version in account ID"); -/// Error Message: "storage size can only be zero if storage offset is also zero" -pub const ERR_ACCOUNT_INVALID_STORAGE_OFFSET_FOR_SIZE: MasmError = MasmError::from_static_str("storage size can only be zero if storage offset is also zero"); /// Error Message: "the active account is not native" pub const ERR_ACCOUNT_IS_NOT_NATIVE: MasmError = MasmError::from_static_str("the active account is not native"); /// Error Message: "account nonce is already at its maximum possible value" pub const ERR_ACCOUNT_NONCE_AT_MAX: MasmError = MasmError::from_static_str("account nonce is already at its maximum possible value"); /// Error Message: "account nonce can only be incremented once" pub const ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE: MasmError = MasmError::from_static_str("account nonce can only be incremented once"); +/// Error Message: "number of account procedures must be at least 2" +pub const ERR_ACCOUNT_NOT_ENOUGH_PROCEDURES: MasmError = MasmError::from_static_str("number of account procedures must be at least 2"); /// Error Message: "provided procedure index is out of bounds" pub const ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS: MasmError = MasmError::from_static_str("provided procedure index is out of bounds"); /// Error Message: "account procedure is not the authentication procedure; some procedures (e.g. `incr_nonce`) can be called only from the authentication procedure" pub const ERR_ACCOUNT_PROC_NOT_AUTH_PROC: MasmError = MasmError::from_static_str("account procedure is not the authentication procedure; some procedures (e.g. `incr_nonce`) can be called only from the authentication procedure"); -/// Error Message: "account procedure is not part of the account code" -pub const ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE: MasmError = MasmError::from_static_str("account procedure is not part of the account code"); +/// Error Message: "procedure is not part of the account code" +pub const ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE: MasmError = MasmError::from_static_str("procedure is not part of the account code"); /// Error Message: "failed to read an account map item from a non-map storage slot" pub const ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT: MasmError = MasmError::from_static_str("failed to read an account map item from a non-map storage slot"); /// Error Message: "ID of the new account does not match the ID computed from the seed and commitments" @@ -56,13 +55,17 @@ pub const ERR_ACCOUNT_STACK_UNDERFLOW: MasmError = MasmError::from_static_str("f pub const ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH: MasmError = MasmError::from_static_str("computed account storage commitment does not match recorded account storage commitment"); /// Error Message: "storage map entries provided as advice inputs do not have the same storage map root as the root of the map the new account commits to" pub const ERR_ACCOUNT_STORAGE_MAP_ENTRIES_DO_NOT_MATCH_MAP_ROOT: MasmError = MasmError::from_static_str("storage map entries provided as advice inputs do not have the same storage map root as the root of the map the new account commits to"); -/// Error Message: "provided storage slot index is out of bounds" -pub const ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS: MasmError = MasmError::from_static_str("provided storage slot index is out of bounds"); +/// Error Message: "slot IDs must be unique and sorted in ascending order" +pub const ERR_ACCOUNT_STORAGE_SLOTS_MUST_BE_SORTED_AND_UNIQUE: MasmError = MasmError::from_static_str("slot IDs must be unique and sorted in ascending order"); /// Error Message: "number of account procedures exceeds the maximum limit of 256" pub const ERR_ACCOUNT_TOO_MANY_PROCEDURES: MasmError = MasmError::from_static_str("number of account procedures exceeds the maximum limit of 256"); /// Error Message: "number of account storage slots exceeds the maximum limit of 255" pub const ERR_ACCOUNT_TOO_MANY_STORAGE_SLOTS: MasmError = MasmError::from_static_str("number of account storage slots exceeds the maximum limit of 255"); +/// Error Message: "storage slot with the provided name does not exist" +pub const ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME: MasmError = MasmError::from_static_str("storage slot with the provided name does not exist"); +/// Error Message: "auth procedure has been called from outside the epilogue" +pub const ERR_EPILOGUE_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT: MasmError = MasmError::from_static_str("auth procedure has been called from outside the epilogue"); /// Error Message: "executed transaction neither changed the account state, nor consumed any notes" pub const ERR_EPILOGUE_EXECUTED_TRANSACTION_IS_EMPTY: MasmError = MasmError::from_static_str("executed transaction neither changed the account state, nor consumed any notes"); /// Error Message: "nonce cannot be 0 after an account-creating transaction" @@ -74,8 +77,6 @@ pub const ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME: MasmError = Ma pub const ERR_FAUCET_BURN_CANNOT_EXCEED_EXISTING_TOTAL_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn can not exceed the existing total supply"); /// Error Message: "the burn_non_fungible_asset procedure can only be called on a non-fungible faucet" pub const ERR_FAUCET_BURN_NON_FUNGIBLE_ASSET_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET: MasmError = MasmError::from_static_str("the burn_non_fungible_asset procedure can only be called on a non-fungible faucet"); -/// Error Message: "storage offset is invalid for a faucet account (0 is prohibited as it is the reserved data slot for faucets)" -pub const ERR_FAUCET_INVALID_STORAGE_OFFSET: MasmError = MasmError::from_static_str("storage offset is invalid for a faucet account (0 is prohibited as it is the reserved data slot for faucets)"); /// Error Message: "the faucet_is_non_fungible_asset_issued procedure can only be called on a non-fungible faucet" pub const ERR_FAUCET_IS_NF_ASSET_ISSUED_PROC_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUCET: MasmError = MasmError::from_static_str("the faucet_is_non_fungible_asset_issued procedure can only be called on a non-fungible faucet"); /// Error Message: "asset mint operation would cause the new total supply to exceed the maximum allowed asset amount" @@ -98,10 +99,6 @@ pub const ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT: MasmError = MasmError::from_st /// Error Message: "maximum allowed number of foreign account to be loaded (64) was exceeded" pub const ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED: MasmError = MasmError::from_static_str("maximum allowed number of foreign account to be loaded (64) was exceeded"); -/// Error Message: "fungible asset build operation called with amount that exceeds the maximum allowed asset amount" -pub const ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT: MasmError = MasmError::from_static_str("fungible asset build operation called with amount that exceeds the maximum allowed asset amount"); -/// Error Message: "distribute would cause the maximum supply to be exceeded" -pub const ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED: MasmError = MasmError::from_static_str("distribute would cause the maximum supply to be exceeded"); /// Error Message: "the origin of the fungible asset is not this faucet" pub const ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN: MasmError = MasmError::from_static_str("the origin of the fungible asset is not this faucet"); /// Error Message: "malformed fungible asset: `ASSET[1]` must be 0" @@ -110,8 +107,6 @@ pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO: MasmError = MasmEr pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID: MasmError = MasmError::from_static_str("malformed fungible asset: `ASSET[2]` and `ASSET[3]` must be a valid fungible faucet id"); /// Error Message: "malformed fungible asset: `ASSET[0]` exceeds the maximum allowed amount" pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS: MasmError = MasmError::from_static_str("malformed fungible asset: `ASSET[0]` exceeds the maximum allowed amount"); -/// Error Message: "failed to build the fungible asset because the provided faucet id is not from a fungible faucet" -pub const ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID: MasmError = MasmError::from_static_str("failed to build the fungible asset because the provided faucet id is not from a fungible faucet"); /// Error Message: "requested input note index should be less than the total number of input notes" pub const ERR_INPUT_NOTE_INDEX_OUT_OF_BOUNDS: MasmError = MasmError::from_static_str("requested input note index should be less than the total number of input notes"); @@ -144,8 +139,6 @@ pub const ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN: MasmError = MasmError::fr pub const ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID: MasmError = MasmError::from_static_str("malformed non-fungible asset: `ASSET[3]` is not a valid non-fungible faucet id"); /// Error Message: "malformed non-fungible asset: the most significant bit must be 0" pub const ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO: MasmError = MasmError::from_static_str("malformed non-fungible asset: the most significant bit must be 0"); -/// Error Message: "failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet" -pub const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID: MasmError = MasmError::from_static_str("failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet"); /// Error Message: "failed to access note assets of active note because no note is currently being processed" pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_WHILE_NO_NOTE_BEING_PROCESSED: MasmError = MasmError::from_static_str("failed to access note assets of active note because no note is currently being processed"); @@ -159,25 +152,27 @@ pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_RECIPIENT_WHILE_NO_NOTE_BEING_PROCESSE pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SCRIPT_ROOT_WHILE_NO_NOTE_BEING_PROCESSED: MasmError = MasmError::from_static_str("failed to access note script root of active note because no note is currently being processed"); /// Error Message: "failed to access note serial number of active note because no note is currently being processed" pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROCESSED: MasmError = MasmError::from_static_str("failed to access note serial number of active note because no note is currently being processed"); -/// Error Message: "note data does not match the commitment" -pub const ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT: MasmError = MasmError::from_static_str("note data does not match the commitment"); /// Error Message: "adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807" pub const ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED: MasmError = MasmError::from_static_str("adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807"); /// Error Message: "failed to find note at the given index; index must be within [0, num_of_notes]" pub const ERR_NOTE_INVALID_INDEX: MasmError = MasmError::from_static_str("failed to find note at the given index; index must be within [0, num_of_notes]"); -/// Error Message: "invalid note type for the given note tag prefix" -pub const ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX: MasmError = MasmError::from_static_str("invalid note type for the given note tag prefix"); -/// Error Message: "the specified number of note inputs does not match the actual number" -pub const ERR_NOTE_INVALID_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("the specified number of note inputs does not match the actual number"); /// Error Message: "invalid note type" pub const ERR_NOTE_INVALID_TYPE: MasmError = MasmError::from_static_str("invalid note type"); /// Error Message: "number of assets in a note exceed 255" pub const ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of assets in a note exceed 255"); -/// Error Message: "the note's tag must fit into a u32 so the 32 most significant bits must be zero" -pub const ERR_NOTE_TAG_MUST_BE_U32: MasmError = MasmError::from_static_str("the note's tag must fit into a u32 so the 32 most significant bits must be zero"); +/// Error Message: "the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero" +pub const ERR_NOTE_TAG_MUST_BE_U32: MasmError = MasmError::from_static_str("the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero"); +/// Error Message: "attachment kind None requires ATTACHMENT to be set to an empty word" +pub const ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_BE_EMPTY_WORD: MasmError = MasmError::from_static_str("attachment kind None requires ATTACHMENT to be set to an empty word"); +/// Error Message: "attachment kind none must have attachment scheme none" +pub const ERR_OUTPUT_NOTE_ATTACHMENT_KIND_NONE_MUST_HAVE_ATTACHMENT_SCHEME_NONE: MasmError = MasmError::from_static_str("attachment kind none must have attachment scheme none"); /// Error Message: "requested output note index should be less than the total number of created output notes" pub const ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS: MasmError = MasmError::from_static_str("requested output note index should be less than the total number of created output notes"); +/// Error Message: "attachment scheme and attachment kind must fit into u32s" +pub const ERR_OUTPUT_NOTE_INVALID_ATTACHMENT_SCHEMES: MasmError = MasmError::from_static_str("attachment scheme and attachment kind must fit into u32s"); +/// Error Message: "attachment kind variant must be between 0 and 2" +pub const ERR_OUTPUT_NOTE_UNKNOWN_ATTACHMENT_KIND: MasmError = MasmError::from_static_str("attachment kind variant must be between 0 and 2"); /// Error Message: "existing accounts must have a non-zero nonce" pub const ERR_PROLOGUE_EXISTING_ACCOUNT_MUST_HAVE_NON_ZERO_NONCE: MasmError = MasmError::from_static_str("existing accounts must have a non-zero nonce"); @@ -209,14 +204,12 @@ pub const ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE: MasmE pub const ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPTY_SMT: MasmError = MasmError::from_static_str("reserved slot for non-fungible faucet is not a valid empty SMT"); /// Error Message: "failed to authenticate note inclusion in block" pub const ERR_PROLOGUE_NOTE_AUTHENTICATION_FAILED: MasmError = MasmError::from_static_str("failed to authenticate note inclusion in block"); -/// Error Message: "number of note inputs exceeded the maximum limit of 128" -pub const ERR_PROLOGUE_NOTE_INPUTS_LEN_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("number of note inputs exceeded the maximum limit of 128"); /// Error Message: "number of input notes exceeds the kernel's maximum limit of 1024" pub const ERR_PROLOGUE_NUMBER_OF_INPUT_NOTES_EXCEEDS_LIMIT: MasmError = MasmError::from_static_str("number of input notes exceeds the kernel's maximum limit of 1024"); /// Error Message: "number of note assets exceeds the maximum limit of 256" pub const ERR_PROLOGUE_NUMBER_OF_NOTE_ASSETS_EXCEEDS_LIMIT: MasmError = MasmError::from_static_str("number of note assets exceeds the maximum limit of 256"); -/// Error Message: "number of note inputs exceeded the maximum limit of 128" -pub const ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("number of note inputs exceeded the maximum limit of 128"); +/// Error Message: "number of note inputs exceeded the maximum limit of 1024" +pub const ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("number of note inputs exceeded the maximum limit of 1024"); /// Error Message: "account data provided does not match the commitment recorded on-chain" pub const ERR_PROLOGUE_PROVIDED_ACCOUNT_DATA_DOES_NOT_MATCH_ON_CHAIN_COMMITMENT: MasmError = MasmError::from_static_str("account data provided does not match the commitment recorded on-chain"); /// Error Message: "provided info about assets of an input does not match its commitment" @@ -224,8 +217,6 @@ pub const ERR_PROLOGUE_PROVIDED_INPUT_ASSETS_INFO_DOES_NOT_MATCH_ITS_COMMITMENT: /// Error Message: "verification base fee must fit into a u32" pub const ERR_PROLOGUE_VERIFICATION_BASE_FEE_MUST_BE_U32: MasmError = MasmError::from_static_str("verification base fee must fit into a u32"); -/// Error Message: "failed to approve multisig transaction as it was already executed" -pub const ERR_TX_ALREADY_EXECUTED: MasmError = MasmError::from_static_str("failed to approve multisig transaction as it was already executed"); /// Error Message: "transaction expiration block delta must be within 0x1 and 0xFFFF" pub const ERR_TX_INVALID_EXPIRATION_DELTA: MasmError = MasmError::from_static_str("transaction expiration block delta must be within 0x1 and 0xFFFF"); /// Error Message: "number of output notes in the transaction exceeds the maximum limit of 1024" diff --git a/crates/miden-objects/src/lib.rs b/crates/miden-protocol/src/lib.rs similarity index 76% rename from crates/miden-objects/src/lib.rs rename to crates/miden-protocol/src/lib.rs index 34f9219efb..0b4b8be503 100644 --- a/crates/miden-objects/src/lib.rs +++ b/crates/miden-protocol/src/lib.rs @@ -11,64 +11,41 @@ pub mod address; pub mod asset; pub mod batch; pub mod block; +pub mod errors; pub mod note; +mod protocol; pub mod transaction; #[cfg(any(feature = "testing", test))] pub mod testing; mod constants; -mod errors; // RE-EXPORTS // ================================================================================================ pub use constants::*; -pub use errors::{ - AccountDeltaError, - AccountError, - AccountIdError, - AccountTreeError, - AddressError, - AssetError, - AssetVaultError, - AuthSchemeError, - BatchAccountUpdateError, - FeeError, - NetworkIdError, - NoteError, - NullifierTreeError, - PartialBlockchainError, - ProposedBatchError, - ProposedBlockError, - ProvenBatchError, - ProvenTransactionError, - SlotNameError, - StorageMapError, - TokenSymbolError, - TransactionInputError, - TransactionOutputError, - TransactionScriptError, -}; pub use miden_core::mast::{MastForest, MastNodeId}; pub use miden_core::prettier::PrettyPrint; pub use miden_core::{EMPTY_WORD, Felt, FieldElement, ONE, StarkField, WORD_SIZE, ZERO}; +pub use miden_core_lib::CoreLibrary; pub use miden_crypto::hash::rpo::Rpo256 as Hasher; pub use miden_crypto::word; pub use miden_crypto::word::{LexicographicWord, Word, WordError}; +pub use protocol::ProtocolLib; pub mod assembly { pub use miden_assembly::ast::{Module, ModuleKind, ProcedureName, QualifiedProcedureName}; pub use miden_assembly::debuginfo::SourceManagerSync; + pub use miden_assembly::library::LibraryExport; pub use miden_assembly::{ Assembler, DefaultSourceManager, KernelLibrary, Library, - LibraryNamespace, - LibraryPath, Parse, ParseOptions, + Path, SourceFile, SourceId, SourceManager, @@ -102,7 +79,7 @@ pub mod utils { pub mod vm { pub use miden_assembly_syntax::ast::{AttributeSet, QualifiedProcedureName}; pub use miden_core::sys_events::SystemEvent; - pub use miden_core::{AdviceMap, Program, ProgramInfo}; + pub use miden_core::{AdviceMap, EventId, Program, ProgramInfo}; pub use miden_mast_package::{ MastArtifact, Package, diff --git a/crates/miden-objects/src/note/assets.rs b/crates/miden-protocol/src/note/assets.rs similarity index 100% rename from crates/miden-objects/src/note/assets.rs rename to crates/miden-protocol/src/note/assets.rs diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs new file mode 100644 index 0000000000..56a46e4b7d --- /dev/null +++ b/crates/miden-protocol/src/note/attachment.rs @@ -0,0 +1,518 @@ +use alloc::string::ToString; +use alloc::vec::Vec; + +use crate::crypto::SequentialCommit; +use crate::errors::NoteError; +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; +use crate::{Felt, Hasher, Word}; + +// NOTE ATTACHMENT +// ================================================================================================ + +/// The optional attachment for a [`Note`](super::Note). +/// +/// An attachment is a _public_ extension to a note's [`NoteMetadata`](super::NoteMetadata). +/// +/// Example use cases: +/// - Communicate the [`NoteDetails`](super::NoteDetails) of a private note in encrypted form. +/// - In the context of network transactions, encode the ID of the network account that should +/// consume the note. +/// - Communicate details to the receiver of a _private_ note to allow deriving the +/// [`NoteDetails`](super::NoteDetails) of that note. For instance, the payback note of a partial +/// swap note can be private, but the receiver needs to know additional details to fully derive +/// the content of the payback note. They can neither fetch those details from the network, since +/// the note is private, nor is a side-channel available. The note attachment can encode those +/// details. +/// +/// These use cases require different amounts of data, e.g. an account ID takes up just two felts +/// while the details of an encrypted note require many felts. To accommodate these cases, both a +/// computationally efficient [`NoteAttachmentContent::Word`] as well as a more flexible +/// [`NoteAttachmentContent::Array`] variant are available. See the type's docs for more +/// details. +/// +/// Next to the content, a note attachment can optionally specify a [`NoteAttachmentScheme`]. This +/// allows a note attachment to describe itself. For example, a network account target attachment +/// can be identified by a standardized type. For cases when the attachment scheme is known from +/// content or typing is otherwise undesirable, [`NoteAttachmentScheme::none`] can be used. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct NoteAttachment { + attachment_scheme: NoteAttachmentScheme, + content: NoteAttachmentContent, +} + +impl NoteAttachment { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteAttachment`] from a user-defined type and the provided content. + /// + /// # Errors + /// + /// Returns an error if: + /// - The attachment content is [`NoteAttachmentKind::None`] but the scheme is not + /// [`NoteAttachmentScheme::none`]. + pub fn new( + attachment_scheme: NoteAttachmentScheme, + content: NoteAttachmentContent, + ) -> Result { + if content.attachment_kind().is_none() && !attachment_scheme.is_none() { + return Err(NoteError::AttachmentKindNoneMustHaveAttachmentSchemeNone); + } + + Ok(Self { attachment_scheme, content }) + } + + /// Creates a new note attachment with content [`NoteAttachmentContent::Word`] from the provided + /// word. + pub fn new_word(attachment_scheme: NoteAttachmentScheme, word: Word) -> Self { + Self { + attachment_scheme, + content: NoteAttachmentContent::new_word(word), + } + } + + /// Creates a new note attachment with content [`NoteAttachmentContent::Array`] from the + /// provided set of elements. + /// + /// # Errors + /// + /// Returns an error if: + /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + pub fn new_array( + attachment_scheme: NoteAttachmentScheme, + elements: Vec, + ) -> Result { + NoteAttachmentContent::new_array(elements) + .map(|content| Self { attachment_scheme, content }) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the attachment scheme. + pub fn attachment_scheme(&self) -> NoteAttachmentScheme { + self.attachment_scheme + } + + /// Returns the attachment kind. + pub fn attachment_kind(&self) -> NoteAttachmentKind { + self.content.attachment_kind() + } + + /// Returns a reference to the attachment content. + pub fn content(&self) -> &NoteAttachmentContent { + &self.content + } +} + +impl Serializable for NoteAttachment { + fn write_into(&self, target: &mut W) { + self.attachment_scheme().write_into(target); + self.content().write_into(target); + } +} + +impl Deserializable for NoteAttachment { + fn read_from(source: &mut R) -> Result { + let attachment_scheme = NoteAttachmentScheme::read_from(source)?; + let content = NoteAttachmentContent::read_from(source)?; + + Self::new(attachment_scheme, content) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +/// The content of a [`NoteAttachment`]. +/// +/// If a note attachment is not required, [`NoteAttachmentContent::None`] should be used. +/// +/// When a single [`Word`] has sufficient space, [`NoteAttachmentContent::Word`] should be used, as +/// it does not require any hashing. The word itself is encoded into the +/// [`NoteMetadata`](super::NoteMetadata). +/// +/// If the space of a [`Word`] is insufficient, the more flexible +/// [`NoteAttachmentContent::Array`] variant can be used. It contains a set of field elements +/// where only their sequential hash is encoded into the [`NoteMetadata`](super::NoteMetadata). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum NoteAttachmentContent { + /// Signals the absence of a note attachment. + #[default] + None, + + /// A note attachment consisting of a single [`Word`]. + Word(Word), + + /// A note attachment consisting of the commitment to a set of felts. + Array(NoteAttachmentArray), +} + +impl NoteAttachmentContent { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteAttachmentContent::Word`] containing an empty word. + pub fn empty_word() -> Self { + Self::Word(Word::empty()) + } + + /// Creates a new [`NoteAttachmentContent::Word`] from the provided word. + pub fn new_word(word: Word) -> Self { + Self::Word(word) + } + + /// Creates a new [`NoteAttachmentContent::Array`] from the provided elements. + /// + /// # Errors + /// + /// Returns an error if: + /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + pub fn new_array(elements: Vec) -> Result { + NoteAttachmentArray::new(elements).map(Self::from) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`NoteAttachmentKind`]. + pub fn attachment_kind(&self) -> NoteAttachmentKind { + match self { + NoteAttachmentContent::None => NoteAttachmentKind::None, + NoteAttachmentContent::Word(_) => NoteAttachmentKind::Word, + NoteAttachmentContent::Array(_) => NoteAttachmentKind::Array, + } + } + + /// Returns the [`NoteAttachmentContent`] encoded to a [`Word`]. + /// + /// See the type-level documentation for more details. + pub fn to_word(&self) -> Word { + match self { + NoteAttachmentContent::None => Word::empty(), + NoteAttachmentContent::Word(word) => *word, + NoteAttachmentContent::Array(attachment_commitment) => { + attachment_commitment.commitment() + }, + } + } +} + +impl Serializable for NoteAttachmentContent { + fn write_into(&self, target: &mut W) { + self.attachment_kind().write_into(target); + + match self { + NoteAttachmentContent::None => (), + NoteAttachmentContent::Word(word) => { + word.write_into(target); + }, + NoteAttachmentContent::Array(attachment_commitment) => { + attachment_commitment.num_elements().write_into(target); + target.write_many(&attachment_commitment.elements); + }, + } + } +} + +impl Deserializable for NoteAttachmentContent { + fn read_from(source: &mut R) -> Result { + let attachment_kind = NoteAttachmentKind::read_from(source)?; + + match attachment_kind { + NoteAttachmentKind::None => Ok(NoteAttachmentContent::None), + NoteAttachmentKind::Word => { + let word = Word::read_from(source)?; + Ok(NoteAttachmentContent::Word(word)) + }, + NoteAttachmentKind::Array => { + let num_elements = u16::read_from(source)?; + let elements = source.read_many(num_elements as usize)?; + Self::new_array(elements) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + }, + } + } +} + +// NOTE ATTACHMENT COMMITMENT +// ================================================================================================ + +/// The type contained in [`NoteAttachmentContent::Array`] that commits to a set of field +/// elements. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoteAttachmentArray { + elements: Vec, + commitment: Word, +} + +impl NoteAttachmentArray { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The maximum size of a note attachment that commits to a set of elements. + /// + /// Each element holds roughly 8 bytes of data and so this allows for a maximum of + /// 2048 * 8 = 2^14 = 16384 bytes. + pub const MAX_NUM_ELEMENTS: u16 = 2048; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteAttachmentArray`] from the provided elements. + /// + /// # Errors + /// + /// Returns an error if: + /// - The maximum number of elements exceeds [`NoteAttachmentArray::MAX_NUM_ELEMENTS`]. + pub fn new(elements: Vec) -> Result { + if elements.len() > Self::MAX_NUM_ELEMENTS as usize { + return Err(NoteError::NoteAttachmentArraySizeExceeded(elements.len())); + } + + let commitment = Hasher::hash_elements(&elements); + Ok(Self { elements, commitment }) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the elements this note attachment commits to. + pub fn as_slice(&self) -> &[Felt] { + &self.elements + } + + /// Returns the number of elements this note attachment commits to. + pub fn num_elements(&self) -> u16 { + u16::try_from(self.elements.len()).expect("type should enforce that size fits in u16") + } + + /// Returns the commitment over the contained field elements. + pub fn commitment(&self) -> Word { + self.commitment + } +} + +impl SequentialCommit for NoteAttachmentArray { + type Commitment = Word; + + fn to_elements(&self) -> Vec { + self.elements.clone() + } + + fn to_commitment(&self) -> Self::Commitment { + self.commitment + } +} + +impl From for NoteAttachmentContent { + fn from(array: NoteAttachmentArray) -> Self { + NoteAttachmentContent::Array(array) + } +} + +// NOTE ATTACHMENT SCHEME +// ================================================================================================ + +/// The user-defined type of a [`NoteAttachment`]. +/// +/// A note attachment scheme is an arbitrary 32-bit unsigned integer. +/// +/// Value `0` is reserved to signal that the scheme is none or absent. Whenever the kind of +/// attachment is not standardized or interoperability is unimportant, this none value can be +/// used. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NoteAttachmentScheme(u32); + +impl NoteAttachmentScheme { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The reserved value to signal an absent note attachment scheme. + const NONE: u32 = 0; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteAttachmentScheme`] from a `u32`. + pub const fn new(attachment_scheme: u32) -> Self { + Self(attachment_scheme) + } + + /// Returns the [`NoteAttachmentScheme`] that signals the absence of an attachment scheme. + pub const fn none() -> Self { + Self(Self::NONE) + } + + /// Returns `true` if the attachment scheme is the reserved value that signals an absent scheme, + /// `false` otherwise. + pub const fn is_none(&self) -> bool { + self.0 == Self::NONE + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the note attachment scheme as a u32. + pub const fn as_u32(&self) -> u32 { + self.0 + } +} + +impl Default for NoteAttachmentScheme { + /// Returns [`NoteAttachmentScheme::none`]. + fn default() -> Self { + Self::none() + } +} + +impl core::fmt::Display for NoteAttachmentScheme { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_fmt(format_args!("{}", self.0)) + } +} + +impl Serializable for NoteAttachmentScheme { + fn write_into(&self, target: &mut W) { + self.as_u32().write_into(target); + } +} + +impl Deserializable for NoteAttachmentScheme { + fn read_from(source: &mut R) -> Result { + let attachment_scheme = u32::read_from(source)?; + Ok(Self::new(attachment_scheme)) + } +} + +// NOTE ATTACHMENT KIND +// ================================================================================================ + +/// The type of [`NoteAttachmentContent`]. +/// +/// See its docs for more details on each type. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum NoteAttachmentKind { + /// Signals the absence of a note attachment. + #[default] + None = Self::NONE, + + /// A note attachment consisting of a single [`Word`]. + Word = Self::WORD, + + /// A note attachment consisting of the commitment to a set of felts. + Array = Self::ARRAY, +} + +impl NoteAttachmentKind { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + const NONE: u8 = 0; + const WORD: u8 = 1; + const ARRAY: u8 = 2; + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the attachment kind as a u8. + pub const fn as_u8(&self) -> u8 { + *self as u8 + } + + /// Returns `true` if the attachment kind is `None`, `false` otherwise. + pub const fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Returns `true` if the attachment kind is `Word`, `false` otherwise. + pub const fn is_word(&self) -> bool { + matches!(self, Self::Word) + } + + /// Returns `true` if the attachment kind is `Array`, `false` otherwise. + pub const fn is_array(&self) -> bool { + matches!(self, Self::Array) + } +} + +impl TryFrom for NoteAttachmentKind { + type Error = NoteError; + + fn try_from(value: u8) -> Result { + match value { + Self::NONE => Ok(Self::None), + Self::WORD => Ok(Self::Word), + Self::ARRAY => Ok(Self::Array), + _ => Err(NoteError::UnknownNoteAttachmentKind(value)), + } + } +} + +impl core::fmt::Display for NoteAttachmentKind { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let output = match self { + NoteAttachmentKind::None => "None", + NoteAttachmentKind::Word => "Word", + NoteAttachmentKind::Array => "Array", + }; + + f.write_str(output) + } +} + +impl Serializable for NoteAttachmentKind { + fn write_into(&self, target: &mut W) { + self.as_u8().write_into(target); + } +} + +impl Deserializable for NoteAttachmentKind { + fn read_from(source: &mut R) -> Result { + let attachment_kind = u8::read_from(source)?; + Self::try_from(attachment_kind) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + + #[rstest::rstest] + #[case::attachment_none(NoteAttachment::default())] + #[case::attachment_word(NoteAttachment::new_word(NoteAttachmentScheme::new(1), Word::from([3, 4, 5, 6u32])))] + #[case::attachment_array(NoteAttachment::new_array( + NoteAttachmentScheme::new(u32::MAX), + vec![Felt::new(5), Felt::new(6), Felt::new(7)], + )?)] + #[test] + fn note_attachment_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { + assert_eq!(attachment, NoteAttachment::read_from_bytes(&attachment.to_bytes())?); + Ok(()) + } + + #[test] + fn note_attachment_commitment_fails_on_too_many_elements() -> anyhow::Result<()> { + let too_many_elements = (NoteAttachmentArray::MAX_NUM_ELEMENTS as usize) + 1; + let elements = vec![Felt::from(1u32); too_many_elements]; + let err = NoteAttachmentArray::new(elements).unwrap_err(); + + assert_matches!(err, NoteError::NoteAttachmentArraySizeExceeded(len) => { + len == too_many_elements + }); + + Ok(()) + } + + #[test] + fn note_attachment_kind_fails_on_unknown_variant() -> anyhow::Result<()> { + let err = NoteAttachmentKind::try_from(3u8).unwrap_err(); + assert_matches!(err, NoteError::UnknownNoteAttachmentKind(3u8)); + Ok(()) + } +} diff --git a/crates/miden-objects/src/note/details.rs b/crates/miden-protocol/src/note/details.rs similarity index 100% rename from crates/miden-objects/src/note/details.rs rename to crates/miden-protocol/src/note/details.rs diff --git a/crates/miden-objects/src/note/execution_hint.rs b/crates/miden-protocol/src/note/execution_hint.rs similarity index 78% rename from crates/miden-objects/src/note/execution_hint.rs rename to crates/miden-protocol/src/note/execution_hint.rs index 90572de305..10d11503cb 100644 --- a/crates/miden-objects/src/note/execution_hint.rs +++ b/crates/miden-protocol/src/note/execution_hint.rs @@ -1,8 +1,9 @@ // NOTE EXECUTION HINT // ================================================================================================ +use crate::Felt; use crate::block::BlockNumber; -use crate::{Felt, NoteError}; +use crate::errors::NoteError; /// Specifies the conditions under which a note is ready to be consumed. /// These conditions are meant to be encoded in the note script as well. @@ -15,7 +16,7 @@ use crate::{Felt, NoteError}; /// [`NoteExecutionHint`] can be encoded into a [`Felt`] with the following layout: /// /// ```text -/// [26 zero bits | payload (32 bits) | tag (6 bits)] +/// [24 zero bits | payload (32 bits) | tag (8 bits)] /// ``` /// /// This way, hints such as [NoteExecutionHint::Always], are represented by `Felt::new(1)`. @@ -27,10 +28,7 @@ pub enum NoteExecutionHint { /// The note's script can be executed at any time. Always, /// The note's script can be executed after the specified block number. - /// - /// The block number cannot be [`u32::MAX`] which is enforced through the [`AfterBlockNumber`] - /// type. - AfterBlock { block_num: AfterBlockNumber }, + AfterBlock { block_num: BlockNumber }, /// The note's script can be executed in the specified slot within the specified round. /// /// The slot is defined as follows: @@ -74,13 +72,8 @@ impl NoteExecutionHint { } /// Creates a [NoteExecutionHint::AfterBlock] variant based on the given `block_num` - /// - /// # Errors - /// - /// Returns an error if `block_num` is equal to [`u32::MAX`]. - pub fn after_block(block_num: BlockNumber) -> Result { - AfterBlockNumber::new(block_num) - .map(|block_number| NoteExecutionHint::AfterBlock { block_num: block_number }) + pub fn after_block(block_num: BlockNumber) -> Self { + NoteExecutionHint::AfterBlock { block_num } } /// Creates a [NoteExecutionHint::OnBlockSlot] for the given parameters. See the variants @@ -103,7 +96,7 @@ impl NoteExecutionHint { } Ok(NoteExecutionHint::Always) }, - Self::AFTER_BLOCK_TAG => NoteExecutionHint::after_block(payload.into()), + Self::AFTER_BLOCK_TAG => Ok(NoteExecutionHint::after_block(BlockNumber::from(payload))), Self::ON_BLOCK_SLOT_TAG => { let remainder = ((payload >> 24) & 0xff) as u8; if remainder != 0 { @@ -151,15 +144,7 @@ impl NoteExecutionHint { } } - /// Encodes the [`NoteExecutionHint`] into a 6-bit tag and a 32-bit payload. - /// - /// # Guarantees - /// - /// Since the tag has at most 6 bits, the returned byte is guaranteed to have its two most - /// significant bits set to `0`. - /// - /// The payload is guaranteed to contain at least one `0` bit to make encoding it into - /// [`NoteMetadata`](crate::note::NoteMetadata) safely possible. + /// Encodes the [`NoteExecutionHint`] into an 8-bit tag and a 32-bit payload. pub fn into_parts(&self) -> (u8, u32) { match self { NoteExecutionHint::None => (Self::NONE_TAG, 0), @@ -187,12 +172,13 @@ impl From for Felt { /// Tries to convert a `u64` into a [`NoteExecutionHint`] with the expected layout documented on the /// type. /// -/// Note: The upper 26 bits are not enforced to be zero. +/// Note: The upper 24 bits are not enforced to be zero. impl TryFrom for NoteExecutionHint { type Error = NoteError; fn try_from(value: u64) -> Result { - let tag = (value & 0b111111) as u8; - let payload = ((value >> 6) & 0xffffffff) as u32; + let tag = (value & 0b1111_1111) as u8; + // Shift the payload and cut off / ignore the upper 32 bits. + let payload = (value >> 8) as u32; Self::from_parts(tag, payload) } @@ -202,51 +188,7 @@ impl TryFrom for NoteExecutionHint { impl From for u64 { fn from(value: NoteExecutionHint) -> Self { let (tag, payload) = value.into_parts(); - ((payload as u64) << 6) | (tag as u64) - } -} - -// AFTER BLOCK NUMBER -// ================================================================================================ - -/// A wrapper around a block number which enforces that it is not `u32::MAX`. -/// -/// Used for the [`NoteExecutionHint::AfterBlock`] variant where this constraint is needed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AfterBlockNumber(BlockNumber); - -impl AfterBlockNumber { - /// Creates a new [`AfterBlockNumber`] from the given `block_number`. - /// - /// # Errors - /// - /// Returns an error if: - /// - `block_number` is equal to `u32::MAX`. - pub fn new(block_number: BlockNumber) -> Result { - if block_number.as_u32() == u32::MAX { - Err(NoteError::NoteExecutionHintAfterBlockCannotBeU32Max) - } else { - Ok(Self(block_number)) - } - } - - /// Returns the block number as a `u32`. - pub fn as_u32(&self) -> u32 { - self.0.as_u32() - } -} - -impl From for u32 { - fn from(block_number: AfterBlockNumber) -> Self { - block_number.0.as_u32() - } -} - -impl TryFrom for AfterBlockNumber { - type Error = NoteError; - - fn try_from(block_number: u32) -> Result { - Self::new(block_number.into()) + ((payload as u64) << 8) | (tag as u64) } } @@ -255,7 +197,6 @@ impl TryFrom for AfterBlockNumber { #[cfg(test)] mod tests { - use assert_matches::assert_matches; use super::*; @@ -269,7 +210,7 @@ mod tests { fn test_serialization_round_trip() { assert_hint_serde(NoteExecutionHint::None); assert_hint_serde(NoteExecutionHint::Always); - assert_hint_serde(NoteExecutionHint::after_block(15.into()).unwrap()); + assert_hint_serde(NoteExecutionHint::after_block(15.into())); assert_hint_serde(NoteExecutionHint::OnBlockSlot { round_len: 9, slot_len: 12, @@ -279,7 +220,7 @@ mod tests { #[test] fn test_encode_round_trip() { - let hint = NoteExecutionHint::after_block(15.into()).unwrap(); + let hint = NoteExecutionHint::after_block(15.into()); let hint_int: u64 = hint.into(); let decoded_hint: NoteExecutionHint = hint_int.try_into().unwrap(); assert_eq!(hint, decoded_hint); @@ -305,7 +246,7 @@ mod tests { let always = NoteExecutionHint::always(); assert!(always.can_be_consumed(100.into()).unwrap()); - let after_block = NoteExecutionHint::after_block(12345.into()).unwrap(); + let after_block = NoteExecutionHint::after_block(12345.into()); assert!(!after_block.can_be_consumed(12344.into()).unwrap()); assert!(after_block.can_be_consumed(12345.into()).unwrap()); @@ -331,12 +272,4 @@ mod tests { NoteExecutionHint::from_parts(10, 1).unwrap_err(); } - - #[test] - fn test_after_block_fails_on_u32_max() { - assert_matches!( - NoteExecutionHint::after_block(u32::MAX.into()).unwrap_err(), - NoteError::NoteExecutionHintAfterBlockCannotBeU32Max - ); - } } diff --git a/crates/miden-objects/src/note/file.rs b/crates/miden-protocol/src/note/file.rs similarity index 96% rename from crates/miden-objects/src/note/file.rs rename to crates/miden-protocol/src/note/file.rs index 48a77902aa..62cd7ba863 100644 --- a/crates/miden-objects/src/note/file.rs +++ b/crates/miden-protocol/src/note/file.rs @@ -137,7 +137,6 @@ impl Deserializable for NoteFile { mod tests { use alloc::vec::Vec; - use miden_core::Felt; use miden_core::utils::{Deserializable, Serializable}; use crate::Word; @@ -172,14 +171,7 @@ mod tests { let recipient = NoteRecipient::new(serial_num, script, note_inputs); let asset = Asset::Fungible(FungibleAsset::new(faucet, 100).unwrap()); - let metadata = NoteMetadata::new( - faucet, - NoteType::Public, - NoteTag::from(123), - crate::note::NoteExecutionHint::None, - Felt::new(0), - ) - .unwrap(); + let metadata = NoteMetadata::new(faucet, NoteType::Public, NoteTag::from(123)); Note::new(NoteAssets::new(vec![asset]).unwrap(), metadata, recipient) } diff --git a/crates/miden-objects/src/note/header.rs b/crates/miden-protocol/src/note/header.rs similarity index 55% rename from crates/miden-objects/src/note/header.rs rename to crates/miden-protocol/src/note/header.rs index 93ec96df71..04aac21de4 100644 --- a/crates/miden-objects/src/note/header.rs +++ b/crates/miden-protocol/src/note/header.rs @@ -1,11 +1,8 @@ -use alloc::vec::Vec; - use super::{ ByteReader, ByteWriter, Deserializable, DeserializationError, - Felt, NoteId, NoteMetadata, Serializable, @@ -19,7 +16,7 @@ use crate::Hasher; /// Holds the strictly required, public information of a note. /// /// See [NoteId] and [NoteMetadata] for additional details. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct NoteHeader { note_id: NoteId, note_metadata: NoteMetadata, @@ -43,9 +40,14 @@ impl NoteHeader { &self.note_metadata } + /// Consumes self and returns the note header's metadata. + pub fn into_metadata(self) -> NoteMetadata { + self.note_metadata + } + /// Returns a commitment to the note and its metadata. /// - /// > hash(NOTE_ID || NOTE_METADATA) + /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) /// /// This value is used primarily for authenticating notes consumed when they are consumed /// in a transaction. @@ -59,64 +61,12 @@ impl NoteHeader { /// Returns a commitment to the note and its metadata. /// -/// > hash(NOTE_ID || NOTE_METADATA) +/// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) /// /// This value is used primarily for authenticating notes consumed when they are consumed /// in a transaction. pub fn compute_note_commitment(id: NoteId, metadata: &NoteMetadata) -> Word { - Hasher::merge(&[id.as_word(), Word::from(metadata)]) -} - -// CONVERSIONS FROM NOTE HEADER -// ================================================================================================ - -impl From for [Felt; 8] { - fn from(note_header: NoteHeader) -> Self { - (¬e_header).into() - } -} - -impl From for [Word; 2] { - fn from(note_header: NoteHeader) -> Self { - (¬e_header).into() - } -} - -impl From for [u8; 64] { - fn from(note_header: NoteHeader) -> Self { - (¬e_header).into() - } -} - -impl From<&NoteHeader> for [Felt; 8] { - fn from(note_header: &NoteHeader) -> Self { - let mut elements: [Felt; 8] = Default::default(); - elements[..4].copy_from_slice(note_header.note_id.as_elements()); - elements[4..].copy_from_slice(Word::from(note_header.metadata()).as_elements()); - elements - } -} - -impl From<&NoteHeader> for [Word; 2] { - fn from(note_header: &NoteHeader) -> Self { - let mut elements: [Word; 2] = Default::default(); - elements[0].copy_from_slice(note_header.note_id.as_elements()); - elements[1].copy_from_slice(Word::from(note_header.metadata()).as_elements()); - elements - } -} - -impl From<&NoteHeader> for [u8; 64] { - fn from(note_header: &NoteHeader) -> Self { - let mut elements: [u8; 64] = [0; 64]; - let note_metadata_bytes = Word::from(note_header.metadata()) - .iter() - .flat_map(|x| x.as_int().to_le_bytes()) - .collect::>(); - elements[..32].copy_from_slice(¬e_header.note_id.as_bytes()); - elements[32..].copy_from_slice(¬e_metadata_bytes); - elements - } + Hasher::merge(&[id.as_word(), metadata.to_commitment()]) } // SERIALIZATION diff --git a/crates/miden-objects/src/note/inputs.rs b/crates/miden-protocol/src/note/inputs.rs similarity index 66% rename from crates/miden-objects/src/note/inputs.rs rename to crates/miden-protocol/src/note/inputs.rs index 5d0366eeaf..7d7b04a0c6 100644 --- a/crates/miden-objects/src/note/inputs.rs +++ b/crates/miden-protocol/src/note/inputs.rs @@ -8,19 +8,18 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{Felt, Hasher, MAX_INPUTS_PER_NOTE, WORD_SIZE, Word, ZERO}; +use crate::{Felt, Hasher, MAX_INPUTS_PER_NOTE, Word}; // NOTE INPUTS // ================================================================================================ /// A container for note inputs. /// -/// A note can be associated with up to 128 input values. Each value is represented by a single -/// field element. Thus, note input values can contain up to ~1 KB of data. +/// A note can be associated with up to 1024 input values. Each value is represented by a single +/// field element. Thus, note input values can contain up to ~8 KB of data. /// -/// All inputs associated with a note can be reduced to a single commitment which is computed by -/// first padding the inputs with ZEROs to the next multiple of 8, and then by computing a -/// sequential hash of the resulting elements. +/// All inputs associated with a note can be reduced to a single commitment which is computed as an +/// RPO256 hash over the input elements. #[derive(Clone, Debug)] pub struct NoteInputs { values: Vec, @@ -34,13 +33,15 @@ impl NoteInputs { /// Returns [NoteInputs] instantiated from the provided values. /// /// # Errors - /// Returns an error if the number of provided inputs is greater than 128. + /// Returns an error if the number of provided inputs is greater than 1024. pub fn new(values: Vec) -> Result { if values.len() > MAX_INPUTS_PER_NOTE { return Err(NoteError::TooManyInputs(values.len())); } - Ok(pad_and_build(values)) + let commitment = Hasher::hash_elements(&values); + + Ok(Self { values, commitment }) } // PUBLIC ACCESSORS @@ -53,14 +54,14 @@ impl NoteInputs { /// Returns the number of input values. /// - /// The returned value is guaranteed to be smaller than or equal to 128. - pub fn num_values(&self) -> u8 { - const _: () = assert!(MAX_INPUTS_PER_NOTE <= u8::MAX as usize); + /// The returned value is guaranteed to be smaller than or equal to 1024. + pub fn num_values(&self) -> u16 { + const _: () = assert!(MAX_INPUTS_PER_NOTE <= u16::MAX as usize); debug_assert!( - self.values.len() < MAX_INPUTS_PER_NOTE, + self.values.len() <= MAX_INPUTS_PER_NOTE, "The constructor should have checked the number of inputs" ); - self.values.len() as u8 + self.values.len() as u16 } /// Returns a reference to the input values. @@ -69,19 +70,14 @@ impl NoteInputs { } /// Returns the note's input as a vector of field elements. - /// - /// The format is `INPUTS || PADDING`, where: - /// - /// - INPUTS is the variable inputs for the note - /// - PADDING is the optional padding to align the data with a 2WORD boundary pub fn to_elements(&self) -> Vec { - pad_inputs(&self.values) + self.values.to_vec() } } impl Default for NoteInputs { fn default() -> Self { - pad_and_build(vec![]) + Self::new(vec![]).expect("empty values should be valid") } } @@ -111,45 +107,20 @@ impl TryFrom> for NoteInputs { } } -// HELPER FUNCTIONS -// ================================================================================================ - -/// Returns a vector with built from the provided inputs and padded to the next multiple of 8. -fn pad_inputs(inputs: &[Felt]) -> Vec { - const BLOCK_SIZE: usize = WORD_SIZE * 2; - - let padded_len = inputs.len().next_multiple_of(BLOCK_SIZE); - let mut padded_inputs = Vec::with_capacity(padded_len); - padded_inputs.extend(inputs.iter()); - padded_inputs.resize(padded_len, ZERO); - - padded_inputs -} - -/// Pad `values` and returns a new `NoteInputs`. -fn pad_and_build(values: Vec) -> NoteInputs { - let commitment = { - let padded_values = pad_inputs(&values); - Hasher::hash_elements(&padded_values) - }; - - NoteInputs { values, commitment } -} - // SERIALIZATION // ================================================================================================ impl Serializable for NoteInputs { fn write_into(&self, target: &mut W) { let NoteInputs { values, commitment: _commitment } = self; - target.write_u8(values.len().try_into().expect("inputs len is not a u8 value")); + target.write_u16(values.len().try_into().expect("inputs len is not a u16 value")); target.write_many(values); } } impl Deserializable for NoteInputs { fn read_from(source: &mut R) -> Result { - let num_values = source.read_u8()? as usize; + let num_values = source.read_u16()? as usize; let values = source.read_many::(num_values)?; Self::new(values).map_err(|v| DeserializationError::InvalidValue(format!("{v}"))) } @@ -168,7 +139,7 @@ mod tests { fn test_input_ordering() { // inputs are provided in reverse stack order let inputs = vec![Felt::new(1), Felt::new(2), Felt::new(3)]; - // we expect the inputs to be padded to length 16 and to remain in reverse stack order. + // we expect the inputs to remain in reverse stack order. let expected_ordering = vec![Felt::new(1), Felt::new(2), Felt::new(3)]; let note_inputs = NoteInputs::new(inputs).expect("note created should succeed"); diff --git a/crates/miden-objects/src/note/location.rs b/crates/miden-protocol/src/note/location.rs similarity index 100% rename from crates/miden-objects/src/note/location.rs rename to crates/miden-protocol/src/note/location.rs diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs new file mode 100644 index 0000000000..d1a58ceace --- /dev/null +++ b/crates/miden-protocol/src/note/metadata.rs @@ -0,0 +1,366 @@ +use super::{ + AccountId, + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Felt, + NoteTag, + NoteType, + Serializable, + Word, +}; +use crate::Hasher; +use crate::errors::NoteError; +use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme}; + +// NOTE METADATA +// ================================================================================================ + +/// The metadata associated with a note. +/// +/// Note metadata consists of two parts: +/// - The header of the metadata, which consists of: +/// - the sender of the note +/// - the [`NoteType`] +/// - the [`NoteTag`] +/// - type information about the [`NoteAttachment`]. +/// - The optional [`NoteAttachment`]. +/// +/// # Word layout & validity +/// +/// [`NoteMetadata`] can be encoded into two words, a header and an attachment word. +/// +/// The header word has the following layout: +/// +/// ```text +/// 0th felt: [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bit)] +/// 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)] +/// ``` +/// +/// The felt validity of each part of the layout is guaranteed: +/// - 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 be set to `1`. So the +/// note type 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 value of the attachment word depends on the +/// [`NoteAttachmentKind`](crate::note::NoteAttachmentKind): +/// - [`NoteAttachmentKind::None`](crate::note::NoteAttachmentKind::None): Empty word. +/// - [`NoteAttachmentKind::Word`](crate::note::NoteAttachmentKind::Word): The raw word itself. +/// - [`NoteAttachmentKind::Array`](crate::note::NoteAttachmentKind::Array): The commitment to the +/// elements. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NoteMetadata { + /// The ID of the account which created the note. + sender: AccountId, + + /// Defines how the note is to be stored (e.g. public or private). + note_type: NoteType, + + /// A value which can be used by the recipient(s) to identify notes intended for them. + tag: NoteTag, + + /// The optional attachment of a note's metadata. + /// + /// Defaults to [`NoteAttachment::default`]. + attachment: NoteAttachment, +} + +impl NoteMetadata { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Returns a new [`NoteMetadata`] instantiated with the specified parameters. + pub fn new(sender: AccountId, note_type: NoteType, tag: NoteTag) -> Self { + Self { + sender, + note_type, + tag, + attachment: NoteAttachment::default(), + } + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the account which created the note. + pub fn sender(&self) -> AccountId { + self.sender + } + + /// Returns the note's type. + pub fn note_type(&self) -> NoteType { + self.note_type + } + + /// Returns the tag associated with the note. + pub fn tag(&self) -> NoteTag { + self.tag + } + + /// Returns the attachment of the note. + pub fn attachment(&self) -> &NoteAttachment { + &self.attachment + } + + /// Returns `true` if the note is private. + pub fn is_private(&self) -> bool { + self.note_type == NoteType::Private + } + + /// Returns the header of a [`NoteMetadata`] as a [`Word`]. + /// + /// See [`NoteMetadata`] docs for more details. + fn to_header(&self) -> NoteMetadataHeader { + NoteMetadataHeader { + sender: self.sender, + note_type: self.note_type, + tag: self.tag, + attachment_kind: self.attachment().content().attachment_kind(), + attachment_scheme: self.attachment.attachment_scheme(), + } + } + + /// Returns the [`Word`] that represents the header of a [`NoteMetadata`]. + /// + /// See [`NoteMetadata`] docs for more details. + pub fn to_header_word(&self) -> Word { + Word::from(self.to_header()) + } + + /// Returns the [`Word`] that represents the attachment of a [`NoteMetadata`]. + /// + /// See [`NoteMetadata`] docs for more details. + pub fn to_attachment_word(&self) -> Word { + self.attachment.content().to_word() + } + + /// Returns the commitment to the note metadata, which is defined as: + /// + /// ```text + /// hash(NOTE_METADATA_HEADER || NOTE_METADATA_ATTACHMENT) + /// ``` + pub fn to_commitment(&self) -> Word { + Hasher::merge(&[self.to_header_word(), self.to_attachment_word()]) + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Overwrites the note's attachment with the provided one. + pub fn set_attachment(&mut self, attachment: NoteAttachment) { + self.attachment = attachment; + } + + /// Overwrites the note's attachment with the provided one. + pub fn with_attachment(mut self, attachment: NoteAttachment) -> Self { + self.attachment = attachment; + self + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for NoteMetadata { + fn write_into(&self, target: &mut W) { + self.note_type().write_into(target); + self.sender().write_into(target); + self.tag().write_into(target); + self.attachment().write_into(target); + } +} + +impl Deserializable for NoteMetadata { + fn read_from(source: &mut R) -> Result { + let note_type = NoteType::read_from(source)?; + let sender = AccountId::read_from(source)?; + let tag = NoteTag::read_from(source)?; + let attachment = NoteAttachment::read_from(source)?; + + Ok(NoteMetadata::new(sender, note_type, tag).with_attachment(attachment)) + } +} + +// NOTE METADATA HEADER +// ================================================================================================ + +/// The header representation of [`NoteMetadata`]. +/// +/// See the metadata's type for details on this type's [`Word`] layout. +/// +/// This is intended to be a private type meant for encapsulating the conversion from and to words. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct NoteMetadataHeader { + sender: AccountId, + note_type: NoteType, + tag: NoteTag, + attachment_kind: NoteAttachmentKind, + attachment_scheme: NoteAttachmentScheme, +} + +impl From for Word { + fn from(header: NoteMetadataHeader) -> Self { + let mut metadata = Word::empty(); + + metadata[0] = merge_sender_suffix_and_note_type(header.sender.suffix(), header.note_type); + metadata[1] = header.sender.prefix().as_felt(); + metadata[2] = Felt::from(header.tag); + metadata[3] = + merge_attachment_kind_scheme(header.attachment_kind, header.attachment_scheme); + + metadata + } +} + +impl TryFrom for NoteMetadataHeader { + type Error = NoteError; + + /// Decodes a [`NoteMetadataHeader`] from a [`Word`]. + fn try_from(word: Word) -> Result { + let (sender_suffix, note_type) = unmerge_sender_suffix_and_note_type(word[0])?; + let sender_prefix = word[1]; + let tag = u32::try_from(word[2]).map(NoteTag::new).map_err(|_| { + NoteError::other("failed to convert note tag from metadata header to u32") + })?; + let (attachment_kind, attachment_scheme) = unmerge_attachment_kind_scheme(word[3])?; + + let sender = AccountId::try_from([sender_prefix, sender_suffix]).map_err(|source| { + NoteError::other_with_source("failed to decode account ID from metadata header", source) + })?; + + Ok(Self { + sender, + note_type, + tag, + attachment_kind, + attachment_scheme, + }) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Merges the suffix of an [`AccountId`] and the [`NoteType`] into a single [`Felt`]. +/// +/// The layout is as follows: +/// +/// ```text +/// [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bits)] +/// ``` +/// +/// The most significant bit of the suffix is guaranteed to be zero, so the felt retains its +/// validity. +/// +/// The `sender_id_suffix` is the suffix of the sender's account ID. +fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType) -> Felt { + let mut merged = sender_id_suffix.as_int(); + + 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; + + // 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. +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); + + let note_type_byte = element.as_int() as u8 & NOTE_TYPE_MASK; + let note_type = NoteType::try_from(note_type_byte).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_int() & SENDER_SUFFIX_MASK).expect("felt should still be valid"); + + Ok((sender_suffix, note_type)) +} + +/// Merges the [`NoteAttachmentScheme`] and [`NoteAttachmentKind`] into a single [`Felt`]. +/// +/// The layout is as follows: +/// +/// ```text +/// [30 zero bits | attachment_kind (2 bits) | attachment_scheme (32 bits)] +/// ``` +fn merge_attachment_kind_scheme( + attachment_kind: NoteAttachmentKind, + attachment_scheme: NoteAttachmentScheme, +) -> Felt { + debug_assert!(attachment_kind.as_u8() < 4, "attachment kind should fit into two bits"); + let mut merged = (attachment_kind.as_u8() as u64) << 32; + let attachment_scheme = attachment_scheme.as_u32(); + merged |= attachment_scheme as u64; + + Felt::try_from(merged).expect("the upper bit should be zero and the felt therefore valid") +} + +/// Unmerges the attachment kind and attachment scheme. +fn unmerge_attachment_kind_scheme( + element: Felt, +) -> Result<(NoteAttachmentKind, NoteAttachmentScheme), NoteError> { + let attachment_scheme = element.as_int() as u32; + let attachment_kind = (element.as_int() >> 32) as u8; + + let attachment_scheme = NoteAttachmentScheme::new(attachment_scheme); + let attachment_kind = NoteAttachmentKind::try_from(attachment_kind).map_err(|source| { + NoteError::other_with_source( + "failed to decode attachment kind from metadata header", + source, + ) + })?; + + Ok((attachment_kind, attachment_scheme)) +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + + use super::*; + use crate::note::NoteAttachmentScheme; + use crate::testing::account_id::ACCOUNT_ID_MAX_ONES; + + #[rstest::rstest] + #[case::attachment_none(NoteAttachment::default())] + #[case::attachment_raw(NoteAttachment::new_word(NoteAttachmentScheme::new(0), Word::from([3, 4, 5, 6u32])))] + #[case::attachment_commitment(NoteAttachment::new_array( + NoteAttachmentScheme::new(u32::MAX), + vec![Felt::new(5), Felt::new(6), Felt::new(7)], + )?)] + #[test] + fn note_metadata_serde(#[case] attachment: NoteAttachment) -> anyhow::Result<()> { + // Use the Account ID with the maximum one bits to test if the merge function always + // produces valid felts. + let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); + let note_type = NoteType::Public; + let tag = NoteTag::new(u32::MAX); + let metadata = NoteMetadata::new(sender, note_type, tag).with_attachment(attachment); + + // Serialization Roundtrip + let deserialized = NoteMetadata::read_from_bytes(&metadata.to_bytes())?; + assert_eq!(deserialized, metadata); + + // Metadata Header Roundtrip + let header = NoteMetadataHeader::try_from(metadata.to_header_word())?; + assert_eq!(header, metadata.to_header()); + + Ok(()) + } +} diff --git a/crates/miden-objects/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs similarity index 88% rename from crates/miden-objects/src/note/mod.rs rename to crates/miden-protocol/src/note/mod.rs index 1ecf14bfa9..27aeda9a54 100644 --- a/crates/miden-objects/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -3,7 +3,8 @@ use miden_crypto::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use miden_processor::DeserializationError; use crate::account::AccountId; -use crate::{Felt, Hasher, NoteError, WORD_SIZE, ZERO}; +use crate::errors::NoteError; +use crate::{Felt, Hasher, WORD_SIZE, ZERO}; mod assets; pub use assets::NoteAssets; @@ -20,14 +21,23 @@ pub use inputs::NoteInputs; mod metadata; pub use metadata::NoteMetadata; +mod attachment; +pub use attachment::{ + NoteAttachment, + NoteAttachmentArray, + NoteAttachmentContent, + NoteAttachmentKind, + NoteAttachmentScheme, +}; + mod execution_hint; -pub use execution_hint::{AfterBlockNumber, NoteExecutionHint}; +pub use execution_hint::NoteExecutionHint; mod note_id; pub use note_id::NoteId; mod note_tag; -pub use note_tag::{NoteExecutionMode, NoteTag}; +pub use note_tag::NoteTag; mod note_type; pub use note_type::NoteType; @@ -149,18 +159,13 @@ impl Note { /// Returns a commitment to the note and its metadata. /// - /// > hash(NOTE_ID || NOTE_METADATA) + /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) /// /// This value is used primarily for authenticating notes consumed when the are consumed /// in a transaction. pub fn commitment(&self) -> Word { self.header.commitment() } - - /// Returns true if this note is intended to be executed by the network rather than a user. - pub fn is_network_note(&self) -> bool { - self.metadata().tag().execution_mode() == NoteExecutionMode::Network - } } // AS REF @@ -175,12 +180,6 @@ impl AsRef for Note { // CONVERSIONS FROM NOTE // ================================================================================================ -impl From<&Note> for NoteHeader { - fn from(note: &Note) -> Self { - note.header - } -} - impl From for NoteHeader { fn from(note: Note) -> Self { note.header @@ -202,17 +201,7 @@ impl From for NoteDetails { impl From for PartialNote { fn from(note: Note) -> Self { let (assets, recipient, ..) = note.details.into_parts(); - PartialNote::new(*note.header.metadata(), recipient.digest(), assets) - } -} - -impl From<&Note> for PartialNote { - fn from(note: &Note) -> Self { - PartialNote::new( - *note.header.metadata(), - note.details.recipient().digest(), - note.details.assets().clone(), - ) + PartialNote::new(note.header.into_metadata(), recipient.digest(), assets) } } diff --git a/crates/miden-objects/src/note/note_id.rs b/crates/miden-protocol/src/note/note_id.rs similarity index 69% rename from crates/miden-objects/src/note/note_id.rs rename to crates/miden-protocol/src/note/note_id.rs index 96e5fab8fc..054ca5a564 100644 --- a/crates/miden-objects/src/note/note_id.rs +++ b/crates/miden-protocol/src/note/note_id.rs @@ -1,6 +1,8 @@ use alloc::string::String; use core::fmt::Display; +use miden_protocol_macros::WordWrapper; + use super::{Felt, Hasher, NoteDetails, Word}; use crate::WordError; use crate::utils::serde::{ @@ -28,7 +30,7 @@ use crate::utils::serde::{ /// - Every note can be reduced to a single unique ID. /// - To compute a note ID, we do not need to know the note's serial_num. Knowing the hash of the /// serial_num (as well as script root, input commitment, and note assets) is sufficient. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, WordWrapper)] pub struct NoteId(Word); impl NoteId { @@ -36,26 +38,6 @@ impl NoteId { pub fn new(recipient: Word, asset_commitment: Word) -> Self { Self(Hasher::merge(&[recipient, asset_commitment])) } - - /// Returns the elements representation of this note ID. - pub fn as_elements(&self) -> &[Felt] { - self.0.as_elements() - } - - /// Returns the byte representation of this note ID. - 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 digest defining this note ID. - pub fn as_word(&self) -> Word { - self.0 - } } impl Display for NoteId { @@ -73,43 +55,12 @@ impl From<&NoteDetails> for NoteId { } } -impl From for NoteId { - fn from(digest: Word) -> Self { - Self(digest) - } -} - impl NoteId { /// Attempts to convert from a hexadecimal string to [NoteId]. + /// + /// Callers must ensure the provided value is an actual [`NoteId`]. pub fn try_from_hex(hex_value: &str) -> Result { - Word::try_from(hex_value).map(NoteId::from) - } -} - -// CONVERSIONS FROM NOTE ID -// ================================================================================================ - -impl From for Word { - fn from(id: NoteId) -> Self { - id.as_word() - } -} - -impl From for [u8; 32] { - fn from(id: NoteId) -> Self { - id.0.into() - } -} - -impl From<&NoteId> for Word { - fn from(id: &NoteId) -> Self { - id.0 - } -} - -impl From<&NoteId> for [u8; 32] { - fn from(id: &NoteId) -> Self { - id.0.into() + Word::try_from(hex_value).map(NoteId::from_raw) } } diff --git a/crates/miden-protocol/src/note/note_tag.rs b/crates/miden-protocol/src/note/note_tag.rs new file mode 100644 index 0000000000..3c1ee2242a --- /dev/null +++ b/crates/miden-protocol/src/note/note_tag.rs @@ -0,0 +1,312 @@ +use core::fmt; + +use miden_crypto::Felt; + +use super::{ + AccountId, + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + NoteError, + Serializable, +}; +use crate::account::AccountStorageMode; + +// NOTE TAG +// ================================================================================================ + +/// [`NoteTag`]s are 32-bits of data that serve as best-effort filters for notes. +/// +/// Tags enable quick lookups for notes related to particular use cases, scripts, or account +/// prefixes. +/// +/// ## Account Targets +/// +/// A note targeted at an account is a note that is intended or even enforced to be consumed by a +/// specific account. One example is a P2ID note that can only be consumed by a specific account ID. +/// The tag for such a note should make it easy for the receiver to find the note. Therefore, the +/// tag encodes a certain number of bits of the receiver account's ID, by convention. Notably, it +/// may not encode the full 32 bits of the target account's ID to preserve the receiver's privacy. +/// See also the section on privacy below. +/// +/// Because this convention is widely used, the note tag provides a dedicated constructor for this: +/// [`NoteTag::with_account_target`]. +/// +/// ## Use Case Tags +/// +/// Use case notes are notes that are not intended to be consumed by a specific account, but by +/// anyone willing to fulfill the note's contract. One example is a SWAP note that trades one asset +/// against another. Such a use case note can define the structure of their note tags. A sensible +/// structure for a SWAP note could be: +/// - encoding the 2 bits of the note's type. +/// - encoding the note script root, i.e. making it identifiable as a SWAP note, for example by +/// using 16 bits of the SWAP script root. +/// - encoding the SWAP pair, for example by using 8 bits of the offered asset faucet ID and 8 bits +/// of the requested asset faucet ID. +/// +/// This allows clients to search for a public SWAP note that trades USDC against ETH only through +/// the note tag. Since tags are not validated in any way and only act as best-effort filters, +/// further local filtering is almost always necessary. For example, there could easily be a +/// collision on the 8 bits used in SWAP tag's faucet IDs. +/// +/// ## Privacy vs Efficiency +/// +/// Using note tags strikes a balance between privacy and efficiency. Without tags, querying a +/// specific note ID reveals a user's interest to the node. Conversely, downloading and filtering +/// all registered notes locally is highly inefficient. Tags allow users to adjust their level of +/// privacy by choosing how broadly or narrowly they define their search criteria, letting them find +/// the right balance between revealing too much information and incurring excessive computational +/// overhead. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] +pub struct NoteTag(u32); + +impl NoteTag { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The default note tag length for an account ID with local execution. + pub const DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH: u8 = 14; + /// The default note tag length for an account ID with network execution. + pub const DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH: u8 = 30; + /// The maximum number of bits that can be encoded into the tag for local accounts. + pub const MAX_ACCOUNT_TARGET_TAG_LENGTH: u8 = 30; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NoteTag`] from an arbitrary `u32`. + pub const fn new(tag: u32) -> Self { + Self(tag) + } + + /// Constructs a note tag that targets the given `account_id`. + /// + /// The tag is constructed as follows: + /// + /// - For local execution ([`AccountStorageMode::Private`] or [`AccountStorageMode::Public`]), + /// the two most significant bits are set to `0b00`. The following 14 bits are set to the most + /// significant bits of the account ID, and the remaining 16 bits are set to 0. + /// - For network execution ([`AccountStorageMode::Network`]), the most significant bits are set + /// to `0b00` and the remaining bits are set to the 30 most significant bits of the account + /// ID. + pub fn with_account_target(account_id: AccountId) -> Self { + match account_id.storage_mode() { + AccountStorageMode::Network => Self::from_network_account_id(account_id), + AccountStorageMode::Private | AccountStorageMode::Public => { + // safe to unwrap since DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH < + // MAX_ACCOUNT_TARGET_TAG_LENGTH + Self::with_custom_account_target( + account_id, + Self::DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH, + ) + .unwrap() + }, + } + } + + /// Constructs a note tag that targets the given `account_id` with a custom `tag_len`. + /// + /// The tag is constructed by: + /// - Setting the two most significant bits to zero. + /// - The next `tag_len` bits are set to the most significant bits of the account ID prefix. + /// - The remaining bits are set to zero. + /// + /// # Errors + /// + /// Returns an error if `tag_len` is larger than [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`]. + pub fn with_custom_account_target( + account_id: AccountId, + tag_len: u8, + ) -> Result { + if tag_len > Self::MAX_ACCOUNT_TARGET_TAG_LENGTH { + return Err(NoteError::NoteTagLengthTooLarge(tag_len)); + } + + let prefix_id: u64 = account_id.prefix().into(); + + // Shift the high bits of the account ID such that they are laid out as: + // [34 zero bits | remaining high bits (30 bits)]. + let high_bits = prefix_id >> 34; + + // This is equivalent to the following layout, interpreted as a u32: + // [2 zero bits | remaining high bits (30 bits)]. + let high_bits = high_bits as u32; + + // Select the top `tag_len` bits of the account ID, i.e.: + // [2 zero bits | remaining high bits (tag_len bits) | (30 - tag_len) zero bits]. + let high_bits = high_bits & (u32::MAX << (32 - 2 - tag_len)); + + Ok(Self(high_bits)) + } + + /// Constructs a network account note tag from the specified `account_id`. + /// + /// The tag is constructed as follows: + /// + /// - The two most significant bits are set to `0b00`. + /// - The remaining bits are set to the 30 most significant bits of the account ID. + pub(crate) fn from_network_account_id(account_id: AccountId) -> Self { + let prefix_id: u64 = account_id.prefix().into(); + + // Shift the high bits of the account ID such that they are laid out as: + // [34 zero bits | remaining high bits (30 bits)]. + let high_bits = prefix_id >> 34; + + // This is equivalent to the following layout, interpreted as a u32: + // [2 zero bits | remaining high bits (30 bits)]. + Self(high_bits as u32) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the inner u32 value of this tag. + pub fn as_u32(&self) -> u32 { + self.0 + } +} + +impl fmt::Display for NoteTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_u32()) + } +} + +// CONVERSIONS INTO NOTE TAG +// ================================================================================================ + +impl From for NoteTag { + fn from(tag: u32) -> Self { + Self::new(tag) + } +} + +// CONVERSIONS FROM NOTE TAG +// ================================================================================================ + +impl From for u32 { + fn from(tag: NoteTag) -> Self { + tag.as_u32() + } +} + +impl From for Felt { + fn from(tag: NoteTag) -> Self { + Felt::from(tag.as_u32()) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for NoteTag { + fn write_into(&self, target: &mut W) { + self.as_u32().write_into(target); + } +} + +impl Deserializable for NoteTag { + fn read_from(source: &mut R) -> Result { + let tag = u32::read_from(source)?; + Ok(Self::new(tag)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + + use super::NoteTag; + use crate::account::AccountId; + use crate::testing::account_id::{ + ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET, + ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_SENDER, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3, + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, + ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2, + ACCOUNT_ID_SENDER, + }; + + #[test] + fn from_account_id() { + let private_accounts = [ + AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(), + AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap(), + AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap(), + AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(), + AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(), + ]; + let public_accounts = [ + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(), + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(), + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE).unwrap(), + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2) + .unwrap(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2).unwrap(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1).unwrap(), + ]; + let network_accounts = [ + AccountId::try_from(ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE).unwrap(), + AccountId::try_from(ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET).unwrap(), + AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET).unwrap(), + ]; + + for account_id in private_accounts.iter().chain(public_accounts.iter()) { + let tag = NoteTag::with_account_target(*account_id); + assert_eq!(tag.as_u32() >> 30, 0, "two most significant bits should be zero"); + assert_eq!(tag.as_u32() << 16, 0, "16 least significant bits should be zero"); + assert_eq!( + (account_id.prefix().as_u64() >> 50) as u32, + tag.as_u32() >> 16, + "14 most significant bits should match" + ); + } + + for account_id in network_accounts { + let tag = NoteTag::with_account_target(account_id); + assert_eq!(tag.as_u32() >> 30, 0, "two most significant bits should be zero"); + assert_eq!( + account_id.prefix().as_u64() >> 34, + tag.as_u32() as u64, + "30 most significant bits should match" + ); + } + } + + #[test] + fn from_custom_account_target() -> anyhow::Result<()> { + let account_id = AccountId::try_from(ACCOUNT_ID_SENDER)?; + let tag = NoteTag::with_custom_account_target( + account_id, + NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH, + )?; + + assert_eq!(tag.as_u32() >> 30, 0, "two most significant bits should be zero"); + assert_eq!( + (account_id.prefix().as_u64() >> 34) as u32, + tag.as_u32(), + "30 most significant bits should match" + ); + + Ok(()) + } +} diff --git a/crates/miden-objects/src/note/note_type.rs b/crates/miden-protocol/src/note/note_type.rs similarity index 99% rename from crates/miden-objects/src/note/note_type.rs rename to crates/miden-protocol/src/note/note_type.rs index 9a5871da0d..f426ea58ab 100644 --- a/crates/miden-objects/src/note/note_type.rs +++ b/crates/miden-protocol/src/note/note_type.rs @@ -1,6 +1,8 @@ use core::fmt::Display; use core::str::FromStr; +use crate::Felt; +use crate::errors::NoteError; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -8,7 +10,6 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{Felt, NoteError}; // CONSTANTS // ================================================================================================ diff --git a/crates/miden-objects/src/note/nullifier.rs b/crates/miden-protocol/src/note/nullifier.rs similarity index 78% rename from crates/miden-objects/src/note/nullifier.rs rename to crates/miden-protocol/src/note/nullifier.rs index 8f4f3704e7..26377caa8a 100644 --- a/crates/miden-objects/src/note/nullifier.rs +++ b/crates/miden-protocol/src/note/nullifier.rs @@ -2,6 +2,7 @@ use alloc::string::String; use core::fmt::{Debug, Display, Formatter}; use miden_crypto::WordError; +use miden_protocol_macros::WordWrapper; use super::{ ByteReader, @@ -36,7 +37,7 @@ const NULLIFIER_PREFIX_SHIFT: u8 = 48; /// - We cannot derive a note's commitment from its nullifier, or a note's nullifier from its hash. /// - To compute the nullifier we must know all components of the note: serial_num, script_root, /// input_commitment and asset_commitment. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, WordWrapper)] pub struct Nullifier(Word); impl Nullifier { @@ -55,21 +56,11 @@ impl Nullifier { Self(Hasher::hash_elements(&elements)) } - /// Returns the elements of this nullifier. - pub fn as_elements(&self) -> &[Felt] { - self.0.as_elements() - } - /// Returns the most significant felt (the last element in array) pub fn most_significant_felt(&self) -> Felt { self.as_elements()[3] } - /// Returns the digest defining this nullifier. - pub fn as_word(&self) -> Word { - self.0 - } - /// Returns the prefix of this nullifier. /// /// Nullifier prefix is defined as the 16 most significant bits of the nullifier value. @@ -79,13 +70,10 @@ impl Nullifier { /// Creates a Nullifier from a hex string. Assumes that the string starts with "0x" and /// that the hexadecimal characters are big-endian encoded. + /// + /// Callers must ensure the provided value is an actual [`Nullifier`]. pub fn from_hex(hex_value: &str) -> Result { - Word::try_from(hex_value).map(Self::from) - } - - /// Returns a big-endian, hex-encoded string. - pub fn to_hex(&self) -> String { - self.0.to_hex() + Word::try_from(hex_value).map(Self::from_raw) } #[cfg(any(feature = "testing", test))] @@ -122,39 +110,6 @@ impl From<&NoteDetails> for Nullifier { } } -impl From for Nullifier { - fn from(digest: Word) -> Self { - Self(digest) - } -} - -// CONVERSIONS FROM NULLIFIER -// ================================================================================================ - -impl From for Word { - fn from(nullifier: Nullifier) -> Self { - nullifier.0 - } -} - -impl From for [u8; 32] { - fn from(nullifier: Nullifier) -> Self { - nullifier.0.into() - } -} - -impl From<&Nullifier> for Word { - fn from(nullifier: &Nullifier) -> Self { - nullifier.0 - } -} - -impl From<&Nullifier> for [u8; 32] { - fn from(nullifier: &Nullifier) -> Self { - nullifier.0.into() - } -} - // SERIALIZATION // ================================================================================================ diff --git a/crates/miden-objects/src/note/partial.rs b/crates/miden-protocol/src/note/partial.rs similarity index 82% rename from crates/miden-objects/src/note/partial.rs rename to crates/miden-protocol/src/note/partial.rs index 1b39af8a53..03553c3d2e 100644 --- a/crates/miden-objects/src/note/partial.rs +++ b/crates/miden-protocol/src/note/partial.rs @@ -23,7 +23,7 @@ use crate::Word; /// and generally does not have enough info to execute the note. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PartialNote { - metadata: NoteMetadata, + header: NoteHeader, recipient_digest: Word, assets: NoteAssets, } @@ -31,7 +31,9 @@ pub struct PartialNote { impl PartialNote { /// Returns a new [PartialNote] instantiated from the provided parameters. pub fn new(metadata: NoteMetadata, recipient_digest: Word, assets: NoteAssets) -> Self { - Self { metadata, recipient_digest, assets } + let note_id = NoteId::new(recipient_digest, assets.commitment()); + let header = NoteHeader::new(note_id, metadata); + Self { header, recipient_digest, assets } } /// Returns the ID corresponding to this note. @@ -41,7 +43,7 @@ impl PartialNote { /// Returns the metadata associated with this note. pub fn metadata(&self) -> &NoteMetadata { - &self.metadata + self.header.metadata() } /// Returns the digest of the recipient associated with this note. @@ -55,17 +57,10 @@ impl PartialNote { pub fn assets(&self) -> &NoteAssets { &self.assets } -} - -impl From<&PartialNote> for NoteHeader { - fn from(note: &PartialNote) -> Self { - NoteHeader::new(note.id(), note.metadata) - } -} -impl From for NoteHeader { - fn from(note: PartialNote) -> Self { - NoteHeader::new(note.id(), note.metadata) + /// Returns the [`NoteHeader`] of this note. + pub fn header(&self) -> &NoteHeader { + &self.header } } @@ -74,7 +69,9 @@ impl From for NoteHeader { impl Serializable for PartialNote { fn write_into(&self, target: &mut W) { - self.metadata.write_into(target); + // Serialize only metadata since the note ID in the header can be recomputed from the + // remaining data. + self.header().metadata().write_into(target); self.recipient_digest.write_into(target); self.assets.write_into(target) } diff --git a/crates/miden-objects/src/note/recipient.rs b/crates/miden-protocol/src/note/recipient.rs similarity index 100% rename from crates/miden-objects/src/note/recipient.rs rename to crates/miden-protocol/src/note/recipient.rs diff --git a/crates/miden-objects/src/note/script.rs b/crates/miden-protocol/src/note/script.rs similarity index 99% rename from crates/miden-objects/src/note/script.rs rename to crates/miden-protocol/src/note/script.rs index 457d72cfb5..eb11c82e0b 100644 --- a/crates/miden-objects/src/note/script.rs +++ b/crates/miden-protocol/src/note/script.rs @@ -6,6 +6,7 @@ use miden_processor::MastNodeExt; use super::Felt; use crate::assembly::mast::{MastForest, MastNodeId}; +use crate::errors::NoteError; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -14,7 +15,7 @@ use crate::utils::serde::{ Serializable, }; use crate::vm::Program; -use crate::{NoteError, PrettyPrint, Word}; +use crate::{PrettyPrint, Word}; // NOTE SCRIPT // ================================================================================================ diff --git a/crates/miden-protocol/src/protocol.rs b/crates/miden-protocol/src/protocol.rs new file mode 100644 index 0000000000..8ba96c9392 --- /dev/null +++ b/crates/miden-protocol/src/protocol.rs @@ -0,0 +1,70 @@ +use alloc::sync::Arc; + +use crate::assembly::Library; +use crate::assembly::mast::MastForest; +use crate::utils::serde::Deserializable; +use crate::utils::sync::LazyLock; + +// CONSTANTS +// ================================================================================================ + +const PROTOCOL_LIB_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/assets/protocol.masl")); + +// PROTOCOL LIBRARY +// ================================================================================================ + +#[derive(Clone)] +pub struct ProtocolLib(Library); + +impl ProtocolLib { + /// Returns a reference to the [`MastForest`] of the inner [`Library`]. + pub fn mast_forest(&self) -> &Arc { + self.0.mast_forest() + } +} + +impl AsRef for ProtocolLib { + fn as_ref(&self) -> &Library { + &self.0 + } +} + +impl From for Library { + fn from(value: ProtocolLib) -> Self { + value.0 + } +} + +impl Default for ProtocolLib { + fn default() -> Self { + static PROTOCOL_LIB: LazyLock = LazyLock::new(|| { + let contents = Library::read_from_bytes(PROTOCOL_LIB_BYTES) + .expect("protocol lib masl should be well-formed"); + ProtocolLib(contents) + }); + PROTOCOL_LIB.clone() + } +} + +// TESTS +// ================================================================================================ + +// NOTE: Most protocol-related tests can be found in miden-testing. +#[cfg(all(test, feature = "std"))] +mod tests { + use super::ProtocolLib; + use crate::assembly::Path; + + #[test] + fn test_compile() { + let path = Path::new("::miden::protocol::active_account::get_id"); + let miden = ProtocolLib::default(); + let exists = miden.0.module_infos().any(|module| { + module + .procedures() + .any(|(_, proc)| module.path().join(&proc.name).as_path() == path) + }); + + assert!(exists); + } +} diff --git a/crates/miden-objects/src/testing/account.rs b/crates/miden-protocol/src/testing/account.rs similarity index 100% rename from crates/miden-objects/src/testing/account.rs rename to crates/miden-protocol/src/testing/account.rs diff --git a/crates/miden-objects/src/testing/account_code.rs b/crates/miden-protocol/src/testing/account_code.rs similarity index 70% rename from crates/miden-objects/src/testing/account_code.rs rename to crates/miden-protocol/src/testing/account_code.rs index e8ddbbdbb9..bd1df53194 100644 --- a/crates/miden-objects/src/testing/account_code.rs +++ b/crates/miden-protocol/src/testing/account_code.rs @@ -7,11 +7,11 @@ use crate::account::{AccountCode, AccountComponent, AccountType}; use crate::testing::noop_auth_component::NoopAuthComponent; pub const CODE: &str = " - export.foo + pub proc foo push.1.2 mul end - export.bar + pub proc bar push.1.2 add end "; @@ -19,9 +19,10 @@ pub const CODE: &str = " impl AccountCode { /// Creates a mock [AccountCode] with default assembler and mock code pub fn mock() -> AccountCode { - let component = AccountComponent::compile(CODE, Assembler::default(), vec![]) - .unwrap() - .with_supports_all_types(); + let library = Assembler::default() + .assemble_library([CODE]) + .expect("mock account component should assemble"); + let component = AccountComponent::new(library, vec![]).unwrap().with_supports_all_types(); Self::from_components( &[NoopAuthComponent.into(), component], diff --git a/crates/miden-objects/src/testing/account_id.rs b/crates/miden-protocol/src/testing/account_id.rs similarity index 98% rename from crates/miden-objects/src/testing/account_id.rs rename to crates/miden-protocol/src/testing/account_id.rs index b90d0cc679..6147ff8546 100644 --- a/crates/miden-objects/src/testing/account_id.rs +++ b/crates/miden-protocol/src/testing/account_id.rs @@ -145,8 +145,8 @@ pub const fn account_id( /// # Example /// /// ``` -/// # use miden_objects::account::{AccountType, AccountStorageMode, AccountId}; -/// # use miden_objects::testing::account_id::{AccountIdBuilder}; +/// # use miden_protocol::account::{AccountType, AccountStorageMode, AccountId}; +/// # use miden_protocol::testing::account_id::{AccountIdBuilder}; /// /// let mut rng = rand::rng(); /// diff --git a/crates/miden-objects/src/testing/add_component.rs b/crates/miden-protocol/src/testing/add_component.rs similarity index 97% rename from crates/miden-objects/src/testing/add_component.rs rename to crates/miden-protocol/src/testing/add_component.rs index eb4b79d79f..3415a70a16 100644 --- a/crates/miden-objects/src/testing/add_component.rs +++ b/crates/miden-protocol/src/testing/add_component.rs @@ -6,7 +6,7 @@ use crate::utils::sync::LazyLock; // ================================================================================================ const ADD_CODE: &str = " - export.add5 + pub proc add5 add.5 end "; diff --git a/crates/miden-objects/src/testing/asset.rs b/crates/miden-protocol/src/testing/asset.rs similarity index 99% rename from crates/miden-objects/src/testing/asset.rs rename to crates/miden-protocol/src/testing/asset.rs index 1f6964d2aa..b1b12223bd 100644 --- a/crates/miden-objects/src/testing/asset.rs +++ b/crates/miden-protocol/src/testing/asset.rs @@ -1,9 +1,9 @@ use rand::Rng; use rand::distr::StandardUniform; -use crate::AssetError; use crate::account::{AccountId, AccountIdPrefix, AccountType}; use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; +use crate::errors::AssetError; use crate::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, diff --git a/crates/miden-objects/src/testing/block.rs b/crates/miden-protocol/src/testing/block.rs similarity index 92% rename from crates/miden-objects/src/testing/block.rs rename to crates/miden-protocol/src/testing/block.rs index 52948bece2..3ef1a2c624 100644 --- a/crates/miden-objects/src/testing/block.rs +++ b/crates/miden-protocol/src/testing/block.rs @@ -1,4 +1,4 @@ -use miden_crypto::merkle::Smt; +use miden_crypto::merkle::smt::Smt; #[cfg(not(target_family = "wasm"))] use winter_rand_utils::rand_value; @@ -6,7 +6,9 @@ use crate::Word; use crate::account::Account; use crate::block::account_tree::{AccountTree, account_id_to_smt_key}; use crate::block::{BlockHeader, BlockNumber, FeeParameters}; +use crate::crypto::dsa::ecdsa_k256_keccak::SecretKey; use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; +use crate::testing::random_signer::RandomBlockSigner; impl BlockHeader { /// Creates a mock block. The account tree is formed from the provided `accounts`, @@ -33,6 +35,7 @@ impl BlockHeader { let fee_parameters = FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500) .expect("native asset ID should be a fungible faucet ID"); + let validator_key = SecretKey::random().public_key(); #[cfg(not(target_family = "wasm"))] let ( @@ -41,7 +44,6 @@ impl BlockHeader { nullifier_root, note_root, tx_commitment, - proof_commitment, timestamp, ) = { let prev_block_commitment = rand_value::(); @@ -49,7 +51,6 @@ impl BlockHeader { let nullifier_root = rand_value::(); let note_root = note_root.unwrap_or(rand_value::()); let tx_commitment = rand_value::(); - let proof_commitment = rand_value::(); let timestamp = rand_value(); ( @@ -58,7 +59,6 @@ impl BlockHeader { nullifier_root, note_root, tx_commitment, - proof_commitment, timestamp, ) }; @@ -70,7 +70,6 @@ impl BlockHeader { nullifier_root, note_root, tx_commitment, - proof_commitment, timestamp, ) = { ( @@ -80,7 +79,6 @@ impl BlockHeader { note_root.unwrap_or_default(), Default::default(), Default::default(), - Default::default(), ) }; @@ -94,7 +92,7 @@ impl BlockHeader { note_root, tx_commitment, tx_kernel_commitment, - proof_commitment, + validator_key, fee_parameters, timestamp, ) diff --git a/crates/miden-objects/src/testing/block_note_tree.rs b/crates/miden-protocol/src/testing/block_note_tree.rs similarity index 93% rename from crates/miden-objects/src/testing/block_note_tree.rs rename to crates/miden-protocol/src/testing/block_note_tree.rs index 9779c06c57..3304527f9a 100644 --- a/crates/miden-objects/src/testing/block_note_tree.rs +++ b/crates/miden-protocol/src/testing/block_note_tree.rs @@ -19,7 +19,7 @@ impl BlockNoteTree { // SAFETY: This is only called from test code. Reconsider if this changes. let block_note_index = BlockNoteIndex::new(batch_idx, *note_idx_in_batch) .expect("output note batch indices should fit into a block"); - (block_note_index, note.id(), *note.metadata()) + (block_note_index, note.id(), note.metadata()) }) }); diff --git a/crates/miden-objects/src/testing/constants.rs b/crates/miden-protocol/src/testing/constants.rs similarity index 100% rename from crates/miden-objects/src/testing/constants.rs rename to crates/miden-protocol/src/testing/constants.rs diff --git a/crates/miden-lib/src/testing/mock_util_lib.rs b/crates/miden-protocol/src/testing/mock_util_lib.rs similarity index 52% rename from crates/miden-lib/src/testing/mock_util_lib.rs rename to crates/miden-protocol/src/testing/mock_util_lib.rs index 5f6c99cb37..f9d454f5ee 100644 --- a/crates/miden-lib/src/testing/mock_util_lib.rs +++ b/crates/miden-protocol/src/testing/mock_util_lib.rs @@ -1,30 +1,28 @@ -use miden_objects::assembly::Library; -use miden_objects::assembly::diagnostics::NamedSource; -use miden_objects::utils::sync::LazyLock; +use miden_assembly::diagnostics::NamedSource; +use crate::assembly::Library; use crate::transaction::TransactionKernel; +use crate::utils::sync::LazyLock; const MOCK_UTIL_LIBRARY_CODE: &str = " - use.miden::output_note + use miden::protocol::output_note - # Inputs: [] - # Outputs: [note_idx] - export.create_random_note + #! Inputs: [] + #! Outputs: [note_idx] + pub proc create_default_note push.1.2.3.4 # = RECIPIENT - push.1 # = NoteExecutionHint::Always push.2 # = NoteType::Private - push.0 # = aux - push.0xc0000000 # = NoteTag::LocalAny - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] + push.0 # = NoteTag + # => [tag, note_type, RECIPIENT] exec.output_note::create # => [note_idx] end - # Inputs: [ASSET] - # Outputs: [] - export.create_random_note_with_asset - exec.create_random_note + #! Inputs: [ASSET] + #! Outputs: [] + pub proc create_default_note_with_asset + exec.create_default_note # => [note_idx, ASSET] movdn.4 @@ -36,9 +34,8 @@ const MOCK_UTIL_LIBRARY_CODE: &str = " "; static MOCK_UTIL_LIBRARY: LazyLock = LazyLock::new(|| { - let source = NamedSource::new("mock::util", MOCK_UTIL_LIBRARY_CODE); TransactionKernel::assembler() - .assemble_library([source]) + .assemble_library([NamedSource::new("mock::util", MOCK_UTIL_LIBRARY_CODE)]) .expect("mock util library should be valid") }); diff --git a/crates/miden-objects/src/testing/mod.rs b/crates/miden-protocol/src/testing/mod.rs similarity index 79% rename from crates/miden-objects/src/testing/mod.rs rename to crates/miden-protocol/src/testing/mod.rs index 7bf6c65286..9b04908a64 100644 --- a/crates/miden-objects/src/testing/mod.rs +++ b/crates/miden-protocol/src/testing/mod.rs @@ -6,8 +6,11 @@ pub mod asset; pub mod block; pub mod block_note_tree; pub mod constants; +pub mod mock_util_lib; pub mod noop_auth_component; pub mod note; pub mod partial_blockchain; +pub mod random_signer; +pub mod slot_name; pub mod storage; pub mod tx; diff --git a/crates/miden-objects/src/testing/noop_auth_component.rs b/crates/miden-protocol/src/testing/noop_auth_component.rs similarity index 97% rename from crates/miden-objects/src/testing/noop_auth_component.rs rename to crates/miden-protocol/src/testing/noop_auth_component.rs index 8ebc9fb800..35b7a79126 100644 --- a/crates/miden-objects/src/testing/noop_auth_component.rs +++ b/crates/miden-protocol/src/testing/noop_auth_component.rs @@ -6,7 +6,7 @@ use crate::utils::sync::LazyLock; // ================================================================================================ const NOOP_AUTH_CODE: &str = " - export.auth_noop + pub proc auth_noop push.0 drop end "; diff --git a/crates/miden-objects/src/testing/note.rs b/crates/miden-protocol/src/testing/note.rs similarity index 87% rename from crates/miden-objects/src/testing/note.rs rename to crates/miden-protocol/src/testing/note.rs index 1ffd6c8168..56f2bedf84 100644 --- a/crates/miden-objects/src/testing/note.rs +++ b/crates/miden-protocol/src/testing/note.rs @@ -1,11 +1,11 @@ use alloc::vec::Vec; +use crate::Word; use crate::assembly::Assembler; use crate::asset::FungibleAsset; use crate::note::{ Note, NoteAssets, - NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, @@ -14,7 +14,6 @@ use crate::note::{ NoteType, }; use crate::testing::account_id::ACCOUNT_ID_SENDER; -use crate::{Word, ZERO}; pub const DEFAULT_NOTE_CODE: &str = "begin nop end"; @@ -28,11 +27,8 @@ impl Note { let metadata = NoteMetadata::new( sender_id, NoteType::Private, - NoteTag::from_account_id(sender_id), - NoteExecutionHint::Always, - ZERO, - ) - .unwrap(); + NoteTag::with_account_target(sender_id), + ); let inputs = NoteInputs::new(Vec::new()).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, inputs); diff --git a/crates/miden-objects/src/testing/partial_blockchain.rs b/crates/miden-protocol/src/testing/partial_blockchain.rs similarity index 98% rename from crates/miden-objects/src/testing/partial_blockchain.rs rename to crates/miden-protocol/src/testing/partial_blockchain.rs index 4497abbdf3..325c12566e 100644 --- a/crates/miden-objects/src/testing/partial_blockchain.rs +++ b/crates/miden-protocol/src/testing/partial_blockchain.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; -use crate::PartialBlockchainError; use crate::block::{BlockHeader, BlockNumber, Blockchain}; +use crate::errors::PartialBlockchainError; use crate::transaction::PartialBlockchain; impl PartialBlockchain { diff --git a/crates/miden-protocol/src/testing/random_signer.rs b/crates/miden-protocol/src/testing/random_signer.rs new file mode 100644 index 0000000000..4d104e2f21 --- /dev/null +++ b/crates/miden-protocol/src/testing/random_signer.rs @@ -0,0 +1,22 @@ +// NO STD ECDSA SIGNER +// ================================================================================================ + +use crate::block::BlockSigner; +use crate::crypto::dsa::ecdsa_k256_keccak::SecretKey; + +/// An insecure, random block signer for testing purposes. +pub trait RandomBlockSigner: BlockSigner { + fn random() -> Self; +} + +// NO STD SECRET KEY BLOCK SIGNER +// ================================================================================================ + +impl RandomBlockSigner for SecretKey { + fn random() -> Self { + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; + let mut rng = ChaCha20Rng::from_os_rng(); + SecretKey::with_rng(&mut rng) + } +} diff --git a/crates/miden-protocol/src/testing/slot_name.rs b/crates/miden-protocol/src/testing/slot_name.rs new file mode 100644 index 0000000000..e37e00dd00 --- /dev/null +++ b/crates/miden-protocol/src/testing/slot_name.rs @@ -0,0 +1,8 @@ +use crate::account::StorageSlotName; + +impl StorageSlotName { + /// Returns a new slot name with the format `"miden::test::slot::{index}"`. + pub fn mock(index: usize) -> Self { + Self::new(format!("miden::test::slot::{index}")).expect("storage slot name should be valid") + } +} diff --git a/crates/miden-protocol/src/testing/storage.rs b/crates/miden-protocol/src/testing/storage.rs new file mode 100644 index 0000000000..c4c0fc47cb --- /dev/null +++ b/crates/miden-protocol/src/testing/storage.rs @@ -0,0 +1,150 @@ +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use miden_core::{Felt, Word}; + +use crate::account::{ + AccountStorage, + AccountStorageDelta, + StorageMap, + StorageMapDelta, + StorageSlot, + StorageSlotDelta, + StorageSlotName, +}; +use crate::note::NoteAssets; +use crate::utils::sync::LazyLock; + +// ACCOUNT STORAGE DELTA +// ================================================================================================ + +impl AccountStorageDelta { + // CONSTRUCTORS + // ---------------------------------------------------------------------------------------- + + /// Creates an [`AccountStorageDelta`] from the given iterators. + pub fn from_iters( + cleared_values: impl IntoIterator, + updated_values: impl IntoIterator, + updated_maps: impl IntoIterator, + ) -> Self { + let deltas = + cleared_values + .into_iter() + .map(|slot_name| (slot_name, StorageSlotDelta::with_empty_value())) + .chain(updated_values.into_iter().map(|(slot_name, slot_value)| { + (slot_name, StorageSlotDelta::Value(slot_value)) + })) + .chain( + updated_maps.into_iter().map(|(slot_name, map_delta)| { + (slot_name, StorageSlotDelta::Map(map_delta)) + }), + ) + .collect(); + + Self::from_raw(deltas) + } + + // MUTATORS + // ------------------------------------------------------------------------------------------- + + pub fn add_cleared_items(mut self, items: impl IntoIterator) -> Self { + items + .into_iter() + .for_each(|slot_name| self.set_item(slot_name, Word::empty()).expect("TODO")); + + self + } + + pub fn add_updated_values( + mut self, + items: impl IntoIterator, + ) -> Self { + items.into_iter().for_each(|(slot_name, slot_value)| { + self.set_item(slot_name, slot_value).expect("TODO") + }); + + self + } + + pub fn add_updated_maps( + mut self, + items: impl IntoIterator, + ) -> Self { + items.into_iter().for_each(|(slot_name, map_delta)| { + for (key, value) in map_delta.entries() { + self.set_map_item(slot_name.clone(), *key.inner(), *value).expect("TODO") + } + }); + + self + } +} + +// CONSTANTS +// ================================================================================================ + +pub static MOCK_VALUE_SLOT0: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::test::value0").expect("storage slot name should be valid") +}); +pub static MOCK_VALUE_SLOT1: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::test::value1").expect("storage slot name should be valid") +}); +pub static MOCK_MAP_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::test::map").expect("storage slot name should be valid") +}); + +pub const STORAGE_VALUE_0: Word = + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); +pub const STORAGE_VALUE_1: Word = + Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]); +pub const STORAGE_LEAVES_2: [(Word, Word); 2] = [ + ( + Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), + Word::new([Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)]), + ), + ( + Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]), + Word::new([Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)]), + ), +]; + +impl AccountStorage { + /// Create account storage. + pub fn mock() -> Self { + AccountStorage::new(Self::mock_storage_slots()).unwrap() + } + + pub fn mock_storage_slots() -> Vec { + vec![Self::mock_value_slot0(), Self::mock_value_slot1(), Self::mock_map_slot()] + } + + pub fn mock_value_slot0() -> StorageSlot { + StorageSlot::with_value(MOCK_VALUE_SLOT0.clone(), STORAGE_VALUE_0) + } + + pub fn mock_value_slot1() -> StorageSlot { + StorageSlot::with_value(MOCK_VALUE_SLOT1.clone(), STORAGE_VALUE_1) + } + + pub fn mock_map_slot() -> StorageSlot { + StorageSlot::with_map(MOCK_MAP_SLOT.clone(), Self::mock_map()) + } + + pub fn mock_map() -> StorageMap { + StorageMap::with_entries(STORAGE_LEAVES_2).unwrap() + } +} + +// UTILITIES +// -------------------------------------------------------------------------------------------- + +/// Returns a list of strings, one for each note asset. +pub fn prepare_assets(note_assets: &NoteAssets) -> Vec { + let mut assets = Vec::new(); + for &asset in note_assets.iter() { + let asset_word = Word::from(asset); + assets.push(asset_word.to_string()); + } + assets +} diff --git a/crates/miden-objects/src/testing/tx.rs b/crates/miden-protocol/src/testing/tx.rs similarity index 100% rename from crates/miden-objects/src/testing/tx.rs rename to crates/miden-protocol/src/testing/tx.rs diff --git a/crates/miden-objects/src/transaction/executed_tx.rs b/crates/miden-protocol/src/transaction/executed_tx.rs similarity index 99% rename from crates/miden-objects/src/transaction/executed_tx.rs rename to crates/miden-protocol/src/transaction/executed_tx.rs index 65084eb1fc..27bd067a81 100644 --- a/crates/miden-objects/src/transaction/executed_tx.rs +++ b/crates/miden-protocol/src/transaction/executed_tx.rs @@ -5,7 +5,6 @@ use super::{ AccountHeader, AccountId, AdviceInputs, - BlockHeader, InputNote, InputNotes, NoteId, @@ -16,7 +15,7 @@ use super::{ }; use crate::account::PartialAccount; use crate::asset::FungibleAsset; -use crate::block::BlockNumber; +use crate::block::{BlockHeader, BlockNumber}; use crate::transaction::TransactionInputs; use crate::utils::serde::{ ByteReader, diff --git a/crates/miden-objects/src/transaction/inputs/account.rs b/crates/miden-protocol/src/transaction/inputs/account.rs similarity index 96% rename from crates/miden-objects/src/transaction/inputs/account.rs rename to crates/miden-protocol/src/transaction/inputs/account.rs index 4d7dd2faf9..f0c2296323 100644 --- a/crates/miden-objects/src/transaction/inputs/account.rs +++ b/crates/miden-protocol/src/transaction/inputs/account.rs @@ -1,8 +1,8 @@ use crate::Word; use crate::account::{AccountCode, AccountId, PartialAccount, PartialStorage}; use crate::asset::PartialVault; -use crate::block::AccountWitness; -use crate::crypto::merkle::{SmtProof, SmtProofError}; +use crate::block::account_tree::AccountWitness; +use crate::crypto::merkle::smt::{SmtProof, SmtProofError}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; // ACCOUNT INPUTS @@ -103,7 +103,7 @@ mod tests { use crate::account::{Account, AccountCode, AccountId, AccountStorage, PartialAccount}; use crate::asset::AssetVault; - use crate::block::AccountWitness; + use crate::block::account_tree::AccountWitness; use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use crate::transaction::AccountInputs; diff --git a/crates/miden-protocol/src/transaction/inputs/mod.rs b/crates/miden-protocol/src/transaction/inputs/mod.rs new file mode 100644 index 0000000000..a8122e0908 --- /dev/null +++ b/crates/miden-protocol/src/transaction/inputs/mod.rs @@ -0,0 +1,538 @@ +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec::Vec; +use core::fmt::Debug; + +use miden_core::utils::{Deserializable, Serializable}; +use miden_crypto::merkle::NodeIndex; +use miden_crypto::merkle::smt::{LeafIndex, SmtLeaf, SmtProof}; + +use super::PartialBlockchain; +use crate::account::{ + AccountCode, + AccountHeader, + AccountId, + AccountStorageHeader, + PartialAccount, + PartialStorage, + StorageMap, + StorageMapWitness, + StorageSlotId, + StorageSlotName, +}; +use crate::asset::{AssetVaultKey, AssetWitness, PartialVault}; +use crate::block::account_tree::{AccountWitness, account_id_to_smt_index}; +use crate::block::{BlockHeader, BlockNumber}; +use crate::crypto::merkle::SparseMerklePath; +use crate::errors::{TransactionInputError, TransactionInputsExtractionError}; +use crate::note::{Note, NoteInclusionProof}; +use crate::transaction::{TransactionAdviceInputs, TransactionArgs, TransactionScript}; +use crate::{Felt, Word}; + +#[cfg(test)] +mod tests; + +mod account; +pub use account::AccountInputs; + +mod notes; +use miden_processor::{AdviceInputs, SMT_DEPTH}; +pub use notes::{InputNote, InputNotes, ToInputNoteCommitments}; + +// TRANSACTION INPUTS +// ================================================================================================ + +/// Contains the data required to execute a transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransactionInputs { + account: PartialAccount, + block_header: BlockHeader, + blockchain: PartialBlockchain, + input_notes: InputNotes, + tx_args: TransactionArgs, + advice_inputs: AdviceInputs, + foreign_account_code: Vec, + /// Pre-fetched asset witnesses for note assets and the fee asset. + asset_witnesses: Vec, + /// Storage slot names for foreign accounts. + foreign_account_slot_names: BTreeMap, +} + +impl TransactionInputs { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + + /// Returns new [`TransactionInputs`] instantiated with the specified parameters. + /// + /// # Errors + /// + /// Returns an error if: + /// - The partial blockchain does not track the block headers required to prove inclusion of any + /// authenticated input note. + pub fn new( + account: PartialAccount, + block_header: BlockHeader, + blockchain: PartialBlockchain, + input_notes: InputNotes, + ) -> Result { + // Check that the partial blockchain and block header are consistent. + if blockchain.chain_length() != block_header.block_num() { + return Err(TransactionInputError::InconsistentChainLength { + expected: block_header.block_num(), + actual: blockchain.chain_length(), + }); + } + if blockchain.peaks().hash_peaks() != block_header.chain_commitment() { + return Err(TransactionInputError::InconsistentChainCommitment { + expected: block_header.chain_commitment(), + actual: blockchain.peaks().hash_peaks(), + }); + } + // Validate the authentication paths of the input notes. + for note in input_notes.iter() { + if let InputNote::Authenticated { note, proof } = note { + let note_block_num = proof.location().block_num(); + let block_header = if note_block_num == block_header.block_num() { + &block_header + } else { + blockchain.get_block(note_block_num).ok_or( + TransactionInputError::InputNoteBlockNotInPartialBlockchain(note.id()), + )? + }; + validate_is_in_block(note, proof, block_header)?; + } + } + + Ok(Self { + account, + block_header, + blockchain, + input_notes, + tx_args: TransactionArgs::default(), + advice_inputs: AdviceInputs::default(), + foreign_account_code: Vec::new(), + asset_witnesses: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), + }) + } + + /// Replaces the transaction inputs and assigns the given asset witnesses. + pub fn with_asset_witnesses(mut self, witnesses: Vec) -> Self { + self.asset_witnesses = witnesses; + self + } + + /// Replaces the transaction inputs and assigns the given foreign account code. + pub fn with_foreign_account_code(mut self, foreign_account_code: Vec) -> Self { + self.foreign_account_code = foreign_account_code; + self + } + + /// Replaces the transaction inputs and assigns the given transaction arguments. + pub fn with_tx_args(mut self, tx_args: TransactionArgs) -> Self { + self.set_tx_args_inner(tx_args); + self + } + + /// Replaces the transaction inputs and assigns the given foreign account slot names. + pub fn with_foreign_account_slot_names( + mut self, + foreign_account_slot_names: BTreeMap, + ) -> Self { + self.foreign_account_slot_names = foreign_account_slot_names; + self + } + + /// Replaces the transaction inputs and assigns the given advice inputs. + pub fn with_advice_inputs(mut self, advice_inputs: AdviceInputs) -> Self { + self.set_advice_inputs(advice_inputs); + self + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Replaces the input notes for the transaction. + pub fn set_input_notes(&mut self, new_notes: Vec) { + self.input_notes = new_notes.into(); + } + + /// Replaces the advice inputs for the transaction. + /// + /// Note: the advice stack from the provided advice inputs is discarded. + pub fn set_advice_inputs(&mut self, new_advice_inputs: AdviceInputs) { + let AdviceInputs { map, store, .. } = new_advice_inputs; + self.advice_inputs = AdviceInputs { stack: Default::default(), map, store }; + self.tx_args.extend_advice_inputs(self.advice_inputs.clone()); + } + + /// Updates the transaction arguments of the inputs. + #[cfg(feature = "testing")] + pub fn set_tx_args(&mut self, tx_args: TransactionArgs) { + self.set_tx_args_inner(tx_args); + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the account against which the transaction is executed. + pub fn account(&self) -> &PartialAccount { + &self.account + } + + /// Returns block header for the block referenced by the transaction. + pub fn block_header(&self) -> &BlockHeader { + &self.block_header + } + + /// Returns partial blockchain containing authentication paths for all notes consumed by the + /// transaction. + pub fn blockchain(&self) -> &PartialBlockchain { + &self.blockchain + } + + /// Returns the notes to be consumed in the transaction. + pub fn input_notes(&self) -> &InputNotes { + &self.input_notes + } + + /// Returns the block number referenced by the inputs. + pub fn ref_block(&self) -> BlockNumber { + self.block_header.block_num() + } + + /// Returns the transaction script to be executed. + pub fn tx_script(&self) -> Option<&TransactionScript> { + self.tx_args.tx_script() + } + + /// Returns the foreign account code to be executed. + pub fn foreign_account_code(&self) -> &[AccountCode] { + &self.foreign_account_code + } + + /// Returns the pre-fetched witnesses for note and fee assets. + pub fn asset_witnesses(&self) -> &[AssetWitness] { + &self.asset_witnesses + } + + /// Returns the foreign account storage slot names. + pub fn foreign_account_slot_names(&self) -> &BTreeMap { + &self.foreign_account_slot_names + } + + /// Returns the advice inputs to be consumed in the transaction. + pub fn advice_inputs(&self) -> &AdviceInputs { + &self.advice_inputs + } + + /// Returns the transaction arguments to be consumed in the transaction. + pub fn tx_args(&self) -> &TransactionArgs { + &self.tx_args + } + + // DATA EXTRACTORS + // -------------------------------------------------------------------------------------------- + + /// Reads the storage map witness for the given account and map key. + pub fn read_storage_map_witness( + &self, + map_root: Word, + map_key: Word, + ) -> Result { + // Convert map key into the index at which the key-value pair for this key is stored + let leaf_index = StorageMap::map_key_to_leaf_index(map_key); + + // Construct sparse Merkle path. + let merkle_path = self.advice_inputs.store.get_path(map_root, leaf_index.into())?; + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.path)?; + + // Construct SMT leaf. + let merkle_node = self.advice_inputs.store.get_node(map_root, leaf_index.into())?; + let smt_leaf_elements = self + .advice_inputs + .map + .get(&merkle_node) + .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; + let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, leaf_index)?; + + // Construct SMT proof and witness. + let smt_proof = SmtProof::new(sparse_path, smt_leaf)?; + let storage_witness = StorageMapWitness::new(smt_proof, [map_key])?; + + Ok(storage_witness) + } + + /// Reads the vault asset witnesses for the given account and vault keys. + pub fn read_vault_asset_witnesses( + &self, + vault_root: Word, + vault_keys: BTreeSet, + ) -> Result, TransactionInputsExtractionError> { + let mut asset_witnesses = Vec::new(); + for vault_key in vault_keys { + let smt_index = vault_key.to_leaf_index(); + // Construct sparse Merkle path. + let merkle_path = self.advice_inputs.store.get_path(vault_root, smt_index.into())?; + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.path)?; + + // Construct SMT leaf. + let merkle_node = self.advice_inputs.store.get_node(vault_root, smt_index.into())?; + let smt_leaf_elements = self + .advice_inputs + .map + .get(&merkle_node) + .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; + let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, smt_index)?; + + // Construct SMT proof and witness. + let smt_proof = SmtProof::new(sparse_path, smt_leaf)?; + let asset_witness = AssetWitness::new(smt_proof)?; + asset_witnesses.push(asset_witness); + } + Ok(asset_witnesses) + } + + /// Reads AccountInputs for a foreign account from the advice inputs. + /// + /// This function reverses the process of [`TransactionAdviceInputs::add_foreign_accounts`] by: + /// 1. Reading the account header from the advice map using the account_id_key. + /// 2. Building a PartialAccount from the header and foreign account code. + /// 3. Creating an AccountWitness. + pub fn read_foreign_account_inputs( + &self, + account_id: AccountId, + ) -> Result { + if account_id == self.account().id() { + return Err(TransactionInputsExtractionError::AccountNotForeign); + } + + // Read the account header elements from the advice map. + let account_id_key = TransactionAdviceInputs::account_id_map_key(account_id); + let header_elements = self + .advice_inputs + .map + .get(&account_id_key) + .ok_or(TransactionInputsExtractionError::ForeignAccountNotFound(account_id))?; + + // Parse the header from elements. + let header = AccountHeader::try_from_elements(header_elements)?; + + // Construct and return account inputs. + let partial_account = self.read_foreign_partial_account(&header)?; + let witness = self.read_foreign_account_witness(&header)?; + Ok(AccountInputs::new(partial_account, witness)) + } + + /// Reads a foreign partial account from the advice inputs based on the account ID corresponding + /// to the provided header. + fn read_foreign_partial_account( + &self, + header: &AccountHeader, + ) -> Result { + // Derive the partial vault from the header. + let partial_vault = PartialVault::new(header.vault_root()); + + // Find the corresponding foreign account code. + let account_code = self + .foreign_account_code + .iter() + .find(|code| code.commitment() == header.code_commitment()) + .ok_or(TransactionInputsExtractionError::ForeignAccountCodeNotFound(header.id()))? + .clone(); + + // Try to get storage header from advice map using storage commitment as key. + let storage_header_elements = self + .advice_inputs + .map + .get(&header.storage_commitment()) + .ok_or(TransactionInputsExtractionError::StorageHeaderNotFound(header.id()))?; + + // Get slot names for this foreign account, or use empty map if not available. + let storage_header = AccountStorageHeader::try_from_elements( + storage_header_elements, + self.foreign_account_slot_names(), + )?; + + // Build partial storage. + let partial_storage = PartialStorage::new(storage_header, [])?; + + // Create the partial account. + let partial_account = PartialAccount::new( + header.id(), + header.nonce(), + account_code, + partial_storage, + partial_vault, + None, // We know that foreign accounts are existing accounts so a seed is not required. + )?; + + Ok(partial_account) + } + + /// Reads a foreign account witness from the advice inputs based on the account ID corresponding + /// to the provided header. + fn read_foreign_account_witness( + &self, + header: &AccountHeader, + ) -> Result { + // Get the account tree root from the block header. + let account_tree_root = self.block_header.account_root(); + let leaf_index: NodeIndex = account_id_to_smt_index(header.id()).into(); + + // Get the Merkle path from the merkle store. + let merkle_path = self.advice_inputs.store.get_path(account_tree_root, leaf_index)?; + + // Convert the Merkle path to SparseMerklePath. + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.path)?; + + // Create the account witness. + let witness = AccountWitness::new(header.id(), header.commitment(), sparse_path)?; + + Ok(witness) + } + + // CONVERSIONS + // -------------------------------------------------------------------------------------------- + + /// Consumes these transaction inputs and returns their underlying components. + pub fn into_parts( + self, + ) -> ( + PartialAccount, + BlockHeader, + PartialBlockchain, + InputNotes, + TransactionArgs, + ) { + (self.account, self.block_header, self.blockchain, self.input_notes, self.tx_args) + } + + // HELPER METHODS + // -------------------------------------------------------------------------------------------- + + /// Replaces the current tx_args with the provided value. + /// + /// This also appends advice inputs from these transaction inputs to the advice inputs of the + /// tx args. + fn set_tx_args_inner(&mut self, tx_args: TransactionArgs) { + self.tx_args = tx_args; + self.tx_args.extend_advice_inputs(self.advice_inputs.clone()); + } +} + +// SERIALIZATION / DESERIALIZATION +// ================================================================================================ + +impl Serializable for TransactionInputs { + fn write_into(&self, target: &mut W) { + self.account.write_into(target); + self.block_header.write_into(target); + self.blockchain.write_into(target); + self.input_notes.write_into(target); + self.tx_args.write_into(target); + self.advice_inputs.write_into(target); + self.foreign_account_code.write_into(target); + self.asset_witnesses.write_into(target); + self.foreign_account_slot_names.write_into(target); + } +} + +impl Deserializable for TransactionInputs { + fn read_from( + source: &mut R, + ) -> Result { + let account = PartialAccount::read_from(source)?; + let block_header = BlockHeader::read_from(source)?; + let blockchain = PartialBlockchain::read_from(source)?; + let input_notes = InputNotes::read_from(source)?; + let tx_args = TransactionArgs::read_from(source)?; + let advice_inputs = AdviceInputs::read_from(source)?; + let foreign_account_code = Vec::::read_from(source)?; + let asset_witnesses = Vec::::read_from(source)?; + let foreign_account_slot_names = + BTreeMap::::read_from(source)?; + + Ok(TransactionInputs { + account, + block_header, + blockchain, + input_notes, + tx_args, + advice_inputs, + foreign_account_code, + asset_witnesses, + foreign_account_slot_names, + }) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +// TODO(sergerad): Move this fn to crypto SmtLeaf::try_from_elements. +pub fn smt_leaf_from_elements( + elements: &[Felt], + leaf_index: LeafIndex, +) -> Result { + use miden_crypto::merkle::smt::SmtLeaf; + + // Based on the miden-crypto SMT leaf serialization format. + + if elements.is_empty() { + return Ok(SmtLeaf::new_empty(leaf_index)); + } + + // Elements should be organized into a contiguous array of K/V Words (4 Felts each). + if !elements.len().is_multiple_of(8) { + return Err(TransactionInputsExtractionError::LeafConversionError( + "invalid SMT leaf format: elements length must be divisible by 8".into(), + )); + } + + let num_entries = elements.len() / 8; + + if num_entries == 1 { + // Single entry. + let key = Word::new([elements[0], elements[1], elements[2], elements[3]]); + let value = Word::new([elements[4], elements[5], elements[6], elements[7]]); + Ok(SmtLeaf::new_single(key, value)) + } else { + // Multiple entries. + let mut entries = Vec::with_capacity(num_entries); + // Read k/v pairs from each entry. + for i in 0..num_entries { + let base_idx = i * 8; + let key = Word::new([ + elements[base_idx], + elements[base_idx + 1], + elements[base_idx + 2], + elements[base_idx + 3], + ]); + let value = Word::new([ + elements[base_idx + 4], + elements[base_idx + 5], + elements[base_idx + 6], + elements[base_idx + 7], + ]); + entries.push((key, value)); + } + let leaf = SmtLeaf::new_multiple(entries)?; + Ok(leaf) + } +} + +/// Validates whether the provided note belongs to the note tree of the specified block. +fn validate_is_in_block( + note: &Note, + proof: &NoteInclusionProof, + block_header: &BlockHeader, +) -> Result<(), TransactionInputError> { + let note_index = proof.location().node_index_in_block().into(); + let note_commitment = note.commitment(); + proof + .note_path() + .verify(note_index, note_commitment, &block_header.note_root()) + .map_err(|_| { + TransactionInputError::InputNoteNotInBlock(note.id(), proof.location().block_num()) + }) +} diff --git a/crates/miden-objects/src/transaction/inputs/notes.rs b/crates/miden-protocol/src/transaction/inputs/notes.rs similarity index 97% rename from crates/miden-objects/src/transaction/inputs/notes.rs rename to crates/miden-protocol/src/transaction/inputs/notes.rs index 35663c12ca..638ae56eba 100644 --- a/crates/miden-objects/src/transaction/inputs/notes.rs +++ b/crates/miden-protocol/src/transaction/inputs/notes.rs @@ -3,6 +3,7 @@ use alloc::vec::Vec; use super::TransactionInputError; use crate::note::{Note, NoteId, NoteInclusionProof, NoteLocation, Nullifier}; +use crate::transaction::InputNoteCommitment; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -145,6 +146,12 @@ impl InputNotes { Self::new(input_note_vec) } + + /// Returns a vector of input note commitments based on the input notes. + pub fn to_commitments(&self) -> InputNotes { + let notes = self.notes.iter().map(InputNoteCommitment::from).collect(); + InputNotes::::new_unchecked(notes) + } } impl IntoIterator for InputNotes { @@ -366,7 +373,7 @@ mod input_notes_tests { use miden_core::Word; use super::InputNotes; - use crate::TransactionInputError; + use crate::errors::TransactionInputError; use crate::note::Note; use crate::transaction::InputNote; diff --git a/crates/miden-protocol/src/transaction/inputs/tests.rs b/crates/miden-protocol/src/transaction/inputs/tests.rs new file mode 100644 index 0000000000..09500ef25b --- /dev/null +++ b/crates/miden-protocol/src/transaction/inputs/tests.rs @@ -0,0 +1,375 @@ +use alloc::string::ToString; +use alloc::vec; +use std::collections::BTreeMap; +use std::vec::Vec; + +use miden_core::utils::{Deserializable, Serializable}; + +use crate::account::{ + AccountCode, + AccountHeader, + AccountId, + AccountStorageHeader, + PartialAccount, + PartialStorage, + StorageSlotHeader, + StorageSlotName, + StorageSlotType, +}; +use crate::asset::PartialVault; +use crate::errors::TransactionInputsExtractionError; +use crate::testing::account_id::{ + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, +}; +use crate::transaction::TransactionInputs; +use crate::{Felt, Word}; + +#[test] +fn test_read_foreign_account_inputs_missing_data() { + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let foreign_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(); + + // Create minimal transaction inputs with empty advice map. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let partial_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code, + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + let tx_inputs = TransactionInputs { + account: partial_account, + block_header: crate::block::BlockHeader::mock(0, None, None, &[], Word::default()), + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs: crate::vm::AdviceInputs::default(), + foreign_account_code: Vec::new(), + asset_witnesses: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), + }; + + // Try to read foreign account that doesn't exist in advice map. + let result = tx_inputs.read_foreign_account_inputs(foreign_account_id); + + assert!( + matches!(result, Err(TransactionInputsExtractionError::ForeignAccountNotFound(id)) if id == foreign_account_id) + ); +} + +#[test] +fn test_read_foreign_account_inputs_with_storage_data() { + use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2; + + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let foreign_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(); + + // Create minimal transaction inputs with proper advice map. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let partial_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code.clone(), + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + // Create foreign account header and storage data. + let foreign_header = AccountHeader::new( + foreign_account_id, + Felt::new(5), + Word::default(), + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + code.commitment(), + ); + + // Create storage slots with test data. + let slot_name1 = StorageSlotName::new("test::slot1::value".to_string()).unwrap(); + let slot_name2 = StorageSlotName::new("test::slot2::value".to_string()).unwrap(); + let slot1 = StorageSlotHeader::new( + slot_name1, + StorageSlotType::Value, + Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]), + ); + let slot2 = StorageSlotHeader::new( + slot_name2, + StorageSlotType::Map, + Word::new([Felt::new(50), Felt::new(60), Felt::new(70), Felt::new(80)]), + ); + + let mut slots = vec![slot1, slot2]; + slots.sort_by_key(|slot| slot.id()); + let foreign_storage_header = AccountStorageHeader::new(slots.clone()).unwrap(); + + // Create advice inputs with both account header and storage header. + let mut advice_inputs = crate::vm::AdviceInputs::default(); + let account_id_key = + crate::transaction::TransactionAdviceInputs::account_id_map_key(foreign_account_id); + advice_inputs.map.insert(account_id_key, foreign_header.as_elements().to_vec()); + advice_inputs + .map + .insert(foreign_header.storage_commitment(), foreign_storage_header.to_elements()); + + let foreign_account_slot_names = BTreeMap::from([ + (slots[0].id(), slots[0].name().clone()), + (slots[1].id(), slots[1].name().clone()), + ]); + let tx_inputs = TransactionInputs { + account: partial_account, + block_header: crate::block::BlockHeader::mock(0, None, None, &[], Word::default()), + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs, + foreign_account_code: vec![code], + asset_witnesses: Vec::new(), + foreign_account_slot_names, + }; + + // Try to read foreign account with storage data. + // Should succeed and create partial account with proper storage. + let account_inputs = tx_inputs.read_foreign_account_inputs(foreign_account_id).unwrap(); + assert_eq!(account_inputs.id(), foreign_account_id); + assert_eq!(account_inputs.account().nonce(), Felt::new(5)); + + // Verify storage was properly reconstructed. + let storage = account_inputs.account().storage(); + assert_eq!(storage.header().slots().count(), 2); + + // Verify witness data is valid. + let witness = account_inputs.witness(); + assert_eq!(witness.id(), foreign_account_id); + + // Verify the witness can compute a valid account root. + let computed_root = account_inputs.compute_account_root(); + assert!( + computed_root.is_ok(), + "Failed to compute account root from witness: {:?}", + computed_root.err() + ); + + // Test that the witness path has the expected depth for SMT. + assert_eq!(witness.path().depth(), 64, "Witness path should have SMT depth of 64"); +} + +#[test] +fn test_read_foreign_account_inputs_with_proper_witness() { + use crate::block::account_tree::AccountTree; + use crate::crypto::merkle::smt::Smt; + use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2; + + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let foreign_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(); + + // Create a native account. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let native_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code.clone(), + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + // Create a foreign account with proper commitment. + let foreign_header = AccountHeader::new( + foreign_account_id, + Felt::new(5), + Word::default(), + Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + code.commitment(), + ); + + // Create storage header for the foreign account. + let foreign_storage_header = AccountStorageHeader::new(vec![]).unwrap(); + + // Create an account tree and insert both accounts to get proper Merkle paths. + let mut account_tree = AccountTree::::default(); + + // Insert native account. + let native_commitment = AccountHeader::from(&native_account).commitment(); + account_tree.insert(native_account_id, native_commitment).unwrap(); + + // Insert foreign account. + let _foreign_partial_account = PartialAccount::new( + foreign_account_id, + Felt::new(5), + code.clone(), + PartialStorage::new(foreign_storage_header.clone(), []).unwrap(), + PartialVault::new(Word::default()), + None, + ) + .unwrap(); + account_tree.insert(foreign_account_id, foreign_header.commitment()).unwrap(); + + // Get the account tree root and create witness. + let account_tree_root = account_tree.root(); + let foreign_witness = account_tree.open(foreign_account_id); + + // Create advice inputs with proper Merkle store data. + let mut advice_inputs = crate::vm::AdviceInputs::default(); + + // Add account header to advice map. + let account_id_key = + crate::transaction::TransactionAdviceInputs::account_id_map_key(foreign_account_id); + advice_inputs.map.insert(account_id_key, foreign_header.as_elements().to_vec()); + + // Add storage header to advice map. + advice_inputs + .map + .insert(foreign_header.storage_commitment(), foreign_storage_header.to_elements()); + + // Add authenticated nodes from the witness to the Merkle store. + advice_inputs.store.extend(foreign_witness.authenticated_nodes()); + + // Add the account leaf to the advice map (needed for witness verification). + let leaf = foreign_witness.leaf(); + advice_inputs.map.insert(leaf.hash(), leaf.to_elements()); + + // Create block header with the account tree root. + let block_header = crate::block::BlockHeader::mock(0, None, None, &[], account_tree_root); + + let tx_inputs = TransactionInputs { + account: native_account, + block_header, + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs, + foreign_account_code: vec![code], + asset_witnesses: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), + }; + + // Test reading foreign account inputs. + // Should succeed and create proper witness. + let account_inputs = tx_inputs.read_foreign_account_inputs(foreign_account_id).unwrap(); + assert_eq!(account_inputs.id(), foreign_account_id); + assert_eq!(account_inputs.account().nonce(), Felt::new(5)); + + // Verify witness data. + let witness = account_inputs.witness(); + assert_eq!(witness.id(), foreign_account_id); + + // Verify the witness contains the expected account ID and can compute a root. + let computed_root = account_inputs.compute_account_root(); + assert!( + computed_root.is_ok(), + "Failed to compute account root from witness: {:?}", + computed_root.err() + ); + + // The computed root should be consistent - we're mainly testing that + // the witness was properly reconstructed from the Merkle store data. + let _computed_root_value = computed_root.unwrap(); + + // Test that the witness path has the expected depth (64 for SMT). + assert_eq!(witness.path().depth(), 64, "Witness path should have SMT depth of 64"); +} + +#[test] +fn test_transaction_inputs_serialization_with_foreign_slot_names() { + use miden_core::Felt; + + use crate::account::{ + AccountCode, + AccountId, + AccountStorageHeader, + PartialAccount, + PartialStorage, + StorageSlotName, + }; + use crate::asset::PartialVault; + use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; + + // Create test account IDs + let native_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + + // Create some slot names and IDs. + let slot_name_1 = StorageSlotName::new("test::slot1::value".to_string()).unwrap(); + let slot_name_2 = StorageSlotName::new("test::slot2::map".to_string()).unwrap(); + let slot_name_3 = StorageSlotName::new("another::slot::value".to_string()).unwrap(); + + let slot_id_1 = slot_name_1.id(); + let slot_id_2 = slot_name_2.id(); + let slot_id_3 = slot_name_3.id(); + + // Create foreign account slot names map. + let mut foreign_account_slot_names = BTreeMap::new(); + foreign_account_slot_names.insert(slot_id_1, slot_name_1.clone()); + foreign_account_slot_names.insert(slot_id_2, slot_name_2.clone()); + foreign_account_slot_names.insert(slot_id_3, slot_name_3.clone()); + + // Create a basic TransactionInputs with foreign slot names. + let code = AccountCode::mock(); + let storage_header = AccountStorageHeader::new(vec![]).unwrap(); + let partial_storage = PartialStorage::new(storage_header, []).unwrap(); + let partial_vault = PartialVault::new(Word::default()); + let partial_account = PartialAccount::new( + native_account_id, + Felt::new(10), + code, + partial_storage, + partial_vault, + None, + ) + .unwrap(); + + let original_tx_inputs = TransactionInputs { + account: partial_account, + block_header: crate::block::BlockHeader::mock(0, None, None, &[], Word::default()), + blockchain: crate::transaction::PartialBlockchain::default(), + input_notes: crate::transaction::InputNotes::new(vec![]).unwrap(), + tx_args: crate::transaction::TransactionArgs::default(), + advice_inputs: crate::vm::AdviceInputs::default(), + foreign_account_code: Vec::new(), + asset_witnesses: Vec::new(), + foreign_account_slot_names, + }; + + // Test serialization roundtrip. + let serialized = original_tx_inputs.to_bytes(); + let deserialized = TransactionInputs::read_from_bytes(&serialized).unwrap(); + + // Verify that foreign account slot names are preserved. + assert_eq!( + original_tx_inputs.foreign_account_slot_names(), + deserialized.foreign_account_slot_names() + ); + + // Verify specific slot names. + let deserialized_slots = deserialized.foreign_account_slot_names(); + + // Check slots. + assert_eq!(deserialized_slots.get(&slot_id_1).unwrap(), &slot_name_1); + assert_eq!(deserialized_slots.get(&slot_id_2).unwrap(), &slot_name_2); + assert_eq!(deserialized_slots.get(&slot_id_3).unwrap(), &slot_name_3); + + // Verify the entire structure is identical. + assert_eq!(original_tx_inputs, deserialized); +} diff --git a/crates/miden-lib/src/transaction/inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs similarity index 85% rename from crates/miden-lib/src/transaction/inputs.rs rename to crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 84140838e8..aec2c04229 100644 --- a/crates/miden-lib/src/transaction/inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -1,16 +1,23 @@ use alloc::vec::Vec; -use miden_objects::account::{AccountHeader, AccountId, PartialAccount}; -use miden_objects::block::AccountWitness; -use miden_objects::crypto::SequentialCommit; -use miden_objects::crypto::merkle::InnerNodeInfo; -use miden_objects::transaction::{AccountInputs, InputNote, PartialBlockchain, TransactionInputs}; -use miden_objects::vm::AdviceInputs; -use miden_objects::{EMPTY_WORD, Felt, FieldElement, Word, ZERO}; use miden_processor::AdviceMutation; -use thiserror::Error; -use super::TransactionKernel; +use crate::account::{AccountHeader, AccountId, PartialAccount}; +use crate::asset::AssetWitness; +use crate::block::account_tree::AccountWitness; +use crate::crypto::SequentialCommit; +use crate::crypto::merkle::InnerNodeInfo; +use crate::crypto::merkle::smt::SmtProof; +use crate::note::NoteAttachmentContent; +use crate::transaction::{ + AccountInputs, + InputNote, + PartialBlockchain, + TransactionInputs, + TransactionKernel, +}; +use crate::vm::AdviceInputs; +use crate::{EMPTY_WORD, Felt, FieldElement, Word, ZERO}; // TRANSACTION ADVICE INPUTS // ================================================================================================ @@ -25,13 +32,13 @@ impl TransactionAdviceInputs { /// /// The created advice inputs will be populated with the data required for executing a /// transaction with the specified transaction inputs. - pub fn new(tx_inputs: &TransactionInputs) -> Result { + pub fn new(tx_inputs: &TransactionInputs) -> Self { let mut inputs = TransactionAdviceInputs(tx_inputs.advice_inputs().clone()); inputs.build_stack(tx_inputs); inputs.add_kernel_commitment(); inputs.add_partial_blockchain(tx_inputs.blockchain()); - inputs.add_input_notes(tx_inputs)?; + inputs.add_input_notes(tx_inputs); // Add the script's MAST forest's advice inputs. if let Some(tx_script) = tx_inputs.tx_args().tx_script() { @@ -46,7 +53,7 @@ impl TransactionAdviceInputs { // Inject native account. let partial_native_acc = tx_inputs.account(); - inputs.add_account(partial_native_acc)?; + inputs.add_account(partial_native_acc); // If a seed was provided, extend the map appropriately. if let Some(seed) = tx_inputs.account().seed() { @@ -68,10 +75,14 @@ impl TransactionAdviceInputs { } } + tx_inputs.asset_witnesses().iter().for_each(|asset_witness| { + inputs.add_asset_witness(asset_witness.clone()); + }); + // Extend with extra user-supplied advice. inputs.extend(tx_inputs.tx_args().advice_inputs().clone()); - Ok(inputs) + inputs } /// Returns a reference to the underlying advice inputs. @@ -96,6 +107,16 @@ impl TransactionAdviceInputs { .into_iter() } + // PUBLIC UTILITIES + // -------------------------------------------------------------------------------------------- + + /// Returns the advice map key where: + /// - the seed for native accounts is stored. + /// - the account header for foreign accounts is stored. + pub fn account_id_map_key(id: AccountId) -> Word { + Word::from([id.suffix(), id.prefix().as_felt(), ZERO, ZERO]) + } + // MUTATORS // -------------------------------------------------------------------------------------------- @@ -108,9 +129,9 @@ impl TransactionAdviceInputs { pub fn add_foreign_accounts<'inputs>( &mut self, foreign_account_inputs: impl IntoIterator, - ) -> Result<(), TransactionAdviceMapMismatch> { + ) { for foreign_acc in foreign_account_inputs { - self.add_account(foreign_acc.account())?; + self.add_account(foreign_acc.account()); self.add_account_witness(foreign_acc.witness()); // for foreign accounts, we need to insert the id to state mapping @@ -121,8 +142,6 @@ impl TransactionAdviceInputs { // ACCOUNT_ID |-> [ID_AND_NONCE, VAULT_ROOT, STORAGE_COMMITMENT, CODE_COMMITMENT] self.add_map_entry(account_id_key, header.as_elements()); } - - Ok(()) } /// Extend the advice stack with the transaction inputs. @@ -136,7 +155,7 @@ impl TransactionAdviceInputs { /// NULLIFIER_ROOT, /// TX_COMMITMENT, /// TX_KERNEL_COMMITMENT - /// PROOF_COMMITMENT, + /// VALIDATOR_KEY_COMMITMENT, /// [block_num, version, timestamp, 0], /// [native_asset_id_suffix, native_asset_id_prefix, verification_base_fee, 0] /// [0, 0, 0, 0] @@ -161,7 +180,7 @@ impl TransactionAdviceInputs { self.extend_stack(header.nullifier_root()); self.extend_stack(header.tx_commitment()); self.extend_stack(header.tx_kernel_commitment()); - self.extend_stack(header.proof_commitment()); + self.extend_stack(header.validator_key().to_commitment()); self.extend_stack([ header.block_num().into(), header.version().into(), @@ -253,33 +272,18 @@ impl TransactionAdviceInputs { /// - The account code commitment |-> procedures vector. /// - The leaf hash |-> (key, value), for all leaves of the partial vault. /// - If present, the Merkle leaves associated with the account storage maps. - fn add_account( - &mut self, - account: &PartialAccount, - ) -> Result<(), TransactionAdviceMapMismatch> { + fn add_account(&mut self, account: &PartialAccount) { // --- account code ------------------------------------------------------- // CODE_COMMITMENT -> [[ACCOUNT_PROCEDURE_DATA]] let code = account.code(); self.add_map_entry(code.commitment(), code.as_elements()); - // Extend the advice map with the account code's advice inputs. - // This ensures that the advice map is available during the note script execution when it - // calls the account's code that relies on the it's advice map data (data segments) loaded - // into the advice provider - self.0.map.merge(account.code().mast().advice_map()).map_err( - |((key, existing_val), incoming_val)| TransactionAdviceMapMismatch { - key, - existing_val: existing_val.to_vec(), - incoming_val: incoming_val.to_vec(), - }, - )?; - // --- account storage ---------------------------------------------------- // STORAGE_COMMITMENT |-> [[STORAGE_SLOT_DATA]] let storage_header = account.storage().header(); - self.add_map_entry(storage_header.compute_commitment(), storage_header.as_elements()); + self.add_map_entry(storage_header.to_commitment(), storage_header.to_elements()); // populate Merkle store and advice map with nodes info needed to access storage map entries self.extend_merkle_store(account.storage().inner_nodes()); @@ -290,8 +294,6 @@ impl TransactionAdviceInputs { // populate Merkle store and advice map with nodes info needed to access vault assets self.extend_merkle_store(account.vault().inner_nodes()); self.extend_map(account.vault().leaves().map(|leaf| (leaf.hash(), leaf.to_elements()))); - - Ok(()) } /// Adds an account witness to the advice inputs. @@ -307,6 +309,14 @@ impl TransactionAdviceInputs { self.extend_merkle_store(witness.authenticated_nodes()); } + /// Adds an asset witness to the advice inputs. + fn add_asset_witness(&mut self, witness: AssetWitness) { + self.extend_merkle_store(witness.authenticated_nodes()); + + let smt_proof = SmtProof::from(witness); + self.extend_map([(smt_proof.leaf().hash(), smt_proof.leaf().to_elements())]); + } + // NOTE INJECTION // -------------------------------------------------------------------------------------------- @@ -328,12 +338,9 @@ impl TransactionAdviceInputs { /// - The note's position in the note tree /// /// The data above is processed by `prologue::process_input_notes_data`. - fn add_input_notes( - &mut self, - tx_inputs: &TransactionInputs, - ) -> Result<(), TransactionAdviceMapMismatch> { + fn add_input_notes(&mut self, tx_inputs: &TransactionInputs) { if tx_inputs.input_notes().is_empty() { - return Ok(()); + return; } let mut note_data = Vec::new(); @@ -343,9 +350,19 @@ impl TransactionAdviceInputs { let recipient = note.recipient(); let note_arg = tx_inputs.tx_args().get_note_args(note.id()).unwrap_or(&EMPTY_WORD); - // recipient inputs / assets commitments + // recipient inputs self.add_map_entry(recipient.inputs().commitment(), recipient.inputs().to_elements()); + // assets commitments self.add_map_entry(assets.commitment(), assets.to_padded_assets()); + // array attachments + if let NoteAttachmentContent::Array(array_attachment) = + note.metadata().attachment().content() + { + self.add_map_entry( + array_attachment.commitment(), + array_attachment.as_slice().to_vec(), + ); + } // note details / metadata note_data.extend(recipient.serial_num()); @@ -353,7 +370,8 @@ impl TransactionAdviceInputs { note_data.extend(*recipient.inputs().commitment()); note_data.extend(*assets.commitment()); note_data.extend(*note_arg); - note_data.extend(Word::from(note.metadata())); + note_data.extend(note.metadata().to_header_word()); + note_data.extend(note.metadata().to_attachment_word()); note_data.push(recipient.inputs().num_values().into()); note_data.push((assets.num_assets() as u32).into()); note_data.extend(assets.to_padded_assets()); @@ -387,19 +405,9 @@ impl TransactionAdviceInputs { note_data.push(Felt::ZERO) }, } - - self.0.map.merge(note.script().mast().advice_map()).map_err( - |((key, existing_val), incoming_val)| TransactionAdviceMapMismatch { - key, - existing_val: existing_val.to_vec(), - incoming_val: incoming_val.to_vec(), - }, - )?; } self.add_map_entry(tx_inputs.input_notes().commitment(), note_data); - - Ok(()) } // HELPER METHODS @@ -419,18 +427,11 @@ impl TransactionAdviceInputs { self.0.stack.extend(iter); } - /// Extends the [`MerkleStore`](miden_objects::crypto::merkle::MerkleStore) with the given + /// Extends the [`MerkleStore`](crate::crypto::merkle::MerkleStore) with the given /// nodes. fn extend_merkle_store(&mut self, iter: impl Iterator) { self.0.store.extend(iter); } - - /// Returns the advice map key where: - /// - the seed for native accounts is stored. - /// - the account header for foreign accounts is stored. - fn account_id_map_key(id: AccountId) -> Word { - Word::from([id.suffix(), id.prefix().as_felt(), ZERO, ZERO]) - } } // CONVERSIONS @@ -447,16 +448,3 @@ impl From for TransactionAdviceInputs { Self(inner) } } - -// CONFLICT ERROR -// ================================================================================================ - -#[derive(Debug, Error)] -#[error( - "conflicting map entry for key {key}: existing={existing_val:?}, incoming={incoming_val:?}" -)] -pub struct TransactionAdviceMapMismatch { - pub key: Word, - pub existing_val: Vec, - pub incoming_val: Vec, -} diff --git a/crates/miden-lib/src/transaction/memory.rs b/crates/miden-protocol/src/transaction/kernel/memory.rs similarity index 65% rename from crates/miden-lib/src/transaction/memory.rs rename to crates/miden-protocol/src/transaction/kernel/memory.rs index 4fbb83178b..8b33b214ae 100644 --- a/crates/miden-lib/src/transaction/memory.rs +++ b/crates/miden-protocol/src/transaction/kernel/memory.rs @@ -12,63 +12,49 @@ pub type StorageSlot = u8; // General layout // -// Here the "end address" is the last memory address occupied by the current data -// -// | Section | Start address, pointer (word pointer) | End address, pointer (word pointer) | Comment | -// | ------------------ | :-----------------------------------: | :---------------------------------: | ------------------------------------------ | -// | Bookkeeping | 0 (0) | 88 (22) | | -// | Global inputs | 400 (100) | 439 (109) | | -// | Block header | 800 (200) | 843 (210) | | -// | Partial blockchain | 1_200 (300) | 1_331? (332?) | | -// | Kernel data | 1_600 (400) | 1_739 (434) | 34 procedures in total, 4 elements each | -// | Accounts data | 8_192 (2048) | 532_479 (133_119) | 64 accounts max, 8192 elements each | -// | Account delta | 532_480 (133_120) | 532_742 (133_185) | | -// | Input notes | 4_194_304 (1_048_576) | 6_356_991 (1_589_247) | nullifiers data segment + 1024 input notes | -// | | | | max, 2048 elements each | -// | Output notes | 16_777_216 (4_194_304) | 18_874_367 (4_718_591) | 1024 output notes max, 2048 elements each | -// | Link Map Memory | 33_554_432 (8_388_608) | 67_108_863 (16_777_215) | Enough for 2_097_151 key-value pairs | +// | Section | Start address | Size in elements | Comment | +// | ------------------ | ------------- | ---------------- | ------------------------------------------ | +// | Bookkeeping | 0 | 89 | | +// | Global inputs | 400 | 40 | | +// | Block header | 800 | 44 | | +// | Partial blockchain | 1_200 | 132 | | +// | 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 | 2_162_688 | nullifiers data segment + 1024 input notes | +// | | | | max, 2048 elements each | +// | Output notes | 16_777_216 | 2_097_152 | 1024 output notes max, 2048 elements each | +// | Link Map Memory | 33_554_432 | 33_554_432 | Enough for 2_097_151 key-value pairs | // Relative layout of one account // -// Here the "end pointer" is the last memory pointer occupied by the current data +// | Section | Start address | Size in elements | Comment | +// | ------------------ | ------------- | ---------------- | -------------------------------------- | +// | ID and nonce | 0 | 4 | | +// | Vault root | 4 | 4 | | +// | Storage commitment | 8 | 4 | | +// | Code commitment | 12 | 4 | | +// | Padding | 16 | 12 | | +// | Num procedures | 28 | 4 | | +// | Procedures roots | 32 | 1_024 | 256 procedures max, 4 elements each | +// | Padding | 1_056 | 4 | | +// | Proc tracking | 1_060 | 256 | 256 procedures max, 1 element each | +// | Num storage slots | 1_316 | 4 | | +// | Initial slot info | 1_320 | 1_020 | Only initialized on the native account | +// | Active slot info | 2_340 | 1_020 | 255 slots max, 8 elements each | +// | Padding | 3_360 | 4_832 | | // -// | Section | Start address, pointer (word pointer) | End address, pointer (word pointer) | Comment | -// | ------------------ | :-----------------------------------: | :---------------------------------: | ----------------------------------- | -// | ID and nonce | 0 (0) | 3 (0) | | -// | Vault root | 4 (1) | 7 (1) | | -// | Storage commitment | 8 (2) | 11 (2) | | -// | Code commitment | 12 (3) | 15 (3) | | -// | Padding | 16 (4) | 27 (6) | | -// | Num procedures | 28 (7) | 31 (7) | | -// | Procedures info | 32 (8) | 2_079 (519) | 255 procedures max, 8 elements each | -// | Padding | 2_080 (520) | 2_083 (520) | | -// | Proc tracking | 2_084 (521) | 2_339 (584) | 255 procedures max, 1 element each | -// | Num storage slots | 2_340 (585) | 2_343 (585) | | -// | Storage slot info | 2_344 (586) | 4_383 (1095) | 255 slots max, 8 elements each | -// | Initial slot info | 4_384 (1096) | 6_423 (1545) | Only present on the native account | -// | Padding | 6_424 (1545) | 8_191 (2047) | | +// Storage slots are laid out as [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE]. // Relative layout of the native account's delta. // -// Here the "end pointer" is the last memory pointer occupied by the current data -// // For now each Storage Map pointer (a link map ptr) occupies a single element. // -// | Section | Start address (word pointer) | End address (word pointer) | Comment | -// | ---------------------------- | :--------------------------: | :------------------------: | ----------------------------------- | -// | Fungible Asset Delta Ptr | 0 (0) | 3 (0) | | -// | Non-Fungible Asset Delta Ptr | 4 (1) | 7 (1) | | -// | Storage Map Delta Ptrs | 8 (2) | 263 (65) | Max 255 storage map deltas | - -// RESERVED ACCOUNT STORAGE SLOTS -// ------------------------------------------------------------------------------------------------ - -/// The account storage slot at which faucet data is stored. -/// -/// - Fungible faucet: The faucet data consists of [0, 0, 0, total_issuance]. -/// - Non-fungible faucet: The faucet data consists of SMT root containing minted non-fungible -/// assets. -pub const FAUCET_STORAGE_DATA_SLOT: StorageSlot = 0; +// | Section | Start address | Size in elements | Comment | +// | ---------------------------- | ------------- | ---------------- | ----------------------------------- | +// | Fungible Asset Delta Ptr | 0 | 4 | | +// | Non-Fungible Asset Delta Ptr | 4 | 4 | | +// | Storage Map Delta Ptrs | 8 | 256 | Max 255 storage map deltas | // BOOKKEEPING // ------------------------------------------------------------------------------------------------ @@ -149,40 +135,40 @@ pub const AUTH_ARGS_PTR: MemoryAddress = 436; // BLOCK DATA // ------------------------------------------------------------------------------------------------ -/// The memory address at which the block data section begins +/// The memory address at which the block data section begins. pub const BLOCK_DATA_SECTION_OFFSET: MemoryOffset = 800; -/// The memory address at which the previous block commitment is stored +/// The memory address at which the previous block commitment is stored. pub const PREV_BLOCK_COMMITMENT_PTR: MemoryAddress = 800; -/// The memory address at which the chain commitment is stored +/// The memory address at which the chain commitment is stored. pub const CHAIN_COMMITMENT_PTR: MemoryAddress = 804; -/// The memory address at which the state root is stored +/// The memory address at which the state root is stored. pub const ACCT_DB_ROOT_PTR: MemoryAddress = 808; -/// The memory address at which the nullifier db root is store +/// The memory address at which the nullifier db root is store. pub const NULLIFIER_DB_ROOT_PTR: MemoryAddress = 812; -/// The memory address at which the TX commitment is stored +/// The memory address at which the TX commitment is stored. pub const TX_COMMITMENT_PTR: MemoryAddress = 816; -/// The memory address at which the transaction kernel commitment is stored +/// The memory address at which the transaction kernel commitment is stored. pub const TX_KERNEL_COMMITMENT_PTR: MemoryAddress = 820; -/// The memory address at which the proof commitment is stored -pub const PROOF_COMMITMENT_PTR: MemoryAddress = 824; +/// The memory address at which the public key is stored. +pub const VALIDATOR_KEY_COMMITMENT_PTR: MemoryAddress = 824; -/// The memory address at which the block number is stored +/// The memory address at which the block number is stored. pub const BLOCK_METADATA_PTR: MemoryAddress = 828; -/// The index of the block number within the block metadata +/// The index of the block number within the block metadata. pub const BLOCK_NUMBER_IDX: DataIndex = 0; -/// The index of the protocol version within the block metadata +/// The index of the protocol version within the block metadata. pub const PROTOCOL_VERSION_IDX: DataIndex = 1; -/// The index of the timestamp within the block metadata +/// The index of the timestamp within the block metadata. pub const TIMESTAMP_IDX: DataIndex = 2; /// The memory address at which the fee parameters are stored. These occupy a double word. @@ -197,19 +183,19 @@ pub const NATIVE_ASSET_ID_PREFIX_IDX: DataIndex = 1; /// The index of the verification base fee within the block fee parameters. pub const VERIFICATION_BASE_FEE_IDX: DataIndex = 2; -/// The memory address at which the note root is stored +/// The memory address at which the note root is stored. pub const NOTE_ROOT_PTR: MemoryAddress = 840; // CHAIN DATA // ------------------------------------------------------------------------------------------------ -/// The memory address at which the chain data section begins +/// The memory address at which the chain data section begins. pub const PARTIAL_BLOCKCHAIN_PTR: MemoryAddress = 1200; -/// The memory address at which the total number of leaves in the partial blockchain is stored +/// The memory address at which the total number of leaves in the partial blockchain is stored. pub const PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR: MemoryAddress = 1200; -/// The memory address at which the partial blockchain peaks are stored +/// The memory address at which the partial blockchain peaks are stored. pub const PARTIAL_BLOCKCHAIN_PEAKS_PTR: MemoryAddress = 1204; // KERNEL DATA @@ -219,13 +205,13 @@ pub const PARTIAL_BLOCKCHAIN_PEAKS_PTR: MemoryAddress = 1204; pub const NUM_KERNEL_PROCEDURES_PTR: MemoryAddress = 1600; /// The memory address at which the section, where the hashes of the kernel procedures are stored, -/// begins +/// begins. pub const KERNEL_PROCEDURES_PTR: MemoryAddress = 1604; // ACCOUNT DATA // ------------------------------------------------------------------------------------------------ -/// The size of the memory segment allocated to core account data (excluding new code commitment) +/// The size of the memory segment allocated to core account data (excluding new code commitment). pub const ACCT_DATA_MEM_SIZE: MemSize = 16; /// The memory address at which the native account is stored. @@ -275,12 +261,12 @@ pub const NATIVE_ACCT_CODE_COMMITMENT_PTR: MemoryAddress = /// The offset at which the number of procedures contained in the account code is stored relative to /// the start of the account data segment. -pub const NUM_ACCT_PROCEDURES_OFFSET: MemoryAddress = 28; +pub const ACCT_NUM_PROCEDURES_OFFSET: MemoryAddress = 28; /// The memory address at which the number of procedures contained in the account code is stored in /// the native account. pub const NATIVE_NUM_ACCT_PROCEDURES_PTR: MemoryAddress = - NATIVE_ACCOUNT_DATA_PTR + NUM_ACCT_PROCEDURES_OFFSET; + NATIVE_ACCOUNT_DATA_PTR + ACCT_NUM_PROCEDURES_OFFSET; /// The offset at which the account procedures section begins relative to the start of the account /// data segment. @@ -292,7 +278,7 @@ pub const NATIVE_ACCT_PROCEDURES_SECTION_PTR: MemoryAddress = /// The offset at which the account procedures call tracking section begins relative to the start of /// the account data segment. -pub const ACCT_PROCEDURES_CALL_TRACKING_OFFSET: MemoryAddress = 2084; +pub const ACCT_PROCEDURES_CALL_TRACKING_OFFSET: MemoryAddress = 1060; /// The memory address at which the account procedures call tracking section begins in the native /// account. @@ -301,23 +287,38 @@ pub const NATIVE_ACCT_PROCEDURES_CALL_TRACKING_PTR: MemoryAddress = /// The offset at which the number of storage slots contained in the account storage is stored /// relative to the start of the account data segment. -pub const NUM_ACCT_STORAGE_SLOTS_OFFSET: MemoryAddress = 2340; +pub const ACCT_NUM_STORAGE_SLOTS_OFFSET: MemoryAddress = 1316; /// The memory address at which number of storage slots contained in the account storage is stored /// in the native account. pub const NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR: MemoryAddress = - NATIVE_ACCOUNT_DATA_PTR + NUM_ACCT_STORAGE_SLOTS_OFFSET; - -/// The offset at which the account storage slots section begins relative to the start of the -/// account data segment. -pub const ACCT_STORAGE_SLOTS_SECTION_OFFSET: MemoryAddress = 2344; + NATIVE_ACCOUNT_DATA_PTR + ACCT_NUM_STORAGE_SLOTS_OFFSET; /// The number of elements that each storage slot takes up in memory. pub const ACCT_STORAGE_SLOT_NUM_ELEMENTS: u8 = 8; -/// The memory address at which the account storage slots section begins in the native account. +/// The offset of the slot type in the storage slot. +pub const ACCT_STORAGE_SLOT_TYPE_OFFSET: u8 = 1; + +/// The offset of the slot's ID suffix in the storage slot. +pub const ACCT_STORAGE_SLOT_ID_SUFFIX_OFFSET: u8 = 2; + +/// The offset of the slot's ID prefix in the storage slot. +pub const ACCT_STORAGE_SLOT_ID_PREFIX_OFFSET: u8 = 3; + +/// The offset of the slot value in the storage slot. +pub const ACCT_STORAGE_SLOT_VALUE_OFFSET: u8 = 4; + +/// The offset at which the account's active storage slots section begins relative to the start of +/// the account data segment. +/// +/// This section contains the current values of the account storage slots. +pub const ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET: MemoryAddress = 2340; + +/// The memory address at which the account's active storage slots section begins in the native +/// account. pub const NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR: MemoryAddress = - NATIVE_ACCOUNT_DATA_PTR + ACCT_STORAGE_SLOTS_SECTION_OFFSET; + NATIVE_ACCOUNT_DATA_PTR + ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET; // NOTES DATA // ================================================================================================ @@ -341,14 +342,14 @@ pub const NOTE_MEM_SIZE: MemoryAddress = 2048; // // Here `n` represents number of input notes. // -// Each nullifier occupies a single word. A data section for each note consists of exactly 512 -// words and is laid out like so: +// Each nullifier occupies a single word. A data section for each note consists of exactly 2048 +// elements and is laid out like so: // -// ┌──────┬────────┬────────┬────────┬────────────┬───────────┬──────┬───────┬────────┬────────┬───────┬─────┬───────┬─────────┬ -// │ NOTE │ SERIAL │ SCRIPT │ INPUTS │ ASSETS | RECIPIENT │ META │ NOTE │ NUM │ NUM │ ASSET │ ... │ ASSET │ PADDING │ -// │ ID │ NUM │ ROOT │ HASH │ COMMITMENT | │ DATA │ ARGS │ INPUTS │ ASSETS │ 0 │ │ n │ │ -// ├──────┼────────┼────────┼────────┼────────────┼───────────┼──────┼───────┼────────┼────────┼───────┼─────┼───────┼─────────┤ -// 0 4 8 12 16 20 24 28 32 36 40 + 4n +// ┌──────┬────────┬────────┬────────┬────────────┬───────────┬──────────┬────────────┬───────┬────────┬────────┬───────┬─────┬───────┬─────────┬ +// │ NOTE │ SERIAL │ SCRIPT │ INPUTS │ ASSETS | RECIPIENT │ METADATA │ ATTACHMENT │ NOTE │ NUM │ NUM │ ASSET │ ... │ ASSET │ PADDING │ +// │ ID │ NUM │ ROOT │ HASH │ COMMITMENT | │ HEADER │ │ ARGS │ INPUTS │ ASSETS │ 0 │ │ n │ │ +// ├──────┼────────┼────────┼────────┼────────────┼───────────┼──────────┼────────────┼───────┼────────┼────────┼───────┼─────┼───────┼─────────┤ +// 0 4 8 12 16 20 24 28 32 36 40 44 + 4n // // - NUM_INPUTS is encoded as [num_inputs, 0, 0, 0]. // - NUM_ASSETS is encoded as [num_assets, 0, 0, 0]. @@ -357,7 +358,7 @@ pub const NOTE_MEM_SIZE: MemoryAddress = 2048; // // Notice that note input values are not loaded to the memory, only their length. In order to obtain // the input values the advice map should be used: they are stored there as -// `INPUTS_COMMITMENT -> INPUTS || PADDING`. +// `INPUTS_COMMITMENT -> INPUTS`. // // As opposed to the asset values, input values are never used in kernel memory, so their presence // there is unnecessary. @@ -381,12 +382,15 @@ pub const INPUT_NOTE_SCRIPT_ROOT_OFFSET: MemoryOffset = 8; pub const INPUT_NOTE_INPUTS_COMMITMENT_OFFSET: MemoryOffset = 12; pub const INPUT_NOTE_ASSETS_COMMITMENT_OFFSET: MemoryOffset = 16; pub const INPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 20; -pub const INPUT_NOTE_METADATA_OFFSET: MemoryOffset = 24; -pub const INPUT_NOTE_ARGS_OFFSET: MemoryOffset = 28; -pub const INPUT_NOTE_NUM_INPUTS_OFFSET: MemoryOffset = 32; -pub const INPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 36; -pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 40; +pub const INPUT_NOTE_METADATA_HEADER_OFFSET: MemoryOffset = 24; +pub const INPUT_NOTE_ATTACHMENT_OFFSET: MemoryOffset = 28; +pub const INPUT_NOTE_ARGS_OFFSET: MemoryOffset = 32; +pub const INPUT_NOTE_NUM_INPUTS_OFFSET: MemoryOffset = 36; +pub const INPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 40; +pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 44; +#[allow(clippy::empty_line_after_outer_attr)] +#[rustfmt::skip] // OUTPUT NOTES DATA // ------------------------------------------------------------------------------------------------ // Output notes section contains data of all notes produced by a transaction. The section starts at @@ -400,11 +404,11 @@ pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 40; // 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: // -// ┌──────┬──────────┬───────────┬────────────┬────────────────┬─────────┬─────┬─────────┬─────────┐ -// │ NOTE │ METADATA │ RECIPIENT │ ASSETS │ NUM ASSETS │ ASSET 0 │ ... │ ASSET n │ PADDING │ -// | ID | | | COMMITMENT | AND DIRTY FLAG | | | | | -// ├──────┼──────────┼───────────┼────────────┼────────────────┼─────────┼─────┼─────────┼─────────┤ -// 0 1 2 3 4 5 5 + n +// ┌──────┬──────────┬────────────┬───────────┬────────────┬────────────────┬─────────┬─────┬─────────┬─────────┐ +// │ NOTE │ METADATA │ METADATA │ RECIPIENT │ ASSETS │ NUM ASSETS │ ASSET 0 │ ... │ ASSET n │ PADDING │ +// | ID | HEADER | ATTACHMENT | | COMMITMENT | AND DIRTY FLAG | | | | | +// ├──────┼──────────┼────────────┼───────────┼────────────┼────────────────┼─────────┼─────┼─────────┼─────────┤ +// 0 1 2 3 4 5 6 6 + n // // The NUM_ASSETS_AND_DIRTY_FLAG word has the following layout: // `[num_assets, assets_commitment_dirty_flag, 0, 0]`, where: @@ -420,17 +424,15 @@ pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 40; /// The memory address at which the output notes section begins. pub const OUTPUT_NOTE_SECTION_OFFSET: MemoryOffset = 16_777_216; -/// The size of the core output note data segment. -pub const OUTPUT_NOTE_CORE_DATA_SIZE: MemSize = 16; - /// The offsets at which data of an output note is stored relative to the start of its data segment. pub const OUTPUT_NOTE_ID_OFFSET: MemoryOffset = 0; -pub const OUTPUT_NOTE_METADATA_OFFSET: MemoryOffset = 4; -pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 8; -pub const OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET: MemoryOffset = 12; -pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 16; -pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 17; -pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 20; +pub const OUTPUT_NOTE_METADATA_HEADER_OFFSET: MemoryOffset = 4; +pub const OUTPUT_NOTE_ATTACHMENT_OFFSET: MemoryOffset = 8; +pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 12; +pub const OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET: MemoryOffset = 16; +pub const OUTPUT_NOTE_NUM_ASSETS_OFFSET: MemoryOffset = 20; +pub const OUTPUT_NOTE_DIRTY_FLAG_OFFSET: MemoryOffset = 21; +pub const OUTPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 24; // LINK MAP // ------------------------------------------------------------------------------------------------ diff --git a/crates/miden-lib/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/kernel/mod.rs similarity index 80% rename from crates/miden-lib/src/transaction/mod.rs rename to crates/miden-protocol/src/transaction/kernel/mod.rs index 54c55744cc..334376165a 100644 --- a/crates/miden-lib/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/kernel/mod.rs @@ -2,44 +2,33 @@ use alloc::string::ToString; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_objects::account::AccountId; +use miden_core_lib::CoreLibrary; + +use crate::account::{AccountHeader, AccountId}; #[cfg(any(feature = "testing", test))] -use miden_objects::assembly::Library; -use miden_objects::assembly::debuginfo::SourceManagerSync; -use miden_objects::assembly::{Assembler, DefaultSourceManager, KernelLibrary}; -use miden_objects::asset::FungibleAsset; -use miden_objects::block::BlockNumber; -use miden_objects::crypto::SequentialCommit; -use miden_objects::transaction::{OutputNote, OutputNotes, TransactionInputs, TransactionOutputs}; -use miden_objects::utils::serde::Deserializable; -use miden_objects::utils::sync::LazyLock; -use miden_objects::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs}; -use miden_objects::{Felt, Hasher, TransactionOutputError, Word}; -use miden_stdlib::StdLibrary; - -use super::MidenLib; +use crate::assembly::Library; +use crate::assembly::debuginfo::SourceManagerSync; +use crate::assembly::{Assembler, DefaultSourceManager, KernelLibrary}; +use crate::asset::FungibleAsset; +use crate::block::BlockNumber; +use crate::crypto::SequentialCommit; +use crate::errors::TransactionOutputError; +use crate::protocol::ProtocolLib; +use crate::transaction::{OutputNote, OutputNotes, TransactionInputs, TransactionOutputs}; +use crate::utils::serde::Deserializable; +use crate::utils::sync::LazyLock; +use crate::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs}; +use crate::{Felt, Hasher, Word}; + +mod procedures; pub mod memory; -mod events; -pub use events::{EventId, TransactionEvent}; - -mod inputs; -pub use inputs::{TransactionAdviceInputs, TransactionAdviceMapMismatch}; - -mod outputs; -pub use outputs::{ - ACCOUNT_UPDATE_COMMITMENT_WORD_IDX, - EXPIRATION_BLOCK_ELEMENT_IDX, - FEE_ASSET_WORD_IDX, - OUTPUT_NOTES_COMMITMENT_WORD_IDX, - parse_final_account_header, -}; +mod advice_inputs; +mod tx_event_id; -pub use crate::errors::{TransactionEventError, TransactionTraceParsingError}; - -mod kernel_procedures; -use kernel_procedures::KERNEL_PROCEDURES; +pub use advice_inputs::TransactionAdviceInputs; +pub use tx_event_id::TransactionEventId; // CONSTANTS // ================================================================================================ @@ -78,7 +67,7 @@ impl TransactionKernel { // -------------------------------------------------------------------------------------------- /// Array of kernel procedures. - pub const PROCEDURES: &'static [Word] = &KERNEL_PROCEDURES; + pub const PROCEDURES: &'static [Word] = &procedures::KERNEL_PROCEDURES; // KERNEL SOURCE CODE // -------------------------------------------------------------------------------------------- @@ -121,9 +110,7 @@ impl TransactionKernel { /// Transforms the provided [`TransactionInputs`] into stack and advice /// inputs needed to execute a transaction kernel for a specific transaction. - pub fn prepare_inputs( - tx_inputs: &TransactionInputs, - ) -> Result<(StackInputs, TransactionAdviceInputs), TransactionAdviceMapMismatch> { + pub fn prepare_inputs(tx_inputs: &TransactionInputs) -> (StackInputs, TransactionAdviceInputs) { let account = tx_inputs.account(); let stack_inputs = TransactionKernel::build_input_stack( @@ -134,30 +121,30 @@ impl TransactionKernel { tx_inputs.block_header().block_num(), ); - let tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs)?; + let tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs); - Ok((stack_inputs, tx_advice_inputs)) + (stack_inputs, tx_advice_inputs) } // ASSEMBLER CONSTRUCTOR // -------------------------------------------------------------------------------------------- /// Returns a new Miden assembler instantiated with the transaction kernel and loaded with the - /// Miden stdlib as well as with miden-lib. + /// core lib as well as with miden-lib. pub fn assembler() -> Assembler { Self::assembler_with_source_manager(Arc::new(DefaultSourceManager::default())) } /// Returns a new assembler instantiated with the transaction kernel and loaded with the - /// Miden stdlib as well as with miden-lib. + /// core lib as well as with miden-lib. pub fn assembler_with_source_manager(source_manager: Arc) -> Assembler { #[cfg(all(any(feature = "testing", test), feature = "std"))] source_manager_ext::load_masm_source_files(&source_manager); Assembler::with_kernel(source_manager, Self::kernel()) - .with_dynamic_library(StdLibrary::default()) + .with_dynamic_library(CoreLibrary::default()) .expect("failed to load std-lib") - .with_dynamic_library(MidenLib::default()) + .with_dynamic_library(ProtocolLib::default()) .expect("failed to load miden-lib") } @@ -274,19 +261,19 @@ impl TransactionKernel { stack: &StackOutputs, // FIXME TODO add an extension trait for this one ) -> Result<(Word, Word, FungibleAsset, BlockNumber), TransactionOutputError> { let output_notes_commitment = stack - .get_stack_word_be(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4) + .get_stack_word_be(TransactionOutputs::OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4) .expect("output_notes_commitment (first word) missing"); let account_update_commitment = stack - .get_stack_word_be(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4) + .get_stack_word_be(TransactionOutputs::ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4) .expect("account_update_commitment (second word) missing"); let fee = stack - .get_stack_word_be(FEE_ASSET_WORD_IDX * 4) + .get_stack_word_be(TransactionOutputs::FEE_ASSET_WORD_IDX * 4) .expect("fee_asset (third word) missing"); let expiration_block_num = stack - .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX) + .get_stack_item(TransactionOutputs::EXPIRATION_BLOCK_ELEMENT_IDX) .expect("tx_expiration_block_num (element on index 12) missing"); let expiration_block_num = u32::try_from(expiration_block_num.as_int()) @@ -359,7 +346,7 @@ impl TransactionKernel { .get(&final_account_commitment) .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?; - let account = parse_final_account_header(final_account_data) + let account = AccountHeader::try_from_elements(final_account_data) .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?; // validate output notes @@ -443,58 +430,6 @@ impl TransactionKernel { Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES) .expect("failed to deserialize transaction kernel library") } - - /// Returns the mock account and faucet libraries. - pub fn mock_libraries() -> impl Iterator { - use miden_objects::account::AccountCode; - - use crate::testing::mock_account_code::MockAccountCodeExt; - - vec![AccountCode::mock_account_library(), AccountCode::mock_faucet_library()].into_iter() - } - - /// Returns an [`Assembler`] with the transaction kernel as a library. - /// - /// This assembler is the same as [`TransactionKernel::assembler`] but additionally includes the - /// kernel library on the namespace of `$kernel`. The `$kernel` library is added separately - /// because even though the library (`api.masm`) and the kernel binary (`main.masm`) include - /// this code, it is not otherwise accessible. By adding it separately, we can invoke procedures - /// from the kernel library to test them individually. - pub fn with_kernel_library(source_manager: Arc) -> Assembler { - Self::assembler_with_source_manager(source_manager) - .with_dynamic_library(Self::library()) - .expect("failed to load kernel library (/lib)") - .with_debug_mode(true) - } - - /// Returns an [`Assembler`] with the `mock::{account, faucet, util}` libraries. - /// - /// This assembler is the same as [`TransactionKernel::with_kernel_library`] but additionally - /// includes: - /// - [`MockAccountCodeExt::mock_account_library`][account_lib], - /// - [`MockAccountCodeExt::mock_faucet_library`][faucet_lib], - /// - [`mock_util_library`][util_lib] - /// - /// [account_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_account_library - /// [faucet_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_faucet_library - /// [util_lib]: crate::testing::mock_util_lib::mock_util_library - pub fn with_mock_libraries(source_manager: Arc) -> Assembler { - use crate::testing::mock_util_lib::mock_util_library; - - let mut assembler = Self::with_kernel_library(source_manager); - - for library in Self::mock_libraries() { - assembler - .link_dynamic_library(library) - .expect("failed to add mock account libraries"); - } - - assembler - .link_static_library(mock_util_library()) - .expect("failed to add mock test library"); - - assembler - } } impl SequentialCommit for TransactionKernel { @@ -507,13 +442,13 @@ impl SequentialCommit for TransactionKernel { } #[cfg(all(any(feature = "testing", test), feature = "std"))] -mod source_manager_ext { +pub(crate) mod source_manager_ext { use std::path::{Path, PathBuf}; use std::vec::Vec; use std::{fs, io}; - use miden_objects::assembly::SourceManager; - use miden_objects::assembly::debuginfo::SourceManagerExt; + use crate::assembly::SourceManager; + use crate::assembly::debuginfo::SourceManagerExt; /// Loads all files with a .masm extension in the `asm` directory into the provided source /// manager. diff --git a/crates/miden-lib/src/transaction/kernel_procedures.rs b/crates/miden-protocol/src/transaction/kernel/procedures.rs similarity index 53% rename from crates/miden-lib/src/transaction/kernel_procedures.rs rename to crates/miden-protocol/src/transaction/kernel/procedures.rs index eaad10e432..60c5fb4349 100644 --- a/crates/miden-lib/src/transaction/kernel_procedures.rs +++ b/crates/miden-protocol/src/transaction/kernel/procedures.rs @@ -1,94 +1,96 @@ // This file is generated by build.rs, do not modify -use miden_objects::{Word, word}; +use crate::{Word, word}; // KERNEL PROCEDURES // ================================================================================================ /// Hashes of all dynamically executed kernel procedures. -pub const KERNEL_PROCEDURES: [Word; 52] = [ +pub const KERNEL_PROCEDURES: [Word; 53] = [ // account_get_initial_commitment - word!("0x1c95a0386ebf3645c6271253a4ae49ea4be8610dea7b4436c58951277a75f0c1"), + word!("0x1de52b747e823a098f3e146cf2e2b7c3f585a4424ec54c9022414d9ca2574375"), // account_compute_commitment - word!("0x1aed40e2cc4d3798448f4efdce1a14c9598611da065eebe58432f144c3bca9de"), + word!("0xdcb7c06bc7617d49bcda33d1753e327cc744b9f83e2019d61eca016b33c527a7"), // account_get_id word!("0xd76288f2e94b9e6a8f7eeee45c4ee0a23997d78496f6132e3f55681efea809c4"), // account_get_nonce word!("0x4a1f11db21ddb1f0ebf7c9fd244f896a95e99bb136008185da3e7d6aa85827a3"), // account_incr_nonce - word!("0x99c6b16e86eb9eae02657256b8859e2809acd05bf25810933cf50e72d876d8bf"), + word!("0xd1316d21f95e20385283ac72914585a4a3281ff69935540d130fd1ace25ec9ae"), // account_get_code_commitment - word!("0xb2ebd0acc4ef40d37c403190dc07d03d7df9169fb8752e8025e3fe469b5ee192"), + word!("0x8c044d53df35c71425c806b8997c6541e0aac70d7880657e9354b9854626ce3e"), // account_get_initial_storage_commitment word!("0x91377c2852feb7a2798e54d7dbaa2d97000270ec4c0d0888b26d720a25ae0e84"), // account_compute_storage_commitment - word!("0xa87008550383e1a88dde5d0adefc68ee3bf477aec07e4700f9101241aa1e868f"), + word!("0x8732c9765d2b35f0d9f26dcec349f18a7234c9e988057a14e358f97ea123cb5f"), // account_get_item - word!("0xe1e6843fb47f24476a12ef8cd19dd5de2dd74b90433051b26720dce5ab223bf0"), + word!("0xdc6917a6d797c0717a56255e3e94e6d4f1317e92862c7331c3070d82402828ec"), // account_get_initial_item - word!("0x5e956c876cd6eaaa15f5800a5232c6b4e3e50e0335a31ba2e4a9e5f2401aece4"), + word!("0x1853416c007dc75de04c25aaf2376fa7e98d6c010a46bb90e504491f5634ee12"), // account_set_item - word!("0x84b5206c5a0dccf56568bc0157b8322e8a506332bc212f1ad35bab4fe9f6bfed"), + word!("0x29392c01f8953e4e4f6dd8eba69c53bd5f4ff7f54beeaab2e86d8ef7c8d982a0"), // account_get_map_item - word!("0xeb40115d6ca9bed0ac817b246e3d247bb3386e56ee17692d30a3be799f98f2f6"), + word!("0x41e4f17b24281fbb05279fbba5ece3a5181055217be0d1d33cc44b71b6d19a23"), // account_get_initial_map_item - word!("0x054f48624a30f260269cc9d780e9d84b77091e29f1d44c6450ab73e0eb91cc39"), + word!("0x30f13f40cd4de71c1fae252218e27138e0b406a26e9c22cfb0219633fef9de23"), // account_set_map_item - word!("0x33ab43462ceb87f00c5fbebd47cdf948246db0e82b8e85c98edc1b568b784ba3"), + word!("0x722b97307928cda6600f5ed17a5c55c8130200520ff076b08f0f77706f6e81ea"), // account_get_initial_vault_root word!("0x46297d9ac95afd60c7ef1a065e024ad49aa4c019f6b3924191905449b244d4ec"), // account_get_vault_root word!("0x42a2bfb8eac4fce9bbf75ea15215b00729faeeaf7fff784692948d3f618a9bb7"), // account_add_asset - word!("0x11890564573ff2ad0916b1a8ca12a9d2546f4c3dcf09fa4bfb6d72d701a3d7ff"), + word!("0x9a63b5385a8ac6404306852c13b1f7c2625449ce6afde6c14d27701e9e3fb550"), // account_remove_asset - word!("0xcb3118521acfe552030b988439713717fdee54d9e7cb54116d4034cf89ce8a34"), + word!("0x2ac6fa0e12e581a59edbf7ab0995033aa308dbf77ecaf9b9215ebcf27da5a942"), // account_get_balance - word!("0x1ed792cc7775aa1ce2f32367a3d430561ec9bceb33f5bb222691c49a6bde8112"), + word!("0x6a5eb788fd2beec7555874f978a4dd2f2c4f5d8088cd33e148c61450e4510fe1"), // account_get_initial_balance - word!("0xdc1320d6f044c40d37e5e835a584b643d6e77e3fc1136f498815298e28c912b8"), + word!("0x2e0decbc35a10c15ba72c14ed3e32dc9d4a3866f66114c073b3fc7b127362b74"), // account_has_non_fungible_asset - word!("0xfaad11de0c026551df15231790c2364cc598e891444bf826da01b524b1a8ca8f"), + word!("0xffe57961158c8e5f8a3aaa773943ee208fac7ed4786a7c8b6fed04ba54f39111"), // account_compute_delta_commitment - word!("0xbb3ff91c5791cf0062df00954ebea8d97a79084d053e7c3a4555391faebdc819"), + word!("0x09767ee5e29aeca91a57f3af3871bbfb3037681e193444b3f7af878894c1aaa3"), // account_get_num_procedures word!("0x53b5ec38b7841948762c258010e6e07ad93963bcaac2d83813f8edb6710dc720"), // account_get_procedure_root - word!("0x4d7b2e6083820088cd1139ed658b631cf391989b16de8af6741d7e17de9245cd"), + word!("0xa45796077477599813ea54d88a83dfca3e64b1c112117970d63a1c24f7cfef5a"), // account_was_procedure_called - word!("0x37f1f8e67ec9105153721ad59c1f765f58da8f87b0b8eea303701bf4583a5b89"), + word!("0xd9e08c27f3f1e7d01d257cbb4583ecf14f5e362d9c19e39ada8bc19dcc820d45"), // account_has_procedure - word!("0x667d5ce1b7a54c3b8965666ce90e59085c97775b82eba25dddfe218db5fe137d"), + word!("0xb0b63fdd01af0bcb4aacb2412e934cdc7691308647152d416c7ae4fc909da076"), // faucet_mint_asset - word!("0xfc1923fc651f8122a2f32fce96711203bd8309180ce6f484ea6fd4ceb175e3f0"), + word!("0x3d533697caf8b2522507c33b98af7652f3eb2e4f5d29d64f3d147af07ed2c494"), // faucet_burn_asset - word!("0x2633226e8831cfb1e9970ae2ee79b83678538f2c6656a755c707ba85cd462562"), + word!("0xcad46a403b78da4082d3e256025fd427e19b3fbc261e062cd5b2f853617311a7"), // faucet_get_total_fungible_asset_issuance - word!("0x7d32952d4dc0edd0311e3424b8128df2d48cf949f800c28218fbc851a8db42b5"), + word!("0x0953a2f2ec88ad0b7008c3d71aca46ebfcbb58a8ffdf59390616497c6693e8ab"), // faucet_is_non_fungible_asset_issued - word!("0x0323d18fe6bd7bbada87d265d2ffb27f2a7828b1b6865d6d1a3b4a64924a9f7f"), + word!("0x9d9d5ec39551a1ab1c063a6dae9f3633195c89e8e31bbb6b217957f0ea8323c2"), // input_note_get_metadata - word!("0x7ad3e94585e7a397ee27443c98b376ed8d4ba762122af6413fde9314c00a6219"), + word!("0x447b342e38855a9402cde0ea52ecb5e4c1fe542b535a5364cb5caa8e94c82442"), // input_note_get_assets_info - word!("0x159439fe48dbc11e674c5d05830d0408dcfa033c26e85e01256002c6cbc07e9a"), + word!("0xe0817bed99fb61180e705b2c9e5ca8c8f0c62864953247a56acbc65b7d58c2d5"), // input_note_get_script_root word!("0x527036257e58c3a84cf0aa170fb3f219a4553db17d269279355ad164a2b90ac5"), // input_note_get_inputs_info - word!("0xdd8bbf4cdb48051da346bc89760b77fdf4c948904276a99d96409922a00bd322"), + word!("0xb7f45ec34f7708355551dcf1f82c9c40e2c19252f8d5c98dcf9ef1aa0a3eb878"), // input_note_get_serial_number word!("0x25815e02b7976d8e5c297dde60d372cc142c81f702f424ac0920190528c547ee"), // input_note_get_recipient word!("0xd3c255177f9243bb1a523a87615bbe76dd5a3605fcae87eb9d3a626d4ecce33c"), // output_note_create - word!("0x6562a9d1d8605d158415a4dcd4c8fd48fc2b6c21ec64c188db9def16b991a560"), + word!("0xf6d7790691427c5d54ac9dd3b7ed1bb587fa6e864bdf7ba372f022a45c7caa47"), // output_note_get_metadata - word!("0xde4a5b57f9d53692459383e6cf6302ef3602a348896ed6ab6fdf67e07fa483ff"), + word!("0x3db8427f20eb70603b72aa574a986506eb7216312004aeaf8b2a7e55d0049a48"), // output_note_get_assets_info - word!("0x7e5d726b5f25f6cfd533bd0294853f3fceea62c41e5f2fd68919d8d53a48b3f8"), + word!("0xbbf90ac2a7e16ee6d2336f7389d5b972bf0c1fa9938405945b85f948bf24fc4f"), // output_note_get_recipient - word!("0xc824115ed79a2e1670daed8c18fba1bc15f54c5ec0ec6699de69a00b21d9df92"), + word!("0x1ce137f0c5be72832970e6c818968a789f65b97db34515bfebb767705f28db67"), // output_note_add_asset - word!("0x9b6929d1ce24b3a97c6fb098f2fa8d0958beb15f91e268b9c787194b0a977a0d"), + word!("0xaf22383e4390f4f15a429768f79aa445f8a535bb21b0807172b9ef2de063d9d1"), + // output_note_set_attachment + word!("0x800ab6457b20be22a721d61770ab493334004d2b5d01a6bbd245e49554c31a2c"), // tx_get_num_input_notes word!("0xfcc186d4b65c584f3126dda1460b01eef977efd76f9e36f972554af28e33c685"), // tx_get_input_notes_commitment @@ -96,7 +98,7 @@ pub const KERNEL_PROCEDURES: [Word; 52] = [ // tx_get_num_output_notes word!("0x2511fca9c078cd96e526fd488d1362cbfd597eb3db8452aedb00beffee9782b4"), // tx_get_output_notes_commitment - word!("0xd5b22dae48ec4b20ed479f2c43573d34930720886371ef6b484310a3bea4e818"), + word!("0x8b9b29c837b5d0834f550d7f32703b35e2ff014b523dd581a09a0b94a925fcec"), // tx_get_block_commitment word!("0xe474b491a64d222397fcf83ee5db7b048061988e5e83ce99b91bae6fd75a3522"), // tx_get_block_number @@ -104,7 +106,7 @@ pub const KERNEL_PROCEDURES: [Word; 52] = [ // tx_get_block_timestamp word!("0x7903185b847517debb6c2072364e3e757b99ee623e97c2bd0a4661316c5c5418"), // tx_start_foreign_context - word!("0x4bfde60ab4b1e42148ceea2845ecf9aae061a577972baf348379701760d476d7"), + word!("0x753c9c0afcbd36fc7b7ba1e8dc7b668c8aa6036ae8cf5c891587f10117669d76"), // tx_end_foreign_context word!("0xaa0018aa8da890b73511879487f65553753fb7df22de380dd84c11e6f77eec6f"), // tx_get_expiration_delta diff --git a/crates/miden-lib/src/transaction/events.rs b/crates/miden-protocol/src/transaction/kernel/tx_event_id.rs similarity index 60% rename from crates/miden-lib/src/transaction/events.rs rename to crates/miden-protocol/src/transaction/kernel/tx_event_id.rs index 2b3c5edd67..1b6373bbf4 100644 --- a/crates/miden-lib/src/transaction/events.rs +++ b/crates/miden-protocol/src/transaction/kernel/tx_event_id.rs @@ -1,8 +1,8 @@ use core::fmt; -pub use miden_core::EventId; +use miden_core::EventId; -use super::TransactionEventError; +use crate::errors::TransactionEventError; // CONSTANTS // ================================================================================================ @@ -19,7 +19,7 @@ include!(concat!(env!("OUT_DIR"), "/assets/transaction_events.rs")); /// by the transaction kernel are in the `miden` namespace. #[repr(u64)] #[derive(Debug, Clone, Eq, PartialEq)] -pub enum TransactionEvent { +pub enum TransactionEventId { AccountBeforeForeignLoad = ACCOUNT_BEFORE_FOREIGN_LOAD, AccountVaultBeforeAddAsset = ACCOUNT_VAULT_BEFORE_ADD_ASSET, @@ -51,6 +51,8 @@ pub enum TransactionEvent { NoteBeforeAddAsset = NOTE_BEFORE_ADD_ASSET, NoteAfterAddAsset = NOTE_AFTER_ADD_ASSET, + NoteBeforeSetAttachment = NOTE_BEFORE_SET_ATTACHMENT, + AuthRequest = AUTH_REQUEST, PrologueStart = PROLOGUE_START, @@ -80,7 +82,7 @@ pub enum TransactionEvent { Unauthorized = AUTH_UNAUTHORIZED, } -impl TransactionEvent { +impl TransactionEventId { /// Returns `true` if the event is privileged, i.e. it is only allowed to be emitted from the /// root context of the VM, which is where the transaction kernel executes. pub fn is_privileged(&self) -> bool { @@ -94,13 +96,13 @@ impl TransactionEvent { } } -impl fmt::Display for TransactionEvent { +impl fmt::Display for TransactionEventId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } } -impl TryFrom for TransactionEvent { +impl TryFrom for TransactionEventId { type Error = TransactionEventError; fn try_from(event_id: EventId) -> Result { @@ -109,76 +111,82 @@ impl TryFrom for TransactionEvent { let name = EVENT_NAME_LUT.get(&raw).copied(); match raw { - ACCOUNT_BEFORE_FOREIGN_LOAD => Ok(TransactionEvent::AccountBeforeForeignLoad), + ACCOUNT_BEFORE_FOREIGN_LOAD => Ok(TransactionEventId::AccountBeforeForeignLoad), - ACCOUNT_VAULT_BEFORE_ADD_ASSET => Ok(TransactionEvent::AccountVaultBeforeAddAsset), - ACCOUNT_VAULT_AFTER_ADD_ASSET => Ok(TransactionEvent::AccountVaultAfterAddAsset), + ACCOUNT_VAULT_BEFORE_ADD_ASSET => Ok(TransactionEventId::AccountVaultBeforeAddAsset), + ACCOUNT_VAULT_AFTER_ADD_ASSET => Ok(TransactionEventId::AccountVaultAfterAddAsset), ACCOUNT_VAULT_BEFORE_REMOVE_ASSET => { - Ok(TransactionEvent::AccountVaultBeforeRemoveAsset) + Ok(TransactionEventId::AccountVaultBeforeRemoveAsset) + }, + ACCOUNT_VAULT_AFTER_REMOVE_ASSET => { + Ok(TransactionEventId::AccountVaultAfterRemoveAsset) }, - ACCOUNT_VAULT_AFTER_REMOVE_ASSET => Ok(TransactionEvent::AccountVaultAfterRemoveAsset), - ACCOUNT_VAULT_BEFORE_GET_BALANCE => Ok(TransactionEvent::AccountVaultBeforeGetBalance), + ACCOUNT_VAULT_BEFORE_GET_BALANCE => { + Ok(TransactionEventId::AccountVaultBeforeGetBalance) + }, ACCOUNT_VAULT_BEFORE_HAS_NON_FUNGIBLE_ASSET => { - Ok(TransactionEvent::AccountVaultBeforeHasNonFungibleAsset) + Ok(TransactionEventId::AccountVaultBeforeHasNonFungibleAsset) }, - ACCOUNT_STORAGE_BEFORE_SET_ITEM => Ok(TransactionEvent::AccountStorageBeforeSetItem), - ACCOUNT_STORAGE_AFTER_SET_ITEM => Ok(TransactionEvent::AccountStorageAfterSetItem), + ACCOUNT_STORAGE_BEFORE_SET_ITEM => Ok(TransactionEventId::AccountStorageBeforeSetItem), + ACCOUNT_STORAGE_AFTER_SET_ITEM => Ok(TransactionEventId::AccountStorageAfterSetItem), ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM => { - Ok(TransactionEvent::AccountStorageBeforeGetMapItem) + Ok(TransactionEventId::AccountStorageBeforeGetMapItem) }, ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM => { - Ok(TransactionEvent::AccountStorageBeforeSetMapItem) + Ok(TransactionEventId::AccountStorageBeforeSetMapItem) }, ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM => { - Ok(TransactionEvent::AccountStorageAfterSetMapItem) + Ok(TransactionEventId::AccountStorageAfterSetMapItem) }, - ACCOUNT_BEFORE_INCREMENT_NONCE => Ok(TransactionEvent::AccountBeforeIncrementNonce), - ACCOUNT_AFTER_INCREMENT_NONCE => Ok(TransactionEvent::AccountAfterIncrementNonce), + ACCOUNT_BEFORE_INCREMENT_NONCE => Ok(TransactionEventId::AccountBeforeIncrementNonce), + ACCOUNT_AFTER_INCREMENT_NONCE => Ok(TransactionEventId::AccountAfterIncrementNonce), + + ACCOUNT_PUSH_PROCEDURE_INDEX => Ok(TransactionEventId::AccountPushProcedureIndex), - ACCOUNT_PUSH_PROCEDURE_INDEX => Ok(TransactionEvent::AccountPushProcedureIndex), + NOTE_BEFORE_CREATED => Ok(TransactionEventId::NoteBeforeCreated), + NOTE_AFTER_CREATED => Ok(TransactionEventId::NoteAfterCreated), - NOTE_BEFORE_CREATED => Ok(TransactionEvent::NoteBeforeCreated), - NOTE_AFTER_CREATED => Ok(TransactionEvent::NoteAfterCreated), + NOTE_BEFORE_ADD_ASSET => Ok(TransactionEventId::NoteBeforeAddAsset), + NOTE_AFTER_ADD_ASSET => Ok(TransactionEventId::NoteAfterAddAsset), - NOTE_BEFORE_ADD_ASSET => Ok(TransactionEvent::NoteBeforeAddAsset), - NOTE_AFTER_ADD_ASSET => Ok(TransactionEvent::NoteAfterAddAsset), + NOTE_BEFORE_SET_ATTACHMENT => Ok(TransactionEventId::NoteBeforeSetAttachment), - AUTH_REQUEST => Ok(TransactionEvent::AuthRequest), + AUTH_REQUEST => Ok(TransactionEventId::AuthRequest), - PROLOGUE_START => Ok(TransactionEvent::PrologueStart), - PROLOGUE_END => Ok(TransactionEvent::PrologueEnd), + PROLOGUE_START => Ok(TransactionEventId::PrologueStart), + PROLOGUE_END => Ok(TransactionEventId::PrologueEnd), - NOTES_PROCESSING_START => Ok(TransactionEvent::NotesProcessingStart), - NOTES_PROCESSING_END => Ok(TransactionEvent::NotesProcessingEnd), + NOTES_PROCESSING_START => Ok(TransactionEventId::NotesProcessingStart), + NOTES_PROCESSING_END => Ok(TransactionEventId::NotesProcessingEnd), - NOTE_EXECUTION_START => Ok(TransactionEvent::NoteExecutionStart), - NOTE_EXECUTION_END => Ok(TransactionEvent::NoteExecutionEnd), + NOTE_EXECUTION_START => Ok(TransactionEventId::NoteExecutionStart), + NOTE_EXECUTION_END => Ok(TransactionEventId::NoteExecutionEnd), - TX_SCRIPT_PROCESSING_START => Ok(TransactionEvent::TxScriptProcessingStart), - TX_SCRIPT_PROCESSING_END => Ok(TransactionEvent::TxScriptProcessingEnd), + TX_SCRIPT_PROCESSING_START => Ok(TransactionEventId::TxScriptProcessingStart), + TX_SCRIPT_PROCESSING_END => Ok(TransactionEventId::TxScriptProcessingEnd), - EPILOGUE_START => Ok(TransactionEvent::EpilogueStart), - EPILOGUE_AUTH_PROC_START => Ok(TransactionEvent::EpilogueAuthProcStart), - EPILOGUE_AUTH_PROC_END => Ok(TransactionEvent::EpilogueAuthProcEnd), + EPILOGUE_START => Ok(TransactionEventId::EpilogueStart), + EPILOGUE_AUTH_PROC_START => Ok(TransactionEventId::EpilogueAuthProcStart), + EPILOGUE_AUTH_PROC_END => Ok(TransactionEventId::EpilogueAuthProcEnd), EPILOGUE_AFTER_TX_CYCLES_OBTAINED => { - Ok(TransactionEvent::EpilogueAfterTxCyclesObtained) + Ok(TransactionEventId::EpilogueAfterTxCyclesObtained) }, EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT => { - Ok(TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount) + Ok(TransactionEventId::EpilogueBeforeTxFeeRemovedFromAccount) }, - EPILOGUE_END => Ok(TransactionEvent::EpilogueEnd), + EPILOGUE_END => Ok(TransactionEventId::EpilogueEnd), - LINK_MAP_SET => Ok(TransactionEvent::LinkMapSet), - LINK_MAP_GET => Ok(TransactionEvent::LinkMapGet), + LINK_MAP_SET => Ok(TransactionEventId::LinkMapSet), + LINK_MAP_GET => Ok(TransactionEventId::LinkMapGet), - AUTH_UNAUTHORIZED => Ok(TransactionEvent::Unauthorized), + AUTH_UNAUTHORIZED => Ok(TransactionEventId::Unauthorized), _ => Err(TransactionEventError::InvalidTransactionEvent(event_id, name)), } diff --git a/crates/miden-objects/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs similarity index 90% rename from crates/miden-objects/src/transaction/mod.rs rename to crates/miden-protocol/src/transaction/mod.rs index b11954de1a..feef2fc878 100644 --- a/crates/miden-objects/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -1,11 +1,11 @@ use super::account::{AccountDelta, AccountHeader, AccountId}; -use super::block::BlockHeader; use super::note::{NoteId, Nullifier}; use super::vm::AdviceInputs; use super::{Felt, Hasher, WORD_SIZE, Word, ZERO}; mod executed_tx; mod inputs; +mod kernel; mod ordered_transactions; mod outputs; mod partial_blockchain; @@ -17,6 +17,7 @@ mod tx_summary; pub use executed_tx::{ExecutedTransaction, TransactionMeasurements}; pub use inputs::{AccountInputs, InputNote, InputNotes, ToInputNoteCommitments, TransactionInputs}; +pub use kernel::{TransactionAdviceInputs, TransactionEventId, TransactionKernel, memory}; pub use ordered_transactions::OrderedTransactionHeaders; pub use outputs::{OutputNote, OutputNotes, TransactionOutputs}; pub use partial_blockchain::PartialBlockchain; diff --git a/crates/miden-objects/src/transaction/ordered_transactions.rs b/crates/miden-protocol/src/transaction/ordered_transactions.rs similarity index 100% rename from crates/miden-objects/src/transaction/ordered_transactions.rs rename to crates/miden-protocol/src/transaction/ordered_transactions.rs diff --git a/crates/miden-objects/src/transaction/outputs.rs b/crates/miden-protocol/src/transaction/outputs.rs similarity index 85% rename from crates/miden-objects/src/transaction/outputs.rs rename to crates/miden-protocol/src/transaction/outputs.rs index f45e313a54..179da30367 100644 --- a/crates/miden-objects/src/transaction/outputs.rs +++ b/crates/miden-protocol/src/transaction/outputs.rs @@ -6,6 +6,7 @@ use core::fmt::Debug; use crate::account::AccountHeader; use crate::asset::FungibleAsset; use crate::block::BlockNumber; +use crate::errors::TransactionOutputError; use crate::note::{ Note, NoteAssets, @@ -23,7 +24,7 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; -use crate::{Felt, Hasher, MAX_OUTPUT_NOTES_PER_TX, TransactionOutputError, Word}; +use crate::{Felt, Hasher, MAX_OUTPUT_NOTES_PER_TX, Word}; // TRANSACTION OUTPUTS // ================================================================================================ @@ -43,6 +44,23 @@ pub struct TransactionOutputs { pub expiration_block_num: BlockNumber, } +impl TransactionOutputs { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The index of the word at which the final account nonce is stored on the output stack. + pub const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0; + + /// The index of the word at which the account update commitment is stored on the output stack. + pub const ACCOUNT_UPDATE_COMMITMENT_WORD_IDX: usize = 1; + + /// The index of the word at which the fee asset is stored on the output stack. + pub const FEE_ASSET_WORD_IDX: usize = 2; + + /// The index of the item at which the expiration block height is stored on the output stack. + pub const EXPIRATION_BLOCK_ELEMENT_IDX: usize = 12; +} + impl Serializable for TransactionOutputs { fn write_into(&self, target: &mut W) { self.account.write_into(target); @@ -85,6 +103,7 @@ pub struct OutputNotes { impl OutputNotes { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- + /// Returns new [OutputNotes] instantiated from the provide vector of notes. /// /// # Errors @@ -103,7 +122,7 @@ impl OutputNotes { } } - let commitment = Self::compute_commitment(notes.iter().map(NoteHeader::from)); + let commitment = Self::compute_commitment(notes.iter().map(OutputNote::header)); Ok(Self { notes, commitment }) } @@ -146,9 +165,13 @@ impl OutputNotes { /// Computes a commitment to output notes. /// - /// For a non-empty list of notes, this is a sequential hash of (note_id, metadata) tuples for - /// the notes created in a transaction. For an empty list, [EMPTY_WORD] is returned. - pub(crate) fn compute_commitment(notes: impl ExactSizeIterator) -> Word { + /// - For an empty list, [`Word::empty`] is returned. + /// - For a non-empty list of notes, this is a sequential hash of (note_id, metadata_commitment) + /// tuples for the notes created in a transaction, where `metadata_commitment` is the return + /// value of [`NoteMetadata::to_commitment`]. + pub(crate) fn compute_commitment<'header>( + notes: impl ExactSizeIterator, + ) -> Word { if notes.len() == 0 { return Word::empty(); } @@ -156,7 +179,7 @@ impl OutputNotes { let mut elements: Vec = Vec::with_capacity(notes.len() * 8); for note_header in notes { elements.extend_from_slice(note_header.id().as_elements()); - elements.extend_from_slice(Word::from(note_header.metadata()).as_elements()); + elements.extend_from_slice(note_header.metadata().to_commitment().as_elements()); } Hasher::hash_elements(&elements) @@ -261,40 +284,30 @@ impl OutputNote { pub fn shrink(&self) -> Self { match self { OutputNote::Full(note) if note.metadata().is_private() => { - OutputNote::Header(*note.header()) + OutputNote::Header(note.header().clone()) }, - OutputNote::Partial(note) => OutputNote::Header(note.into()), + OutputNote::Partial(note) => OutputNote::Header(note.header().clone()), _ => self.clone(), } } + /// Returns a reference to the [`NoteHeader`] of this note. + pub fn header(&self) -> &NoteHeader { + match self { + OutputNote::Full(note) => note.header(), + OutputNote::Partial(note) => note.header(), + OutputNote::Header(header) => header, + } + } + /// Returns a commitment to the note and its metadata. /// - /// > hash(NOTE_ID || NOTE_METADATA) + /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) pub fn commitment(&self) -> Word { compute_note_commitment(self.id(), self.metadata()) } } -// CONVERSIONS -// ------------------------------------------------------------------------------------------------ - -impl From for NoteHeader { - fn from(value: OutputNote) -> Self { - (&value).into() - } -} - -impl From<&OutputNote> for NoteHeader { - fn from(value: &OutputNote) -> Self { - match value { - OutputNote::Full(note) => note.into(), - OutputNote::Partial(note) => note.into(), - OutputNote::Header(note) => *note, - } - } -} - // SERIALIZATION // ------------------------------------------------------------------------------------------------ @@ -336,9 +349,10 @@ mod output_notes_tests { use assert_matches::assert_matches; use super::OutputNotes; + use crate::Word; + use crate::errors::TransactionOutputError; use crate::note::Note; use crate::transaction::OutputNote; - use crate::{TransactionOutputError, Word}; #[test] fn test_duplicate_output_notes() -> anyhow::Result<()> { diff --git a/crates/miden-objects/src/transaction/partial_blockchain.rs b/crates/miden-protocol/src/transaction/partial_blockchain.rs similarity index 93% rename from crates/miden-objects/src/transaction/partial_blockchain.rs rename to crates/miden-protocol/src/transaction/partial_blockchain.rs index 2142ad69dd..86dfcb409f 100644 --- a/crates/miden-objects/src/transaction/partial_blockchain.rs +++ b/crates/miden-protocol/src/transaction/partial_blockchain.rs @@ -2,9 +2,10 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; use core::ops::RangeTo; -use crate::PartialBlockchainError; use crate::block::{BlockHeader, BlockNumber}; -use crate::crypto::merkle::{InnerNodeInfo, MmrPeaks, PartialMmr}; +use crate::crypto::merkle::InnerNodeInfo; +use crate::crypto::merkle::mmr::{MmrPeaks, PartialMmr}; +use crate::errors::PartialBlockchainError; use crate::utils::serde::{Deserializable, Serializable}; // PARTIAL BLOCKCHAIN @@ -28,9 +29,9 @@ use crate::utils::serde::{Deserializable, Serializable}; /// /// # Guarantees /// -/// The [`PartialBlockchain`] contains the full authenticated [`BlockHeader`]s of all blocks it -/// tracks in its partial MMR and users of this type can make this assumption. This is ensured when -/// using [`PartialBlockchain::new`]. [`PartialBlockchain::new_unchecked`] should only be used +/// The [`PartialBlockchain`] contains the full authenticated [`BlockHeader`]s of all blocks +/// it tracks in its partial MMR and users of this type can make this assumption. This is ensured +/// when using [`PartialBlockchain::new`]. [`PartialBlockchain::new_unchecked`] should only be used /// whenever this guarantee can be upheld. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PartialBlockchain { @@ -182,7 +183,7 @@ impl PartialBlockchain { /// # Panics /// Panics if the `block_header.block_num` is not equal to the current chain length (i.e., the /// provided block header is not the next block in the chain). - pub fn add_block(&mut self, block_header: BlockHeader, track: bool) { + pub fn add_block(&mut self, block_header: &BlockHeader, track: bool) { assert_eq!(block_header.block_num(), self.chain_length()); self.mmr.add(block_header.commitment(), track); } @@ -277,13 +278,17 @@ impl Default for PartialBlockchain { mod tests { use assert_matches::assert_matches; use miden_core::utils::{Deserializable, Serializable}; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; use super::PartialBlockchain; + use crate::Word; use crate::alloc::vec::Vec; use crate::block::{BlockHeader, BlockNumber, FeeParameters}; - use crate::crypto::merkle::{Mmr, PartialMmr}; + use crate::crypto::dsa::ecdsa_k256_keccak::SecretKey; + use crate::crypto::merkle::mmr::{Mmr, PartialMmr}; + use crate::errors::PartialBlockchainError; use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; - use crate::{PartialBlockchainError, Word}; #[test] fn test_partial_blockchain_add() { @@ -298,9 +303,9 @@ mod tests { // add a new block to the partial blockchain, this reduces the number of peaks to 1 let block_num = 3; - let bock_header = int_to_block_header(block_num); - mmr.add(bock_header.commitment()); - partial_blockchain.add_block(bock_header, true); + let block_header = int_to_block_header(block_num); + mmr.add(block_header.commitment()); + partial_blockchain.add_block(&block_header, true); assert_eq!( mmr.open(block_num as usize).unwrap(), @@ -309,9 +314,9 @@ mod tests { // add one more block to the partial blockchain, the number of peaks is again 2 let block_num = 4; - let bock_header = int_to_block_header(block_num); - mmr.add(bock_header.commitment()); - partial_blockchain.add_block(bock_header, true); + let block_header = int_to_block_header(block_num); + mmr.add(block_header.commitment()); + partial_blockchain.add_block(&block_header, true); assert_eq!( mmr.open(block_num as usize).unwrap(), @@ -320,9 +325,9 @@ mod tests { // add one more block to the partial blockchain, the number of peaks is still 2 let block_num = 5; - let bock_header = int_to_block_header(block_num); - mmr.add(bock_header.commitment()); - partial_blockchain.add_block(bock_header, true); + let block_header = int_to_block_header(block_num); + mmr.add(block_header.commitment()); + partial_blockchain.add_block(&block_header, true); assert_eq!( mmr.open(block_num as usize).unwrap(), @@ -426,6 +431,8 @@ mod tests { let fee_parameters = FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500) .expect("native asset ID should be a fungible faucet ID"); + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let validator_key = SecretKey::with_rng(&mut rng).public_key(); BlockHeader::new( 0, @@ -437,7 +444,7 @@ mod tests { Word::empty(), Word::empty(), Word::empty(), - Word::empty(), + validator_key, fee_parameters, 0, ) diff --git a/crates/miden-objects/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs similarity index 98% rename from crates/miden-objects/src/transaction/proven_tx.rs rename to crates/miden-protocol/src/transaction/proven_tx.rs index 3b002c80c9..ab679b9ffc 100644 --- a/crates/miden-objects/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -7,6 +7,7 @@ use crate::account::Account; use crate::account::delta::AccountUpdateDetails; use crate::asset::FungibleAsset; use crate::block::BlockNumber; +use crate::errors::ProvenTransactionError; use crate::note::NoteHeader; use crate::transaction::{ AccountId, @@ -24,7 +25,7 @@ use crate::utils::serde::{ Serializable, }; use crate::vm::ExecutionProof; -use crate::{ACCOUNT_UPDATE_MAX_SIZE, ProvenTransactionError, Word}; +use crate::{ACCOUNT_UPDATE_MAX_SIZE, Word}; // PROVEN TRANSACTION // ================================================================================================ @@ -636,7 +637,7 @@ impl From<&InputNote> for InputNoteCommitment { }, InputNote::Unauthenticated { note } => Self { nullifier: note.nullifier(), - header: Some(*note.header()), + header: Some(note.header().clone()), }, } } @@ -654,7 +655,7 @@ impl ToInputNoteCommitments for InputNoteCommitment { } fn note_commitment(&self) -> Option { - self.header.map(|header| header.commitment()) + self.header.as_ref().map(NoteHeader::commitment) } } @@ -701,9 +702,11 @@ mod tests { AccountType, AccountVaultDelta, StorageMapDelta, + StorageSlotName, }; use crate::asset::FungibleAsset; use crate::block::BlockNumber; + use crate::errors::ProvenTransactionError; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, @@ -712,14 +715,7 @@ mod tests { use crate::testing::noop_auth_component::NoopAuthComponent; use crate::transaction::{ProvenTransactionBuilder, TxAccountUpdate}; use crate::utils::Serializable; - use crate::{ - ACCOUNT_UPDATE_MAX_SIZE, - EMPTY_WORD, - LexicographicWord, - ONE, - ProvenTransactionError, - Word, - }; + use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, LexicographicWord, ONE, Word}; fn check_if_sync() {} fn check_if_send() {} @@ -776,7 +772,8 @@ mod tests { let storage_delta = StorageMapDelta::new(map); // A delta that exceeds the limit returns an error. - let storage_delta = AccountStorageDelta::from_iters([], [], [(4, storage_delta)]); + let storage_delta = + AccountStorageDelta::from_iters([], [], [(StorageSlotName::mock(4), storage_delta)]); let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE) .unwrap(); let details = AccountUpdateDetails::Delta(delta); diff --git a/crates/miden-objects/src/transaction/transaction_id.rs b/crates/miden-protocol/src/transaction/transaction_id.rs similarity index 69% rename from crates/miden-objects/src/transaction/transaction_id.rs rename to crates/miden-protocol/src/transaction/transaction_id.rs index 8a8648af61..e75f7662f7 100644 --- a/crates/miden-objects/src/transaction/transaction_id.rs +++ b/crates/miden-protocol/src/transaction/transaction_id.rs @@ -1,6 +1,8 @@ use alloc::string::String; use core::fmt::{Debug, Display}; +use miden_protocol_macros::WordWrapper; + use super::{Felt, Hasher, ProvenTransaction, WORD_SIZE, Word, ZERO}; use crate::utils::serde::{ ByteReader, @@ -23,7 +25,7 @@ use crate::utils::serde::{ /// This achieves the following properties: /// - Transactions are identical if and only if they have the same ID. /// - Computing transaction ID can be done solely from public transaction data. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, WordWrapper)] pub struct TransactionId(Word); impl TransactionId { @@ -41,26 +43,6 @@ impl TransactionId { elements[12..].copy_from_slice(output_notes_commitment.as_elements()); Self(Hasher::hash_elements(&elements)) } - - /// Returns the elements representation of this transaction ID. - pub fn as_elements(&self) -> &[Felt] { - self.0.as_elements() - } - - /// Returns the byte representation of this transaction ID. - 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 digest defining this transaction ID. - pub fn as_word(&self) -> Word { - self.0 - } } impl Debug for TransactionId { @@ -89,39 +71,6 @@ impl From<&ProvenTransaction> for TransactionId { } } -impl From for TransactionId { - fn from(digest: Word) -> Self { - Self(digest) - } -} - -// CONVERSIONS FROM TRANSACTION ID -// ================================================================================================ - -impl From for Word { - fn from(id: TransactionId) -> Self { - id.0 - } -} - -impl From for [u8; 32] { - fn from(id: TransactionId) -> Self { - id.0.into() - } -} - -impl From<&TransactionId> for Word { - fn from(id: &TransactionId) -> Self { - id.0 - } -} - -impl From<&TransactionId> for [u8; 32] { - fn from(id: &TransactionId) -> Self { - id.0.into() - } -} - // SERIALIZATION // ================================================================================================ diff --git a/crates/miden-objects/src/transaction/tx_args.rs b/crates/miden-protocol/src/transaction/tx_args.rs similarity index 100% rename from crates/miden-objects/src/transaction/tx_args.rs rename to crates/miden-protocol/src/transaction/tx_args.rs diff --git a/crates/miden-objects/src/transaction/tx_header.rs b/crates/miden-protocol/src/transaction/tx_header.rs similarity index 78% rename from crates/miden-objects/src/transaction/tx_header.rs rename to crates/miden-protocol/src/transaction/tx_header.rs index bdec27be5e..210d722e4b 100644 --- a/crates/miden-objects/src/transaction/tx_header.rs +++ b/crates/miden-protocol/src/transaction/tx_header.rs @@ -3,11 +3,14 @@ use alloc::vec::Vec; use miden_processor::DeserializationError; use crate::Word; +use crate::asset::FungibleAsset; use crate::note::NoteHeader; use crate::transaction::{ AccountId, + ExecutedTransaction, InputNoteCommitment, InputNotes, + OutputNote, OutputNotes, ProvenTransaction, TransactionId, @@ -29,6 +32,7 @@ pub struct TransactionHeader { final_state_commitment: Word, input_notes: InputNotes, output_notes: Vec, + fee: FungibleAsset, } impl TransactionHeader { @@ -42,17 +46,18 @@ impl TransactionHeader { /// The input notes and output notes must be in the same order as they appeared in the /// transaction that this header represents, otherwise an incorrect ID will be computed. /// - /// Note that this cannot validate that the [`AccountId`] is valid with respect to the other - /// data. This must be validated outside of this type. + /// Note that this cannot validate that the [`AccountId`] or the fee asset is valid with respect + /// to the other data. This must be validated outside of this type. pub fn new( account_id: AccountId, initial_state_commitment: Word, final_state_commitment: Word, input_notes: InputNotes, output_notes: Vec, + fee: FungibleAsset, ) -> Self { let input_notes_commitment = input_notes.commitment(); - let output_notes_commitment = OutputNotes::compute_commitment(output_notes.iter().copied()); + let output_notes_commitment = OutputNotes::compute_commitment(output_notes.iter()); let id = TransactionId::new( initial_state_commitment, @@ -68,6 +73,7 @@ impl TransactionHeader { final_state_commitment, input_notes, output_notes, + fee, } } @@ -84,6 +90,7 @@ impl TransactionHeader { final_state_commitment: Word, input_notes: InputNotes, output_notes: Vec, + fee: FungibleAsset, ) -> Self { Self { id, @@ -92,6 +99,7 @@ impl TransactionHeader { final_state_commitment, input_notes, output_notes, + fee, } } @@ -141,6 +149,11 @@ impl TransactionHeader { pub fn output_notes(&self) -> &[NoteHeader] { &self.output_notes } + + /// Returns the fee paid by this transaction. + pub fn fee(&self) -> FungibleAsset { + self.fee + } } impl From<&ProvenTransaction> for TransactionHeader { @@ -154,7 +167,23 @@ impl From<&ProvenTransaction> for TransactionHeader { tx.account_update().initial_state_commitment(), tx.account_update().final_state_commitment(), tx.input_notes().clone(), - tx.output_notes().iter().map(NoteHeader::from).collect(), + tx.output_notes().iter().map(OutputNote::header).cloned().collect(), + tx.fee(), + ) + } +} + +impl From<&ExecutedTransaction> for TransactionHeader { + /// Constructs a [`TransactionHeader`] from a [`ExecutedTransaction`]. + fn from(tx: &ExecutedTransaction) -> Self { + TransactionHeader::new_unchecked( + tx.id(), + tx.account_id(), + tx.initial_account().initial_commitment(), + tx.final_account().commitment(), + tx.input_notes().to_commitments(), + tx.output_notes().iter().map(OutputNote::header).cloned().collect(), + tx.fee(), ) } } @@ -164,11 +193,22 @@ impl From<&ProvenTransaction> for TransactionHeader { impl Serializable for TransactionHeader { fn write_into(&self, target: &mut W) { - self.account_id.write_into(target); - self.initial_state_commitment.write_into(target); - self.final_state_commitment.write_into(target); - self.input_notes.write_into(target); - self.output_notes.write_into(target); + let Self { + id: _, + account_id, + initial_state_commitment, + final_state_commitment, + input_notes, + output_notes, + fee, + } = self; + + account_id.write_into(target); + initial_state_commitment.write_into(target); + final_state_commitment.write_into(target); + input_notes.write_into(target); + output_notes.write_into(target); + fee.write_into(target); } } @@ -179,6 +219,7 @@ impl Deserializable for TransactionHeader { let final_state_commitment = ::read_from(source)?; let input_notes = >::read_from(source)?; let output_notes = >::read_from(source)?; + let fee = FungibleAsset::read_from(source)?; let tx_header = Self::new( account_id, @@ -186,6 +227,7 @@ impl Deserializable for TransactionHeader { final_state_commitment, input_notes, output_notes, + fee, ); Ok(tx_header) diff --git a/crates/miden-objects/src/transaction/tx_summary.rs b/crates/miden-protocol/src/transaction/tx_summary.rs similarity index 100% rename from crates/miden-objects/src/transaction/tx_summary.rs rename to crates/miden-protocol/src/transaction/tx_summary.rs diff --git a/crates/miden-lib/Cargo.toml b/crates/miden-standards/Cargo.toml similarity index 52% rename from crates/miden-lib/Cargo.toml rename to crates/miden-standards/Cargo.toml index 288d2fb02b..a57fb2814e 100644 --- a/crates/miden-lib/Cargo.toml +++ b/crates/miden-standards/Cargo.toml @@ -1,12 +1,12 @@ [package] authors.workspace = true categories = ["no-std"] -description = "Standard library of the Miden protocol" +description = "Standards of the Miden protocol" edition.workspace = true homepage.workspace = true keywords = ["kernel", "miden", "transaction"] license.workspace = true -name = "miden-lib" +name = "miden-standards" readme = "README.md" repository.workspace = true rust-version.workspace = true @@ -15,33 +15,35 @@ version.workspace = true [lib] [features] -default = ["std"] -std = ["miden-assembly/std", "miden-objects/std", "miden-processor/std", "miden-stdlib/std"] -testing = ["dep:rand", "miden-objects/testing"] -with-debug-info = ["miden-stdlib/with-debug-info"] +default = ["std"] +std = ["miden-assembly/std", "miden-core-lib/std", "miden-processor/std", "miden-protocol/std"] +testing = ["dep:rand", "miden-assembly/testing", "miden-protocol/testing"] [dependencies] # Miden dependencies -miden-core = { workspace = true } -miden-objects = { workspace = true } miden-processor = { workspace = true } -miden-stdlib = { workspace = true } +miden-protocol = { workspace = true } # External dependencies rand = { optional = true, workspace = true } thiserror = { workspace = true } [build-dependencies] -Inflector = { version = "0.11" } fs-err = { version = "3" } miden-assembly = { workspace = true } miden-core = { workspace = true } -miden-stdlib = { workspace = true } +miden-core-lib = { workspace = true } +miden-protocol = { workspace = true } regex = { version = "1.11" } walkdir = { version = "2.5" } [dev-dependencies] anyhow = "1.0" assert_matches = { workspace = true } -miden-objects = { features = ["testing"], workspace = true } miden-processor = { features = ["testing"], workspace = true } +miden-protocol = { features = ["testing"], workspace = true } + +# When building as a dev-dependency (e.g., `cargo test --workspace` or `cargo check --all-targets`), +# enable the `testing` feature. This is a workaround for Cargo's lack of test-specific features. +# See: https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987 +miden-standards = { features = ["testing"], path = "." } diff --git a/crates/miden-lib/README.md b/crates/miden-standards/README.md similarity index 61% rename from crates/miden-lib/README.md rename to crates/miden-standards/README.md index 918fa2c761..7cdd97adcb 100644 --- a/crates/miden-lib/README.md +++ b/crates/miden-standards/README.md @@ -1,6 +1,6 @@ -# Transaction kernel +# Miden Standards -This crate contains the code of the Miden protocol kernels and standardized smart contracts. +This crate contains the code of Miden's standardized smart contracts. ## Status diff --git a/crates/miden-lib/asm/account_components/ecdsa_k256_keccak.masm b/crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak.masm similarity index 86% rename from crates/miden-lib/asm/account_components/ecdsa_k256_keccak.masm rename to crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak.masm index 2f4a867d36..e226eeee00 100644 --- a/crates/miden-lib/asm/account_components/ecdsa_k256_keccak.masm +++ b/crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak.masm @@ -2,8 +2,8 @@ # # See the `AuthEcdsaK256Keccak` Rust type's documentation for more details. -use miden::auth::ecdsa_k256_keccak -use miden::active_account +use miden::standards::auth::ecdsa_k256_keccak +use miden::protocol::active_account type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } @@ -11,7 +11,7 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # ================================================================================================= # The slot in this component's storage layout where the public key is stored. -const PUBLIC_KEY_SLOT = 0 +const PUBLIC_KEY_SLOT = word("miden::standards::auth::ecdsa_k256_keccak::public_key") #! Authenticate a transaction using the ECDSA signature scheme. #! @@ -34,7 +34,8 @@ pub proc auth_tx_ecdsa_k256_keccak(auth_args: BeWord) # Fetch public key from storage. # --------------------------------------------------------------------------------------------- - push.PUBLIC_KEY_SLOT exec.active_account::get_item + + push.PUBLIC_KEY_SLOT[0..2] exec.active_account::get_item # => [PUB_KEY, pad(16)] exec.ecdsa_k256_keccak::authenticate_transaction diff --git a/crates/miden-lib/asm/account_components/ecdsa_k256_keccak_acl.masm b/crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak_acl.masm similarity index 84% rename from crates/miden-lib/asm/account_components/ecdsa_k256_keccak_acl.masm rename to crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak_acl.masm index 8f2929c417..173b811386 100644 --- a/crates/miden-lib/asm/account_components/ecdsa_k256_keccak_acl.masm +++ b/crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak_acl.masm @@ -2,10 +2,10 @@ # # See the `AuthEcdsaK256KeccakAcl` Rust type's documentation for more details. -use miden::active_account -use miden::native_account -use miden::tx -use std::word +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::word type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } @@ -13,13 +13,13 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # ================================================================================================ # The slot in this component's storage layout where the public key is stored. -const PUBLIC_KEY_SLOT = 0 +const PUBLIC_KEY_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_acl::public_key") # The slot where the authentication configuration is stored. -const AUTH_CONFIG_SLOT = 1 +const AUTH_CONFIG_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_acl::config") # The slot where the map of auth trigger procedure roots is stored. -const AUTH_TRIGGER_PROCS_MAP_SLOT = 2 +const AUTH_TRIGGER_PROCS_MAP_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_acl::trigger_procedure_roots") #! Authenticate a transaction using the ECDSA signature scheme based on procedure calls and note usage. #! @@ -35,12 +35,13 @@ const AUTH_TRIGGER_PROCS_MAP_SLOT = 2 #! Outputs: [pad(16)] #! #! Invocation: call -pub proc auth_tx_ecdsa_k256_keccak_acl.2(auth_args: BeWord) +@locals(2) +pub proc auth_tx_ecdsa_k256_keccak_acl(auth_args: BeWord) dropw # => [pad(16)] # Get the authentication configuration - push.AUTH_CONFIG_SLOT exec.active_account::get_item + push.AUTH_CONFIG_SLOT[0..2] exec.active_account::get_item # => [0, allow_unauthorized_input_notes, allow_unauthorized_output_notes, num_auth_trigger_procs, pad(16)] drop @@ -62,8 +63,8 @@ pub proc auth_tx_ecdsa_k256_keccak_acl.2(auth_args: BeWord) # => [require_acl_auth, i, pad(16)] # Get the procedure root from storage - dup.1 sub.1 push.0.0.0 push.AUTH_TRIGGER_PROCS_MAP_SLOT - # => [AUTH_TRIGGER_PROCS_MAP_SLOT, [0, 0, 0, i-1], require_acl_auth, i, pad(16)] + dup.1 sub.1 push.0.0.0 push.AUTH_TRIGGER_PROCS_MAP_SLOT[0..2] + # => [trigger_proc_slot_prefix, trigger_proc_slot_suffix, [0, 0, 0, i-1], require_acl_auth, i, pad(16)] exec.active_account::get_map_item # => [AUTH_TRIGGER_PROC_ROOT, require_acl_auth, i, pad(16)] @@ -124,10 +125,10 @@ pub proc auth_tx_ecdsa_k256_keccak_acl.2(auth_args: BeWord) # If authentication is required, perform signature verification if.true # Fetch public key from storage. - push.PUBLIC_KEY_SLOT exec.active_account::get_item + push.PUBLIC_KEY_SLOT[0..2] exec.active_account::get_item # => [PUB_KEY, pad(16)] - exec.::miden::auth::ecdsa_k256_keccak::authenticate_transaction + exec.::miden::standards::auth::ecdsa_k256_keccak::authenticate_transaction else # ------ Check if initial account commitment differs from current commitment ------ diff --git a/crates/miden-lib/asm/account_components/multisig_ecdsa_k256_keccak.masm b/crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak_multisig.masm similarity index 87% rename from crates/miden-lib/asm/account_components/multisig_ecdsa_k256_keccak.masm rename to crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak_multisig.masm index 86f30222ef..b03b896d6a 100644 --- a/crates/miden-lib/asm/account_components/multisig_ecdsa_k256_keccak.masm +++ b/crates/miden-standards/asm/account_components/auth/ecdsa_k256_keccak_multisig.masm @@ -2,9 +2,9 @@ # # See the `AuthEcdsaK256KeccakMultisig` Rust type's documentation for more details. -use miden::active_account -use miden::native_account -use miden::auth +use miden::protocol::active_account +use miden::protocol::native_account +use miden::standards::auth type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } @@ -29,19 +29,19 @@ const AUTH_UNAUTHORIZED_EVENT = event("miden::auth::unauthorized") # number of approvers are stored as: # [default_threshold, num_approvers, 0, 0]. # The threshold is guaranteed to be less than or equal to num_approvers. -const THRESHOLD_CONFIG_SLOT = 0 +const THRESHOLD_CONFIG_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_multisig::threshold_config") # The slot in this component's storage layout where the public keys map is stored. # Map entries: [key_index, 0, 0, 0] => APPROVER_PUBLIC_KEY -const PUBLIC_KEYS_MAP_SLOT = 1 +const APPROVER_PUBLIC_KEYS_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_multisig::approver_public_keys") # The slot in this component's storage layout where executed transactions are stored. # Map entries: transaction_message => [is_executed, 0, 0, 0] -const EXECUTED_TXS_SLOT = 2 +const EXECUTED_TXS_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_multisig::executed_transactions") # The slot in this component's storage layout where procedure thresholds are stored. # Map entries: PROC_ROOT => [proc_threshold, 0, 0, 0] -const.PROC_THRESHOLD_ROOTS_SLOT=3 +const PROC_THRESHOLD_ROOTS_SLOT = word("miden::standards::auth::ecdsa_k256_keccak_multisig::procedure_thresholds") # Executed Transaction Flag Constant const IS_EXECUTED_FLAG = [1, 0, 0, 0] @@ -69,14 +69,14 @@ proc assert_new_tx(msg: BeWord) swapw # => [MSG, IS_EXECUTED_FLAG] - push.EXECUTED_TXS_SLOT - # => [index, MSG, IS_EXECUTED_FLAG] + push.EXECUTED_TXS_SLOT[0..2] + # => [txs_slot_prefix, txs_slot_suffix, MSG, IS_EXECUTED_FLAG] # Set the key value pair in the map to mark transaction as executed exec.native_account::set_map_item - # => [OLD_MAP_ROOT, [0, 0, 0, is_executed]] + # => [[0, 0, 0, is_executed]] - dropw drop drop drop + drop drop drop # => [is_executed] assertz.err=ERR_TX_ALREADY_EXECUTED @@ -115,13 +115,13 @@ proc cleanup_pubkey_mapping(init_num_of_approvers: u32, new_num_of_approvers: u3 padw swapw # => [[0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers] - push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, [0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers] + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_prefix, pub_key_slot_suffix, [0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers] exec.native_account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, new_num_of_approvers] + # => [OLD_VALUE, i-1, new_num_of_approvers] - dropw dropw + dropw # => [i-1, new_num_of_approvers] dup.1 dup.1 @@ -151,7 +151,8 @@ end #! Locals: #! 0: new_num_of_approvers #! 1: init_num_of_approvers -pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) +@locals(2) +pub proc update_signers_and_threshold(multisig_config_hash: BeWord) adv.push_mapval # => [MULTISIG_CONFIG_HASH, pad(12)] @@ -178,8 +179,8 @@ pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG # => [MULTISIG_CONFIG, pad(12)] - push.THRESHOLD_CONFIG_SLOT - # => [slot, MULTISIG_CONFIG, pad(12)] + push.THRESHOLD_CONFIG_SLOT[0..2] + # => [config_slot_prefix, config_slot_suffix, MULTISIG_CONFIG, pad(12)] exec.native_account::set_item # => [OLD_THRESHOLD_CONFIG, pad(12)] @@ -205,13 +206,13 @@ pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) swapw # => [[0, 0, 0, i-1], PUB_KEY, i-1, pad(12)] - push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, [0, 0, 0, i-1], PUB_KEY, i-1, pad(12)] + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_prefix, pub_key_slot_suffix, [0, 0, 0, i-1], PUB_KEY, i-1, pad(12)] exec.native_account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, pad(12)] + # => [OLD_VALUE, i-1, pad(12)] - dropw dropw + dropw # => [i-1, pad(12)] dup neq.0 @@ -238,7 +239,8 @@ end # #! Inputs: [default_threshold] #! Outputs: [transaction_threshold] -proc compute_transaction_threshold.1(default_threshold: u32) -> u32 +@locals(1) +proc compute_transaction_threshold(default_threshold: u32) -> u32 # 1. initialize transaction_threshold = 0 # 2. iterate through all account procedures # a. check if the procedure was called during the transaction @@ -277,8 +279,8 @@ proc compute_transaction_threshold.1(default_threshold: u32) -> u32 if.true # => [PROC_ROOT, num_procedures-1, transaction_threshold] - push.PROC_THRESHOLD_ROOTS_SLOT - # => [PROC_THRESHOLD_ROOTS_SLOT, PROC_ROOT, num_procedures-1, transaction_threshold] + push.PROC_THRESHOLD_ROOTS_SLOT[0..2] + # => [proc_roots_slot_prefix, proc_roots_slot_suffix, PROC_ROOT, num_procedures-1, transaction_threshold] # 2b. get the override proc_threshold of that procedure # if the procedure has no override threshold, the returned map item will be [0, 0, 0, 0] @@ -353,7 +355,8 @@ end #! - the same transaction has already been executed (replay protection). #! #! Invocation: call -pub proc auth_tx_ecdsa_k256_keccak_multisig.1(salt: BeWord) +@locals(1) +pub proc auth_tx_ecdsa_k256_keccak_multisig(salt: BeWord) exec.native_account::incr_nonce drop # => [SALT] @@ -372,8 +375,8 @@ pub proc auth_tx_ecdsa_k256_keccak_multisig.1(salt: BeWord) # ------ Verifying approver signatures ------ - push.THRESHOLD_CONFIG_SLOT - # => [index, TX_SUMMARY_COMMITMENT] + push.THRESHOLD_CONFIG_SLOT[0..2] + # => [config_slot_prefix, config_slot_suffix, TX_SUMMARY_COMMITMENT] exec.active_account::get_initial_item # => [0, 0, num_of_approvers, default_threshold, TX_SUMMARY_COMMITMENT] @@ -384,10 +387,10 @@ pub proc auth_tx_ecdsa_k256_keccak_multisig.1(salt: BeWord) swap movdn.5 # => [num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_prefix, pub_key_slot_suffix, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - exec.::miden::auth::ecdsa_k256_keccak::verify_signatures + exec.::miden::standards::auth::ecdsa_k256_keccak::verify_signatures # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, default_threshold] # ------ Checking threshold is >= num_verified_signatures ------ diff --git a/crates/miden-lib/asm/account_components/rpo_falcon_512.masm b/crates/miden-standards/asm/account_components/auth/falcon_512_rpo.masm similarity index 73% rename from crates/miden-lib/asm/account_components/rpo_falcon_512.masm rename to crates/miden-standards/asm/account_components/auth/falcon_512_rpo.masm index 7e68c230ee..bee640eab6 100644 --- a/crates/miden-lib/asm/account_components/rpo_falcon_512.masm +++ b/crates/miden-standards/asm/account_components/auth/falcon_512_rpo.masm @@ -1,9 +1,9 @@ -# The MASM code of the RPO Falcon 512 authentication Account Component. +# The MASM code of the Falcon 512 RPO authentication Account Component. # -# See the `AuthRpoFalcon512` Rust type's documentation for more details. +# See the `AuthFalcon512Rpo` Rust type's documentation for more details. -use miden::auth::rpo_falcon512 -use miden::active_account +use miden::standards::auth::falcon512_rpo +use miden::protocol::active_account type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } @@ -11,7 +11,7 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # ================================================================================================= # The slot in this component's storage layout where the public key is stored. -const PUBLIC_KEY_SLOT = 0 +const PUBLIC_KEY_SLOT = word("miden::standards::auth::falcon512_rpo::public_key") #! Authenticate a transaction using the Falcon signature scheme. #! @@ -28,15 +28,16 @@ const PUBLIC_KEY_SLOT = 0 #! Outputs: [pad(16)] #! #! Invocation: call -pub proc auth_tx_rpo_falcon512(auth_args: BeWord) +pub proc auth_tx_falcon512_rpo(auth_args: BeWord) dropw # => [pad(16)] # Fetch public key from storage. # --------------------------------------------------------------------------------------------- - push.PUBLIC_KEY_SLOT exec.active_account::get_item + + push.PUBLIC_KEY_SLOT[0..2] exec.active_account::get_item # => [PUB_KEY, pad(16)] - exec.rpo_falcon512::authenticate_transaction + exec.falcon512_rpo::authenticate_transaction # => [pad(16)] end diff --git a/crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm b/crates/miden-standards/asm/account_components/auth/falcon_512_rpo_acl.masm similarity index 80% rename from crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm rename to crates/miden-standards/asm/account_components/auth/falcon_512_rpo_acl.masm index 72fd4916a0..80081ab6c6 100644 --- a/crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm +++ b/crates/miden-standards/asm/account_components/auth/falcon_512_rpo_acl.masm @@ -1,11 +1,11 @@ -# The MASM code of the RPO Falcon 512 authentication Account Component with ACL. +# The MASM code of the Falcon 512 RPO authentication Account Component with ACL. # -# See the `AuthRpoFalcon512Acl` Rust type's documentation for more details. +# See the `AuthFalcon512RpoAcl` Rust type's documentation for more details. -use miden::active_account -use miden::native_account -use miden::tx -use std::word +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::word type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } @@ -13,13 +13,13 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # ================================================================================================ # The slot in this component's storage layout where the public key is stored. -const PUBLIC_KEY_SLOT = 0 +const PUBLIC_KEY_SLOT = word("miden::standards::auth::falcon512_rpo_acl::public_key") # The slot where the authentication configuration is stored. -const AUTH_CONFIG_SLOT = 1 +const AUTH_CONFIG_SLOT = word("miden::standards::auth::falcon512_rpo_acl::config") # The slot where the map of auth trigger procedure roots is stored. -const AUTH_TRIGGER_PROCS_MAP_SLOT = 2 +const AUTH_TRIGGER_PROCS_MAP_SLOT = word("miden::standards::auth::falcon512_rpo_acl::trigger_procedure_roots") #! Authenticate a transaction using the Falcon signature scheme based on procedure calls and note usage. #! @@ -28,19 +28,20 @@ const AUTH_TRIGGER_PROCS_MAP_SLOT = 2 #! 2. If input notes were consumed and allow_unauthorized_input_notes is false #! 3. If output notes were created and allow_unauthorized_output_notes is false #! -#! If any of these conditions are true, standard RpoFalcon512 signature verification is performed. +#! If any of these conditions are true, standard Falcon512Rpo signature verification is performed. #! Otherwise, only the nonce is incremented. #! #! Inputs: [AUTH_ARGS, pad(12)] #! Outputs: [pad(16)] #! #! Invocation: call -pub proc auth_tx_rpo_falcon512_acl.2(auth_args: BeWord) +@locals(2) +pub proc auth_tx_falcon512_rpo_acl(auth_args: BeWord) dropw # => [pad(16)] # Get the authentication configuration - push.AUTH_CONFIG_SLOT exec.active_account::get_item + push.AUTH_CONFIG_SLOT[0..2] exec.active_account::get_item # => [0, allow_unauthorized_input_notes, allow_unauthorized_output_notes, num_auth_trigger_procs, pad(16)] drop @@ -62,8 +63,8 @@ pub proc auth_tx_rpo_falcon512_acl.2(auth_args: BeWord) # => [require_acl_auth, i, pad(16)] # Get the procedure root from storage - dup.1 sub.1 push.0.0.0 push.AUTH_TRIGGER_PROCS_MAP_SLOT - # => [AUTH_TRIGGER_PROCS_MAP_SLOT, [0, 0, 0, i-1], require_acl_auth, i, pad(16)] + dup.1 sub.1 push.0.0.0 push.AUTH_TRIGGER_PROCS_MAP_SLOT[0..2] + # => [trigger_proc_slot_prefix, trigger_proc_slot_suffix, [0, 0, 0, i-1], require_acl_auth, i, pad(16)] exec.active_account::get_map_item # => [AUTH_TRIGGER_PROC_ROOT, require_acl_auth, i, pad(16)] @@ -124,10 +125,10 @@ pub proc auth_tx_rpo_falcon512_acl.2(auth_args: BeWord) # If authentication is required, perform signature verification if.true # Fetch public key from storage. - push.PUBLIC_KEY_SLOT exec.active_account::get_item + push.PUBLIC_KEY_SLOT[0..2] exec.active_account::get_item # => [PUB_KEY, pad(16)] - exec.::miden::auth::rpo_falcon512::authenticate_transaction + exec.::miden::standards::auth::falcon512_rpo::authenticate_transaction else # ------ Check if initial account commitment differs from current commitment ------ diff --git a/crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm b/crates/miden-standards/asm/account_components/auth/falcon_512_rpo_multisig.masm similarity index 86% rename from crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm rename to crates/miden-standards/asm/account_components/auth/falcon_512_rpo_multisig.masm index 92a0cf0a21..e3f3e2d6e7 100644 --- a/crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm +++ b/crates/miden-standards/asm/account_components/auth/falcon_512_rpo_multisig.masm @@ -1,10 +1,10 @@ -# The MASM code of the Multi-Signature RPO Falcon 512 Authentication Component. +# The MASM code of the Multi-Signature Falcon 512 RPO Authentication Component. # -# See the `AuthRpoFalcon512Multisig` Rust type's documentation for more details. +# See the `AuthFalcon512RpoMultisig` Rust type's documentation for more details. -use miden::active_account -use miden::native_account -use miden::auth +use miden::protocol::active_account +use miden::protocol::native_account +use miden::standards::auth type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } @@ -29,19 +29,19 @@ const AUTH_UNAUTHORIZED_EVENT = event("miden::auth::unauthorized") # number of approvers are stored as: # [default_threshold, num_approvers, 0, 0]. # The threshold is guaranteed to be less than or equal to num_approvers. -const THRESHOLD_CONFIG_SLOT = 0 +const THRESHOLD_CONFIG_SLOT = word("miden::standards::auth::falcon512_rpo_multisig::threshold_config") # The slot in this component's storage layout where the public keys map is stored. # Map entries: [key_index, 0, 0, 0] => APPROVER_PUBLIC_KEY -const PUBLIC_KEYS_MAP_SLOT = 1 +const APPROVER_PUBLIC_KEYS_SLOT = word("miden::standards::auth::falcon512_rpo_multisig::approver_public_keys") # The slot in this component's storage layout where executed transactions are stored. # Map entries: transaction_message => [is_executed, 0, 0, 0] -const EXECUTED_TXS_SLOT = 2 +const EXECUTED_TXS_SLOT = word("miden::standards::auth::falcon512_rpo_multisig::executed_transactions") # The slot in this component's storage layout where procedure thresholds are stored. # Map entries: PROC_ROOT => [proc_threshold, 0, 0, 0] -const.PROC_THRESHOLD_ROOTS_SLOT=3 +const PROC_THRESHOLD_ROOTS_SLOT = word("miden::standards::auth::falcon512_rpo_multisig::procedure_thresholds") # Executed Transaction Flag Constant const IS_EXECUTED_FLAG = [1, 0, 0, 0] @@ -69,14 +69,14 @@ proc assert_new_tx(msg: BeWord) swapw # => [MSG, IS_EXECUTED_FLAG] - push.EXECUTED_TXS_SLOT - # => [index, MSG, IS_EXECUTED_FLAG] + push.EXECUTED_TXS_SLOT[0..2] + # => [txs_slot_prefix, txs_slot_suffix, MSG, IS_EXECUTED_FLAG] # Set the key value pair in the map to mark transaction as executed exec.native_account::set_map_item - # => [OLD_MAP_ROOT, [0, 0, 0, is_executed]] + # => [[0, 0, 0, is_executed]] - dropw drop drop drop + drop drop drop # => [is_executed] assertz.err=ERR_TX_ALREADY_EXECUTED @@ -115,13 +115,13 @@ proc cleanup_pubkey_mapping(init_num_of_approvers: u32, new_num_of_approvers: u3 padw swapw # => [[0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers] - push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, [0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers] + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_prefix, pub_key_slot_suffix, [0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers] exec.native_account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, new_num_of_approvers] + # => [OLD_VALUE, i-1, new_num_of_approvers] - dropw dropw + dropw # => [i-1, new_num_of_approvers] dup.1 dup.1 @@ -151,7 +151,8 @@ end #! Locals: #! 0: new_num_of_approvers #! 1: init_num_of_approvers -pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) +@locals(2) +pub proc update_signers_and_threshold(multisig_config_hash: BeWord) adv.push_mapval # => [MULTISIG_CONFIG_HASH, pad(12)] @@ -178,8 +179,8 @@ pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG # => [MULTISIG_CONFIG, pad(12)] - push.THRESHOLD_CONFIG_SLOT - # => [slot, MULTISIG_CONFIG, pad(12)] + push.THRESHOLD_CONFIG_SLOT[0..2] + # => [config_slot_prefix, config_slot_suffix, MULTISIG_CONFIG, pad(12)] exec.native_account::set_item # => [OLD_THRESHOLD_CONFIG, pad(12)] @@ -205,13 +206,13 @@ pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) swapw # => [[0, 0, 0, i-1], PUB_KEY, i-1, pad(12)] - push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, [0, 0, 0, i-1], PUB_KEY, i-1, pad(12)] + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_prefix, pub_key_slot_suffix, [0, 0, 0, i-1], PUB_KEY, i-1, pad(12)] exec.native_account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, pad(12)] + # => [OLD_VALUE, i-1, pad(12)] - dropw dropw + dropw # => [i-1, pad(12)] dup neq.0 @@ -238,7 +239,8 @@ end # #! Inputs: [default_threshold] #! Outputs: [transaction_threshold] -proc compute_transaction_threshold.1(default_threshold: u32) -> u32 +@locals(1) +proc compute_transaction_threshold(default_threshold: u32) -> u32 # 1. initialize transaction_threshold = 0 # 2. iterate through all account procedures # a. check if the procedure was called during the transaction @@ -277,8 +279,8 @@ proc compute_transaction_threshold.1(default_threshold: u32) -> u32 if.true # => [PROC_ROOT, num_procedures-1, transaction_threshold] - push.PROC_THRESHOLD_ROOTS_SLOT - # => [PROC_THRESHOLD_ROOTS_SLOT, PROC_ROOT, num_procedures-1, transaction_threshold] + push.PROC_THRESHOLD_ROOTS_SLOT[0..2] + # => [proc_roots_slot_prefix, proc_roots_slot_suffix, PROC_ROOT, num_procedures-1, transaction_threshold] # 2b. get the override proc_threshold of that procedure # if the procedure has no override threshold, the returned map item will be [0, 0, 0, 0] @@ -353,7 +355,8 @@ end #! - the same transaction has already been executed (replay protection). #! #! Invocation: call -pub proc auth_tx_rpo_falcon512_multisig.1(salt: BeWord) +@locals(1) +pub proc auth_tx_falcon512_rpo_multisig(salt: BeWord) exec.native_account::incr_nonce drop # => [SALT] @@ -372,8 +375,8 @@ pub proc auth_tx_rpo_falcon512_multisig.1(salt: BeWord) # ------ Verifying approver signatures ------ - push.THRESHOLD_CONFIG_SLOT - # => [index, TX_SUMMARY_COMMITMENT] + push.THRESHOLD_CONFIG_SLOT[0..2] + # => [config_slot_prefix, config_slot_suffix, TX_SUMMARY_COMMITMENT] exec.active_account::get_initial_item # => [0, 0, num_of_approvers, default_threshold, TX_SUMMARY_COMMITMENT] @@ -384,10 +387,10 @@ pub proc auth_tx_rpo_falcon512_multisig.1(salt: BeWord) swap movdn.5 # => [num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_prefix, pub_key_slot_suffix, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - exec.::miden::auth::rpo_falcon512::verify_signatures + exec.::miden::standards::auth::falcon512_rpo::verify_signatures # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, default_threshold] # ------ Checking threshold is >= num_verified_signatures ------ diff --git a/crates/miden-lib/asm/account_components/no_auth.masm b/crates/miden-standards/asm/account_components/auth/no_auth.masm similarity index 93% rename from crates/miden-lib/asm/account_components/no_auth.masm rename to crates/miden-standards/asm/account_components/auth/no_auth.masm index 7726b6c183..4ced08325f 100644 --- a/crates/miden-lib/asm/account_components/no_auth.masm +++ b/crates/miden-standards/asm/account_components/auth/no_auth.masm @@ -1,6 +1,6 @@ -use miden::active_account -use miden::native_account -use std::word +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::word #! Increment the nonce only if the account commitment has changed #! diff --git a/crates/miden-lib/asm/account_components/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm similarity index 53% rename from crates/miden-lib/asm/account_components/basic_fungible_faucet.masm rename to crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index f436d1b541..5d3b13a920 100644 --- a/crates/miden-lib/asm/account_components/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -2,5 +2,5 @@ # # See the `BasicFungibleFaucet` Rust type's documentation for more details. -pub proc ::miden::contracts::faucets::basic_fungible::distribute -pub proc ::miden::contracts::faucets::basic_fungible::burn +pub use ::miden::standards::faucets::basic_fungible::distribute +pub use ::miden::standards::faucets::basic_fungible::burn diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm new file mode 100644 index 0000000000..7d350a4224 --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -0,0 +1,8 @@ +# The MASM code of the Network Fungible Faucet Account Component. +# +# See the `NetworkFungibleFaucet` Rust type's documentation for more details. + +pub use ::miden::standards::faucets::network_fungible::distribute +pub use ::miden::standards::faucets::network_fungible::burn +pub use ::miden::standards::faucets::network_fungible::transfer_ownership +pub use ::miden::standards::faucets::network_fungible::renounce_ownership diff --git a/crates/miden-standards/asm/account_components/metadata/schema_commitment.masm b/crates/miden-standards/asm/account_components/metadata/schema_commitment.masm new file mode 100644 index 0000000000..cc9b85230a --- /dev/null +++ b/crates/miden-standards/asm/account_components/metadata/schema_commitment.masm @@ -0,0 +1,5 @@ +# The MASM code of the Storage Commitment metadata Component. +# +# See the `AccountSchemaCommitment` Rust type's documentation for more details. + +pub use ::miden::standards::metadata::storage_schema::get_schema_commitment diff --git a/crates/miden-lib/asm/account_components/basic_wallet.masm b/crates/miden-standards/asm/account_components/wallets/basic_wallet.masm similarity index 50% rename from crates/miden-lib/asm/account_components/basic_wallet.masm rename to crates/miden-standards/asm/account_components/wallets/basic_wallet.masm index 36fbc7d7b9..e6b613900e 100644 --- a/crates/miden-lib/asm/account_components/basic_wallet.masm +++ b/crates/miden-standards/asm/account_components/wallets/basic_wallet.masm @@ -2,5 +2,5 @@ # # See the `BasicWallet` Rust type's documentation for more details. -pub proc ::miden::contracts::wallets::basic::receive_asset -pub proc ::miden::contracts::wallets::basic::move_asset_to_note +pub use ::miden::standards::wallets::basic::receive_asset +pub use ::miden::standards::wallets::basic::move_asset_to_note diff --git a/crates/miden-standards/asm/note_scripts/BURN.masm b/crates/miden-standards/asm/note_scripts/BURN.masm new file mode 100644 index 0000000000..07426b1bc5 --- /dev/null +++ b/crates/miden-standards/asm/note_scripts/BURN.masm @@ -0,0 +1,5 @@ +use miden::standards::notes::burn + +begin + exec.burn::main +end diff --git a/crates/miden-standards/asm/note_scripts/MINT.masm b/crates/miden-standards/asm/note_scripts/MINT.masm new file mode 100644 index 0000000000..272fc4ab9a --- /dev/null +++ b/crates/miden-standards/asm/note_scripts/MINT.masm @@ -0,0 +1,5 @@ +use miden::standards::notes::mint + +begin + exec.mint::main +end diff --git a/crates/miden-standards/asm/note_scripts/P2ID.masm b/crates/miden-standards/asm/note_scripts/P2ID.masm new file mode 100644 index 0000000000..d3050ea305 --- /dev/null +++ b/crates/miden-standards/asm/note_scripts/P2ID.masm @@ -0,0 +1,5 @@ +use miden::standards::notes::p2id + +begin + exec.p2id::main +end diff --git a/crates/miden-standards/asm/note_scripts/P2IDE.masm b/crates/miden-standards/asm/note_scripts/P2IDE.masm new file mode 100644 index 0000000000..6d65869eef --- /dev/null +++ b/crates/miden-standards/asm/note_scripts/P2IDE.masm @@ -0,0 +1,5 @@ +use miden::standards::notes::p2ide + +begin + exec.p2ide::main +end diff --git a/crates/miden-standards/asm/note_scripts/SWAP.masm b/crates/miden-standards/asm/note_scripts/SWAP.masm new file mode 100644 index 0000000000..7d4f95b31b --- /dev/null +++ b/crates/miden-standards/asm/note_scripts/SWAP.masm @@ -0,0 +1,5 @@ +use miden::standards::notes::swap + +begin + exec.swap::main +end diff --git a/crates/miden-standards/asm/standards/access/ownable.masm b/crates/miden-standards/asm/standards/access/ownable.masm new file mode 100644 index 0000000000..79702c1945 --- /dev/null +++ b/crates/miden-standards/asm/standards/access/ownable.masm @@ -0,0 +1,175 @@ +# miden::standards::access::ownable +# +# Provides ownership management functionality for account components. +# This template can be imported and used by any component that needs owner controls. + +use miden::protocol::active_account +use miden::protocol::account_id +use miden::protocol::active_note +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================ + +# The slot in this component's storage layout where the owner config is stored. +const OWNER_CONFIG_SLOT = word("miden::standards::access::ownable::owner_config") + +# ZERO_ADDRESS word (all zeros) used to represent no owner +# Format: [prefix=0, suffix=0, 0, 0] as stored in account storage +const ZERO_ADDRESS = [0, 0, 0, 0] + +# ERRORS +# ================================================================================================ + +const ERR_SENDER_NOT_OWNER = "note sender is not the owner" + +# INTERNAL PROCEDURES +# ================================================================================================ + +#! Returns the owner AccountId from storage. +#! +#! Inputs: [] +#! Outputs: [owner_prefix, owner_suffix] +#! +#! Where: +#! - owner_{prefix, suffix} are the prefix and suffix felts of the owner AccountId. +proc owner + push.OWNER_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [owner_prefix, owner_suffix, 0, 0] + + # Storage format in memory: [0, 0, suffix, prefix] (word[0], word[1], word[2], word[3]) + # mem_loadw_be loads big-endian (reversed), so stack gets: [prefix, suffix, 0, 0] + # Stack: [owner_prefix (pos 0), owner_suffix (pos 1), 0 (pos 2), 0 (pos 3)] + # We want: [owner_prefix, owner_suffix] + # Move zeros to top using movup, then drop them + movup.2 + # => [0, owner_prefix, owner_suffix, 0] (moves element at pos 2 to pos 0) + + movup.3 + # => [0, 0, owner_prefix, owner_suffix] (moves element at pos 3 to pos 0) + + drop drop + # => [owner_prefix, owner_suffix] +end + +#! Checks if the given account ID is the owner of this component. +#! +#! Inputs: [account_id_prefix, account_id_suffix] +#! Outputs: [is_owner] +#! +#! Where: +#! - account_id_{prefix, suffix} are the prefix and suffix felts of the AccountId to check. +#! - is_owner is 1 if the account is the owner, 0 otherwise. +proc is_owner + + exec.owner + # => [owner_prefix, owner_suffix, account_id_prefix, account_id_suffix] + + exec.account_id::is_equal + # => [is_owner] + +end + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Checks if the note sender is the owner and panics if not. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the note sender is not the owner. +pub proc verify_owner + exec.active_note::get_sender + # => [sender_prefix, sender_suffix] + + exec.is_owner + # => [is_owner] + + assert.err=ERR_SENDER_NOT_OWNER + # => [] +end + +#! Returns the owner AccountId. +#! +#! Inputs: [pad(16)] +#! Outputs: [owner_prefix, owner_suffix, pad(14)] +#! +#! Where: +#! - owner_{prefix, suffix} are the prefix and suffix felts of the owner AccountId. +#! +#! Invocation: call +pub proc get_owner + exec.owner + # => [owner_prefix, owner_suffix, pad(14)] +end + +#! Transfers ownership to a new account. +#! +#! Can only be called by the current owner. +#! +#! Inputs: [new_owner_prefix, new_owner_suffix, pad(14)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - new_owner_{prefix, suffix} are the prefix and suffix felts of the new owner AccountId. +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: call +pub proc transfer_ownership + # Check that the caller is the owner + exec.verify_owner + # => [new_owner_prefix, new_owner_suffix, pad(14)] + + push.0 movdn.2 push.0 movdn.2 + # => [new_owner_prefix, new_owner_suffix, 0, 0, pad(14)] + + push.OWNER_CONFIG_SLOT[0..2] + # => [slot_prefix, slot_suffix, new_owner_prefix, new_owner_suffix, 0, 0, pad(14)] + + exec.native_account::set_item + # => [OLD_OWNER_WORD, pad(14)] + + # When the stack has 16 elements, dropw will shift in zeros from the right, + # resulting in [pad(16)]. So dropw is sufficient here. + dropw + # => [pad(16)] +end + +#! Renounces ownership, leaving the component without an owner. +#! +#! Can only be called by the current owner. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: call +#! +#! Important Note! +#! This feature allows the owner to relinquish administrative privileges, a common pattern +#! after an initial stage with centralized administration is over. Once ownership is renounced, +#! the component becomes permanently ownerless and cannot be managed by any account. +pub proc renounce_ownership + exec.verify_owner + # => [pad(16)] + + # ---- Push ZERO_ADDRESS to storage ---- + push.ZERO_ADDRESS + # => [0, 0, 0, 0, pad(16)] + + push.OWNER_CONFIG_SLOT[0..2] + # => [slot_prefix, slot_suffix, 0, 0, 0, 0, pad(16)] + + exec.native_account::set_item + # => [OLD_OWNER_WORD, pad(16)] + + dropw + # => [pad(16)] +end + diff --git a/crates/miden-lib/asm/miden/auth/ecdsa_k256_keccak.masm b/crates/miden-standards/asm/standards/auth/ecdsa_k256_keccak.masm similarity index 86% rename from crates/miden-lib/asm/miden/auth/ecdsa_k256_keccak.masm rename to crates/miden-standards/asm/standards/auth/ecdsa_k256_keccak.masm index c2b04142df..a0be086829 100644 --- a/crates/miden-lib/asm/miden/auth/ecdsa_k256_keccak.masm +++ b/crates/miden-standards/asm/standards/auth/ecdsa_k256_keccak.masm @@ -1,23 +1,22 @@ -use.miden::active_account -use.miden::native_account -use.miden::auth -use.miden::tx -use.std::crypto::dsa::ecdsa::secp256k1 +use miden::core::crypto::dsa::ecdsa_k256_keccak +use miden::core::crypto::hashes::rpo256 +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::standards::auth # CONSTANTS # ================================================================================================= # The event to request an authentication signature. -const.AUTH_REQUEST_EVENT=event("miden::auth::request") - -# The slot in this component's storage layout where the public key is stored. -const.PUBLIC_KEY_SLOT=0 +const AUTH_REQUEST_EVENT=event("miden::auth::request") # Local Memory Addresses for multisig operations -const.NUM_OF_APPROVERS_LOC=0 -const.PUB_KEY_MAP_IDX_LOC=4 -const.CURRENT_PK_LOC=8 -const.SUCCESSFUL_VERIFICATIONS_LOC=12 +const NUM_OF_APPROVERS_LOC=0 +const PUB_KEY_SLOT_SUFFIX_LOC=4 +const PUB_KEY_SLOT_PREFIX_LOC=5 +const CURRENT_PK_LOC=8 +const SUCCESSFUL_VERIFICATIONS_LOC=12 #! Authenticate a transaction using the ECDSA signature scheme. #! @@ -34,7 +33,7 @@ const.SUCCESSFUL_VERIFICATIONS_LOC=12 #! Outputs: [] #! #! Invocation: exec -export.authenticate_transaction +pub proc authenticate_transaction # Increment the account's nonce. # --------------------------------------------------------------------------------------------- # This has to happen before computing the delta commitment, otherwise that procedure will abort @@ -65,7 +64,7 @@ export.authenticate_transaction # Verify the signature against the public key and the message. The procedure gets as inputs the # hash of the public key and the message via the operand stack. The signature is provided via # the advice stack. The signature is valid if and only if the procedure returns. - exec.secp256k1::verify_ecdsa_k256_keccak + exec.ecdsa_k256_keccak::verify # OS => [] # AS => [] end @@ -81,10 +80,12 @@ end #! the owner public key mapping - the previous signers must authorize the change to the new signers, #! not the new signers authorizing themselves. #! -#! Inputs: [pub_key_slot_idx, num_of_approvers, MSG] +#! Inputs: [pub_key_slot_prefix, pub_key_slot_suffix, num_of_approvers, MSG] #! Outputs: [num_verified_signatures, MSG] -export.verify_signatures.16 - loc_store.PUB_KEY_MAP_IDX_LOC +@locals(16) +pub proc verify_signatures + loc_store.PUB_KEY_SLOT_PREFIX_LOC + loc_store.PUB_KEY_SLOT_SUFFIX_LOC # => [num_of_approvers, MSG] # Initializing SUCCESSFUL_VERIFICATIONS local memory address to 0 @@ -104,8 +105,9 @@ export.verify_signatures.16 # Fetch public key from storage map. # ----------------------------------------------------------------------------------------- - sub.1 dup push.0.0.0 loc_load.PUB_KEY_MAP_IDX_LOC - # => [owner_key_slot, [0, 0, 0, i-1], i-1, MSG] + sub.1 dup push.0.0.0 + loc_load.PUB_KEY_SLOT_SUFFIX_LOC loc_load.PUB_KEY_SLOT_PREFIX_LOC + # => [owner_key_slot_prefix, owner_key_slot_suffix, [0, 0, 0, i-1], i-1, MSG] # Get public key from initial storage state exec.active_account::get_initial_map_item @@ -120,7 +122,7 @@ export.verify_signatures.16 movup.4 movdn.8 swapw dupw movdnw.2 # => [MSG, OWNER_PUB_KEY, MSG, i-1] - hmerge + exec.rpo256::merge # => [SIG_KEY, MSG, i-1] adv.has_mapkey @@ -155,7 +157,7 @@ export.verify_signatures.16 # OS => [PUB_KEY, MSG, MSG, i-1] # AS => [SIGNATURE] - exec.secp256k1::verify_ecdsa_k256_keccak + exec.ecdsa_k256_keccak::verify # => [MSG, i-1] loc_load.SUCCESSFUL_VERIFICATIONS_LOC diff --git a/crates/miden-lib/asm/miden/auth/rpo_falcon512.masm b/crates/miden-standards/asm/standards/auth/falcon512_rpo.masm similarity index 86% rename from crates/miden-lib/asm/miden/auth/rpo_falcon512.masm rename to crates/miden-standards/asm/standards/auth/falcon512_rpo.masm index 81da11a100..7c9ccbf757 100644 --- a/crates/miden-lib/asm/miden/auth/rpo_falcon512.masm +++ b/crates/miden-standards/asm/standards/auth/falcon512_rpo.masm @@ -1,23 +1,22 @@ -use.miden::active_account -use.miden::native_account -use.miden::auth -use.miden::tx -use.std::crypto::dsa::rpo_falcon512 +use miden::core::crypto::dsa::falcon512rpo +use miden::core::crypto::hashes::rpo256 +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::standards::auth # CONSTANTS # ================================================================================================= # The event to request an authentication signature. -const.AUTH_REQUEST_EVENT=event("miden::auth::request") - -# The slot in this component's storage layout where the public key is stored. -const.PUBLIC_KEY_SLOT=0 +const AUTH_REQUEST_EVENT=event("miden::auth::request") # Local Memory Addresses for multisig operations -const.NUM_OF_APPROVERS_LOC=0 -const.PUB_KEY_MAP_IDX_LOC=4 -const.CURRENT_PK_LOC=8 -const.SUCCESSFUL_VERIFICATIONS_LOC=12 +const NUM_OF_APPROVERS_LOC=0 +const PUB_KEY_SLOT_SUFFIX_LOC=4 +const PUB_KEY_SLOT_PREFIX_LOC=5 +const CURRENT_PK_LOC=8 +const SUCCESSFUL_VERIFICATIONS_LOC=12 #! Authenticate a transaction using the Falcon signature scheme. #! @@ -34,7 +33,7 @@ const.SUCCESSFUL_VERIFICATIONS_LOC=12 #! Outputs: [] #! #! Invocation: exec -export.authenticate_transaction +pub proc authenticate_transaction # Increment the account's nonce. # --------------------------------------------------------------------------------------------- # This has to happen before computing the delta commitment, otherwise that procedure will abort @@ -65,7 +64,7 @@ export.authenticate_transaction # Verify the signature against the public key and the message. The procedure gets as inputs the # hash of the public key and the message via the operand stack. The signature is provided via # the advice stack. The signature is valid if and only if the procedure returns. - exec.rpo_falcon512::verify + exec.falcon512rpo::verify # OS => [] # AS => [] end @@ -81,10 +80,12 @@ end #! the owner public key mapping - the previous signers must authorize the change to the new signers, #! not the new signers authorizing themselves. #! -#! Inputs: [pub_key_slot_idx, num_of_approvers, MSG] +#! Inputs: [pub_key_slot_prefix, pub_key_slot_suffix, num_of_approvers, MSG] #! Outputs: [num_verified_signatures, MSG] -export.verify_signatures.16 - loc_store.PUB_KEY_MAP_IDX_LOC +@locals(16) +pub proc verify_signatures + loc_store.PUB_KEY_SLOT_PREFIX_LOC + loc_store.PUB_KEY_SLOT_SUFFIX_LOC # => [num_of_approvers, MSG] # Initializing SUCCESSFUL_VERIFICATIONS local memory address to 0 @@ -104,8 +105,9 @@ export.verify_signatures.16 # Fetch public key from storage map. # ----------------------------------------------------------------------------------------- - sub.1 dup push.0.0.0 loc_load.PUB_KEY_MAP_IDX_LOC - # => [owner_key_slot, [0, 0, 0, i-1], i-1, MSG] + sub.1 dup push.0.0.0 + loc_load.PUB_KEY_SLOT_SUFFIX_LOC loc_load.PUB_KEY_SLOT_PREFIX_LOC + # => [owner_key_slot_prefix, owner_key_slot_suffix, [0, 0, 0, i-1], i-1, MSG] # Get public key from initial storage state exec.active_account::get_initial_map_item @@ -120,7 +122,7 @@ export.verify_signatures.16 movup.4 movdn.8 swapw dupw movdnw.2 # => [MSG, OWNER_PUB_KEY, MSG, i-1] - hmerge + exec.rpo256::merge # => [SIG_KEY, MSG, i-1] adv.has_mapkey @@ -155,7 +157,7 @@ export.verify_signatures.16 # OS => [PUB_KEY, MSG, MSG, i-1] # AS => [SIGNATURE] - exec.rpo_falcon512::verify + exec.falcon512rpo::verify # => [MSG, i-1] loc_load.SUCCESSFUL_VERIFICATIONS_LOC diff --git a/crates/miden-lib/asm/miden/auth/mod.masm b/crates/miden-standards/asm/standards/auth/mod.masm similarity index 91% rename from crates/miden-lib/asm/miden/auth/mod.masm rename to crates/miden-standards/asm/standards/auth/mod.masm index a62b6141cc..ec213ee4b8 100644 --- a/crates/miden-lib/asm/miden/auth/mod.masm +++ b/crates/miden-standards/asm/standards/auth/mod.masm @@ -1,10 +1,11 @@ -use.miden::native_account -use.miden::tx -use.std::crypto::hashes::rpo +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::crypto::hashes::rpo256 #! Inputs: [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT] #! Outputs: [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT] -export.adv_insert_hqword.16 +@locals(16) +pub proc adv_insert_hqword loc_storew_be.0 movdnw.3 loc_storew_be.4 @@ -46,7 +47,7 @@ end #! - OUTPUT_NOTES_COMMITMENT is the commitment to the transaction's output notes. #! - INPUT_NOTES_COMMITMENT is the commitment to the transaction's inputs notes. #! - ACCOUNT_DELTA_COMMITMENT is the commitment to the transaction's account delta. -export.create_tx_summary +pub proc create_tx_summary exec.native_account::compute_delta_commitment # => [ACCOUNT_DELTA_COMMITMENT, SALT] @@ -70,7 +71,7 @@ end #! - OUTPUT_NOTES_COMMITMENT is the commitment to the transaction's output notes. #! - INPUT_NOTES_COMMITMENT is the commitment to the transaction's inputs notes. #! - ACCOUNT_DELTA_COMMITMENT is the commitment to the transaction's account delta. -export.hash_tx_summary +pub proc hash_tx_summary swapdw # => [INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT, SALT, OUTPUT_NOTES_COMMITMENT] @@ -78,7 +79,7 @@ export.hash_tx_summary padw movdnw.2 # => [INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT, CAPACITY, SALT, OUTPUT_NOTES_COMMITMENT] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM, SALT, OUTPUT_NOTES_COMMITMENT] # drop rate words @@ -88,9 +89,9 @@ export.hash_tx_summary movdnw.2 # => [SALT, OUTPUT_NOTES_COMMITMENT, PERM] - hperm + exec.rpo256::permute # => [RATE, RATE, PERM] - exec.rpo::squeeze_digest + exec.rpo256::squeeze_digest # => [TX_SUMMARY_COMMITMENT] end diff --git a/crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm b/crates/miden-standards/asm/standards/faucets/basic_fungible.masm similarity index 87% rename from crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm rename to crates/miden-standards/asm/standards/faucets/basic_fungible.masm index 130df4d663..11fbb4353b 100644 --- a/crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/basic_fungible.masm @@ -8,29 +8,26 @@ # - decimals are the decimals of the token. # - token_symbol as three chars encoded in a Felt. -use.miden::contracts::faucets +use miden::standards::faucets # CONSTANTS # ================================================================================================= -const.PRIVATE_NOTE=2 +const PRIVATE_NOTE=2 # ERRORS # ================================================================================================= -const.ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED="distribute would cause the maximum supply to be exceeded" +const ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED="distribute would cause the maximum supply to be exceeded" -const.ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" +const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" # CONSTANTS # ================================================================================================= -# The slot in this component's storage layout where the metadata is stored. -const.METADATA_SLOT=0 - #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! #! Inputs: [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] -#! Outputs: [pad(16)] +#! Outputs: [note_idx, pad(15)] #! #! Where: #! - amount is the amount to be minted and sent. @@ -40,15 +37,16 @@ const.METADATA_SLOT=0 #! - execution_hint is the execution hint of the note that holds the asset. #! - RECIPIENT is the recipient of the asset, i.e., #! hash(hash(hash(serial_num, [0; 4]), script_root), input_commitment). +#! - note_idx is the index of the created note. #! #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. #! - the total issuance after minting is greater than the maximum allowed supply. #! #! Invocation: call -export.distribute +pub proc distribute exec.faucets::distribute - # => [pad(16)] + # => [note_idx, pad(15)] end #! Burns the fungible asset from the active note. @@ -67,4 +65,4 @@ end #! - the transaction is executed against an account which is not a fungible asset faucet. #! - the transaction is executed against a faucet which is not the origin of the specified asset. #! - the amount about to be burned is greater than the outstanding supply of the asset. -export.faucets::burn +pub use faucets::burn diff --git a/crates/miden-lib/asm/miden/contracts/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm similarity index 70% rename from crates/miden-lib/asm/miden/contracts/faucets/mod.masm rename to crates/miden-standards/asm/standards/faucets/mod.masm index b16d3ad808..68a7d0652e 100644 --- a/crates/miden-lib/asm/miden/contracts/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -1,80 +1,81 @@ -use.miden::active_account -use.miden::active_note -use.miden::faucet -use.miden::output_note +use miden::protocol::active_account +use miden::protocol::active_note +use miden::protocol::faucet +use miden::protocol::output_note # CONSTANTS # ================================================================================================= -const.PRIVATE_NOTE=2 +const PRIVATE_NOTE=2 # ERRORS # ================================================================================================= -const.ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED="distribute would cause the maximum supply to be exceeded" +const ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED="distribute would cause the maximum supply to be exceeded" -const.ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" +const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" # CONSTANTS # ================================================================================================= -# The slot in this component's storage layout where the metadata is stored. -const.METADATA_SLOT=0 +# The standard slot where fungible faucet metadata like token symbol or decimals are stored. +const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! -#! Inputs: [amount, tag, aux, note_type, execution_hint, RECIPIENT] -#! Outputs: [] +#! Inputs: [amount, tag, note_type, RECIPIENT] +#! Outputs: [note_idx] #! #! Where: #! - amount is the amount to be minted and sent. #! - tag is the tag to be included in the note. -#! - aux is the auxiliary data to be included in the note. #! - note_type is the type of the note that holds the asset. -#! - execution_hint is the execution hint of the note that holds the asset. #! - RECIPIENT is the recipient of the asset, i.e., #! hash(hash(hash(serial_num, [0; 4]), script_root), input_commitment). +#! - note_idx is the index of the created note. #! #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. #! - the total issuance after minting is greater than the maximum allowed supply. #! #! Invocation: exec -export.distribute +pub proc distribute # get max supply of this faucet. We assume it is stored at pos 3 of slot 0 - push.METADATA_SLOT exec.active_account::get_item drop drop drop - # => [max_supply, amount, tag, aux, note_type, execution_hint, RECIPIENT] + push.METADATA_SLOT[0..2] exec.active_account::get_item drop drop drop + # => [max_supply, amount, tag, note_type, RECIPIENT] # get total issuance of this faucet so far and add amount to be minted exec.faucet::get_total_issuance - # => [total_issuance, max_supply, amount, tag, aux, note_type, execution_hint, RECIPIENT] + # => [total_issuance, max_supply, amount, tag, note_type, RECIPIENT] # compute maximum amount that can be minted, max_mint_amount = max_supply - total_issuance sub - # => [max_supply - total_issuance, amount, tag, aux, note_type, execution_hint, RECIPIENT] + # => [max_supply - total_issuance, amount, tag, note_type, RECIPIENT] # check that amount =< max_supply - total_issuance, fails if otherwise dup.1 gte assert.err=ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED - # => [amount, tag, aux, note_type, execution_hint, RECIPIENT] + # => [amount, tag, note_type, RECIPIENT] # creating the asset exec.faucet::create_fungible_asset - # => [ASSET, tag, aux, note_type, execution_hint, RECIPIENT] + # => [ASSET, tag, note_type, RECIPIENT] # mint the asset; this is needed to satisfy asset preservation logic. exec.faucet::mint - # => [ASSET, tag, aux, note_type, execution_hint, RECIPIENT] + # => [ASSET, tag, note_type, RECIPIENT] - # store and drop the ASSET - movdnw.2 - # => [tag, aux, note_type, execution_hint, RECIPIENT, ASSET] + movdn.9 movdn.9 movdn.9 movdn.9 + # => [tag, note_type, RECIPIENT, ASSET] # create a note exec.output_note::create # => [note_idx, ASSET] # load the ASSET and add it to the note - movdn.4 exec.output_note::add_asset - # => [] + dup movdn.5 movdn.5 + # => [ASSET, note_idx, note_idx] + + exec.output_note::add_asset + # => [note_idx] end #! Burns the fungible asset from the active note. @@ -96,7 +97,7 @@ end #! - the amount about to be burned is greater than the outstanding supply of the asset. #! #! Invocation: call -export.burn +pub proc burn # Get the assets from the note. This will fail if not called from a note context. push.0 exec.active_note::get_assets # => [num_assets, dest_ptr, pad(16)] diff --git a/crates/miden-lib/asm/miden/contracts/faucets/network_fungible.masm b/crates/miden-standards/asm/standards/faucets/network_fungible.masm similarity index 54% rename from crates/miden-lib/asm/miden/contracts/faucets/network_fungible.masm rename to crates/miden-standards/asm/standards/faucets/network_fungible.masm index a983169cfa..5f405db8fe 100644 --- a/crates/miden-lib/asm/miden/contracts/faucets/network_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/network_fungible.masm @@ -1,72 +1,79 @@ -use.miden::active_account -use.miden::account_id -use.miden::active_note -use.miden::contracts::faucets -use.miden::contracts::faucets::basic_fungible +use miden::protocol::active_note +use miden::standards::faucets +use miden::standards::access::ownable -# CONSTANTS +# PUBLIC INTERFACE # ================================================================================================ -# The slot in this component's storage layout where the owner config is stored. -const.OWNER_CONFIG_SLOT=1 +# OWNER MANAGEMENT +# ------------------------------------------------------------------------------------------------ -# ERRORS -const.ERR_ONLY_OWNER_CAN_MINT="note sender is not the owner of the faucet who can mint assets" - -#! Checks if the note sender is the owner of this faucet. +#! Returns the owner AccountId. #! #! Inputs: [] -#! Outputs: [is_owner] +#! Outputs: [owner_prefix, owner_suffix, pad(14)] #! -#! Where: -#! - is_owner is 1 if the sender is the owner, 0 otherwise. -proc.is_owner - push.OWNER_CONFIG_SLOT - # => [owner_config_slot] - - exec.active_account::get_item - # => [owner_prefix, owner_suffix, 0, 0] +#! Invocation: call +pub use ownable::get_owner - exec.active_note::get_sender - # => [sender_prefix, sender_suffix, owner_prefix, owner_suffix, 0, 0] +#! Transfers ownership to a new account. +#! +#! Can only be called by the current owner. +#! +#! Inputs: [new_owner_prefix, new_owner_suffix, pad(14)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - new_owner_{prefix, suffix} are the prefix and suffix felts of the new owner AccountId. +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: call +pub use ownable::transfer_ownership - exec.account_id::is_equal - # => [are_equal, 0, 0] +#! Renounces ownership, leaving the component without an owner. +#! +#! Can only be called by the current owner. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: call +pub use ownable::renounce_ownership - movdn.2 drop drop - # => [is_owner] -end +# ASSET DISTRIBUTION +# ------------------------------------------------------------------------------------------------ #! Distributes freshly minted fungible assets to the provided recipient. #! #! This procedure first checks if the note sender is the owner of the faucet, and then #! mints the asset and creates an output note with that asset for the recipient. #! -#! Inputs: [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] -#! Outputs: [pad(16)] +#! Inputs: [amount, tag, note_type, RECIPIENT, pad(9)] +#! Outputs: [note_idx, pad(15)] #! #! Where: #! - amount is the amount to be minted and sent. #! - tag is the tag to be included in the note. -#! - aux is the auxiliary data to be included in the note. #! - note_type is the type of the note that holds the asset. -#! - execution_hint is the execution hint of the note that holds the asset. #! - RECIPIENT is the recipient of the asset. +#! - note_idx is the index of the created note. #! #! Panics if: #! - the note sender is not the owner of this faucet. #! - any of the validations in faucets::distribute fail. #! #! Invocation: call -export.distribute - exec.is_owner - # => [is_owner, amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - assert.err=ERR_ONLY_OWNER_CAN_MINT +pub proc distribute + exec.ownable::verify_owner # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] exec.faucets::distribute - # => [pad(16)] + # => [note_idx, pad(15)] end #! Burns the fungible asset from the active note. @@ -85,4 +92,4 @@ end #! - the amount about to be burned is greater than the outstanding supply of the asset. #! #! Invocation: call -export.faucets::burn +pub use faucets::burn diff --git a/crates/miden-standards/asm/standards/metadata/storage_schema.masm b/crates/miden-standards/asm/standards/metadata/storage_schema.masm new file mode 100644 index 0000000000..fe61927c30 --- /dev/null +++ b/crates/miden-standards/asm/standards/metadata/storage_schema.masm @@ -0,0 +1,19 @@ +# This module defines the storage schema functionality for accounts. It exposes the storage +# slot at which an account stores a commitment to its storage schema, and provides a helper +# procedure to load that commitment from the active account's storage. + +use miden::protocol::active_account + +# CONSTANTS +# ================================================================================================= + +# The slot in this component's storage layout where the account storage schema commitment is stored +const SCHEMA_COMMITMENT_SLOT = word("miden::standards::metadata::storage_schema") + +pub proc get_schema_commitment + dropw + # => [pad(16)] + + push.SCHEMA_COMMITMENT_SLOT[0..2] exec.active_account::get_item + # => [SCHEMA_COMMITMENT, pad(16)] +end diff --git a/crates/miden-lib/asm/note_scripts/BURN.masm b/crates/miden-standards/asm/standards/notes/burn.masm similarity index 95% rename from crates/miden-lib/asm/note_scripts/BURN.masm rename to crates/miden-standards/asm/standards/notes/burn.masm index 42b68ed11b..20696cc7bc 100644 --- a/crates/miden-lib/asm/note_scripts/BURN.masm +++ b/crates/miden-standards/asm/standards/notes/burn.masm @@ -1,4 +1,4 @@ -use.miden::contracts::faucets +use miden::standards::faucets #! BURN script: burns the asset from the note by calling the faucet's burn procedure. #! This note can be executed against any faucet account that exposes the faucets::burn procedure @@ -18,7 +18,7 @@ use.miden::contracts::faucets #! Panics if: #! - account does not expose burn procedure. #! - any of the validations in the burn procedure fail. -begin +pub proc main dropw # => [pad(16)] diff --git a/crates/miden-standards/asm/standards/notes/mint.masm b/crates/miden-standards/asm/standards/notes/mint.masm new file mode 100644 index 0000000000..7934306e33 --- /dev/null +++ b/crates/miden-standards/asm/standards/notes/mint.masm @@ -0,0 +1,139 @@ +use miden::protocol::active_note +use miden::protocol::note +use miden::protocol::output_note +use miden::standards::faucets::network_fungible->network_faucet + +# CONSTANTS +# ================================================================================================= + +const MINT_NOTE_NUM_INPUTS_PRIVATE=12 +const MINT_NOTE_MIN_NUM_INPUTS_PUBLIC=16 + +const OUTPUT_NOTE_TYPE_PUBLIC=1 +const OUTPUT_NOTE_TYPE_PRIVATE=2 + +# Memory Addresses of MINT note inputs +# The attachment is at the same memory address for both private and public inputs. +const ATTACHMENT_KIND_ADDRESS=2 +const ATTACHMENT_SCHEME_ADDRESS=3 +const ATTACHMENT_ADDRESS=4 +const OUTPUT_PUBLIC_NOTE_INPUTS_ADDR=16 + +# ERRORS +# ================================================================================================= + +const ERR_MINT_WRONG_NUMBER_OF_INPUTS="MINT script expects exactly 12 inputs for private or 16+ inputs for public output notes" + +#! Network Faucet MINT script: mints assets by calling the network faucet's distribute function. +#! This note is intended to be executed against a network fungible faucet account. +#! +#! Requires that the account exposes: +#! - miden::standards::faucets::network_fungible::distribute procedure. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Note inputs support two modes. Depending on the number of note inputs, +#! a private or public note is created on consumption of the MINT note: +#! +#! Private mode (12 inputs) - creates a private note: +#! - tag: Note tag for the output note +#! - amount: The amount to mint +#! - attachment_scheme: The user-defined type of the attachment. +#! - attachment_kind: The attachment kind of the attachment. +#! - ATTACHMENT: The attachment to be set. +#! - RECIPIENT: The recipient digest (4 elements) +#! +#! Public mode (16+ inputs) - creates a public note with variable-length inputs: +#! - tag: Note tag for the output note +#! - amount: The amount to mint +#! - attachment_scheme: The user-defined type of the attachment. +#! - attachment_kind: The attachment kind of the attachment. +#! - ATTACHMENT: The attachment to be set. +#! - SCRIPT_ROOT: Script root of the output note (4 elements) +#! - SERIAL_NUM: Serial number of the output note (4 elements) +#! - [INPUTS]: Variable-length inputs for the output note (Vec) +#! The number of output note inputs = num_mint_note_inputs - 16 +#! +#! Panics if: +#! - account does not expose distribute procedure. +#! - the number of inputs is not exactly 12 for private or less than 16 for public output notes. +pub proc main + dropw + # => [pad(16)] + # Load note inputs into memory starting at address 0 + push.0 exec.active_note::get_inputs + # => [total_inputs, inputs_ptr, pad(16)] + + dup + # => [num_inputs, num_inputs, inputs_ptr, pad(16)] + + u32assert2.err=ERR_MINT_WRONG_NUMBER_OF_INPUTS + u32gte.MINT_NOTE_MIN_NUM_INPUTS_PUBLIC + # => [is_public_output_note, total_inputs, inputs_ptr, pad(16)] + + if.true + # public output note creation + # => [total_inputs, inputs_ptr, pad(16)] + + movdn.9 drop + # => [EMPTY_WORD, EMPTY_WORD, total_inputs, pad(8)] + + mem_loadw_be.8 + # => [SCRIPT_ROOT, EMPTY_WORD, total_inputs, pad(8)] + + swapw mem_loadw_be.12 + # => [SERIAL_NUM, SCRIPT_ROOT, total_inputs, pad(8)] + + # compute variable length note inputs for the output note + movup.8 sub.MINT_NOTE_MIN_NUM_INPUTS_PUBLIC + # => [num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT, pad(8)] + + push.OUTPUT_PUBLIC_NOTE_INPUTS_ADDR + # => [inputs_ptr, num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT, pad(8)] + + exec.note::build_recipient + # => [RECIPIENT, pad(12)] + + # push note_type, and load tag and amount + push.OUTPUT_NOTE_TYPE_PUBLIC + mem_load.0 mem_load.1 + # => [amount, tag, note_type, RECIPIENT, pad(12)] + else + # private output note creation + + eq.MINT_NOTE_NUM_INPUTS_PRIVATE assert.err=ERR_MINT_WRONG_NUMBER_OF_INPUTS drop + # => [inputs_ptr, pad(16)] + + drop + # => [pad(16)] + + mem_loadw_be.8 + # => [RECIPIENT, pad(12)] + + # push note_type, and load tag and amount + push.OUTPUT_NOTE_TYPE_PRIVATE + mem_load.0 mem_load.1 + # => [amount, tag, note_type, RECIPIENT, pad(12)] + end + # => [amount, tag, note_type, RECIPIENT, pad(12)] + + # distribute expects 9 pad elements, returns 15 and 12 are provided here. + # so the total number of pads after calling is 12 + (15-9) = 18 + call.network_faucet::distribute + # => [note_idx, pad(18))] + + padw mem_loadw_be.ATTACHMENT_ADDRESS + # => [ATTACHMENT, note_idx, pad(18))] + + mem_load.ATTACHMENT_KIND_ADDRESS + mem_load.ATTACHMENT_SCHEME_ADDRESS + movup.6 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(18))] + + exec.output_note::set_attachment + # => [pad(18))] + + drop drop + # => [pad(16)] +end diff --git a/crates/miden-lib/asm/note_scripts/P2ID.masm b/crates/miden-standards/asm/standards/notes/p2id.masm similarity index 76% rename from crates/miden-lib/asm/note_scripts/P2ID.masm rename to crates/miden-standards/asm/standards/notes/p2id.masm index f5b565d223..b315ad4e5a 100644 --- a/crates/miden-lib/asm/note_scripts/P2ID.masm +++ b/crates/miden-standards/asm/standards/notes/p2id.masm @@ -1,19 +1,20 @@ -use.miden::active_account -use.miden::account_id -use.miden::active_note +use miden::protocol::active_account +use miden::protocol::account_id +use miden::protocol::active_note +use miden::standards::wallets::basic->basic_wallet -# ERRORS +# ERRORS # ================================================================================================= -const.ERR_P2ID_WRONG_NUMBER_OF_INPUTS="P2ID note expects exactly 2 note inputs" +const ERR_P2ID_WRONG_NUMBER_OF_INPUTS="P2ID note expects exactly 2 note inputs" -const.ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transaction address do not match" +const ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transaction address do not match" #! Pay-to-ID script: adds all assets from the note to the account, assuming ID of the account #! matches target account ID specified by the note inputs. #! #! Requires that the account exposes: -#! - miden::contracts::wallets::basic::receive_asset procedure. +#! - miden::standards::wallets::basic::receive_asset procedure. #! #! Inputs: [] #! Outputs: [] @@ -22,12 +23,12 @@ const.ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transacti #! - target_account_id is the ID of the account for which the note is intended. #! #! Panics if: -#! - Account does not expose miden::contracts::wallets::basic::receive_asset procedure. +#! - Account does not expose miden::standards::wallets::basic::receive_asset procedure. #! - Account ID of executing account is not equal to the Account ID specified via note inputs. #! - The same non-fungible asset already exists in the account. #! - Adding a fungible asset would result in amount overflow, i.e., the total amount would be #! greater than 2^63. -begin +pub proc main # store the note inputs to memory starting at address 0 padw push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr, EMPTY_WORD] @@ -47,6 +48,6 @@ begin exec.account_id::is_equal assert.err=ERR_P2ID_TARGET_ACCT_MISMATCH # => [] - exec.active_note::add_assets_to_account + exec.basic_wallet::add_assets_to_account # => [] end diff --git a/crates/miden-lib/asm/note_scripts/P2IDE.masm b/crates/miden-standards/asm/standards/notes/p2ide.masm similarity index 87% rename from crates/miden-lib/asm/note_scripts/P2IDE.masm rename to crates/miden-standards/asm/standards/notes/p2ide.masm index 8e885cc02d..1345bc4acb 100644 --- a/crates/miden-lib/asm/note_scripts/P2IDE.masm +++ b/crates/miden-standards/asm/standards/notes/p2ide.masm @@ -1,20 +1,21 @@ -use.miden::active_account -use.miden::account_id -use.miden::active_note -use.miden::tx +use miden::protocol::active_account +use miden::protocol::account_id +use miden::protocol::active_note +use miden::protocol::tx +use miden::standards::wallets::basic->basic_wallet -# ERRORS +# ERRORS # ================================================================================================= -const.ERR_P2IDE_WRONG_NUMBER_OF_INPUTS="P2IDE note expects exactly 4 note inputs" +const ERR_P2IDE_WRONG_NUMBER_OF_INPUTS="P2IDE note expects exactly 4 note inputs" -const.ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER="failed to reclaim P2IDE note because the reclaiming account is not the sender" +const ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER="failed to reclaim P2IDE note because the reclaiming account is not the sender" -const.ERR_P2IDE_RECLAIM_HEIGHT_NOT_REACHED="failed to reclaim P2IDE note because the reclaim block height is not reached yet" +const ERR_P2IDE_RECLAIM_HEIGHT_NOT_REACHED="failed to reclaim P2IDE note because the reclaim block height is not reached yet" -const.ERR_P2IDE_RECLAIM_DISABLED="P2IDE reclaim is disabled" +const ERR_P2IDE_RECLAIM_DISABLED="P2IDE reclaim is disabled" -const.ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED="failed to consume P2IDE note because the note is still timelocked" +const ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED="failed to consume P2IDE note because the note is still timelocked" # HELPER PROCEDURES # ================================================================================================= @@ -23,7 +24,7 @@ const.ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED="failed to consume P2IDE note becaus #! #! Inputs: [current_block_height, timelock_block_height] #! Outputs: [current_block_height] -proc.verify_unlocked +proc verify_unlocked dup movdn.2 # => [current_block_height, timelock_block_height, current_block_height] @@ -43,7 +44,7 @@ end #! - the reclaim of the active note is disabled. #! - the reclaim block height is not reached yet. #! - the account attempting to reclaim the note is not the sender account. -proc.reclaim_note +proc reclaim_note # check that the reclaim of the active note is enabled movup.3 dup neq.0 assert.err=ERR_P2IDE_RECLAIM_DISABLED # => [reclaim_block_height, account_id_prefix, account_id_suffix, current_block_height] @@ -64,7 +65,7 @@ proc.reclaim_note # => [] # add note assets to account - exec.active_note::add_assets_to_account + exec.basic_wallet::add_assets_to_account # => [] end @@ -78,7 +79,7 @@ end #! the transaction's reference block number is greater than or equal to the note's reclaim block height. #! #! Requires that the account exposes: -#! - miden::contracts::wallets::basic::receive_asset procedure. +#! - miden::standards::wallets::basic::receive_asset procedure. #! #! Inputs: [] #! Outputs: [] @@ -89,7 +90,7 @@ end #! - timelock_block_height is the block height at which the note can be consumed by the target. #! #! Panics if: -#! - The account does not expose miden::contracts::wallets::basic::receive_asset procedure. +#! - The account does not expose miden::standards::wallets::basic::receive_asset procedure. #! - The note is consumed before the timelock expired, i.e. the transaction's reference block #! number is less than the timelock block height. #! - Before reclaim block height: the account ID of the executing account is not equal to the specified @@ -99,7 +100,7 @@ end #! - The same non-fungible asset already exists in the account. #! - Adding a fungible asset would result in an amount overflow, i.e., the total amount would be #! greater than 2^63. -begin +pub proc main # store the note inputs to memory starting at address 0 push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr] @@ -130,7 +131,7 @@ begin if.true # we can safely consume the note since the active account is the target of the note - dropw exec.active_note::add_assets_to_account + dropw exec.basic_wallet::add_assets_to_account # => [] else diff --git a/crates/miden-lib/asm/note_scripts/SWAP.masm b/crates/miden-standards/asm/standards/notes/swap.masm similarity index 59% rename from crates/miden-lib/asm/note_scripts/SWAP.masm rename to crates/miden-standards/asm/standards/notes/swap.masm index 479e328593..8c95668cd1 100644 --- a/crates/miden-lib/asm/note_scripts/SWAP.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -1,44 +1,55 @@ -use.miden::active_note -use.miden::output_note -use.miden::contracts::wallets::basic->wallet +use miden::protocol::active_note +use miden::protocol::output_note +use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= -const.SWAP_NOTE_INPUTS_NUMBER=12 +const SWAP_NOTE_INPUTS_NUMBER=16 + +const PAYBACK_NOTE_TYPE_ADDRESS=0 +const PAYBACK_NOTE_TAG_ADDRESS=1 +const ATTACHMENT_KIND_ADDRESS=2 +const ATTACHMENT_SCHEME_ADDRESS=3 +const ATTACHMENT_ADDRESS=4 +const REQUESTED_ASSET_ADDRESS=8 +const PAYBACK_RECIPIENT_ADDRESS=12 # ERRORS # ================================================================================================= -const.ERR_SWAP_WRONG_NUMBER_OF_INPUTS="SWAP script expects exactly 12 note inputs" +const ERR_SWAP_WRONG_NUMBER_OF_INPUTS="SWAP script expects exactly 16 note inputs" -const.ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset" +const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset" #! Swap script: adds an asset from the note into consumers account and #! creates a note consumable by note issuer containing requested ASSET. #! #! Requires that the account exposes: -#! - miden::contracts::wallets::basic::receive_asset procedure. -#! - miden::contracts::wallets::basic::move_asset_to_note procedure. +#! - miden::standards::wallets::basic::receive_asset procedure. +#! - miden::standards::wallets::basic::move_asset_to_note procedure. #! #! Inputs: [ARGS] #! Outputs: [] #! #! Note inputs are assumed to be as follows: -#! - REQUESTED_ASSET -#! - PAYBACK_NOTE_RECIPIENT -#! - payback_note_execution_hint #! - payback_note_type -#! - payback_note_aux #! - payback_note_tag +#! - attachment_kind +#! - attachment_scheme +#! - ATTACHMENT +#! - REQUESTED_ASSET +#! - PAYBACK_RECIPIENT #! #! Panics if: -#! - account does not expose miden::contracts::wallets::basic::receive_asset procedure. -#! - account does not expose miden::contracts::wallets::basic::move_asset_to_note procedure. +#! - account does not expose miden::standards::wallets::basic::receive_asset procedure. +#! - account does not expose miden::standards::wallets::basic::move_asset_to_note procedure. #! - account vault does not contain the requested asset. #! - adding a fungible asset would result in amount overflow, i.e., the total amount would be #! greater than 2^63. -begin +#! - the attachment kind or scheme does not fit into a u32. +#! - the attachment kind is an unknown variant. +pub proc main # dropping note args dropw # => [] @@ -49,21 +60,21 @@ begin push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr] - # make sure the number of inputs is 12 + # check number of inputs eq.SWAP_NOTE_INPUTS_NUMBER assert.err=ERR_SWAP_WRONG_NUMBER_OF_INPUTS - # => [inputs_ptr] + drop + # => [] - # load REQUESTED_ASSET - mem_loadw_be + mem_loadw_be.REQUESTED_ASSET_ADDRESS # => [REQUESTED_ASSET] - # load PAYBACK_NOTE_RECIPIENT - padw mem_loadw_be.4 + padw mem_loadw_be.PAYBACK_RECIPIENT_ADDRESS # => [PAYBACK_NOTE_RECIPIENT, REQUESTED_ASSET] # load payback P2ID details - padw mem_loadw_be.8 - # => [tag, aux, note_type, execution_hint, PAYBACK_NOTE_RECIPIENT, REQUESTED_ASSET] + mem_load.PAYBACK_NOTE_TYPE_ADDRESS + mem_load.PAYBACK_NOTE_TAG_ADDRESS + # => [tag, note_type, PAYBACK_NOTE_RECIPIENT, REQUESTED_ASSET] # create payback P2ID note exec.output_note::create @@ -83,7 +94,16 @@ begin call.wallet::move_asset_to_note # => [REQUESTED_ASSET, note_idx, pad(11)] - dropw drop push.0 + dropw + # => [note_idx, pad(11)] + + mem_loadw_be.ATTACHMENT_ADDRESS + mem_load.ATTACHMENT_KIND_ADDRESS + mem_load.ATTACHMENT_SCHEME_ADDRESS + movup.6 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + + exec.output_note::set_attachment # => [pad(12)] # --- move assets from the SWAP note into the account ------------------------- diff --git a/crates/miden-standards/asm/standards/wallets/basic.masm b/crates/miden-standards/asm/standards/wallets/basic.masm new file mode 100644 index 0000000000..837803c09c --- /dev/null +++ b/crates/miden-standards/asm/standards/wallets/basic.masm @@ -0,0 +1,120 @@ +use miden::protocol::native_account +use miden::protocol::output_note +use miden::protocol::active_note + +# CONSTANTS +# ================================================================================================= +const PUBLIC_NOTE=1 + +#! Adds the provided asset to the active account. +#! +#! Inputs: [ASSET, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ASSET is the asset to be received, can be fungible or non-fungible +#! +#! Panics if: +#! - the same non-fungible asset already exists in the account. +#! - adding a fungible asset would result in amount overflow, i.e., +#! the total amount would be greater than 2^63. +#! +#! Invocation: call +pub proc receive_asset + exec.native_account::add_asset + # => [ASSET', pad(12)] + + # drop the final asset + dropw + # => [pad(16)] +end + +#! Removes the specified asset from the account and adds it to the output note with the specified +#! index. +#! +#! This procedure is expected to be invoked using a `call` instruction. It makes no guarantees about +#! the contents of the `PAD` elements shown below. It is the caller's responsibility to make sure +#! these elements do not contain any meaningful data. +#! +#! Inputs: [ASSET, note_idx, pad(11)] +#! Outputs: [ASSET, note_idx, pad(11)] +#! +#! Where: +#! - note_idx is the index of the output note. +#! - ASSET is the fungible or non-fungible asset of interest. +#! +#! Panics if: +#! - the fungible asset is not found in the vault. +#! - the amount of the fungible asset in the vault is less than the amount to be removed. +#! - the non-fungible asset is not found in the vault. +#! +#! Invocation: call +pub proc move_asset_to_note + # remove the asset from the account + exec.native_account::remove_asset + # => [ASSET, note_idx, pad(11)] + + dupw dup.8 movdn.4 + # => [ASSET, note_idx, ASSET, note_idx, pad(11)] + + exec.output_note::add_asset + # => [ASSET, note_idx, pad(11)] +end + +#! Adds all assets from the active note to the native account's vault. +#! +#! Inputs: [] +#! Outputs: [] +@locals(1024) +pub proc add_assets_to_account + # write assets to local memory starting at offset 0 + # we have allocated 4 * MAX_ASSETS_PER_NOTE number of locals so all assets should fit + # since the asset memory will be overwritten, we don't have to initialize the locals to zero + locaddr.0 exec.active_note::get_assets + # => [num_of_assets, ptr = 0] + + # compute the pointer at which we should stop iterating + mul.4 dup.1 add + # => [end_ptr, ptr] + + # pad the stack and move the pointer to the top + padw movup.5 + # => [ptr, EMPTY_WORD, end_ptr] + + # loop if the amount of assets is non-zero + dup dup.6 neq + # => [should_loop, ptr, EMPTY_WORD, end_ptr] + + while.true + # => [ptr, EMPTY_WORD, end_ptr] + + # save the pointer so that we can use it later + dup movdn.5 + # => [ptr, EMPTY_WORD, ptr, end_ptr] + + # load the asset + mem_loadw_be + # => [ASSET, ptr, end_ptr] + + # pad the stack before call + padw swapw padw padw swapdw + # => [ASSET, pad(12), ptr, end_ptr] + + # add asset to the account + call.receive_asset + # => [pad(16), ptr, end_ptr] + + # clean the stack after call + dropw dropw dropw + # => [EMPTY_WORD, ptr, end_ptr] + + # increment the pointer and continue looping if ptr != end_ptr + movup.4 add.4 dup dup.6 neq + # => [should_loop, ptr+4, EMPTY_WORD, end_ptr] + end + # => [ptr', EMPTY_WORD, end_ptr] + + # clear the stack + drop dropw drop + # => [] +end diff --git a/crates/miden-standards/build.rs b/crates/miden-standards/build.rs new file mode 100644 index 0000000000..f819b74ae2 --- /dev/null +++ b/crates/miden-standards/build.rs @@ -0,0 +1,496 @@ +use std::env; +use std::path::Path; + +use fs_err as fs; +use miden_assembly::diagnostics::{IntoDiagnostic, NamedSource, Result, WrapErr}; +use miden_assembly::utils::Serializable; +use miden_assembly::{Assembler, Library, Report}; +use miden_protocol::transaction::TransactionKernel; + +// 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_STANDARDS_DIR: &str = "standards"; +const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts"; +const ASM_ACCOUNT_COMPONENTS_DIR: &str = "account_components"; + +const STANDARDS_LIB_NAMESPACE: &str = "miden::standards"; + +const STANDARDS_ERRORS_FILE: &str = "src/errors/standards.rs"; +const STANDARDS_ERRORS_ARRAY_NAME: &str = "STANDARDS_ERRORS"; + +// PRE-PROCESSING +// ================================================================================================ + +/// Read and parse the contents from `./asm`. +/// - Compiles the contents of asm/standards directory into a Miden library file (.masl) under +/// standards namespace. +/// - Compiles the contents of asm/note_scripts directory into individual .masb files. +/// - Compiles the contents of asm/account_components directory into individual .masl files. +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"); + + // 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); + + // set target directory to {OUT_DIR}/assets + let target_dir = Path::new(&build_dir).join(ASSETS_DIR); + + // compile standards library + let standards_lib = + compile_standards_lib(&source_dir, &target_dir, TransactionKernel::assembler())?; + + let mut assembler = TransactionKernel::assembler(); + assembler.link_static_library(standards_lib)?; + + // compile note scripts + compile_note_scripts( + &source_dir.join(ASM_NOTE_SCRIPTS_DIR), + &target_dir.join(ASM_NOTE_SCRIPTS_DIR), + assembler.clone(), + )?; + + // compile account components + compile_account_components( + &source_dir.join(ASM_ACCOUNT_COMPONENTS_DIR), + &target_dir.join(ASM_ACCOUNT_COMPONENTS_DIR), + assembler, + )?; + + generate_error_constants(&source_dir)?; + + Ok(()) +} + +// COMPILE PROTOCOL LIB +// ================================================================================================ + +/// Reads the MASM files from "{source_dir}/standards" directory, compiles them into a Miden +/// assembly library, saves the library into "{target_dir}/standards.masl", and returns the compiled +/// library. +fn compile_standards_lib( + source_dir: &Path, + target_dir: &Path, + assembler: Assembler, +) -> Result { + let source_dir = source_dir.join(ASM_STANDARDS_DIR); + + let standards_lib = assembler.assemble_library_from_dir(source_dir, STANDARDS_LIB_NAMESPACE)?; + + let output_file = target_dir.join("standards").with_extension(Library::LIBRARY_EXTENSION); + standards_lib.write_to_file(output_file).into_diagnostic()?; + + Ok(standards_lib) +} + +// COMPILE EXECUTABLE MODULES +// ================================================================================================ + +/// Reads all MASM files from the "{source_dir}", complies each file individually into a MASB +/// file, and stores the compiled files into the "{target_dir}". +/// +/// The source files are expected to contain executable programs. +fn compile_note_scripts(source_dir: &Path, target_dir: &Path, assembler: Assembler) -> Result<()> { + fs::create_dir_all(target_dir) + .into_diagnostic() + .wrap_err("failed to create note_scripts directory")?; + + for masm_file_path in shared::get_masm_files(source_dir).unwrap() { + // read the MASM file, parse it, and serialize the parsed AST to bytes + let code = assembler.clone().assemble_program(masm_file_path.clone())?; + + let bytes = code.to_bytes(); + + let masm_file_name = masm_file_path + .file_name() + .expect("file name should exist") + .to_str() + .ok_or_else(|| Report::msg("failed to convert file name to &str"))?; + let mut masb_file_path = target_dir.join(masm_file_name); + + // write the binary MASB to the output dir + masb_file_path.set_extension("masb"); + fs::write(masb_file_path, bytes).unwrap(); + } + Ok(()) +} + +// COMPILE ACCOUNT COMPONENTS +// ================================================================================================ + +/// Compiles the account components in `source_dir` into MASL libraries and stores the compiled +/// files in `target_dir`, preserving the subdirectory structure. +fn compile_account_components( + source_dir: &Path, + target_dir: &Path, + assembler: Assembler, +) -> Result<()> { + if !target_dir.exists() { + fs::create_dir_all(target_dir).unwrap(); + } + + for masm_file_path in shared::get_masm_files(source_dir).unwrap() { + let component_name = masm_file_path + .file_stem() + .expect("masm file should have a file stem") + .to_str() + .expect("file stem should be valid UTF-8") + .to_owned(); + + let component_source_code = fs::read_to_string(&masm_file_path) + .expect("reading the component's MASM source code should succeed"); + + let named_source = NamedSource::new(component_name.clone(), component_source_code); + + let component_library = assembler + .clone() + .assemble_library([named_source]) + .expect("library assembly should succeed"); + + // Preserve the subdirectory structure: compute relative path from source_dir + let relative_dir = masm_file_path + .parent() + .and_then(|p| p.strip_prefix(source_dir).ok()) + .unwrap_or(Path::new("")); + + let output_dir = target_dir.join(relative_dir); + if !output_dir.exists() { + fs::create_dir_all(&output_dir).unwrap(); + } + + let component_file_path = + output_dir.join(component_name).with_extension(Library::LIBRARY_EXTENSION); + component_library.write_to_file(component_file_path).into_diagnostic()?; + } + + Ok(()) +} + +// ERROR CONSTANTS FILE GENERATION +// ================================================================================================ + +/// Reads all MASM files from the `asm_source_dir` and extracts its error constants and their +/// associated error message and generates a Rust file for each category of errors. +/// For example: +/// +/// ```text +/// const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY="new account must have an empty vault" +/// ``` +/// +/// would generate a Rust file for transaction kernel errors (since the error belongs to that +/// category, identified by the category extracted from `ERR_`) with - roughly - the +/// following content: +/// +/// ```rust +/// pub const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY: MasmError = +/// MasmError::from_static_str("new account must have an empty vault"); +/// ``` +/// +/// and add the constant to the error constants array. +/// +/// 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(()); + } + + // Miden standards errors + // ------------------------------------------ + + let errors = shared::extract_all_masm_errors(asm_source_dir) + .context("failed to extract all masm errors")?; + shared::generate_error_file( + shared::ErrorModule { + file_name: STANDARDS_ERRORS_FILE, + array_name: STANDARDS_ERRORS_ARRAY_NAME, + is_crate_local: false, + }, + errors, + )?; + + Ok(()) +} + +/// This module should be kept in sync with the copy in miden-protocol's build.rs. +mod shared { + use std::collections::BTreeMap; + use std::fmt::Write; + use std::io::{self}; + use std::path::{Path, PathBuf}; + + use fs_err as fs; + use miden_assembly::Report; + use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; + 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. + /// + /// All non-MASM files are skipped. + pub fn get_masm_files>(dir_path: P) -> Result> { + let mut files = Vec::new(); + + let path = dir_path.as_ref(); + if path.is_dir() { + for entry in WalkDir::new(path) { + let entry = entry.into_diagnostic()?; + let file_path = entry.path().to_path_buf(); + if is_masm_file(&file_path).into_diagnostic()? { + files.push(file_path); + } + } + } else { + println!("cargo:warn=The specified path is not a directory."); + } + + Ok(files) + } + + /// Returns true if the provided path resolves to a file with `.masm` extension. + /// + /// # Errors + /// Returns an error if the path could not be converted to a UTF-8 string. + pub fn is_masm_file(path: &Path) -> io::Result { + if let Some(extension) = path.extension() { + let extension = extension + .to_str() + .ok_or_else(|| io::Error::other("invalid UTF-8 filename"))? + .to_lowercase(); + Ok(extension == "masm") + } else { + Ok(false) + } + } + + /// Extract all masm errors from the given path and returns a map by error category. + pub fn extract_all_masm_errors(asm_source_dir: &Path) -> Result> { + // We use a BTree here to order the errors by their categories which is the first part after + // the ERR_ prefix and to allow for the same error to be defined multiple times in + // different files (as long as the constant name and error messages match). + let mut errors = BTreeMap::new(); + + // Walk all files of the kernel source directory. + for entry in WalkDir::new(asm_source_dir) { + let entry = entry.into_diagnostic()?; + if !is_masm_file(entry.path()).into_diagnostic()? { + continue; + } + let file_contents = std::fs::read_to_string(entry.path()).into_diagnostic()?; + extract_masm_errors(&mut errors, &file_contents)?; + } + + let errors = errors + .into_iter() + .map(|(error_name, error)| NamedError { name: error_name, message: error.message }) + .collect(); + + Ok(errors) + } + + /// Extracts the errors from a single masm file and inserts them into the provided map. + pub fn extract_masm_errors( + errors: &mut BTreeMap, + file_contents: &str, + ) -> Result<()> { + let regex = Regex::new(r#"const\s*ERR_(?.*)\s*=\s*"(?.*)""#).unwrap(); + + for capture in regex.captures_iter(file_contents) { + let error_name = capture + .name("name") + .expect("error name should be captured") + .as_str() + .trim() + .to_owned(); + let error_message = capture + .name("message") + .expect("error code should be captured") + .as_str() + .trim() + .to_owned(); + + if let Some(ExtractedError { message: existing_error_message, .. }) = + errors.get(&error_name) + && existing_error_message != &error_message + { + return Err(Report::msg(format!( + "Transaction kernel error constant ERR_{error_name} is already defined elsewhere but its error message is different" + ))); + } + + // Enforce the "no trailing punctuation" rule from the Rust error guidelines on MASM + // errors. + if error_message.ends_with(".") { + return Err(Report::msg(format!( + "Error messages should not end with a period: `ERR_{error_name}: {error_message}`" + ))); + } + + errors.insert(error_name, ExtractedError { message: error_message }); + } + + Ok(()) + } + + pub fn is_new_error_category<'a>( + last_error: &mut Option<&'a str>, + current_error: &'a str, + ) -> bool { + let is_new = match last_error { + Some(last_err) => { + let last_category = + last_err.split("_").next().expect("there should be at least one entry"); + let new_category = + current_error.split("_").next().expect("there should be at least one entry"); + last_category != new_category + }, + None => false, + }; + + last_error.replace(current_error); + + is_new + } + + /// Generates the content of an error file for the given category and the set of errors and + /// writes it to the category's file. + pub fn generate_error_file(module: ErrorModule, errors: Vec) -> Result<()> { + let mut output = String::new(); + + if module.is_crate_local { + writeln!(output, "use crate::errors::MasmError;\n").unwrap(); + } else { + writeln!(output, "use miden_protocol::errors::MasmError;\n").unwrap(); + } + + writeln!( + output, + "// 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, ...). +" + ) + .unwrap(); + + writeln!( + output, + "// {} +// ================================================================================================ +", + module.array_name.replace("_", " ") + ) + .unwrap(); + + let mut last_error = None; + for named_error in errors.iter() { + let NamedError { name, message } = named_error; + + // Group errors into blocks separate by newlines. + if is_new_error_category(&mut last_error, name) { + writeln!(output).into_diagnostic()?; + } + + writeln!(output, "/// Error Message: \"{message}\"").into_diagnostic()?; + writeln!( + output, + r#"pub const ERR_{name}: MasmError = MasmError::from_static_str("{message}");"# + ) + .into_diagnostic()?; + } + + std::fs::write(module.file_name, output).into_diagnostic()?; + + Ok(()) + } + + pub type ErrorName = String; + + #[derive(Debug, Clone)] + pub struct ExtractedError { + pub message: String, + } + + #[derive(Debug, Clone)] + pub struct NamedError { + pub name: ErrorName, + pub message: String, + } + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct ErrorModule { + pub file_name: &'static str, + pub array_name: &'static str, + pub is_crate_local: bool, + } +} diff --git a/crates/miden-standards/src/account/auth/ecdsa_k256_keccak.rs b/crates/miden-standards/src/account/auth/ecdsa_k256_keccak.rs new file mode 100644 index 0000000000..e26661972c --- /dev/null +++ b/crates/miden-standards/src/account/auth/ecdsa_k256_keccak.rs @@ -0,0 +1,55 @@ +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountComponent, StorageSlot, StorageSlotName}; +use miden_protocol::utils::sync::LazyLock; + +use crate::account::components::ecdsa_k256_keccak_library; + +static ECDSA_PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak::public_key") + .expect("storage slot name should be valid") +}); + +/// An [`AccountComponent`] implementing the ECDSA K256 Keccak signature scheme for authentication +/// of transactions. +/// +/// It reexports the procedures from `miden::standards::auth::ecdsa_k256_keccak`. When linking +/// against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. The procedures of this component are: +/// - `verify_signatures`, which can be used to verify a signature provided via the advice stack to +/// authenticate a transaction. +/// - `authenticate_transaction`, which can be used to authenticate a transaction using the ECDSA +/// signature scheme. +/// +/// This component supports all account types. +/// +/// [builder]: crate::code_builder::CodeBuilder +pub struct AuthEcdsaK256Keccak { + pub_key: PublicKeyCommitment, +} + +impl AuthEcdsaK256Keccak { + /// Creates a new [`AuthEcdsaK256Keccak`] component with the given `public_key`. + pub fn new(pub_key: PublicKeyCommitment) -> Self { + Self { pub_key } + } + + /// Returns the [`StorageSlotName`] where the public key is stored. + pub fn public_key_slot() -> &'static StorageSlotName { + &ECDSA_PUBKEY_SLOT_NAME + } +} + +impl From for AccountComponent { + fn from(ecdsa: AuthEcdsaK256Keccak) -> Self { + AccountComponent::new( + ecdsa_k256_keccak_library(), + vec![StorageSlot::with_value( + AuthEcdsaK256Keccak::public_key_slot().clone(), + ecdsa.pub_key.into(), + )], + ) + .expect("ecdsa component should satisfy the requirements of a valid account component") + .with_supports_all_types() + } +} diff --git a/crates/miden-lib/src/account/auth/ecdsa_k256_keccak_acl.rs b/crates/miden-standards/src/account/auth/ecdsa_k256_keccak_acl.rs similarity index 75% rename from crates/miden-lib/src/account/auth/ecdsa_k256_keccak_acl.rs rename to crates/miden-standards/src/account/auth/ecdsa_k256_keccak_acl.rs index 186ebd026f..276a85f1bf 100644 --- a/crates/miden-lib/src/account/auth/ecdsa_k256_keccak_acl.rs +++ b/crates/miden-standards/src/account/auth/ecdsa_k256_keccak_acl.rs @@ -1,11 +1,34 @@ use alloc::vec::Vec; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountCode, AccountComponent, StorageMap, StorageSlot}; -use miden_objects::{AccountError, Word}; +use miden_protocol::Word; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{ + AccountCode, + AccountComponent, + StorageMap, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::errors::AccountError; +use miden_protocol::utils::sync::LazyLock; use crate::account::components::ecdsa_k256_keccak_acl_library; +static PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak_acl::public_key") + .expect("storage slot name should be valid") +}); + +static CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak_acl::config") + .expect("storage slot name should be valid") +}); + +static TRIGGER_PROCEDURE_ROOT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak_acl::trigger_procedure_roots") + .expect("storage slot name should be valid") +}); + /// Configuration for [`AuthEcdsaK256KeccakAcl`] component. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthEcdsaK256KeccakAclConfig { @@ -96,7 +119,7 @@ impl Default for AuthEcdsaK256KeccakAclConfig { /// /// ## Storage Layout /// - Slot 0(value): Public key (same as EcdsaK256Keccak) -/// - Slot 1(value): [num_tracked_procs, allow_unauthorized_output_notes, +/// - Slot 1(value): [num_trigger_procs, allow_unauthorized_output_notes, /// allow_unauthorized_input_notes, 0] /// - Slot 2(map): A map with trigger procedure roots /// @@ -133,25 +156,46 @@ impl AuthEcdsaK256KeccakAcl { Ok(Self { pub_key, config }) } + + /// Returns the [`StorageSlotName`] where the public key is stored. + pub fn public_key_slot() -> &'static StorageSlotName { + &PUBKEY_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the component's configuration is stored. + pub fn config_slot() -> &'static StorageSlotName { + &CONFIG_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the trigger procedure roots are stored. + pub fn trigger_procedure_roots_slot() -> &'static StorageSlotName { + &TRIGGER_PROCEDURE_ROOT_SLOT_NAME + } } impl From for AccountComponent { fn from(ecdsa: AuthEcdsaK256KeccakAcl) -> Self { let mut storage_slots = Vec::with_capacity(3); - // Slot 0: Public key - storage_slots.push(StorageSlot::Value(ecdsa.pub_key.into())); - // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes, - // allow_unauthorized_input_notes, 0] + // Public key slot + storage_slots.push(StorageSlot::with_value( + AuthEcdsaK256KeccakAcl::public_key_slot().clone(), + ecdsa.pub_key.into(), + )); + + // Config slot let num_procs = ecdsa.config.auth_trigger_procedures.len() as u32; - storage_slots.push(StorageSlot::Value(Word::from([ - num_procs, - u32::from(ecdsa.config.allow_unauthorized_output_notes), - u32::from(ecdsa.config.allow_unauthorized_input_notes), - 0, - ]))); - - // Slot 2: A map with tracked procedure roots + storage_slots.push(StorageSlot::with_value( + AuthEcdsaK256KeccakAcl::config_slot().clone(), + Word::from([ + num_procs, + u32::from(ecdsa.config.allow_unauthorized_output_notes), + u32::from(ecdsa.config.allow_unauthorized_input_notes), + 0, + ]), + )); + + // Trigger procedure roots slot // We add the map even if there are no trigger procedures, to always maintain the same // storage layout. let map_entries = ecdsa @@ -162,7 +206,10 @@ impl From for AccountComponent { .map(|(i, proc_root)| (Word::from([i as u32, 0, 0, 0]), *proc_root)); // Safe to unwrap because we know that the map keys are unique. - storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap())); + storage_slots.push(StorageSlot::with_map( + AuthEcdsaK256KeccakAcl::trigger_procedure_roots_slot().clone(), + StorageMap::with_entries(map_entries).unwrap(), + )); AccountComponent::new(ecdsa_k256_keccak_acl_library(), storage_slots) .expect( @@ -174,8 +221,8 @@ impl From for AccountComponent { #[cfg(test)] mod tests { - use miden_objects::Word; - use miden_objects::account::AccountBuilder; + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; use super::*; use crate::account::components::WellKnownComponent; @@ -189,8 +236,8 @@ mod tests { allow_unauthorized_output_notes: bool, /// Allow unauthorized input notes flag allow_unauthorized_input_notes: bool, - /// Expected slot 1 value [num_procs, allow_output, allow_input, 0] - expected_slot_1: Word, + /// Expected config slot value [num_procs, allow_output, allow_input, 0] + expected_config_slot: Word, } /// Helper function to get the basic wallet procedures for testing @@ -229,20 +276,29 @@ mod tests { .build() .expect("account building failed"); - // Assert public key in slot 0 - let public_key_slot = account.storage().get_item(0).expect("storage slot 0 access failed"); + // Check public key storage + let public_key_slot = account + .storage() + .get_item(AuthEcdsaK256KeccakAcl::public_key_slot()) + .expect("public key storage slot access failed"); assert_eq!(public_key_slot, public_key.into()); - // Assert configuration in slot 1 - let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed"); - assert_eq!(slot_1, config.expected_slot_1); + // Check configuration storage + let config_slot = account + .storage() + .get_item(AuthEcdsaK256KeccakAcl::config_slot()) + .expect("config storage slot access failed"); + assert_eq!(config_slot, config.expected_config_slot); - // Assert procedure roots in map (slot 2) + // Check procedure roots if config.with_procedures { for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() { let proc_root = account .storage() - .get_map_item(2, Word::from([i as u32, 0, 0, 0])) + .get_map_item( + AuthEcdsaK256KeccakAcl::trigger_procedure_roots_slot(), + Word::from([i as u32, 0, 0, 0]), + ) .expect("storage map access failed"); assert_eq!(proc_root, *expected_proc_root); } @@ -250,7 +306,7 @@ mod tests { // When no procedures, the map should return empty for key [0,0,0,0] let proc_root = account .storage() - .get_map_item(2, Word::empty()) + .get_map_item(AuthEcdsaK256KeccakAcl::trigger_procedure_roots_slot(), Word::empty()) .expect("storage map access failed"); assert_eq!(proc_root, Word::empty()); } @@ -263,7 +319,7 @@ mod tests { with_procedures: false, allow_unauthorized_output_notes: false, allow_unauthorized_input_notes: false, - expected_slot_1: Word::empty(), // [0, 0, 0, 0] + expected_config_slot: Word::empty(), // [0, 0, 0, 0] }); } @@ -274,7 +330,7 @@ mod tests { with_procedures: true, allow_unauthorized_output_notes: false, allow_unauthorized_input_notes: false, - expected_slot_1: Word::from([2u32, 0, 0, 0]), + expected_config_slot: Word::from([2u32, 0, 0, 0]), }); } @@ -285,7 +341,7 @@ mod tests { with_procedures: false, allow_unauthorized_output_notes: true, allow_unauthorized_input_notes: false, - expected_slot_1: Word::from([0u32, 1, 0, 0]), + expected_config_slot: Word::from([0u32, 1, 0, 0]), }); } @@ -296,7 +352,7 @@ mod tests { with_procedures: true, allow_unauthorized_output_notes: true, allow_unauthorized_input_notes: false, - expected_slot_1: Word::from([2u32, 1, 0, 0]), + expected_config_slot: Word::from([2u32, 1, 0, 0]), }); } @@ -307,7 +363,7 @@ mod tests { with_procedures: false, allow_unauthorized_output_notes: false, allow_unauthorized_input_notes: true, - expected_slot_1: Word::from([0u32, 0, 1, 0]), + expected_config_slot: Word::from([0u32, 0, 1, 0]), }); } @@ -318,7 +374,7 @@ mod tests { with_procedures: true, allow_unauthorized_output_notes: true, allow_unauthorized_input_notes: true, - expected_slot_1: Word::from([2u32, 1, 1, 0]), + expected_config_slot: Word::from([2u32, 1, 1, 0]), }); } } diff --git a/crates/miden-lib/src/account/auth/ecdsa_k256_keccak_multisig.rs b/crates/miden-standards/src/account/auth/ecdsa_k256_keccak_multisig.rs similarity index 67% rename from crates/miden-lib/src/account/auth/ecdsa_k256_keccak_multisig.rs rename to crates/miden-standards/src/account/auth/ecdsa_k256_keccak_multisig.rs index 7bb653cb62..f29bf71732 100644 --- a/crates/miden-lib/src/account/auth/ecdsa_k256_keccak_multisig.rs +++ b/crates/miden-standards/src/account/auth/ecdsa_k256_keccak_multisig.rs @@ -1,12 +1,36 @@ use alloc::collections::BTreeSet; use alloc::vec::Vec; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountComponent, StorageMap, StorageSlot}; -use miden_objects::{AccountError, Word}; +use miden_protocol::Word; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountComponent, StorageMap, StorageSlot, StorageSlotName}; +use miden_protocol::errors::AccountError; +use miden_protocol::utils::sync::LazyLock; use crate::account::components::ecdsa_k256_keccak_multisig_library; +static THRESHOLD_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak_multisig::threshold_config") + .expect("storage slot name should be valid") +}); + +static APPROVER_PUBKEYS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak_multisig::approver_public_keys") + .expect("storage slot name should be valid") +}); + +static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new( + "miden::standards::auth::ecdsa_k256_keccak_multisig::executed_transactions", + ) + .expect("storage slot name should be valid") +}); + +static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::ecdsa_k256_keccak_multisig::procedure_thresholds") + .expect("storage slot name should be valid") +}); + // MULTISIG AUTHENTICATION COMPONENT // ================================================================================================ @@ -104,22 +128,40 @@ impl AuthEcdsaK256KeccakMultisig { pub fn new(config: AuthEcdsaK256KeccakMultisigConfig) -> Result { Ok(Self { config }) } + + /// Returns the [`StorageSlotName`] where the threshold configuration is stored. + pub fn threshold_config_slot() -> &'static StorageSlotName { + &THRESHOLD_CONFIG_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the approver public keys are stored. + pub fn approver_public_keys_slot() -> &'static StorageSlotName { + &APPROVER_PUBKEYS_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the executed transactions are stored. + pub fn executed_transactions_slot() -> &'static StorageSlotName { + &EXECUTED_TRANSACTIONS_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the procedure thresholds are stored. + pub fn procedure_thresholds_slot() -> &'static StorageSlotName { + &PROCEDURE_THRESHOLDS_SLOT_NAME + } } impl From for AccountComponent { fn from(multisig: AuthEcdsaK256KeccakMultisig) -> Self { let mut storage_slots = Vec::with_capacity(3); - // Slot 0: [threshold, num_approvers, 0, 0] + // Threshold config slot (value: [threshold, num_approvers, 0, 0]) let num_approvers = multisig.config.approvers().len() as u32; - storage_slots.push(StorageSlot::Value(Word::from([ - multisig.config.default_threshold(), - num_approvers, - 0, - 0, - ]))); - - // Slot 1: A map with approver public keys + storage_slots.push(StorageSlot::with_value( + AuthEcdsaK256KeccakMultisig::threshold_config_slot().clone(), + Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]), + )); + + // Approver public keys slot (map) let map_entries = multisig .config .approvers() @@ -128,13 +170,19 @@ impl From for AccountComponent { .map(|(i, pub_key)| (Word::from([i as u32, 0, 0, 0]), (*pub_key).into())); // Safe to unwrap because we know that the map keys are unique. - storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap())); + storage_slots.push(StorageSlot::with_map( + AuthEcdsaK256KeccakMultisig::approver_public_keys_slot().clone(), + StorageMap::with_entries(map_entries).unwrap(), + )); - // Slot 2: A map which stores executed transactions + // Executed transactions slot (map) let executed_transactions = StorageMap::default(); - storage_slots.push(StorageSlot::Map(executed_transactions)); + storage_slots.push(StorageSlot::with_map( + AuthEcdsaK256KeccakMultisig::executed_transactions_slot().clone(), + executed_transactions, + )); - // Slot 3: A map which stores procedure thresholds (PROC_ROOT -> threshold) + // Procedure thresholds slot (map: PROC_ROOT -> threshold) let proc_threshold_roots = StorageMap::with_entries( multisig .config @@ -143,7 +191,10 @@ impl From for AccountComponent { .map(|(proc_root, threshold)| (*proc_root, Word::from([*threshold, 0, 0, 0]))), ) .unwrap(); - storage_slots.push(StorageSlot::Map(proc_threshold_roots)); + storage_slots.push(StorageSlot::with_map( + AuthEcdsaK256KeccakMultisig::procedure_thresholds_slot().clone(), + proc_threshold_roots, + )); AccountComponent::new(ecdsa_k256_keccak_multisig_library(), storage_slots) .expect("Multisig auth component should satisfy the requirements of a valid account component") @@ -155,8 +206,8 @@ impl From for AccountComponent { mod tests { use alloc::string::ToString; - use miden_objects::Word; - use miden_objects::account::AccountBuilder; + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; use super::*; use crate::account::wallets::BasicWallet; @@ -185,16 +236,22 @@ mod tests { .build() .expect("account building failed"); - // Verify slot 0: [threshold, num_approvers, 0, 0] - let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed"); - assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); + // Verify config slot: [threshold, num_approvers, 0, 0] + let config_slot = account + .storage() + .get_item(AuthEcdsaK256KeccakMultisig::threshold_config_slot()) + .expect("config storage slot access failed"); + assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); - // Verify slot 1: Approver public keys in map + // Verify approver pub keys slot for (i, expected_pub_key) in approvers.iter().enumerate() { let stored_pub_key = account .storage() - .get_map_item(1, Word::from([i as u32, 0, 0, 0])) - .expect("storage map access failed"); + .get_map_item( + AuthEcdsaK256KeccakMultisig::approver_public_keys_slot(), + Word::from([i as u32, 0, 0, 0]), + ) + .expect("approver public key storage map access failed"); assert_eq!(stored_pub_key, Word::from(*expected_pub_key)); } } @@ -219,13 +276,19 @@ mod tests { .expect("account building failed"); // Verify storage layout - let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed"); - assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); + let config_slot = account + .storage() + .get_item(AuthEcdsaK256KeccakMultisig::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(1, Word::from([0u32, 0, 0, 0])) - .expect("storage map access failed"); + .get_map_item( + AuthEcdsaK256KeccakMultisig::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)); } diff --git a/crates/miden-standards/src/account/auth/falcon_512_rpo.rs b/crates/miden-standards/src/account/auth/falcon_512_rpo.rs new file mode 100644 index 0000000000..d3f37da6e6 --- /dev/null +++ b/crates/miden-standards/src/account/auth/falcon_512_rpo.rs @@ -0,0 +1,59 @@ +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountComponent, StorageSlot, StorageSlotName}; +use miden_protocol::utils::sync::LazyLock; + +use crate::account::components::falcon_512_rpo_library; + +static FALCON_PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo::public_key") + .expect("storage slot name should be valid") +}); + +/// An [`AccountComponent`] implementing the Falcon512Rpo signature scheme for authentication of +/// transactions. +/// +/// It reexports the procedures from `miden::standards::auth::falcon512_rpo`. When linking against +/// this component, the `miden` library (i.e. [`ProtocolLib`](miden_protocol::ProtocolLib)) must +/// be available to the assembler which is the case when using [`CodeBuilder`][builder]. The +/// procedures of this component are: +/// - `verify_signatures`, which can be used to verify a signature provided via the advice stack to +/// authenticate a transaction. +/// - `authenticate_transaction`, which can be used to authenticate a transaction using the Falcon +/// signature scheme. +/// +/// This component supports all account types. +/// +/// ## Storage Layout +/// +/// - [`Self::public_key_slot`]: Public key +/// +/// [builder]: crate::code_builder::CodeBuilder +pub struct AuthFalcon512Rpo { + pub_key: PublicKeyCommitment, +} + +impl AuthFalcon512Rpo { + /// Creates a new [`AuthFalcon512Rpo`] component with the given `public_key`. + pub fn new(pub_key: PublicKeyCommitment) -> Self { + Self { pub_key } + } + + /// Returns the [`StorageSlotName`] where the public key is stored. + pub fn public_key_slot() -> &'static StorageSlotName { + &FALCON_PUBKEY_SLOT_NAME + } +} + +impl From for AccountComponent { + fn from(falcon: AuthFalcon512Rpo) -> Self { + AccountComponent::new( + falcon_512_rpo_library(), + vec![StorageSlot::with_value( + AuthFalcon512Rpo::public_key_slot().clone(), + falcon.pub_key.into(), + )], + ) + .expect("falcon component should satisfy the requirements of a valid account component") + .with_supports_all_types() + } +} diff --git a/crates/miden-lib/src/account/auth/rpo_falcon_512_acl.rs b/crates/miden-standards/src/account/auth/falcon_512_rpo_acl.rs similarity index 66% rename from crates/miden-lib/src/account/auth/rpo_falcon_512_acl.rs rename to crates/miden-standards/src/account/auth/falcon_512_rpo_acl.rs index f9982d5456..aee82e064d 100644 --- a/crates/miden-lib/src/account/auth/rpo_falcon_512_acl.rs +++ b/crates/miden-standards/src/account/auth/falcon_512_rpo_acl.rs @@ -1,14 +1,37 @@ use alloc::vec::Vec; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountCode, AccountComponent, StorageMap, StorageSlot}; -use miden_objects::{AccountError, Word}; - -use crate::account::components::rpo_falcon_512_acl_library; - -/// Configuration for [`AuthRpoFalcon512Acl`] component. +use miden_protocol::Word; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{ + AccountCode, + AccountComponent, + StorageMap, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::errors::AccountError; +use miden_protocol::utils::sync::LazyLock; + +use crate::account::components::falcon_512_rpo_acl_library; + +static PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_acl::public_key") + .expect("storage slot name should be valid") +}); + +static CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_acl::config") + .expect("storage slot name should be valid") +}); + +static TRIGGER_PROCEDURE_ROOT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_acl::trigger_procedure_roots") + .expect("storage slot name should be valid") +}); + +/// Configuration for [`AuthFalcon512RpoAcl`] component. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuthRpoFalcon512AclConfig { +pub struct AuthFalcon512RpoAclConfig { /// List of procedure roots that require authentication when called. pub auth_trigger_procedures: Vec, /// When `false`, creating output notes (sending notes to other accounts) requires @@ -19,7 +42,7 @@ pub struct AuthRpoFalcon512AclConfig { pub allow_unauthorized_input_notes: bool, } -impl AuthRpoFalcon512AclConfig { +impl AuthFalcon512RpoAclConfig { /// Creates a new configuration with no trigger procedures and both flags set to `false` (most /// restrictive). pub fn new() -> Self { @@ -49,14 +72,14 @@ impl AuthRpoFalcon512AclConfig { } } -impl Default for AuthRpoFalcon512AclConfig { +impl Default for AuthFalcon512RpoAclConfig { fn default() -> Self { Self::new() } } /// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the -/// RpoFalcon512 signature scheme for authentication of transactions. +/// Falcon512Rpo signature scheme for authentication of transactions. /// /// This component provides fine-grained authentication control based on three conditions: /// 1. **Procedure-based authentication**: Requires authentication when any of the specified trigger @@ -95,10 +118,11 @@ impl Default for AuthRpoFalcon512AclConfig { /// allowing free note processing. /// /// ## Storage Layout -/// - Slot 0(value): Public key (same as RpoFalcon512) -/// - Slot 1(value): [num_tracked_procs, allow_unauthorized_output_notes, -/// allow_unauthorized_input_notes, 0] -/// - Slot 2(map): A map with trigger procedure roots +/// +/// - [`Self::public_key_slot`]: Public key +/// - [`Self::config_slot`]: `[num_trigger_procs, allow_unauthorized_output_notes, +/// allow_unauthorized_input_notes, 0]` +/// - [`Self::trigger_procedure_roots_slot`]: A map with trigger procedure roots /// /// ## Important Note on Procedure Detection /// The procedure-based authentication relies on the `was_procedure_called` kernel function, @@ -109,20 +133,20 @@ impl Default for AuthRpoFalcon512AclConfig { /// procedures for authentication. /// /// This component supports all account types. -pub struct AuthRpoFalcon512Acl { +pub struct AuthFalcon512RpoAcl { pub_key: PublicKeyCommitment, - config: AuthRpoFalcon512AclConfig, + config: AuthFalcon512RpoAclConfig, } -impl AuthRpoFalcon512Acl { - /// Creates a new [`AuthRpoFalcon512Acl`] component with the given `public_key` and +impl AuthFalcon512RpoAcl { + /// Creates a new [`AuthFalcon512RpoAcl`] component with the given `public_key` and /// configuration. /// /// # Panics /// Panics if more than [AccountCode::MAX_NUM_PROCEDURES] procedures are specified. pub fn new( pub_key: PublicKeyCommitment, - config: AuthRpoFalcon512AclConfig, + config: AuthFalcon512RpoAclConfig, ) -> Result { let max_procedures = AccountCode::MAX_NUM_PROCEDURES; if config.auth_trigger_procedures.len() > max_procedures { @@ -133,26 +157,46 @@ impl AuthRpoFalcon512Acl { Ok(Self { pub_key, config }) } + + /// Returns the [`StorageSlotName`] where the public key is stored. + pub fn public_key_slot() -> &'static StorageSlotName { + &PUBKEY_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the component's configuration is stored. + pub fn config_slot() -> &'static StorageSlotName { + &CONFIG_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the trigger procedure roots are stored. + pub fn trigger_procedure_roots_slot() -> &'static StorageSlotName { + &TRIGGER_PROCEDURE_ROOT_SLOT_NAME + } } -impl From for AccountComponent { - fn from(falcon: AuthRpoFalcon512Acl) -> Self { +impl From for AccountComponent { + fn from(falcon: AuthFalcon512RpoAcl) -> Self { let mut storage_slots = Vec::with_capacity(3); - // Slot 0: Public key - storage_slots.push(StorageSlot::Value(falcon.pub_key.into())); + // Public key slot + storage_slots.push(StorageSlot::with_value( + AuthFalcon512RpoAcl::public_key_slot().clone(), + falcon.pub_key.into(), + )); - // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes, - // allow_unauthorized_input_notes, 0] + // Config slot let num_procs = falcon.config.auth_trigger_procedures.len() as u32; - storage_slots.push(StorageSlot::Value(Word::from([ - num_procs, - u32::from(falcon.config.allow_unauthorized_output_notes), - u32::from(falcon.config.allow_unauthorized_input_notes), - 0, - ]))); - - // Slot 2: A map with tracked procedure roots + storage_slots.push(StorageSlot::with_value( + AuthFalcon512RpoAcl::config_slot().clone(), + Word::from([ + num_procs, + u32::from(falcon.config.allow_unauthorized_output_notes), + u32::from(falcon.config.allow_unauthorized_input_notes), + 0, + ]), + )); + + // Trigger procedure roots slot // We add the map even if there are no trigger procedures, to always maintain the same // storage layout. let map_entries = falcon @@ -163,9 +207,12 @@ impl From for AccountComponent { .map(|(i, proc_root)| (Word::from([i as u32, 0, 0, 0]), *proc_root)); // Safe to unwrap because we know that the map keys are unique. - storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap())); + storage_slots.push(StorageSlot::with_map( + AuthFalcon512RpoAcl::trigger_procedure_roots_slot().clone(), + StorageMap::with_entries(map_entries).unwrap(), + )); - AccountComponent::new(rpo_falcon_512_acl_library(), storage_slots) + AccountComponent::new(falcon_512_rpo_acl_library(), storage_slots) .expect( "ACL auth component should satisfy the requirements of a valid account component", ) @@ -175,8 +222,8 @@ impl From for AccountComponent { #[cfg(test)] mod tests { - use miden_objects::Word; - use miden_objects::account::AccountBuilder; + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; use super::*; use crate::account::components::WellKnownComponent; @@ -190,8 +237,8 @@ mod tests { allow_unauthorized_output_notes: bool, /// Allow unauthorized input notes flag allow_unauthorized_input_notes: bool, - /// Expected slot 1 value [num_procs, allow_output, allow_input, 0] - expected_slot_1: Word, + /// Expected config slot value [num_procs, allow_output, allow_input, 0] + expected_config_slot: Word, } /// Helper function to get the basic wallet procedures for testing @@ -208,7 +255,7 @@ mod tests { let public_key = PublicKeyCommitment::from(Word::empty()); // Build the configuration - let mut acl_config = AuthRpoFalcon512AclConfig::new() + let mut acl_config = AuthFalcon512RpoAclConfig::new() .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes) .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes); @@ -222,7 +269,7 @@ mod tests { // Create component and account let component = - AuthRpoFalcon512Acl::new(public_key, acl_config).expect("component creation failed"); + AuthFalcon512RpoAcl::new(public_key, acl_config).expect("component creation failed"); let account = AccountBuilder::new([0; 32]) .with_auth_component(component) @@ -230,20 +277,29 @@ mod tests { .build() .expect("account building failed"); - // Assert public key in slot 0 - let public_key_slot = account.storage().get_item(0).expect("storage slot 0 access failed"); + // Check public key storage + let public_key_slot = account + .storage() + .get_item(AuthFalcon512RpoAcl::public_key_slot()) + .expect("public key storage slot access failed"); assert_eq!(public_key_slot, public_key.into()); - // Assert configuration in slot 1 - let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed"); - assert_eq!(slot_1, config.expected_slot_1); + // Check configuration storage + let config_slot = account + .storage() + .get_item(AuthFalcon512RpoAcl::config_slot()) + .expect("config storage slot access failed"); + assert_eq!(config_slot, config.expected_config_slot); - // Assert procedure roots in map (slot 2) + // Check procedure roots if config.with_procedures { for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() { let proc_root = account .storage() - .get_map_item(2, Word::from([i as u32, 0, 0, 0])) + .get_map_item( + AuthFalcon512RpoAcl::trigger_procedure_roots_slot(), + Word::from([i as u32, 0, 0, 0]), + ) .expect("storage map access failed"); assert_eq!(proc_root, *expected_proc_root); } @@ -251,7 +307,7 @@ mod tests { // When no procedures, the map should return empty for key [0,0,0,0] let proc_root = account .storage() - .get_map_item(2, Word::empty()) + .get_map_item(AuthFalcon512RpoAcl::trigger_procedure_roots_slot(), Word::empty()) .expect("storage map access failed"); assert_eq!(proc_root, Word::empty()); } @@ -259,67 +315,67 @@ mod tests { /// Test ACL component with no procedures and both authorization flags set to false #[test] - fn test_rpo_falcon_512_acl_no_procedures() { + fn test_falcon_512_rpo_acl_no_procedures() { test_acl_component(AclTestConfig { with_procedures: false, allow_unauthorized_output_notes: false, allow_unauthorized_input_notes: false, - expected_slot_1: Word::empty(), // [0, 0, 0, 0] + expected_config_slot: Word::empty(), // [0, 0, 0, 0] }); } /// Test ACL component with two procedures and both authorization flags set to false #[test] - fn test_rpo_falcon_512_acl_with_two_procedures() { + fn test_falcon_512_rpo_acl_with_two_procedures() { test_acl_component(AclTestConfig { with_procedures: true, allow_unauthorized_output_notes: false, allow_unauthorized_input_notes: false, - expected_slot_1: Word::from([2u32, 0, 0, 0]), + expected_config_slot: Word::from([2u32, 0, 0, 0]), }); } /// Test ACL component with no procedures and allow_unauthorized_output_notes set to true #[test] - fn test_rpo_falcon_512_acl_with_allow_unauthorized_output_notes() { + fn test_falcon_512_rpo_acl_with_allow_unauthorized_output_notes() { test_acl_component(AclTestConfig { with_procedures: false, allow_unauthorized_output_notes: true, allow_unauthorized_input_notes: false, - expected_slot_1: Word::from([0u32, 1, 0, 0]), + expected_config_slot: Word::from([0u32, 1, 0, 0]), }); } /// Test ACL component with two procedures and allow_unauthorized_output_notes set to true #[test] - fn test_rpo_falcon_512_acl_with_procedures_and_allow_unauthorized_output_notes() { + fn test_falcon_512_rpo_acl_with_procedures_and_allow_unauthorized_output_notes() { test_acl_component(AclTestConfig { with_procedures: true, allow_unauthorized_output_notes: true, allow_unauthorized_input_notes: false, - expected_slot_1: Word::from([2u32, 1, 0, 0]), + expected_config_slot: Word::from([2u32, 1, 0, 0]), }); } /// Test ACL component with no procedures and allow_unauthorized_input_notes set to true #[test] - fn test_rpo_falcon_512_acl_with_allow_unauthorized_input_notes() { + fn test_falcon_512_rpo_acl_with_allow_unauthorized_input_notes() { test_acl_component(AclTestConfig { with_procedures: false, allow_unauthorized_output_notes: false, allow_unauthorized_input_notes: true, - expected_slot_1: Word::from([0u32, 0, 1, 0]), + expected_config_slot: Word::from([0u32, 0, 1, 0]), }); } /// Test ACL component with two procedures and both authorization flags set to true #[test] - fn test_rpo_falcon_512_acl_with_both_allow_flags() { + fn test_falcon_512_rpo_acl_with_both_allow_flags() { test_acl_component(AclTestConfig { with_procedures: true, allow_unauthorized_output_notes: true, allow_unauthorized_input_notes: true, - expected_slot_1: Word::from([2u32, 1, 1, 0]), + expected_config_slot: Word::from([2u32, 1, 1, 0]), }); } } diff --git a/crates/miden-lib/src/account/auth/rpo_falcon_512_multisig.rs b/crates/miden-standards/src/account/auth/falcon_512_rpo_multisig.rs similarity index 55% rename from crates/miden-lib/src/account/auth/rpo_falcon_512_multisig.rs rename to crates/miden-standards/src/account/auth/falcon_512_rpo_multisig.rs index 2d933d990a..0b038c20a1 100644 --- a/crates/miden-lib/src/account/auth/rpo_falcon_512_multisig.rs +++ b/crates/miden-standards/src/account/auth/falcon_512_rpo_multisig.rs @@ -1,24 +1,46 @@ use alloc::collections::BTreeSet; use alloc::vec::Vec; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountComponent, StorageMap, StorageSlot}; -use miden_objects::{AccountError, Word}; - -use crate::account::components::rpo_falcon_512_multisig_library; +use miden_protocol::Word; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountComponent, StorageMap, StorageSlot, StorageSlotName}; +use miden_protocol::errors::AccountError; +use miden_protocol::utils::sync::LazyLock; + +use crate::account::components::falcon_512_rpo_multisig_library; + +static THRESHOLD_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_multisig::threshold_config") + .expect("storage slot name should be valid") +}); + +static APPROVER_PUBKEYS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_multisig::approver_public_keys") + .expect("storage slot name should be valid") +}); + +static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_multisig::executed_transactions") + .expect("storage slot name should be valid") +}); + +static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::falcon512_rpo_multisig::procedure_thresholds") + .expect("storage slot name should be valid") +}); // MULTISIG AUTHENTICATION COMPONENT // ================================================================================================ -/// Configuration for [`AuthRpoFalcon512Multisig`] component. +/// Configuration for [`AuthFalcon512RpoMultisig`] component. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuthRpoFalcon512MultisigConfig { +pub struct AuthFalcon512RpoMultisigConfig { approvers: Vec, default_threshold: u32, proc_thresholds: Vec<(Word, u32)>, } -impl AuthRpoFalcon512MultisigConfig { +impl AuthFalcon512RpoMultisigConfig { /// Creates a new configuration with the given approvers and a default threshold. /// /// The `default_threshold` must be at least 1 and at most the number of approvers. @@ -80,46 +102,66 @@ impl AuthRpoFalcon512MultisigConfig { } } -/// An [`AccountComponent`] implementing a multisig based on RpoFalcon512 signatures. +/// An [`AccountComponent`] implementing a multisig based on Falcon512Rpo signatures. /// /// It enforces a threshold of approver signatures for every transaction, with optional /// per-procedure thresholds overrides. Non-uniform thresholds (especially a threshold of one) /// should be used with caution for private multisig accounts, as a single approver could withhold /// the new state from other approvers, effectively locking them out. /// -/// The storage layout is: -/// - Slot 0(value): [threshold, num_approvers, 0, 0] -/// - Slot 1(map): A map with approver public keys (index -> pubkey) -/// - Slot 2(map): A map which stores executed transactions -/// - Slot 3(map): A map which stores procedure thresholds (PROC_ROOT -> threshold) +/// ## Storage Layout +/// +/// - [`Self::threshold_config_slot`]: `[threshold, num_approvers, 0, 0]` +/// - [`Self::approver_public_keys_slot`]: A map with approver public keys (index -> pubkey) +/// - [`Self::executed_transactions_slot`]: A map which stores executed transactions +/// - [`Self::procedure_thresholds_slot`]: A map which stores procedure thresholds (PROC_ROOT -> +/// threshold) /// /// This component supports all account types. #[derive(Debug)] -pub struct AuthRpoFalcon512Multisig { - config: AuthRpoFalcon512MultisigConfig, +pub struct AuthFalcon512RpoMultisig { + config: AuthFalcon512RpoMultisigConfig, } -impl AuthRpoFalcon512Multisig { - /// Creates a new [`AuthRpoFalcon512Multisig`] component from the provided configuration. - pub fn new(config: AuthRpoFalcon512MultisigConfig) -> Result { +impl AuthFalcon512RpoMultisig { + /// Creates a new [`AuthFalcon512RpoMultisig`] component from the provided configuration. + pub fn new(config: AuthFalcon512RpoMultisigConfig) -> Result { Ok(Self { config }) } + + /// Returns the [`StorageSlotName`] where the threshold configuration is stored. + pub fn threshold_config_slot() -> &'static StorageSlotName { + &THRESHOLD_CONFIG_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the approver public keys are stored. + pub fn approver_public_keys_slot() -> &'static StorageSlotName { + &APPROVER_PUBKEYS_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the executed transactions are stored. + pub fn executed_transactions_slot() -> &'static StorageSlotName { + &EXECUTED_TRANSACTIONS_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the procedure thresholds are stored. + pub fn procedure_thresholds_slot() -> &'static StorageSlotName { + &PROCEDURE_THRESHOLDS_SLOT_NAME + } } -impl From for AccountComponent { - fn from(multisig: AuthRpoFalcon512Multisig) -> Self { +impl From for AccountComponent { + fn from(multisig: AuthFalcon512RpoMultisig) -> Self { let mut storage_slots = Vec::with_capacity(3); - // Slot 0: [threshold, num_approvers, 0, 0] + // Threshold config slot (value: [threshold, num_approvers, 0, 0]) let num_approvers = multisig.config.approvers().len() as u32; - storage_slots.push(StorageSlot::Value(Word::from([ - multisig.config.default_threshold(), - num_approvers, - 0, - 0, - ]))); - - // Slot 1: A map with approver public keys + storage_slots.push(StorageSlot::with_value( + AuthFalcon512RpoMultisig::threshold_config_slot().clone(), + Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]), + )); + + // Approver public keys slot (map) let map_entries = multisig .config .approvers() @@ -128,13 +170,19 @@ impl From for AccountComponent { .map(|(i, pub_key)| (Word::from([i as u32, 0, 0, 0]), (*pub_key).into())); // Safe to unwrap because we know that the map keys are unique. - storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap())); + storage_slots.push(StorageSlot::with_map( + AuthFalcon512RpoMultisig::approver_public_keys_slot().clone(), + StorageMap::with_entries(map_entries).unwrap(), + )); - // Slot 2: A map which stores executed transactions + // Executed transactions slot (map) let executed_transactions = StorageMap::default(); - storage_slots.push(StorageSlot::Map(executed_transactions)); + storage_slots.push(StorageSlot::with_map( + AuthFalcon512RpoMultisig::executed_transactions_slot().clone(), + executed_transactions, + )); - // Slot 3: A map which stores procedure thresholds (PROC_ROOT -> threshold) + // Procedure thresholds slot (map: PROC_ROOT -> threshold) let proc_threshold_roots = StorageMap::with_entries( multisig .config @@ -143,9 +191,12 @@ impl From for AccountComponent { .map(|(proc_root, threshold)| (*proc_root, Word::from([*threshold, 0, 0, 0]))), ) .unwrap(); - storage_slots.push(StorageSlot::Map(proc_threshold_roots)); + storage_slots.push(StorageSlot::with_map( + AuthFalcon512RpoMultisig::procedure_thresholds_slot().clone(), + proc_threshold_roots, + )); - AccountComponent::new(rpo_falcon_512_multisig_library(), storage_slots) + AccountComponent::new(falcon_512_rpo_multisig_library(), storage_slots) .expect("Multisig auth component should satisfy the requirements of a valid account component") .with_supports_all_types() } @@ -155,8 +206,8 @@ impl From for AccountComponent { mod tests { use alloc::string::ToString; - use miden_objects::Word; - use miden_objects::account::AccountBuilder; + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; use super::*; use crate::account::wallets::BasicWallet; @@ -172,8 +223,8 @@ mod tests { let threshold = 2u32; // Create multisig component - let multisig_component = AuthRpoFalcon512Multisig::new( - AuthRpoFalcon512MultisigConfig::new(approvers.clone(), threshold) + let multisig_component = AuthFalcon512RpoMultisig::new( + AuthFalcon512RpoMultisigConfig::new(approvers.clone(), threshold) .expect("invalid multisig config"), ) .expect("multisig component creation failed"); @@ -185,16 +236,22 @@ mod tests { .build() .expect("account building failed"); - // Verify slot 0: [threshold, num_approvers, 0, 0] - let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed"); - assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); + // Verify config slot: [threshold, num_approvers, 0, 0] + let config_slot = account + .storage() + .get_item(AuthFalcon512RpoMultisig::threshold_config_slot()) + .expect("config storage slot access failed"); + assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); - // Verify slot 1: Approver public keys in map + // Verify approver pub keys slot for (i, expected_pub_key) in approvers.iter().enumerate() { let stored_pub_key = account .storage() - .get_map_item(1, Word::from([i as u32, 0, 0, 0])) - .expect("storage map access failed"); + .get_map_item( + AuthFalcon512RpoMultisig::approver_public_keys_slot(), + Word::from([i as u32, 0, 0, 0]), + ) + .expect("approver public key storage map access failed"); assert_eq!(stored_pub_key, Word::from(*expected_pub_key)); } } @@ -206,8 +263,8 @@ mod tests { let approvers = vec![pub_key]; let threshold = 1u32; - let multisig_component = AuthRpoFalcon512Multisig::new( - AuthRpoFalcon512MultisigConfig::new(approvers.clone(), threshold) + let multisig_component = AuthFalcon512RpoMultisig::new( + AuthFalcon512RpoMultisigConfig::new(approvers.clone(), threshold) .expect("invalid multisig config"), ) .expect("multisig component creation failed"); @@ -219,13 +276,19 @@ mod tests { .expect("account building failed"); // Verify storage layout - let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed"); - assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); + let config_slot = account + .storage() + .get_item(AuthFalcon512RpoMultisig::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(1, Word::from([0u32, 0, 0, 0])) - .expect("storage map access failed"); + .get_map_item( + AuthFalcon512RpoMultisig::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)); } @@ -236,11 +299,11 @@ mod tests { let approvers = vec![pub_key]; // Test threshold = 0 (should fail) - let result = AuthRpoFalcon512MultisigConfig::new(approvers.clone(), 0); + let result = AuthFalcon512RpoMultisigConfig::new(approvers.clone(), 0); assert!(result.unwrap_err().to_string().contains("threshold must be at least 1")); // Test threshold > number of approvers (should fail) - let result = AuthRpoFalcon512MultisigConfig::new(approvers, 2); + let result = AuthFalcon512RpoMultisigConfig::new(approvers, 2); assert!( result .unwrap_err() @@ -257,7 +320,7 @@ mod tests { // Test with duplicate approvers (should fail) let approvers = vec![pub_key_1, pub_key_2, pub_key_1]; - let result = AuthRpoFalcon512MultisigConfig::new(approvers, 2); + let result = AuthFalcon512RpoMultisigConfig::new(approvers, 2); assert!( result .unwrap_err() diff --git a/crates/miden-lib/src/account/auth/mod.rs b/crates/miden-standards/src/account/auth/mod.rs similarity index 58% rename from crates/miden-lib/src/account/auth/mod.rs rename to crates/miden-standards/src/account/auth/mod.rs index 3eaedd20dc..5d239b6cfb 100644 --- a/crates/miden-lib/src/account/auth/mod.rs +++ b/crates/miden-standards/src/account/auth/mod.rs @@ -13,11 +13,11 @@ pub use ecdsa_k256_keccak_multisig::{ AuthEcdsaK256KeccakMultisigConfig, }; -mod rpo_falcon_512; -pub use rpo_falcon_512::AuthRpoFalcon512; +mod falcon_512_rpo; +pub use falcon_512_rpo::AuthFalcon512Rpo; -mod rpo_falcon_512_acl; -pub use rpo_falcon_512_acl::{AuthRpoFalcon512Acl, AuthRpoFalcon512AclConfig}; +mod falcon_512_rpo_acl; +pub use falcon_512_rpo_acl::{AuthFalcon512RpoAcl, AuthFalcon512RpoAclConfig}; -mod rpo_falcon_512_multisig; -pub use rpo_falcon_512_multisig::{AuthRpoFalcon512Multisig, AuthRpoFalcon512MultisigConfig}; +mod falcon_512_rpo_multisig; +pub use falcon_512_rpo_multisig::{AuthFalcon512RpoMultisig, AuthFalcon512RpoMultisigConfig}; diff --git a/crates/miden-lib/src/account/auth/no_auth.rs b/crates/miden-standards/src/account/auth/no_auth.rs similarity index 94% rename from crates/miden-lib/src/account/auth/no_auth.rs rename to crates/miden-standards/src/account/auth/no_auth.rs index 21d9d2fb7e..1424ecbecb 100644 --- a/crates/miden-lib/src/account/auth/no_auth.rs +++ b/crates/miden-standards/src/account/auth/no_auth.rs @@ -1,4 +1,4 @@ -use miden_objects::account::AccountComponent; +use miden_protocol::account::AccountComponent; use crate::account::components::no_auth_library; @@ -41,7 +41,7 @@ impl From for AccountComponent { #[cfg(test)] mod tests { - use miden_objects::account::AccountBuilder; + use miden_protocol::account::AccountBuilder; use super::*; use crate::account::wallets::BasicWallet; diff --git a/crates/miden-lib/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs similarity index 50% rename from crates/miden-lib/src/account/components/mod.rs rename to crates/miden-standards/src/account/components/mod.rs index 8e6f257dcd..3b0f7e2f3f 100644 --- a/crates/miden-lib/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -1,27 +1,35 @@ -use alloc::collections::BTreeMap; +use alloc::collections::BTreeSet; use alloc::vec::Vec; -use miden_objects::Word; -use miden_objects::account::AccountProcedureInfo; -use miden_objects::assembly::Library; -use miden_objects::utils::Deserializable; -use miden_objects::utils::sync::LazyLock; use miden_processor::MastNodeExt; +use miden_protocol::Word; +use miden_protocol::account::AccountProcedureRoot; +use miden_protocol::assembly::{Library, LibraryExport}; +use miden_protocol::utils::Deserializable; +use miden_protocol::utils::sync::LazyLock; use crate::account::interface::AccountComponentInterface; +// WALLET LIBRARIES +// ================================================================================================ + // Initialize the Basic Wallet library only once. static BASIC_WALLET_LIBRARY: LazyLock = LazyLock::new(|| { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/assets/account_components/basic_wallet.masl")); + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/wallets/basic_wallet.masl" + )); Library::read_from_bytes(bytes).expect("Shipped Basic Wallet library is well-formed") }); +// AUTH LIBRARIES +// ================================================================================================ + /// Initialize the ECDSA K256 Keccak library only once. static ECDSA_K256_KECCAK_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/ecdsa_k256_keccak.masl" + "/assets/account_components/auth/ecdsa_k256_keccak.masl" )); Library::read_from_bytes(bytes).expect("Shipped Ecdsa K256 Keccak library is well-formed") }); @@ -30,7 +38,7 @@ static ECDSA_K256_KECCAK_LIBRARY: LazyLock = LazyLock::new(|| { static ECDSA_K256_KECCAK_ACL_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/ecdsa_k256_keccak_acl.masl" + "/assets/account_components/auth/ecdsa_k256_keccak_acl.masl" )); Library::read_from_bytes(bytes).expect("Shipped Ecdsa K256 Keccak ACL library is well-formed") }); @@ -39,59 +47,77 @@ static ECDSA_K256_KECCAK_ACL_LIBRARY: LazyLock = LazyLock::new(|| { static ECDSA_K256_KECCAK_MULTISIG_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/multisig_ecdsa_k256_keccak.masl" + "/assets/account_components/auth/ecdsa_k256_keccak_multisig.masl" )); Library::read_from_bytes(bytes) .expect("Shipped Multisig Ecdsa K256 Keccak library is well-formed") }); -// Initialize the Rpo Falcon 512 library only once. -static RPO_FALCON_512_LIBRARY: LazyLock = LazyLock::new(|| { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/assets/account_components/rpo_falcon_512.masl")); - Library::read_from_bytes(bytes).expect("Shipped Rpo Falcon 512 library is well-formed") -}); - -// Initialize the Basic Fungible Faucet library only once. -static BASIC_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { +// Initialize the Falcon 512 RPO library only once. +static FALCON_512_RPO_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/basic_fungible_faucet.masl" + "/assets/account_components/auth/falcon_512_rpo.masl" )); - Library::read_from_bytes(bytes).expect("Shipped Basic Fungible Faucet library is well-formed") + Library::read_from_bytes(bytes).expect("Shipped Falcon 512 RPO library is well-formed") }); -// Initialize the Network Fungible Faucet library only once. -static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { +// Initialize the Falcon 512 RPO ACL library only once. +static FALCON_512_RPO_ACL_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/network_fungible_faucet.masl" + "/assets/account_components/auth/falcon_512_rpo_acl.masl" )); - Library::read_from_bytes(bytes).expect("Shipped Network Fungible Faucet library is well-formed") + Library::read_from_bytes(bytes).expect("Shipped Falcon 512 RPO ACL library is well-formed") }); -// Initialize the Rpo Falcon 512 ACL library only once. -static RPO_FALCON_512_ACL_LIBRARY: LazyLock = LazyLock::new(|| { +// Initialize the Multisig Falcon 512 RPO library only once. +static FALCON_512_RPO_MULTISIG_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/rpo_falcon_512_acl.masl" + "/assets/account_components/auth/falcon_512_rpo_multisig.masl" )); - Library::read_from_bytes(bytes).expect("Shipped Rpo Falcon 512 ACL library is well-formed") + Library::read_from_bytes(bytes).expect("Shipped Multisig Falcon 512 RPO library is well-formed") }); // Initialize the NoAuth library only once. static NO_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/account_components/no_auth.masl")); + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/account_components/auth/no_auth.masl")); Library::read_from_bytes(bytes).expect("Shipped NoAuth library is well-formed") }); -// Initialize the Multisig Rpo Falcon 512 library only once. -static RPO_FALCON_512_MULTISIG_LIBRARY: LazyLock = LazyLock::new(|| { +// FAUCET LIBRARIES +// ================================================================================================ + +// Initialize the Basic Fungible Faucet library only once. +static BASIC_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/faucets/basic_fungible_faucet.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Basic Fungible Faucet library is well-formed") +}); + +// Initialize the Network Fungible Faucet library only once. +static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( env!("OUT_DIR"), - "/assets/account_components/multisig_rpo_falcon_512.masl" + "/assets/account_components/faucets/network_fungible_faucet.masl" )); - Library::read_from_bytes(bytes).expect("Shipped Multisig Rpo Falcon 512 library is well-formed") + Library::read_from_bytes(bytes).expect("Shipped Network Fungible Faucet library is well-formed") +}); + +// METADATA LIBRARIES +// ================================================================================================ + +// Initialize the Storage Schema library only once. +static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/metadata/schema_commitment.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed") }); /// Returns the Basic Wallet Library. @@ -109,6 +135,11 @@ pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Storage Schema Library. +pub fn storage_schema_library() -> Library { + STORAGE_SCHEMA_LIBRARY.clone() +} + /// Returns the ECDSA K256 Keccak Library. pub fn ecdsa_k256_keccak_library() -> Library { ECDSA_K256_KECCAK_LIBRARY.clone() @@ -124,14 +155,14 @@ pub fn ecdsa_k256_keccak_multisig_library() -> Library { ECDSA_K256_KECCAK_MULTISIG_LIBRARY.clone() } -/// Returns the Rpo Falcon 512 Library. -pub fn rpo_falcon_512_library() -> Library { - RPO_FALCON_512_LIBRARY.clone() +/// Returns the Falcon 512 RPO Library. +pub fn falcon_512_rpo_library() -> Library { + FALCON_512_RPO_LIBRARY.clone() } -/// Returns the Rpo Falcon 512 ACL Library. -pub fn rpo_falcon_512_acl_library() -> Library { - RPO_FALCON_512_ACL_LIBRARY.clone() +/// Returns the Falcon 512 RPO ACL Library. +pub fn falcon_512_rpo_acl_library() -> Library { + FALCON_512_RPO_ACL_LIBRARY.clone() } /// Returns the NoAuth Library. @@ -139,9 +170,9 @@ pub fn no_auth_library() -> Library { NO_AUTH_LIBRARY.clone() } -/// Returns the RPO Falcon 512 Multisig Library. -pub fn rpo_falcon_512_multisig_library() -> Library { - RPO_FALCON_512_MULTISIG_LIBRARY.clone() +/// Returns the Falcon 512 RPO Multisig Library. +pub fn falcon_512_rpo_multisig_library() -> Library { + FALCON_512_RPO_MULTISIG_LIBRARY.clone() } // WELL KNOWN COMPONENTS @@ -155,9 +186,9 @@ pub enum WellKnownComponent { AuthEcdsaK256Keccak, AuthEcdsaK256KeccakAcl, AuthEcdsaK256KeccakMultisig, - AuthRpoFalcon512, - AuthRpoFalcon512Acl, - AuthRpoFalcon512Multisig, + AuthFalcon512Rpo, + AuthFalcon512RpoAcl, + AuthFalcon512RpoMultisig, AuthNoAuth, } @@ -171,19 +202,22 @@ impl WellKnownComponent { Self::AuthEcdsaK256Keccak => ECDSA_K256_KECCAK_LIBRARY.as_ref(), Self::AuthEcdsaK256KeccakAcl => ECDSA_K256_KECCAK_ACL_LIBRARY.as_ref(), Self::AuthEcdsaK256KeccakMultisig => ECDSA_K256_KECCAK_MULTISIG_LIBRARY.as_ref(), - Self::AuthRpoFalcon512 => RPO_FALCON_512_LIBRARY.as_ref(), - Self::AuthRpoFalcon512Acl => RPO_FALCON_512_ACL_LIBRARY.as_ref(), - Self::AuthRpoFalcon512Multisig => RPO_FALCON_512_MULTISIG_LIBRARY.as_ref(), + Self::AuthFalcon512Rpo => FALCON_512_RPO_LIBRARY.as_ref(), + Self::AuthFalcon512RpoAcl => FALCON_512_RPO_ACL_LIBRARY.as_ref(), + Self::AuthFalcon512RpoMultisig => FALCON_512_RPO_MULTISIG_LIBRARY.as_ref(), Self::AuthNoAuth => NO_AUTH_LIBRARY.as_ref(), }; - library.exports().map(|export| { - library - .mast_forest() - .get_node_by_id(export.node) - .expect("export node not in the forest") - .digest() - }) + library + .exports() + .filter(|export| matches!(export, LibraryExport::Procedure(_))) + .map(|proc_export| { + library + .mast_forest() + .get_node_by_id(proc_export.unwrap_procedure().node) + .expect("export node not in the forest") + .digest() + }) } /// Checks whether procedures from the current component are present in the procedures map @@ -191,20 +225,16 @@ impl WellKnownComponent { /// interface to the component interface vector. fn extract_component( &self, - procedures_map: &mut BTreeMap, + procedures_set: &mut BTreeSet, component_interface_vec: &mut Vec, ) { // Determine if this component should be extracted based on procedure matching - if self - .procedure_digests() - .all(|proc_digest| procedures_map.contains_key(&proc_digest)) - { - // Extract the storage offset from any matching procedure - let mut storage_offset = 0u8; + if self.procedure_digests().all(|proc_digest| { + procedures_set.contains(&AccountProcedureRoot::from_raw(proc_digest)) + }) { + // Remove the procedure root of any matching procedure. self.procedure_digests().for_each(|component_procedure| { - if let Some(proc_info) = procedures_map.remove(&component_procedure) { - storage_offset = proc_info.storage_offset(); - } + procedures_set.remove(&AccountProcedureRoot::from_raw(component_procedure)); }); // Create the appropriate component interface @@ -212,22 +242,28 @@ impl WellKnownComponent { Self::BasicWallet => { component_interface_vec.push(AccountComponentInterface::BasicWallet) }, - Self::BasicFungibleFaucet => component_interface_vec - .push(AccountComponentInterface::BasicFungibleFaucet(storage_offset)), - Self::NetworkFungibleFaucet => component_interface_vec - .push(AccountComponentInterface::NetworkFungibleFaucet(storage_offset)), - Self::AuthEcdsaK256Keccak => component_interface_vec - .push(AccountComponentInterface::AuthEcdsaK256Keccak(storage_offset)), - Self::AuthEcdsaK256KeccakAcl => component_interface_vec - .push(AccountComponentInterface::AuthEcdsaK256KeccakAcl(storage_offset)), + Self::BasicFungibleFaucet => { + component_interface_vec.push(AccountComponentInterface::BasicFungibleFaucet) + }, + Self::NetworkFungibleFaucet => { + component_interface_vec.push(AccountComponentInterface::NetworkFungibleFaucet) + }, + Self::AuthEcdsaK256Keccak => { + component_interface_vec.push(AccountComponentInterface::AuthEcdsaK256Keccak) + }, + Self::AuthEcdsaK256KeccakAcl => { + component_interface_vec.push(AccountComponentInterface::AuthEcdsaK256KeccakAcl) + }, Self::AuthEcdsaK256KeccakMultisig => component_interface_vec - .push(AccountComponentInterface::AuthEcdsaK256KeccakMultisig(storage_offset)), - Self::AuthRpoFalcon512 => component_interface_vec - .push(AccountComponentInterface::AuthRpoFalcon512(storage_offset)), - Self::AuthRpoFalcon512Acl => component_interface_vec - .push(AccountComponentInterface::AuthRpoFalcon512Acl(storage_offset)), - Self::AuthRpoFalcon512Multisig => component_interface_vec - .push(AccountComponentInterface::AuthRpoFalcon512Multisig(storage_offset)), + .push(AccountComponentInterface::AuthEcdsaK256KeccakMultisig), + Self::AuthFalcon512Rpo => { + component_interface_vec.push(AccountComponentInterface::AuthFalcon512Rpo) + }, + Self::AuthFalcon512RpoAcl => { + component_interface_vec.push(AccountComponentInterface::AuthFalcon512RpoAcl) + }, + Self::AuthFalcon512RpoMultisig => component_interface_vec + .push(AccountComponentInterface::AuthFalcon512RpoMultisig), Self::AuthNoAuth => { component_interface_vec.push(AccountComponentInterface::AuthNoAuth) }, @@ -238,19 +274,19 @@ impl WellKnownComponent { /// Gets all well known components which could be constructed from the provided procedures map /// and pushes them to the `component_interface_vec`. pub fn extract_well_known_components( - procedures_map: &mut BTreeMap, + procedures_set: &mut BTreeSet, component_interface_vec: &mut Vec, ) { - Self::BasicWallet.extract_component(procedures_map, component_interface_vec); - Self::BasicFungibleFaucet.extract_component(procedures_map, component_interface_vec); - Self::NetworkFungibleFaucet.extract_component(procedures_map, component_interface_vec); - Self::AuthEcdsaK256Keccak.extract_component(procedures_map, component_interface_vec); - Self::AuthEcdsaK256KeccakAcl.extract_component(procedures_map, component_interface_vec); + Self::BasicWallet.extract_component(procedures_set, component_interface_vec); + Self::BasicFungibleFaucet.extract_component(procedures_set, component_interface_vec); + Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); + Self::AuthEcdsaK256Keccak.extract_component(procedures_set, component_interface_vec); + Self::AuthEcdsaK256KeccakAcl.extract_component(procedures_set, component_interface_vec); Self::AuthEcdsaK256KeccakMultisig - .extract_component(procedures_map, component_interface_vec); - Self::AuthRpoFalcon512.extract_component(procedures_map, component_interface_vec); - Self::AuthRpoFalcon512Acl.extract_component(procedures_map, component_interface_vec); - Self::AuthRpoFalcon512Multisig.extract_component(procedures_map, component_interface_vec); - Self::AuthNoAuth.extract_component(procedures_map, component_interface_vec); + .extract_component(procedures_set, component_interface_vec); + Self::AuthFalcon512Rpo.extract_component(procedures_set, component_interface_vec); + Self::AuthFalcon512RpoAcl.extract_component(procedures_set, component_interface_vec); + Self::AuthFalcon512RpoMultisig.extract_component(procedures_set, component_interface_vec); + Self::AuthNoAuth.extract_component(procedures_set, component_interface_vec); } } diff --git a/crates/miden-lib/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs similarity index 78% rename from crates/miden-lib/src/account/faucets/basic_fungible.rs rename to crates/miden-standards/src/account/faucets/basic_fungible.rs index 9d86cadf92..b848b43431 100644 --- a/crates/miden-lib/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -1,4 +1,4 @@ -use miden_objects::account::{ +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, @@ -6,20 +6,21 @@ use miden_objects::account::{ AccountStorageMode, AccountType, StorageSlot, + StorageSlotName, }; -use miden_objects::asset::{FungibleAsset, TokenSymbol}; -use miden_objects::{Felt, FieldElement, Word}; +use miden_protocol::asset::{FungibleAsset, TokenSymbol}; +use miden_protocol::{Felt, FieldElement, Word}; use super::FungibleFaucetError; use crate::account::AuthScheme; use crate::account::auth::{ AuthEcdsaK256KeccakAcl, AuthEcdsaK256KeccakAclConfig, - AuthRpoFalcon512Acl, - AuthRpoFalcon512AclConfig, + AuthFalcon512RpoAcl, + AuthFalcon512RpoAclConfig, }; use crate::account::components::basic_fungible_faucet_library; -use crate::account::interface::{AccountComponentInterface, AccountInterface}; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; // BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -41,10 +42,10 @@ procedure_digest!( /// An [`AccountComponent`] implementing a basic fungible faucet. /// -/// It reexports the procedures from `miden::contracts::faucets::basic_fungible`. When linking -/// against this component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be -/// available to the assembler which is the case when using -/// [`TransactionKernel::assembler()`][kasm]. The procedures of this component are: +/// It reexports the procedures from `miden::standards::faucets::basic_fungible`. When linking +/// against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. The procedures of this component are: /// - `distribute`, which mints an assets and create a note for the provided recipient. /// - `burn`, which burns the provided asset. /// @@ -55,7 +56,11 @@ procedure_digest!( /// /// This component supports accounts of type [`AccountType::FungibleFaucet`]. /// -/// [kasm]: crate::transaction::TransactionKernel::assembler +/// ## Storage Layout +/// +/// - [`Self::metadata_slot`]: Fungible faucet metadata +/// +/// [builder]: crate::code_builder::CodeBuilder pub struct BasicFungibleFaucet { symbol: TokenSymbol, decimals: u8, @@ -69,8 +74,8 @@ impl BasicFungibleFaucet { /// The maximum number of decimals supported by the component. pub const MAX_DECIMALS: u8 = 12; - const DISTRIBUTE_PROC_NAME: &str = "distribute"; - const BURN_PROC_NAME: &str = "burn"; + const DISTRIBUTE_PROC_NAME: &str = "basic_fungible_faucet::distribute"; + const BURN_PROC_NAME: &str = "basic_fungible_faucet::burn"; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -120,12 +125,13 @@ impl BasicFungibleFaucet { storage: &AccountStorage, ) -> Result { for component in interface.components().iter() { - if let AccountComponentInterface::BasicFungibleFaucet(offset) = component { - // obtain metadata from storage using offset provided by BasicFungibleFaucet - // interface + if let AccountComponentInterface::BasicFungibleFaucet = component { let faucet_metadata = storage - .get_item(*offset) - .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?; + .get_item(BasicFungibleFaucet::metadata_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: BasicFungibleFaucet::metadata_slot().clone(), + source: err, + })?; let [max_supply, decimals, token_symbol, _] = *faucet_metadata; // verify metadata values @@ -148,6 +154,11 @@ impl BasicFungibleFaucet { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- + /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored. + pub fn metadata_slot() -> &'static StorageSlotName { + &super::METADATA_SLOT_NAME + } + /// Returns the symbol of the faucet. pub fn symbol(&self) -> TokenSymbol { self.symbol @@ -184,8 +195,10 @@ impl From for AccountComponent { faucet.symbol.into(), Felt::ZERO, ]); + let storage_slot = + StorageSlot::with_value(BasicFungibleFaucet::metadata_slot().clone(), metadata); - AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)]) + AccountComponent::new(basic_fungible_faucet_library(), vec![storage_slot]) .expect("basic fungible faucet component should satisfy the requirements of a valid account component") .with_supported_type(AccountType::FungibleFaucet) } @@ -195,7 +208,7 @@ impl TryFrom for BasicFungibleFaucet { type Error = FungibleFaucetError; fn try_from(account: Account) -> Result { - let account_interface = AccountInterface::from(&account); + let account_interface = AccountInterface::from_account(&account); BasicFungibleFaucet::try_from_interface(account_interface, account.storage()) } @@ -205,7 +218,7 @@ impl TryFrom<&Account> for BasicFungibleFaucet { type Error = FungibleFaucetError; fn try_from(account: &Account) -> Result { - let account_interface = AccountInterface::from(account); + let account_interface = AccountInterface::from_account(account); BasicFungibleFaucet::try_from_interface(account_interface, account.storage()) } @@ -226,9 +239,9 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// The storage layout of the faucet account is: /// - Slot 0: Reserved slot for faucets. /// - Slot 1: Public Key of the authentication component. -/// - Slot 2: [num_tracked_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, +/// - Slot 2: [num_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, /// 0]. -/// - Slot 3: A map with tracked procedure roots. +/// - Slot 3: A map with trigger procedure roots. /// - Slot 4: Token metadata of the faucet. pub fn create_basic_fungible_faucet( init_seed: [u8; 32], @@ -241,9 +254,9 @@ pub fn create_basic_fungible_faucet( let distribute_proc_root = BasicFungibleFaucet::distribute_digest(); let auth_component: AccountComponent = match auth_scheme { - AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new( + AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512RpoAcl::new( pub_key, - AuthRpoFalcon512AclConfig::new() + AuthFalcon512RpoAclConfig::new() .with_auth_trigger_procedures(vec![distribute_proc_root]) .with_allow_unauthorized_input_notes(true), ) @@ -262,7 +275,7 @@ pub fn create_basic_fungible_faucet( "basic fungible faucets cannot be created with NoAuth authentication scheme".into(), )); }, - AuthScheme::RpoFalcon512Multisig { threshold: _, pub_keys: _ } => { + AuthScheme::Falcon512RpoMultisig { threshold: _, pub_keys: _ } => { return Err(FungibleFaucetError::UnsupportedAuthScheme( "basic fungible faucets do not support multisig authentication".into(), )); @@ -298,8 +311,9 @@ pub fn create_basic_fungible_faucet( #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_objects::account::auth::PublicKeyCommitment; - use miden_objects::{FieldElement, ONE, Word}; + use miden_protocol::account::AccountStorage; + use miden_protocol::account::auth::PublicKeyCommitment; + use miden_protocol::{FieldElement, ONE, Word}; use super::{ AccountBuilder, @@ -312,13 +326,13 @@ mod tests { TokenSymbol, create_basic_fungible_faucet, }; - use crate::account::auth::AuthRpoFalcon512; + use crate::account::auth::{AuthFalcon512Rpo, AuthFalcon512RpoAcl}; use crate::account::wallets::BasicWallet; #[test] fn faucet_contract_creation() { let pub_key_word = Word::new([ONE; 4]); - let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key: pub_key_word.into() }; + let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key: pub_key_word.into() }; // we need to use an initial seed to create the wallet account let init_seed: [u8; 32] = [ @@ -342,36 +356,50 @@ mod tests { ) .unwrap(); - // The reserved faucet slot should be initialized to an empty word. - assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::empty()); + // The faucet sysdata slot should be initialized to an empty word. + assert_eq!( + faucet_account + .storage() + .get_item(AccountStorage::faucet_sysdata_slot()) + .unwrap(), + Word::empty() + ); - // The falcon auth component is added first so its assigned storage slot for the public key - // will be 1. - assert_eq!(faucet_account.storage().get_item(1).unwrap(), pub_key_word); + // The falcon auth component's public key should be present. + assert_eq!( + faucet_account + .storage() + .get_item(AuthFalcon512RpoAcl::public_key_slot()) + .unwrap(), + pub_key_word + ); - // Slot 2 stores [num_tracked_procs, allow_unauthorized_output_notes, - // allow_unauthorized_input_notes, 0]. With 1 tracked procedure (distribute), - // allow_unauthorized_output_notes=false, and allow_unauthorized_input_notes=true, - // this should be [1, 0, 1, 0]. + // The config slot of the auth component stores: + // [num_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, 0]. + // + // With 1 trigger procedure (distribute), allow_unauthorized_output_notes=false, and + // allow_unauthorized_input_notes=true, this should be [1, 0, 1, 0]. assert_eq!( - faucet_account.storage().get_item(2).unwrap(), + faucet_account.storage().get_item(AuthFalcon512RpoAcl::config_slot()).unwrap(), [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into() ); - // The procedure root map in slot 3 should contain the distribute procedure root. + // The procedure root map should contain the distribute procedure root. let distribute_root = BasicFungibleFaucet::distribute_digest(); assert_eq!( faucet_account .storage() - .get_map_item(3, [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into()) + .get_map_item( + AuthFalcon512RpoAcl::trigger_procedure_roots_slot(), + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into() + ) .unwrap(), distribute_root ); - // Check that faucet metadata was initialized to the given values. The faucet component is - // added second, so its assigned storage slot for the metadata will be 2. + // Check that faucet metadata was initialized to the given values. assert_eq!( - faucet_account.storage().get_item(4).unwrap(), + faucet_account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(), [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into() ); @@ -401,20 +429,20 @@ mod tests { BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100)) .expect("failed to create a fungible faucet component"), ) - .with_auth_component(AuthRpoFalcon512::new(mock_public_key)) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) .build_existing() .expect("failed to create wallet account"); let basic_ff = BasicFungibleFaucet::try_from(faucet_account) .expect("basic fungible faucet creation failed"); - assert_eq!(basic_ff.symbol(), token_symbol); - assert_eq!(basic_ff.decimals(), 10); - assert_eq!(basic_ff.max_supply(), Felt::new(100)); + assert_eq!(basic_ff.symbol, token_symbol); + assert_eq!(basic_ff.decimals, 10); + assert_eq!(basic_ff.max_supply, Felt::new(100)); // invalid account: basic fungible faucet component is missing let invalid_faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) - .with_auth_component(AuthRpoFalcon512::new(mock_public_key)) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) // we need to add some other component so the builder doesn't fail .with_component(BasicWallet) .build_existing() diff --git a/crates/miden-lib/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs similarity index 70% rename from crates/miden-lib/src/account/faucets/mod.rs rename to crates/miden-standards/src/account/faucets/mod.rs index f09f361044..9733cd53ab 100644 --- a/crates/miden-lib/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -1,17 +1,22 @@ use alloc::string::String; -use miden_objects::account::{Account, AccountType}; -use miden_objects::{AccountError, Felt, TokenSymbolError}; +use miden_protocol::Felt; +use miden_protocol::account::{Account, AccountStorage, AccountType, StorageSlotName}; +use miden_protocol::errors::{AccountError, TokenSymbolError}; +use miden_protocol::utils::sync::LazyLock; use thiserror::Error; -use crate::transaction::memory::FAUCET_STORAGE_DATA_SLOT; - mod basic_fungible; mod network_fungible; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; +static METADATA_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::fungible_faucets::metadata") + .expect("storage slot name should be valid") +}); + // FUNGIBLE FAUCET // ================================================================================================ @@ -19,7 +24,6 @@ pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet /// account's reserved storage slot. pub trait FungibleFaucetExt { const ISSUANCE_ELEMENT_INDEX: usize; - const ISSUANCE_STORAGE_SLOT: u8; /// Returns the amount of tokens (in base units) issued from this fungible faucet. /// @@ -30,17 +34,19 @@ pub trait FungibleFaucetExt { impl FungibleFaucetExt for Account { const ISSUANCE_ELEMENT_INDEX: usize = 3; - const ISSUANCE_STORAGE_SLOT: u8 = FAUCET_STORAGE_DATA_SLOT; fn get_token_issuance(&self) -> Result { if self.account_type() != AccountType::FungibleFaucet { return Err(FungibleFaucetError::NotAFungibleFaucetAccount); } - let slot = self - .storage() - .get_item(Self::ISSUANCE_STORAGE_SLOT) - .map_err(|_| FungibleFaucetError::InvalidStorageOffset(Self::ISSUANCE_STORAGE_SLOT))?; + let slot = + self.storage().get_item(AccountStorage::faucet_sysdata_slot()).map_err(|err| { + FungibleFaucetError::StorageLookupFailed { + slot_name: AccountStorage::faucet_sysdata_slot().clone(), + source: err, + } + })?; Ok(slot[Self::ISSUANCE_ELEMENT_INDEX]) } } @@ -59,8 +65,11 @@ pub enum FungibleFaucetError { "account interface provided for faucet creation does not have basic fungible faucet component" )] NoAvailableInterface, - #[error("storage offset `{0}` is invalid")] - InvalidStorageOffset(u8), + #[error("failed to retrieve storage slot with name {slot_name}")] + StorageLookupFailed { + slot_name: StorageSlotName, + source: AccountError, + }, #[error("invalid token symbol")] InvalidTokenSymbol(#[source] TokenSymbolError), #[error("unsupported authentication scheme: {0}")] diff --git a/crates/miden-lib/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs similarity index 76% rename from crates/miden-lib/src/account/faucets/network_fungible.rs rename to crates/miden-standards/src/account/faucets/network_fungible.rs index 3407fd71fc..fb150d2d9d 100644 --- a/crates/miden-lib/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -1,4 +1,4 @@ -use miden_objects::account::{ +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, @@ -7,14 +7,16 @@ use miden_objects::account::{ AccountStorageMode, AccountType, StorageSlot, + StorageSlotName, }; -use miden_objects::asset::TokenSymbol; -use miden_objects::{Felt, FieldElement, Word}; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, FieldElement, Word}; use super::{BasicFungibleFaucet, FungibleFaucetError}; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; -use crate::account::interface::{AccountComponentInterface, AccountInterface}; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -34,12 +36,17 @@ procedure_digest!( network_fungible_faucet_library ); +static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::ownable::owner_config") + .expect("storage slot name should be valid") +}); + /// An [`AccountComponent`] implementing a network fungible faucet. /// -/// It reexports the procedures from `miden::contracts::faucets::network_fungible`. When linking -/// against this component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be -/// available to the assembler which is the case when using -/// [`TransactionKernel::assembler()`][kasm]. The procedures of this component are: +/// It reexports the procedures from `miden::standards::faucets::network_fungible`. When linking +/// against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. The procedures of this component are: /// - `distribute`, which mints an assets and create a note for the provided recipient. /// - `burn`, which burns the provided asset. /// @@ -47,13 +54,12 @@ procedure_digest!( /// authentication while `burn` does not require authentication and can be called by anyone. /// Thus, this component must be combined with a component providing authentication. /// -/// This component supports accounts of type [`AccountType::FungibleFaucet`]. +/// ## Storage Layout /// -/// Unlike [`super::BasicFungibleFaucet`], this component uses two storage slots: -/// - First slot: Token metadata `[max_supply, decimals, token_symbol, 0]` -/// - Second slot: Owner account ID as a single Word +/// - [`Self::metadata_slot`]: Fungible faucet metadata. +/// - [`Self::owner_config_slot`]: The owner account of this network faucet. /// -/// [kasm]: crate::transaction::TransactionKernel::assembler +/// [builder]: crate::code_builder::CodeBuilder pub struct NetworkFungibleFaucet { faucet: BasicFungibleFaucet, owner_account_id: AccountId, @@ -66,8 +72,8 @@ impl NetworkFungibleFaucet { /// The maximum number of decimals supported by the component. pub const MAX_DECIMALS: u8 = 12; - const DISTRIBUTE_PROC_NAME: &str = "distribute"; - const BURN_PROC_NAME: &str = "burn"; + const DISTRIBUTE_PROC_NAME: &str = "network_fungible_faucet::distribute"; + const BURN_PROC_NAME: &str = "network_fungible_faucet::burn"; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -78,7 +84,7 @@ impl NetworkFungibleFaucet { /// Returns an error if: /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. /// - the max supply parameter exceeds maximum possible amount for a fungible asset - /// ([`miden_objects::asset::FungibleAsset::MAX_AMOUNT`]) + /// ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]) pub fn new( symbol: TokenSymbol, decimals: u8, @@ -100,7 +106,7 @@ impl NetworkFungibleFaucet { /// [`AccountComponentInterface::NetworkFungibleFaucet`] component. /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. /// - the max supply value exceeds maximum possible amount for a fungible asset of - /// [`miden_objects::asset::FungibleAsset::MAX_AMOUNT`]. + /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. /// - the token symbol encoded value exceeds the maximum value of /// [`TokenSymbol::MAX_ENCODED_VALUE`]. fn try_from_interface( @@ -108,18 +114,24 @@ impl NetworkFungibleFaucet { storage: &AccountStorage, ) -> Result { for component in interface.components().iter() { - if let AccountComponentInterface::NetworkFungibleFaucet(offset) = component { + if let AccountComponentInterface::NetworkFungibleFaucet = component { // obtain metadata from storage using offset provided by NetworkFungibleFaucet // interface let faucet_metadata = storage - .get_item(*offset) - .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?; + .get_item(NetworkFungibleFaucet::metadata_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: NetworkFungibleFaucet::metadata_slot().clone(), + source: err, + })?; let [max_supply, decimals, token_symbol, _] = *faucet_metadata; // obtain owner account ID from the next storage slot let owner_account_id_word: Word = storage - .get_item(*offset + 1) - .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset + 1))?; + .get_item(NetworkFungibleFaucet::owner_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: NetworkFungibleFaucet::owner_config_slot().clone(), + source: err, + })?; // Convert Word back to AccountId // Storage format: [0, 0, suffix, prefix] @@ -149,6 +161,17 @@ impl NetworkFungibleFaucet { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- + /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s metadata is stored. + pub fn metadata_slot() -> &'static StorageSlotName { + &super::METADATA_SLOT_NAME + } + + /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s owner configuration is + /// stored. + pub fn owner_config_slot() -> &'static StorageSlotName { + &OWNER_CONFIG_SLOT_NAME + } + /// Returns the symbol of the faucet. pub fn symbol(&self) -> TokenSymbol { self.faucet.symbol() @@ -191,7 +214,7 @@ impl From for AccountComponent { Felt::ZERO, ]); - // Convert AccountId to Word representation for storage + // Convert AccountId into its Word encoding for storage. let owner_account_id_word: Word = [ Felt::new(0), Felt::new(0), @@ -200,12 +223,16 @@ impl From for AccountComponent { ] .into(); - // Second storage slot stores the owner account ID - let owner_slot = StorageSlot::Value(owner_account_id_word); + let metadata_slot = + StorageSlot::with_value(NetworkFungibleFaucet::metadata_slot().clone(), metadata); + let owner_slot = StorageSlot::with_value( + NetworkFungibleFaucet::owner_config_slot().clone(), + owner_account_id_word, + ); AccountComponent::new( network_fungible_faucet_library(), - vec![StorageSlot::Value(metadata), owner_slot] + vec![metadata_slot, owner_slot] ) .expect("network fungible faucet component should satisfy the requirements of a valid account component") .with_supported_type(AccountType::FungibleFaucet) @@ -216,7 +243,7 @@ impl TryFrom for NetworkFungibleFaucet { type Error = FungibleFaucetError; fn try_from(account: Account) -> Result { - let account_interface = AccountInterface::from(&account); + let account_interface = AccountInterface::from_account(&account); NetworkFungibleFaucet::try_from_interface(account_interface, account.storage()) } @@ -226,7 +253,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { type Error = FungibleFaucetError; fn try_from(account: &Account) -> Result { - let account_interface = AccountInterface::from(account); + let account_interface = AccountInterface::from_account(account); NetworkFungibleFaucet::try_from_interface(account_interface, account.storage()) } @@ -250,9 +277,9 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// The storage layout of the network faucet account is: /// - Slot 0: Reserved slot for faucets. /// - Slot 1: Public Key of the authentication component. -/// - Slot 2: [num_tracked_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, +/// - Slot 2: [num_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, /// 0]. -/// - Slot 3: A map with tracked procedure roots. +/// - Slot 3: A map with trigger procedure roots. /// - Slot 4: Token metadata of the faucet. /// - Slot 5: Owner account ID. pub fn create_network_fungible_faucet( diff --git a/crates/miden-lib/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs similarity index 52% rename from crates/miden-lib/src/account/interface/component.rs rename to crates/miden-standards/src/account/interface/component.rs index bff5b88b7a..110e23d1b3 100644 --- a/crates/miden-lib/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -1,14 +1,20 @@ -use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountId, AccountProcedureInfo, AccountStorage}; -use miden_objects::note::PartialNote; -use miden_objects::{Felt, FieldElement, Word}; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountId, AccountProcedureRoot, AccountStorage, StorageSlotName}; +use miden_protocol::note::PartialNote; +use miden_protocol::{Felt, FieldElement, Word}; use crate::AuthScheme; -use crate::account::components::WellKnownComponent; +use crate::account::auth::{ + AuthEcdsaK256Keccak, + AuthEcdsaK256KeccakAcl, + AuthEcdsaK256KeccakMultisig, + AuthFalcon512Rpo, + AuthFalcon512RpoAcl, + AuthFalcon512RpoMultisig, +}; use crate::account::interface::AccountInterfaceError; // ACCOUNT COMPONENT INTERFACE @@ -21,42 +27,28 @@ pub enum AccountComponentInterface { BasicWallet, /// Exposes procedures from the /// [`BasicFungibleFaucet`][crate::account::faucets::BasicFungibleFaucet] module. - /// - /// Internal value holds the storage slot index where faucet metadata is stored. This metadata - /// slot has a format of `[max_supply, faucet_decimals, token_symbol, 0]`. - BasicFungibleFaucet(u8), + BasicFungibleFaucet, /// Exposes procedures from the /// [`NetworkFungibleFaucet`][crate::account::faucets::NetworkFungibleFaucet] module. - /// - /// Internal value holds the storage slot index where faucet metadata is stored. This metadata - /// slot has a format of `[max_supply, faucet_decimals, token_symbol, 0]`. - NetworkFungibleFaucet(u8), + NetworkFungibleFaucet, /// Exposes procedures from the - /// [`AuthRpoFalcon512`][crate::account::auth::AuthRpoFalcon512] module. - /// - /// Internal value holds the storage slot index where the public key for the EcdsaK256Keccak - /// authentication scheme is stored. - AuthEcdsaK256Keccak(u8), - /// Internal value holds the storage slot index where the public key for the EcdsaK256Keccak - /// authentication scheme is stored. - AuthEcdsaK256KeccakAcl(u8), - /// Internal value holds the storage slot index where the multisig for EcdsaK256Keccak - /// configuration is stored. - AuthEcdsaK256KeccakMultisig(u8), - /// - /// Internal value holds the storage slot index where the public key for the RpoFalcon512 - /// authentication scheme is stored. - AuthRpoFalcon512(u8), + /// [`AuthEcdsaK256Keccak`][crate::account::auth::AuthEcdsaK256Keccak] module. + AuthEcdsaK256Keccak, /// Exposes procedures from the - /// [`AuthRpoFalcon512Acl`][crate::account::auth::AuthRpoFalcon512Acl] module. - /// - /// Internal value holds the storage slot index where the public key for the RpoFalcon512 - /// authentication scheme is stored. - AuthRpoFalcon512Acl(u8), - /// Exposes procedures from the multisig RpoFalcon512 authentication module. - /// - /// Internal value holds the storage slot index where the multisig configuration is stored. - AuthRpoFalcon512Multisig(u8), + /// [`AuthEcdsaK256KeccakAcl`][crate::account::auth::AuthEcdsaK256KeccakAcl] module. + AuthEcdsaK256KeccakAcl, + /// Exposes procedures from the + /// [`AuthEcdsaK256KeccakMultisig`][crate::account::auth::AuthEcdsaK256KeccakMultisig] module. + AuthEcdsaK256KeccakMultisig, + /// Exposes procedures from the + /// [`AuthFalcon512Rpo`][crate::account::auth::AuthFalcon512Rpo] module. + AuthFalcon512Rpo, + /// Exposes procedures from the + /// [`AuthFalcon512RpoAcl`][crate::account::auth::AuthFalcon512RpoAcl] module. + AuthFalcon512RpoAcl, + /// Exposes procedures from the + /// [`AuthFalcon512RpoMultisig`][crate::account::auth::AuthFalcon512RpoMultisig] module. + AuthFalcon512RpoMultisig, /// Exposes procedures from the [`NoAuth`][crate::account::auth::NoAuth] module. /// /// This authentication scheme provides no cryptographic authentication and only increments @@ -64,9 +56,9 @@ pub enum AccountComponentInterface { AuthNoAuth, /// A non-standard, custom interface which exposes the contained procedures. /// - /// Custom interface holds procedures which are not part of some standard interface which is - /// used by this account. Each custom interface holds procedures with the same storage offset. - Custom(Vec), + /// Custom interface holds all procedures which are not part of some standard interface which is + /// used by this account. + Custom(Vec), } impl AccountComponentInterface { @@ -78,30 +70,28 @@ impl AccountComponentInterface { pub fn name(&self) -> String { match self { AccountComponentInterface::BasicWallet => "Basic Wallet".to_string(), - AccountComponentInterface::BasicFungibleFaucet(_) => { - "Basic Fungible Faucet".to_string() - }, - AccountComponentInterface::NetworkFungibleFaucet(_) => { + AccountComponentInterface::BasicFungibleFaucet => "Basic Fungible Faucet".to_string(), + AccountComponentInterface::NetworkFungibleFaucet => { "Network Fungible Faucet".to_string() }, - AccountComponentInterface::AuthEcdsaK256Keccak(_) => "ECDSA K256 Keccak".to_string(), - AccountComponentInterface::AuthEcdsaK256KeccakAcl(_) => { + AccountComponentInterface::AuthEcdsaK256Keccak => "ECDSA K256 Keccak".to_string(), + AccountComponentInterface::AuthEcdsaK256KeccakAcl => { "ECDSA K256 Keccak ACL".to_string() }, - AccountComponentInterface::AuthEcdsaK256KeccakMultisig(_) => { + AccountComponentInterface::AuthEcdsaK256KeccakMultisig => { "ECDSA K256 Keccak Multisig".to_string() }, - AccountComponentInterface::AuthRpoFalcon512(_) => "RPO Falcon512".to_string(), - AccountComponentInterface::AuthRpoFalcon512Acl(_) => "RPO Falcon512 ACL".to_string(), - AccountComponentInterface::AuthRpoFalcon512Multisig(_) => { - "RPO Falcon512 Multisig".to_string() + AccountComponentInterface::AuthFalcon512Rpo => "Falcon512 RPO".to_string(), + AccountComponentInterface::AuthFalcon512RpoAcl => "Falcon512 RPO ACL".to_string(), + AccountComponentInterface::AuthFalcon512RpoMultisig => { + "Falcon512 RPO Multisig".to_string() }, AccountComponentInterface::AuthNoAuth => "No Auth".to_string(), - AccountComponentInterface::Custom(proc_info_vec) => { - let result = proc_info_vec + AccountComponentInterface::Custom(proc_root_vec) => { + let result = proc_root_vec .iter() - .map(|proc_info| proc_info.mast_root().to_hex()[..9].to_string()) + .map(|proc_root| proc_root.mast_root().to_hex()[..9].to_string()) .collect::>() .join(", "); format!("Custom([{result}])") @@ -115,12 +105,12 @@ impl AccountComponentInterface { pub fn is_auth_component(&self) -> bool { matches!( self, - AccountComponentInterface::AuthEcdsaK256Keccak(_) - | AccountComponentInterface::AuthEcdsaK256KeccakAcl(_) - | AccountComponentInterface::AuthEcdsaK256KeccakMultisig(_) - | AccountComponentInterface::AuthRpoFalcon512(_) - | AccountComponentInterface::AuthRpoFalcon512Acl(_) - | AccountComponentInterface::AuthRpoFalcon512Multisig(_) + AccountComponentInterface::AuthEcdsaK256Keccak + | AccountComponentInterface::AuthEcdsaK256KeccakAcl + | AccountComponentInterface::AuthEcdsaK256KeccakMultisig + | AccountComponentInterface::AuthFalcon512Rpo + | AccountComponentInterface::AuthFalcon512RpoAcl + | AccountComponentInterface::AuthFalcon512RpoMultisig | AccountComponentInterface::AuthNoAuth ) } @@ -128,79 +118,61 @@ impl AccountComponentInterface { /// Returns the authentication schemes associated with this component interface. pub fn get_auth_schemes(&self, storage: &AccountStorage) -> Vec { match self { - AccountComponentInterface::AuthEcdsaK256Keccak(storage_index) - | AccountComponentInterface::AuthEcdsaK256KeccakAcl(storage_index) => { + AccountComponentInterface::AuthEcdsaK256Keccak => { vec![AuthScheme::EcdsaK256Keccak { pub_key: PublicKeyCommitment::from( storage - .get_item(*storage_index) + .get_item(AuthEcdsaK256Keccak::public_key_slot()) .expect("invalid storage index of the public key"), ), }] }, - AccountComponentInterface::AuthEcdsaK256KeccakMultisig(storage_index) => { - vec![extract_multisig_auth_scheme(storage, *storage_index)] - }, - AccountComponentInterface::AuthRpoFalcon512(storage_index) - | AccountComponentInterface::AuthRpoFalcon512Acl(storage_index) => { - vec![AuthScheme::RpoFalcon512 { + AccountComponentInterface::AuthEcdsaK256KeccakAcl => { + vec![AuthScheme::EcdsaK256Keccak { pub_key: PublicKeyCommitment::from( storage - .get_item(*storage_index) + .get_item(AuthEcdsaK256KeccakAcl::public_key_slot()) .expect("invalid storage index of the public key"), ), }] }, - AccountComponentInterface::AuthRpoFalcon512Multisig(storage_index) => { - vec![extract_multisig_auth_scheme(storage, *storage_index)] + AccountComponentInterface::AuthEcdsaK256KeccakMultisig => { + vec![extract_multisig_auth_scheme( + storage, + AuthEcdsaK256KeccakMultisig::threshold_config_slot(), + AuthEcdsaK256KeccakMultisig::approver_public_keys_slot(), + )] + }, + AccountComponentInterface::AuthFalcon512Rpo => { + vec![AuthScheme::Falcon512Rpo { + pub_key: PublicKeyCommitment::from( + storage + .get_item(AuthFalcon512Rpo::public_key_slot()) + .expect("invalid slot name of the AuthFalcon512Rpo public key"), + ), + }] + }, + AccountComponentInterface::AuthFalcon512RpoAcl => { + vec![AuthScheme::Falcon512Rpo { + pub_key: PublicKeyCommitment::from( + storage + .get_item(AuthFalcon512RpoAcl::public_key_slot()) + .expect("invalid slot name of the AuthFalcon512RpoAcl public key"), + ), + }] + }, + AccountComponentInterface::AuthFalcon512RpoMultisig => { + vec![extract_multisig_auth_scheme( + storage, + AuthFalcon512RpoMultisig::threshold_config_slot(), + AuthFalcon512RpoMultisig::approver_public_keys_slot(), + )] }, AccountComponentInterface::AuthNoAuth => vec![AuthScheme::NoAuth], _ => vec![], // Non-auth components return empty vector } } - /// Creates a vector of [AccountComponentInterface] instances. This vector specifies the - /// components which were used to create an account with the provided procedures info array. - pub fn from_procedures(procedures: &[AccountProcedureInfo]) -> Vec { - let mut component_interface_vec = Vec::new(); - - let mut procedures: BTreeMap<_, _> = procedures - .iter() - .map(|procedure_info| (*procedure_info.mast_root(), procedure_info)) - .collect(); - - // Well known component interfaces - // ---------------------------------------------------------------------------------------- - - // Get all available well known components which could be constructed from the `procedures` - // map and push them to the `component_interface_vec` - WellKnownComponent::extract_well_known_components( - &mut procedures, - &mut component_interface_vec, - ); - - // Custom component interfaces - // ---------------------------------------------------------------------------------------- - - let mut custom_interface_procs_map = BTreeMap::>::new(); - procedures.into_iter().for_each(|(_, proc_info)| { - match custom_interface_procs_map.get_mut(&proc_info.storage_offset()) { - Some(proc_vec) => proc_vec.push(*proc_info), - None => { - custom_interface_procs_map.insert(proc_info.storage_offset(), vec![*proc_info]); - }, - } - }); - - if !custom_interface_procs_map.is_empty() { - for proc_vec in custom_interface_procs_map.into_values() { - component_interface_vec.push(AccountComponentInterface::Custom(proc_vec)); - } - } - - component_interface_vec - } - /// Generates a body for the note creation of the `send_note` transaction script. The resulting /// code could use different procedures for note creation, which depends on the used interface. /// @@ -217,10 +189,10 @@ impl AccountComponentInterface { /// /// ```masm /// push.{note_information} - /// call.::miden::output_note::create + /// call.::miden::protocol::output_note::create /// /// push.{note asset} - /// call.::miden::contracts::wallets::basic::move_asset_to_note dropw + /// call.::miden::standards::wallets::basic::move_asset_to_note dropw /// dropw dropw dropw drop /// ``` /// @@ -230,7 +202,7 @@ impl AccountComponentInterface { /// push.{note information} /// /// push.{asset amount} - /// call.::miden::contracts::faucets::basic_fungible::distribute dropw dropw drop + /// call.::miden::standards::faucets::basic_fungible::distribute dropw dropw drop /// ``` /// /// # Errors: @@ -254,21 +226,19 @@ impl AccountComponentInterface { } body.push_str(&format!( - "push.{recipient} - push.{execution_hint} + " + push.{recipient} push.{note_type} - push.{aux} - push.{tag}\n", + push.{tag} + # => [tag, note_type, RECIPIENT, pad(16)] + ", recipient = partial_note.recipient_digest(), note_type = Felt::from(partial_note.metadata().note_type()), - execution_hint = Felt::from(partial_note.metadata().execution_hint()), - aux = partial_note.metadata().aux(), tag = Felt::from(partial_note.metadata().tag()), )); - // stack => [tag, aux, note_type, execution_hint, RECIPIENT] match self { - AccountComponentInterface::BasicFungibleFaucet(_) => { + AccountComponentInterface::BasicFungibleFaucet => { if partial_note.assets().num_assets() != 1 { return Err(AccountInterfaceError::FaucetNoteWithoutAsset); } @@ -284,27 +254,36 @@ impl AccountComponentInterface { } body.push_str(&format!( - "push.{amount} - call.::miden::contracts::faucets::basic_fungible::distribute dropw dropw drop\n", + " + push.{amount} + call.::miden::standards::faucets::basic_fungible::distribute + # => [note_idx, pad(25)] + swapdw dropw dropw swap drop + # => [note_idx, pad(16)]\n + ", amount = asset.unwrap_fungible().amount() )); - // stack => [] }, AccountComponentInterface::BasicWallet => { - body.push_str("call.::miden::output_note::create\n"); - // stack => [note_idx] + body.push_str( + " + exec.::miden::protocol::output_note::create + # => [note_idx, pad(16)]\n + ", + ); for asset in partial_note.assets().iter() { body.push_str(&format!( - "push.{asset} - call.::miden::contracts::wallets::basic::move_asset_to_note dropw\n", + " + push.{asset} + # => [ASSET, note_idx, pad(16)] + call.::miden::standards::wallets::basic::move_asset_to_note + dropw + # => [note_idx, pad(16)]\n + ", asset = Word::from(*asset) )); - // stack => [note_idx] } - - body.push_str("dropw dropw dropw drop\n"); - // stack => [] }, _ => { return Err(AccountInterfaceError::UnsupportedInterface { @@ -312,6 +291,22 @@ impl AccountComponentInterface { }); }, } + + body.push_str(&format!( + " + push.{ATTACHMENT} + push.{attachment_kind} + push.{attachment_scheme} + movup.6 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(16)] + exec.::miden::protocol::output_note::set_attachment + # => [pad(16)] + ", + ATTACHMENT = partial_note.metadata().to_attachment_word(), + attachment_scheme = + partial_note.metadata().attachment().attachment_scheme().as_u32(), + attachment_kind = partial_note.metadata().attachment().attachment_kind().as_u8(), + )); } Ok(body) @@ -322,23 +317,20 @@ impl AccountComponentInterface { // ================================================================================================ /// Extracts authentication scheme from a multisig component. -fn extract_multisig_auth_scheme(storage: &AccountStorage, storage_index: u8) -> AuthScheme { +fn extract_multisig_auth_scheme( + storage: &AccountStorage, + config_slot: &StorageSlotName, + approver_public_keys_slot: &StorageSlotName, +) -> AuthScheme { // Read the multisig configuration from the config slot // Format: [threshold, num_approvers, 0, 0] let config = storage - .get_item(storage_index) - .expect("invalid storage index of the multisig configuration"); + .get_item(config_slot) + .expect("invalid slot name of the multisig configuration"); let threshold = config[0].as_int() as u32; let num_approvers = config[1].as_int() as u8; - // The multisig component has a fixed storage layout: - // - Slot 0: [threshold, num_approvers, 0, 0] - // - Slot 1: Map with public keys - // - Slot 2: Map with executed transactions - // The public keys are always stored in slot 1, regardless of storage_index - let pub_keys_map_slot = storage_index + 1; - let mut pub_keys = Vec::new(); // Read each public key from the map @@ -346,7 +338,7 @@ fn extract_multisig_auth_scheme(storage: &AccountStorage, storage_index: u8) -> // The multisig component stores keys using pattern [index, 0, 0, 0] let map_key = [Felt::new(key_index as u64), Felt::ZERO, Felt::ZERO, Felt::ZERO]; - match storage.get_map_item(pub_keys_map_slot, map_key.into()) { + match storage.get_map_item(approver_public_keys_slot, map_key.into()) { Ok(pub_key) => { pub_keys.push(PublicKeyCommitment::from(pub_key)); }, @@ -356,11 +348,11 @@ fn extract_multisig_auth_scheme(storage: &AccountStorage, storage_index: u8) -> "Failed to read public key {} from multisig configuration at storage slot {}. \ Expected key pattern [index, 0, 0, 0]. \ This indicates corrupted multisig storage or incorrect storage layout.", - key_index, pub_keys_map_slot + key_index, approver_public_keys_slot ); }, } } - AuthScheme::RpoFalcon512Multisig { threshold, pub_keys } + AuthScheme::Falcon512RpoMultisig { threshold, pub_keys } } diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs new file mode 100644 index 0000000000..e563408c33 --- /dev/null +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -0,0 +1,261 @@ +use alloc::collections::BTreeSet; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_processor::MastNodeExt; +use miden_protocol::Word; +use miden_protocol::account::{Account, AccountCode, AccountId, AccountProcedureRoot}; +use miden_protocol::assembly::mast::{MastForest, MastNode, MastNodeId}; +use miden_protocol::note::{Note, NoteScript}; + +use crate::AuthScheme; +use crate::account::components::{ + WellKnownComponent, + basic_fungible_faucet_library, + basic_wallet_library, + ecdsa_k256_keccak_acl_library, + ecdsa_k256_keccak_library, + ecdsa_k256_keccak_multisig_library, + falcon_512_rpo_acl_library, + falcon_512_rpo_library, + falcon_512_rpo_multisig_library, + network_fungible_faucet_library, + no_auth_library, +}; +use crate::account::interface::{ + AccountComponentInterface, + AccountInterface, + NoteAccountCompatibility, +}; +use crate::note::WellKnownNote; + +// ACCOUNT INTERFACE EXTENSION TRAIT +// ================================================================================================ + +/// An extension for [`AccountInterface`] that allows instantiation from higher-level types. +pub trait AccountInterfaceExt { + /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication + /// schemes and account code. + fn from_code(account_id: AccountId, auth: Vec, code: &AccountCode) -> Self; + + /// Creates a new [`AccountInterface`] instance from the provided [`Account`]. + fn from_account(account: &Account) -> Self; + + /// Returns [NoteAccountCompatibility::Maybe] if the provided note is compatible with the + /// current [AccountInterface], and [NoteAccountCompatibility::No] otherwise. + fn is_compatible_with(&self, note: &Note) -> NoteAccountCompatibility; + + /// Returns the set of digests of all procedures from all account component interfaces. + fn get_procedure_digests(&self) -> BTreeSet; +} + +impl AccountInterfaceExt for AccountInterface { + fn from_code(account_id: AccountId, auth: Vec, code: &AccountCode) -> Self { + let components = AccountComponentInterface::from_procedures(code.procedures()); + + Self::new(account_id, auth, components) + } + + fn from_account(account: &Account) -> Self { + let components = AccountComponentInterface::from_procedures(account.code().procedures()); + let mut auth = Vec::new(); + + // Find the auth component and extract all auth schemes from it + // An account should have only one auth component + for component in components.iter() { + if component.is_auth_component() { + auth = component.get_auth_schemes(account.storage()); + break; + } + } + + Self::new(account.id(), auth, components) + } + + /// Returns [NoteAccountCompatibility::Maybe] if the provided note is compatible with the + /// current [AccountInterface], and [NoteAccountCompatibility::No] otherwise. + fn is_compatible_with(&self, note: &Note) -> NoteAccountCompatibility { + if let Some(well_known_note) = WellKnownNote::from_note(note) { + if well_known_note.is_compatible_with(self) { + NoteAccountCompatibility::Maybe + } else { + NoteAccountCompatibility::No + } + } else { + verify_note_script_compatibility(note.script(), self.get_procedure_digests()) + } + } + + fn get_procedure_digests(&self) -> BTreeSet { + let mut component_proc_digests = BTreeSet::new(); + for component in self.components.iter() { + match component { + AccountComponentInterface::BasicWallet => { + component_proc_digests + .extend(basic_wallet_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::BasicFungibleFaucet => { + component_proc_digests + .extend(basic_fungible_faucet_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::NetworkFungibleFaucet => { + component_proc_digests.extend( + network_fungible_faucet_library().mast_forest().procedure_digests(), + ); + }, + AccountComponentInterface::AuthEcdsaK256Keccak => { + component_proc_digests + .extend(ecdsa_k256_keccak_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::AuthEcdsaK256KeccakAcl => { + component_proc_digests + .extend(ecdsa_k256_keccak_acl_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::AuthEcdsaK256KeccakMultisig => { + component_proc_digests.extend( + ecdsa_k256_keccak_multisig_library().mast_forest().procedure_digests(), + ); + }, + AccountComponentInterface::AuthFalcon512Rpo => { + component_proc_digests + .extend(falcon_512_rpo_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::AuthFalcon512RpoAcl => { + component_proc_digests + .extend(falcon_512_rpo_acl_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::AuthFalcon512RpoMultisig => { + component_proc_digests.extend( + falcon_512_rpo_multisig_library().mast_forest().procedure_digests(), + ); + }, + AccountComponentInterface::AuthNoAuth => { + component_proc_digests + .extend(no_auth_library().mast_forest().procedure_digests()); + }, + AccountComponentInterface::Custom(custom_procs) => { + component_proc_digests + .extend(custom_procs.iter().map(|info| *info.mast_root())); + }, + } + } + + component_proc_digests + } +} + +/// An extension for [`AccountComponentInterface`] that allows instantiation from a set of procedure +/// roots. +pub trait AccountComponentInterfaceExt { + /// Creates a vector of [`AccountComponentInterface`] instances from the provided set of + /// procedures. + fn from_procedures(procedures: &[AccountProcedureRoot]) -> Vec; +} + +impl AccountComponentInterfaceExt for AccountComponentInterface { + fn from_procedures(procedures: &[AccountProcedureRoot]) -> Vec { + let mut component_interface_vec = Vec::new(); + + let mut procedures = BTreeSet::from_iter(procedures.iter().copied()); + + // Well known component interfaces + // ---------------------------------------------------------------------------------------- + + // Get all available well known components which could be constructed from the + // `procedures` map and push them to the `component_interface_vec` + WellKnownComponent::extract_well_known_components( + &mut procedures, + &mut component_interface_vec, + ); + + // Custom component interfaces + // ---------------------------------------------------------------------------------------- + + // All remaining procedures are put into the custom bucket. + component_interface_vec + .push(AccountComponentInterface::Custom(procedures.into_iter().collect())); + + component_interface_vec + } +} + +// HELPER FUNCTIONS +// ------------------------------------------------------------------------------------------------ + +/// Verifies that the provided note script is compatible with the target account interfaces. +/// +/// This is achieved by checking that at least one execution branch in the note script is compatible +/// with the account procedures vector. +/// +/// This check relies on the fact that account procedures are the only procedures that are `call`ed +/// from note scripts, while kernel procedures are `sycall`ed. +fn verify_note_script_compatibility( + note_script: &NoteScript, + account_procedures: BTreeSet, +) -> NoteAccountCompatibility { + // collect call branches of the note script + let branches = collect_call_branches(note_script); + + // if none of the branches are compatible with the target account, return a `CheckResult::No` + if !branches.iter().any(|call_targets| call_targets.is_subset(&account_procedures)) { + return NoteAccountCompatibility::No; + } + + NoteAccountCompatibility::Maybe +} + +/// Collect call branches by recursively traversing through program execution branches and +/// accumulating call targets. +fn collect_call_branches(note_script: &NoteScript) -> Vec> { + let mut branches = vec![BTreeSet::new()]; + + let entry_node = note_script.entrypoint(); + recursively_collect_call_branches(entry_node, &mut branches, ¬e_script.mast()); + branches +} + +/// Generates a list of calls invoked in each execution branch of the provided code block. +fn recursively_collect_call_branches( + mast_node_id: MastNodeId, + branches: &mut Vec>, + note_script_forest: &Arc, +) { + let mast_node = ¬e_script_forest[mast_node_id]; + + match mast_node { + MastNode::Block(_) => {}, + MastNode::Join(join_node) => { + recursively_collect_call_branches(join_node.first(), branches, note_script_forest); + recursively_collect_call_branches(join_node.second(), branches, note_script_forest); + }, + MastNode::Split(split_node) => { + let current_branch = branches.last().expect("at least one execution branch").clone(); + recursively_collect_call_branches(split_node.on_false(), branches, note_script_forest); + + // If the previous branch had additional calls we need to create a new branch + if branches.last().expect("at least one execution branch").len() > current_branch.len() + { + branches.push(current_branch); + } + + recursively_collect_call_branches(split_node.on_true(), branches, note_script_forest); + }, + MastNode::Loop(loop_node) => { + recursively_collect_call_branches(loop_node.body(), branches, note_script_forest); + }, + MastNode::Call(call_node) => { + if call_node.is_syscall() { + return; + } + + let callee_digest = note_script_forest[call_node.callee()].digest(); + + branches + .last_mut() + .expect("at least one execution branch") + .insert(callee_digest); + }, + MastNode::Dyn(_) => {}, + MastNode::External(_) => {}, + } +} diff --git a/crates/miden-lib/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs similarity index 50% rename from crates/miden-lib/src/account/interface/mod.rs rename to crates/miden-standards/src/account/interface/mod.rs index 48193fcef9..cdc967759a 100644 --- a/crates/miden-lib/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -1,32 +1,14 @@ -use alloc::collections::BTreeSet; use alloc::string::String; -use alloc::sync::Arc; use alloc::vec::Vec; -use miden_objects::Word; -use miden_objects::account::{Account, AccountCode, AccountId, AccountIdPrefix, AccountType}; -use miden_objects::assembly::mast::{MastForest, MastNode, MastNodeId}; -use miden_objects::note::{Note, NoteScript, PartialNote}; -use miden_objects::transaction::TransactionScript; -use miden_processor::MastNodeExt; +use miden_protocol::account::{AccountId, AccountIdPrefix, AccountType}; +use miden_protocol::note::PartialNote; +use miden_protocol::transaction::TransactionScript; use thiserror::Error; use crate::AuthScheme; -use crate::account::components::{ - basic_fungible_faucet_library, - basic_wallet_library, - ecdsa_k256_keccak_acl_library, - ecdsa_k256_keccak_library, - ecdsa_k256_keccak_multisig_library, - network_fungible_faucet_library, - no_auth_library, - rpo_falcon_512_acl_library, - rpo_falcon_512_library, - rpo_falcon_512_multisig_library, -}; -use crate::errors::ScriptBuilderError; -use crate::note::WellKnownNote; -use crate::utils::ScriptBuilder; +use crate::code_builder::CodeBuilder; +use crate::errors::CodeBuilderError; #[cfg(test)] mod test; @@ -34,6 +16,9 @@ mod test; mod component; pub use component::AccountComponentInterface; +mod extension; +pub use extension::{AccountComponentInterfaceExt, AccountInterfaceExt}; + // ACCOUNT INTERFACE // ================================================================================================ @@ -54,10 +39,12 @@ impl AccountInterface { // -------------------------------------------------------------------------------------------- /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication - /// schemes and account code. - pub fn new(account_id: AccountId, auth: Vec, code: &AccountCode) -> Self { - let components = AccountComponentInterface::from_procedures(code.procedures()); - + /// schemes and account component interfaces. + pub fn new( + account_id: AccountId, + auth: Vec, + components: Vec, + ) -> Self { Self { account_id, auth, components } } @@ -85,8 +72,8 @@ impl AccountInterface { } /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are - /// [`AccountStorageMode::Public`](miden_objects::account::AccountStorageMode::Public) or - /// [`AccountStorageMode::Network`](miden_objects::account::AccountStorageMode::Network), + /// [`AccountStorageMode::Public`](miden_protocol::account::AccountStorageMode::Public) or + /// [`AccountStorageMode::Network`](miden_protocol::account::AccountStorageMode::Network), /// `false` otherwise. pub fn has_public_state(&self) -> bool { self.account_id.has_public_state() @@ -116,78 +103,6 @@ impl AccountInterface { pub fn components(&self) -> &Vec { &self.components } - - /// Returns [NoteAccountCompatibility::Maybe] if the provided note is compatible with the - /// current [AccountInterface], and [NoteAccountCompatibility::No] otherwise. - pub fn is_compatible_with(&self, note: &Note) -> NoteAccountCompatibility { - if let Some(well_known_note) = WellKnownNote::from_note(note) { - if well_known_note.is_compatible_with(self) { - NoteAccountCompatibility::Maybe - } else { - NoteAccountCompatibility::No - } - } else { - verify_note_script_compatibility(note.script(), self.get_procedure_digests()) - } - } - - /// Returns a digests set of all procedures from all account component interfaces. - pub(crate) fn get_procedure_digests(&self) -> BTreeSet { - let mut component_proc_digests = BTreeSet::new(); - for component in self.components.iter() { - match component { - AccountComponentInterface::BasicWallet => { - component_proc_digests - .extend(basic_wallet_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::BasicFungibleFaucet(_) => { - component_proc_digests - .extend(basic_fungible_faucet_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::NetworkFungibleFaucet(_) => { - component_proc_digests.extend( - network_fungible_faucet_library().mast_forest().procedure_digests(), - ); - }, - AccountComponentInterface::AuthEcdsaK256Keccak(_) => { - component_proc_digests - .extend(ecdsa_k256_keccak_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::AuthEcdsaK256KeccakAcl(_) => { - component_proc_digests - .extend(ecdsa_k256_keccak_acl_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::AuthEcdsaK256KeccakMultisig(_) => { - component_proc_digests.extend( - ecdsa_k256_keccak_multisig_library().mast_forest().procedure_digests(), - ); - }, - AccountComponentInterface::AuthRpoFalcon512(_) => { - component_proc_digests - .extend(rpo_falcon_512_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::AuthRpoFalcon512Acl(_) => { - component_proc_digests - .extend(rpo_falcon_512_acl_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::AuthRpoFalcon512Multisig(_) => { - component_proc_digests.extend( - rpo_falcon_512_multisig_library().mast_forest().procedure_digests(), - ); - }, - AccountComponentInterface::AuthNoAuth => { - component_proc_digests - .extend(no_auth_library().mast_forest().procedure_digests()); - }, - AccountComponentInterface::Custom(custom_procs) => { - component_proc_digests - .extend(custom_procs.iter().map(|info| *info.mast_root())); - }, - } - } - - component_proc_digests - } } // ------------------------------------------------------------------------------------------------ @@ -214,12 +129,12 @@ impl AccountInterface { /// /// ```masm /// begin - /// push.{expiration_delta} exec.::miden::tx::update_expiration_block_delta + /// push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta /// /// push.{note information} /// /// push.{asset amount} - /// call.::miden::contracts::faucets::basic_fungible::distribute dropw dropw drop + /// call.::miden::standards::faucets::basic_fungible::distribute dropw dropw drop /// end /// ``` /// @@ -237,7 +152,6 @@ impl AccountInterface { &self, output_notes: &[PartialNote], expiration_delta: Option, - in_debug_mode: bool, ) -> Result { let note_creation_source = self.build_create_notes_section(output_notes)?; @@ -247,7 +161,7 @@ impl AccountInterface { note_creation_source, ); - let tx_script = ScriptBuilder::new(in_debug_mode) + let tx_script = CodeBuilder::new() .compile_tx_script(script) .map_err(AccountInterfaceError::InvalidTransactionScript)?; @@ -271,12 +185,12 @@ impl AccountInterface { output_notes: &[PartialNote], ) -> Result { if let Some(basic_fungible_faucet) = self.components().iter().find(|component_interface| { - matches!(component_interface, AccountComponentInterface::BasicFungibleFaucet(_)) + matches!(component_interface, AccountComponentInterface::BasicFungibleFaucet) }) { basic_fungible_faucet.send_note_body(*self.id(), output_notes) } else if let Some(_network_fungible_faucet) = self.components().iter().find(|component_interface| { - matches!(component_interface, AccountComponentInterface::NetworkFungibleFaucet(_)) + matches!(component_interface, AccountComponentInterface::NetworkFungibleFaucet) }) { // Network fungible faucet doesn't support send_note_body, because minting @@ -292,35 +206,15 @@ impl AccountInterface { /// Returns a string with the expiration delta update procedure call for the script. fn build_set_tx_expiration_section(&self, expiration_delta: Option) -> String { if let Some(expiration_delta) = expiration_delta { - format!("push.{expiration_delta} exec.::miden::tx::update_expiration_block_delta\n") + format!( + "push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta\n" + ) } else { String::new() } } } -impl From<&Account> for AccountInterface { - fn from(account: &Account) -> Self { - let components = AccountComponentInterface::from_procedures(account.code().procedures()); - let mut auth = Vec::new(); - - // Find the auth component and extract all auth schemes from it - // An account should have only one auth component - for component in components.iter() { - if component.is_auth_component() { - auth = component.get_auth_schemes(account.storage()); - break; - } - } - - Self { - account_id: account.id(), - auth, - components, - } - } -} - // NOTE ACCOUNT COMPATIBILITY // ================================================================================================ @@ -339,87 +233,6 @@ pub enum NoteAccountCompatibility { Yes, } -// HELPER FUNCTIONS -// ------------------------------------------------------------------------------------------------ - -/// Verifies that the provided note script is compatible with the target account interfaces. -/// -/// This is achieved by checking that at least one execution branch in the note script is compatible -/// with the account procedures vector. -/// -/// This check relies on the fact that account procedures are the only procedures that are `call`ed -/// from note scripts, while kernel procedures are `sycall`ed. -fn verify_note_script_compatibility( - note_script: &NoteScript, - account_procedures: BTreeSet, -) -> NoteAccountCompatibility { - // collect call branches of the note script - let branches = collect_call_branches(note_script); - - // if none of the branches are compatible with the target account, return a `CheckResult::No` - if !branches.iter().any(|call_targets| call_targets.is_subset(&account_procedures)) { - return NoteAccountCompatibility::No; - } - - NoteAccountCompatibility::Maybe -} - -/// Collect call branches by recursively traversing through program execution branches and -/// accumulating call targets. -fn collect_call_branches(note_script: &NoteScript) -> Vec> { - let mut branches = vec![BTreeSet::new()]; - - let entry_node = note_script.entrypoint(); - recursively_collect_call_branches(entry_node, &mut branches, ¬e_script.mast()); - branches -} - -/// Generates a list of calls invoked in each execution branch of the provided code block. -fn recursively_collect_call_branches( - mast_node_id: MastNodeId, - branches: &mut Vec>, - note_script_forest: &Arc, -) { - let mast_node = ¬e_script_forest[mast_node_id]; - - match mast_node { - MastNode::Block(_) => {}, - MastNode::Join(join_node) => { - recursively_collect_call_branches(join_node.first(), branches, note_script_forest); - recursively_collect_call_branches(join_node.second(), branches, note_script_forest); - }, - MastNode::Split(split_node) => { - let current_branch = branches.last().expect("at least one execution branch").clone(); - recursively_collect_call_branches(split_node.on_false(), branches, note_script_forest); - - // If the previous branch had additional calls we need to create a new branch - if branches.last().expect("at least one execution branch").len() > current_branch.len() - { - branches.push(current_branch); - } - - recursively_collect_call_branches(split_node.on_true(), branches, note_script_forest); - }, - MastNode::Loop(loop_node) => { - recursively_collect_call_branches(loop_node.body(), branches, note_script_forest); - }, - MastNode::Call(call_node) => { - if call_node.is_syscall() { - return; - } - - let callee_digest = note_script_forest[call_node.callee()].digest(); - - branches - .last_mut() - .expect("at least one execution branch") - .insert(callee_digest); - }, - MastNode::Dyn(_) => {}, - MastNode::External(_) => {}, - } -} - // ACCOUNT INTERFACE ERROR // ============================================================================================ @@ -431,7 +244,7 @@ pub enum AccountInterfaceError { #[error("note created by the basic fungible faucet doesn't contain exactly one asset")] FaucetNoteWithoutAsset, #[error("invalid transaction script")] - InvalidTransactionScript(#[source] ScriptBuilderError), + InvalidTransactionScript(#[source] CodeBuilderError), #[error("invalid sender account: {0}")] InvalidSenderAccount(AccountId), #[error("{} interface does not support the generation of the standard send_note script", interface.name())] diff --git a/crates/miden-lib/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs similarity index 75% rename from crates/miden-lib/src/account/interface/test.rs rename to crates/miden-standards/src/account/interface/test.rs index 1f89b366d4..e6639b32f0 100644 --- a/crates/miden-lib/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -1,49 +1,44 @@ -use alloc::string::ToString; -use alloc::sync::Arc; -use alloc::vec::Vec; - use assert_matches::assert_matches; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountBuilder, AccountComponent, AccountType, StorageSlot}; -use miden_objects::assembly::diagnostics::NamedSource; -use miden_objects::assembly::{Assembler, DefaultSourceManager}; -use miden_objects::asset::{FungibleAsset, NonFungibleAsset, TokenSymbol}; -use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin}; -use miden_objects::note::{ +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountBuilder, AccountComponent, AccountType}; +use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, TokenSymbol}; +use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, + NoteAttachment, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, }; -use miden_objects::{AccountError, Felt, NoteError, Word, ZERO}; +use miden_protocol::{Felt, Word}; use crate::AuthScheme; use crate::account::auth::{ AuthEcdsaK256Keccak, - AuthRpoFalcon512, - AuthRpoFalcon512Multisig, - AuthRpoFalcon512MultisigConfig, + AuthFalcon512Rpo, + AuthFalcon512RpoMultisig, + AuthFalcon512RpoMultisigConfig, NoAuth, }; use crate::account::faucets::BasicFungibleFaucet; use crate::account::interface::{ AccountComponentInterface, AccountInterface, + AccountInterfaceExt, NoteAccountCompatibility, }; use crate::account::wallets::BasicWallet; +use crate::code_builder::CodeBuilder; use crate::note::{create_p2id_note, create_p2ide_note, create_swap_note}; use crate::testing::account_interface::get_public_keys_from_account; -use crate::transaction::TransactionKernel; -use crate::utils::ScriptBuilder; // DEFAULT NOTES // ================================================================================================ @@ -58,7 +53,7 @@ fn test_basic_wallet_default_notes() { .build_existing() .expect("failed to create wallet account"); - let wallet_account_interface = AccountInterface::from(&wallet_account); + let wallet_account_interface = AccountInterface::from_account(&wallet_account); let mock_seed = Word::from([Felt::new(4), Felt::new(5), Felt::new(6), Felt::new(7)]).as_bytes(); let faucet_account = AccountBuilder::new(mock_seed) @@ -74,7 +69,7 @@ fn test_basic_wallet_default_notes() { ) .build_existing() .expect("failed to create wallet account"); - let faucet_account_interface = AccountInterface::from(&faucet_account); + let faucet_account_interface = AccountInterface::from_account(&faucet_account); let p2id_note = create_p2id_note( ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), @@ -106,9 +101,9 @@ fn test_basic_wallet_default_notes() { offered_asset, requested_asset, NoteType::Public, - ZERO, + NoteAttachment::default(), NoteType::Public, - ZERO, + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), ) .unwrap(); @@ -150,18 +145,16 @@ fn test_basic_wallet_default_notes() { #[test] fn test_custom_account_default_note() { let account_custom_code_source = " - use.miden::contracts::wallets::basic + use miden::standards::wallets::basic - export.basic::receive_asset + pub use basic::receive_asset "; - let account_component = AccountComponent::compile( - account_custom_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - vec![], - ) - .unwrap() - .with_supports_all_types(); + let account_code = CodeBuilder::default() + .compile_component_code("test::account_custom", account_custom_code_source) + .unwrap(); + let account_component = + AccountComponent::new(account_code, vec![]).unwrap().with_supports_all_types(); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let target_account = AccountBuilder::new(mock_seed) @@ -169,7 +162,7 @@ fn test_custom_account_default_note() { .with_component(account_component.clone()) .build_existing() .unwrap(); - let target_account_interface = AccountInterface::from(&target_account); + let target_account_interface = AccountInterface::from_account(&target_account); let p2id_note = create_p2id_note( ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), @@ -201,9 +194,9 @@ fn test_custom_account_default_note() { offered_asset, requested_asset, NoteType::Public, - ZERO, + NoteAttachment::default(), NoteType::Public, - ZERO, + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), ) .unwrap(); @@ -234,9 +227,9 @@ fn test_required_asset_same_as_offered() { offered_asset, requested_asset, NoteType::Public, - ZERO, + NoteAttachment::default(), NoteType::Public, - ZERO, + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), ); @@ -255,25 +248,18 @@ fn test_basic_wallet_custom_notes() { .with_assets(vec![FungibleAsset::mock(20)]) .build_existing() .expect("failed to create wallet account"); - let wallet_account_interface = AccountInterface::from(&wallet_account); + let wallet_account_interface = AccountInterface::from_account(&wallet_account); let sender_account_id = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into().unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(wallet_account.id()); - let metadata = NoteMetadata::new( - sender_account_id, - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - ) - .unwrap(); + let tag = NoteTag::with_account_target(wallet_account.id()); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, tag); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " - use.miden::tx - use.miden::contracts::wallets::basic->wallet - use.miden::contracts::faucets::basic_fungible->fungible_faucet + use miden::protocol::tx + use miden::standards::wallets::basic->wallet + use miden::standards::faucets::basic_fungible->fungible_faucet begin push.1 @@ -292,17 +278,17 @@ fn test_basic_wallet_custom_notes() { end end "; - let note_script = ScriptBuilder::default().compile_note_script(compatible_source_code).unwrap(); + let note_script = CodeBuilder::default().compile_note_script(compatible_source_code).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); assert_eq!( NoteAccountCompatibility::Maybe, wallet_account_interface.is_compatible_with(&compatible_custom_note) ); let incompatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.miden::contracts::faucets::basic_fungible->fungible_faucet + use miden::standards::wallets::basic->wallet + use miden::standards::faucets::basic_fungible->fungible_faucet begin push.1 @@ -320,8 +306,7 @@ fn test_basic_wallet_custom_notes() { end end "; - let note_script = - ScriptBuilder::default().compile_note_script(incompatible_source_code).unwrap(); + let note_script = CodeBuilder::default().compile_note_script(incompatible_source_code).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); let incompatible_custom_note = Note::new(vault, metadata, recipient); assert_eq!( @@ -346,24 +331,17 @@ fn test_basic_fungible_faucet_custom_notes() { ) .build_existing() .expect("failed to create wallet account"); - let faucet_account_interface = AccountInterface::from(&faucet_account); + let faucet_account_interface = AccountInterface::from_account(&faucet_account); let sender_account_id = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into().unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(faucet_account.id()); - let metadata = NoteMetadata::new( - sender_account_id, - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - ) - .unwrap(); + let tag = NoteTag::with_account_target(faucet_account.id()); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, tag); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.miden::contracts::faucets::basic_fungible->fungible_faucet + use miden::standards::wallets::basic->wallet + use miden::standards::faucets::basic_fungible->fungible_faucet begin push.1 @@ -381,17 +359,17 @@ fn test_basic_fungible_faucet_custom_notes() { end end "; - let note_script = ScriptBuilder::default().compile_note_script(compatible_source_code).unwrap(); + let note_script = CodeBuilder::default().compile_note_script(compatible_source_code).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); assert_eq!( NoteAccountCompatibility::Maybe, faucet_account_interface.is_compatible_with(&compatible_custom_note) ); let incompatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.miden::contracts::faucets::basic_fungible->fungible_faucet + use miden::standards::wallets::basic->wallet + use miden::standards::faucets::basic_fungible->fungible_faucet begin push.1 @@ -411,8 +389,7 @@ fn test_basic_fungible_faucet_custom_notes() { end end "; - let note_script = - ScriptBuilder::default().compile_note_script(incompatible_source_code).unwrap(); + let note_script = CodeBuilder::default().compile_note_script(incompatible_source_code).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); let incompatible_custom_note = Note::new(vault, metadata, recipient); assert_eq!( @@ -429,23 +406,20 @@ fn test_basic_fungible_faucet_custom_notes() { #[test] fn test_custom_account_custom_notes() { let account_custom_code_source = " - export.procedure_1 + pub proc procedure_1 push.1.2.3.4 dropw end - export.procedure_2 + pub proc procedure_2 push.5.6.7.8 dropw end "; - let account_component = AccountComponent::compile_with_path( - account_custom_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - vec![], - "test::account::component_1", - ) - .unwrap() - .with_supports_all_types(); + let account_code = CodeBuilder::default() + .compile_component_code("test::account::component_1", account_custom_code_source) + .unwrap(); + let account_component = + AccountComponent::new(account_code, vec![]).unwrap().with_supports_all_types(); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let target_account = AccountBuilder::new(mock_seed) @@ -453,7 +427,7 @@ fn test_custom_account_custom_notes() { .with_component(account_component.clone()) .build_existing() .unwrap(); - let target_account_interface = AccountInterface::from(&target_account); + let target_account_interface = AccountInterface::from_account(&target_account); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let sender_account = AccountBuilder::new(mock_seed) @@ -464,20 +438,13 @@ fn test_custom_account_custom_notes() { .expect("failed to create wallet account"); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(target_account.id()); - let metadata = NoteMetadata::new( - sender_account.id(), - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - ) - .unwrap(); + let tag = NoteTag::with_account_target(target_account.id()); + let metadata = NoteMetadata::new(sender_account.id(), NoteType::Public, tag); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.test::account::component_1->test_account + use miden::standards::wallets::basic->wallet + use test::account::component_1->test_account begin push.1 @@ -494,21 +461,21 @@ fn test_custom_account_custom_notes() { end end "; - let note_script = ScriptBuilder::default() - .with_dynamically_linked_library(account_component.library()) + let note_script = CodeBuilder::default() + .with_dynamically_linked_library(account_component.component_code()) .unwrap() .compile_note_script(compatible_source_code) .unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); assert_eq!( NoteAccountCompatibility::Maybe, target_account_interface.is_compatible_with(&compatible_custom_note) ); let incompatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.test::account::component_1->test_account + use miden::standards::wallets::basic->wallet + use test::account::component_1->test_account begin push.1 @@ -521,8 +488,8 @@ fn test_custom_account_custom_notes() { end end "; - let note_script = ScriptBuilder::default() - .with_dynamically_linked_library(account_component.library()) + let note_script = CodeBuilder::default() + .with_dynamically_linked_library(account_component.component_code()) .unwrap() .compile_note_script(incompatible_source_code) .unwrap(); @@ -542,23 +509,20 @@ fn test_custom_account_custom_notes() { #[test] fn test_custom_account_multiple_components_custom_notes() { let account_custom_code_source = " - export.procedure_1 + pub proc procedure_1 push.1.2.3.4 dropw end - export.procedure_2 + pub proc procedure_2 push.5.6.7.8 dropw end "; - let custom_component = AccountComponent::compile_with_path( - account_custom_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - vec![], - "test::account::component_1", - ) - .unwrap() - .with_supports_all_types(); + let custom_code = CodeBuilder::default() + .compile_component_code("test::account::component_1", account_custom_code_source) + .unwrap(); + let custom_component = + AccountComponent::new(custom_code, vec![]).unwrap().with_supports_all_types(); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let target_account = AccountBuilder::new(mock_seed) @@ -567,7 +531,7 @@ fn test_custom_account_multiple_components_custom_notes() { .with_component(BasicWallet) .build_existing() .unwrap(); - let target_account_interface = AccountInterface::from(&target_account); + let target_account_interface = AccountInterface::from_account(&target_account); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let sender_account = AccountBuilder::new(mock_seed) @@ -578,22 +542,14 @@ fn test_custom_account_multiple_components_custom_notes() { .expect("failed to create wallet account"); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(target_account.id()); - let metadata = NoteMetadata::new( - sender_account.id(), - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - ) - .unwrap(); + let tag = NoteTag::with_account_target(target_account.id()); + let metadata = NoteMetadata::new(sender_account.id(), NoteType::Public, tag); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.miden::contracts::auth::basic->basic_auth - use.test::account::component_1->test_account - use.miden::contracts::faucets::basic_fungible->fungible_faucet + use miden::standards::wallets::basic->wallet + use test::account::component_1->test_account + use miden::standards::faucets::basic_fungible->fungible_faucet begin push.1 @@ -615,23 +571,22 @@ fn test_custom_account_multiple_components_custom_notes() { end end "; - let note_script = ScriptBuilder::default() - .with_dynamically_linked_library(custom_component.library()) + let note_script = CodeBuilder::default() + .with_dynamically_linked_library(custom_component.component_code()) .unwrap() .compile_note_script(compatible_source_code) .unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); - let compatible_custom_note = Note::new(vault.clone(), metadata, recipient); + let compatible_custom_note = Note::new(vault.clone(), metadata.clone(), recipient); assert_eq!( NoteAccountCompatibility::Maybe, target_account_interface.is_compatible_with(&compatible_custom_note) ); let incompatible_source_code = " - use.miden::contracts::wallets::basic->wallet - use.miden::contracts::auth::basic->basic_auth - use.test::account::component_1->test_account - use.miden::contracts::faucets::basic_fungible->fungible_faucet + use miden::standards::wallets::basic->wallet + use test::account::component_1->test_account + use miden::standards::faucets::basic_fungible->fungible_faucet begin push.1 @@ -654,8 +609,8 @@ fn test_custom_account_multiple_components_custom_notes() { end end "; - let note_script = ScriptBuilder::default() - .with_dynamically_linked_library(custom_component.library()) + let note_script = CodeBuilder::default() + .with_dynamically_linked_library(custom_component.component_code()) .unwrap() .compile_note_script(incompatible_source_code) .unwrap(); @@ -667,53 +622,14 @@ fn test_custom_account_multiple_components_custom_notes() { ); } -// HELPER TRAIT +// HELPERS // ================================================================================================ -/// [AccountComponentExt] is a helper trait which only implements the `compile_with_path` procedure -/// for testing purposes. -trait AccountComponentExt { - fn compile_with_path( - source_code: impl ToString, - assembler: Assembler, - storage_slots: Vec, - library_path: impl AsRef, - ) -> Result; -} - -impl AccountComponentExt for AccountComponent { - /// Returns a new [`AccountComponent`] whose library is compiled from the provided `source_code` - /// using the specified `assembler`, `library_path`, and with the given `storage_slots`. - /// - /// All procedures exported from the provided code will become members of the account's public - /// interface when added to an [`AccountCode`](crate::account::AccountCode), and could be called - /// using the provided library path. - /// - /// # Errors - /// - /// Returns an error if: - /// - the compilation of the provided source code fails. - /// - The number of storage slots exceeds 255. - fn compile_with_path( - source_code: impl ToString, - assembler: Assembler, - storage_slots: Vec, - library_path: impl AsRef, - ) -> Result { - let source = NamedSource::new(library_path, source_code.to_string()); - let library = assembler - .assemble_library([source]) - .map_err(AccountError::AccountComponentAssemblyError)?; - - Self::new(library, storage_slots) - } -} - /// Helper function to create a mock auth component for testing -fn get_mock_auth_component() -> AuthRpoFalcon512 { +fn get_mock_auth_component() -> AuthFalcon512Rpo { let mock_word = Word::from([0, 1, 2, 3u32]); let mock_public_key = PublicKeyCommitment::from(mock_word); - AuthRpoFalcon512::new(mock_public_key) + AuthFalcon512Rpo::new(mock_public_key) } /// Helper function to create a mock Ecdsa auth component for testing @@ -735,13 +651,13 @@ fn test_get_auth_scheme_ecdsa_k256_keccak() { .build_existing() .expect("failed to create wallet account"); - let wallet_account_interface = AccountInterface::from(&wallet_account); + let wallet_account_interface = AccountInterface::from_account(&wallet_account); // Find the EcdsaK256Keccak component interface let ecdsa_k256_keccak_component = wallet_account_interface .components() .iter() - .find(|component| matches!(component, AccountComponentInterface::AuthEcdsaK256Keccak(_))) + .find(|component| matches!(component, AccountComponentInterface::AuthEcdsaK256Keccak)) .expect("should have EcdsaK256Keccak component"); // Test get_auth_schemes method @@ -757,7 +673,7 @@ fn test_get_auth_scheme_ecdsa_k256_keccak() { } #[test] -fn test_get_auth_scheme_rpo_falcon512() { +fn test_get_auth_scheme_falcon512_rpo() { let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let wallet_account = AccountBuilder::new(mock_seed) .with_auth_component(get_mock_auth_component()) @@ -765,24 +681,24 @@ fn test_get_auth_scheme_rpo_falcon512() { .build_existing() .expect("failed to create wallet account"); - let wallet_account_interface = AccountInterface::from(&wallet_account); + let wallet_account_interface = AccountInterface::from_account(&wallet_account); - // Find the RpoFalcon512 component interface + // Find the Falcon512Rpo component interface let rpo_falcon_component = wallet_account_interface .components() .iter() - .find(|component| matches!(component, AccountComponentInterface::AuthRpoFalcon512(_))) - .expect("should have RpoFalcon512 component"); + .find(|component| matches!(component, AccountComponentInterface::AuthFalcon512Rpo)) + .expect("should have Falcon512Rpo component"); // Test get_auth_schemes method let auth_schemes = rpo_falcon_component.get_auth_schemes(wallet_account.storage()); assert_eq!(auth_schemes.len(), 1); let auth_scheme = &auth_schemes[0]; match auth_scheme { - AuthScheme::RpoFalcon512 { pub_key } => { + AuthScheme::Falcon512Rpo { pub_key } => { assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); }, - _ => panic!("Expected RpoFalcon512 auth scheme"), + _ => panic!("Expected Falcon512Rpo auth scheme"), } } @@ -795,7 +711,7 @@ fn test_get_auth_scheme_no_auth() { .build_existing() .expect("failed to create no-auth account"); - let no_auth_account_interface = AccountInterface::from(&no_auth_account); + let no_auth_account_interface = AccountInterface::from_account(&no_auth_account); // Find the NoAuth component interface let no_auth_component = no_auth_account_interface @@ -839,17 +755,17 @@ fn test_account_interface_from_account_uses_get_auth_scheme() { .build_existing() .expect("failed to create wallet account"); - let wallet_account_interface = AccountInterface::from(&wallet_account); + let wallet_account_interface = AccountInterface::from_account(&wallet_account); // Should have exactly one auth scheme assert_eq!(wallet_account_interface.auth().len(), 1); match &wallet_account_interface.auth()[0] { - AuthScheme::RpoFalcon512 { pub_key } => { + AuthScheme::Falcon512Rpo { pub_key } => { let expected_pub_key = PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32])); assert_eq!(*pub_key, expected_pub_key); }, - _ => panic!("Expected RpoFalcon512 auth scheme"), + _ => panic!("Expected Falcon512Rpo auth scheme"), } // Test with NoAuth @@ -859,7 +775,7 @@ fn test_account_interface_from_account_uses_get_auth_scheme() { .build_existing() .expect("failed to create no-auth account"); - let no_auth_account_interface = AccountInterface::from(&no_auth_account); + let no_auth_account_interface = AccountInterface::from_account(&no_auth_account); // Should have exactly one auth scheme assert_eq!(no_auth_account_interface.auth().len(), 1); @@ -870,7 +786,7 @@ fn test_account_interface_from_account_uses_get_auth_scheme() { } } -/// Test AccountInterface.get_auth_scheme() method with RpoFalcon512 and NoAuth +/// Test AccountInterface.get_auth_scheme() method with Falcon512Rpo and NoAuth #[test] fn test_account_interface_get_auth_scheme() { let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); @@ -880,15 +796,15 @@ fn test_account_interface_get_auth_scheme() { .build_existing() .expect("failed to create wallet account"); - let wallet_account_interface = AccountInterface::from(&wallet_account); + let wallet_account_interface = AccountInterface::from_account(&wallet_account); // Test that auth() method provides the authentication schemes assert_eq!(wallet_account_interface.auth().len(), 1); match &wallet_account_interface.auth()[0] { - AuthScheme::RpoFalcon512 { pub_key } => { + AuthScheme::Falcon512Rpo { pub_key } => { assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); }, - _ => panic!("Expected RpoFalcon512 auth scheme"), + _ => panic!("Expected Falcon512Rpo auth scheme"), } // Test AccountInterface.get_auth_scheme() method with NoAuth @@ -898,7 +814,7 @@ fn test_account_interface_get_auth_scheme() { .build_existing() .expect("failed to create no-auth account"); - let no_auth_account_interface = AccountInterface::from(&no_auth_account); + let no_auth_account_interface = AccountInterface::from_account(&no_auth_account); // Test that auth() method provides the authentication schemes assert_eq!(no_auth_account_interface.auth().len(), 1); @@ -937,8 +853,8 @@ fn test_public_key_extraction_multisig_account() { let threshold = 2u32; // Create multisig component - let multisig_component = AuthRpoFalcon512Multisig::new( - AuthRpoFalcon512MultisigConfig::new(approvers.clone(), threshold).unwrap(), + let multisig_component = AuthFalcon512RpoMultisig::new( + AuthFalcon512RpoMultisigConfig::new(approvers.clone(), threshold).unwrap(), ) .expect("multisig component creation failed"); diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs new file mode 100644 index 0000000000..0d5e24a3b5 --- /dev/null +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -0,0 +1,183 @@ +use alloc::collections::BTreeMap; + +use miden_protocol::Word; +use miden_protocol::account::component::StorageSchema; +use miden_protocol::account::{AccountComponent, StorageSlot, StorageSlotName}; +use miden_protocol::errors::AccountComponentTemplateError; +use miden_protocol::utils::sync::LazyLock; + +use crate::account::components::storage_schema_library; + +pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::storage_schema") + .expect("storage slot name should be valid") +}); + +/// An [`AccountComponent`] exposing the account storage schema commitment. +/// +/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`], +/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`] +/// slot. +/// +/// It reexports the `get_schema_commitment` procedure from +/// `miden::standards::metadata::storage_schema`. +/// +/// ## Storage Layout +/// +/// - [`Self::schema_commitment_slot`]: Storage schema commitment. +pub struct AccountSchemaCommitment { + schema_commitment: Word, +} + +impl AccountSchemaCommitment { + /// Creates a new [`AccountSchemaCommitment`] component from a list of storage schemas. + /// + /// The input schemas are merged into a single schema before the final commitment is computed. + /// + /// # Errors + /// + /// Returns an error if the schemas contain conflicting definitions for the same slot name. + pub fn new(schemas: &[StorageSchema]) -> Result { + Ok(Self { + schema_commitment: compute_schema_commitment(schemas)?, + }) + } + + /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`]. + pub fn from_schema( + storage_schema: &StorageSchema, + ) -> Result { + Self::new(core::slice::from_ref(storage_schema)) + } + + /// Returns the [`StorageSlotName`] where the schema commitment is stored. + pub fn schema_commitment_slot() -> &'static StorageSlotName { + &SCHEMA_COMMITMENT_SLOT_NAME + } +} + +impl From for AccountComponent { + fn from(schema_commitment: AccountSchemaCommitment) -> Self { + AccountComponent::new( + storage_schema_library(), + vec![StorageSlot::with_value( + AccountSchemaCommitment::schema_commitment_slot().clone(), + schema_commitment.schema_commitment, + )], + ) + .expect( + "AccountSchemaCommitment component should satisfy the requirements of a valid account component", + ) + .with_supports_all_types() + } +} + +/// Computes the schema commitment. +/// +/// The account schema commitment is computed from the merged schema commitment. +/// If the passed list of schemas is empty, [`Word::empty()`] is returned. +fn compute_schema_commitment( + schemas: &[StorageSchema], +) -> Result { + if schemas.is_empty() { + return Ok(Word::empty()); + } + + let mut merged_slots = BTreeMap::new(); + for schema in schemas { + for (slot_name, slot_schema) in schema.iter() { + match merged_slots.get(slot_name) { + None => { + merged_slots.insert(slot_name.clone(), slot_schema.clone()); + }, + // Slot exists, check if the schema is the same before erroring + Some(existing) => { + if existing != slot_schema { + return Err(AccountComponentTemplateError::InvalidSchema(format!( + "conflicting definitions for storage slot `{slot_name}`", + ))); + } + }, + } + } + } + + let merged_schema = StorageSchema::new(merged_slots)?; + + Ok(merged_schema.commitment()) +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; + use miden_protocol::account::component::AccountComponentMetadata; + + use super::AccountSchemaCommitment; + use crate::account::auth::NoAuth; + + #[test] + fn storage_schema_commitment_is_order_independent() { + let toml_a = r#" + name = "Component A" + description = "Component A schema" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "test::slot_a" + type = "word" + "#; + + let toml_b = r#" + name = "Component B" + description = "Component B schema" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "test::slot_b" + description = "description is committed to" + type = "word" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); + + let schema_a = metadata_a.storage_schema().clone(); + let schema_b = metadata_b.storage_schema().clone(); + + // Create one component for each of two different accounts, but switch orderings + let component_a = + AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap(); + let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap(); + + let account_a = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(component_a) + .build() + .unwrap(); + + let account_b = AccountBuilder::new([2u8; 32]) + .with_auth_component(NoAuth) + .with_component(component_b) + .build() + .unwrap(); + + let slot_name = AccountSchemaCommitment::schema_commitment_slot(); + let commitment_a = account_a.storage().get_item(slot_name).unwrap(); + let commitment_b = account_b.storage().get_item(slot_name).unwrap(); + + assert_eq!(commitment_a, commitment_b); + } + + #[test] + fn storage_schema_commitment_is_empty_for_no_schemas() { + let component = AccountSchemaCommitment::new(&[]).unwrap(); + + assert_eq!(component.schema_commitment, Word::empty()); + } +} diff --git a/crates/miden-lib/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs similarity index 55% rename from crates/miden-lib/src/account/mod.rs rename to crates/miden-standards/src/account/mod.rs index 78afc9f8dd..af3d4ff69b 100644 --- a/crates/miden-lib/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -1,9 +1,10 @@ -use super::auth::AuthScheme; +use super::auth_scheme::AuthScheme; pub mod auth; pub mod components; pub mod faucets; pub mod interface; +pub mod metadata; pub mod wallets; /// Macro to simplify the creation of static procedure digest constants. @@ -11,8 +12,8 @@ pub mod wallets; /// This macro generates a `LazyLock` static variable that lazily initializes /// the digest of a procedure from a library. /// -/// Note: This macro references exported types from `miden_objects`, so your crate must -/// include `miden-objects` as a dependency. +/// Note: This macro references exported types from `miden_protocol`, so your crate must +/// include `miden_protocol` as a dependency. /// /// # Arguments /// * `$name` - The name of the static variable to create @@ -30,15 +31,9 @@ pub mod wallets; #[macro_export] macro_rules! procedure_digest { ($name:ident, $proc_name:expr, $library_fn:expr) => { - static $name: miden_objects::utils::sync::LazyLock = - miden_objects::utils::sync::LazyLock::new(|| { - let qualified_name = miden_objects::assembly::QualifiedProcedureName::new( - ::core::default::Default::default(), - miden_objects::assembly::ProcedureName::new($proc_name).unwrap_or_else(|_| { - panic!("failed to create name for '{}' procedure", $proc_name) - }), - ); - $library_fn().get_procedure_root_by_name(qualified_name).unwrap_or_else(|| { + static $name: miden_protocol::utils::sync::LazyLock = + miden_protocol::utils::sync::LazyLock::new(|| { + $library_fn().get_procedure_root_by_path($proc_name).unwrap_or_else(|| { panic!("{} should contain '{}' procedure", stringify!($library_fn), $proc_name) }) }); diff --git a/crates/miden-lib/src/account/wallets/mod.rs b/crates/miden-standards/src/account/wallets/mod.rs similarity index 86% rename from crates/miden-lib/src/account/wallets/mod.rs rename to crates/miden-standards/src/account/wallets/mod.rs index 6bd874271b..23e9204322 100644 --- a/crates/miden-lib/src/account/wallets/mod.rs +++ b/crates/miden-standards/src/account/wallets/mod.rs @@ -1,13 +1,14 @@ use alloc::string::String; -use miden_objects::account::{ +use miden_protocol::Word; +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, AccountStorageMode, AccountType, }; -use miden_objects::{AccountError, Word}; +use miden_protocol::errors::AccountError; use thiserror::Error; use super::AuthScheme; @@ -15,9 +16,9 @@ use crate::account::auth::{ AuthEcdsaK256Keccak, AuthEcdsaK256KeccakMultisig, AuthEcdsaK256KeccakMultisigConfig, - AuthRpoFalcon512, - AuthRpoFalcon512Multisig, - AuthRpoFalcon512MultisigConfig, + AuthFalcon512Rpo, + AuthFalcon512RpoMultisig, + AuthFalcon512RpoMultisigConfig, }; use crate::account::components::basic_wallet_library; use crate::procedure_digest; @@ -41,9 +42,9 @@ procedure_digest!( /// An [`AccountComponent`] implementing a basic wallet. /// -/// It reexports the procedures from `miden::contracts::wallets::basic`. When linking against this -/// component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be available to the -/// assembler which is the case when using [`TransactionKernel::assembler()`][kasm]. The procedures +/// It reexports the procedures from `miden::standards::wallets::basic`. When linking against this +/// component, the `miden` library (i.e. [`ProtocolLib`](miden_protocol::ProtocolLib)) must be +/// available to the assembler which is the case when using [`CodeBuilder`][builder]. The procedures /// of this component are: /// - `receive_asset`, which can be used to add an asset to the account. /// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add @@ -54,14 +55,14 @@ procedure_digest!( /// /// This component supports all account types. /// -/// [kasm]: crate::transaction::TransactionKernel::assembler +/// [builder]: crate::code_builder::CodeBuilder pub struct BasicWallet; impl BasicWallet { // CONSTANTS // -------------------------------------------------------------------------------------------- - const RECEIVE_ASSET_PROC_NAME: &str = "receive_asset"; - const MOVE_ASSET_TO_NOTE_PROC_NAME: &str = "move_asset_to_note"; + const RECEIVE_ASSET_PROC_NAME: &str = "basic_wallet::receive_asset"; + const MOVE_ASSET_TO_NOTE_PROC_NAME: &str = "basic_wallet::move_asset_to_note"; // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -131,14 +132,14 @@ pub fn create_basic_wallet( .map_err(BasicWalletError::AccountError)? .into() }, - AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512::new(pub_key).into(), - AuthScheme::RpoFalcon512Multisig { threshold, pub_keys } => { - let config = AuthRpoFalcon512MultisigConfig::new(pub_keys, threshold) + AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512Rpo::new(pub_key).into(), + AuthScheme::Falcon512RpoMultisig { threshold, pub_keys } => { + let config = AuthFalcon512RpoMultisigConfig::new(pub_keys, threshold) .and_then(|cfg| { cfg.with_proc_thresholds(vec![(BasicWallet::receive_asset_digest(), 1)]) }) .map_err(BasicWalletError::AccountError)?; - AuthRpoFalcon512Multisig::new(config) + AuthFalcon512RpoMultisig::new(config) .map_err(BasicWalletError::AccountError)? .into() }, @@ -170,9 +171,9 @@ pub fn create_basic_wallet( #[cfg(test)] mod tests { - use miden_objects::account::auth::PublicKeyCommitment; - use miden_objects::{ONE, Word}; use miden_processor::utils::{Deserializable, Serializable}; + use miden_protocol::account::auth::PublicKeyCommitment; + use miden_protocol::{ONE, Word}; use super::{Account, AccountStorageMode, AccountType, AuthScheme, create_basic_wallet}; use crate::account::wallets::BasicWallet; @@ -182,7 +183,7 @@ mod tests { let pub_key = PublicKeyCommitment::from(Word::from([ONE; 4])); let wallet = create_basic_wallet( [1; 32], - AuthScheme::RpoFalcon512 { pub_key }, + AuthScheme::Falcon512Rpo { pub_key }, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, ); @@ -197,7 +198,7 @@ mod tests { let pub_key = PublicKeyCommitment::from(Word::from([ONE; 4])); let wallet = create_basic_wallet( [1; 32], - AuthScheme::RpoFalcon512 { pub_key }, + AuthScheme::Falcon512Rpo { pub_key }, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, ) diff --git a/crates/miden-lib/src/auth.rs b/crates/miden-standards/src/auth_scheme.rs similarity index 79% rename from crates/miden-lib/src/auth.rs rename to crates/miden-standards/src/auth_scheme.rs index c7c98947e3..d8a5e7cf56 100644 --- a/crates/miden-lib/src/auth.rs +++ b/crates/miden-standards/src/auth_scheme.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use miden_objects::account::auth::PublicKeyCommitment; +use miden_protocol::account::auth::PublicKeyCommitment; /// Defines authentication schemes available to standard and faucet accounts. pub enum AuthScheme { @@ -19,17 +19,17 @@ pub enum AuthScheme { threshold: u32, pub_keys: Vec, }, - /// A single-key authentication scheme which relies RPO Falcon512 signatures. + /// A single-key authentication scheme which relies Falcon512 RPO signatures. /// - /// RPO Falcon512 is a variant of the [Falcon](https://falcon-sign.info/) signature scheme. + /// Falcon512 RPO is a variant of the [Falcon](https://falcon-sign.info/) signature scheme. /// This variant differs from the standard in that instead of using SHAKE256 hash function in /// the hash-to-point algorithm we use RPO256. This makes the signature more efficient to /// verify in Miden VM. - RpoFalcon512 { pub_key: PublicKeyCommitment }, - /// A multi-signature authentication scheme using RPO Falcon512 signatures. + Falcon512Rpo { pub_key: PublicKeyCommitment }, + /// A multi-signature authentication scheme using Falcon512 RPO signatures. /// /// Requires a threshold number of signatures from the provided public keys. - RpoFalcon512Multisig { + Falcon512RpoMultisig { threshold: u32, pub_keys: Vec, }, @@ -46,8 +46,8 @@ impl AuthScheme { AuthScheme::NoAuth => Vec::new(), AuthScheme::EcdsaK256Keccak { pub_key } => vec![*pub_key], AuthScheme::EcdsaK256KeccakMultisig { pub_keys, .. } => pub_keys.clone(), - AuthScheme::RpoFalcon512 { pub_key } => vec![*pub_key], - AuthScheme::RpoFalcon512Multisig { pub_keys, .. } => pub_keys.clone(), + AuthScheme::Falcon512Rpo { pub_key } => vec![*pub_key], + AuthScheme::Falcon512RpoMultisig { pub_keys, .. } => pub_keys.clone(), AuthScheme::Unknown => Vec::new(), } } diff --git a/crates/miden-lib/src/utils/script_builder.rs b/crates/miden-standards/src/code_builder/mod.rs similarity index 55% rename from crates/miden-lib/src/utils/script_builder.rs rename to crates/miden-standards/src/code_builder/mod.rs index e2df108f5e..7ea72c9dc6 100644 --- a/crates/miden-lib/src/utils/script_builder.rs +++ b/crates/miden-standards/src/code_builder/mod.rs @@ -1,26 +1,28 @@ -use alloc::string::String; use alloc::sync::Arc; -use miden_objects::assembly::diagnostics::NamedSource; -use miden_objects::assembly::{ +use miden_protocol::account::AccountComponentCode; +use miden_protocol::assembly::{ Assembler, DefaultSourceManager, Library, - LibraryPath, + Parse, + ParseOptions, + Path, SourceManagerSync, }; -use miden_objects::note::NoteScript; -use miden_objects::transaction::TransactionScript; +use miden_protocol::note::NoteScript; +use miden_protocol::transaction::{TransactionKernel, TransactionScript}; -use crate::errors::ScriptBuilderError; -use crate::transaction::TransactionKernel; +use crate::errors::CodeBuilderError; +use crate::standards_lib::StandardsLib; -// SCRIPT BUILDER +// CODE BUILDER // ================================================================================================ -/// A builder for compiling note scripts and transaction scripts with optional library dependencies. +/// A builder for compiling account components, note scripts, and transaction scripts with optional +/// library dependencies. /// -/// The ScriptBuilder simplifies the process of creating transaction scripts by providing: +/// The [`CodeBuilder`] simplifies the process of creating transaction scripts by providing: /// - A clean API for adding multiple libraries with static or dynamic linking /// - Automatic assembler configuration with all added libraries /// - Debug mode support @@ -41,77 +43,70 @@ use crate::transaction::TransactionKernel; /// /// ## Typical Workflow /// -/// 1. Create a new ScriptBuilder with debug mode preference +/// 1. Create a new CodeBuilder with debug mode preference /// 2. Add any required modules using `link_module()` or `with_linked_module()` /// 3. Add libraries using `link_static_library()` / `link_dynamic_library()` as appropriate /// 4. Compile your script with `compile_note_script()` or `compile_tx_script()` /// -/// Note that the compilation methods consume the ScriptBuilder, so if you need to compile +/// Note that the compiling methods consume the CodeBuilder, so if you need to compile /// multiple scripts with the same configuration, you should clone the builder first. /// /// ## Builder Pattern Example /// /// ```no_run /// # use anyhow::Context; -/// # use miden_lib::utils::ScriptBuilder; -/// # use miden_objects::assembly::Library; -/// # use miden_stdlib::StdLibrary; +/// # use miden_standards::code_builder::CodeBuilder; +/// # use miden_protocol::assembly::Library; +/// # use miden_protocol::CoreLibrary; /// # fn example() -> anyhow::Result<()> { -/// # let module_code = "export.test push.1 add end"; +/// # let module_code = "pub proc test push.1 add end"; /// # let script_code = "begin nop end"; /// # // Create sample libraries for the example -/// # let my_lib = StdLibrary::default().into(); // Convert StdLibrary to Library -/// # let fpi_lib = StdLibrary::default().into(); -/// let script = ScriptBuilder::default() +/// # let my_lib: Library = CoreLibrary::default().into(); // Convert CoreLibrary to Library +/// # let fpi_lib: Library = CoreLibrary::default().into(); +/// let script = CodeBuilder::default() /// .with_linked_module("my::module", module_code).context("failed to link module")? /// .with_statically_linked_library(&my_lib).context("failed to link static library")? /// .with_dynamically_linked_library(&fpi_lib).context("failed to link dynamic library")? // For FPI calls -/// .compile_tx_script(script_code).context("failed to compile tx script")?; +/// .compile_tx_script(script_code).context("failed to parse tx script")?; /// # Ok(()) /// # } /// ``` /// /// # Note -/// The ScriptBuilder automatically includes the `miden` and `std` libraries, which +/// The CodeBuilder automatically includes the `miden` and `std` libraries, which /// provide access to transaction kernel procedures. Due to being available on-chain /// these libraries are linked dynamically and do not add to the size of built script. #[derive(Clone)] -pub struct ScriptBuilder { +pub struct CodeBuilder { assembler: Assembler, source_manager: Arc, } -impl ScriptBuilder { +impl CodeBuilder { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new ScriptBuilder with the specified debug mode. - /// - /// # Arguments - /// * `in_debug_mode` - Whether to enable debug mode in the assembler - pub fn new(in_debug_mode: bool) -> Self { - let source_manager = Arc::new(DefaultSourceManager::default()); - let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone()) - .with_debug_mode(in_debug_mode); - Self { assembler, source_manager } + /// Creates a new CodeBuilder. + pub fn new() -> Self { + Self::with_source_manager(Arc::new(DefaultSourceManager::default())) } - /// Creates a new ScriptBuilder with the specified source manager. - /// - /// The returned builder is instantiated with debug mode enabled. + /// Creates a new CodeBuilder with the specified source manager. /// /// # Arguments /// * `source_manager` - The source manager to use with the internal `Assembler` pub fn with_source_manager(source_manager: Arc) -> Self { let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone()) - .with_debug_mode(true); + .with_dynamic_library(StandardsLib::default()) + .expect("linking std lib should work"); Self { assembler, source_manager } } // LIBRARY MANAGEMENT // -------------------------------------------------------------------------------------------- - /// Compiles and links a module to the script builder. + /// Parses and links a module to the code builder. /// /// This method compiles the provided module code and adds it directly to the assembler /// for use in script compilation. @@ -128,20 +123,17 @@ impl ScriptBuilder { pub fn link_module( &mut self, module_path: impl AsRef, - module_code: impl AsRef, - ) -> Result<(), ScriptBuilderError> { - // Parse the library path - let lib_path = LibraryPath::new(module_path.as_ref()).map_err(|err| { - ScriptBuilderError::build_error_with_source( - format!("invalid module path: {}", module_path.as_ref()), - err, - ) - })?; + module_code: impl Parse, + ) -> Result<(), CodeBuilderError> { + let mut parse_options = ParseOptions::for_library(); + parse_options.path = Some(Path::new(module_path.as_ref()).into()); - let module = NamedSource::new(format!("{lib_path}"), String::from(module_code.as_ref())); + let module = module_code.parse_with_options(self.source_manager(), parse_options).map_err( + |err| CodeBuilderError::build_error_with_report("failed to parse module code", err), + )?; self.assembler.compile_and_statically_link(module).map_err(|err| { - ScriptBuilderError::build_error_with_report("failed to assemble module", err) + CodeBuilderError::build_error_with_report("failed to assemble module", err) })?; Ok(()) @@ -158,9 +150,9 @@ impl ScriptBuilder { /// # Errors /// Returns an error if: /// - adding the library to the assembler failed - pub fn link_static_library(&mut self, library: &Library) -> Result<(), ScriptBuilderError> { + pub fn link_static_library(&mut self, library: &Library) -> Result<(), CodeBuilderError> { self.assembler.link_static_library(library).map_err(|err| { - ScriptBuilderError::build_error_with_report("failed to add static library", err) + CodeBuilderError::build_error_with_report("failed to add static library", err) }) } @@ -177,9 +169,9 @@ impl ScriptBuilder { /// /// # Errors /// Returns an error if the library cannot be added to the assembler - pub fn link_dynamic_library(&mut self, library: &Library) -> Result<(), ScriptBuilderError> { + pub fn link_dynamic_library(&mut self, library: &Library) -> Result<(), CodeBuilderError> { self.assembler.link_dynamic_library(library).map_err(|err| { - ScriptBuilderError::build_error_with_report("failed to add dynamic library", err) + CodeBuilderError::build_error_with_report("failed to add dynamic library", err) }) } @@ -195,7 +187,7 @@ impl ScriptBuilder { pub fn with_statically_linked_library( mut self, library: &Library, - ) -> Result { + ) -> Result { self.link_static_library(library)?; Ok(self) } @@ -211,9 +203,9 @@ impl ScriptBuilder { /// Returns an error if the library cannot be added to the assembler pub fn with_dynamically_linked_library( mut self, - library: &Library, - ) -> Result { - self.link_dynamic_library(library)?; + library: impl AsRef, + ) -> Result { + self.link_dynamic_library(library.as_ref())?; Ok(self) } @@ -230,55 +222,86 @@ impl ScriptBuilder { pub fn with_linked_module( mut self, module_path: impl AsRef, - module_code: impl AsRef, - ) -> Result { + module_code: impl Parse, + ) -> Result { self.link_module(module_path, module_code)?; Ok(self) } - // SCRIPT COMPILATION + // COMPILATION // -------------------------------------------------------------------------------------------- - /// Compiles a transaction script with the provided program code. + /// Compiles the provided module path and MASM code into an [`AccountComponentCode`]. + /// The resulting code can be used to create account components. + /// + /// # Arguments + /// * `component_path` - The path to the account code module (e.g., `my_account::my_module`) + /// * `component_code` - The account component source code + /// + /// # Errors + /// Returns an error if: + /// - Compiling the account component code fails + pub fn compile_component_code( + self, + component_path: impl AsRef, + component_code: impl Parse, + ) -> Result { + let CodeBuilder { assembler, source_manager } = self; + + let mut parse_options = ParseOptions::for_library(); + parse_options.path = Some(Path::new(component_path.as_ref()).into()); + + let module = + component_code + .parse_with_options(source_manager, parse_options) + .map_err(|err| { + CodeBuilderError::build_error_with_report("failed to parse component code", err) + })?; + + let library = assembler.assemble_library([module]).map_err(|err| { + CodeBuilderError::build_error_with_report("failed to parse component code", err) + })?; + + Ok(AccountComponentCode::from(library)) + } + + /// Compiles the provided MASM code into a [`TransactionScript`]. /// - /// The compiled script will have access to all modules that have been added to this builder. + /// The parsed script will have access to all modules that have been added to this builder. /// /// # Arguments - /// * `program` - The transaction script source code + /// * `tx_script` - The transaction script source code /// /// # Errors /// Returns an error if: - /// - The transaction script compilation fails + /// - The transaction script compiling fails pub fn compile_tx_script( self, - tx_script: impl AsRef, - ) -> Result { + tx_script: impl Parse, + ) -> Result { let assembler = self.assembler; - let program = assembler.assemble_program(tx_script.as_ref()).map_err(|err| { - ScriptBuilderError::build_error_with_report("failed to compile transaction script", err) + let program = assembler.assemble_program(tx_script).map_err(|err| { + CodeBuilderError::build_error_with_report("failed to parse transaction script", err) })?; Ok(TransactionScript::new(program)) } - /// Compiles a note script with the provided program code. + /// Compiles the provided MASM code into a [`NoteScript`]. /// - /// The compiled script will have access to all modules that have been added to this builder. + /// The parsed script will have access to all modules that have been added to this builder. /// /// # Arguments /// * `program` - The note script source code /// /// # Errors /// Returns an error if: - /// - The note script compilation fails - pub fn compile_note_script( - self, - program: impl AsRef, - ) -> Result { + /// - The note script compiling fails + pub fn compile_note_script(self, program: impl Parse) -> Result { let assembler = self.assembler; - let program = assembler.assemble_program(program.as_ref()).map_err(|err| { - ScriptBuilderError::build_error_with_report("failed to compile note script", err) + let program = assembler.assemble_program(program).map_err(|err| { + CodeBuilderError::build_error_with_report("failed to parse note script", err) })?; Ok(NoteScript::new(program)) } @@ -294,33 +317,85 @@ impl ScriptBuilder { // TESTING CONVENIENCE FUNCTIONS // -------------------------------------------------------------------------------------------- - /// Returns a [`ScriptBuilder`] with the `mock::{account, faucet, util}` libraries. + /// Returns a [`CodeBuilder`] with the transaction kernel as a library. /// - /// This script builder includes: + /// This assembler is the same as [`TransactionKernel::assembler`] but additionally includes the + /// kernel library on the namespace of `$kernel`. The `$kernel` library is added separately + /// because even though the library (`api.masm`) and the kernel binary (`main.masm`) include + /// this code, it is not otherwise accessible. By adding it separately, we can invoke procedures + /// from the kernel library to test them individually. + #[cfg(any(feature = "testing", test))] + pub fn with_kernel_library(source_manager: Arc) -> Self { + let mut builder = Self::with_source_manager(source_manager); + builder + .link_dynamic_library(&TransactionKernel::library()) + .expect("failed to link kernel library"); + builder + } + + /// Returns a [`CodeBuilder`] with the `mock::{account, faucet, util}` libraries. + /// + /// This assembler includes: /// - [`MockAccountCodeExt::mock_account_library`][account_lib], /// - [`MockAccountCodeExt::mock_faucet_library`][faucet_lib], /// - [`mock_util_library`][util_lib] /// /// [account_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_account_library /// [faucet_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_faucet_library - /// [util_lib]: crate::testing::mock_util_lib::mock_util_library + /// [util_lib]: miden_protocol::testing::mock_util_lib::mock_util_library + #[cfg(any(feature = "testing", test))] + pub fn with_mock_libraries() -> Self { + Self::with_mock_libraries_with_source_manager(Arc::new(DefaultSourceManager::default())) + } + + /// Returns the mock account and faucet libraries used in testing. #[cfg(any(feature = "testing", test))] - pub fn with_mock_libraries() -> Result { - use miden_objects::account::AccountCode; + pub fn mock_libraries() -> impl Iterator { + use miden_protocol::account::AccountCode; use crate::testing::mock_account_code::MockAccountCodeExt; - use crate::testing::mock_util_lib::mock_util_library; - Self::new(true) - .with_dynamically_linked_library(&AccountCode::mock_account_library())? - .with_dynamically_linked_library(&AccountCode::mock_faucet_library())? - .with_statically_linked_library(&mock_util_library()) + vec![AccountCode::mock_account_library(), AccountCode::mock_faucet_library()].into_iter() + } + + #[cfg(any(feature = "testing", test))] + pub fn with_mock_libraries_with_source_manager( + source_manager: Arc, + ) -> Self { + use miden_protocol::testing::mock_util_lib::mock_util_library; + + // Start with the builder linking against the transaction kernel, protocol library and + // standards library. + let mut builder = Self::with_source_manager(source_manager); + + // Expose kernel procedures under `$kernel` for testing. + builder + .link_dynamic_library(&TransactionKernel::library()) + .expect("failed to link kernel library"); + + // Add mock account/faucet libs (built in debug mode) and mock util. + for library in Self::mock_libraries() { + builder + .link_dynamic_library(&library) + .expect("failed to link mock account libraries"); + } + builder + .link_static_library(&mock_util_library()) + .expect("failed to link mock util library"); + + builder } } -impl Default for ScriptBuilder { +impl Default for CodeBuilder { fn default() -> Self { - Self::new(true) + Self::new() + } +} + +impl From for Assembler { + fn from(builder: CodeBuilder) -> Self { + builder.assembler } } @@ -330,28 +405,29 @@ impl Default for ScriptBuilder { #[cfg(test)] mod tests { use anyhow::Context; + use miden_protocol::assembly::diagnostics::NamedSource; use super::*; #[test] - fn test_script_builder_new() { - let _builder = ScriptBuilder::default(); + fn test_code_builder_new() { + let _builder = CodeBuilder::default(); // Test that the builder can be created successfully } #[test] - fn test_script_builder_basic_script_compilation() -> anyhow::Result<()> { - let builder = ScriptBuilder::default(); + fn test_code_builder_basic_script_compiling() -> anyhow::Result<()> { + let builder = CodeBuilder::default(); builder .compile_tx_script("begin nop end") - .context("failed to compile basic tx script")?; + .context("failed to parse basic tx script")?; Ok(()) } #[test] fn test_create_library_and_create_tx_script() -> anyhow::Result<()> { let script_code = " - use.external_contract::counter_contract + use external_contract::counter_contract begin call.counter_contract::increment @@ -359,11 +435,11 @@ mod tests { "; let account_code = " - use.miden::active_account - use.miden::native_account - use.std::sys + use miden::protocol::active_account + use miden::protocol::native_account + use miden::core::sys - export.increment + pub proc increment push.0 exec.active_account::get_item push.1 add @@ -375,21 +451,21 @@ mod tests { let library_path = "external_contract::counter_contract"; - let mut builder_with_lib = ScriptBuilder::default(); + let mut builder_with_lib = CodeBuilder::default(); builder_with_lib .link_module(library_path, account_code) .context("failed to link module")?; builder_with_lib .compile_tx_script(script_code) - .context("failed to compile tx script")?; + .context("failed to parse tx script")?; Ok(()) } #[test] - fn test_compile_library_and_add_to_builder() -> anyhow::Result<()> { + fn test_parse_library_and_add_to_builder() -> anyhow::Result<()> { let script_code = " - use.external_contract::counter_contract + use external_contract::counter_contract begin call.counter_contract::increment @@ -397,11 +473,11 @@ mod tests { "; let account_code = " - use.miden::active_account - use.miden::native_account - use.std::sys + use miden::protocol::active_account + use miden::protocol::native_account + use miden::core::sys - export.increment + pub proc increment push.0 exec.active_account::get_item push.1 add @@ -414,25 +490,25 @@ mod tests { let library_path = "external_contract::counter_contract"; // Test single library - let mut builder_with_lib = ScriptBuilder::default(); + let mut builder_with_lib = CodeBuilder::default(); builder_with_lib .link_module(library_path, account_code) .context("failed to link module")?; builder_with_lib .compile_tx_script(script_code) - .context("failed to compile tx script")?; + .context("failed to parse tx script")?; // Test multiple libraries - let mut builder_with_libs = ScriptBuilder::default(); + let mut builder_with_libs = CodeBuilder::default(); builder_with_libs .link_module(library_path, account_code) .context("failed to link first module")?; builder_with_libs - .link_module("test::lib", "export.test nop end") + .link_module("test::lib", "pub proc test nop end") .context("failed to link second module")?; builder_with_libs .compile_tx_script(script_code) - .context("failed to compile tx script with multiple libraries")?; + .context("failed to parse tx script with multiple libraries")?; Ok(()) } @@ -440,7 +516,7 @@ mod tests { #[test] fn test_builder_style_chaining() -> anyhow::Result<()> { let script_code = " - use.external_contract::counter_contract + use external_contract::counter_contract begin call.counter_contract::increment @@ -448,11 +524,11 @@ mod tests { "; let account_code = " - use.miden::active_account - use.miden::native_account - use.std::sys + use miden::protocol::active_account + use miden::protocol::native_account + use miden::core::sys - export.increment + pub proc increment push.0 exec.active_account::get_item push.1 add @@ -463,11 +539,11 @@ mod tests { "; // Test builder-style chaining with modules - let builder = ScriptBuilder::default() + let builder = CodeBuilder::default() .with_linked_module("external_contract::counter_contract", account_code) .context("failed to link module")?; - builder.compile_tx_script(script_code).context("failed to compile tx script")?; + builder.compile_tx_script(script_code).context("failed to parse tx script")?; Ok(()) } @@ -475,16 +551,16 @@ mod tests { #[test] fn test_multiple_chained_modules() -> anyhow::Result<()> { let script_code = - "use.test::lib1 use.test::lib2 begin exec.lib1::test1 exec.lib2::test2 end"; + "use test::lib1 use test::lib2 begin exec.lib1::test1 exec.lib2::test2 end"; // Test chaining multiple modules - let builder = ScriptBuilder::default() - .with_linked_module("test::lib1", "export.test1 push.1 add end") + let builder = CodeBuilder::default() + .with_linked_module("test::lib1", "pub proc test1 push.1 add end") .context("failed to link first module")? - .with_linked_module("test::lib2", "export.test2 push.2 add end") + .with_linked_module("test::lib2", "pub proc test2 push.2 add end") .context("failed to link second module")?; - builder.compile_tx_script(script_code).context("failed to compile tx script")?; + builder.compile_tx_script(script_code).context("failed to parse tx script")?; Ok(()) } @@ -492,42 +568,22 @@ mod tests { #[test] fn test_static_and_dynamic_linking() -> anyhow::Result<()> { let script_code = " - use.external_contract::contract_1 - use.external_contract::contract_2 + use contracts::static_contract begin - call.contract_1::increment_1 - call.contract_2::increment_2 + call.static_contract::increment_1 end "; let account_code_1 = " - use.miden::active_account - use.miden::native_account - use.std::sys - - export.increment_1 - push.0 - exec.active_account::get_item - push.1 add - push.0 - exec.native_account::set_item - exec.sys::truncate_stack + pub proc increment_1 + push.0 drop end "; let account_code_2 = " - use.miden::active_account - use.miden::native_account - use.std::sys - - export.increment_2 - push.0 - exec.active_account::get_item - push.2 add - push.0 - exec.native_account::set_item - exec.sys::truncate_stack + pub proc increment_2 + push.0 drop end "; @@ -536,15 +592,15 @@ mod tests { let static_lib = temp_assembler .clone() - .assemble_library([NamedSource::new("external_contract::contract_1", account_code_1)]) + .assemble_library([NamedSource::new("contracts::static_contract", account_code_1)]) .map_err(|e| anyhow::anyhow!("failed to assemble static library: {}", e))?; let dynamic_lib = temp_assembler - .assemble_library([NamedSource::new("external_contract::contract_2", account_code_2)]) + .assemble_library([NamedSource::new("contracts::dynamic_contract", account_code_2)]) .map_err(|e| anyhow::anyhow!("failed to assemble dynamic library: {}", e))?; // Test linking both static and dynamic libraries - let builder = ScriptBuilder::default() + let builder = CodeBuilder::default() .with_statically_linked_library(&static_lib) .context("failed to link static library")? .with_dynamically_linked_library(&dynamic_lib) @@ -552,7 +608,7 @@ mod tests { builder .compile_tx_script(script_code) - .context("failed to compile tx script with static and dynamic libraries")?; + .context("failed to parse tx script with static and dynamic libraries")?; Ok(()) } diff --git a/crates/miden-lib/src/errors/script_builder_errors.rs b/crates/miden-standards/src/errors/code_builder_errors.rs similarity index 77% rename from crates/miden-lib/src/errors/script_builder_errors.rs rename to crates/miden-standards/src/errors/code_builder_errors.rs index 76fe01e66b..14f3a9eb71 100644 --- a/crates/miden-lib/src/errors/script_builder_errors.rs +++ b/crates/miden-standards/src/errors/code_builder_errors.rs @@ -2,23 +2,23 @@ use alloc::boxed::Box; use alloc::string::String; use core::error::Error; -use miden_objects::assembly::diagnostics::Report; -use miden_objects::assembly::diagnostics::reporting::PrintDiagnostic; +use miden_protocol::assembly::diagnostics::Report; +use miden_protocol::assembly::diagnostics::reporting::PrintDiagnostic; -// SCRIPT BUILDER ERROR +// CODE BUILDER ERROR // ================================================================================================ #[derive(Debug, thiserror::Error)] #[error("failed to build script: {message}")] -pub struct ScriptBuilderError { +pub struct CodeBuilderError { /// Stack size of `Box` is smaller than String. message: Box, - /// thiserror will return this when calling Error::source on ScriptBuilderError. + /// thiserror will return this when calling Error::source on CodeBuilderError. source: Option>, } -impl ScriptBuilderError { - /// Creates a script builder error from an error message and a source error. +impl CodeBuilderError { + /// Creates a code builder error from an error message and a source error. pub fn build_error_with_source( message: impl Into, source: impl Error + Send + Sync + 'static, @@ -30,7 +30,7 @@ impl ScriptBuilderError { } } - /// Creates a script builder error from a context message and a Report. + /// Creates a code builder error from a context message and a Report. /// /// This method uses PrintDiagnostic to stringify the Report since Report doesn't /// implement core::error::Error and cannot be returned as a source error. @@ -48,7 +48,7 @@ mod error_assertions { /// Asserts at compile time that the passed error has Send + Sync + 'static bounds. fn _assert_error_is_send_sync_static(_: E) {} - fn _assert_script_builder_error_bounds(err: ScriptBuilderError) { + fn _assert_code_builder_error_bounds(err: CodeBuilderError) { _assert_error_is_send_sync_static(err); } } diff --git a/crates/miden-standards/src/errors/mod.rs b/crates/miden-standards/src/errors/mod.rs new file mode 100644 index 0000000000..2bf69e28e0 --- /dev/null +++ b/crates/miden-standards/src/errors/mod.rs @@ -0,0 +1,7 @@ +/// The errors from the MASM code of the Miden standards. +#[cfg(any(feature = "testing", test))] +#[rustfmt::skip] +pub mod standards; + +mod code_builder_errors; +pub use code_builder_errors::CodeBuilderError; diff --git a/crates/miden-lib/src/errors/note_script_errors.rs b/crates/miden-standards/src/errors/standards.rs similarity index 68% rename from crates/miden-lib/src/errors/note_script_errors.rs rename to crates/miden-standards/src/errors/standards.rs index 2ee15d05e9..007723901b 100644 --- a/crates/miden-lib/src/errors/note_script_errors.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -1,29 +1,25 @@ -use crate::errors::MasmError; +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 `miden-lib/asm` directory. +// 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, Prologue, -// Non-Fungible-Asset, ...). +// 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, ...). -// NOTE SCRIPT ERRORS +// STANDARDS ERRORS // ================================================================================================ -/// Error Message: "auth procedure had been called from outside the epilogue" -pub const ERR_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT: MasmError = MasmError::from_static_str("auth procedure had been called from outside the epilogue"); - /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); +/// Error Message: "distribute would cause the maximum supply to be exceeded" +pub const ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED: MasmError = MasmError::from_static_str("distribute would cause the maximum supply to be exceeded"); + /// Error Message: "number of approvers must be equal to or greater than threshold" pub const ERR_MALFORMED_MULTISIG_CONFIG: MasmError = MasmError::from_static_str("number of approvers must be equal to or greater than threshold"); -/// Error Message: "MINT script expects exactly 9 note inputs" -pub const ERR_MINT_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("MINT script expects exactly 9 note inputs"); - -/// Error Message: "note sender is not the owner of the faucet who can mint assets" -pub const ERR_ONLY_OWNER_CAN_MINT: MasmError = MasmError::from_static_str("note sender is not the owner of the faucet who can mint assets"); +/// Error Message: "MINT script expects exactly 12 inputs for private or 16+ inputs for public output notes" +pub const ERR_MINT_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("MINT script expects exactly 12 inputs for private or 16+ inputs for public output notes"); /// Error Message: "failed to reclaim P2IDE note because the reclaiming account is not the sender" pub const ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER: MasmError = MasmError::from_static_str("failed to reclaim P2IDE note because the reclaiming account is not the sender"); @@ -41,10 +37,16 @@ pub const ERR_P2ID_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str( /// Error Message: "P2ID note expects exactly 2 note inputs" pub const ERR_P2ID_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("P2ID note expects exactly 2 note inputs"); +/// Error Message: "note sender is not the owner" +pub const ERR_SENDER_NOT_OWNER: MasmError = MasmError::from_static_str("note sender is not the owner"); + /// Error Message: "SWAP script requires exactly 1 note asset" pub const ERR_SWAP_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("SWAP script requires exactly 1 note asset"); -/// Error Message: "SWAP script expects exactly 12 note inputs" -pub const ERR_SWAP_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("SWAP script expects exactly 12 note inputs"); +/// Error Message: "SWAP script expects exactly 16 note inputs" +pub const ERR_SWAP_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_str("SWAP script expects exactly 16 note inputs"); + +/// Error Message: "failed to approve multisig transaction as it was already executed" +pub const ERR_TX_ALREADY_EXECUTED: MasmError = MasmError::from_static_str("failed to approve multisig transaction as it was already executed"); /// Error Message: "number of approvers or threshold must not be zero" pub const ERR_ZERO_IN_MULTISIG_CONFIG: MasmError = MasmError::from_static_str("number of approvers or threshold must not be zero"); diff --git a/crates/miden-standards/src/lib.rs b/crates/miden-standards/src/lib.rs new file mode 100644 index 0000000000..fdcec02916 --- /dev/null +++ b/crates/miden-standards/src/lib.rs @@ -0,0 +1,21 @@ +#![no_std] + +#[macro_use] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +mod auth_scheme; +pub use auth_scheme::AuthScheme; + +pub mod account; +pub mod code_builder; +pub mod errors; +pub mod note; +mod standards_lib; + +pub use standards_lib::StandardsLib; + +#[cfg(any(feature = "testing", test))] +pub mod testing; diff --git a/crates/miden-standards/src/note/mint_inputs.rs b/crates/miden-standards/src/note/mint_inputs.rs new file mode 100644 index 0000000000..3fd62e3e67 --- /dev/null +++ b/crates/miden-standards/src/note/mint_inputs.rs @@ -0,0 +1,118 @@ +use alloc::vec::Vec; + +use miden_protocol::errors::NoteError; +use miden_protocol::note::{NoteAttachment, NoteInputs, NoteRecipient}; +use miden_protocol::{Felt, MAX_INPUTS_PER_NOTE, Word}; + +/// Represents the different input formats for MINT notes. +/// - Private: Creates a private output note using a precomputed recipient digest (12 MINT note +/// inputs) +/// - Public: Creates a public output note by providing script root, serial number, and +/// variable-length inputs (16+ MINT note inputs: 16 fixed + variable number of output note +/// inputs) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MintNoteInputs { + Private { + recipient_digest: Word, + amount: Felt, + tag: Felt, + attachment: NoteAttachment, + }, + Public { + recipient: NoteRecipient, + amount: Felt, + tag: Felt, + attachment: NoteAttachment, + }, +} + +impl MintNoteInputs { + pub fn new_private(recipient_digest: Word, amount: Felt, tag: Felt) -> Self { + Self::Private { + recipient_digest, + amount, + tag, + attachment: NoteAttachment::default(), + } + } + + pub fn new_public( + recipient: NoteRecipient, + amount: Felt, + tag: Felt, + ) -> Result { + // Calculate total number of inputs that will be created: + // 16 fixed inputs (tag, amount, attachment_kind, attachment_scheme, ATTACHMENT, + // SCRIPT_ROOT, SERIAL_NUM) + variable recipient inputs length + const FIXED_PUBLIC_INPUTS: usize = 16; + let total_inputs = FIXED_PUBLIC_INPUTS + recipient.inputs().num_values() as usize; + + if total_inputs > MAX_INPUTS_PER_NOTE { + return Err(NoteError::TooManyInputs(total_inputs)); + } + + Ok(Self::Public { + recipient, + amount, + tag, + attachment: NoteAttachment::default(), + }) + } + + /// Overwrites the [`NoteAttachment`] of the note inputs. + pub fn with_attachment(self, attachment: NoteAttachment) -> Self { + match self { + MintNoteInputs::Private { + recipient_digest, + amount, + tag, + attachment: _, + } => MintNoteInputs::Private { + recipient_digest, + amount, + tag, + attachment, + }, + MintNoteInputs::Public { recipient, amount, tag, attachment: _ } => { + MintNoteInputs::Public { recipient, amount, tag, attachment } + }, + } + } +} + +impl From for NoteInputs { + fn from(mint_inputs: MintNoteInputs) -> Self { + match mint_inputs { + MintNoteInputs::Private { + recipient_digest, + amount, + tag, + attachment, + } => { + let attachment_scheme = Felt::from(attachment.attachment_scheme().as_u32()); + let attachment_kind = Felt::from(attachment.attachment_kind().as_u8()); + let attachment = attachment.content().to_word(); + + let mut input_values = Vec::with_capacity(12); + input_values.extend_from_slice(&[tag, amount, attachment_kind, attachment_scheme]); + input_values.extend_from_slice(attachment.as_elements()); + input_values.extend_from_slice(recipient_digest.as_elements()); + NoteInputs::new(input_values) + .expect("number of inputs should not exceed max inputs") + }, + MintNoteInputs::Public { recipient, amount, tag, attachment } => { + let attachment_scheme = Felt::from(attachment.attachment_scheme().as_u32()); + let attachment_kind = Felt::from(attachment.attachment_kind().as_u8()); + let attachment = attachment.content().to_word(); + + let mut input_values = vec![tag, amount, attachment_kind, attachment_scheme]; + input_values.extend_from_slice(attachment.as_elements()); + input_values.extend_from_slice(recipient.script().root().as_elements()); + input_values.extend_from_slice(recipient.serial_num().as_elements()); + input_values.extend_from_slice(recipient.inputs().values()); + NoteInputs::new(input_values) + .expect("number of inputs should not exceed max inputs") + }, + } + } +} diff --git a/crates/miden-lib/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs similarity index 69% rename from crates/miden-lib/src/note/mod.rs rename to crates/miden-standards/src/note/mod.rs index c5e1a4a5c8..002de6e7d7 100644 --- a/crates/miden-lib/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -1,26 +1,35 @@ use alloc::vec::Vec; -use miden_objects::account::AccountId; -use miden_objects::asset::Asset; -use miden_objects::block::BlockNumber; -use miden_objects::crypto::rand::FeltRng; -use miden_objects::note::{ +use miden_protocol::account::AccountId; +use miden_protocol::asset::Asset; +use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ Note, NoteAssets, + NoteAttachment, NoteDetails, - NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }; -use miden_objects::{Felt, NoteError, Word}; +use miden_protocol::{Felt, Word}; use utils::build_swap_tag; +pub mod mint_inputs; pub mod utils; +mod network_account_target; +pub use network_account_target::NetworkAccountTarget; + +mod well_known_note_attachment; +pub use well_known_note_attachment::WellKnownNoteAttachment; + mod well_known_note; +pub use mint_inputs::MintNoteInputs; pub use well_known_note::{NoteConsumptionStatus, WellKnownNote}; // STANDARDIZED SCRIPTS @@ -41,15 +50,15 @@ pub fn create_p2id_note( target: AccountId, assets: Vec, note_type: NoteType, - aux: Felt, + attachment: NoteAttachment, rng: &mut R, ) -> Result { let serial_num = rng.draw_word(); let recipient = utils::build_p2id_recipient(target, serial_num)?; - let tag = NoteTag::from_account_id(target); + let tag = NoteTag::with_account_target(target); - let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag).with_attachment(attachment); let vault = NoteAssets::new(assets)?; Ok(Note::new(vault, metadata, recipient)) @@ -75,27 +84,22 @@ pub fn create_p2ide_note( reclaim_height: Option, timelock_height: Option, note_type: NoteType, - aux: Felt, + attachment: NoteAttachment, rng: &mut R, ) -> Result { let serial_num = rng.draw_word(); let recipient = utils::build_p2ide_recipient(target, reclaim_height, timelock_height, serial_num)?; - let tag = NoteTag::from_account_id(target); + let tag = NoteTag::with_account_target(target); - let execution_hint = match timelock_height { - Some(height) => NoteExecutionHint::after_block(height)?, - None => NoteExecutionHint::always(), - }; - - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag).with_attachment(attachment); let vault = NoteAssets::new(assets)?; Ok(Note::new(vault, metadata, recipient)) } /// Generates a SWAP note - swap of assets between two accounts - and returns the note as well as -/// [NoteDetails] for the payback note. +/// [`NoteDetails`] for the payback note. /// /// This script enables a swap of 2 assets between the `sender` account and any other account that /// is willing to consume the note. The consumer will receive the `offered_asset` and will create a @@ -108,9 +112,9 @@ pub fn create_swap_note( offered_asset: Asset, requested_asset: Asset, swap_note_type: NoteType, - swap_note_aux: Felt, + swap_note_attachment: NoteAttachment, payback_note_type: NoteType, - payback_note_aux: Felt, + payback_note_attachment: NoteAttachment, rng: &mut R, ) -> Result<(Note, NoteDetails), NoteError> { if requested_asset == offered_asset { @@ -122,32 +126,32 @@ pub fn create_swap_note( let payback_serial_num = rng.draw_word(); let payback_recipient = utils::build_p2id_recipient(sender, payback_serial_num)?; - let payback_recipient_word: Word = payback_recipient.digest(); let requested_asset_word: Word = requested_asset.into(); - let payback_tag = NoteTag::from_account_id(sender); - - let inputs = NoteInputs::new(vec![ - requested_asset_word[0], - requested_asset_word[1], - requested_asset_word[2], - requested_asset_word[3], - payback_recipient_word[0], - payback_recipient_word[1], - payback_recipient_word[2], - payback_recipient_word[3], - NoteExecutionHint::always().into(), + let payback_tag = NoteTag::with_account_target(sender); + + let attachment_scheme = Felt::from(payback_note_attachment.attachment_scheme().as_u32()); + let attachment_kind = Felt::from(payback_note_attachment.attachment_kind().as_u8()); + let attachment = payback_note_attachment.content().to_word(); + + let mut inputs = Vec::with_capacity(16); + inputs.extend_from_slice(&[ payback_note_type.into(), - payback_note_aux, payback_tag.into(), - ])?; + attachment_scheme, + attachment_kind, + ]); + inputs.extend_from_slice(attachment.as_elements()); + inputs.extend_from_slice(requested_asset_word.as_elements()); + inputs.extend_from_slice(payback_recipient.digest().as_elements()); + let inputs = NoteInputs::new(inputs)?; // build the tag for the SWAP use case - let tag = build_swap_tag(swap_note_type, &offered_asset, &requested_asset)?; + let tag = build_swap_tag(swap_note_type, &offered_asset, &requested_asset); let serial_num = rng.draw_word(); // build the outgoing note let metadata = - NoteMetadata::new(sender, swap_note_type, tag, NoteExecutionHint::always(), swap_note_aux)?; + NoteMetadata::new(sender, swap_note_type, tag).with_attachment(swap_note_attachment); let assets = NoteAssets::new(vec![offered_asset])?; let recipient = NoteRecipient::new(serial_num, note_script, inputs); let note = Note::new(assets, metadata, recipient); @@ -162,12 +166,12 @@ pub fn create_swap_note( /// Generates a MINT note - a note that instructs a network faucet to mint fungible assets. /// /// This script enables the creation of a PUBLIC note that, when consumed by a network faucet, -/// will mint the specified amount of fungible assets and create a PRIVATE note with the given -/// RECIPIENT. The MINT note uses note-based authentication, checking if the note sender equals -/// the faucet owner to authorize minting. +/// will mint the specified amount of fungible assets and create either a PRIVATE or PUBLIC +/// output note depending on the input configuration. The MINT note uses note-based authentication, +/// checking if the note sender equals the faucet owner to authorize minting. /// -/// MINT notes are always PUBLIC (for network execution) and output notes are always PRIVATE -/// (TODO: enable public output note creation from MINT note consumption). +/// MINT notes are always PUBLIC (for network execution). Output notes can be either PRIVATE +/// or PUBLIC depending on the MintNoteInputs variant used. /// /// The passed-in `rng` is used to generate a serial number for the note. The note's tag /// is automatically set to the faucet's account ID for proper routing. @@ -175,12 +179,8 @@ pub fn create_swap_note( /// # Parameters /// - `faucet_id`: The account ID of the network faucet that will mint the assets /// - `sender`: The account ID of the note creator (must be the faucet owner) -/// - `target_recipient`: The recipient digest for the output P2ID note that will receive the minted -/// assets -/// - `output_note_tag`: The tag for the output P2ID note -/// - `amount`: The amount of fungible assets to mint -/// - `aux`: Auxiliary data for the MINT note -/// - `output_note_aux`: Auxiliary data for the output P2ID note +/// - `mint_inputs`: The input configuration specifying private or public output mode +/// - `attachment`: The [`NoteAttachment`] of the MINT note /// - `rng`: Random number generator for creating the serial number /// /// # Errors @@ -188,11 +188,8 @@ pub fn create_swap_note( pub fn create_mint_note( faucet_id: AccountId, sender: AccountId, - target_recipient: Word, - output_note_tag: Felt, - amount: Felt, - aux: Felt, - output_note_aux: Felt, + mint_inputs: MintNoteInputs, + attachment: NoteAttachment, rng: &mut R, ) -> Result { let note_script = WellKnownNote::MINT.script(); @@ -200,26 +197,13 @@ pub fn create_mint_note( // MINT notes are always public for network execution let note_type = NoteType::Public; - // Output notes are always private (for now) - let output_note_type = NoteType::Private; - - let execution_hint = NoteExecutionHint::always(); - - let inputs = NoteInputs::new(vec![ - target_recipient[0], - target_recipient[1], - target_recipient[2], - target_recipient[3], - execution_hint.into(), - output_note_type.into(), - output_note_aux, - output_note_tag, - amount, - ])?; - - let tag = NoteTag::from_account_id(faucet_id); - - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux)?; + + // Convert MintNoteInputs to NoteInputs + let inputs = NoteInputs::from(mint_inputs); + + let tag = NoteTag::with_account_target(faucet_id); + + let metadata = NoteMetadata::new(sender, note_type, tag).with_attachment(attachment); let assets = NoteAssets::new(vec![])?; // MINT notes have no assets let recipient = NoteRecipient::new(serial_num, note_script, inputs); @@ -242,7 +226,7 @@ pub fn create_mint_note( /// - `sender`: The account ID of the note creator /// - `faucet_id`: The account ID of the faucet that will burn the assets /// - `fungible_asset`: The fungible asset to be burned -/// - `aux`: Auxiliary data for the note +/// - `attachment`: The [`NoteAttachment`] of the BURN note /// - `rng`: Random number generator for creating the serial number /// /// # Errors @@ -251,7 +235,7 @@ pub fn create_burn_note( sender: AccountId, faucet_id: AccountId, fungible_asset: Asset, - aux: Felt, + attachment: NoteAttachment, rng: &mut R, ) -> Result { let note_script = WellKnownNote::BURN.script(); @@ -259,13 +243,11 @@ pub fn create_burn_note( // BURN notes are always public let note_type = NoteType::Public; - // Use always execution hint for BURN notes - let execution_hint = NoteExecutionHint::always(); let inputs = NoteInputs::new(vec![])?; - let tag = NoteTag::from_account_id(faucet_id); + let tag = NoteTag::with_account_target(faucet_id); - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag).with_attachment(attachment); let assets = NoteAssets::new(vec![fungible_asset])?; // BURN notes contain the asset to burn let recipient = NoteRecipient::new(serial_num, note_script, inputs); diff --git a/crates/miden-standards/src/note/network_account_target.rs b/crates/miden-standards/src/note/network_account_target.rs new file mode 100644 index 0000000000..2ea446bafb --- /dev/null +++ b/crates/miden-standards/src/note/network_account_target.rs @@ -0,0 +1,181 @@ +use miden_protocol::Word; +use miden_protocol::account::AccountId; +use miden_protocol::errors::{AccountIdError, NoteError}; +use miden_protocol::note::{ + NoteAttachment, + NoteAttachmentContent, + NoteAttachmentKind, + NoteAttachmentScheme, + NoteExecutionHint, +}; + +use crate::note::WellKnownNoteAttachment; + +// NETWORK ACCOUNT TARGET +// ================================================================================================ + +/// A [`NoteAttachment`] for notes targeted at network accounts. +/// +/// It can be encoded to and from a [`NoteAttachmentContent::Word`] with the following layout: +/// +/// ```text +/// - 0th felt: [target_id_suffix (56 bits) | 8 zero bits] +/// - 1st felt: [target_id_prefix (64 bits)] +/// - 2nd felt: [24 zero bits | exec_hint_payload (32 bits) | exec_hint_tag (8 bits)] +/// - 3rd felt: [64 zero bits] +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NetworkAccountTarget { + target_id: AccountId, + exec_hint: NoteExecutionHint, +} + +impl NetworkAccountTarget { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The standardized scheme of [`NetworkAccountTarget`] attachments. + pub const ATTACHMENT_SCHEME: NoteAttachmentScheme = + WellKnownNoteAttachment::NetworkAccountTarget.attachment_scheme(); + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NetworkAccountTarget`] from the provided parts. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided `target_id` does not have + /// [`AccountStorageMode::Network`](miden_protocol::account::AccountStorageMode::Network). + pub fn new( + target_id: AccountId, + exec_hint: NoteExecutionHint, + ) -> Result { + // TODO: Once AccountStorageMode::Network is removed, this should check is_public. + if !target_id.is_network() { + return Err(NetworkAccountTargetError::TargetNotNetwork(target_id)); + } + + Ok(Self { target_id, exec_hint }) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`AccountId`] at which the note is targeted. + pub fn target_id(&self) -> AccountId { + self.target_id + } + + /// Returns the [`NoteExecutionHint`] of the note. + pub fn execution_hint(&self) -> NoteExecutionHint { + self.exec_hint + } +} + +impl From for NoteAttachment { + fn from(network_attachment: NetworkAccountTarget) -> Self { + let mut word = Word::empty(); + word[0] = network_attachment.target_id.suffix(); + word[1] = network_attachment.target_id.prefix().as_felt(); + word[2] = network_attachment.exec_hint.into(); + + NoteAttachment::new_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word) + } +} + +impl TryFrom<&NoteAttachment> for NetworkAccountTarget { + type Error = NetworkAccountTargetError; + + fn try_from(attachment: &NoteAttachment) -> Result { + if attachment.attachment_scheme() != Self::ATTACHMENT_SCHEME { + return Err(NetworkAccountTargetError::AttachmentSchemeMismatch( + attachment.attachment_scheme(), + )); + } + + match attachment.content() { + NoteAttachmentContent::Word(word) => { + let id_suffix = word[0]; + let id_prefix = word[1]; + let exec_hint = word[2]; + + let target_id = AccountId::try_from([id_prefix, id_suffix]) + .map_err(NetworkAccountTargetError::DecodeTargetId)?; + + let exec_hint = NoteExecutionHint::try_from(exec_hint.as_int()) + .map_err(NetworkAccountTargetError::DecodeExecutionHint)?; + + NetworkAccountTarget::new(target_id, exec_hint) + }, + _ => Err(NetworkAccountTargetError::AttachmentKindMismatch( + attachment.content().attachment_kind(), + )), + } + } +} + +// NETWORK ACCOUNT TARGET ERROR +// ================================================================================================ + +#[derive(Debug, thiserror::Error)] +pub enum NetworkAccountTargetError { + #[error("target account ID must be of type network account")] + TargetNotNetwork(AccountId), + #[error( + "attachment scheme {0} did not match expected type {expected}", + expected = NetworkAccountTarget::ATTACHMENT_SCHEME + )] + AttachmentSchemeMismatch(NoteAttachmentScheme), + #[error( + "attachment kind {0} did not match expected type {expected}", + expected = NoteAttachmentKind::Word + )] + AttachmentKindMismatch(NoteAttachmentKind), + #[error("failed to decode target account ID")] + DecodeTargetId(#[source] AccountIdError), + #[error("failed to decode execution hint")] + DecodeExecutionHint(#[source] NoteError), +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::testing::account_id::AccountIdBuilder; + + use super::*; + + #[test] + fn network_account_target_serde() -> anyhow::Result<()> { + let id = AccountIdBuilder::new() + .storage_mode(AccountStorageMode::Network) + .build_with_rng(&mut rand::rng()); + let network_account_target = NetworkAccountTarget::new(id, NoteExecutionHint::Always)?; + assert_eq!( + network_account_target, + NetworkAccountTarget::try_from(&NoteAttachment::from(network_account_target))? + ); + + Ok(()) + } + + #[test] + fn network_account_target_fails_on_private_network_target_account() -> anyhow::Result<()> { + let id = AccountIdBuilder::new() + .storage_mode(AccountStorageMode::Private) + .build_with_rng(&mut rand::rng()); + let err = NetworkAccountTarget::new(id, NoteExecutionHint::Always).unwrap_err(); + + assert_matches!( + err, + NetworkAccountTargetError::TargetNotNetwork(account_id) if account_id == id + ); + + Ok(()) + } +} diff --git a/crates/miden-lib/src/note/utils.rs b/crates/miden-standards/src/note/utils.rs similarity index 60% rename from crates/miden-lib/src/note/utils.rs rename to crates/miden-standards/src/note/utils.rs index 0968a042a0..b111e68bce 100644 --- a/crates/miden-lib/src/note/utils.rs +++ b/crates/miden-standards/src/note/utils.rs @@ -1,8 +1,9 @@ -use miden_objects::account::AccountId; -use miden_objects::asset::Asset; -use miden_objects::block::BlockNumber; -use miden_objects::note::{NoteExecutionMode, NoteInputs, NoteRecipient, NoteTag, NoteType}; -use miden_objects::{Felt, NoteError, Word}; +use miden_protocol::account::AccountId; +use miden_protocol::asset::Asset; +use miden_protocol::block::BlockNumber; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{NoteInputs, NoteRecipient, NoteTag, NoteType}; +use miden_protocol::{Felt, Word}; use super::well_known_note::WellKnownNote; @@ -47,18 +48,26 @@ pub fn build_p2ide_recipient( /// Returns a note tag for a swap note with the specified parameters. /// -/// Use case ID for the returned tag is set to 0. +/// The tag is laid out as follows: /// -/// Tag payload is constructed by taking asset tags (8 bits of each faucet ID) and concatenating -/// them together as offered_asset_tag + requested_asset tag. +/// ```text +/// [ +/// note_type (2 bits) | script_root (14 bits) +/// | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits) +/// ] +/// ``` /// -/// Network execution hint for the returned tag is set to `Local`. +/// The script root serves as the use case identifier of the SWAP tag. pub fn build_swap_tag( note_type: NoteType, offered_asset: &Asset, requested_asset: &Asset, -) -> Result { - const SWAP_USE_CASE_ID: u16 = 0; +) -> NoteTag { + let swap_root_bytes = WellKnownNote::SWAP.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; // 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(); @@ -67,20 +76,19 @@ pub fn build_swap_tag( let requested_asset_id: u64 = requested_asset.faucet_id_prefix().into(); let requested_asset_tag = (requested_asset_id >> 56) as u8; - let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); + let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); - let execution = NoteExecutionMode::Local; - match note_type { - NoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution), - _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), - } + let tag = + ((note_type as u8 as u32) << 30) | ((swap_use_case_id as u32) << 16) | asset_pair as u32; + + NoteTag::new(tag) } #[cfg(test)] mod tests { - use miden_objects::account::{AccountIdVersion, AccountStorageMode, AccountType}; - use miden_objects::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; - use miden_objects::{self}; + use miden_protocol::account::{AccountIdVersion, AccountStorageMode, AccountType}; + use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; + use miden_protocol::{self}; use super::*; @@ -129,16 +137,24 @@ mod tests { // The fungible ID starts with 0xcdb1. // The non fungible ID starts with 0xabec. // The expected tag payload is thus 0xcdab. - let expected_tag_payload = 0xcdab; - - let actual_tag = - build_swap_tag(NoteType::Public, &offered_asset, &requested_asset).unwrap(); - - // 0 is the SWAP use case ID. - let expected_tag = - NoteTag::for_public_use_case(0, expected_tag_payload, NoteExecutionMode::Local) - .unwrap(); - - assert_eq!(actual_tag, expected_tag); + let expected_asset_pair = 0xcdab; + + let note_type = NoteType::Public; + let actual_tag = build_swap_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"); + // Check the 8 bits of the first script root byte. + assert_eq!( + (actual_tag.as_u32() >> 22) as u8, + WellKnownNote::SWAP.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. + assert_eq!( + ((actual_tag.as_u32() & 0b00000000_00111111_00000000_00000000) >> 16) as u8, + WellKnownNote::SWAP.script().root().as_bytes()[1] >> 2, + "swap script root byte 1 should match with the lower two bits set to zero" + ); } } diff --git a/crates/miden-lib/src/note/well_known_note.rs b/crates/miden-standards/src/note/well_known_note.rs similarity index 92% rename from crates/miden-lib/src/note/well_known_note.rs rename to crates/miden-standards/src/note/well_known_note.rs index 0a0b200217..98d44c59e2 100644 --- a/crates/miden-lib/src/note/well_known_note.rs +++ b/crates/miden-standards/src/note/well_known_note.rs @@ -1,17 +1,17 @@ use alloc::boxed::Box; -use alloc::string::String; +use alloc::string::{String, ToString}; use core::error::Error; -use miden_objects::account::AccountId; -use miden_objects::block::BlockNumber; -use miden_objects::note::{Note, NoteScript}; -use miden_objects::utils::Deserializable; -use miden_objects::utils::sync::LazyLock; -use miden_objects::vm::Program; -use miden_objects::{Felt, Word}; +use miden_protocol::account::AccountId; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::{Note, NoteScript}; +use miden_protocol::utils::Deserializable; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::vm::Program; +use miden_protocol::{Felt, Word}; use crate::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; -use crate::account::interface::{AccountComponentInterface, AccountInterface}; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::account::wallets::BasicWallet; // WELL KNOWN NOTE SCRIPTS @@ -125,10 +125,10 @@ impl WellKnownNote { const P2IDE_NUM_INPUTS: usize = 4; /// Expected number of inputs of the SWAP note. - const SWAP_NUM_INPUTS: usize = 10; + const SWAP_NUM_INPUTS: usize = 16; - /// Expected number of inputs of the MINT note. - const MINT_NUM_INPUTS: usize = 9; + /// Expected number of inputs of the MINT note (private mode). + const MINT_NUM_INPUTS_PRIVATE: usize = 8; /// Expected number of inputs of the BURN note. const BURN_NUM_INPUTS: usize = 0; @@ -169,7 +169,7 @@ impl WellKnownNote { Self::P2ID => Self::P2ID_NUM_INPUTS, Self::P2IDE => Self::P2IDE_NUM_INPUTS, Self::SWAP => Self::SWAP_NUM_INPUTS, - Self::MINT => Self::MINT_NUM_INPUTS, + Self::MINT => Self::MINT_NUM_INPUTS_PRIVATE, Self::BURN => Self::BURN_NUM_INPUTS, } } @@ -429,6 +429,27 @@ pub enum NoteConsumptionStatus { NeverConsumable(Box), } +impl Clone for NoteConsumptionStatus { + fn clone(&self) -> Self { + match self { + NoteConsumptionStatus::Consumable => NoteConsumptionStatus::Consumable, + NoteConsumptionStatus::ConsumableAfter(block_height) => { + NoteConsumptionStatus::ConsumableAfter(*block_height) + }, + NoteConsumptionStatus::ConsumableWithAuthorization => { + NoteConsumptionStatus::ConsumableWithAuthorization + }, + NoteConsumptionStatus::UnconsumableConditions => { + NoteConsumptionStatus::UnconsumableConditions + }, + NoteConsumptionStatus::NeverConsumable(error) => { + let err = error.to_string(); + NoteConsumptionStatus::NeverConsumable(err.into()) + }, + } + } +} + #[derive(thiserror::Error, Debug)] #[error("{message}")] struct StaticAnalysisError { diff --git a/crates/miden-standards/src/note/well_known_note_attachment.rs b/crates/miden-standards/src/note/well_known_note_attachment.rs new file mode 100644 index 0000000000..90ca70a5b3 --- /dev/null +++ b/crates/miden-standards/src/note/well_known_note_attachment.rs @@ -0,0 +1,18 @@ +use miden_protocol::note::NoteAttachmentScheme; + +/// The [`NoteAttachmentScheme`]s of well-known note attachmens. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum WellKnownNoteAttachment { + /// See [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) for details. + NetworkAccountTarget, +} + +impl WellKnownNoteAttachment { + /// Returns the [`NoteAttachmentScheme`] of the well-known attachment. + pub const fn attachment_scheme(&self) -> NoteAttachmentScheme { + match self { + WellKnownNoteAttachment::NetworkAccountTarget => NoteAttachmentScheme::new(1u32), + } + } +} diff --git a/crates/miden-standards/src/standards_lib.rs b/crates/miden-standards/src/standards_lib.rs new file mode 100644 index 0000000000..b8429b6acb --- /dev/null +++ b/crates/miden-standards/src/standards_lib.rs @@ -0,0 +1,72 @@ +use alloc::sync::Arc; + +use miden_protocol::assembly::Library; +use miden_protocol::assembly::mast::MastForest; +use miden_protocol::utils::serde::Deserializable; +use miden_protocol::utils::sync::LazyLock; + +// CONSTANTS +// ================================================================================================ + +const STANDARDS_LIB_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/standards.masl")); + +// MIDEN STANDARDS LIBRARY +// ================================================================================================ + +#[derive(Clone)] +pub struct StandardsLib(Library); + +impl StandardsLib { + /// Returns a reference to the [`MastForest`] of the inner [`Library`]. + pub fn mast_forest(&self) -> &Arc { + self.0.mast_forest() + } +} + +impl AsRef for StandardsLib { + fn as_ref(&self) -> &Library { + &self.0 + } +} + +impl From for Library { + fn from(value: StandardsLib) -> Self { + value.0 + } +} + +impl Default for StandardsLib { + fn default() -> Self { + static STANDARDS_LIB: LazyLock = LazyLock::new(|| { + let contents = Library::read_from_bytes(STANDARDS_LIB_BYTES) + .expect("standards lib masl should be well-formed"); + StandardsLib(contents) + }); + STANDARDS_LIB.clone() + } +} + +// TESTS +// ================================================================================================ + +// NOTE: Most standards-related tests can be found in miden-testing. +#[cfg(all(test, feature = "std"))] +mod tests { + use miden_protocol::assembly::Path; + + use super::StandardsLib; + + #[test] + fn test_compile() { + let path = Path::new("::miden::standards::faucets::basic_fungible::distribute"); + let miden = StandardsLib::default(); + let exists = miden.0.module_infos().any(|module| { + module + .procedures() + .any(|(_, proc)| module.path().join(&proc.name).as_path() == path) + }); + + assert!(exists); + } +} diff --git a/crates/miden-lib/src/testing/account_component/conditional_auth.rs b/crates/miden-standards/src/testing/account_component/conditional_auth.rs similarity index 75% rename from crates/miden-lib/src/testing/account_component/conditional_auth.rs rename to crates/miden-standards/src/testing/account_component/conditional_auth.rs index 40e847e26f..3ca719e220 100644 --- a/crates/miden-lib/src/testing/account_component/conditional_auth.rs +++ b/crates/miden-standards/src/testing/account_component/conditional_auth.rs @@ -1,21 +1,20 @@ use alloc::string::String; -use miden_objects::account::AccountComponent; -use miden_objects::assembly::Library; -use miden_objects::utils::sync::LazyLock; +use miden_protocol::account::{AccountComponent, AccountComponentCode}; +use miden_protocol::utils::sync::LazyLock; -use crate::transaction::TransactionKernel; +use crate::code_builder::CodeBuilder; pub const ERR_WRONG_ARGS_MSG: &str = "auth procedure args are incorrect"; static CONDITIONAL_AUTH_CODE: LazyLock = LazyLock::new(|| { format!( r#" - use.miden::native_account + use miden::protocol::native_account - const.WRONG_ARGS="{ERR_WRONG_ARGS_MSG}" + const WRONG_ARGS="{ERR_WRONG_ARGS_MSG}" - export.auth_conditional + pub proc auth_conditional # => [AUTH_ARGS] # If [97, 98, 99] is passed as an argument, all good. @@ -34,9 +33,9 @@ static CONDITIONAL_AUTH_CODE: LazyLock = LazyLock::new(|| { ) }); -static CONDITIONAL_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { - TransactionKernel::assembler() - .assemble_library([CONDITIONAL_AUTH_CODE.as_str()]) +static CONDITIONAL_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { + CodeBuilder::default() + .compile_component_code("mock::conditional_auth", CONDITIONAL_AUTH_CODE.as_str()) .expect("conditional auth code should be valid") }); diff --git a/crates/miden-lib/src/testing/account_component/incr_nonce.rs b/crates/miden-standards/src/testing/account_component/incr_nonce.rs similarity index 63% rename from crates/miden-lib/src/testing/account_component/incr_nonce.rs rename to crates/miden-standards/src/testing/account_component/incr_nonce.rs index 1af38a1529..34087c04eb 100644 --- a/crates/miden-lib/src/testing/account_component/incr_nonce.rs +++ b/crates/miden-standards/src/testing/account_component/incr_nonce.rs @@ -1,24 +1,26 @@ -use miden_objects::account::AccountComponent; -use miden_objects::assembly::Library; -use miden_objects::utils::sync::LazyLock; +use miden_protocol::account::AccountComponent; +use miden_protocol::assembly::Library; +use miden_protocol::utils::sync::LazyLock; -use crate::transaction::TransactionKernel; +use crate::code_builder::CodeBuilder; const INCR_NONCE_AUTH_CODE: &str = " - use.miden::native_account + use miden::protocol::native_account - export.auth_incr_nonce + pub proc auth_incr_nonce exec.native_account::incr_nonce drop end "; static INCR_NONCE_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { - TransactionKernel::assembler() - .assemble_library([INCR_NONCE_AUTH_CODE]) + CodeBuilder::default() + .compile_component_code("incr_nonce", INCR_NONCE_AUTH_CODE) .expect("incr nonce code should be valid") + .into_library() }); -/// Creates a mock authentication [`AccountComponent`] for testing purposes. +/// Creates a mock authentication [`AccountComponent`] for testing purposes under the "incr_nonce" +/// namespace. /// /// The component defines an `auth_incr_nonce` procedure that always increments the nonce by 1. pub struct IncrNonceAuthComponent; diff --git a/crates/miden-lib/src/testing/account_component/mock_account_component.rs b/crates/miden-standards/src/testing/account_component/mock_account_component.rs similarity index 91% rename from crates/miden-lib/src/testing/account_component/mock_account_component.rs rename to crates/miden-standards/src/testing/account_component/mock_account_component.rs index 91e51c6166..3b1f95dc1e 100644 --- a/crates/miden-lib/src/testing/account_component/mock_account_component.rs +++ b/crates/miden-standards/src/testing/account_component/mock_account_component.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use miden_objects::account::{AccountCode, AccountComponent, AccountStorage, StorageSlot}; +use miden_protocol::account::{AccountCode, AccountComponent, AccountStorage, StorageSlot}; use crate::testing::mock_account_code::MockAccountCodeExt; @@ -13,7 +13,7 @@ use crate::testing::mock_account_code::MockAccountCodeExt; /// arbitrary number of storage slots (within the overall limit) so anything can be set for testing /// purposes. /// -/// This component supports all [`AccountType`](miden_objects::account::AccountType)s for testing +/// This component supports all [`AccountType`](miden_protocol::account::AccountType)s for testing /// purposes. /// /// [account_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_account_library diff --git a/crates/miden-lib/src/testing/account_component/mock_faucet_component.rs b/crates/miden-standards/src/testing/account_component/mock_faucet_component.rs similarity index 84% rename from crates/miden-lib/src/testing/account_component/mock_faucet_component.rs rename to crates/miden-standards/src/testing/account_component/mock_faucet_component.rs index 88225d10ee..0520d541cc 100644 --- a/crates/miden-lib/src/testing/account_component/mock_faucet_component.rs +++ b/crates/miden-standards/src/testing/account_component/mock_faucet_component.rs @@ -1,4 +1,4 @@ -use miden_objects::account::{AccountCode, AccountComponent, AccountType}; +use miden_protocol::account::{AccountCode, AccountComponent, AccountType}; use crate::testing::mock_account_code::MockAccountCodeExt; @@ -10,7 +10,7 @@ use crate::testing::mock_account_code::MockAccountCodeExt; /// It uses the [`MockAccountCodeExt::mock_faucet_library`][faucet_lib] and contains no storage /// slots. /// -/// This component supports the faucet [`AccountType`](miden_objects::account::AccountType)s for +/// This component supports the faucet [`AccountType`](miden_protocol::account::AccountType)s for /// testing purposes. /// /// [faucet_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_faucet_library diff --git a/crates/miden-lib/src/testing/account_component/mod.rs b/crates/miden-standards/src/testing/account_component/mod.rs similarity index 100% rename from crates/miden-lib/src/testing/account_component/mod.rs rename to crates/miden-standards/src/testing/account_component/mod.rs diff --git a/crates/miden-lib/src/testing/account_interface.rs b/crates/miden-standards/src/testing/account_interface.rs similarity index 60% rename from crates/miden-lib/src/testing/account_interface.rs rename to crates/miden-standards/src/testing/account_interface.rs index dbac4986eb..932c03ee4f 100644 --- a/crates/miden-lib/src/testing/account_interface.rs +++ b/crates/miden-standards/src/testing/account_interface.rs @@ -1,13 +1,13 @@ use alloc::vec::Vec; -use miden_objects::Word; -use miden_objects::account::Account; +use miden_protocol::Word; +use miden_protocol::account::Account; -use crate::account::interface::AccountInterface; +use crate::account::interface::{AccountInterface, AccountInterfaceExt}; /// Helper function to extract public keys from an account pub fn get_public_keys_from_account(account: &Account) -> Vec { - let interface: AccountInterface = account.into(); + let interface = AccountInterface::from_account(account); interface .auth() diff --git a/crates/miden-lib/src/testing/mock_account.rs b/crates/miden-standards/src/testing/mock_account.rs similarity index 81% rename from crates/miden-lib/src/testing/mock_account.rs rename to crates/miden-standards/src/testing/mock_account.rs index 35fd96e137..e92e5f595d 100644 --- a/crates/miden-lib/src/testing/mock_account.rs +++ b/crates/miden-standards/src/testing/mock_account.rs @@ -1,4 +1,4 @@ -use miden_objects::account::{ +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, @@ -8,11 +8,10 @@ use miden_objects::account::{ StorageMap, StorageSlot, }; -use miden_objects::asset::{AssetVault, NonFungibleAsset}; -use miden_objects::testing::constants::{self}; -use miden_objects::testing::noop_auth_component::NoopAuthComponent; -use miden_objects::testing::storage::FAUCET_STORAGE_DATA_SLOT; -use miden_objects::{Felt, Word, ZERO}; +use miden_protocol::asset::{AssetVault, NonFungibleAsset}; +use miden_protocol::testing::constants::{self}; +use miden_protocol::testing::noop_auth_component::NoopAuthComponent; +use miden_protocol::{Felt, Word, ZERO}; use crate::testing::account_component::{MockAccountComponent, MockFaucetComponent}; @@ -49,8 +48,10 @@ pub trait MockAccountExt { .expect("account should be valid"); let (_id, vault, mut storage, code, nonce, _seed) = account.into_parts(); - let faucet_data_slot = Word::from([ZERO, ZERO, ZERO, initial_balance]); - storage.set_item(FAUCET_STORAGE_DATA_SLOT, faucet_data_slot).unwrap(); + let faucet_sysdata_slot = Word::from([ZERO, ZERO, ZERO, initial_balance]); + storage + .set_item(AccountStorage::faucet_sysdata_slot(), faucet_sysdata_slot) + .unwrap(); Account::new_existing(account_id, vault, storage, code, nonce) } @@ -71,8 +72,11 @@ pub trait MockAccountExt { let asset = NonFungibleAsset::mock(&constants::NON_FUNGIBLE_ASSET_DATA_2); let non_fungible_storage_map = StorageMap::with_entries([(asset.vault_key().into(), asset.into())]).unwrap(); - let storage = - AccountStorage::new(vec![StorageSlot::Map(non_fungible_storage_map)]).unwrap(); + let storage = AccountStorage::new(vec![StorageSlot::with_map( + AccountStorage::faucet_sysdata_slot().clone(), + non_fungible_storage_map, + )]) + .unwrap(); Account::new_existing(account_id, vault, storage, code, nonce) } diff --git a/crates/miden-lib/src/testing/mock_account_code.rs b/crates/miden-standards/src/testing/mock_account_code.rs similarity index 51% rename from crates/miden-lib/src/testing/mock_account_code.rs rename to crates/miden-standards/src/testing/mock_account_code.rs index 50c757ba81..cabcb23028 100644 --- a/crates/miden-lib/src/testing/mock_account_code.rs +++ b/crates/miden-standards/src/testing/mock_account_code.rs @@ -1,91 +1,92 @@ -use miden_objects::account::AccountCode; -use miden_objects::assembly::Library; -use miden_objects::assembly::diagnostics::NamedSource; -use miden_objects::utils::sync::LazyLock; +use miden_protocol::account::AccountCode; +use miden_protocol::assembly::Library; +use miden_protocol::utils::sync::LazyLock; -use crate::transaction::TransactionKernel; +use crate::code_builder::CodeBuilder; const MOCK_FAUCET_CODE: &str = " - use.miden::faucet + use miden::protocol::faucet - # Stack: [ASSET, pad(12)] - # Output: [ASSET, pad(12)] - export.mint + #! Inputs: [ASSET, pad(12)] + #! Outputs: [ASSET, pad(12)] + pub proc mint exec.faucet::mint # => [ASSET, pad(12)] end - # Stack: [ASSET, pad(12)] - # Output: [ASSET, pad(12)] - export.burn + #! Inputs: [ASSET, pad(12)] + #! Outputs: [ASSET, pad(12)] + pub proc burn exec.faucet::burn # => [ASSET, pad(12)] end "; const MOCK_ACCOUNT_CODE: &str = " - use.miden::active_account - use.miden::native_account - use.miden::tx + use miden::protocol::active_account + use miden::protocol::native_account + use miden::protocol::tx - export.::miden::contracts::wallets::basic::receive_asset - export.::miden::contracts::wallets::basic::move_asset_to_note + pub use ::miden::standards::wallets::basic::receive_asset + pub use ::miden::standards::wallets::basic::move_asset_to_note # Note: all account's export procedures below should be only called or dyncall'ed, so it # is assumed that the operand stack at the beginning of their execution is pad'ed and # does not have any other valuable information. - # Stack: [index, VALUE_TO_SET, pad(11)] - # Output: [PREVIOUS_STORAGE_VALUE, pad(12)] - export.set_item + #! Inputs: [slot_id_prefix, slot_id_suffix, VALUE, pad(10)] + #! Outputs: [OLD_VALUE, pad(12)] + pub proc set_item exec.native_account::set_item - # => [V, pad(12)] + # => [OLD_VALUE, pad(12)] end - # Stack: [index, pad(15)] - # Output: [VALUE, pad(12)] - export.get_item + #! Inputs: [slot_id_prefix, slot_id_suffix, pad(14)] + #! Outputs: [VALUE, pad(12)] + pub proc get_item exec.active_account::get_item - # => [VALUE, pad(15)] + # => [VALUE, pad(14)] # truncate the stack - movup.8 drop movup.8 drop movup.8 drop + movup.4 drop movup.4 drop # => [VALUE, pad(12)] end - # Stack: [index, pad(15)] - # Output: [VALUE, pad(12)] - export.get_initial_item + #! Inputs: [slot_id_prefix, slot_id_suffix, pad(14)] + #! Outputs: [VALUE, pad(12)] + pub proc get_initial_item exec.active_account::get_initial_item - # => [VALUE, pad(15)] + # => [VALUE, pad(14)] # truncate the stack - movup.8 drop movup.8 drop movup.8 drop + movup.4 drop movup.4 drop # => [VALUE, pad(12)] end - # Stack: [index, KEY, VALUE, pad(7)] - # Output: [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] - export.set_map_item + #! Inputs: [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE, pad(6)] + #! Outputs: [OLD_VALUE, pad(12)] + pub proc set_map_item exec.native_account::set_map_item - # => [R', V, pad(8)] + # => [OLD_VALUE, pad(12)] end - # Stack: [index, KEY, pad(11)] - # Output: [VALUE, pad(12)] - export.get_map_item + #! Inputs: [slot_id_prefix, slot_id_suffix, KEY, pad(10)] + #! Outputs: [VALUE, pad(12)] + pub proc get_map_item exec.active_account::get_map_item + # => [VALUE, pad(12)] end - # Stack: [index, KEY, pad(11)] - # Output: [VALUE, pad(12)] - export.get_initial_map_item + #! Inputs: [slot_id_prefix, slot_id_suffix, KEY, pad(10)] + #! Outputs: [INIT_VALUE, pad(12)] + pub proc get_initial_map_item exec.active_account::get_initial_map_item + # => [INIT_VALUE, pad(12)] end - # Stack: [pad(16)] - # Output: [CODE_COMMITMENT, pad(12)] - export.get_code_commitment + #! Inputs: [pad(16)] + #! Outputs: [CODE_COMMITMENT, pad(12)] + pub proc get_code_commitment exec.active_account::get_code_commitment # => [CODE_COMMITMENT, pad(16)] @@ -94,9 +95,9 @@ const MOCK_ACCOUNT_CODE: &str = " # => [CODE_COMMITMENT, pad(12)] end - # Stack: [pad(16)] - # Output: [CODE_COMMITMENT, pad(12)] - export.compute_storage_commitment + #! Inputs: [pad(16)] + #! Outputs: [CODE_COMMITMENT, pad(12)] + pub proc compute_storage_commitment exec.active_account::compute_storage_commitment # => [STORAGE_COMMITMENT, pad(16)] @@ -104,32 +105,32 @@ const MOCK_ACCOUNT_CODE: &str = " # => [STORAGE_COMMITMENT, pad(12)] end - # Stack: [ASSET, pad(12)] - # Output: [ASSET', pad(12)] - export.add_asset + #! Inputs: [ASSET, pad(12)] + #! Outputs: [ASSET', pad(12)] + pub proc add_asset exec.native_account::add_asset # => [ASSET', pad(12)] end - # Stack: [ASSET, pad(12)] - # Output: [ASSET, pad(12)] - export.remove_asset + #! Inputs: [ASSET, pad(12)] + #! Outputs: [ASSET, pad(12)] + pub proc remove_asset exec.native_account::remove_asset # => [ASSET, pad(12)] end - # Stack: [pad(16)] - # Output: [3, pad(12)] - export.account_procedure_1 + #! Inputs: [pad(16)] + #! Outputs: [3, pad(12)] + pub proc account_procedure_1 push.1.2 add # truncate the stack swap drop end - # Stack: [pad(16)] - # Output: [1, pad(12)] - export.account_procedure_2 + #! Inputs: [pad(16)] + #! Outputs: [1, pad(12)] + pub proc account_procedure_2 push.2.1 sub # truncate the stack @@ -138,17 +139,17 @@ const MOCK_ACCOUNT_CODE: &str = " "; static MOCK_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { - let source = NamedSource::new("mock::faucet", MOCK_FAUCET_CODE); - TransactionKernel::assembler() - .assemble_library([source]) + CodeBuilder::default() + .compile_component_code("mock::faucet", MOCK_FAUCET_CODE) .expect("mock faucet code should be valid") + .into_library() }); static MOCK_ACCOUNT_LIBRARY: LazyLock = LazyLock::new(|| { - let source = NamedSource::new("mock::account", MOCK_ACCOUNT_CODE); - TransactionKernel::assembler() - .assemble_library([source]) + CodeBuilder::default() + .compile_component_code("mock::account", MOCK_ACCOUNT_CODE) .expect("mock account code should be valid") + .into_library() }); // MOCK ACCOUNT CODE EXT diff --git a/crates/miden-lib/src/testing/mod.rs b/crates/miden-standards/src/testing/mod.rs similarity index 83% rename from crates/miden-lib/src/testing/mod.rs rename to crates/miden-standards/src/testing/mod.rs index fa43a14205..f08811b562 100644 --- a/crates/miden-lib/src/testing/mod.rs +++ b/crates/miden-standards/src/testing/mod.rs @@ -1,6 +1,7 @@ pub mod account_component; pub mod account_interface; + pub mod mock_account; pub mod mock_account_code; -pub mod mock_util_lib; + pub mod note; diff --git a/crates/miden-lib/src/testing/note.rs b/crates/miden-standards/src/testing/note.rs similarity index 72% rename from crates/miden-lib/src/testing/note.rs rename to crates/miden-standards/src/testing/note.rs index 34c9e372b7..4e4460f432 100644 --- a/crates/miden-lib/src/testing/note.rs +++ b/crates/miden-standards/src/testing/note.rs @@ -2,26 +2,26 @@ use alloc::string::{String, ToString}; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_objects::account::AccountId; -use miden_objects::assembly::debuginfo::{SourceLanguage, SourceManagerSync, Uri}; -use miden_objects::assembly::{DefaultSourceManager, Library}; -use miden_objects::asset::Asset; -use miden_objects::note::{ +use miden_protocol::account::AccountId; +use miden_protocol::assembly::debuginfo::{SourceLanguage, SourceManagerSync, Uri}; +use miden_protocol::assembly::{DefaultSourceManager, Library}; +use miden_protocol::asset::Asset; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, + NoteAttachment, NoteInputs, NoteMetadata, NoteRecipient, - NoteScript, NoteTag, NoteType, }; -use miden_objects::testing::note::DEFAULT_NOTE_CODE; -use miden_objects::{Felt, NoteError, Word, ZERO}; +use miden_protocol::testing::note::DEFAULT_NOTE_CODE; +use miden_protocol::{Felt, Word}; use rand::Rng; -use crate::transaction::TransactionKernel; +use crate::code_builder::CodeBuilder; // NOTE BUILDER // ================================================================================================ @@ -32,11 +32,10 @@ pub struct NoteBuilder { inputs: Vec, assets: Vec, note_type: NoteType, - note_execution_hint: NoteExecutionHint, serial_num: Word, tag: NoteTag, code: String, - aux: Felt, + attachment: NoteAttachment, dyn_libraries: Vec, source_manager: Arc, } @@ -55,12 +54,11 @@ impl NoteBuilder { inputs: vec![], assets: vec![], note_type: NoteType::Public, - note_execution_hint: NoteExecutionHint::None, serial_num, // The note tag is not under test, so we choose a value that is always valid. - tag: NoteTag::from_account_id(sender), + tag: NoteTag::with_account_target(sender), code: DEFAULT_NOTE_CODE.to_string(), - aux: ZERO, + attachment: NoteAttachment::default(), dyn_libraries: Vec::new(), source_manager: Arc::new(DefaultSourceManager::default()), } @@ -83,11 +81,6 @@ impl NoteBuilder { self } - pub fn note_execution_hint(mut self, note_execution_hint: NoteExecutionHint) -> Self { - self.note_execution_hint = note_execution_hint; - self - } - pub fn tag(mut self, tag: u32) -> Self { self.tag = tag.into(); self @@ -109,8 +102,9 @@ impl NoteBuilder { self } - pub fn aux(mut self, aux: Felt) -> Self { - self.aux = aux; + /// Overwrites the attachment. + pub fn attachment(mut self, attachment: impl Into) -> Self { + self.attachment = attachment.into(); self } @@ -144,29 +138,19 @@ impl NoteBuilder { self.code, ); - let mut assembler = TransactionKernel::assembler_with_source_manager(self.source_manager) - .with_debug_mode(true); + let mut builder = CodeBuilder::with_source_manager(self.source_manager.clone()); for dyn_library in self.dyn_libraries { - assembler - .link_dynamic_library(dyn_library) + builder + .link_dynamic_library(&dyn_library) .expect("library should link successfully"); } - let code = assembler - .clone() - .with_debug_mode(true) - .assemble_program(virtual_source_file) - .unwrap(); - - let note_script = NoteScript::new(code); + let note_script = builder + .compile_note_script(virtual_source_file) + .expect("note script should compile"); let vault = NoteAssets::new(self.assets)?; - let metadata = NoteMetadata::new( - self.sender, - self.note_type, - self.tag, - self.note_execution_hint, - self.aux, - )?; + let metadata = NoteMetadata::new(self.sender, self.note_type, self.tag) + .with_attachment(self.attachment); let inputs = NoteInputs::new(self.inputs)?; let recipient = NoteRecipient::new(self.serial_num, note_script, inputs); diff --git a/crates/miden-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index 01e4fe2fc2..af565ab386 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -13,31 +13,36 @@ rust-version.workspace = true version.workspace = true [features] -std = ["miden-lib/std"] +std = ["miden-protocol/std", "miden-standards/std", "miden-tx/std"] [dependencies] # Workspace dependencies +miden-agglayer = { features = ["testing"], workspace = true } miden-block-prover = { features = ["testing"], workspace = true } -miden-lib = { features = ["testing"], workspace = true } -miden-objects = { features = ["testing"], workspace = true } +miden-protocol = { features = ["testing"], workspace = true } +miden-standards = { features = ["testing"], workspace = true } miden-tx = { features = ["testing"], workspace = true } miden-tx-batch-prover = { features = ["testing"], workspace = true } # Miden dependencies +miden-assembly = { workspace = true } +miden-core-lib = { workspace = true } miden-processor = { workspace = true } # External dependencies anyhow = { workspace = true } itertools = { default-features = false, features = ["use_alloc"], version = "0.14" } rand = { features = ["os_rng", "small_rng"], workspace = true } -rand_chacha = { default-features = false, version = "0.9" } -thiserror = { workspace = true } +rand_chacha = { workspace = true } winterfell = { version = "0.13" } [dev-dependencies] anyhow = { features = ["backtrace", "std"], workspace = true } assert_matches = { workspace = true } -miden-objects = { features = ["std"], workspace = true } +hex = { version = "0.4" } +miden-crypto = { workspace = true } +miden-protocol = { features = ["std"], workspace = true } +primitive-types = { workspace = true } rstest = { workspace = true } tokio = { features = ["macros", "rt"], workspace = true } winter-rand-utils = { version = "0.13" } diff --git a/crates/miden-testing/src/executor.rs b/crates/miden-testing/src/executor.rs index a8ae18cc2e..02b79705ed 100644 --- a/crates/miden-testing/src/executor.rs +++ b/crates/miden-testing/src/executor.rs @@ -2,6 +2,8 @@ use miden_processor::DefaultHost; use miden_processor::fast::{ExecutionOutput, FastProcessor}; use miden_processor::{AdviceInputs, AsyncHost, ExecutionError, Program, StackInputs}; +#[cfg(test)] +use miden_protocol::assembly::Assembler; // CODE EXECUTOR // ================================================================================================ @@ -37,18 +39,18 @@ impl CodeExecutor { /// Compiles and runs the desired code in the host and returns the [`Process`] state. /// /// To improve the error message quality, convert the returned [`ExecutionError`] into a - /// [`Report`](miden_objects::assembly::diagnostics::Report). + /// [`Report`](miden_protocol::assembly::diagnostics::Report). #[cfg(test)] pub async fn run(self, code: &str) -> Result { use alloc::borrow::ToOwned; use alloc::sync::Arc; - use miden_lib::transaction::TransactionKernel; - use miden_objects::assembly::debuginfo::{SourceLanguage, Uri}; - use miden_objects::assembly::{DefaultSourceManager, SourceManagerSync}; + use miden_protocol::assembly::debuginfo::{SourceLanguage, Uri}; + use miden_protocol::assembly::{DefaultSourceManager, SourceManagerSync}; + use miden_standards::code_builder::CodeBuilder; let source_manager: Arc = Arc::new(DefaultSourceManager::default()); - let assembler = TransactionKernel::with_kernel_library(source_manager.clone()); + let assembler: Assembler = CodeBuilder::with_kernel_library(source_manager.clone()).into(); // Virtual file name should be unique. let virtual_source_file = @@ -61,7 +63,7 @@ impl CodeExecutor { /// Executes the provided [`Program`] and returns the [`Process`] state. /// /// To improve the error message quality, convert the returned [`ExecutionError`] into a - /// [`Report`](miden_objects::assembly::diagnostics::Report). + /// [`Report`](miden_protocol::assembly::diagnostics::Report). pub async fn execute_program( mut self, program: Program, @@ -86,7 +88,7 @@ impl CodeExecutor { #[cfg(test)] impl CodeExecutor { pub fn with_default_host() -> Self { - use miden_lib::transaction::TransactionKernel; + use miden_protocol::transaction::TransactionKernel; let mut host = DefaultHost::default(); diff --git a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs index 058d6a7f80..2d44e187b9 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs @@ -3,16 +3,17 @@ use std::collections::BTreeMap; use anyhow::Context; use assert_matches::assert_matches; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::testing::note::NoteBuilder; -use miden_objects::account::{Account, AccountId, AccountStorageMode}; -use miden_objects::batch::ProposedBatch; -use miden_objects::block::BlockNumber; -use miden_objects::crypto::merkle::MerkleError; -use miden_objects::note::{Note, NoteType}; -use miden_objects::testing::account_id::AccountIdBuilder; -use miden_objects::transaction::{InputNote, InputNoteCommitment, OutputNote, PartialBlockchain}; -use miden_objects::{BatchAccountUpdateError, ProposedBatchError, Word}; +use miden_protocol::Word; +use miden_protocol::account::{Account, AccountId, AccountStorageMode}; +use miden_protocol::batch::ProposedBatch; +use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::merkle::MerkleError; +use miden_protocol::errors::{BatchAccountUpdateError, ProposedBatchError}; +use miden_protocol::note::{Note, NoteType}; +use miden_protocol::testing::account_id::AccountIdBuilder; +use miden_protocol::transaction::{InputNote, InputNoteCommitment, OutputNote, PartialBlockchain}; +use miden_standards::testing::account_component::MockAccountComponent; +use miden_standards::testing::note::NoteBuilder; use rand::rngs::SmallRng; use rand::{Rng, SeedableRng}; @@ -293,20 +294,30 @@ async fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> let block2 = chain.prove_next_block()?; let block3 = chain.prove_next_block()?; - assert_eq!(block1.output_notes().count(), 2, "block 1 should contain note1 and note2"); + assert_eq!( + block1.body().output_notes().count(), + 2, + "block 1 should contain note1 and note2" + ); assert!( - block1.output_notes().any(|(_, note)| note.commitment() == note1.commitment()), + block1 + .body() + .output_notes() + .any(|(_, note)| note.commitment() == note1.commitment()), "block 1 should contain note1" ); assert!( - block1.output_notes().any(|(_, note)| note.commitment() == note2.commitment()), + block1 + .body() + .output_notes() + .any(|(_, note)| note.commitment() == note2.commitment()), "block 1 should contain note2" ); // Consume the authenticated note as an unauthenticated one in the transaction. let tx1 = MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.commitment()) - .ref_block_commitment(block2.commitment()) + .ref_block_commitment(block2.header().commitment()) .unauthenticated_notes(vec![note2.clone()]) .build()?; diff --git a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs index 3c583dd06a..95be1c04c5 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs @@ -1,19 +1,19 @@ use alloc::vec::Vec; use anyhow::Context; -use miden_objects::Word; -use miden_objects::account::AccountId; -use miden_objects::asset::FungibleAsset; -use miden_objects::block::BlockNumber; -use miden_objects::crypto::merkle::SparseMerklePath; -use miden_objects::note::{Note, NoteInclusionProof, Nullifier}; -use miden_objects::transaction::{ +use miden_protocol::Word; +use miden_protocol::account::AccountId; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::merkle::SparseMerklePath; +use miden_protocol::note::{Note, NoteInclusionProof, Nullifier}; +use miden_protocol::transaction::{ InputNote, OutputNote, ProvenTransaction, ProvenTransactionBuilder, }; -use miden_objects::vm::ExecutionProof; +use miden_protocol::vm::ExecutionProof; /// A builder to build mocked [`ProvenTransaction`]s. pub struct MockProvenTxBuilder { diff --git a/crates/miden-testing/src/kernel_tests/block/proven_block_error.rs b/crates/miden-testing/src/kernel_tests/block/header_errors.rs similarity index 85% rename from crates/miden-testing/src/kernel_tests/block/proven_block_error.rs rename to crates/miden-testing/src/kernel_tests/block/header_errors.rs index f50937fdbb..e3d3864f3f 100644 --- a/crates/miden-testing/src/kernel_tests/block/proven_block_error.rs +++ b/crates/miden-testing/src/kernel_tests/block/header_errors.rs @@ -2,18 +2,25 @@ use alloc::vec::Vec; use anyhow::Context; use assert_matches::assert_matches; -use miden_block_prover::{LocalBlockProver, ProvenBlockError}; -use miden_lib::testing::account_component::{IncrNonceAuthComponent, MockAccountComponent}; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{Account, AccountBuilder, AccountComponent, AccountId, StorageSlot}; -use miden_objects::asset::FungibleAsset; -use miden_objects::batch::ProvenBatch; -use miden_objects::block::{BlockInputs, BlockNumber, ProposedBlock}; -use miden_objects::note::NoteType; -use miden_objects::transaction::ProvenTransactionBuilder; -use miden_objects::vm::ExecutionProof; -use miden_objects::{AccountTreeError, NullifierTreeError, Word}; +use miden_protocol::Word; +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountId, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::batch::ProvenBatch; +use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock}; +use miden_protocol::errors::{AccountTreeError, NullifierTreeError, ProposedBlockError}; +use miden_protocol::note::NoteType; +use miden_protocol::transaction::ProvenTransactionBuilder; +use miden_protocol::vm::ExecutionProof; +use miden_standards::testing::account_component::{IncrNonceAuthComponent, MockAccountComponent}; +use miden_standards::testing::mock_account::MockAccountExt; use miden_tx::LocalTransactionProver; use crate::kernel_tests::block::utils::MockChainBlockExt; @@ -73,10 +80,10 @@ async fn witness_test_setup() -> anyhow::Result { }) } -/// Tests that a proven block cannot be built if witnesses from a stale account tree are used +/// Tests that a block cannot be built if witnesses from a stale account tree are used /// (i.e. an account tree whose root is not in the previous block header). #[tokio::test] -async fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { +async fn block_building_fails_on_stale_account_witnesses() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -97,11 +104,11 @@ async fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { let proposed_block0 = ProposedBlock::new(invalid_account_tree_block_inputs, batches.clone()) .context("failed to propose block 0")?; - let error = LocalBlockProver::new(0).prove_dummy(proposed_block0).unwrap_err(); + let error = proposed_block0.into_header_and_body().unwrap_err(); assert_matches!( error, - ProvenBlockError::StaleAccountTreeRoot { + ProposedBlockError::StaleAccountTreeRoot { prev_block_account_root, .. } if prev_block_account_root == valid_block_inputs.prev_block_header().account_root() @@ -110,10 +117,10 @@ async fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { Ok(()) } -/// Tests that a proven block cannot be built if witnesses from a stale nullifier tree are used +/// Tests that a block cannot be built if witnesses from a stale nullifier tree are used /// (i.e. a nullifier tree whose root is not in the previous block header). #[tokio::test] -async fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> { +async fn block_building_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -134,11 +141,11 @@ async fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> let proposed_block2 = ProposedBlock::new(invalid_nullifier_tree_block_inputs, batches.clone()) .context("failed to propose block 2")?; - let error = LocalBlockProver::new(0).prove_dummy(proposed_block2).unwrap_err(); + let error = proposed_block2.into_header_and_body().unwrap_err(); assert_matches!( error, - ProvenBlockError::StaleNullifierTreeRoot { + ProposedBlockError::StaleNullifierTreeRoot { prev_block_nullifier_root, .. } if prev_block_nullifier_root == valid_block_inputs.prev_block_header().nullifier_root() @@ -147,10 +154,10 @@ async fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> Ok(()) } -/// Tests that a proven block cannot be built if both witnesses from a stale account tree and from +/// Tests that a block cannot be built if both witnesses from a stale account tree and from /// the current account tree are used which results in different account tree roots. #[tokio::test] -async fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> { +async fn block_building_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -180,11 +187,11 @@ async fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<() let proposed_block1 = ProposedBlock::new(stale_account_witness_block_inputs, batches.clone()) .context("failed to propose block 1")?; - let error = LocalBlockProver::new(0).prove_dummy(proposed_block1).unwrap_err(); + let error = proposed_block1.into_header_and_body().unwrap_err(); assert_matches!( error, - ProvenBlockError::AccountWitnessTracking { + ProposedBlockError::AccountWitnessTracking { source: AccountTreeError::TreeRootConflict { .. }, .. } @@ -193,10 +200,10 @@ async fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<() Ok(()) } -/// Tests that a proven block cannot be built if both witnesses from a stale nullifier tree and from +/// Tests that a block cannot be built if both witnesses from a stale nullifier tree and from /// the current nullifier tree are used which results in different nullifier tree roots. #[tokio::test] -async fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> { +async fn block_building_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -228,11 +235,11 @@ async fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result< let proposed_block3 = ProposedBlock::new(invalid_nullifier_witness_block_inputs, batches) .context("failed to propose block 3")?; - let error = LocalBlockProver::new(0).prove_dummy(proposed_block3).unwrap_err(); + let error = proposed_block3.into_header_and_body().unwrap_err(); assert_matches!( error, - ProvenBlockError::NullifierWitnessRootMismatch(NullifierTreeError::TreeRootConflict(_)) + ProposedBlockError::NullifierWitnessRootMismatch(NullifierTreeError::TreeRootConflict(_)) ); Ok(()) @@ -241,7 +248,7 @@ async fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result< /// Tests that creating an account when an existing account with the same account ID prefix exists, /// results in an error. #[tokio::test] -async fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() +async fn block_building_fails_on_creating_account_with_existing_account_id_prefix() -> anyhow::Result<()> { // Construct a new account. // -------------------------------------------------------------------------------------------- @@ -252,9 +259,10 @@ async fn proven_block_fails_on_creating_account_with_existing_account_id_prefix( let account = AccountBuilder::new([5; 32]) .with_auth_component(auth_component.clone()) - .with_component(MockAccountComponent::with_slots(vec![StorageSlot::Value(Word::from( - [5u32; 4], - ))])) + .with_component(MockAccountComponent::with_slots(vec![StorageSlot::with_value( + StorageSlotName::new("miden::test_slot")?, + Word::from([5u32; 4]), + )])) .build() .context("failed to build account")?; @@ -321,12 +329,12 @@ async fn proven_block_fails_on_creating_account_with_existing_account_id_prefix( let block = mock_chain.propose_block(batches).context("failed to propose block")?; - let err = LocalBlockProver::new(0).prove_dummy(block).unwrap_err(); + let err = block.into_header_and_body().unwrap_err(); // This should fail when we try to _insert_ the same two prefixes into the partial tree. assert_matches!( err, - ProvenBlockError::AccountIdPrefixDuplicate { + ProposedBlockError::AccountIdPrefixDuplicate { source: AccountTreeError::DuplicateIdPrefix { duplicate_prefix } } if duplicate_prefix == new_id.prefix() ); @@ -336,16 +344,17 @@ async fn proven_block_fails_on_creating_account_with_existing_account_id_prefix( /// Tests that creating two accounts in the same block whose ID prefixes match, results in an error. #[tokio::test] -async fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix() +async fn block_building_fails_on_creating_account_with_duplicate_account_id_prefix() -> anyhow::Result<()> { // Construct a new account. // -------------------------------------------------------------------------------------------- let mock_chain = MockChain::new(); let account = AccountBuilder::new([5; 32]) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![StorageSlot::Value(Word::from( - [5u32; 4], - ))])) + .with_component(MockAccountComponent::with_slots(vec![StorageSlot::with_value( + StorageSlotName::new("miden::test_slot")?, + Word::from([5u32; 4]), + )])) .build() .context("failed to build account")?; @@ -417,12 +426,12 @@ async fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix let block = mock_chain.propose_block(batches).context("failed to propose block")?; - let err = LocalBlockProver::new(0).prove_dummy(block).unwrap_err(); + let err = block.into_header_and_body().unwrap_err(); // This should fail when we try to _track_ the same two prefixes in the partial tree. assert_matches!( err, - ProvenBlockError::AccountWitnessTracking { + ProposedBlockError::AccountWitnessTracking { source: AccountTreeError::DuplicateIdPrefix { duplicate_prefix } } if duplicate_prefix == id0.prefix() ); diff --git a/crates/miden-testing/src/kernel_tests/block/mod.rs b/crates/miden-testing/src/kernel_tests/block/mod.rs index ecd426ab64..b55f773b60 100644 --- a/crates/miden-testing/src/kernel_tests/block/mod.rs +++ b/crates/miden-testing/src/kernel_tests/block/mod.rs @@ -1,6 +1,6 @@ mod proposed_block_errors; mod proposed_block_success; -mod proven_block_error; +mod header_errors; mod proven_block_success; pub(crate) mod utils; diff --git a/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs b/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs index cc7aabf71f..f7fea9e494 100644 --- a/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs +++ b/crates/miden-testing/src/kernel_tests/block/proposed_block_errors.rs @@ -3,13 +3,14 @@ use std::collections::BTreeMap; use std::vec::Vec; use assert_matches::assert_matches; -use miden_lib::note::create_p2id_note; -use miden_objects::asset::FungibleAsset; -use miden_objects::block::{BlockInputs, BlockNumber, ProposedBlock}; -use miden_objects::crypto::merkle::SparseMerklePath; -use miden_objects::note::{NoteInclusionProof, NoteType}; -use miden_objects::{MAX_BATCHES_PER_BLOCK, ProposedBlockError, ZERO}; use miden_processor::crypto::MerklePath; +use miden_protocol::MAX_BATCHES_PER_BLOCK; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock}; +use miden_protocol::crypto::merkle::SparseMerklePath; +use miden_protocol::errors::ProposedBlockError; +use miden_protocol::note::{NoteAttachment, NoteInclusionProof, NoteType}; +use miden_standards::note::create_p2id_note; use miden_tx::LocalTransactionProver; use crate::kernel_tests::block::utils::MockChainBlockExt; @@ -352,7 +353,7 @@ async fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_referen account1.id(), vec![], NoteType::Private, - ZERO, + NoteAttachment::default(), builder.rng_mut(), )?; let spawn_note = builder.add_spawn_note([&p2id_note])?; @@ -418,7 +419,7 @@ async fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_referen .expect("note proof should have been fetched") .clone(); let mut original_merkle_path = MerklePath::from(original_note_proof.note_path().clone()); - original_merkle_path.push(block2.commitment()); + original_merkle_path.push(block2.header().commitment()); // Add a random hash to the path to make it invalid. let invalid_note_path = SparseMerklePath::try_from(original_merkle_path).unwrap(); let invalid_note_proof = NoteInclusionProof::new( diff --git a/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs b/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs index e9ab238fe9..cce09fcaaa 100644 --- a/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs +++ b/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs @@ -4,16 +4,16 @@ use std::vec::Vec; use anyhow::Context; use assert_matches::assert_matches; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::testing::note::NoteBuilder; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{Account, AccountId, AccountStorageMode}; -use miden_objects::asset::FungibleAsset; -use miden_objects::block::{BlockInputs, ProposedBlock}; -use miden_objects::note::{Note, NoteType}; -use miden_objects::testing::account_id::ACCOUNT_ID_SENDER; -use miden_objects::transaction::{ExecutedTransaction, OutputNote, TransactionHeader}; -use miden_objects::{Felt, FieldElement}; +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{Account, AccountId, AccountStorageMode}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::block::{BlockInputs, ProposedBlock}; +use miden_protocol::note::{Note, NoteType}; +use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; +use miden_protocol::transaction::{ExecutedTransaction, OutputNote, TransactionHeader}; +use miden_protocol::{Felt, FieldElement}; +use miden_standards::testing::account_component::MockAccountComponent; +use miden_standards::testing::note::NoteBuilder; use miden_tx::LocalTransactionProver; use rand::Rng; diff --git a/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs b/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs index ab324b942c..573b5e81d8 100644 --- a/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs +++ b/crates/miden-testing/src/kernel_tests/block/proven_block_success.rs @@ -3,16 +3,14 @@ use std::collections::BTreeMap; use std::vec::Vec; use anyhow::Context; -use miden_block_prover::LocalBlockProver; -use miden_lib::note::create_p2id_note; -use miden_objects::asset::FungibleAsset; -use miden_objects::batch::BatchNoteTree; -use miden_objects::block::account_tree::AccountTree; -use miden_objects::block::{BlockInputs, BlockNoteIndex, BlockNoteTree, ProposedBlock}; -use miden_objects::crypto::merkle::Smt; -use miden_objects::note::NoteType; -use miden_objects::transaction::InputNoteCommitment; -use miden_objects::{MIN_PROOF_SECURITY_LEVEL, ZERO}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::batch::BatchNoteTree; +use miden_protocol::block::account_tree::AccountTree; +use miden_protocol::block::{BlockInputs, BlockNoteIndex, BlockNoteTree, ProposedBlock}; +use miden_protocol::crypto::merkle::smt::Smt; +use miden_protocol::note::{NoteAttachment, NoteType}; +use miden_protocol::transaction::InputNoteCommitment; +use miden_standards::note::create_p2id_note; use crate::kernel_tests::block::utils::MockChainBlockExt; use crate::utils::create_p2any_note; @@ -39,7 +37,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account0.id(), vec![asset], NoteType::Private, - ZERO, + NoteAttachment::default(), builder.rng_mut(), )?; let output_note1 = create_p2id_note( @@ -47,7 +45,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account1.id(), vec![asset], NoteType::Private, - ZERO, + NoteAttachment::default(), builder.rng_mut(), )?; let output_note2 = create_p2id_note( @@ -55,7 +53,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account2.id(), vec![asset], NoteType::Private, - ZERO, + NoteAttachment::default(), builder.rng_mut(), )?; let output_note3 = create_p2id_note( @@ -63,7 +61,7 @@ async fn proven_block_success() -> anyhow::Result<()> { account3.id(), vec![asset], NoteType::Private, - ZERO, + NoteAttachment::default(), builder.rng_mut(), )?; @@ -94,10 +92,9 @@ async fn proven_block_success() -> anyhow::Result<()> { // Sanity check: Batches should have two output notes each. assert_eq!(batch0.output_notes().len(), 2); assert_eq!(batch1.output_notes().len(), 2); + let batches = vec![batch0.clone(), batch1.clone()]; - let proposed_block = chain - .propose_block([batch0.clone(), batch1.clone()]) - .context("failed to propose block")?; + let proposed_block = chain.propose_block(batches.clone()).context("failed to propose block")?; // Compute expected block note tree. // -------------------------------------------------------------------------------------------- @@ -118,7 +115,7 @@ async fn proven_block_success() -> anyhow::Result<()> { ( BlockNoteIndex::new(batch_idx, note_idx_in_batch).unwrap(), note.id(), - *note.metadata(), + note.metadata(), ) }, )) @@ -147,9 +144,7 @@ async fn proven_block_success() -> anyhow::Result<()> { // Prove block. // -------------------------------------------------------------------------------------------- - let proven_block = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL) - .prove_dummy(proposed_block) - .context("failed to prove proposed block")?; + let proven_block = chain.prove_block(proposed_block.clone())?; // Check tree/chain commitments against expected values. // -------------------------------------------------------------------------------------------- @@ -168,27 +163,27 @@ async fn proven_block_success() -> anyhow::Result<()> { assert_eq!(proven_block.header().note_root(), expected_block_note_tree.root()); // Assert that the block note tree can be reconstructed. - assert_eq!(proven_block.build_output_note_tree(), expected_block_note_tree); + assert_eq!(proven_block.body().compute_block_note_tree(), expected_block_note_tree); // Check input notes / nullifiers. // -------------------------------------------------------------------------------------------- - assert_eq!(proven_block.created_nullifiers().len(), 4); - assert!(proven_block.created_nullifiers().contains(&input_note0.nullifier())); - assert!(proven_block.created_nullifiers().contains(&input_note1.nullifier())); - assert!(proven_block.created_nullifiers().contains(&input_note2.nullifier())); - assert!(proven_block.created_nullifiers().contains(&input_note3.nullifier())); + assert_eq!(proven_block.body().created_nullifiers().len(), 4); + assert!(proven_block.body().created_nullifiers().contains(&input_note0.nullifier())); + assert!(proven_block.body().created_nullifiers().contains(&input_note1.nullifier())); + assert!(proven_block.body().created_nullifiers().contains(&input_note2.nullifier())); + assert!(proven_block.body().created_nullifiers().contains(&input_note3.nullifier())); // Check output notes. // -------------------------------------------------------------------------------------------- - assert_eq!(proven_block.output_note_batches().len(), 2); + assert_eq!(proven_block.body().output_note_batches().len(), 2); assert_eq!( - proven_block.output_note_batches()[0], + proven_block.body().output_note_batches()[0], batch0.output_notes().iter().cloned().enumerate().collect::>() ); assert_eq!( - proven_block.output_note_batches()[1], + proven_block.body().output_note_batches()[1], batch1.output_notes().iter().cloned().enumerate().collect::>() ); @@ -199,6 +194,7 @@ async fn proven_block_success() -> anyhow::Result<()> { for (tx, batch) in [(&tx0, &batch0), (&tx1, &batch0), (&tx2, &batch1), (&tx3, &batch1)] { let updated_account = tx.account_id(); let block_account_update = proven_block + .body() .updated_accounts() .iter() .find(|update| update.account_id() == updated_account) @@ -342,10 +338,8 @@ async fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { assert_eq!(output_notes_batch0.len(), 2); assert_eq!(output_notes_batch0, &expected_output_notes_batch0); - let proven_block = LocalBlockProver::new(0) - .prove_dummy(proposed_block) - .context("failed to prove block")?; - let actual_block_note_tree = proven_block.build_output_note_tree(); + let proven_block = chain.prove_block(proposed_block.clone())?; + let actual_block_note_tree = proven_block.body().compute_block_note_tree(); // Remove the erased note to get the expected batch note tree. let mut batch_tree = BatchNoteTree::with_contiguous_leaves( @@ -390,7 +384,7 @@ async fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- let latest_block_header = chain.latest_block_header(); - assert_eq!(latest_block_header.commitment(), blockx.commitment()); + assert_eq!(latest_block_header.commitment(), blockx.header().commitment()); // Sanity check: The account and nullifier tree roots should not be the empty tree roots. assert_ne!(latest_block_header.account_root(), AccountTree::::default().root()); @@ -407,18 +401,17 @@ async fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> { BTreeMap::default(), ); + let batches = Vec::new(); let proposed_block = - ProposedBlock::new(block_inputs, Vec::new()).context("failed to propose block")?; + ProposedBlock::new(block_inputs, batches.clone()).context("failed to propose block")?; - let proven_block = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL) - .prove_dummy(proposed_block) - .context("failed to prove proposed block")?; + let proven_block = chain.prove_block(proposed_block.clone())?; // Nothing should be created or updated. - assert_eq!(proven_block.updated_accounts().len(), 0); - assert_eq!(proven_block.output_note_batches().len(), 0); - assert_eq!(proven_block.created_nullifiers().len(), 0); - assert!(proven_block.build_output_note_tree().is_empty()); + assert_eq!(proven_block.body().updated_accounts().len(), 0); + assert_eq!(proven_block.body().output_note_batches().len(), 0); + assert_eq!(proven_block.body().created_nullifiers().len(), 0); + assert!(proven_block.body().compute_block_note_tree().is_empty()); // Account and nullifier root should match the previous block header's roots, since nothing has // changed. diff --git a/crates/miden-testing/src/kernel_tests/block/utils.rs b/crates/miden-testing/src/kernel_tests/block/utils.rs index 20e407616b..f3b5de580e 100644 --- a/crates/miden-testing/src/kernel_tests/block/utils.rs +++ b/crates/miden-testing/src/kernel_tests/block/utils.rs @@ -1,11 +1,11 @@ use std::vec::Vec; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::AccountId; -use miden_objects::batch::ProvenBatch; -use miden_objects::block::BlockNumber; -use miden_objects::note::{Note, NoteId}; -use miden_objects::transaction::{ExecutedTransaction, ProvenTransaction, TransactionScript}; +use miden_protocol::account::AccountId; +use miden_protocol::batch::ProvenBatch; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::{Note, NoteId}; +use miden_protocol::transaction::{ExecutedTransaction, ProvenTransaction, TransactionScript}; +use miden_standards::code_builder::CodeBuilder; use miden_tx::LocalTransactionProver; use crate::{MockChain, TxContextInput}; @@ -101,7 +101,7 @@ impl MockChainBlockExt for MockChain { fn update_expiration_tx_script(expiration_delta: u16) -> TransactionScript { let code = format!( " - use.miden::tx + use miden::protocol::tx begin push.{expiration_delta} @@ -110,5 +110,5 @@ fn update_expiration_tx_script(expiration_delta: u16) -> TransactionScript { " ); - ScriptBuilder::default().compile_tx_script(code).unwrap() + CodeBuilder::default().compile_tx_script(code).unwrap() } diff --git a/crates/miden-testing/src/kernel_tests/tx/mod.rs b/crates/miden-testing/src/kernel_tests/tx/mod.rs index 781bae46dc..c3a9f84f77 100644 --- a/crates/miden-testing/src/kernel_tests/tx/mod.rs +++ b/crates/miden-testing/src/kernel_tests/tx/mod.rs @@ -1,31 +1,17 @@ -use alloc::string::String; - use anyhow::Context; -use miden_lib::transaction::memory::{ - self, - MemoryOffset, - NOTE_MEM_SIZE, - NUM_OUTPUT_NOTES_PTR, - OUTPUT_NOTE_ASSETS_OFFSET, - OUTPUT_NOTE_DIRTY_FLAG_OFFSET, - OUTPUT_NOTE_METADATA_OFFSET, - OUTPUT_NOTE_NUM_ASSETS_OFFSET, - OUTPUT_NOTE_RECIPIENT_OFFSET, - OUTPUT_NOTE_SECTION_OFFSET, -}; -use miden_objects::account::{Account, AccountId}; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::note::{Note, NoteType}; -use miden_objects::testing::account_id::{ +use miden_processor::ContextId; +use miden_processor::fast::ExecutionOutput; +use miden_protocol::account::{Account, AccountId}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::note::{Note, NoteType}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_SENDER, }; -use miden_objects::testing::storage::prepare_assets; -use miden_objects::vm::StackInputs; -use miden_objects::{Felt, Word, ZERO}; -use miden_processor::ContextId; -use miden_processor::fast::ExecutionOutput; +use miden_protocol::transaction::memory::{self, MemoryOffset}; +use miden_protocol::vm::StackInputs; +use miden_protocol::{Felt, Word, ZERO}; use crate::MockChain; @@ -59,16 +45,9 @@ pub trait ExecutionOutputExt { /// Reads an element from transaction kernel memory or returns [`ZERO`] if that location is not /// initialized. - fn get_kernel_mem_element(&self, addr: u32) -> Felt { - // TODO: Use Memory::read_element once it no longer requires &mut self. - // https://github.com/0xMiden/miden-vm/issues/2237 - - // Copy of how Memory::read_element is implemented in Miden VM. - let idx = addr % miden_objects::WORD_SIZE as u32; - let word_addr = addr - idx; - - self.get_kernel_mem_word(word_addr)[idx as usize] - } + // Unused for now, but may become useful in the future. + #[allow(dead_code)] + fn get_kernel_mem_element(&self, addr: u32) -> Felt; /// Reads an element from the stack. fn get_stack_element(&self, idx: usize) -> Felt; @@ -109,70 +88,19 @@ impl ExecutionOutputExt for ExecutionOutput { fn get_stack_word_le(&self, index: usize) -> Word { self.stack.get_stack_word_le(index).expect("index must be in bounds") } -} -pub fn input_note_data_ptr(note_idx: u32) -> memory::MemoryAddress { - memory::INPUT_NOTE_DATA_SECTION_OFFSET + note_idx * memory::NOTE_MEM_SIZE -} + fn get_kernel_mem_element(&self, addr: u32) -> Felt { + let tx_kernel_context = ContextId::root(); + let err_ctx = (); -/// Returns MASM code that defines a procedure called `create_mock_notes` which creates the notes -/// specified in `notes`, which stores output note metadata in the transaction host's memory. -pub fn create_mock_notes_procedure(notes: &[Note]) -> String { - if notes.is_empty() { - return String::new(); + self.memory + .read_element(tx_kernel_context, Felt::from(addr), &err_ctx) + .expect("address converted from u32 should be in bounds") } +} - let mut script = String::from( - "proc.create_mock_notes - # remove padding from prologue - dropw dropw dropw dropw - ", - ); - - for (idx, note) in notes.iter().enumerate() { - let metadata = Word::from(note.metadata()); - let recipient = note.recipient().digest(); - let assets = prepare_assets(note.assets()); - let num_assets = assets.len(); - let note_offset = (idx as u32) * NOTE_MEM_SIZE; - - assert!(num_assets == 1, "notes are expected to have one asset only"); - - script.push_str(&format!( - " - # populate note {idx} - push.{metadata} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_METADATA_OFFSET} add add mem_storew_be dropw - - push.{recipient} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_RECIPIENT_OFFSET} add add mem_storew_be dropw - - push.{num_assets} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_NUM_ASSETS_OFFSET} add add mem_store - - push.1 # dirty flag should be `1` by default - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_DIRTY_FLAG_OFFSET} add add mem_store - - push.{first_asset} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_ASSETS_OFFSET} add add mem_storew_be dropw - ", - idx = idx, - metadata = metadata, - recipient = recipient, - num_assets = num_assets, - first_asset = assets[0], - note_offset = note_offset, - )); - } - script.push_str(&format!( - "# set num output notes - push.{count} push.{NUM_OUTPUT_NOTES_PTR} mem_store - end - ", - count = notes.len(), - )); - - script +pub fn input_note_data_ptr(note_idx: u32) -> memory::MemoryAddress { + memory::INPUT_NOTE_DATA_SECTION_OFFSET + note_idx * memory::NOTE_MEM_SIZE } // HELPER STRUCTURE diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account.rs b/crates/miden-testing/src/kernel_tests/tx/test_account.rs index a4f5a4398a..d0146be16b 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -3,40 +3,41 @@ use alloc::vec::Vec; use std::collections::BTreeMap; use anyhow::Context; -use miden_lib::errors::tx_kernel_errors::{ - ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO, - ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO, - ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE, - ERR_ACCOUNT_ID_UNKNOWN_VERSION, - ERR_ACCOUNT_NONCE_AT_MAX, - ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE, - ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS, - ERR_FAUCET_INVALID_STORAGE_OFFSET, -}; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::transaction::TransactionKernel; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{ +use assert_matches::assert_matches; +use miden_processor::{ExecutionError, Word}; +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{ Account, AccountBuilder, AccountCode, AccountComponent, AccountId, - AccountIdVersion, - AccountProcedureInfo, AccountStorage, AccountStorageMode, AccountType, StorageMap, StorageSlot, + StorageSlotContent, + StorageSlotDelta, + StorageSlotId, + StorageSlotName, + StorageSlotType, }; -use miden_objects::assembly::diagnostics::{IntoDiagnostic, NamedSource, Report, WrapErr, miette}; -use miden_objects::assembly::{DefaultSourceManager, Library}; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset}; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::{ +use miden_protocol::assembly::diagnostics::{IntoDiagnostic, NamedSource, Report, WrapErr, miette}; +use miden_protocol::assembly::{DefaultSourceManager, Library}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::errors::tx_kernel::{ + ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO, + ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO, + ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE, + ERR_ACCOUNT_ID_UNKNOWN_VERSION, + ERR_ACCOUNT_NONCE_AT_MAX, + ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE, + ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME, + ERR_FAUCET_STORAGE_DATA_SLOT_IS_RESERVED, +}; +use miden_protocol::note::NoteType; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, @@ -44,11 +45,14 @@ use miden_objects::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::testing::storage::STORAGE_LEAVES_2; -use miden_objects::transaction::{ExecutedTransaction, OutputNote, TransactionScript}; -use miden_objects::{LexicographicWord, StarkField}; -use miden_processor::{EMPTY_WORD, ExecutionError, MastNodeExt, Word}; -use miden_tx::{LocalTransactionProver, TransactionExecutorError}; +use miden_protocol::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1}; +use miden_protocol::transaction::{OutputNote, TransactionKernel}; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{LexicographicWord, StarkField}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockAccountComponent; +use miden_standards::testing::mock_account::MockAccountExt; +use miden_tx::LocalTransactionProver; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use winter_rand_utils::rand_value; @@ -62,7 +66,6 @@ use crate::{ MockChain, TransactionContextBuilder, TxContextInput, - assert_execution_error, assert_transaction_executor_error, }; @@ -77,16 +80,18 @@ pub async fn compute_commitment() -> miette::Result<()> { let mut account_clone = account.clone(); let key = Word::from([1, 2, 3, 4u32]); let value = Word::from([2, 3, 4, 5u32]); - account_clone.storage_mut().set_map_item(2, key, value).unwrap(); + let mock_map_slot = &*MOCK_MAP_SLOT; + account_clone.storage_mut().set_map_item(mock_map_slot, key, value).unwrap(); let expected_commitment = account_clone.commitment(); let tx_script = format!( r#" - use.std::word + use miden::core::word + + use miden::protocol::active_account + use mock::account->mock_account - use.miden::prologue - use.miden::active_account - use.mock::account->mock_account + const MOCK_MAP_SLOT = word("{mock_map_slot}") begin exec.active_account::get_initial_commitment @@ -107,8 +112,8 @@ pub async fn compute_commitment() -> miette::Result<()> { padw push.0.0.0 push.{value} push.{key} - push.2 - # => [slot_idx = 2, KEY, VALUE, pad(7)] + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE, pad(7)] call.mock_account::set_map_item dropw dropw dropw dropw # => [STORAGE_COMMITMENT0] @@ -139,8 +144,7 @@ pub async fn compute_commitment() -> miette::Result<()> { ); let tx_context_builder = TransactionContextBuilder::new(account); - let tx_script = ScriptBuilder::with_mock_libraries() - .into_diagnostic()? + let tx_script = CodeBuilder::with_mock_libraries() .compile_tx_script(tx_script) .into_diagnostic()?; let tx_context = tx_context_builder @@ -184,7 +188,7 @@ async fn test_account_type() -> miette::Result<()> { let code = format!( " - use.$kernel::account_id + use $kernel::account_id begin exec.account_id::{procedure} @@ -256,7 +260,7 @@ async fn test_account_validate_id() -> miette::Result<()> { let suffix = Felt::try_from((account_id % (1u128 << 64)) as u64).unwrap(); let code = " - use.$kernel::account_id + use $kernel::account_id begin exec.account_id::validate @@ -309,7 +313,7 @@ async fn test_is_faucet_procedure() -> miette::Result<()> { let code = format!( " - use.$kernel::account_id + use $kernel::account_id begin push.{prefix} @@ -350,8 +354,8 @@ pub async fn test_compute_code_commitment() -> miette::Result<()> { let code = format!( r#" - use.$kernel::prologue - use.mock::account->mock_account + use $kernel::prologue + use mock::account->mock_account begin exec.prologue::prepare_transaction @@ -374,28 +378,31 @@ pub async fn test_compute_code_commitment() -> miette::Result<()> { #[tokio::test] async fn test_get_item() -> miette::Result<()> { - for storage_item in [AccountStorage::mock_item_0(), AccountStorage::mock_item_1()] { + for storage_item in [AccountStorage::mock_value_slot0(), AccountStorage::mock_value_slot1()] { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); let code = format!( - " - use.$kernel::account - use.$kernel::prologue + r#" + use $kernel::account + use $kernel::prologue + + const SLOT_NAME = word("{slot_name}") begin exec.prologue::prepare_transaction # push the account storage item index - push.{item_index} + push.SLOT_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix] # assert the item value is correct exec.account::get_item push.{item_value} - assert_eqw + assert_eqw.err="expected item to have value {item_value}" end - ", - item_index = storage_item.index, - item_value = &storage_item.slot.value(), + "#, + slot_name = storage_item.name(), + item_value = &storage_item.content().value(), ); tx_context.execute_code(&code).await.unwrap(); @@ -406,56 +413,46 @@ async fn test_get_item() -> miette::Result<()> { #[tokio::test] async fn test_get_map_item() -> miette::Result<()> { + let slot = AccountStorage::mock_map_slot(); let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_item_2().slot])) + .with_component(MockAccountComponent::with_slots(vec![slot.clone()])) .build_existing() .unwrap(); let tx_context = TransactionContextBuilder::new(account).build().unwrap(); - for (key, value) in STORAGE_LEAVES_2 { + let StorageSlotContent::Map(map) = slot.content() else { + panic!("expected map") + }; + + for (key, expected_value) in map.entries() { let code = format!( - " - use.$kernel::prologue + r#" + use $kernel::prologue + use mock::account + + const SLOT_NAME = word("{slot_name}") begin exec.prologue::prepare_transaction # get the map item - push.{map_key} - push.{item_index} - call.::mock::account::get_map_item + push.{key} + push.SLOT_NAME[0..2] + call.account::get_map_item + # => [VALUE] - # truncate the stack - swapw dropw movup.4 drop + push.{expected_value} + assert_eqw.err="value did not match {expected_value}" + + exec.::miden::core::sys::truncate_stack end - ", - item_index = 0, - map_key = &key, + "#, + slot_name = slot.name(), ); - let exec_output = &mut tx_context.execute_code(&code).await?; - assert_eq!( - exec_output.get_stack_word_be(0), - value, - "get_map_item result doesn't match the expected value", - ); - assert_eq!( - exec_output.get_stack_word_be(4), - Word::empty(), - "The rest of the stack must be cleared", - ); - assert_eq!( - exec_output.get_stack_word_be(8), - Word::empty(), - "The rest of the stack must be cleared", - ); - assert_eq!( - exec_output.get_stack_word_be(12), - Word::empty(), - "The rest of the stack must be cleared", - ); + tx_context.execute_code(&code).await?; } Ok(()) @@ -463,23 +460,31 @@ async fn test_get_map_item() -> miette::Result<()> { #[tokio::test] async fn test_get_storage_slot_type() -> miette::Result<()> { - for storage_item in [ - AccountStorage::mock_item_0(), - AccountStorage::mock_item_1(), - AccountStorage::mock_item_2(), + for slot_name in [ + AccountStorage::mock_value_slot0().name(), + AccountStorage::mock_value_slot1().name(), + AccountStorage::mock_map_slot().name(), ] { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); + let (slot_idx, slot) = tx_context + .account() + .storage() + .slots() + .iter() + .enumerate() + .find(|(_, slot)| slot.name() == slot_name) + .unwrap(); let code = format!( " - use.$kernel::account - use.$kernel::prologue + use $kernel::account + use $kernel::prologue begin exec.prologue::prepare_transaction - # push the account storage item index - push.{item_index} + # push the account storage slot index + push.{slot_idx} # get the type of the respective storage slot exec.account::get_storage_slot_type @@ -488,14 +493,17 @@ async fn test_get_storage_slot_type() -> miette::Result<()> { swap drop end ", - item_index = storage_item.index, ); let exec_output = &tx_context.execute_code(&code).await.unwrap(); - let storage_slot_type = storage_item.slot.slot_type(); - - assert_eq!(storage_slot_type, exec_output.get_stack_element(0).try_into().unwrap()); + assert_eq!( + slot.slot_type(), + StorageSlotType::try_from( + u8::try_from(exec_output.get_stack_element(0).as_int()).unwrap() + ) + .unwrap() + ); assert_eq!(exec_output.get_stack_element(1), ZERO, "the rest of the stack is empty"); assert_eq!(exec_output.get_stack_element(2), ZERO, "the rest of the stack is empty"); assert_eq!(exec_output.get_stack_element(3), ZERO, "the rest of the stack is empty"); @@ -519,40 +527,185 @@ async fn test_get_storage_slot_type() -> miette::Result<()> { Ok(()) } +/// Tests that accessing an unknown slot fails with the expected error message. +/// +/// This tests both accounts with empty storage and non-empty storage. +#[tokio::test] +async fn test_account_get_item_fails_on_unknown_slot() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let account_empty_storage = builder.add_existing_mock_account(Auth::IncrNonce)?; + assert_eq!(account_empty_storage.storage().num_slots(), 0); + + let account_non_empty_storage = builder.add_existing_mock_account(Auth::BasicAuth)?; + assert_eq!(account_non_empty_storage.storage().num_slots(), 1); + + let chain = builder.build()?; + + let code = r#" + use mock::account + + const UNKNOWN_SLOT_NAME = word("unknown::slot::name") + + begin + push.UNKNOWN_SLOT_NAME[0..2] + call.account::get_item + end + "#; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; + + let result = chain + .build_tx_context(account_empty_storage, &[], &[])? + .tx_script(tx_script.clone()) + .build()? + .execute() + .await; + assert_transaction_executor_error!(result, ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME); + + let result = chain + .build_tx_context(account_non_empty_storage, &[], &[])? + .tx_script(tx_script) + .build()? + .execute() + .await; + assert_transaction_executor_error!(result, ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME); + + Ok(()) +} + +/// Tests that accessing the protocol-reserved faucet metadata slot fails with the expected error +/// message. #[tokio::test] -async fn test_set_item() -> miette::Result<()> { +async fn test_account_set_item_fails_on_reserved_faucet_metadata_slot() -> anyhow::Result<()> { + let code = r#" + use miden::protocol::native_account + + const FAUCET_SYSDATA_SLOT=word("miden::protocol::faucet::sysdata") + + begin + push.FAUCET_SYSDATA_SLOT[0..2] + exec.native_account::set_item + end + "#; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; + + let tx_context = TransactionContextBuilder::with_fungible_faucet( + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + Felt::from(0u32), + ) + .tx_script(tx_script) + .build() + .unwrap(); + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_FAUCET_STORAGE_DATA_SLOT_IS_RESERVED); + + Ok(()) +} + +#[tokio::test] +async fn test_is_slot_id_lt() -> miette::Result<()> { + // Note that the slot IDs derived from the names are essentially randomly sorted, so these cover + // "less than" and "greater than" outcomes. + let mut test_cases = (0..100) + .map(|i| { + let prev_slot = StorageSlotName::mock(i).id(); + let curr_slot = StorageSlotName::mock(i + 1).id(); + (prev_slot, curr_slot) + }) + .collect::>(); + + // Extend with special case where prefix matches and suffix determines the outcome. + let prefix = Felt::from(100u32); + test_cases.extend([ + // prev_slot == curr_slot + ( + StorageSlotId::new(Felt::from(50u32), prefix), + StorageSlotId::new(Felt::from(50u32), prefix), + ), + // prev_slot < curr_slot + ( + StorageSlotId::new(Felt::from(50u32), prefix), + StorageSlotId::new(Felt::from(51u32), prefix), + ), + // prev_slot > curr_slot + ( + StorageSlotId::new(Felt::from(51u32), prefix), + StorageSlotId::new(Felt::from(50u32), prefix), + ), + ]); + + for (prev_slot, curr_slot) in test_cases { + let code = format!( + r#" + use $kernel::account + + begin + push.{curr_suffix}.{curr_prefix}.{prev_suffix}.{prev_prefix} + # => [prev_slot_id_prefix, prev_slot_id_suffix, curr_slot_id_prefix, curr_slot_id_suffix] + + exec.account::is_slot_id_lt + # => [is_slot_id_lt] + + push.{is_lt} + assert_eq.err="is_slot_id_lt was not {is_lt}" + # => [] + end + "#, + prev_prefix = prev_slot.prefix(), + prev_suffix = prev_slot.suffix(), + curr_prefix = curr_slot.prefix(), + curr_suffix = curr_slot.suffix(), + is_lt = u8::from(prev_slot < curr_slot) + ); + + CodeExecutor::with_default_host().run(&code).await?; + } + + Ok(()) +} + +#[tokio::test] +async fn test_set_item() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); - let new_storage_item = Word::from([91, 92, 93, 94u32]); + let slot_name = &*MOCK_VALUE_SLOT0; + let new_value = Word::from([91, 92, 93, 94u32]); + let old_value = tx_context.account().storage().get_item(slot_name)?; let code = format!( - " - use.$kernel::account - use.$kernel::prologue + r#" + use $kernel::account + use $kernel::prologue + + const MOCK_VALUE_SLOT0 = word("{slot_name}") begin exec.prologue::prepare_transaction # set the storage item - push.{new_storage_item} - push.{new_storage_item_index} + push.{new_value} + push.MOCK_VALUE_SLOT0[0..2] + # => [slot_id_prefix, slot_id_suffix, NEW_VALUE] + exec.account::set_item # assert old value was correctly returned - push.1.2.3.4 assert_eqw + push.{old_value} + assert_eqw.err="old value did not match" # assert new value has been correctly set - push.{new_storage_item_index} + push.MOCK_VALUE_SLOT0[0..2] + # => [slot_id_prefix, slot_id_suffix] + exec.account::get_item - push.{new_storage_item} - assert_eqw + push.{new_value} + assert_eqw.err="new value did not match" end - ", - new_storage_item = &new_storage_item, - new_storage_item_index = 0, + "#, ); - tx_context.execute_code(&code).await.unwrap(); + tx_context.execute_code(&code).await?; Ok(()) } @@ -562,21 +715,23 @@ async fn test_set_map_item() -> miette::Result<()> { let (new_key, new_value) = (Word::from([109, 110, 111, 112u32]), Word::from([9, 10, 11, 12u32])); + let slot = AccountStorage::mock_map_slot(); let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_item_2().slot])) + .with_component(MockAccountComponent::with_slots(vec![slot.clone()])) .build_existing() .unwrap(); let tx_context = TransactionContextBuilder::new(account).build().unwrap(); - let storage_item = AccountStorage::mock_item_2(); let code = format!( - " - use.std::sys + r#" + use miden::core::sys + + use $kernel::prologue + use mock::account->mock_account - use.$kernel::prologue - use.mock::account->mock_account + const SLOT_NAME=word("{slot_name}") begin exec.prologue::prepare_transaction @@ -584,23 +739,33 @@ async fn test_set_map_item() -> miette::Result<()> { # set the map item push.{new_value} push.{new_key} - push.{item_index} + push.SLOT_NAME[0..2] call.mock_account::set_map_item - # double check that on storage slot is indeed the new map - push.{item_index} + # double check that the storage slot is indeed the new map + push.SLOT_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, OLD_VALUE] + + # pad the stack + repeat.14 push.0 movdn.2 end + # => [slot_id_prefix, slot_id_suffix, pad(14), OLD_VALUE] + call.mock_account::get_item + # => [MAP_ROOT, pad(12), OLD_VALUE] # truncate the stack + repeat.3 swapw dropw end + # => [MAP_ROOT, OLD_VALUE] + exec.sys::truncate_stack end - ", - item_index = 0, + "#, + slot_name = slot.name(), new_key = &new_key, new_value = &new_value, ); - let exec_output = &tx_context.execute_code(&code).await.unwrap(); + let exec_output = &tx_context.execute_code(&code).await?; let mut new_storage_map = AccountStorage::mock_map(); new_storage_map.insert(new_key, new_value).unwrap(); @@ -608,166 +773,18 @@ async fn test_set_map_item() -> miette::Result<()> { assert_eq!( new_storage_map.root(), exec_output.get_stack_word_be(0), - "get_item must return the new updated value", - ); - assert_eq!( - storage_item.slot.value(), - exec_output.get_stack_word_be(4), - "The original value stored in the map doesn't match the expected value", + "get_item should return the updated root", ); - Ok(()) -} - -#[tokio::test] -async fn test_account_component_storage_offset() -> miette::Result<()> { - // setup assembler - let assembler = - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())); - - // The following code will execute the following logic that will be asserted during the test: - // - // 1. foo_write will set word [1, 2, 3, 4] in storage at location 0 (0 offset by 0) - // 2. foo_read will read word [1, 2, 3, 4] in storage from location 0 (0 offset by 0) - // 3. bar_write will set word [5, 6, 7, 8] in storage at location 1 (0 offset by 1) - // 4. bar_read will read word [5, 6, 7, 8] in storage from location 1 (0 offset by 1) - // - // We will then assert that we are able to retrieve the correct elements from storage - // insuring consistent "set" and "get" using offsets. - let source_code_component1 = " - use.std::word - use.miden::active_account - use.miden::native_account - - export.foo_write - push.1.2.3.4.0 - exec.native_account::set_item - - dropw - end - - export.foo_read - push.0 - exec.active_account::get_item - push.1.2.3.4 - - exec.word::eq assert - end - "; - - let source_code_component2 = " - use.std::word - use.miden::active_account - use.miden::native_account - - export.bar_write - push.5.6.7.8.0 - exec.native_account::set_item - - dropw - end - - export.bar_read - push.0 - exec.active_account::get_item - push.5.6.7.8 - - exec.word::eq assert - end - "; - - // Compile source code to find MAST roots of procedures. - let code1 = assembler.clone().assemble_library([source_code_component1]).unwrap(); - let code2 = assembler.clone().assemble_library([source_code_component2]).unwrap(); - let find_procedure_digest_by_name = |name: &str, lib: &Library| { - lib.exports().find_map(|export| { - if export.name.name.as_str() == name { - Some(lib.mast_forest()[lib.get_export_node_id(&export.name)].digest()) - } else { - None - } - }) + let old_value_for_key = match slot.content() { + StorageSlotContent::Map(original_map) => original_map.get(&new_key), + _ => panic!("expected map"), }; - - let foo_write = find_procedure_digest_by_name("foo_write", &code1).unwrap(); - let foo_read = find_procedure_digest_by_name("foo_read", &code1).unwrap(); - let bar_write = find_procedure_digest_by_name("bar_write", &code2).unwrap(); - let bar_read = find_procedure_digest_by_name("bar_read", &code2).unwrap(); - - // Compile source code into components. - let component1 = AccountComponent::compile( - source_code_component1, - assembler.clone(), - vec![StorageSlot::Value(Word::empty())], - ) - .unwrap() - .with_supported_type(AccountType::RegularAccountUpdatableCode); - - let component2 = AccountComponent::compile( - source_code_component2, - assembler.clone(), - vec![StorageSlot::Value(Word::empty())], - ) - .unwrap() - .with_supported_type(AccountType::RegularAccountUpdatableCode); - - let mut account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) - .with_auth_component(Auth::IncrNonce) - .with_component(component1) - .with_component(component2) - .build_existing() - .unwrap(); - - // Assert that the storage offset and size have been set correctly. - for (procedure_digest, expected_offset, expected_size) in - [(foo_write, 0, 1), (foo_read, 0, 1), (bar_write, 1, 1), (bar_read, 1, 1)] - { - let procedure_info = account - .code() - .procedures() - .iter() - .find(|proc| proc.mast_root() == &procedure_digest) - .unwrap(); - assert_eq!( - procedure_info.storage_offset(), - expected_offset, - "failed for procedure {procedure_digest}" - ); - assert_eq!( - procedure_info.storage_size(), - expected_size, - "failed for procedure {procedure_digest}" - ); - } - - // setup transaction script - let tx_script_source_code = format!( - " - begin - call.{foo_write} - call.{foo_read} - call.{bar_write} - call.{bar_read} - end - " + assert_eq!( + old_value_for_key, + exec_output.get_stack_word_be(4), + "set_map_item must return the old value for the key (empty word for new key)", ); - let tx_script_program = assembler.assemble_program(tx_script_source_code).unwrap(); - let tx_script = TransactionScript::new(tx_script_program); - - // setup transaction context - let tx_context = TransactionContextBuilder::new(account.clone()) - .tx_script(tx_script) - .build() - .unwrap(); - - // execute code in context - let tx = tx_context.execute().await.into_diagnostic()?; - account.apply_delta(tx.account_delta()).unwrap(); - - // assert that elements have been set at the correct locations in storage - assert_eq!(account.storage().get_item(0).unwrap(), Word::from([1, 2, 3, 4u32])); - - assert_eq!(account.storage().get_item(1).unwrap(), Word::from([5, 6, 7, 8u32])); Ok(()) } @@ -789,125 +806,14 @@ async fn create_account_with_empty_storage_slots() -> anyhow::Result<()> { Ok(()) } -async fn create_procedure_metadata_test_account( - account_type: AccountType, - storage_offset: u8, - storage_size: u8, -) -> anyhow::Result> { - let mock_chain = MockChain::new(); - - let version = AccountIdVersion::Version0; - - let mock_code = AccountCode::mock(); - let code = AccountCode::from_parts( - mock_code.mast(), - mock_code - .mast() - .procedure_digests() - .map(|mast_root| { - AccountProcedureInfo::new(mast_root, storage_offset, storage_size).unwrap() - }) - .collect(), - ); - - let storage = AccountStorage::new(vec![StorageSlot::Value(EMPTY_WORD)]).unwrap(); - - let seed = AccountId::compute_account_seed( - [9; 32], - account_type, - AccountStorageMode::Private, - version, - code.commitment(), - storage.commitment(), - ) - .context("failed to compute seed")?; - let id = AccountId::new(seed, version, code.commitment(), storage.commitment()) - .context("failed to compute ID")?; - - let account = - Account::new(id, AssetVault::default(), storage, code, Felt::from(0u32), Some(seed))?; - - let tx_inputs = mock_chain.get_transaction_inputs(&account, &[], &[])?; - let tx_context = TransactionContextBuilder::new(account).tx_inputs(tx_inputs).build()?; - - let result = tx_context.execute().await.map_err(|err| { - let TransactionExecutorError::TransactionProgramExecutionFailed(exec_err) = err else { - panic!("should have received an execution error"); - }; - - exec_err - }); - - Ok(result) -} - -/// Tests that creating an account whose procedure accesses the reserved faucet storage slot fails. -#[tokio::test] -async fn creating_faucet_account_with_procedure_accessing_reserved_slot_fails() -> anyhow::Result<()> -{ - // Set offset to 0 for a faucet which should be disallowed. - let execution_res = create_procedure_metadata_test_account(AccountType::FungibleFaucet, 0, 1) - .await - .context("failed to create test account")?; - - assert_execution_error!(execution_res, ERR_FAUCET_INVALID_STORAGE_OFFSET); - - Ok(()) -} - -/// Tests that creating a faucet whose procedure offset+size is out of bounds fails. -#[tokio::test] -async fn creating_faucet_with_procedure_offset_plus_size_out_of_bounds_fails() -> anyhow::Result<()> -{ - // Set offset to lowest allowed value 1 and size to 1 while number of slots is 1 which should - // result in an out of bounds error. - let execution_res = create_procedure_metadata_test_account(AccountType::FungibleFaucet, 1, 1) - .await - .context("failed to create test account")?; - - assert_execution_error!(execution_res, ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS); - - // Set offset to 2 while number of slots is 1 which should result in an out of bounds error. - let execution_res = create_procedure_metadata_test_account(AccountType::FungibleFaucet, 2, 1) - .await - .context("failed to create test account")?; - - assert_execution_error!(execution_res, ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS); - - Ok(()) -} - -/// Tests that creating an account whose procedure offset+size is out of bounds fails. -#[tokio::test] -async fn creating_account_with_procedure_offset_plus_size_out_of_bounds_fails() -> anyhow::Result<()> -{ - // Set size to 2 while number of slots is 1 which should result in an out of bounds error. - let execution_res = - create_procedure_metadata_test_account(AccountType::RegularAccountImmutableCode, 0, 2) - .await - .context("failed to create test account")?; - - assert_execution_error!(execution_res, ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS); - - // Set offset to 2 while number of slots is 1 which should result in an out of bounds error. - let execution_res = - create_procedure_metadata_test_account(AccountType::RegularAccountImmutableCode, 2, 1) - .await - .context("failed to create test account")?; - - assert_execution_error!(execution_res, ERR_ACCOUNT_STORAGE_SLOT_INDEX_OUT_OF_BOUNDS); - - Ok(()) -} - #[tokio::test] async fn test_get_initial_storage_commitment() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = format!( r#" - use.miden::active_account - use.$kernel::prologue + use miden::protocol::active_account + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -918,7 +824,7 @@ async fn test_get_initial_storage_commitment() -> anyhow::Result<()> { assert_eqw.err="actual storage commitment is not equal to the expected one" end "#, - expected_storage_commitment = &tx_context.account().storage().commitment(), + expected_storage_commitment = &tx_context.account().storage().to_commitment(), ); tx_context.execute_code(&code).await?; @@ -940,23 +846,28 @@ async fn test_compute_storage_commitment() -> anyhow::Result<()> { let mut account_clone = tx_context.account().clone(); let account_storage = account_clone.storage_mut(); - let init_storage_commitment = account_storage.commitment(); + let init_storage_commitment = account_storage.to_commitment(); + + let mock_value_slot0 = &*MOCK_VALUE_SLOT0; + let mock_map_slot = &*MOCK_MAP_SLOT; - account_storage.set_item(0, [9, 10, 11, 12].map(Felt::new).into())?; - let storage_commitment_0 = account_storage.commitment(); + account_storage.set_item(mock_value_slot0, [9, 10, 11, 12].map(Felt::new).into())?; + let storage_commitment_value = account_storage.to_commitment(); account_storage.set_map_item( - 2, + mock_map_slot, [101, 102, 103, 104].map(Felt::new).into(), [5, 6, 7, 8].map(Felt::new).into(), )?; - let storage_commitment_2 = account_storage.commitment(); + let storage_commitment_map = account_storage.to_commitment(); let code = format!( r#" - use.miden::account - use.$kernel::prologue - use.mock::account->mock_account + use $kernel::prologue + use mock::account->mock_account + + const MOCK_VALUE_SLOT0=word("{mock_value_slot0}") + const MOCK_MAP_SLOT=word("{mock_map_slot}") begin exec.prologue::prepare_transaction @@ -966,34 +877,39 @@ async fn test_compute_storage_commitment() -> anyhow::Result<()> { push.{init_storage_commitment} assert_eqw.err="storage commitment at the beginning of the transaction is not equal to the expected one" - # update the 0th (value) storage slot - push.9.10.11.12.0 + # update the value storage slot + push.9.10.11.12 + push.MOCK_VALUE_SLOT0[0..2] call.mock_account::set_item dropw drop # => [] - # assert the correctness of the storage commitment after the 0th slot was updated + # assert the correctness of the storage commitment after the value slot was updated call.mock_account::compute_storage_commitment - push.{storage_commitment_0} - assert_eqw.err="storage commitment after the 0th slot was updated is not equal to the expected one" + push.{storage_commitment_value} + assert_eqw.err="storage commitment after the value slot was updated is not equal to the expected one" # get the storage commitment once more to get the cached data and assert that this data # didn't change call.mock_account::compute_storage_commitment - push.{storage_commitment_0} + push.{storage_commitment_value} assert_eqw.err="storage commitment should remain the same" - # update the 2nd (map) storage slot - push.5.6.7.8.101.102.103.104.2 # [idx, KEY, VALUE] + # update the map storage slot + push.5.6.7.8.101.102.103.104 + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] + call.mock_account::set_map_item dropw dropw # => [] - # assert the correctness of the storage commitment after the 2nd slot was updated + # assert the correctness of the storage commitment after the map slot was updated call.mock_account::compute_storage_commitment - push.{storage_commitment_2} - assert_eqw.err="storage commitment after the 2nd slot was updated is not equal to the expected one" + push.{storage_commitment_map} + assert_eqw.err="storage commitment after the map slot was updated is not equal to the expected one" end "#, ); + tx_context.execute_code(&code).await?; Ok(()) @@ -1005,13 +921,18 @@ async fn test_compute_storage_commitment() -> anyhow::Result<()> { /// accounts. #[tokio::test] async fn prove_account_creation_with_non_empty_storage() -> anyhow::Result<()> { - let slot0 = StorageSlot::Value(Word::from([1, 2, 3, 4u32])); - let slot1 = StorageSlot::Value(Word::from([10, 20, 30, 40u32])); + let slot_name0 = StorageSlotName::mock(0); + let slot_name1 = StorageSlotName::mock(1); + let slot_name2 = StorageSlotName::mock(2); + + let slot0 = StorageSlot::with_value(slot_name0.clone(), Word::from([1, 2, 3, 4u32])); + let slot1 = StorageSlot::with_value(slot_name1.clone(), Word::from([10, 20, 30, 40u32])); let mut map_entries = Vec::new(); for _ in 0..10 { map_entries.push((rand_value::(), rand_value::())); } - let map_slot = StorageSlot::Map(StorageMap::with_entries(map_entries.clone())?); + let map_slot = + StorageSlot::with_map(slot_name2.clone(), StorageMap::with_entries(map_entries.clone())?); let account = AccountBuilder::new([6; 32]) .storage_mode(AccountStorageMode::Public) @@ -1031,16 +952,28 @@ async fn prove_account_creation_with_non_empty_storage() -> anyhow::Result<()> { assert_eq!(tx.account_delta().nonce_delta(), Felt::new(1)); - assert_eq!(tx.account_delta().storage().values().get(&0).unwrap(), &slot0.value()); - assert_eq!(tx.account_delta().storage().values().get(&1).unwrap(), &slot1.value()); - - assert_eq!( - tx.account_delta().storage().maps().get(&2).unwrap().entries(), - &BTreeMap::from_iter( + assert_matches!( + tx.account_delta().storage().get(&slot_name0).unwrap(), + StorageSlotDelta::Value(value) => { + assert_eq!(*value, slot0.value()) + } + ); + assert_matches!( + tx.account_delta().storage().get(&slot_name1).unwrap(), + StorageSlotDelta::Value(value) => { + assert_eq!(*value, slot1.value()) + } + ); + assert_matches!( + tx.account_delta().storage().get(&slot_name2).unwrap(), + StorageSlotDelta::Map(map_delta) => { + let expected = &BTreeMap::from_iter( map_entries .into_iter() .map(|(key, value)| { (LexicographicWord::new(key), value) }) - ) + ); + assert_eq!(expected, map_delta.entries()) + } ); assert!(tx.account_delta().vault().is_empty()); @@ -1076,9 +1009,9 @@ async fn test_get_vault_root() -> anyhow::Result<()> { // get the initial vault root let code = format!( - " - use.miden::active_account - use.$kernel::prologue + r#" + use miden::protocol::active_account + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -1086,9 +1019,9 @@ async fn test_get_vault_root() -> anyhow::Result<()> { # get the initial vault root exec.active_account::get_initial_vault_root push.{expected_vault_root} - assert_eqw + assert_eqw.err="initial vault root mismatch" end - ", + "#, expected_vault_root = &account.vault().root(), ); tx_context.execute_code(&code).await?; @@ -1098,9 +1031,9 @@ async fn test_get_vault_root() -> anyhow::Result<()> { let code = format!( r#" - use.miden::active_account - use.$kernel::prologue - use.mock::account->mock_account + use miden::protocol::active_account + use $kernel::prologue + use mock::account->mock_account begin exec.prologue::prepare_transaction @@ -1113,7 +1046,7 @@ async fn test_get_vault_root() -> anyhow::Result<()> { # get the current vault root exec.active_account::get_vault_root push.{expected_vault_root} - assert_eqw.err="actual vault root is not equal to the expected one" + assert_eqw.err="vault root mismatch" end "#, fungible_asset = Word::from(&fungible_asset), @@ -1124,13 +1057,13 @@ async fn test_get_vault_root() -> anyhow::Result<()> { Ok(()) } -/// This test checks the correctness of the `miden::active_account::get_initial_balance` procedure -/// in two cases: +/// This test checks the correctness of the `miden::protocol::active_account::get_initial_balance` +/// procedure in two cases: /// - when a note adds the asset which already exists in the account vault. /// - when a note adds the asset which doesn't exist in the account vault. -/// +/// /// As part of the test pipeline it also checks the correctness of the -/// `miden::active_account::get_balance` procedure. +/// `miden::protocol::active_account::get_balance` procedure. #[tokio::test] async fn test_get_init_balance_addition() -> anyhow::Result<()> { // prepare the testing data @@ -1182,7 +1115,7 @@ async fn test_get_init_balance_addition() -> anyhow::Result<()> { let add_existing_source = format!( r#" - use.miden::active_account + use miden::protocol::active_account begin # push faucet ID prefix and suffix @@ -1213,7 +1146,7 @@ async fn test_get_init_balance_addition() -> anyhow::Result<()> { initial_balance + fungible_asset_for_note_existing.unwrap_fungible().amount(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(add_existing_source)?; + let tx_script = CodeBuilder::default().compile_tx_script(add_existing_source)?; let tx_context = mock_chain .build_tx_context( @@ -1236,7 +1169,7 @@ async fn test_get_init_balance_addition() -> anyhow::Result<()> { let add_new_source = format!( r#" - use.miden::active_account + use miden::protocol::active_account begin # push faucet ID prefix and suffix @@ -1266,7 +1199,7 @@ async fn test_get_init_balance_addition() -> anyhow::Result<()> { final_balance = initial_balance + fungible_asset_for_note_new.unwrap_fungible().amount(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(add_new_source)?; + let tx_script = CodeBuilder::default().compile_tx_script(add_new_source)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_new_asset])? @@ -1278,11 +1211,11 @@ async fn test_get_init_balance_addition() -> anyhow::Result<()> { Ok(()) } -/// This test checks the correctness of the `miden::active_account::get_initial_balance` procedure -/// in case when we create a note which removes an asset from the account vault. +/// This test checks the correctness of the `miden::protocol::active_account::get_initial_balance` +/// procedure in case when we create a note which removes an asset from the account vault. /// /// As part of the test pipeline it also checks the correctness of the -/// `miden::active_account::get_balance` procedure. +/// `miden::protocol::active_account::get_balance` procedure. #[tokio::test] async fn test_get_init_balance_subtraction() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -1313,13 +1246,13 @@ async fn test_get_init_balance_subtraction() -> anyhow::Result<()> { let remove_existing_source = format!( r#" - use.miden::active_account - use.miden::contracts::wallets::basic->wallet - use.mock::util + use miden::protocol::active_account + use miden::standards::wallets::basic->wallet + use mock::util # Inputs: [ASSET, note_idx] # Outputs: [ASSET, note_idx] - proc.move_asset_to_note + proc move_asset_to_note # pad the stack before call push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw # => [ASSET, note_idx, pad(11)] @@ -1334,7 +1267,7 @@ async fn test_get_init_balance_subtraction() -> anyhow::Result<()> { begin # create random note and move the asset into it - exec.util::create_random_note + exec.util::create_default_note # => [note_idx] push.{REMOVED_ASSET} @@ -1370,8 +1303,7 @@ async fn test_get_init_balance_subtraction() -> anyhow::Result<()> { initial_balance - fungible_asset_for_note_existing.unwrap_fungible().amount(), ); - let tx_script = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(remove_existing_source)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(remove_existing_source)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[])? @@ -1409,8 +1341,8 @@ async fn test_authenticate_and_track_procedure() -> miette::Result<()> { let code = format!( " - use.$kernel::account - use.$kernel::prologue + use $kernel::account + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -1455,6 +1387,7 @@ async fn test_was_procedure_called() -> miette::Result<()> { .with_component(mock_component) .build_existing() .unwrap(); + let mock_value_slot1 = &*MOCK_VALUE_SLOT1; // Create a transaction script that: // 1. Checks that get_item hasn't been called yet @@ -1462,9 +1395,12 @@ async fn test_was_procedure_called() -> miette::Result<()> { // 3. Checks that get_item has been called // 4. Calls get_item **again** // 5. Checks that `was_procedure_called` returns `true` - let tx_script_code = r#" - use.mock::account->mock_account - use.miden::native_account + let tx_script_code = format!( + r#" + use mock::account->mock_account + use miden::protocol::native_account + + const MOCK_VALUE_SLOT1 = word("{mock_value_slot1}") begin # First check that get_item procedure hasn't been called yet @@ -1473,7 +1409,7 @@ async fn test_was_procedure_called() -> miette::Result<()> { assertz.err="procedure should not have been called" # Call the procedure first time - push.0 + push.MOCK_VALUE_SLOT1[0..2] call.mock_account::get_item dropw # => [] @@ -1482,18 +1418,18 @@ async fn test_was_procedure_called() -> miette::Result<()> { assert.err="procedure should have been called" # Call the procedure second time - push.0 + push.MOCK_VALUE_SLOT1[0..2] call.mock_account::get_item dropw procref.mock_account::get_item exec.native_account::was_procedure_called assert.err="2nd call should not change the was_called flag" end - "#; + "# + ); // Compile the transaction script using the testing assembler with mock account - let tx_script = ScriptBuilder::with_mock_libraries() - .into_diagnostic()? + let tx_script = CodeBuilder::with_mock_libraries() .compile_tx_script(tx_script_code) .into_diagnostic()?; @@ -1516,30 +1452,38 @@ async fn test_was_procedure_called() -> miette::Result<()> { /// `tx script -> account code -> external library` #[tokio::test] async fn transaction_executor_account_code_using_custom_library() -> miette::Result<()> { - const EXTERNAL_LIBRARY_CODE: &str = r#" - use.miden::native_account + let external_library_code = format!( + r#" + use miden::protocol::native_account - export.external_setter + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") + + pub proc external_setter push.2.3.4.5 - push.0 + push.MOCK_VALUE_SLOT0[0..2] exec.native_account::set_item dropw dropw - end"#; + end"#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + ); const ACCOUNT_COMPONENT_CODE: &str = " - use.external_library::external_module + use external_library::external_module - export.custom_setter + pub proc custom_setter exec.external_module::external_setter end"; let external_library_source = - NamedSource::new("external_library::external_module", EXTERNAL_LIBRARY_CODE); + NamedSource::new("external_library::external_module", external_library_code); let external_library = TransactionKernel::assembler().assemble_library([external_library_source])?; - let mut assembler = - TransactionKernel::with_mock_libraries(Arc::new(DefaultSourceManager::default())); + let mut assembler: miden_protocol::assembly::Assembler = + CodeBuilder::with_mock_libraries_with_source_manager(Arc::new( + DefaultSourceManager::default(), + )) + .into(); assembler.link_static_library(&external_library)?; let account_component_source = @@ -1548,7 +1492,7 @@ async fn transaction_executor_account_code_using_custom_library() -> miette::Res assembler.clone().assemble_library([account_component_source]).unwrap(); let tx_script_src = "\ - use.account_component::account_module + use account_component::account_module begin call.account_module::custom_setter @@ -1566,7 +1510,7 @@ async fn transaction_executor_account_code_using_custom_library() -> miette::Res .build_existing() .into_diagnostic()?; - let tx_script = ScriptBuilder::default() + let tx_script = CodeBuilder::default() .with_dynamically_linked_library(&account_component_lib) .into_diagnostic()? .compile_tx_script(tx_script_src) @@ -1583,9 +1527,10 @@ async fn transaction_executor_account_code_using_custom_library() -> miette::Res assert_eq!(executed_tx.account_delta().nonce_delta(), Felt::new(1)); // Make sure that account storage has been updated as per the tx script call. + assert_eq!(executed_tx.account_delta().storage().values().count(), 1); assert_eq!( - *executed_tx.account_delta().storage().values(), - BTreeMap::from([(0, Word::from([2, 3, 4, 5u32]))]), + executed_tx.account_delta().storage().get(&MOCK_VALUE_SLOT0).unwrap(), + &StorageSlotDelta::Value(Word::from([2, 3, 4, 5u32])), ); Ok(()) } @@ -1594,17 +1539,18 @@ async fn transaction_executor_account_code_using_custom_library() -> miette::Res #[tokio::test] async fn incrementing_nonce_twice_fails() -> anyhow::Result<()> { let source_code = " - use.miden::native_account + use miden::protocol::native_account - export.auth_incr_nonce_twice + pub proc auth_incr_nonce_twice exec.native_account::incr_nonce drop exec.native_account::incr_nonce drop end "; + let faulty_auth_code = + CodeBuilder::default().compile_component_code("test::faulty_auth", source_code)?; let faulty_auth_component = - AccountComponent::compile(source_code, TransactionKernel::assembler(), vec![])? - .with_supports_all_types(); + AccountComponent::new(faulty_auth_code, vec![])?.with_supports_all_types(); let account = AccountBuilder::new([5; 32]) .with_auth_component(faulty_auth_component) .with_component(MockAccountComponent::with_empty_slots()) @@ -1629,8 +1575,8 @@ async fn test_has_procedure() -> miette::Result<()> { .unwrap(); let tx_script_code = r#" - use.mock::account->mock_account - use.miden::active_account + use mock::account->mock_account + use miden::protocol::active_account begin # check that get_item procedure is available on the mock account @@ -1655,8 +1601,7 @@ async fn test_has_procedure() -> miette::Result<()> { "#; // Compile the transaction script using the testing assembler with mock account - let tx_script = ScriptBuilder::with_mock_libraries() - .into_diagnostic()? + let tx_script = CodeBuilder::with_mock_libraries() .compile_tx_script(tx_script_code) .into_diagnostic()?; @@ -1681,106 +1626,118 @@ async fn test_get_initial_item() -> miette::Result<()> { // Test that get_initial_item returns the initial value before any changes let code = format!( - " - use.$kernel::account - use.$kernel::prologue - use.mock::account->mock_account + r#" + use $kernel::account + use $kernel::prologue + use mock::account->mock_account + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin exec.prologue::prepare_transaction - # get initial value of storage slot 0 - push.0 + # get initial value of the storage slot + push.MOCK_VALUE_SLOT0[0..2] exec.account::get_initial_item push.{expected_initial_value} - assert_eqw.err=\"initial value should match expected\" + assert_eqw.err="initial value should match expected" # modify the storage slot - push.9.10.11.12.0 - call.mock_account::set_item dropw drop + push.9.10.11.12 + push.MOCK_VALUE_SLOT0[0..2] + call.mock_account::set_item dropw drop drop # get_item should return the new value - push.0 + push.MOCK_VALUE_SLOT0[0..2] exec.account::get_item push.9.10.11.12 - assert_eqw.err=\"current value should be updated\" + assert_eqw.err="current value should be updated" # get_initial_item should still return the initial value - push.0 + push.MOCK_VALUE_SLOT0[0..2] exec.account::get_initial_item push.{expected_initial_value} - assert_eqw.err=\"initial value should remain unchanged\" + assert_eqw.err="initial value should remain unchanged" end - ", - expected_initial_value = &AccountStorage::mock_item_0().slot.value(), + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + expected_initial_value = &AccountStorage::mock_value_slot0().content().value(), ); - tx_context.execute_code(&code).await.unwrap(); + tx_context.execute_code(&code).await?; Ok(()) } #[tokio::test] async fn test_get_initial_map_item() -> miette::Result<()> { + let map_slot = AccountStorage::mock_map_slot(); let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_item_2().slot])) + .with_component(MockAccountComponent::with_slots(vec![map_slot.clone()])) .build_existing() .unwrap(); let tx_context = TransactionContextBuilder::new(account).build().unwrap(); // Use the first key-value pair from the mock storage - let (initial_key, initial_value) = STORAGE_LEAVES_2[0]; + let StorageSlotContent::Map(map) = map_slot.content() else { + panic!("expected map"); + }; + + let (initial_key, initial_value) = map.entries().next().unwrap(); let new_key = Word::from([201, 202, 203, 204u32]); let new_value = Word::from([301, 302, 303, 304u32]); + let mock_map_slot = map_slot.name(); let code = format!( - " - use.$kernel::prologue - use.mock::account->mock_account + r#" + use $kernel::prologue + use mock::account->mock_account + + const MOCK_MAP_SLOT = word("{mock_map_slot}") begin exec.prologue::prepare_transaction # get initial value from map push.{initial_key} - push.0 + push.MOCK_MAP_SLOT[0..2] call.mock_account::get_initial_map_item push.{initial_value} - assert_eqw.err=\"initial map value should match expected\" + assert_eqw.err="initial map value should match expected" # add a new key-value pair to the map push.{new_value} push.{new_key} - push.0 + push.MOCK_MAP_SLOT[0..2] call.mock_account::set_map_item dropw dropw # get_map_item should return the new value push.{new_key} - push.0 + push.MOCK_MAP_SLOT[0..2] call.mock_account::get_map_item push.{new_value} - assert_eqw.err=\"current map value should be updated\" + assert_eqw.err="current map value should be updated" # get_initial_map_item should still return the initial value for the initial key push.{initial_key} - push.0 + push.MOCK_MAP_SLOT[0..2] call.mock_account::get_initial_map_item push.{initial_value} - assert_eqw.err=\"initial map value should remain unchanged\" + assert_eqw.err="initial map value should remain unchanged" # get_initial_map_item for the new key should return empty word (default) push.{new_key} - push.0 + push.MOCK_MAP_SLOT[0..2] call.mock_account::get_initial_map_item padw - assert_eqw.err=\"new key should have empty initial value\" + assert_eqw.err="new key should have empty initial value" - dropw dropw + dropw dropw dropw end - ", + "#, initial_key = &initial_key, initial_value = &initial_value, new_key = &new_key, @@ -1810,3 +1767,127 @@ async fn incrementing_nonce_overflow_fails() -> anyhow::Result<()> { Ok(()) } + +/// Tests that merging two components that have a procedure with the same mast root +/// (`get_slot_content`) works. +/// +/// Asserts that the procedure is callable via both names. +#[tokio::test] +async fn merging_components_with_same_mast_root_succeeds() -> anyhow::Result<()> { + static TEST_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::slot::test").expect("storage slot name should be valid") + }); + + static COMPONENT_1_LIBRARY: LazyLock = LazyLock::new(|| { + let code = format!( + r#" + use miden::protocol::active_account + + const TEST_SLOT_NAME = word("{test_slot_name}") + + pub proc get_slot_content + push.TEST_SLOT_NAME[0..2] + exec.active_account::get_item + swapw dropw + end + "#, + test_slot_name = &*TEST_SLOT_NAME + ); + + let source = NamedSource::new("component1::interface", code); + TransactionKernel::assembler() + .assemble_library([source]) + .expect("mock account code should be valid") + }); + + static COMPONENT_2_LIBRARY: LazyLock = LazyLock::new(|| { + let code = format!( + r#" + use miden::protocol::active_account + use miden::protocol::native_account + + const TEST_SLOT_NAME = word("{test_slot_name}") + + pub proc get_slot_content + push.TEST_SLOT_NAME[0..2] + exec.active_account::get_item + swapw dropw + end + + pub proc set_slot_content + push.5.6.7.8 + push.TEST_SLOT_NAME[0..2] + exec.native_account::set_item + swapw dropw + end + "#, + test_slot_name = &*TEST_SLOT_NAME + ); + + let source = NamedSource::new("component2::interface", code); + TransactionKernel::assembler() + .assemble_library([source]) + .expect("mock account code should be valid") + }); + + struct CustomComponent1 { + slot: StorageSlot, + } + + impl From for AccountComponent { + fn from(component: CustomComponent1) -> AccountComponent { + AccountComponent::new(COMPONENT_1_LIBRARY.clone(), vec![component.slot]) + .expect("should be valid") + .with_supports_all_types() + } + } + + struct CustomComponent2; + + impl From for AccountComponent { + fn from(_component: CustomComponent2) -> AccountComponent { + AccountComponent::new(COMPONENT_2_LIBRARY.clone(), vec![]) + .expect("should be valid") + .with_supports_all_types() + } + } + + let slot = StorageSlot::with_value(TEST_SLOT_NAME.clone(), Word::from([1, 2, 3, 4u32])); + + let account = AccountBuilder::new([42; 32]) + .with_auth_component(Auth::IncrNonce) + .with_component(CustomComponent1 { slot: slot.clone() }) + .with_component(CustomComponent2) + .build() + .context("failed to build account")?; + + let tx_script = r#" + use component1::interface->comp1_interface + use component2::interface->comp2_interface + + begin + call.comp1_interface::get_slot_content + push.1.2.3.4 + assert_eqw.err="failed to get slot content1" + + call.comp2_interface::set_slot_content + + call.comp2_interface::get_slot_content + push.5.6.7.8 + assert_eqw.err="failed to get slot content2" + end + "#; + + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(COMPONENT_1_LIBRARY.clone())? + .with_dynamically_linked_library(COMPONENT_2_LIBRARY.clone())? + .compile_tx_script(tx_script)?; + + TransactionContextBuilder::new(account) + .tx_script(tx_script) + .build()? + .execute() + .await?; + + Ok(()) +} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs b/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs index 503d074266..e7e49b7efb 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs @@ -3,10 +3,8 @@ use std::collections::BTreeMap; use std::string::String; use anyhow::Context; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{ +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{ Account, AccountBuilder, AccountDelta, @@ -16,10 +14,12 @@ use miden_objects::account::{ AccountType, StorageMap, StorageSlot, + StorageSlotDelta, + StorageSlotName, }; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; -use miden_objects::note::{Note, NoteExecutionHint, NoteTag, NoteType}; -use miden_objects::testing::account_id::{ +use miden_protocol::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; +use miden_protocol::note::{Note, NoteTag, NoteType}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3, @@ -27,17 +27,19 @@ use miden_objects::testing::account_id::{ ACCOUNT_ID_SENDER, AccountIdBuilder, }; -use miden_objects::testing::asset::NonFungibleAssetBuilder; -use miden_objects::testing::constants::{ +use miden_protocol::testing::asset::NonFungibleAssetBuilder; +use miden_protocol::testing::constants::{ CONSUMED_ASSET_1_AMOUNT, CONSUMED_ASSET_3_AMOUNT, FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA, NON_FUNGIBLE_ASSET_DATA_2, }; -use miden_objects::testing::storage::{STORAGE_INDEX_0, STORAGE_INDEX_2}; -use miden_objects::transaction::TransactionScript; -use miden_objects::{EMPTY_WORD, Felt, FieldElement, LexicographicWord, Word, ZERO}; +use miden_protocol::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0}; +use miden_protocol::transaction::TransactionScript; +use miden_protocol::{EMPTY_WORD, Felt, FieldElement, LexicographicWord, Word, ZERO}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockAccountComponent; use miden_tx::LocalTransactionProver; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -106,71 +108,80 @@ async fn delta_nonce() -> anyhow::Result<()> { /// - Slot 3: [1,3,5,7] -> [2,3,4,5] -> [1,3,5,7] -> Delta: None #[tokio::test] async fn storage_delta_for_value_slots() -> anyhow::Result<()> { + let slot_0_name = StorageSlotName::mock(0); let slot_0_init_value = Word::from([2, 4, 6, 8u32]); let slot_0_tmp_value = Word::from([3, 4, 5, 6u32]); let slot_0_final_value = EMPTY_WORD; + let slot_1_name = StorageSlotName::mock(1); let slot_1_init_value = EMPTY_WORD; let slot_1_final_value = Word::from([3, 4, 5, 6u32]); + let slot_2_name = StorageSlotName::mock(2); let slot_2_init_value = Word::from([1, 3, 5, 7u32]); let slot_2_final_value = slot_2_init_value; + let slot_3_name = StorageSlotName::mock(3); let slot_3_init_value = Word::from([1, 3, 5, 7u32]); let slot_3_tmp_value = Word::from([2, 3, 4, 5u32]); let slot_3_final_value = slot_3_init_value; let TestSetup { mock_chain, account_id, .. } = setup_test( vec![ - StorageSlot::Value(slot_0_init_value), - StorageSlot::Value(slot_1_init_value), - StorageSlot::Value(slot_2_init_value), - StorageSlot::Value(slot_3_init_value), + StorageSlot::with_value(slot_0_name.clone(), slot_0_init_value), + StorageSlot::with_value(slot_1_name.clone(), slot_1_init_value), + StorageSlot::with_value(slot_2_name.clone(), slot_2_init_value), + StorageSlot::with_value(slot_3_name.clone(), slot_3_init_value), ], [], [], )?; - let tx_script = compile_tx_script(format!( - " + let tx_script = parse_tx_script(format!( + r#" + const SLOT_0_NAME = word("{slot_0_name}") + const SLOT_1_NAME = word("{slot_1_name}") + const SLOT_2_NAME = word("{slot_2_name}") + const SLOT_3_NAME = word("{slot_3_name}") + begin push.{slot_0_tmp_value} - push.0 - # => [index, VALUE] + push.SLOT_0_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, VALUE] exec.set_item # => [] push.{slot_0_final_value} - push.0 - # => [index, VALUE] + push.SLOT_0_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, VALUE] exec.set_item # => [] push.{slot_1_final_value} - push.1 - # => [index, VALUE] + push.SLOT_1_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, VALUE] exec.set_item # => [] push.{slot_2_final_value} - push.2 - # => [index, VALUE] + push.SLOT_2_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, VALUE] exec.set_item # => [] push.{slot_3_tmp_value} - push.3 - # => [index, VALUE] + push.SLOT_3_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, VALUE] exec.set_item # => [] push.{slot_3_final_value} - push.3 - # => [index, VALUE] + push.SLOT_3_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, VALUE] exec.set_item # => [] end - " + "# ))?; let executed_tx = mock_chain @@ -186,12 +197,14 @@ async fn storage_delta_for_value_slots() -> anyhow::Result<()> { .account_delta() .storage() .values() - .iter() - .map(|(k, v)| (*k, *v)) - .collect::>(); + .map(|(slot_name, value)| (slot_name.clone(), *value)) + .collect::>(); // Note that slots 2 and 3 are absent because their values haven't effectively changed. - assert_eq!(storage_values_delta, &[(0u8, slot_0_final_value), (1u8, slot_1_final_value)]); + assert_eq!( + storage_values_delta, + BTreeMap::from_iter([(slot_0_name, slot_0_final_value), (slot_1_name, slot_1_final_value)]) + ); Ok(()) } @@ -234,81 +247,97 @@ async fn storage_delta_for_map_slots() -> anyhow::Result<()> { let key5_tmp_value = Word::from([2, 3, 4, 5u32]); let key5_final_value = Word::from([1, 2, 3, 4u32]); + let slot_0_name = StorageSlotName::mock(0); let mut map0 = StorageMap::new(); map0.insert(key0, key0_init_value).unwrap(); map0.insert(key1, key1_init_value).unwrap(); + let slot_1_name = StorageSlotName::mock(1); let mut map1 = StorageMap::new(); map1.insert(key2, key2_init_value).unwrap(); map1.insert(key3, key3_init_value).unwrap(); map1.insert(key4, key4_init_value).unwrap(); + let slot_2_name = StorageSlotName::mock(2); let mut map2 = StorageMap::new(); map2.insert(key5, key5_init_value).unwrap(); let TestSetup { mock_chain, account_id, .. } = setup_test( vec![ - StorageSlot::Map(map0), - StorageSlot::Map(map1), - StorageSlot::Map(map2), + StorageSlot::with_map(slot_0_name.clone(), map0), + StorageSlot::with_map(slot_1_name.clone(), map1), + StorageSlot::with_map(slot_2_name.clone(), map2), // Include an empty map which does not receive any updates, to test that the "metadata // header" in the delta commitment is not appended if there are no updates to a map // slot. - StorageSlot::Map(StorageMap::new()), + StorageSlot::with_map(StorageSlotName::mock(3), StorageMap::new()), ], [], [], )?; - let tx_script = compile_tx_script(format!( - " + let tx_script = parse_tx_script(format!( + r#" + const SLOT_0_NAME = word("{slot_0_name}") + const SLOT_1_NAME = word("{slot_1_name}") + const SLOT_2_NAME = word("{slot_2_name}") + begin - push.{key0_final_value} push.{key0} push.0 - # => [index, KEY, VALUE] + push.{key0_final_value} push.{key0} + push.SLOT_0_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key1_tmp_value} push.{key1} push.0 - # => [index, KEY, VALUE] + push.{key1_tmp_value} push.{key1} + push.SLOT_0_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key1_final_value} push.{key1} push.0 - # => [index, KEY, VALUE] + push.{key1_final_value} push.{key1} + push.SLOT_0_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key2_final_value} push.{key2} push.1 - # => [index, KEY, VALUE] + push.{key2_final_value} push.{key2} + push.SLOT_1_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key3_final_value} push.{key3} push.1 - # => [index, KEY, VALUE] + push.{key3_final_value} push.{key3} + push.SLOT_1_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key4_tmp_value} push.{key4} push.1 - # => [index, KEY, VALUE] + push.{key4_tmp_value} push.{key4} + push.SLOT_1_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key4_final_value} push.{key4} push.1 - # => [index, KEY, VALUE] + push.{key4_final_value} push.{key4} + push.SLOT_1_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key5_tmp_value} push.{key5} push.2 - # => [index, KEY, VALUE] + push.{key5_tmp_value} push.{key5} + push.SLOT_2_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] - push.{key5_final_value} push.{key5} push.2 - # => [index, KEY, VALUE] + push.{key5_final_value} push.{key5} + push.SLOT_2_NAME[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] exec.set_map_item # => [] end - " + "# ))?; let executed_tx = mock_chain @@ -318,17 +347,25 @@ async fn storage_delta_for_map_slots() -> anyhow::Result<()> { .execute() .await .context("failed to execute transaction")?; - let maps_delta = executed_tx.account_delta().storage().maps(); + let maps_delta = executed_tx.account_delta().storage().maps().collect::>(); // Note that there should be no delta for map2 since it was normalized to an empty map which // should be removed. assert_eq!(maps_delta.len(), 2); - assert!(maps_delta.get(&2).is_none(), "map2 should not have a delta"); + assert!(!maps_delta.contains_key(&slot_2_name), "map2 should not have a delta"); - let mut map0_delta = - maps_delta.get(&0).expect("delta for map 0 should exist").clone().into_map(); - let mut map1_delta = - maps_delta.get(&1).expect("delta for map 1 should exist").clone().into_map(); + let mut map0_delta = maps_delta + .get(&slot_0_name) + .map(|map_delta| (*map_delta).clone()) + .expect("delta for map 0 should exist") + .into_map(); + + let mut map1_delta = maps_delta + .get(&slot_1_name) + .map(|map_delta| (*map_delta).clone()) + .expect("delta for map 1 should exist") + .clone() + .into_map(); assert_eq!(map0_delta.len(), 2); assert_eq!(map0_delta.remove(&LexicographicWord::new(key0)).unwrap(), key0_final_value); @@ -387,7 +424,7 @@ async fn fungible_asset_delta() -> anyhow::Result<()> { [added_asset0, added_asset1, added_asset2, added_asset4].map(Asset::from), )?; - let tx_script = compile_tx_script(format!( + let tx_script = parse_tx_script(format!( " begin push.{asset0} exec.create_note_with_asset @@ -479,7 +516,7 @@ async fn non_fungible_asset_delta() -> anyhow::Result<()> { let TestSetup { mock_chain, account_id, notes } = setup_test([], [asset1, asset3].map(Asset::from), [asset0, asset2].map(Asset::from))?; - let tx_script = compile_tx_script(format!( + let tx_script = parse_tx_script(format!( " begin push.{asset1} exec.create_note_with_asset @@ -562,27 +599,13 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { let removed_assets = [removed_asset_1, removed_asset_2, removed_asset_3]; let tag1 = - NoteTag::from_account_id(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?); - let tag2 = NoteTag::for_local_use_case(0, 0)?; - let tag3 = NoteTag::for_local_use_case(0, 0)?; + NoteTag::with_account_target(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?); + let tag2 = NoteTag::default(); + let tag3 = NoteTag::default(); let tags = [tag1, tag2, tag3]; - let aux_array = [Felt::new(27), Felt::new(28), Felt::new(29)]; - let note_types = [NoteType::Private; 3]; - tag1.validate(NoteType::Private) - .expect("note tag 1 should support private notes"); - tag2.validate(NoteType::Private) - .expect("note tag 2 should support private notes"); - tag3.validate(NoteType::Private) - .expect("note tag 3 should support private notes"); - - let execution_hint_1 = Felt::from(NoteExecutionHint::always()); - let execution_hint_2 = Felt::from(NoteExecutionHint::none()); - let execution_hint_3 = Felt::from(NoteExecutionHint::on_block_slot(1, 1, 1)); - let hints = [execution_hint_1, execution_hint_2, execution_hint_3]; - let mut send_asset_script = String::new(); for i in 0..3 { send_asset_script.push_str(&format!( @@ -590,40 +613,35 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { ### note {i} # prepare the stack for a new note creation push.0.1.2.3 # recipient - push.{EXECUTION_HINT} # note_execution_hint push.{NOTETYPE} # note_type - push.{aux} # aux push.{tag} # tag - # => [tag, aux, note_type, execution_hint, RECIPIENT] - - # pad the stack before calling the `create_note` - padw padw swapdw - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] + # => [tag, note_type, RECIPIENT] # create the note - call.output_note::create + exec.output_note::create # => [note_idx, pad(15)] # move an asset to the created note to partially deplete fungible asset balance swapw dropw push.{REMOVED_ASSET} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note # => [ASSET, note_idx, pad(11)] # clear the stack dropw dropw dropw dropw ", - EXECUTION_HINT = hints[i], NOTETYPE = note_types[i] as u8, - aux = aux_array[i], tag = tags[i], REMOVED_ASSET = Word::from(removed_assets[i]) )); } let tx_script_src = format!( - "\ - use.mock::account - use.miden::output_note + r#" + use mock::account + use miden::protocol::output_note + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") + const MOCK_MAP_SLOT = word("{mock_map_slot}") ## TRANSACTION SCRIPT ## ======================================================================================== @@ -635,8 +653,8 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { # => [13, 11, 9, 7] # get the index of account storage slot - push.{STORAGE_INDEX_0} - # => [idx, 13, 11, 9, 7] + push.MOCK_VALUE_SLOT0[0..2] + # => [slot_id_prefix, slot_id_suffix, 13, 11, 9, 7] # update the storage value call.account::set_item dropw # => [] @@ -652,8 +670,8 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { # => [14, 15, 16, 17, 18, 19, 20, 21] # get the index of account storage slot - push.{STORAGE_INDEX_2} - # => [idx, 14, 15, 16, 17, 18, 19, 20, 21] + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, 14, 15, 16, 17, 18, 19, 20, 21] # update the storage value call.account::set_map_item dropw dropw dropw @@ -665,10 +683,12 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { dropw dropw dropw dropw end - " + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + mock_map_slot = &*MOCK_MAP_SLOT, ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_src)?; // Create the input note that carries the assets that we will assert later let input_note = { @@ -708,22 +728,27 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { // storage delta // -------------------------------------------------------------------------------------------- // We expect one updated item and one updated map - assert_eq!(executed_transaction.account_delta().storage().values().len(), 1); + assert_eq!(executed_transaction.account_delta().storage().values().count(), 1); assert_eq!( - executed_transaction.account_delta().storage().values().get(&STORAGE_INDEX_0), - Some(&updated_slot_value) + executed_transaction + .account_delta() + .storage() + .get(&MOCK_VALUE_SLOT0) + .cloned() + .map(StorageSlotDelta::unwrap_value), + Some(updated_slot_value) ); - assert_eq!(executed_transaction.account_delta().storage().maps().len(), 1); + assert_eq!(executed_transaction.account_delta().storage().maps().count(), 1); let map_delta = executed_transaction .account_delta() .storage() - .maps() - .get(&STORAGE_INDEX_2) - .context("failed to get expected value from storage map")? - .entries(); + .get(&MOCK_MAP_SLOT) + .cloned() + .map(StorageSlotDelta::unwrap_map) + .unwrap(); assert_eq!( - *map_delta.get(&LexicographicWord::new(updated_map_key)).unwrap(), + *map_delta.entries().get(&LexicographicWord::new(updated_map_key)).unwrap(), updated_map_value ); @@ -778,44 +803,47 @@ async fn proven_tx_storage_maps_matches_executed_tx_for_new_account() -> anyhow: (rand_value(), rand_value()), ])?; + let map0_slot_name = StorageSlotName::mock(1); + let map1_slot_name = StorageSlotName::mock(2); + let map2_slot_name = StorageSlotName::mock(4); + // Build a public account so the proven transaction includes the account update. let account = AccountBuilder::new([1; 32]) .storage_mode(AccountStorageMode::Public) .with_auth_component(Auth::IncrNonce) .with_component(MockAccountComponent::with_slots(vec![ - AccountStorage::mock_item_0().slot, - StorageSlot::Map(map0.clone()), - StorageSlot::Map(map1.clone()), - AccountStorage::mock_item_1().slot, - StorageSlot::Map(map2.clone()), + AccountStorage::mock_value_slot0(), + StorageSlot::with_map(map0_slot_name.clone(), map0.clone()), + StorageSlot::with_map(map1_slot_name.clone(), map1.clone()), + AccountStorage::mock_value_slot1(), + StorageSlot::with_map(map2_slot_name.clone(), map2.clone()), ])) .build()?; - let map0_index = 1; - let map1_index = 2; - let map2_index = 4; // Fetch a random existing key from the map. let existing_key = *map2.entries().next().unwrap().0; let value0 = Word::from([3, 4, 5, 6u32]); let code = format!( - " - use.mock::account + r#" + use mock::account + + const MAP_SLOT=word("{map2_slot_name}") begin # Update an existing key. push.{value0} push.{existing_key} - push.{map2_index} - # => [index, KEY, VALUE] + push.MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] call.account::set_map_item - exec.::std::sys::truncate_stack + exec.::miden::core::sys::truncate_stack end - " + "# ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; @@ -828,15 +856,24 @@ async fn proven_tx_storage_maps_matches_executed_tx_for_new_account() -> anyhow: map2.insert(existing_key, value0)?; - for (map_index, expected_map) in [(map0_index, map0), (map1_index, map1), (map2_index, map2)] { - let map_delta = tx.account_delta().storage().maps().get(&map_index).unwrap(); + for (slot_name, expected_map) in + [(map0_slot_name, map0), (map1_slot_name, map1), (map2_slot_name, map2)] + { + let map_delta = tx + .account_delta() + .storage() + .get(&slot_name) + .cloned() + .map(StorageSlotDelta::unwrap_map) + .unwrap(); assert_eq!( map_delta .entries() .iter() - .map(|(key, value)| (*key.inner(), *value)) + .map(|(key, value)| (key.inner(), value)) .collect::>(), - expected_map.into_entries() + expected_map.entries().collect(), + "map delta does not match for slot {slot_name}", ); } @@ -873,13 +910,16 @@ async fn proven_tx_storage_maps_matches_executed_tx_for_new_account() -> anyhow: /// delta and not normalized away. #[tokio::test] async fn delta_for_new_account_retains_empty_value_storage_slots() -> anyhow::Result<()> { + let slot_name0 = StorageSlotName::mock(0); + let slot_name1 = StorageSlotName::mock(1); + let slot_value2 = Word::from([1, 2, 3, 4u32]); let mut account = AccountBuilder::new(rand::random()) .account_type(AccountType::RegularAccountUpdatableCode) .storage_mode(AccountStorageMode::Network) .with_component(MockAccountComponent::with_slots(vec![ - StorageSlot::empty_value(), - StorageSlot::Value(slot_value2), + StorageSlot::with_empty_value(slot_name0.clone()), + StorageSlot::with_value(slot_name1.clone(), slot_value2), ])) .with_auth_component(Auth::IncrNonce) .build()?; @@ -892,9 +932,25 @@ async fn delta_for_new_account_retains_empty_value_storage_slots() -> anyhow::Re panic!("expected delta"); }; - assert_eq!(delta.storage().values().len(), 2); - assert_eq!(delta.storage().values().get(&0).unwrap(), &Word::empty()); - assert_eq!(delta.storage().values().get(&1).unwrap(), &slot_value2); + assert_eq!(delta.storage().values().count(), 2); + assert_eq!( + delta + .storage() + .get(&slot_name0) + .cloned() + .map(StorageSlotDelta::unwrap_value) + .unwrap(), + Word::empty() + ); + assert_eq!( + delta + .storage() + .get(&slot_name1) + .cloned() + .map(StorageSlotDelta::unwrap_value) + .unwrap(), + slot_value2 + ); let recreated_account = Account::try_from(delta)?; // The recreated account should match the original account with the nonce incremented (and the @@ -909,10 +965,14 @@ async fn delta_for_new_account_retains_empty_value_storage_slots() -> anyhow::Re /// delta. #[tokio::test] async fn delta_for_new_account_retains_empty_map_storage_slots() -> anyhow::Result<()> { + let slot_name0 = StorageSlotName::mock(0); + let mut account = AccountBuilder::new(rand::random()) .account_type(AccountType::RegularAccountUpdatableCode) .storage_mode(AccountStorageMode::Network) - .with_component(MockAccountComponent::with_slots(vec![StorageSlot::empty_map()])) + .with_component(MockAccountComponent::with_slots(vec![StorageSlot::with_empty_map( + slot_name0.clone(), + )])) .with_auth_component(Auth::IncrNonce) .build()?; @@ -924,8 +984,16 @@ async fn delta_for_new_account_retains_empty_map_storage_slots() -> anyhow::Resu panic!("expected delta"); }; - assert_eq!(delta.storage().maps().len(), 1); - assert!(delta.storage().maps().get(&0).unwrap().is_empty()); + assert_eq!(delta.storage().maps().count(), 1); + assert!( + delta + .storage() + .get(&slot_name0) + .cloned() + .map(StorageSlotDelta::unwrap_map) + .unwrap() + .is_empty() + ); let recreated_account = Account::try_from(delta)?; // The recreated account should match the original account with the nonce incremented (and the @@ -999,7 +1067,7 @@ fn setup_test( }) } -fn compile_tx_script(code: impl AsRef) -> anyhow::Result { +fn parse_tx_script(code: impl AsRef) -> anyhow::Result { let code = format!( " {TEST_ACCOUNT_CONVENIENCE_WRAPPERS} @@ -1008,20 +1076,20 @@ fn compile_tx_script(code: impl AsRef) -> anyhow::Result code = code.as_ref() ); - ScriptBuilder::with_mock_libraries()? + CodeBuilder::with_mock_libraries() .compile_tx_script(&code) - .context("failed to compile tx script") + .context("failed to parse tx script") } const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " - use.mock::account - use.miden::output_note + use mock::account + use miden::protocol::output_note - #! Inputs: [index, VALUE] + #! Inputs: [slot_id_prefix, slot_id_suffix, VALUE] #! Outputs: [] - proc.set_item - repeat.11 push.0 movdn.5 end - # => [index, VALUE, pad(11)] + proc set_item + repeat.10 push.0 movdn.6 end + # => [slot_id_prefix, slot_id_suffix, VALUE, pad(10)] call.account::set_item # => [OLD_VALUE, pad(12)] @@ -1029,14 +1097,14 @@ const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " dropw dropw dropw dropw end - #! Inputs: [index, KEY, VALUE] + #! Inputs: [slot_id_prefix, slot_id_suffix, KEY, VALUE] #! Outputs: [] - proc.set_map_item - repeat.7 push.0 movdn.9 end - # => [index, KEY, VALUE, pad(7)] + proc set_map_item + repeat.6 push.0 movdn.10 end + # => [index, KEY, VALUE, pad(6)] call.account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] + # => [OLD_VALUE, pad(12)] dropw dropw dropw dropw # => [] @@ -1044,15 +1112,13 @@ const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " #! Inputs: [ASSET] #! Outputs: [] - proc.create_note_with_asset + proc create_note_with_asset push.0.1.2.3 # recipient - push.1 # note_execution_hint push.2 # note_type private - push.0 # aux push.0xC0000000 # tag - # => [tag, aux, note_type, execution_hint, RECIPIENT, ASSET] + # => [tag, note_type, RECIPIENT, ASSET] - exec.create_note + exec.output_note::create # => [note_idx, ASSET] movdn.4 @@ -1062,22 +1128,9 @@ const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " # => [] end - #! Inputs: [tag, aux, note_type, execution_hint, RECIPIENT] - #! Outputs: [note_idx] - proc.create_note - repeat.8 push.0 movdn.8 end - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] - - call.output_note::create - # => [note_idx, pad(15)] - - repeat.15 swap drop end - # => [note_idx] - end - #! Inputs: [ASSET, note_idx] #! Outputs: [] - proc.move_asset_to_note + proc move_asset_to_note repeat.11 push.0 movdn.5 end # => [ASSET, note_idx, pad(11)] @@ -1089,7 +1142,7 @@ const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " #! Inputs: [ASSET] #! Outputs: [ASSET'] - proc.add_asset + proc add_asset repeat.12 push.0 movdn.4 end # => [ASSET, pad(12)] @@ -1102,7 +1155,7 @@ const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " #! Inputs: [ASSET] #! Outputs: [ASSET] - proc.remove_asset + proc remove_asset repeat.12 push.0 movdn.4 end # => [ASSET, pad(12)] 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 133f437676..dc4beb3519 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 @@ -2,33 +2,36 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use assert_matches::assert_matches; -use miden_lib::note::{NoteConsumptionStatus, WellKnownNote, create_p2id_note, create_p2ide_note}; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::transaction::TransactionKernel; -use miden_objects::account::{Account, AccountId}; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::crypto::rand::FeltRng; -use miden_objects::note::{ +use miden_processor::ExecutionError; +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::{Account, AccountId}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::transaction::{InputNote, OutputNote}; -use miden_objects::{Felt, StarkField, Word, ZERO}; -use miden_processor::ExecutionError; -use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::transaction::{InputNote, OutputNote, TransactionKernel}; +use miden_protocol::{Felt, StarkField, Word}; +use miden_standards::note::{ + NoteConsumptionStatus, + WellKnownNote, + create_p2id_note, + create_p2ide_note, +}; +use miden_standards::testing::mock_account::MockAccountExt; +use miden_standards::testing::note::NoteBuilder; use miden_tx::auth::UnreachableAuth; use miden_tx::{ FailedNote, @@ -789,10 +792,8 @@ fn create_p2ide_note_with_inputs(inputs: impl IntoIterator, sender: NoteInputs::new(inputs.into_iter().map(Felt::new).collect()).unwrap(), ); - let tag = NoteTag::from_account_id(sender); - let metadata = - NoteMetadata::new(sender, NoteType::Public, tag, NoteExecutionHint::always(), ZERO) - .unwrap(); + let tag = NoteTag::with_account_target(sender); + let metadata = NoteMetadata::new(sender, NoteType::Public, tag); Note::new(NoteAssets::default(), metadata, recipient) } 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 ae4122d326..3b2301338f 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 @@ -1,28 +1,27 @@ use alloc::string::String; use anyhow::Context; -use miden_lib::errors::tx_kernel_errors::ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::Account; -use miden_objects::asset::FungibleAsset; -use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin}; -use miden_objects::note::{ +use miden_protocol::account::Account; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; +use miden_protocol::errors::tx_kernel::ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::{EMPTY_WORD, Felt, ONE, WORD_SIZE, Word}; +use miden_protocol::{EMPTY_WORD, Felt, ONE, WORD_SIZE, Word}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::mock_account::MockAccountExt; use crate::kernel_tests::tx::ExecutionOutputExt; use crate::utils::create_public_p2any_note; @@ -49,16 +48,16 @@ async fn test_active_note_get_sender_fails_from_tx_script() -> anyhow::Result<() mock_chain.prove_next_block()?; let code = " - use.miden::active_note + use miden::protocol::active_note begin # try to get the sender from transaction script exec.active_note::get_sender end "; - let tx_script = ScriptBuilder::default() + let tx_script = CodeBuilder::default() .compile_tx_script(code) - .context("failed to compile tx script")?; + .context("failed to parse tx script")?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[p2id_note.id()], &[])? @@ -90,9 +89,9 @@ async fn test_active_note_get_metadata() -> anyhow::Result<()> { let code = format!( r#" - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::active_note + use $kernel::prologue + use $kernel::note->note_internal + use miden::protocol::active_note begin exec.prologue::prepare_transaction @@ -101,17 +100,23 @@ async fn test_active_note_get_metadata() -> anyhow::Result<()> { # get the metadata of the active note exec.active_note::get_metadata - # => [METADATA] + # => [NOTE_ATTACHMENT, METADATA_HEADER] - # assert this metadata - push.{METADATA} + push.{NOTE_ATTACHMENT} + assert_eqw.err="note 0 has incorrect note attachment" + # => [METADATA_HEADER] + + push.{METADATA_HEADER} assert_eqw.err="note 0 has incorrect metadata" + # => [] # truncate the stack swapw dropw end "#, - METADATA = Word::from(tx_context.input_notes().get_note(0).note().metadata()) + METADATA_HEADER = tx_context.input_notes().get_note(0).note().metadata().to_header_word(), + NOTE_ATTACHMENT = + tx_context.input_notes().get_note(0).note().metadata().to_attachment_word() ); tx_context.execute_code(&code).await?; @@ -135,9 +140,9 @@ async fn test_active_note_get_sender() -> anyhow::Result<()> { // calling get_sender should return sender of the active note let code = " - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::active_note + use $kernel::prologue + use $kernel::note->note_internal + use miden::protocol::active_note begin exec.prologue::prepare_transaction @@ -198,10 +203,10 @@ async fn test_active_note_get_assets() -> anyhow::Result<()> { let mut code = String::new(); for asset in note.assets().iter() { code += &format!( - " + r#" # assert the asset is correct - dup padw movup.4 mem_loadw_be push.{asset} assert_eqw push.4 add - ", + dup padw movup.4 mem_loadw_be push.{asset} assert_eqw.err="asset mismatch" push.4 add + "#, asset = Word::from(asset) ); } @@ -210,14 +215,14 @@ async fn test_active_note_get_assets() -> anyhow::Result<()> { // calling get_assets should return assets at the specified address let code = format!( - " - use.std::sys + r#" + use miden::core::sys - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::active_note + use $kernel::prologue + use $kernel::note->note_internal + use miden::protocol::active_note - proc.process_note_0 + proc process_note_0 # drop the note inputs dropw dropw dropw dropw @@ -228,10 +233,10 @@ async fn test_active_note_get_assets() -> anyhow::Result<()> { exec.active_note::get_assets # assert the number of assets is correct - eq.{note_0_num_assets} assert + eq.{note_0_num_assets} assert.err="unexpected num assets for note 0" # assert the pointer is returned - dup eq.{DEST_POINTER_NOTE_0} assert + dup eq.{DEST_POINTER_NOTE_0} assert.err="unexpected dest ptr for note 0" # asset memory assertions {NOTE_0_ASSET_ASSERTIONS} @@ -240,7 +245,7 @@ async fn test_active_note_get_assets() -> anyhow::Result<()> { drop end - proc.process_note_1 + proc process_note_1 # drop the note inputs dropw dropw dropw dropw @@ -251,10 +256,10 @@ async fn test_active_note_get_assets() -> anyhow::Result<()> { exec.active_note::get_assets # assert the number of assets is correct - eq.{note_1_num_assets} assert + eq.{note_1_num_assets} assert.err="unexpected num assets for note 1" # assert the pointer is returned - dup eq.{DEST_POINTER_NOTE_1} assert + dup eq.{DEST_POINTER_NOTE_1} assert.err="unexpected dest ptr for note 1" # asset memory assertions {NOTE_1_ASSET_ASSERTIONS} @@ -285,7 +290,7 @@ async fn test_active_note_get_assets() -> anyhow::Result<()> { # truncate the stack exec.sys::truncate_stack end - ", + "#, note_0_num_assets = notes.get_note(0).note().assets().num_assets(), note_1_num_assets = notes.get_note(1).note().assets().num_assets(), NOTE_0_ASSET_ASSERTIONS = construct_asset_assertions(notes.get_note(0).note()), @@ -340,10 +345,10 @@ async fn test_active_note_get_inputs() -> anyhow::Result<()> { let note0 = tx_context.input_notes().get_note(0).note(); let code = format!( - " - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::active_note + r#" + use $kernel::prologue + use $kernel::note->note_internal + use miden::protocol::active_note begin # => [BH, acct_id, IAH, NC] @@ -360,10 +365,10 @@ async fn test_active_note_get_inputs() -> anyhow::Result<()> { push.{NOTE_0_PTR} exec.active_note::get_inputs # => [num_inputs, dest_ptr] - eq.{num_inputs} assert + eq.{num_inputs} assert.err="unexpected num inputs" # => [dest_ptr] - dup eq.{NOTE_0_PTR} assert + dup eq.{NOTE_0_PTR} assert.err="unexpected dest ptr" # => [dest_ptr] # apply note 1 inputs assertions @@ -374,7 +379,7 @@ async fn test_active_note_get_inputs() -> anyhow::Result<()> { drop # => [] end - ", + "#, num_inputs = note0.inputs().num_values(), inputs_assertions = construct_inputs_assertions(note0), NOTE_0_PTR = 100000000, @@ -385,8 +390,8 @@ async fn test_active_note_get_inputs() -> anyhow::Result<()> { } /// This test checks the scenario when an input note has exactly 8 inputs, and the transaction -/// script attempts to load the inputs to memory using the `miden::active_note::get_inputs` -/// procedure. +/// script attempts to load the inputs to memory using the +/// `miden::protocol::active_note::get_inputs` procedure. /// /// Previously this setup was leading to the incorrect number of note inputs computed during the /// `get_inputs` procedure, see the [issue #1363](https://github.com/0xMiden/miden-base/issues/1363) @@ -402,19 +407,12 @@ async fn test_active_note_get_exactly_8_inputs() -> anyhow::Result<()> { // prepare note data let serial_num = RpoRandomCoin::new(Word::from([4u32; 4])).draw_word(); - let tag = NoteTag::from_account_id(target_id); - let metadata = NoteMetadata::new( - sender_id, - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - ) - .context("failed to create metadata")?; + let tag = NoteTag::with_account_target(target_id); + let metadata = NoteMetadata::new(sender_id, NoteType::Public, tag); let vault = NoteAssets::new(vec![]).context("failed to create input note assets")?; - let note_script = ScriptBuilder::default() + let note_script = CodeBuilder::default() .compile_note_script("begin nop end") - .context("failed to compile note script")?; + .context("failed to parse note script")?; // create a recipient with note inputs, which number divides by 8. For simplicity create 8 input // values @@ -441,8 +439,8 @@ async fn test_active_note_get_exactly_8_inputs() -> anyhow::Result<()> { .build()?; let tx_code = " - use.$kernel::prologue - use.miden::active_note + use $kernel::prologue + use miden::protocol::active_note begin exec.prologue::prepare_transaction @@ -484,8 +482,8 @@ async fn test_active_note_get_serial_number() -> anyhow::Result<()> { // calling get_serial_number should return the serial number of the active note let code = " - use.$kernel::prologue - use.miden::active_note + use $kernel::prologue + use miden::protocol::active_note begin exec.prologue::prepare_transaction @@ -523,8 +521,8 @@ async fn test_active_note_get_script_root() -> anyhow::Result<()> { // calling get_script_root should return script root of the active note let code = " - use.$kernel::prologue - use.miden::active_note + use $kernel::prologue + use miden::protocol::active_note begin exec.prologue::prepare_transaction diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index 092e2608ac..adbacaf792 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -1,12 +1,12 @@ -use miden_objects::account::AccountId; -use miden_objects::asset::NonFungibleAsset; -use miden_objects::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; -use miden_objects::testing::constants::{ +use miden_protocol::account::AccountId; +use miden_protocol::asset::NonFungibleAsset; +use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; +use miden_protocol::testing::constants::{ FUNGIBLE_ASSET_AMOUNT, FUNGIBLE_FAUCET_INITIAL_BALANCE, NON_FUNGIBLE_ASSET_DATA, }; -use miden_objects::{Felt, Hasher, Word}; +use miden_protocol::{Felt, Hasher, Word}; use crate::TransactionContextBuilder; use crate::kernel_tests::tx::ExecutionOutputExt; @@ -21,8 +21,8 @@ async fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.miden::faucet + use $kernel::prologue + use miden::protocol::faucet begin exec.prologue::prepare_transaction @@ -62,8 +62,8 @@ async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.miden::faucet + use $kernel::prologue + use miden::protocol::faucet begin exec.prologue::prepare_transaction @@ -95,7 +95,7 @@ async fn test_validate_non_fungible_asset() -> anyhow::Result<()> { let code = format!( " - use.$kernel::asset + use $kernel::asset begin push.{non_fungible_asset} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs index 37b492d8cc..88c7d85737 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs @@ -1,21 +1,22 @@ use assert_matches::assert_matches; -use miden_lib::errors::tx_kernel_errors::{ +use miden_protocol::account::AccountId; +use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; +use miden_protocol::errors::AssetVaultError; +use miden_protocol::errors::tx_kernel::{ ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW, ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED, ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET, ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS, ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND, }; -use miden_lib::transaction::memory; -use miden_objects::account::AccountId; -use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, }; -use miden_objects::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; -use miden_objects::{AssetVaultError, Felt, ONE, Word, ZERO}; +use miden_protocol::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; +use miden_protocol::transaction::memory; +use miden_protocol::{Felt, ONE, Word, ZERO}; use crate::kernel_tests::tx::ExecutionOutputExt; use crate::{TransactionContextBuilder, assert_execution_error}; @@ -28,8 +29,8 @@ async fn get_balance_returns_correct_amount() -> anyhow::Result<()> { let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let code = format!( r#" - use.$kernel::prologue - use.miden::active_account + use $kernel::prologue + use miden::protocol::active_account begin exec.prologue::prepare_transaction @@ -64,10 +65,9 @@ async fn peek_balance_returns_correct_amount() -> anyhow::Result<()> { let code = format!( r#" - use.$kernel::prologue - use.$kernel::memory - use.$kernel::asset_vault - use.miden::account + use $kernel::prologue + use $kernel::memory + use $kernel::asset_vault begin exec.prologue::prepare_transaction @@ -113,8 +113,8 @@ async fn test_get_balance_non_fungible_fails() -> anyhow::Result<()> { let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(); let code = format!( " - use.$kernel::prologue - use.miden::active_account + use $kernel::prologue + use miden::protocol::active_account begin exec.prologue::prepare_transaction @@ -144,8 +144,8 @@ async fn test_has_non_fungible_asset() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.miden::active_account + use $kernel::prologue + use miden::protocol::active_account begin exec.prologue::prepare_transaction @@ -182,8 +182,8 @@ async fn test_add_fungible_asset_success() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -229,8 +229,8 @@ async fn test_add_non_fungible_asset_fail_overflow() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -260,8 +260,8 @@ async fn test_add_non_fungible_asset_success() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -302,8 +302,8 @@ async fn test_add_non_fungible_asset_fail_duplicate() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -339,8 +339,8 @@ async fn test_remove_fungible_asset_success_no_balance_remaining() -> anyhow::Re let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -384,8 +384,8 @@ async fn test_remove_fungible_asset_fail_remove_too_much() -> anyhow::Result<()> let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -423,8 +423,8 @@ async fn test_remove_fungible_asset_success_balance_remaining() -> anyhow::Resul let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -472,8 +472,8 @@ async fn test_remove_inexisting_non_fungible_asset_fails() -> anyhow::Result<()> let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction @@ -508,8 +508,8 @@ async fn test_remove_non_fungible_asset_success() -> anyhow::Result<()> { let code = format!( " - use.$kernel::prologue - use.mock::account + use $kernel::prologue + use mock::account begin exec.prologue::prepare_transaction diff --git a/crates/miden-testing/src/kernel_tests/tx/test_auth.rs b/crates/miden-testing/src/kernel_tests/tx/test_auth.rs index 676bd9f4e9..c400161482 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_auth.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_auth.rs @@ -1,13 +1,13 @@ use anyhow::Context; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::errors::MasmError; -use miden_lib::errors::note_script_errors::ERR_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT; -use miden_lib::testing::account_component::{ConditionalAuthComponent, ERR_WRONG_ARGS_MSG}; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{Account, AccountBuilder}; -use miden_objects::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; -use miden_objects::{Felt, ONE}; +use miden_protocol::account::{Account, AccountBuilder}; +use miden_protocol::errors::MasmError; +use miden_protocol::errors::tx_kernel::ERR_EPILOGUE_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT; +use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; +use miden_protocol::{Felt, ONE}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::{ConditionalAuthComponent, ERR_WRONG_ARGS_MSG}; +use miden_standards::testing::mock_account::MockAccountExt; use crate::{Auth, TransactionContextBuilder, assert_transaction_executor_error}; @@ -77,12 +77,12 @@ async fn test_auth_procedure_called_from_wrong_context() -> anyhow::Result<()> { // Create a transaction script that calls the auth procedure let tx_script_source = " begin - call.::auth_incr_nonce + call.::incr_nonce::auth_incr_nonce end "; - let tx_script = ScriptBuilder::default() - .with_dynamically_linked_library(auth_component.library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(auth_component.component_code())? .compile_tx_script(tx_script_source)?; let tx_context = TransactionContextBuilder::new(account).tx_script(tx_script).build()?; @@ -91,7 +91,7 @@ async fn test_auth_procedure_called_from_wrong_context() -> anyhow::Result<()> { assert_transaction_executor_error!( execution_result, - ERR_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT + ERR_EPILOGUE_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT ); Ok(()) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs index 9b778fd556..958727913e 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs @@ -1,38 +1,39 @@ use alloc::string::ToString; use alloc::vec::Vec; +use std::borrow::ToOwned; -use miden_lib::errors::tx_kernel_errors::{ +use miden_processor::crypto::RpoRandomCoin; +use miden_processor::{Felt, ONE}; +use miden_protocol::Word; +use miden_protocol::account::{Account, AccountDelta, AccountStorageDelta, AccountVaultDelta}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::errors::tx_kernel::{ ERR_ACCOUNT_DELTA_NONCE_MUST_BE_INCREMENTED_IF_VAULT_OR_STORAGE_CHANGED, ERR_EPILOGUE_EXECUTED_TRANSACTION_IS_EMPTY, ERR_EPILOGUE_NONCE_CANNOT_BE_0, ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME, ERR_TX_INVALID_EXPIRATION_DELTA, }; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::transaction::EXPIRATION_BLOCK_ELEMENT_IDX; -use miden_lib::transaction::memory::{ - NOTE_MEM_SIZE, - OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET, - OUTPUT_NOTE_SECTION_OFFSET, -}; -use miden_lib::utils::ScriptBuilder; -use miden_objects::Word; -use miden_objects::account::{Account, AccountDelta, AccountStorageDelta, AccountVaultDelta}; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::note::{NoteTag, NoteType}; -use miden_objects::testing::account_id::{ +use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_SENDER, }; -use miden_objects::transaction::{OutputNote, OutputNotes}; -use miden_processor::{Felt, ONE}; +use miden_protocol::testing::storage::MOCK_VALUE_SLOT0; +use miden_protocol::transaction::memory::{ + NOTE_MEM_SIZE, + OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET, + OUTPUT_NOTE_SECTION_OFFSET, +}; +use miden_protocol::transaction::{OutputNote, OutputNotes, TransactionOutputs}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::mock_account::MockAccountExt; +use miden_standards::testing::note::NoteBuilder; -use super::{ZERO, create_mock_notes_procedure}; +use super::ZERO; use crate::kernel_tests::tx::ExecutionOutputExt; -use crate::utils::{create_public_p2any_note, create_spawn_note}; +use crate::utils::{create_p2any_note, create_public_p2any_note}; use crate::{ Auth, MockChain, @@ -42,49 +43,51 @@ use crate::{ assert_transaction_executor_error, }; +/// Tests that the return values from the tx kernel main.masm program match the expected values. #[tokio::test] -async fn test_epilogue() -> anyhow::Result<()> { +async fn test_transaction_epilogue() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - let tx_context = { - let output_note_1 = create_public_p2any_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - [FungibleAsset::mock(100)], - ); - - // input_note_1 is needed for maintaining cohesion of involved assets - let input_note_1 = create_public_p2any_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - [FungibleAsset::mock(100)], - ); - let input_note_2 = create_spawn_note([&output_note_1])?; - TransactionContextBuilder::new(account.clone()) - .extend_input_notes(vec![input_note_1, input_note_2]) - .extend_expected_output_notes(vec![OutputNote::Full(output_note_1)]) - .build()? - }; - - let output_notes_data_procedure = - create_mock_notes_procedure(tx_context.expected_output_notes()); + let asset = FungibleAsset::mock(100); + let output_note_1 = create_public_p2any_note(account.id(), [asset]); + // input_note_1 is needed for maintaining cohesion of involved assets + let input_note_1 = + create_public_p2any_note(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into().unwrap(), [asset]); + + let tx_context = TransactionContextBuilder::new(account.clone()) + .extend_input_notes(vec![input_note_1]) + .extend_expected_output_notes(vec![OutputNote::Full(output_note_1.clone())]) + .build()?; let code = format!( " - use.$kernel::prologue - use.$kernel::account - use.$kernel::epilogue - - {output_notes_data_procedure} + use $kernel::prologue + use $kernel::epilogue + use miden::protocol::output_note + use miden::core::sys begin exec.prologue::prepare_transaction - exec.create_mock_notes + push.{recipient} + push.{note_type} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{asset} + exec.output_note::add_asset + # => [] exec.epilogue::finalize_transaction # truncate the stack - repeat.13 movup.13 drop end + exec.sys::truncate_stack end - " + ", + recipient = output_note_1.recipient().digest(), + note_type = Felt::from(output_note_1.metadata().note_type()), + tag = Felt::from(output_note_1.metadata().tag()), + asset = Word::from(asset) ); let exec_output = tx_context.execute_code(&code).await?; @@ -111,7 +114,7 @@ async fn test_epilogue() -> anyhow::Result<()> { .to_commitment(); let account_update_commitment = - miden_objects::Hasher::merge(&[final_account.commitment(), account_delta_commitment]); + miden_protocol::Hasher::merge(&[final_account.commitment(), account_delta_commitment]); let mut expected_stack = Vec::with_capacity(16); expected_stack.extend(output_notes.commitment().as_elements().iter().rev()); @@ -144,48 +147,70 @@ async fn test_epilogue() -> anyhow::Result<()> { Ok(()) } +/// Tests that the output note memory section is correctly populated during finalize_transaction. #[tokio::test] async fn test_compute_output_note_id() -> anyhow::Result<()> { - let tx_context = { - let account = - Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - let output_note_1 = - create_public_p2any_note(ACCOUNT_ID_SENDER.try_into()?, [FungibleAsset::mock(100)]); - - // input_note_1 is needed for maintaining cohesion of involved assets - let input_note_1 = - create_public_p2any_note(ACCOUNT_ID_SENDER.try_into()?, [FungibleAsset::mock(100)]); - let input_note_2 = create_spawn_note([&output_note_1])?; - TransactionContextBuilder::new(account) - .extend_input_notes(vec![input_note_1, input_note_2]) - .extend_expected_output_notes(vec![OutputNote::Full(output_note_1)]) - .build()? - }; - - let output_notes_data_procedure = - create_mock_notes_procedure(tx_context.expected_output_notes()); - - for (note, i) in tx_context.expected_output_notes().iter().zip(0u32..) { - let code = format!( - " - use.$kernel::prologue - use.$kernel::epilogue + let mut rng = RpoRandomCoin::new(Word::from([3, 4, 5, 6u32])); + let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); + let mut assets = account.vault().assets(); + let asset0 = assets.next().unwrap(); + let asset1 = assets.next().unwrap(); + + let output_note0 = create_p2any_note(account.id(), NoteType::Private, [asset0], &mut rng); + let output_note1 = create_p2any_note(account.id(), NoteType::Private, [asset1], &mut rng); + + let tx_context = TransactionContextBuilder::new(account.clone()) + .extend_expected_output_notes(vec![ + OutputNote::Full(output_note0.clone()), + OutputNote::Full(output_note1.clone()), + ]) + .build()?; - {output_notes_data_procedure} + let mut code = " + use $kernel::prologue + use $kernel::epilogue + use miden::protocol::output_note + use miden::core::sys begin - exec.prologue::prepare_transaction - exec.create_mock_notes - exec.epilogue::finalize_transaction + exec.prologue::prepare_transaction" + .to_owned(); + + for note in tx_context.expected_output_notes() { + let asset = note.assets().iter().next().unwrap(); - # truncate the stack - repeat.13 movup.13 drop end - end + code.push_str(&format!( " - ); + push.{recipient} + push.{note_type} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{asset} + call.::miden::standards::wallets::basic::move_asset_to_note + # => [] + ", + recipient = note.recipient().digest(), + note_type = Felt::from(note.metadata().note_type()), + tag = Felt::from(note.metadata().tag()), + asset = Word::from(asset) + )); + } - let exec_output = &tx_context.execute_code(&code).await?; + code.push_str( + " + exec.epilogue::finalize_transaction + # truncate the stack + exec.sys::truncate_stack + end", + ); + + let exec_output = &tx_context.execute_code(&code).await?; + + for (i, note) in tx_context.expected_output_notes().iter().enumerate() { + let i = i as u32; assert_eq!( note.assets().commitment(), exec_output.get_kernel_mem_word( @@ -197,11 +222,12 @@ async fn test_compute_output_note_id() -> anyhow::Result<()> { ); assert_eq!( - Word::from(note.id()), + note.id().as_word(), exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + i * NOTE_MEM_SIZE), "NOTE_ID didn't match expected value", ); } + Ok(()) } @@ -226,20 +252,20 @@ async fn epilogue_fails_when_num_output_assets_exceed_num_input_assets() -> anyh let code = format!( " - use.mock::account - use.mock::util + use mock::account + use mock::util begin # create a note with the output asset push.{OUTPUT_ASSET} - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] end ", OUTPUT_ASSET = Word::from(output_asset), ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; @@ -279,20 +305,20 @@ async fn epilogue_fails_when_num_input_assets_exceed_num_output_assets() -> anyh let code = format!( " - use.mock::account - use.mock::util + use mock::account + use mock::util begin # create a note with the output asset push.{OUTPUT_ASSET} - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] end ", OUTPUT_ASSET = Word::from(input_asset), ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; @@ -317,10 +343,10 @@ async fn test_block_expiration_height_monotonically_decreases() -> anyhow::Resul let test_pairs: [(u64, u64); 3] = [(9, 12), (18, 3), (20, 20)]; let code_template = " - use.$kernel::prologue - use.$kernel::tx - use.$kernel::epilogue - use.$kernel::account + use $kernel::prologue + use $kernel::tx + use $kernel::epilogue + use $kernel::account begin exec.prologue::prepare_transaction @@ -329,7 +355,7 @@ async fn test_block_expiration_height_monotonically_decreases() -> anyhow::Resul push.{value_2} exec.tx::update_expiration_block_delta - push.{min_value} exec.tx::get_expiration_delta assert_eq + push.{min_value} exec.tx::get_expiration_delta assert_eq.err=\"expiration delta mismatch\" exec.epilogue::finalize_transaction @@ -351,7 +377,9 @@ async fn test_block_expiration_height_monotonically_decreases() -> anyhow::Resul let expected_expiry = v1.min(v2) + tx_context.tx_inputs().block_header().block_num().as_u64(); assert_eq!( - exec_output.get_stack_element(EXPIRATION_BLOCK_ELEMENT_IDX).as_int(), + exec_output + .get_stack_element(TransactionOutputs::EXPIRATION_BLOCK_ELEMENT_IDX) + .as_int(), expected_expiry ); } @@ -365,7 +393,7 @@ async fn test_invalid_expiration_deltas() -> anyhow::Result<()> { let test_values = [0u64, u16::MAX as u64 + 1, u32::MAX as u64]; let code_template = " - use.$kernel::tx + use $kernel::tx begin push.{value_1} @@ -388,15 +416,15 @@ async fn test_no_expiration_delta_set() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code_template = " - use.$kernel::prologue - use.$kernel::epilogue - use.$kernel::tx - use.$kernel::account + use $kernel::prologue + use $kernel::epilogue + use $kernel::tx + use $kernel::account begin exec.prologue::prepare_transaction - exec.tx::get_expiration_delta assertz + exec.tx::get_expiration_delta assertz.err=\"expiration delta should be unset\" exec.epilogue::finalize_transaction @@ -409,7 +437,9 @@ async fn test_no_expiration_delta_set() -> anyhow::Result<()> { // Default value should be equal to u32::MAX, set in the prologue assert_eq!( - exec_output.get_stack_element(EXPIRATION_BLOCK_ELEMENT_IDX).as_int() as u32, + exec_output + .get_stack_element(TransactionOutputs::EXPIRATION_BLOCK_ELEMENT_IDX) + .as_int() as u32, u32::MAX ); @@ -423,17 +453,19 @@ async fn test_epilogue_increment_nonce_success() -> anyhow::Result<()> { let expected_nonce = ONE + ONE; let code = format!( - " - use.$kernel::prologue - use.mock::account - use.$kernel::epilogue - use.$kernel::memory + r#" + use $kernel::prologue + use mock::account + use $kernel::epilogue + use $kernel::memory + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin exec.prologue::prepare_transaction push.1.2.3.4 - push.0 + push.MOCK_VALUE_SLOT0[0..2] call.account::set_item dropw @@ -443,9 +475,10 @@ async fn test_epilogue_increment_nonce_success() -> anyhow::Result<()> { dropw dropw dropw dropw exec.memory::get_account_nonce - push.{expected_nonce} assert_eq + push.{expected_nonce} assert_eq.err="nonce mismatch" end - " + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, ); tx_context.execute_code(code.as_str()).await?; @@ -455,21 +488,26 @@ async fn test_epilogue_increment_nonce_success() -> anyhow::Result<()> { /// Tests that changing the account state without incrementing the nonce results in an error. #[tokio::test] async fn epilogue_fails_on_account_state_change_without_nonce_increment() -> anyhow::Result<()> { - let code = " - use.mock::account + let code = format!( + r#" + use mock::account + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin push.91.92.93.94 - push.0 + push.MOCK_VALUE_SLOT0[0..2] repeat.5 movup.5 drop end - # => [index, VALUE] + # => [slot_id_prefix, slot_id_suffix, VALUE] call.account::set_item # => [PREV_VALUE] dropw end - "; + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(code)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; let result = TransactionContextBuilder::with_noop_auth_account() .tx_script(tx_script) @@ -518,43 +556,36 @@ async fn test_epilogue_execute_empty_transaction() -> anyhow::Result<()> { #[tokio::test] async fn test_epilogue_empty_transaction_with_empty_output_note() -> anyhow::Result<()> { let tag = - NoteTag::from_account_id(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?); - let aux = Felt::new(26); + NoteTag::with_account_target(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?); let note_type = NoteType::Private; - // create an empty output note in the transaction script - let tx_script_source = format!( + // create an empty output note + let code = format!( r#" - use.std::word - use.miden::output_note - use.$kernel::prologue - use.$kernel::epilogue - use.$kernel::note + use miden::core::word + use miden::protocol::output_note + use $kernel::prologue + use $kernel::epilogue + use $kernel::note begin exec.prologue::prepare_transaction # prepare the values for note creation push.1.2.3.4 # recipient - push.1 # note_execution_hint (NoteExecutionHint::Always) push.{note_type} # note_type - push.{aux} # aux push.{tag} # tag - # => [tag, aux, note_type, note_execution_hint, RECIPIENT] - - # pad the stack with zeros before calling the `create_note`. - padw padw swapdw - # => [tag, aux, execution_hint, note_type, RECIPIENT, pad(8)] + # => [tag, note_type, RECIPIENT] # create the note - call.output_note::create - # => [note_idx, GARBAGE(15)] + exec.output_note::create + # => [note_idx] # make sure that output note was created: compare the output note hash with an empty # word exec.note::compute_output_notes_commitment exec.word::eqz assertz.err="output note was created, but the output notes hash remains to be zeros" - # => [note_idx, GARBAGE(15)] + # => [note_idx] # clean the stack dropw dropw dropw dropw @@ -568,7 +599,7 @@ async fn test_epilogue_empty_transaction_with_empty_output_note() -> anyhow::Res let tx_context = TransactionContextBuilder::with_noop_auth_account().build()?; - let result = tx_context.execute_code(&tx_script_source).await.map(|_| ()); + let result = tx_context.execute_code(&code).await.map(|_| ()); // assert that even if the output note was created, the transaction is considered empty assert_execution_error!(result, ERR_EPILOGUE_EXECUTED_TRANSACTION_IS_EMPTY); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs index 7eab71ff10..907a3b58a6 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -1,45 +1,42 @@ use alloc::sync::Arc; -use miden_lib::errors::tx_kernel_errors::{ - ERR_FAUCET_NEW_TOTAL_SUPPLY_WOULD_EXCEED_MAX_ASSET_AMOUNT, - ERR_FAUCET_NON_FUNGIBLE_ASSET_ALREADY_ISSUED, - ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND, - ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, - ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, - ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW, -}; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::transaction::TransactionKernel; -use miden_lib::transaction::memory::NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{ +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, AccountId, + AccountStorage, AccountType, StorageMap, }; -use miden_objects::assembly::DefaultSourceManager; -use miden_objects::asset::{FungibleAsset, NonFungibleAsset}; -use miden_objects::testing::account_id::{ +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; +use miden_protocol::errors::tx_kernel::{ + ERR_FAUCET_NEW_TOTAL_SUPPLY_WOULD_EXCEED_MAX_ASSET_AMOUNT, + ERR_FAUCET_NON_FUNGIBLE_ASSET_ALREADY_ISSUED, + ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND, + ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, + ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, + ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW, +}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, ACCOUNT_ID_SENDER, }; -use miden_objects::testing::constants::{ +use miden_protocol::testing::constants::{ CONSUMED_ASSET_1_AMOUNT, FUNGIBLE_ASSET_AMOUNT, FUNGIBLE_FAUCET_INITIAL_BALANCE, NON_FUNGIBLE_ASSET_DATA, NON_FUNGIBLE_ASSET_DATA_2, }; -use miden_objects::testing::noop_auth_component::NoopAuthComponent; -use miden_objects::testing::storage::FAUCET_STORAGE_DATA_SLOT; -use miden_objects::{Felt, Word}; +use miden_protocol::testing::noop_auth_component::NoopAuthComponent; +use miden_protocol::{Felt, Word}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::mock_account::MockAccountExt; -use crate::kernel_tests::tx::ExecutionOutputExt; use crate::utils::create_public_p2any_note; use crate::{TransactionContextBuilder, assert_execution_error, assert_transaction_executor_error}; @@ -49,24 +46,22 @@ use crate::{TransactionContextBuilder, assert_execution_error, assert_transactio #[tokio::test] async fn test_mint_fungible_asset_succeeds() -> anyhow::Result<()> { let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let tx_context = TransactionContextBuilder::with_fungible_faucet( - faucet_id.into(), - Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), - ) - .build()?; + let expected_final_amount = FUNGIBLE_FAUCET_INITIAL_BALANCE + FUNGIBLE_ASSET_AMOUNT; let code = format!( r#" - use.mock::faucet - use.$kernel::asset_vault - use.$kernel::memory - use.$kernel::prologue + use mock::faucet->mock_faucet + use miden::protocol::faucet + use $kernel::asset_vault + use $kernel::memory + use $kernel::prologue begin - # mint asset exec.prologue::prepare_transaction + + # mint asset push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} - call.faucet::mint + call.mock_faucet::mint # assert the correct asset is returned push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} @@ -77,23 +72,24 @@ async fn test_mint_fungible_asset_succeeds() -> anyhow::Result<()> { push.{suffix} push.{prefix} exec.asset_vault::get_balance push.{FUNGIBLE_ASSET_AMOUNT} assert_eq.err="input vault should contain minted asset" + + exec.faucet::get_total_issuance + push.{expected_final_amount} + assert_eq.err="expected total issuance to be {expected_final_amount}" end "#, prefix = faucet_id.prefix().as_felt(), suffix = faucet_id.suffix(), ); - let exec_output = &tx_context.execute_code(&code).await.unwrap(); - - let expected_final_storage_amount = FUNGIBLE_FAUCET_INITIAL_BALANCE + FUNGIBLE_ASSET_AMOUNT; - let faucet_reserved_slot_storage_location = - FAUCET_STORAGE_DATA_SLOT as u32 + NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR; - let faucet_storage_amount_location = faucet_reserved_slot_storage_location + 3; - - let faucet_storage_amount = - exec_output.get_kernel_mem_element(faucet_storage_amount_location).as_int(); + TransactionContextBuilder::with_fungible_faucet( + faucet_id.into(), + Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), + ) + .build()? + .execute_code(&code) + .await?; - assert_eq!(faucet_storage_amount, expected_final_storage_amount); Ok(()) } @@ -104,7 +100,7 @@ async fn mint_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> let code = format!( " - use.mock::faucet + use mock::faucet begin push.{asset} @@ -113,7 +109,7 @@ async fn mint_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> ", asset = Word::from(FungibleAsset::mock(50)) ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(code)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; let result = TransactionContextBuilder::new(account) .tx_script(tx_script) @@ -135,8 +131,8 @@ async fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin exec.prologue::prepare_transaction @@ -157,7 +153,7 @@ async fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> async fn test_mint_fungible_asset_fails_saturate_max_amount() -> anyhow::Result<()> { let code = format!( " - use.mock::faucet + use mock::faucet begin push.{asset} @@ -166,7 +162,7 @@ async fn test_mint_fungible_asset_fails_saturate_max_amount() -> anyhow::Result< ", asset = Word::from(FungibleAsset::mock(FungibleAsset::MAX_AMOUNT)) ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(code)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; let result = TransactionContextBuilder::with_fungible_faucet( FungibleAsset::mock_issuer().into(), @@ -198,13 +194,15 @@ async fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { let code = format!( r#" - use.std::collections::smt + use miden::core::collections::smt + + use $kernel::account + use $kernel::asset_vault + use $kernel::memory + use $kernel::prologue + use mock::faucet->mock_faucet - use.$kernel::account - use.$kernel::asset_vault - use.$kernel::memory - use.$kernel::prologue - use.mock::faucet->mock_faucet + const FAUCET_SYSDATA_SLOT_NAME = word("{faucet_sysdata_slot_name}") begin # mint asset @@ -223,7 +221,7 @@ async fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { assert.err="vault should contain asset" # assert the non-fungible asset has been added to the faucet smt - push.{FAUCET_STORAGE_DATA_SLOT} + push.FAUCET_SYSDATA_SLOT_NAME[0..2] exec.account::get_item push.{asset_vault_key} exec.smt::get @@ -232,6 +230,7 @@ async fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { dropw end "#, + faucet_sysdata_slot_name = AccountStorage::faucet_sysdata_slot(), non_fungible_asset = Word::from(non_fungible_asset), asset_vault_key = StorageMap::hash_key(asset_vault_key.into()), ); @@ -251,8 +250,8 @@ async fn test_mint_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow:: let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin exec.prologue::prepare_transaction @@ -276,7 +275,7 @@ async fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result let code = format!( " - use.mock::faucet + use mock::faucet begin push.{asset} @@ -285,7 +284,7 @@ async fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result ", asset = Word::from(FungibleAsset::mock(50)) ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(code)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; let result = TransactionContextBuilder::new(account) .tx_script(tx_script) @@ -307,8 +306,8 @@ async fn test_mint_non_fungible_asset_fails_asset_already_exists() -> anyhow::Re let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin exec.prologue::prepare_transaction @@ -344,19 +343,22 @@ async fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { }; let faucet_id = tx_context.account().id(); + let expected_final_amount = FUNGIBLE_FAUCET_INITIAL_BALANCE - FUNGIBLE_ASSET_AMOUNT; let code = format!( r#" - use.mock::faucet - use.$kernel::asset_vault - use.$kernel::memory - use.$kernel::prologue + use mock::faucet->mock_faucet + use miden::protocol::faucet + use $kernel::asset_vault + use $kernel::memory + use $kernel::prologue begin - # burn asset exec.prologue::prepare_transaction + + # burn asset push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} - call.faucet::burn + call.mock_faucet::burn # assert the correct asset is returned push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} @@ -368,7 +370,12 @@ async fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { push.{suffix} push.{prefix} exec.asset_vault::get_balance - push.{final_input_vault_asset_amount} assert_eq.err="vault balance does not match expected balance" + push.{final_input_vault_asset_amount} + assert_eq.err="vault balance does not match expected balance" + + exec.faucet::get_total_issuance + push.{expected_final_amount} + assert_eq.err="expected total issuance to be {expected_final_amount}" end "#, prefix = faucet_id.prefix().as_felt(), @@ -376,17 +383,8 @@ async fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { final_input_vault_asset_amount = CONSUMED_ASSET_1_AMOUNT - FUNGIBLE_ASSET_AMOUNT, ); - let exec_output = &tx_context.execute_code(&code).await.unwrap(); - - let expected_final_storage_amount = FUNGIBLE_FAUCET_INITIAL_BALANCE - FUNGIBLE_ASSET_AMOUNT; - let faucet_reserved_slot_storage_location = - FAUCET_STORAGE_DATA_SLOT as u32 + NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR; - let faucet_storage_amount_location = faucet_reserved_slot_storage_location + 3; - - let faucet_storage_amount = - exec_output.get_kernel_mem_element(faucet_storage_amount_location).as_int(); + tx_context.execute_code(&code).await?; - assert_eq!(faucet_storage_amount, expected_final_storage_amount); Ok(()) } @@ -397,7 +395,7 @@ async fn burn_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> let code = format!( " - use.mock::faucet + use mock::faucet begin push.{asset} @@ -406,7 +404,7 @@ async fn burn_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> ", asset = Word::from(FungibleAsset::mock(50)) ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(code)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; let result = TransactionContextBuilder::new(account) .tx_script(tx_script) @@ -430,8 +428,8 @@ async fn test_burn_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin exec.prologue::prepare_transaction @@ -461,8 +459,8 @@ async fn test_burn_fungible_asset_insufficient_input_amount() -> anyhow::Result< let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin exec.prologue::prepare_transaction @@ -498,11 +496,13 @@ async fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { let code = format!( r#" - use.$kernel::account - use.$kernel::asset_vault - use.$kernel::memory - use.$kernel::prologue - use.mock::faucet->mock_faucet + use $kernel::account + use $kernel::asset_vault + use $kernel::memory + use $kernel::prologue + use mock::faucet->mock_faucet + + const FAUCET_SYSDATA_SLOT_NAME = word("{faucet_sysdata_slot_name}") begin exec.prologue::prepare_transaction @@ -519,7 +519,7 @@ async fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { # check that the non-fungible asset is in the account map push.{burnt_asset_vault_key} - push.{FAUCET_STORAGE_DATA_SLOT} + push.FAUCET_SYSDATA_SLOT_NAME[0..2] exec.account::get_map_item push.{non_fungible_asset} assert_eqw.err="non-fungible asset should be in the account map" @@ -541,13 +541,14 @@ async fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { # assert that the non-fungible asset is no longer in the account map push.{burnt_asset_vault_key} - push.{FAUCET_STORAGE_DATA_SLOT} + push.FAUCET_SYSDATA_SLOT_NAME[0..2] exec.account::get_map_item padw assert_eqw.err="burnt asset should have been removed from map" dropw end "#, + faucet_sysdata_slot_name = AccountStorage::faucet_sysdata_slot(), non_fungible_asset = Word::from(non_fungible_asset_burnt), burnt_asset_vault_key = burnt_asset_vault_key, ); @@ -566,8 +567,8 @@ async fn test_burn_non_fungible_asset_fails_does_not_exist() -> anyhow::Result<( let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin # burn asset @@ -592,7 +593,7 @@ async fn burn_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result let code = format!( " - use.mock::faucet + use mock::faucet begin push.{asset} @@ -601,7 +602,7 @@ async fn burn_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result ", asset = Word::from(FungibleAsset::mock(50)) ); - let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(code)?; + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; let result = TransactionContextBuilder::new(account) .tx_script(tx_script) @@ -625,8 +626,8 @@ async fn test_burn_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow:: let code = format!( " - use.$kernel::prologue - use.mock::faucet + use $kernel::prologue + use mock::faucet begin # burn asset @@ -660,8 +661,8 @@ async fn test_is_non_fungible_asset_issued_succeeds() -> anyhow::Result<()> { let code = format!( r#" - use.$kernel::prologue - use.miden::faucet + use $kernel::prologue + use miden::protocol::faucet begin exec.prologue::prepare_transaction @@ -702,8 +703,8 @@ async fn test_get_total_issuance_succeeds() -> anyhow::Result<()> { let code = format!( r#" - use.$kernel::prologue - use.miden::faucet + use $kernel::prologue + use miden::protocol::faucet begin exec.prologue::prepare_transaction @@ -731,13 +732,16 @@ async fn test_get_total_issuance_succeeds() -> anyhow::Result<()> { /// This is used to test that calling these procedures fails as expected. fn setup_non_faucet_account() -> anyhow::Result { // Build a custom non-faucet account that (invalidly) exposes faucet procedures. - let faucet_component = AccountComponent::compile( - "export.::miden::faucet::mint - export.::miden::faucet::burn", - TransactionKernel::with_mock_libraries(Arc::new(DefaultSourceManager::default())), - vec![], - )? - .with_supported_type(AccountType::RegularAccountUpdatableCode); + let faucet_code = CodeBuilder::with_mock_libraries_with_source_manager(Arc::new( + DefaultSourceManager::default(), + )) + .compile_component_code( + "test::non_faucet_component", + "pub use ::miden::protocol::faucet::mint + pub use ::miden::protocol::faucet::burn", + )?; + let faucet_component = AccountComponent::new(faucet_code, vec![])? + .with_supported_type(AccountType::RegularAccountUpdatableCode); Ok(AccountBuilder::new([4; 32]) .account_type(AccountType::RegularAccountUpdatableCode) .with_auth_component(NoopAuthComponent) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs index 0cc326fe60..0a865d1098 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs @@ -1,11 +1,11 @@ use anyhow::Context; use assert_matches::assert_matches; -use miden_objects::account::{AccountId, StorageMap, StorageSlot}; -use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset}; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; -use miden_objects::transaction::{ExecutedTransaction, OutputNote}; -use miden_objects::{self, Felt, Word}; +use miden_protocol::account::{AccountId, StorageMap, StorageSlot, StorageSlotName}; +use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; +use miden_protocol::note::NoteType; +use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; +use miden_protocol::transaction::{ExecutedTransaction, OutputNote}; +use miden_protocol::{self, Felt, Word}; use miden_tx::TransactionExecutorError; use winter_rand_utils::rand_value; @@ -132,8 +132,11 @@ async fn mutate_account_with_storage() -> anyhow::Result { let account = builder.add_existing_mock_account_with_storage_and_assets( Auth::IncrNonce, [ - StorageSlot::Value(rand_value()), - StorageSlot::Map(StorageMap::with_entries([(rand_value(), rand_value())])?), + StorageSlot::with_value(StorageSlotName::mock(0), rand_value()), + StorageSlot::with_map( + StorageSlotName::mock(1), + StorageMap::with_entries([(rand_value(), rand_value())])?, + ), ], [Asset::from(native_asset), NonFungibleAsset::mock(&[1, 2, 3, 4])], )?; @@ -161,8 +164,11 @@ async fn create_output_notes() -> anyhow::Result { let account = builder.add_existing_mock_account_with_storage_and_assets( Auth::IncrNonce, [ - StorageSlot::Value(rand_value()), - StorageSlot::Map(StorageMap::with_entries([(rand_value(), rand_value())])?), + StorageSlot::with_map( + StorageSlotName::mock(0), + StorageMap::with_entries([(rand_value(), rand_value())])?, + ), + StorageSlot::with_value(StorageSlotName::mock(1), rand_value()), ], [Asset::from(native_asset), NonFungibleAsset::mock(&[1, 2, 3, 4])], )?; 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 915c1dd01c..addda7b449 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs @@ -2,47 +2,45 @@ use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use miden_lib::errors::tx_kernel_errors::{ +use miden_processor::fast::ExecutionOutput; +use miden_processor::{AdviceInputs, Felt}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountId, + AccountProcedureRoot, + AccountStorage, + AccountStorageMode, + StorageSlot, +}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; +use miden_protocol::errors::tx_kernel::{ ERR_FOREIGN_ACCOUNT_CONTEXT_AGAINST_NATIVE_ACCOUNT, ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT, ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED, }; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::transaction::TransactionKernel; -use miden_lib::transaction::memory::{ +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, +}; +use miden_protocol::testing::storage::STORAGE_LEAVES_2; +use miden_protocol::transaction::memory::{ ACCOUNT_DATA_LENGTH, + ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET, ACCT_CODE_COMMITMENT_OFFSET, ACCT_ID_AND_NONCE_OFFSET, + ACCT_NUM_PROCEDURES_OFFSET, + ACCT_NUM_STORAGE_SLOTS_OFFSET, ACCT_PROCEDURES_SECTION_OFFSET, ACCT_STORAGE_COMMITMENT_OFFSET, - ACCT_STORAGE_SLOTS_SECTION_OFFSET, ACCT_VAULT_ROOT_OFFSET, NATIVE_ACCOUNT_DATA_PTR, - NUM_ACCT_PROCEDURES_OFFSET, - NUM_ACCT_STORAGE_SLOTS_OFFSET, }; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{ - Account, - AccountBuilder, - AccountComponent, - AccountId, - AccountProcedureInfo, - AccountStorage, - AccountStorageMode, - StorageSlot, -}; -use miden_objects::assembly::DefaultSourceManager; -use miden_objects::assembly::diagnostics::NamedSource; -use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; -use miden_objects::testing::account_id::{ - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, - ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, -}; -use miden_objects::testing::storage::STORAGE_LEAVES_2; -use miden_objects::{FieldElement, Word, ZERO}; -use miden_processor::fast::ExecutionOutput; -use miden_processor::{AdviceInputs, Felt}; +use miden_protocol::{FieldElement, Word, ZERO}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockAccountComponent; use miden_tx::LocalTransactionProver; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -59,22 +57,22 @@ use crate::{Auth, MockChainBuilder, assert_execution_error, assert_transaction_e #[tokio::test] async fn test_fpi_memory_single_account() -> anyhow::Result<()> { // Prepare the test data - let storage_slots = - vec![AccountStorage::mock_item_0().slot, AccountStorage::mock_item_2().slot]; + let mock_value_slot0 = AccountStorage::mock_value_slot0(); + let mock_map_slot = AccountStorage::mock_map_slot(); let foreign_account_code_source = " - use.miden::active_account + use miden::protocol::active_account - export.get_item_foreign + pub proc get_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.1 drop exec.active_account::get_item # truncate the stack - movup.6 movup.6 movup.6 drop drop drop + movup.6 movup.6 drop drop end - export.get_map_item_foreign + pub proc get_map_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.2 drop @@ -83,10 +81,10 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { "; let source_manager = Arc::new(DefaultSourceManager::default()); - let foreign_account_component = AccountComponent::compile( - foreign_account_code_source, - TransactionKernel::with_kernel_library(source_manager.clone()), - storage_slots.clone(), + let foreign_account_component = AccountComponent::new( + CodeBuilder::with_source_manager(source_manager.clone()) + .compile_component_code("test::foreign_account", foreign_account_code_source)?, + vec![mock_value_slot0.clone(), mock_map_slot.clone()], )? .with_supports_all_types(); @@ -97,7 +95,7 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { let native_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_item_2().slot])) + .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_map_slot()])) .storage_mode(AccountStorageMode::Public) .build_existing()?; @@ -123,28 +121,31 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { // invocation let code = format!( - " - use.std::sys + r#" + use miden::core::sys + + use $kernel::prologue + use miden::protocol::tx - use.$kernel::prologue - use.miden::tx + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin exec.prologue::prepare_transaction # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of desired storage item + push.MOCK_VALUE_SLOT0[0..2] # get the hash of the `get_item_foreign` procedure of the foreign account push.{get_item_foreign_hash} # push the foreign account ID push.{foreign_suffix} push.{foreign_prefix} - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(11)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure # => [STORAGE_VALUE_1] @@ -152,7 +153,8 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { # truncate the stack exec.sys::truncate_stack end - ", + "#, + mock_value_slot0 = mock_value_slot0.name(), foreign_prefix = foreign_account.id().prefix().as_felt(), foreign_suffix = foreign_account.id().suffix(), get_item_foreign_hash = foreign_account.code().procedures()[1].mast_root(), @@ -162,8 +164,8 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { assert_eq!( exec_output.get_stack_word_be(0), - storage_slots[0].value(), - "Value at the top of the stack (value in the storage at index 0) should be equal [1, 2, 3, 4]", + mock_value_slot0.content().value(), + "Value at the top of the stack should be equal to [1, 2, 3, 4]", ); foreign_account_data_memory_assertions(&foreign_account, &exec_output); @@ -173,31 +175,34 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { // Check the correctness of the memory layout after `get_map_item` account procedure invocation let code = format!( - " - use.std::sys + r#" + use miden::core::sys + + use $kernel::prologue + use miden::protocol::tx - use.$kernel::prologue - use.miden::tx + const MOCK_MAP_SLOT = word("{mock_map_slot}") begin exec.prologue::prepare_transaction # pad the stack for the `execute_foreign_procedure` execution - padw padw push.0.0 - # => [pad(10)] + padw + # => [pad(4)] # push the key of desired storage item push.{map_key} - # push the index of desired storage item - push.1 + # push the slot name of the desired storage item + push.MOCK_MAP_SLOT[0..2] # get the hash of the `get_map_item_foreign` account procedure push.{get_map_item_foreign_hash} # push the foreign account ID push.{foreign_suffix} push.{foreign_prefix} - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, MAP_ITEM_KEY, pad(10)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, MAP_KEY, pad(4)] exec.tx::execute_foreign_procedure # => [MAP_VALUE] @@ -205,14 +210,15 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { # truncate the stack exec.sys::truncate_stack end - ", + "#, + mock_map_slot = mock_map_slot.name(), foreign_prefix = foreign_account.id().prefix().as_felt(), foreign_suffix = foreign_account.id().suffix(), map_key = STORAGE_LEAVES_2[0].0, get_map_item_foreign_hash = foreign_account.code().procedures()[2].mast_root(), ); - let exec_output = tx_context.execute_code(&code).await.unwrap(); + let exec_output = tx_context.execute_code(&code).await?; assert_eq!( exec_output.get_stack_word_be(0), @@ -229,54 +235,59 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { // result in reuse of the loaded account. let code = format!( - " - use.std::sys + r#" + use miden::core::sys - use.$kernel::prologue - use.miden::tx + use $kernel::prologue + use miden::protocol::tx + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin exec.prologue::prepare_transaction ### Get the storage item at index 0 ##################### # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of desired storage item + push.MOCK_VALUE_SLOT0[0..2] # get the hash of the `get_item_foreign` procedure of the foreign account push.{get_item_foreign_hash} # push the foreign account ID push.{foreign_suffix} push.{foreign_prefix} - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure dropw # => [] ### Get the storage item at index 0 again ############### # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of the desired storage item + push.MOCK_VALUE_SLOT0[0..2] # get the hash of the `get_item_foreign` procedure of the foreign account push.{get_item_foreign_hash} # push the foreign account ID push.{foreign_suffix} push.{foreign_prefix} - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure # truncate the stack exec.sys::truncate_stack end - ", + "#, + mock_value_slot0 = mock_value_slot0.name(), foreign_prefix = foreign_account.id().prefix().as_felt(), foreign_suffix = foreign_account.id().suffix(), get_item_foreign_hash = foreign_account.code().procedures()[1].mast_root(), @@ -301,47 +312,47 @@ async fn test_fpi_memory_single_account() -> anyhow::Result<()> { #[tokio::test] async fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { // Prepare the test data - let storage_slots_1 = vec![AccountStorage::mock_item_0().slot]; - let storage_slots_2 = vec![AccountStorage::mock_item_1().slot]; + let mock_value_slot0 = AccountStorage::mock_value_slot0(); + let mock_value_slot1 = AccountStorage::mock_value_slot1(); let foreign_account_code_source_1 = " - use.miden::active_account + use miden::protocol::active_account - export.get_item_foreign_1 + pub proc get_item_foreign_1 # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.1 drop exec.active_account::get_item # truncate the stack - movup.6 movup.6 movup.6 drop drop drop + movup.6 movup.6 drop drop end "; let foreign_account_code_source_2 = " - use.miden::active_account + use miden::protocol::active_account - export.get_item_foreign_2 + pub proc get_item_foreign_2 # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.2 drop exec.active_account::get_item # truncate the stack - movup.6 movup.6 movup.6 drop drop drop + movup.6 movup.6 drop drop end "; - let foreign_account_component_1 = AccountComponent::compile( - foreign_account_code_source_1, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - storage_slots_1.clone(), + let foreign_account_component_1 = AccountComponent::new( + CodeBuilder::default() + .compile_component_code("test::foreign_account_1", foreign_account_code_source_1)?, + vec![mock_value_slot0.clone()], )? .with_supports_all_types(); - let foreign_account_component_2 = AccountComponent::compile( - foreign_account_code_source_2, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - storage_slots_2.clone(), + let foreign_account_component_2 = AccountComponent::new( + CodeBuilder::default() + .compile_component_code("test::foreign_account_2", foreign_account_code_source_2)?, + vec![mock_value_slot1.clone()], )? .with_supports_all_types(); @@ -388,78 +399,84 @@ async fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { // two foreign procedures from the same account should result in reuse of the loaded account. let code = format!( - " - use.std::sys + r#" + use miden::core::sys + + use $kernel::prologue + use miden::protocol::tx - use.$kernel::prologue - use.miden::tx + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") + const MOCK_VALUE_SLOT1 = word("{mock_value_slot1}") begin exec.prologue::prepare_transaction - ### Get the storage item at index 0 from the first account + ### Get the storage item from the first account # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of desired storage item + push.MOCK_VALUE_SLOT0[0..2] - # get the hash of the `get_item_foreign_1` procedure of the foreign account 1 + # get the hash of the `get_item_foreign` procedure of the foreign account push.{get_item_foreign_1_hash} # push the foreign account ID push.{foreign_1_suffix} push.{foreign_1_prefix} - # => [foreign_account_1_id_prefix, foreign_account_1_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_1_id_prefix, foreign_account_1_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure dropw # => [] - ### Get the storage item at index 0 from the second account + ### Get the storage item from the second account # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of desired storage item + push.MOCK_VALUE_SLOT1[0..2] # get the hash of the `get_item_foreign_2` procedure of the foreign account 2 push.{get_item_foreign_2_hash} # push the foreign account ID push.{foreign_2_suffix} push.{foreign_2_prefix} - # => [foreign_account_2_id_prefix, foreign_account_2_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_2_id_prefix, foreign_account_2_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure dropw # => [] - ### Get the storage item at index 0 from the first account again + ### Get the storage item from the first account again # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of desired storage item + push.MOCK_VALUE_SLOT0[0..2] # get the hash of the `get_item_foreign_1` procedure of the foreign account 1 push.{get_item_foreign_1_hash} # push the foreign account ID push.{foreign_1_suffix} push.{foreign_1_prefix} - # => [foreign_account_1_id_prefix, foreign_account_1_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_1_id_prefix, foreign_account_1_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure # truncate the stack exec.sys::truncate_stack end - ", + "#, + mock_value_slot0 = mock_value_slot0.name(), + mock_value_slot1 = mock_value_slot1.name(), get_item_foreign_1_hash = foreign_account_1.code().procedures()[1].mast_root(), get_item_foreign_2_hash = foreign_account_2.code().procedures()[1].mast_root(), - foreign_1_prefix = foreign_account_1.id().prefix().as_felt(), foreign_1_suffix = foreign_account_1.id().suffix(), - foreign_2_prefix = foreign_account_2.id().prefix().as_felt(), foreign_2_suffix = foreign_account_2.id().suffix(), ); @@ -514,22 +531,23 @@ async fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { #[tokio::test] async fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { // Prepare the test data - let storage_slots = - vec![AccountStorage::mock_item_0().slot, AccountStorage::mock_item_2().slot]; + let mock_value_slot0 = AccountStorage::mock_value_slot0(); + let mock_map_slot = AccountStorage::mock_map_slot(); + let foreign_account_code_source = " - use.miden::active_account + use miden::protocol::active_account - export.get_item_foreign + pub proc get_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.1 drop exec.active_account::get_item # truncate the stack - movup.6 movup.6 movup.6 drop drop drop + movup.6 movup.6 drop drop end - export.get_map_item_foreign + pub proc get_map_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.2 drop @@ -538,10 +556,10 @@ async fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { "; let source_manager = Arc::new(DefaultSourceManager::default()); - let foreign_account_component = AccountComponent::compile( - NamedSource::new("foreign_account", foreign_account_code_source), - TransactionKernel::with_kernel_library(source_manager.clone()), - storage_slots, + let foreign_account_component = AccountComponent::new( + CodeBuilder::with_kernel_library(source_manager.clone()) + .compile_component_code("foreign_account", foreign_account_code_source)?, + vec![mock_value_slot0.clone(), mock_map_slot.clone()], )? .with_supports_all_types(); @@ -562,70 +580,78 @@ async fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { mock_chain.prove_next_block()?; let code = format!( - " - use.std::sys + r#" + use miden::core::sys - use.miden::tx + use miden::protocol::tx + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") + const MOCK_MAP_SLOT = word("{mock_map_slot}") begin - # get the storage item at index 0 + # get the storage item + # pad the stack for the `execute_foreign_procedure` execution # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] - # push the index of desired storage item - push.0 + # push the slot name of desired storage item + push.MOCK_VALUE_SLOT0[0..2] # get the hash of the `get_item_foreign` account procedure procref.::foreign_account::get_item_foreign # push the foreign account ID push.{foreign_suffix} push.{foreign_prefix} - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT + # slot_id_prefix, slot_id_suffix, pad(8)]] exec.tx::execute_foreign_procedure # => [STORAGE_VALUE] # assert the correctness of the obtained value - push.1.2.3.4 assert_eqw + push.1.2.3.4 assert_eqw.err="foreign proc returned unexpected value" # => [] - # get the storage map at index 1 + # get an item from the storage map # pad the stack for the `execute_foreign_procedure` execution - padw padw push.0.0 - # => [pad(10)] + padw + # => [pad(4)] # push the key of desired storage item push.{map_key} - # push the index of desired storage item - push.1 + # push the slot name of the desired storage map + push.MOCK_MAP_SLOT[0..2] # get the hash of the `get_map_item_foreign` account procedure procref.::foreign_account::get_map_item_foreign # push the foreign account ID push.{foreign_suffix} push.{foreign_prefix} - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, MAP_ITEM_KEY, pad(10)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, MAP_ITEM_KEY, pad(4)] exec.tx::execute_foreign_procedure # => [MAP_VALUE] # assert the correctness of the obtained value - push.1.2.3.4 assert_eqw + push.1.2.3.4 assert_eqw.err="foreign proc returned unexpected value" # => [] # truncate the stack exec.sys::truncate_stack end - ", + "#, + mock_value_slot0 = mock_value_slot0.name(), + mock_map_slot = mock_map_slot.name(), foreign_prefix = foreign_account.id().prefix().as_felt(), foreign_suffix = foreign_account.id().suffix(), map_key = STORAGE_LEAVES_2[0].0, ); - let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) - .with_dynamically_linked_library(foreign_account_component.library())? + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(foreign_account_component.component_code())? .compile_tx_script(code)?; let foreign_account_inputs = mock_chain @@ -660,9 +686,9 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu let foreign_account_code_source = format!( " - use.miden::active_account + use miden::protocol::active_account - export.get_asset_balance + pub proc get_asset_balance # get balance of first asset push.{fungible_faucet_id_suffix} push.{fungible_faucet_id_prefix} exec.active_account::get_balance @@ -688,9 +714,9 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu ); let source_manager = Arc::new(DefaultSourceManager::default()); - let foreign_account_component = AccountComponent::compile( - NamedSource::new("foreign_account_code", foreign_account_code_source), - TransactionKernel::assembler_with_source_manager(source_manager.clone()), + let foreign_account_component = AccountComponent::new( + CodeBuilder::with_source_manager(source_manager.clone()) + .compile_component_code("foreign_account_code", foreign_account_code_source)?, vec![], )? .with_supports_all_types(); @@ -714,9 +740,9 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu let code = format!( " - use.std::sys + use miden::core::sys - use.miden::tx + use miden::protocol::tx begin # Get the added balance of two assets from foreign account @@ -746,8 +772,8 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu foreign_suffix = foreign_account.id().suffix(), ); - let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) - .with_dynamically_linked_library(foreign_account_component.library())? + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(foreign_account_component.component_code())? .compile_tx_script(code)?; let foreign_account_inputs = mock_chain.get_foreign_account_inputs(foreign_account.id())?; @@ -773,9 +799,9 @@ async fn foreign_account_get_initial_balance() -> anyhow::Result<()> { let foreign_account_code_source = format!( " - use.miden::active_account + use miden::protocol::active_account - export.get_initial_balance + pub proc get_initial_balance # push the faucet ID on the stack push.{fungible_faucet_id_suffix} push.{fungible_faucet_id_prefix} @@ -793,9 +819,9 @@ async fn foreign_account_get_initial_balance() -> anyhow::Result<()> { ); let source_manager = Arc::new(DefaultSourceManager::default()); - let foreign_account_component = AccountComponent::compile( - NamedSource::new("foreign_account_code", foreign_account_code_source), - TransactionKernel::assembler_with_source_manager(source_manager.clone()), + let foreign_account_component = AccountComponent::new( + CodeBuilder::with_source_manager(source_manager.clone()) + .compile_component_code("foreign_account_code", foreign_account_code_source)?, vec![], )? .with_supports_all_types(); @@ -819,9 +845,9 @@ async fn foreign_account_get_initial_balance() -> anyhow::Result<()> { let code = format!( " - use.std::sys + use miden::core::sys - use.miden::tx + use miden::protocol::tx begin # Get the initial balance of the fungible asset from the foreign account @@ -852,8 +878,8 @@ async fn foreign_account_get_initial_balance() -> anyhow::Result<()> { foreign_suffix = foreign_account.id().suffix(), ); - let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) - .with_dynamically_linked_library(foreign_account_component.library())? + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(foreign_account_component.component_code())? .compile_tx_script(code)?; let foreign_account_inputs = mock_chain.get_foreign_account_inputs(foreign_account.id())?; @@ -883,28 +909,35 @@ async fn foreign_account_get_initial_balance() -> anyhow::Result<()> { #[tokio::test] async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { // ------ SECOND FOREIGN ACCOUNT --------------------------------------------------------------- - let storage_slots = vec![AccountStorage::mock_item_0().slot]; - let second_foreign_account_code_source = r#" - use.miden::tx - use.miden::active_account + let mock_value_slot0 = AccountStorage::mock_value_slot0(); + let mock_value_slot1 = AccountStorage::mock_value_slot1(); + + let second_foreign_account_code_source = format!( + r#" + use miden::protocol::tx + use miden::protocol::active_account + + use miden::core::sys - use.std::sys + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") + const MOCK_VALUE_SLOT1 = word("{mock_value_slot1}") - export.second_account_foreign_proc - # get the storage item at index 1 + pub proc second_account_foreign_proc + # get the storage item at value1 # pad the stack for the `execute_foreign_procedure` execution - padw padw padw push.0.0 - # => [pad(14)] + padw padw + # => [pad(8)] # push the index of desired storage item - push.1 + push.MOCK_VALUE_SLOT1[0..2] # get the hash of the `get_item_foreign` account procedure from the advice stack adv_push.4 # push the foreign account ID from the advice stack adv_push.2 - # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, + # slot_id_prefix, slot_id_suffix, pad(8)] exec.tx::execute_foreign_procedure # => [storage_value] @@ -912,9 +945,10 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { # make sure that the resulting value equals 5 dup push.5 assert_eq.err="value should have been 5" - # get the first element of the 0'th storage slot (it should be 1) and add it to the + # get the first element of the value0 storage slot (it should be 1) and add it to the # obtained foreign value. - push.0 exec.active_account::get_item drop drop drop + push.MOCK_VALUE_SLOT0[0..2] exec.active_account::get_item + drop drop drop add # assert that the resulting value equals 6 @@ -922,13 +956,18 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { exec.sys::truncate_stack end - "#; + "#, + mock_value_slot0 = mock_value_slot0.name(), + mock_value_slot1 = mock_value_slot1.name(), + ); let source_manager = Arc::new(DefaultSourceManager::default()); - let second_foreign_account_component = AccountComponent::compile( - second_foreign_account_code_source, - TransactionKernel::with_kernel_library(source_manager.clone()), - storage_slots, + let second_foreign_account_component = AccountComponent::new( + CodeBuilder::with_kernel_library(source_manager.clone()).compile_component_code( + "test::second_foreign_account", + second_foreign_account_code_source, + )?, + vec![mock_value_slot0.clone()], )? .with_supports_all_types(); @@ -938,15 +977,16 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { .build_existing()?; // ------ FIRST FOREIGN ACCOUNT --------------------------------------------------------------- - let storage_slots = - vec![AccountStorage::mock_item_0().slot, AccountStorage::mock_item_1().slot]; - let first_foreign_account_code_source = r#" - use.miden::tx - use.miden::active_account + let first_foreign_account_code_source = format!( + r#" + use miden::protocol::tx + use miden::protocol::active_account + + use miden::core::sys - use.std::sys + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") - export.first_account_foreign_proc + pub proc first_account_foreign_proc # pad the stack for the `execute_foreign_procedure` execution padw padw padw push.0.0.0 # => [pad(15)] @@ -961,9 +1001,10 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { exec.tx::execute_foreign_procedure # => [storage_value] - # get the second element of the 0'th storage slot (it should be 2) and add it to the + # get the second element of the value0 storage slot (it should be 2) and add it to the # obtained foreign value. - push.0 exec.active_account::get_item drop drop swap drop + push.MOCK_VALUE_SLOT0[0..2] exec.active_account::get_item + drop drop swap drop add # assert that the resulting value equals 8 @@ -972,7 +1013,7 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { exec.sys::truncate_stack end - export.get_item_foreign + pub proc get_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure of the # foreign account, not the native one push.1 drop @@ -981,12 +1022,14 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { # return the first element of the resulting word drop drop drop end - "#; + "#, + mock_value_slot0 = mock_value_slot0.name(), + ); - let first_foreign_account_component = AccountComponent::compile( - NamedSource::new("first_foreign_account", first_foreign_account_code_source), - TransactionKernel::with_kernel_library(source_manager.clone()), - storage_slots, + let first_foreign_account_component = AccountComponent::new( + CodeBuilder::with_kernel_library(source_manager.clone()) + .compile_component_code("first_foreign_account", first_foreign_account_code_source)?, + vec![mock_value_slot0.clone(), mock_value_slot1.clone()], )? .with_supports_all_types(); @@ -1039,10 +1082,8 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { let code = format!( r#" - use.std::sys - - use.miden::tx - use.miden::account + use miden::core::sys + use miden::protocol::tx begin # pad the stack for the `execute_foreign_procedure` execution @@ -1073,11 +1114,11 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { foreign_suffix = first_foreign_account.id().suffix(), ); - let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) - .with_dynamically_linked_library(first_foreign_account_component.library())? + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(first_foreign_account_component.component_code())? .compile_tx_script(code)?; - let executed_transaction = mock_chain + mock_chain .build_tx_context(native_account.id(), &[], &[]) .expect("failed to build tx context") .foreign_accounts(foreign_account_inputs) @@ -1088,7 +1129,161 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { .execute() .await?; - // TODO: Remove later and add a integration test using FPI. + Ok(()) +} + +/// Proves a transaction that uses FPI with two foreign accounts. +/// +/// Call chain: +/// `Native -> First FA -> Second FA` +/// +/// Each foreign account has unique code. The first foreign account calls a procedure on the second +/// foreign account via FPI. We then prove the executed transaction to ensure code for multiple +/// foreign accounts is correctly loaded into the prover host's MAST store. +#[tokio::test] +async fn test_prove_fpi_two_foreign_accounts_chain() -> anyhow::Result<()> { + // ------ SECOND FOREIGN ACCOUNT --------------------------------------------------------------- + // unique procedure which just leaves a constant on the stack + let second_foreign_account_code_source = r#" + use miden::core::sys + + pub proc second_account_foreign_proc + # leave a constant result on the stack + push.3 + + # truncate any padding + exec.sys::truncate_stack + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let second_foreign_account_component = AccountComponent::new( + CodeBuilder::with_kernel_library(source_manager.clone()) + .compile_component_code("foreign_account", second_foreign_account_code_source)?, + vec![], + )? + .with_supports_all_types(); + + let second_foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(second_foreign_account_component.clone()) + .build_existing()?; + + // ------ FIRST FOREIGN ACCOUNT --------------------------------------------------------------- + // unique procedure which calls the second foreign account via FPI and then returns + let first_foreign_account_code_source = format!( + r#" + use miden::protocol::tx + use miden::core::sys + + pub proc first_account_foreign_proc + # pad the stack for the `execute_foreign_procedure` execution + padw padw padw push.0.0.0 + # => [pad(15)] + + # get the hash of the `second_account_foreign_proc` using procref + procref.::foreign_account::second_account_foreign_proc + + # push the ID of the second foreign account + push.{second_foreign_suffix} push.{second_foreign_prefix} + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, pad(15)] + + # call the second foreign account + exec.tx::execute_foreign_procedure + # => [result_from_second] + + # keep the result and drop any padding if present + exec.sys::truncate_stack + end + "#, + second_foreign_prefix = second_foreign_account.id().prefix().as_felt(), + second_foreign_suffix = second_foreign_account.id().suffix(), + ); + + // Link against the second foreign account. + let first_foreign_account_code = CodeBuilder::with_kernel_library(source_manager.clone()) + .with_dynamically_linked_library(second_foreign_account_component.component_code())? + .compile_component_code("first_foreign_account", first_foreign_account_code_source)?; + let first_foreign_account_component = + AccountComponent::new(first_foreign_account_code, vec![])?.with_supports_all_types(); + + let first_foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(first_foreign_account_component.clone()) + .build_existing()?; + + // ------ NATIVE ACCOUNT --------------------------------------------------------------- + let native_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(MockAccountComponent::with_empty_slots()) + .storage_mode(AccountStorageMode::Public) + .build_existing()?; + + let mut mock_chain = MockChainBuilder::with_accounts([ + native_account.clone(), + first_foreign_account.clone(), + second_foreign_account.clone(), + ])? + .build()?; + mock_chain.prove_next_block()?; + + let foreign_account_inputs = vec![ + mock_chain + .get_foreign_account_inputs(first_foreign_account.id()) + .expect("failed to get foreign account inputs"), + mock_chain + .get_foreign_account_inputs(second_foreign_account.id()) + .expect("failed to get foreign account inputs"), + ]; + + // ------ TRANSACTION SCRIPT (Native) ---------------------------------------------------------- + // Call the first foreign account's procedure. It will call into the second FA via FPI. + let code = format!( + r#" + use miden::core::sys + use miden::protocol::tx + + begin + # pad the stack for the `execute_foreign_procedure` execution + padw padw padw push.0.0.0 + # => [pad(15)] + + # get the hash of the `first_account_foreign_proc` procedure + procref.::first_foreign_account::first_account_foreign_proc + + # push the first foreign account ID + push.{foreign_suffix} push.{foreign_prefix} + # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, pad(15)] + + exec.tx::execute_foreign_procedure + # => [result_from_second] + + # assert the result returned from the second FA is 3 + dup push.3 assert_eq.err="result from second foreign account should be 3" + + # truncate any remaining stack items + exec.sys::truncate_stack + end + "#, + foreign_prefix = first_foreign_account.id().prefix().as_felt(), + foreign_suffix = first_foreign_account.id().suffix(), + ); + + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(first_foreign_account_component.component_code())? + .compile_tx_script(code)?; + + let executed_transaction = mock_chain + .build_tx_context(native_account.id(), &[], &[]) + .expect("failed to build tx context") + .foreign_accounts(foreign_account_inputs) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; + + // Prove the executed transaction which uses FPI across two foreign accounts. LocalTransactionProver::default().prove(executed_transaction)?; Ok(()) @@ -1101,17 +1296,21 @@ async fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { #[tokio::test] async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { let mut foreign_accounts = Vec::new(); + let mock_value_slot0 = AccountStorage::mock_value_slot0(); - let last_foreign_account_code_source = " - use.miden::active_account + let last_foreign_account_code_source = format!( + r#" + use miden::protocol::active_account - export.get_item_foreign + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") + + pub proc get_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure # of the foreign account, not the native one push.1 drop # push the index of desired storage item - push.0 + push.MOCK_VALUE_SLOT0[0..2] exec.active_account::get_item @@ -1119,18 +1318,19 @@ async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { drop drop drop # make sure that the resulting value equals 1 - assert + assert.err="expected value to be 1" end - "; + "#, + mock_value_slot0 = mock_value_slot0.name(), + ); - let storage_slots = vec![AccountStorage::mock_item_0().slot]; - let last_foreign_account_component = AccountComponent::compile( - last_foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - storage_slots, - ) - .unwrap() - .with_supports_all_types(); + let last_foreign_account_code = CodeBuilder::default() + .compile_component_code("test::last_foreign_account", last_foreign_account_code_source) + .unwrap(); + let last_foreign_account_component = + AccountComponent::new(last_foreign_account_code, vec![mock_value_slot0.clone()]) + .unwrap() + .with_supports_all_types(); let last_foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) @@ -1145,10 +1345,10 @@ async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { let foreign_account_code_source = format!( " - use.miden::tx - use.std::sys + use miden::protocol::tx + use miden::core::sys - export.read_first_foreign_storage_slot_{foreign_account_index} + pub proc read_first_foreign_storage_slot_{foreign_account_index} # pad the stack for the `execute_foreign_procedure` execution padw padw padw push.0.0.0 # => [pad(15)] @@ -1171,13 +1371,15 @@ async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { next_foreign_prefix = next_account.id().prefix().as_felt(), ); - let foreign_account_component = AccountComponent::compile( - foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - vec![], - ) - .unwrap() - .with_supports_all_types(); + let foreign_account_code = CodeBuilder::default() + .compile_component_code( + format!("test::foreign_account_chain_{foreign_account_index}"), + foreign_account_code_source, + ) + .unwrap(); + let foreign_account_component = AccountComponent::new(foreign_account_code, vec![]) + .unwrap() + .with_supports_all_types(); let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) @@ -1216,9 +1418,9 @@ async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { let code = format!( " - use.std::sys + use miden::core::sys - use.miden::tx + use miden::protocol::tx begin # pad the stack for the `execute_foreign_procedure` execution @@ -1244,7 +1446,7 @@ async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { foreign_suffix = foreign_accounts.last().unwrap().0.id().suffix(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code).unwrap(); + let tx_script = CodeBuilder::default().compile_tx_script(code).unwrap(); let tx_context = mock_chain .build_tx_context(native_account.id(), &[], &[])? @@ -1263,11 +1465,11 @@ async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { async fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { // ------ FIRST FOREIGN ACCOUNT --------------------------------------------------------------- let foreign_account_code_source = " - use.miden::tx + use miden::protocol::tx - use.std::sys + use miden::core::sys - export.first_account_foreign_proc + pub proc first_account_foreign_proc # pad the stack for the `execute_foreign_procedure` execution padw padw padw push.0.0.0 # => [pad(15)] @@ -1286,9 +1488,9 @@ async fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { end "; - let foreign_account_component = AccountComponent::compile( - NamedSource::new("foreign_account", foreign_account_code_source), - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + let foreign_account_component = AccountComponent::new( + CodeBuilder::default() + .compile_component_code("foreign_account", foreign_account_code_source)?, vec![], )? .with_supports_all_types(); @@ -1312,9 +1514,9 @@ async fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { let code = format!( " - use.std::sys + use miden::core::sys - use.miden::tx + use miden::protocol::tx begin # pad the stack for the `execute_foreign_procedure` execution @@ -1339,8 +1541,8 @@ async fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { first_account_foreign_proc_hash = foreign_account.code().procedures()[1].mast_root(), ); - let tx_script = ScriptBuilder::default() - .with_dynamically_linked_library(foreign_account_component.library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(foreign_account_component.component_code())? .compile_tx_script(code)?; let foreign_account_inputs = mock_chain @@ -1375,19 +1577,20 @@ async fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { async fn test_fpi_stale_account() -> anyhow::Result<()> { // Prepare the test data let foreign_account_code_source = " - use.miden::native_account + use miden::protocol::native_account # code is not used in this test - export.set_some_item_foreign + pub proc set_some_item_foreign push.34.1 exec.native_account::set_item end "; - let foreign_account_component = AccountComponent::compile( - foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), - vec![AccountStorage::mock_item_0().slot], + let mock_value_slot0 = AccountStorage::mock_value_slot0(); + let foreign_account_component = AccountComponent::new( + CodeBuilder::default() + .compile_component_code("foreign_account_invalid", foreign_account_code_source)?, + vec![mock_value_slot0.clone()], )? .with_supports_all_types(); @@ -1398,7 +1601,7 @@ async fn test_fpi_stale_account() -> anyhow::Result<()> { let native_account = AccountBuilder::new([4; 32]) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_item_2().slot])) + .with_component(MockAccountComponent::with_slots(vec![AccountStorage::mock_map_slot()])) .build_existing()?; let mut mock_chain = @@ -1411,9 +1614,10 @@ async fn test_fpi_stale_account() -> anyhow::Result<()> { // Modify the account's storage to change its storage commitment and in turn the account // commitment. - foreign_account - .storage_mut() - .set_item(0, Word::from([Felt::ONE, Felt::ONE, Felt::ONE, Felt::ONE]))?; + foreign_account.storage_mut().set_item( + mock_value_slot0.name(), + Word::from([Felt::ONE, Felt::ONE, Felt::ONE, Felt::ONE]), + )?; // We pass the modified foreign account with a witness that is valid against the ref block. This // means the foreign account's commitment does not match the commitment that the account witness @@ -1435,10 +1639,10 @@ async fn test_fpi_stale_account() -> anyhow::Result<()> { let code = format!( " - use.std::sys + use miden::core::sys - use.$kernel::prologue - use.miden::tx + use $kernel::prologue + use miden::protocol::tx begin exec.prologue::prepare_transaction @@ -1473,10 +1677,10 @@ async fn test_fpi_stale_account() -> anyhow::Result<()> { #[tokio::test] async fn test_fpi_get_account_id() -> anyhow::Result<()> { let foreign_account_code_source = " - use.miden::active_account - use.miden::native_account + use miden::protocol::active_account + use miden::protocol::native_account - export.get_current_and_native_ids + pub proc get_current_and_native_ids # get the ID of the current (foreign) account exec.active_account::get_id # => [acct_id_prefix, acct_id_suffix, pad(16)] @@ -1491,9 +1695,9 @@ async fn test_fpi_get_account_id() -> anyhow::Result<()> { end "; - let foreign_account_component = AccountComponent::compile( - NamedSource::new("foreign_account", foreign_account_code_source), - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + let foreign_account_component = AccountComponent::new( + CodeBuilder::default() + .compile_component_code("foreign_account", foreign_account_code_source)?, Vec::new(), )? .with_supports_all_types(); @@ -1516,10 +1720,10 @@ async fn test_fpi_get_account_id() -> anyhow::Result<()> { let code = format!( r#" - use.std::sys + use miden::core::sys - use.miden::tx - use.miden::account_id + use miden::protocol::tx + use miden::protocol::account_id begin # get the IDs of the foreign and native accounts @@ -1561,8 +1765,8 @@ async fn test_fpi_get_account_id() -> anyhow::Result<()> { expected_native_prefix = native_account.id().prefix().as_felt(), ); - let tx_script = ScriptBuilder::default() - .with_dynamically_linked_library(foreign_account_component.library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(foreign_account_component.component_code())? .compile_tx_script(code)?; let foreign_account_inputs = mock_chain @@ -1607,7 +1811,7 @@ fn foreign_account_data_memory_assertions( assert_eq!( exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_STORAGE_COMMITMENT_OFFSET), - foreign_account.storage().commitment(), + foreign_account.storage().to_commitment(), ); assert_eq!( @@ -1616,33 +1820,35 @@ fn foreign_account_data_memory_assertions( ); assert_eq!( - exec_output.get_kernel_mem_word(foreign_account_data_ptr + NUM_ACCT_STORAGE_SLOTS_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_NUM_STORAGE_SLOTS_OFFSET), Word::from([u16::try_from(foreign_account.storage().slots().len()).unwrap(), 0, 0, 0]), ); for (i, elements) in foreign_account .storage() - .as_elements() - .chunks(StorageSlot::NUM_ELEMENTS_PER_STORAGE_SLOT / 2) + .to_elements() + .chunks(StorageSlot::NUM_ELEMENTS / 2) .enumerate() { assert_eq!( exec_output.get_kernel_mem_word( - foreign_account_data_ptr + ACCT_STORAGE_SLOTS_SECTION_OFFSET + (i as u32) * 4 + foreign_account_data_ptr + + ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET + + (i as u32) * 4 ), Word::try_from(elements).unwrap(), ) } assert_eq!( - exec_output.get_kernel_mem_word(foreign_account_data_ptr + NUM_ACCT_PROCEDURES_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_NUM_PROCEDURES_OFFSET), Word::from([u16::try_from(foreign_account.code().num_procedures()).unwrap(), 0, 0, 0]), ); for (i, elements) in foreign_account .code() .as_elements() - .chunks(AccountProcedureInfo::NUM_ELEMENTS_PER_PROC / 2) + .chunks(AccountProcedureRoot::NUM_ELEMENTS) .enumerate() { assert_eq!( @@ -1665,29 +1871,36 @@ async fn test_get_initial_item_and_get_initial_map_item_with_foreign_account() - .storage_mode(AccountStorageMode::Public) .build_existing()?; + let mock_value_slot0 = AccountStorage::mock_value_slot0(); + let mock_map_slot = AccountStorage::mock_map_slot(); let (map_key, map_value) = STORAGE_LEAVES_2[0]; // Create foreign procedures that test get_initial_item and get_initial_map_item - let foreign_account_code_source = " - use.miden::active_account - use.std::sys + let foreign_account_code_source = format!( + r#" + use miden::protocol::active_account + use miden::core::sys + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") - export.test_get_initial_item - push.0 + pub proc test_get_initial_item + push.MOCK_VALUE_SLOT0[0..2] exec.active_account::get_initial_item exec.sys::truncate_stack end - export.test_get_initial_map_item + pub proc test_get_initial_map_item exec.active_account::get_initial_map_item exec.sys::truncate_stack end - "; + "#, + mock_value_slot0 = mock_value_slot0.name() + ); - let foreign_account_component = AccountComponent::compile( - NamedSource::new("foreign_account", foreign_account_code_source), - TransactionKernel::assembler(), - vec![AccountStorage::mock_item_0().slot, AccountStorage::mock_item_2().slot], + let foreign_account_component = AccountComponent::new( + CodeBuilder::default() + .compile_component_code("foreign_account", foreign_account_code_source)?, + vec![mock_value_slot0.clone(), mock_map_slot.clone()], )? .with_supports_all_types(); @@ -1705,43 +1918,45 @@ async fn test_get_initial_item_and_get_initial_map_item_with_foreign_account() - let foreign_account_inputs = mock_chain.get_foreign_account_inputs(foreign_account.id())?; let code = format!( - " - use.std::sys - use.miden::tx + r#" + use miden::core::sys + use miden::protocol::tx - begin + const MOCK_MAP_SLOT = word("{mock_map_slot}") + begin # Test get_initial_item on foreign account padw padw padw push.0.0.0 - # => [ pad(4), pad(4), pad(4), 0, 0, 0 ] + # => [pad(15)] procref.::foreign_account::test_get_initial_item push.{foreign_account_id_suffix} push.{foreign_account_id_prefix} exec.tx::execute_foreign_procedure push.{expected_value_slot_0} - assert_eqw.err=\"foreign account get_initial_item should work\" + assert_eqw.err="foreign account get_initial_item should work" # Test get_initial_map_item on foreign account padw padw push.0.0 push.{map_key} - push.1 + push.MOCK_MAP_SLOT[0..2] procref.::foreign_account::test_get_initial_map_item push.{foreign_account_id_suffix} push.{foreign_account_id_prefix} exec.tx::execute_foreign_procedure push.{map_value} - assert_eqw.err=\"foreign account get_initial_map_item should work\" + assert_eqw.err="foreign account get_initial_map_item should work" exec.sys::truncate_stack end - ", + "#, + mock_map_slot = mock_map_slot.name(), foreign_account_id_prefix = foreign_account.id().prefix().as_felt(), foreign_account_id_suffix = foreign_account.id().suffix(), - expected_value_slot_0 = &AccountStorage::mock_item_0().slot.value(), + expected_value_slot_0 = mock_value_slot0.content().value(), map_key = &map_key, map_value = &map_value, ); - let tx_script = ScriptBuilder::with_mock_libraries()? - .with_dynamically_linked_library(foreign_account_component.library())? + let tx_script = CodeBuilder::with_mock_libraries() + .with_dynamically_linked_library(foreign_account_component.component_code())? .compile_tx_script(code)?; mock_chain diff --git a/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs index b903b3d6b0..84a9d9322a 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs @@ -1,8 +1,8 @@ use alloc::string::String; -use miden_lib::utils::ScriptBuilder; -use miden_objects::Word; -use miden_objects::note::Note; +use miden_protocol::Word; +use miden_protocol::note::Note; +use miden_standards::code_builder::CodeBuilder; use super::{TestSetup, setup_test}; use crate::TxContextInput; @@ -47,7 +47,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { let code = format!( " - use.miden::input_note + use miden::protocol::input_note begin {check_note_0} @@ -74,7 +74,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { ), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context( @@ -104,7 +104,7 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { let code = format!( r#" - use.miden::input_note + use miden::protocol::input_note begin # get the recipient from the input note @@ -120,19 +120,23 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { # get the metadata from the requested input note push.0 exec.input_note::get_metadata - # => [METADATA] + # => [NOTE_ATTACHMENT, METADATA_HEADER] - # assert the correctness of the metadata - push.{METADATA} - assert_eqw.err="note 0 has incorrect metadata" + push.{NOTE_ATTACHMENT} + assert_eqw.err="note 0 has incorrect note attachment" + # => [METADATA_HEADER] + + push.{METADATA_HEADER} + assert_eqw.err="note 0 has incorrect metadata header" # => [] end "#, RECIPIENT = p2id_note_1_asset.recipient().digest(), - METADATA = Word::from(p2id_note_1_asset.metadata()), + METADATA_HEADER = p2id_note_1_asset.metadata().to_header_word(), + NOTE_ATTACHMENT = p2id_note_1_asset.metadata().to_attachment_word(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? @@ -158,7 +162,7 @@ async fn test_get_sender() -> anyhow::Result<()> { let code = format!( r#" - use.miden::input_note + use miden::protocol::input_note begin # get the sender from the input note @@ -181,7 +185,7 @@ async fn test_get_sender() -> anyhow::Result<()> { sender_suffix = p2id_note_1_asset.metadata().sender().suffix(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? @@ -257,7 +261,7 @@ async fn test_get_assets() -> anyhow::Result<()> { let code = format!( " - use.miden::input_note + use miden::protocol::input_note begin {check_note_0} @@ -272,7 +276,7 @@ async fn test_get_assets() -> anyhow::Result<()> { check_note_2 = check_assets_code(2, 8, &p2id_note_2_assets), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context( @@ -302,7 +306,7 @@ async fn test_get_inputs_info() -> anyhow::Result<()> { let code = format!( r#" - use.miden::input_note + use miden::protocol::input_note begin # get the inputs commitment and length from the input note with index 0 (the only one @@ -326,7 +330,7 @@ async fn test_get_inputs_info() -> anyhow::Result<()> { inputs_num = p2id_note_1_asset.inputs().num_values(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? @@ -352,7 +356,7 @@ async fn test_get_script_root() -> anyhow::Result<()> { let code = format!( r#" - use.miden::input_note + use miden::protocol::input_note begin # get the script root from the input note with index 0 (the only one we have) @@ -369,7 +373,7 @@ async fn test_get_script_root() -> anyhow::Result<()> { SCRIPT_ROOT = p2id_note_1_asset.script().root(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? @@ -395,7 +399,7 @@ async fn test_get_serial_number() -> anyhow::Result<()> { let code = format!( r#" - use.miden::input_note + use miden::protocol::input_note begin # get the serial number from the input note with index 0 (the only one we have) @@ -412,7 +416,7 @@ async fn test_get_serial_number() -> anyhow::Result<()> { SERIAL_NUMBER = p2id_note_1_asset.serial_num(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = CodeBuilder::default().compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? diff --git a/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs b/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs index 83829fcd5e..cf0216fa6d 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs @@ -2,17 +2,18 @@ //! //! Once lazy loading is enabled generally, it can be removed and/or integrated into other tests. -use miden_lib::testing::note::NoteBuilder; -use miden_lib::utils::ScriptBuilder; -use miden_objects::LexicographicWord; -use miden_objects::account::{AccountId, AccountStorage}; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::testing::account_id::{ +use miden_protocol::LexicographicWord; +use miden_protocol::account::{AccountId, AccountStorage, StorageSlotDelta}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_NATIVE_ASSET_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, }; -use miden_objects::testing::constants::FUNGIBLE_ASSET_AMOUNT; +use miden_protocol::testing::constants::FUNGIBLE_ASSET_AMOUNT; +use miden_protocol::testing::storage::MOCK_MAP_SLOT; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::note::NoteBuilder; use super::Word; use crate::{Auth, MockChain, TransactionContextBuilder}; @@ -39,7 +40,7 @@ async fn adding_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result<( let code = format!( " - use.mock::account + use mock::account begin push.{FUNGIBLE_ASSET1} @@ -53,7 +54,7 @@ async fn adding_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result<( FUNGIBLE_ASSET2 = Word::from(fungible_asset2) ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; let tx_context = TransactionContextBuilder::with_existing_mock_account() @@ -86,8 +87,8 @@ async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result let code = format!( " - use.mock::account - use.mock::util + use mock::account + use mock::util begin push.{FUNGIBLE_ASSET1} @@ -95,7 +96,7 @@ async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result # => [] # move asset to note to adhere to asset preservation rules - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] push.{FUNGIBLE_ASSET2} @@ -103,7 +104,7 @@ async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result # => [ASSET] # move asset to note to adhere to asset preservation rules - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] end ", @@ -111,7 +112,7 @@ async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result FUNGIBLE_ASSET2 = Word::from(fungible_asset2) ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; @@ -176,37 +177,39 @@ async fn setting_map_item_with_lazy_loading_succeeds() -> anyhow::Result<()> { "test setup requires that the non existent key does not exist" ); - // The index of the mock map in account storage is 2. - let map_index = 2; + // The slot name of the mock map in account storage. + let mock_map_slot = &*MOCK_MAP_SLOT; let value0 = Word::from([3, 4, 5, 6u32]); let value1 = Word::from([9, 8, 7, 6u32]); let code = format!( - " - use.mock::account + r#" + use mock::account + + const MOCK_MAP_SLOT = word("{mock_map_slot}") begin # Update an existing key. push.{value0} push.{existing_key} - push.{map_index} - # => [index, KEY, VALUE] + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] call.account::set_map_item # Insert a non-existent key. push.{value1} push.{non_existent_key} - push.{map_index} - # => [index, KEY, VALUE] + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] call.account::set_map_item - exec.::std::sys::truncate_stack + exec.::miden::core::sys::truncate_stack end - " + "# ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; @@ -217,7 +220,13 @@ async fn setting_map_item_with_lazy_loading_succeeds() -> anyhow::Result<()> { .execute() .await?; - let map_delta = tx.account_delta().storage().maps().get(&map_index).unwrap(); + let map_delta = tx + .account_delta() + .storage() + .get(mock_map_slot) + .cloned() + .map(StorageSlotDelta::unwrap_map) + .unwrap(); assert_eq!(map_delta.entries().get(&LexicographicWord::new(existing_key)).unwrap(), &value0); assert_eq!( map_delta.entries().get(&LexicographicWord::new(non_existent_key)).unwrap(), @@ -240,16 +249,20 @@ async fn getting_map_item_with_lazy_loading_succeeds() -> anyhow::Result<()> { "test setup requires that the non existent key does not exist" ); + let mock_map_slot = &*MOCK_MAP_SLOT; + let code = format!( r#" - use.std::word - use.mock::account + use miden::core::word + use mock::account + + const MOCK_MAP_SLOT = word("{mock_map_slot}") begin # Fetch value from existing key. push.{existing_key} - push.2 - # => [index, KEY] + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY] call.account::get_map_item push.{existing_value} @@ -257,18 +270,18 @@ async fn getting_map_item_with_lazy_loading_succeeds() -> anyhow::Result<()> { # Fetch a non-existent key. push.{non_existent_key} - push.2 - # => [index, KEY] + push.MOCK_MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY] call.account::get_map_item padw assert_eqw.err="non-existent value should be the empty word" - exec.::std::sys::truncate_stack + exec.::miden::core::sys::truncate_stack end "# ); - let builder = ScriptBuilder::with_mock_libraries()?; + let builder = CodeBuilder::with_mock_libraries(); let source_manager = builder.source_manager(); let tx_script = builder.compile_tx_script(code)?; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_link_map.rs b/crates/miden-testing/src/kernel_tests/tx/test_link_map.rs index 69979d73d3..fca31cf22a 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_link_map.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_link_map.rs @@ -3,8 +3,8 @@ use std::collections::BTreeMap; use std::string::String; use anyhow::Context; -use miden_objects::{EMPTY_WORD, LexicographicWord, Word}; use miden_processor::{ONE, ZERO}; +use miden_protocol::{EMPTY_WORD, LexicographicWord, Word}; use miden_tx::{LinkMap, MemoryViewer}; use rand::seq::IteratorRandom; use winter_rand_utils::rand_value; @@ -32,9 +32,9 @@ async fn insertion() -> anyhow::Result<()> { let code = format!( r#" - use.$kernel::link_map + use $kernel::link_map - const.MAP_PTR={map_ptr} + const MAP_PTR={map_ptr} begin # Insert key {entry1_key} into an empty map. @@ -534,7 +534,7 @@ async fn execute_link_map_test(operations: Vec) -> anyhow::Result let code = format!( r#" - use.$kernel::link_map + use $kernel::link_map begin {test_code} end diff --git a/crates/miden-testing/src/kernel_tests/tx/test_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_note.rs index 1c37c1591e..49a5be0c60 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -2,37 +2,35 @@ use alloc::collections::BTreeMap; use alloc::sync::Arc; use anyhow::Context; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::errors::MasmError; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::transaction::TransactionKernel; -use miden_lib::transaction::memory::ACTIVE_INPUT_NOTE_PTR; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountBuilder, AccountId}; -use miden_objects::assembly::DefaultSourceManager; -use miden_objects::assembly::diagnostics::miette::{self, miette}; -use miden_objects::asset::FungibleAsset; -use miden_objects::crypto::dsa::rpo_falcon512::SecretKey; -use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin}; -use miden_objects::note::{ +use miden_processor::fast::ExecutionOutput; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{AccountBuilder, AccountId}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::assembly::diagnostics::miette::{self, miette}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::crypto::dsa::falcon512_rpo::SecretKey; +use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; +use miden_protocol::errors::MasmError; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, - NoteExecutionMode, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::transaction::{OutputNote, TransactionArgs}; -use miden_objects::{Felt, Word, ZERO}; -use miden_processor::fast::ExecutionOutput; +use miden_protocol::transaction::memory::ACTIVE_INPUT_NOTE_PTR; +use miden_protocol::transaction::{OutputNote, TransactionArgs}; +use miden_protocol::{Felt, Hasher, Word, ZERO}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::note::NoteBuilder; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -66,8 +64,8 @@ async fn test_note_setup() -> anyhow::Result<()> { }; let code = " - use.$kernel::prologue - use.$kernel::note + use $kernel::prologue + use $kernel::note begin exec.prologue::prepare_transaction @@ -123,14 +121,14 @@ async fn test_note_script_and_note_args() -> miette::Result<()> { .unwrap() }; - let code = " - use.$kernel::prologue - use.$kernel::memory - use.$kernel::note + let code = " + use $kernel::prologue + use $kernel::memory + use $kernel::note begin exec.prologue::prepare_transaction - exec.memory::get_num_input_notes push.2 assert_eq + exec.memory::get_num_input_notes push.2 assert_eq.err=\"unexpected number of input notes\" exec.note::prepare_note drop # => [NOTE_ARGS0, pad(11), pad(16)] repeat.11 movup.4 drop end @@ -192,49 +190,44 @@ async fn test_build_recipient() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; // Create test script and serial number - let note_script = ScriptBuilder::default().compile_note_script("begin nop end")?; + let note_script = CodeBuilder::default().compile_note_script("begin nop end")?; let serial_num = Word::default(); // Define test values as Words let word_1 = Word::from([1, 2, 3, 4u32]); let word_2 = Word::from([5, 6, 7, 8u32]); - let word_3 = Word::from([9, 10, 11, 12u32]); - let word_4 = Word::from([13, 14, 15, 16u32]); const BASE_ADDR: u32 = 4000; let code = format!( " - use.std::sys - - use.miden::note + use miden::core::sys + use miden::protocol::note begin # put the values that will be hashed into the memory push.{word_1} push.{base_addr} mem_storew_be dropw push.{word_2} push.{addr_1} mem_storew_be dropw - push.{word_3} push.{addr_2} mem_storew_be dropw - push.{word_4} push.{addr_3} mem_storew_be dropw - # Test with 4 values + # Test with 4 values (needs padding to 8) push.{script_root} # SCRIPT_ROOT push.{serial_num} # SERIAL_NUM push.4.4000 # num_inputs, inputs_ptr exec.note::build_recipient # => [RECIPIENT_4] - # Test with 5 values + # Test with 5 values (needs padding to 8) push.{script_root} # SCRIPT_ROOT push.{serial_num} # SERIAL_NUM push.5.4000 # num_inputs, inputs_ptr exec.note::build_recipient # => [RECIPIENT_5, RECIPIENT_4] - # Test with 13 values + # Test with 8 values (no padding needed - exactly one rate block) push.{script_root} # SCRIPT_ROOT push.{serial_num} # SERIAL_NUM - push.13.4000 # num_inputs, inputs_ptr + push.8.4000 # num_inputs, inputs_ptr exec.note::build_recipient - # => [RECIPIENT_13, RECIPIENT_5, RECIPIENT_4] + # => [RECIPIENT_8, RECIPIENT_5, RECIPIENT_4] # truncate the stack exec.sys::truncate_stack @@ -242,38 +235,56 @@ async fn test_build_recipient() -> anyhow::Result<()> { ", word_1 = word_1, word_2 = word_2, - word_3 = word_3, - word_4 = word_4, base_addr = BASE_ADDR, addr_1 = BASE_ADDR + 4, - addr_2 = BASE_ADDR + 8, - addr_3 = BASE_ADDR + 12, script_root = note_script.root(), serial_num = serial_num, ); let exec_output = &tx_context.execute_code(&code).await?; - // Create expected recipients and get their digests - let note_inputs_4 = NoteInputs::new(word_1.to_vec())?; - let recipient_4 = NoteRecipient::new(serial_num, note_script.clone(), note_inputs_4); + // Create expected NoteInputs for each test case + let inputs_4 = word_1.to_vec(); + let note_inputs_4 = NoteInputs::new(inputs_4.clone())?; let mut inputs_5 = word_1.to_vec(); inputs_5.push(word_2[0]); - let note_inputs_5 = NoteInputs::new(inputs_5)?; - let recipient_5 = NoteRecipient::new(serial_num, note_script.clone(), note_inputs_5); + let note_inputs_5 = NoteInputs::new(inputs_5.clone())?; + + let mut inputs_8 = word_1.to_vec(); + inputs_8.extend_from_slice(&word_2.to_vec()); + let note_inputs_8 = NoteInputs::new(inputs_8.clone())?; - let mut inputs_13 = word_1.to_vec(); - inputs_13.extend_from_slice(&word_2.to_vec()); - inputs_13.extend_from_slice(&word_3.to_vec()); - inputs_13.push(word_4[0]); - let note_inputs_13 = NoteInputs::new(inputs_13)?; - let recipient_13 = NoteRecipient::new(serial_num, note_script, note_inputs_13); + // Create expected recipients and get their digests + let recipient_4 = NoteRecipient::new(serial_num, note_script.clone(), note_inputs_4.clone()); + let recipient_5 = NoteRecipient::new(serial_num, note_script.clone(), note_inputs_5.clone()); + let recipient_8 = NoteRecipient::new(serial_num, note_script.clone(), note_inputs_8.clone()); + + for note_inputs in [ + (note_inputs_4, inputs_4.clone()), + (note_inputs_5, inputs_5.clone()), + (note_inputs_8, inputs_8.clone()), + ] { + let inputs_advice_map_key = note_inputs.0.commitment(); + assert_eq!( + exec_output.advice.get_mapped_values(&inputs_advice_map_key).unwrap(), + note_inputs.1, + "advice entry with note inputs should contain the unpadded values" + ); + + let num_inputs_advice_map_key = + Hasher::hash_elements(note_inputs.0.commitment().as_elements()); + assert_eq!( + exec_output.advice.get_mapped_values(&num_inputs_advice_map_key).unwrap(), + &[Felt::from(note_inputs.0.num_values())], + "advice entry with num note inputs should contain the original number of values" + ); + } let mut expected_stack = alloc::vec::Vec::new(); expected_stack.extend_from_slice(recipient_4.digest().as_elements()); expected_stack.extend_from_slice(recipient_5.digest().as_elements()); - expected_stack.extend_from_slice(recipient_13.digest().as_elements()); + expected_stack.extend_from_slice(recipient_8.digest().as_elements()); expected_stack.reverse(); assert_eq!(exec_output.stack[0..12], expected_stack); @@ -293,9 +304,9 @@ async fn test_compute_inputs_commitment() -> anyhow::Result<()> { let code = format!( " - use.std::sys + use miden::core::sys - use.miden::note + use miden::protocol::note begin # put the values that will be hashed into the memory @@ -369,59 +380,45 @@ async fn test_compute_inputs_commitment() -> anyhow::Result<()> { } #[tokio::test] -async fn test_build_metadata() -> miette::Result<()> { +async fn test_build_metadata_header() -> miette::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); let sender = tx_context.account().id(); let receiver = AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE) .map_err(|e| miette::miette!("Failed to convert account ID: {}", e))?; - let test_metadata1 = NoteMetadata::new( - sender, - NoteType::Private, - NoteTag::from_account_id(receiver), - NoteExecutionHint::after_block(500.into()) - .map_err(|e| miette::miette!("Failed to create execution hint: {}", e))?, - Felt::try_from(1u64 << 63).map_err(|e| miette::miette!("Failed to convert felt: {}", e))?, - ) - .map_err(|e| miette::miette!("Failed to create metadata: {}", e))?; - let test_metadata2 = NoteMetadata::new( - sender, - NoteType::Public, - // Use largest allowed use_case_id. - NoteTag::for_public_use_case((1 << 14) - 1, u16::MAX, NoteExecutionMode::Local) - .map_err(|e| miette::miette!("Failed to create note tag: {}", e))?, - NoteExecutionHint::on_block_slot(u8::MAX, u8::MAX, u8::MAX), - Felt::try_from(0u64).map_err(|e| miette::miette!("Failed to convert felt: {}", e))?, - ) - .map_err(|e| miette::miette!("Failed to create metadata: {}", e))?; + let test_metadata1 = + NoteMetadata::new(sender, NoteType::Private, NoteTag::with_account_target(receiver)); + let test_metadata2 = NoteMetadata::new(sender, NoteType::Public, NoteTag::new(u32::MAX)); for (iteration, test_metadata) in [test_metadata1, test_metadata2].into_iter().enumerate() { let code = format!( " - use.$kernel::prologue - use.$kernel::output_note + use $kernel::prologue + use $kernel::output_note begin exec.prologue::prepare_transaction - push.{execution_hint} push.{note_type} push.{aux} push.{tag} - exec.output_note::build_metadata + push.{note_type} push.{tag} + exec.output_note::build_metadata_header # truncate the stack swapw dropw end ", - execution_hint = Felt::from(test_metadata.execution_hint()), note_type = Felt::from(test_metadata.note_type()), - aux = test_metadata.aux(), tag = test_metadata.tag(), ); - let exec_output = tx_context.execute_code(&code).await.unwrap(); + let exec_output = tx_context.execute_code(&code).await?; let metadata_word = exec_output.get_stack_word_be(0); - assert_eq!(Word::from(test_metadata), metadata_word, "failed in iteration {iteration}"); + assert_eq!( + test_metadata.to_header_word(), + metadata_word, + "failed in iteration {iteration}" + ); } Ok(()) @@ -434,8 +431,8 @@ pub async fn test_timelock() -> anyhow::Result<()> { let code = format!( r#" - use.miden::active_note - use.miden::tx + use miden::protocol::active_note + use miden::protocol::tx begin # store the note inputs to memory starting at address 0 @@ -469,7 +466,7 @@ pub async fn test_timelock() -> anyhow::Result<()> { .note_inputs([Felt::from(lock_timestamp)])? .source_manager(source_manager.clone()) .code(code.clone()) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) + .dynamically_linked_libraries(CodeBuilder::mock_libraries()) .build()?; builder.add_output_note(OutputNote::Full(timelock_note.clone())); @@ -532,16 +529,10 @@ async fn test_public_key_as_note_input() -> anyhow::Result<()> { .build_existing()?; let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(target_account.id()); - let metadata = NoteMetadata::new( - sender_account.id(), - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - )?; + let tag = NoteTag::with_account_target(target_account.id()); + let metadata = NoteMetadata::new(sender_account.id(), NoteType::Public, tag); let vault = NoteAssets::new(vec![])?; - let note_script = ScriptBuilder::default().compile_note_script("begin nop end")?; + let note_script = CodeBuilder::default().compile_note_script("begin nop end")?; let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::new(public_key_value.to_vec())?); let note_with_pub_key = Note::new(vault.clone(), metadata, recipient); @@ -554,3 +545,43 @@ async fn test_public_key_as_note_input() -> anyhow::Result<()> { tx_context.execute().await?; Ok(()) } + +#[tokio::test] +async fn test_build_note_tag_for_network_account() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + let account_id = AccountId::try_from(ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET)?; + let expected_tag = NoteTag::with_account_target(account_id).as_u32(); + + let prefix: u64 = account_id.prefix().into(); + let suffix: u64 = account_id.suffix().into(); + + let code = format!( + " + use miden::core::sys + use miden::protocol::note + + begin + push.{suffix}.{prefix} + + exec.note::build_note_tag_for_network_account + # => [network_account_tag] + + exec.sys::truncate_stack + end + ", + suffix = suffix, + prefix = prefix, + ); + + let exec_output = tx_context.execute_code(&code).await?; + let actual_tag = exec_output.stack[0].as_int(); + + assert_eq!( + actual_tag, expected_tag as u64, + "Expected tag {:#010x}, got {:#010x}", + expected_tag, actual_tag + ); + + Ok(()) +} 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 b004a03ff7..262301743c 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 @@ -2,36 +2,26 @@ use alloc::string::String; use alloc::vec::Vec; use anyhow::Context; -use miden_lib::errors::tx_kernel_errors::{ +use miden_protocol::account::{Account, AccountId}; +use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; +use miden_protocol::crypto::rand::RpoRandomCoin; +use miden_protocol::errors::tx_kernel::{ ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT, }; -use miden_lib::note::create_p2id_note; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::transaction::memory::{ - NOTE_MEM_SIZE, - NUM_OUTPUT_NOTES_PTR, - OUTPUT_NOTE_ASSETS_OFFSET, - OUTPUT_NOTE_METADATA_OFFSET, - OUTPUT_NOTE_RECIPIENT_OFFSET, - OUTPUT_NOTE_SECTION_OFFSET, -}; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{Account, AccountId}; -use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset}; -use miden_objects::crypto::rand::RpoRandomCoin; -use miden_objects::note::{ +use miden_protocol::note::{ Note, NoteAssets, + NoteAttachment, + NoteAttachmentScheme, NoteExecutionHint, - NoteExecutionMode, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_SENDER, @@ -42,13 +32,26 @@ use miden_objects::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::testing::constants::NON_FUNGIBLE_ASSET_DATA_2; -use miden_objects::transaction::{OutputNote, OutputNotes}; -use miden_objects::{Felt, Word, ZERO}; +use miden_protocol::testing::constants::NON_FUNGIBLE_ASSET_DATA_2; +use miden_protocol::transaction::memory::{ + NOTE_MEM_SIZE, + NUM_OUTPUT_NOTES_PTR, + OUTPUT_NOTE_ASSETS_OFFSET, + OUTPUT_NOTE_ATTACHMENT_OFFSET, + OUTPUT_NOTE_METADATA_HEADER_OFFSET, + OUTPUT_NOTE_RECIPIENT_OFFSET, + OUTPUT_NOTE_SECTION_OFFSET, +}; +use miden_protocol::transaction::{OutputNote, OutputNotes}; +use miden_protocol::{Felt, Word, ZERO}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::note::{NetworkAccountTarget, create_p2id_note}; +use miden_standards::testing::mock_account::MockAccountExt; +use miden_standards::testing::note::NoteBuilder; use super::{TestSetup, setup_test}; use crate::kernel_tests::tx::ExecutionOutputExt; -use crate::utils::create_public_p2any_note; +use crate::utils::{create_public_p2any_note, create_spawn_note}; use crate::{Auth, MockChain, TransactionContextBuilder, assert_execution_error}; #[tokio::test] @@ -57,25 +60,22 @@ async fn test_create_note() -> anyhow::Result<()> { let account_id = tx_context.account().id(); let recipient = Word::from([0, 1, 2, 3u32]); - let aux = Felt::new(27); - let tag = NoteTag::from_account_id(account_id); + let tag = NoteTag::with_account_target(account_id); let code = format!( " - use.miden::output_note + use miden::protocol::output_note - use.$kernel::prologue + use $kernel::prologue begin exec.prologue::prepare_transaction push.{recipient} - push.{note_execution_hint} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - call.output_note::create + exec.output_note::create # truncate the stack swapdw dropw dropw @@ -83,7 +83,6 @@ async fn test_create_note() -> anyhow::Result<()> { ", recipient = recipient, PUBLIC_NOTE = NoteType::Public as u8, - note_execution_hint = Felt::from(NoteExecutionHint::after_block(23.into()).unwrap()), tag = tag, ); @@ -101,19 +100,21 @@ async fn test_create_note() -> anyhow::Result<()> { "recipient must be stored at the correct memory location", ); - let expected_note_metadata: Word = NoteMetadata::new( - account_id, - NoteType::Public, - tag, - NoteExecutionHint::after_block(23.into())?, - Felt::new(27), - )? - .into(); + let metadata = NoteMetadata::new(account_id, NoteType::Public, tag); + let expected_metadata_header = metadata.to_header_word(); + let expected_note_attachment = metadata.to_attachment_word(); assert_eq!( - exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_OFFSET), - expected_note_metadata, - "metadata must be stored at the correct memory location", + exec_output + .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET), + expected_metadata_header, + "metadata header must be stored at the correct memory location", + ); + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET), + expected_note_attachment, + "attachment must be stored at the correct memory location", ); assert_eq!( @@ -129,7 +130,7 @@ async fn test_create_note_with_invalid_tag() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let invalid_tag = Felt::new((NoteType::Public as u64) << 62); - let valid_tag: Felt = NoteTag::for_local_use_case(0, 0).unwrap().into(); + let valid_tag: Felt = NoteTag::default().into(); // Test invalid tag assert!(tx_context.execute_code(¬e_creation_script(invalid_tag)).await.is_err()); @@ -143,28 +144,24 @@ async fn test_create_note_with_invalid_tag() -> anyhow::Result<()> { fn note_creation_script(tag: Felt) -> String { format!( " - use.miden::output_note - use.$kernel::prologue + use miden::protocol::output_note + use $kernel::prologue begin exec.prologue::prepare_transaction push.{recipient} - push.{execution_hint_always} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - call.output_note::create + exec.output_note::create # clean the stack dropw dropw end ", recipient = Word::from([0, 1, 2, 3u32]), - execution_hint_always = Felt::from(NoteExecutionHint::always()), PUBLIC_NOTE = NoteType::Public as u8, - aux = ZERO, ) } @@ -174,30 +171,26 @@ async fn test_create_note_too_many_notes() -> anyhow::Result<()> { let code = format!( " - use.miden::output_note - use.$kernel::constants - use.$kernel::memory - use.$kernel::prologue + use miden::protocol::output_note + use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX + use $kernel::memory + use $kernel::prologue begin - exec.constants::get_max_num_output_notes + push.MAX_OUTPUT_NOTES_PER_TX exec.memory::set_num_output_notes exec.prologue::prepare_transaction push.{recipient} - push.{execution_hint_always} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - call.output_note::create + exec.output_note::create end ", - tag = NoteTag::for_local_use_case(1234, 5678).unwrap(), + tag = NoteTag::new(1234 << 16 | 5678), recipient = Word::from([0, 1, 2, 3u32]), - execution_hint_always = Felt::from(NoteExecutionHint::always()), PUBLIC_NOTE = NoteType::Public as u8, - aux = ZERO, ); let exec_output = tx_context.execute_code(&code).await; @@ -255,30 +248,25 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { // create output note 1 let output_serial_no_1 = Word::from([8u32; 4]); - let output_tag_1 = NoteTag::from_account_id(network_account); + let output_tag_1 = NoteTag::with_account_target(network_account); let assets = NoteAssets::new(vec![input_asset_1])?; - let metadata = NoteMetadata::new( - tx_context.tx_inputs().account().id(), - NoteType::Public, - output_tag_1, - NoteExecutionHint::Always, - ZERO, - )?; + let metadata = + NoteMetadata::new(tx_context.tx_inputs().account().id(), NoteType::Public, output_tag_1); let inputs = NoteInputs::new(vec![])?; let recipient = NoteRecipient::new(output_serial_no_1, input_note_1.script().clone(), inputs); let output_note_1 = Note::new(assets, metadata, recipient); // create output note 2 let output_serial_no_2 = Word::from([11u32; 4]); - let output_tag_2 = NoteTag::from_account_id(local_account); + let output_tag_2 = NoteTag::with_account_target(local_account); let assets = NoteAssets::new(vec![input_asset_2])?; - let metadata = NoteMetadata::new( - tx_context.tx_inputs().account().id(), - NoteType::Public, - output_tag_2, - NoteExecutionHint::after_block(123.into())?, - ZERO, + let attachment = NoteAttachment::new_array( + NoteAttachmentScheme::new(5), + [42, 43, 44, 45, 46u32].map(Felt::from).to_vec(), )?; + let metadata = + NoteMetadata::new(tx_context.tx_inputs().account().id(), NoteType::Public, output_tag_2) + .with_attachment(attachment); let inputs = NoteInputs::new(vec![])?; let recipient = NoteRecipient::new(output_serial_no_2, input_note_2.script().clone(), inputs); let output_note_2 = Note::new(assets, metadata, recipient); @@ -292,42 +280,44 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { let code = format!( " - use.std::sys + use miden::core::sys - use.miden::tx - use.miden::output_note + use miden::protocol::tx + use miden::protocol::output_note - use.$kernel::prologue + use $kernel::prologue begin - # => [BH, acct_id, IAH, NC] exec.prologue::prepare_transaction # => [] # create output note 1 push.{recipient_1} - push.{NOTE_EXECUTION_HINT_1} push.{PUBLIC_NOTE} - push.{aux_1} push.{tag_1} - call.output_note::create + exec.output_note::create # => [note_idx] push.{asset_1} - call.output_note::add_asset + exec.output_note::add_asset # => [] # create output note 2 push.{recipient_2} - push.{NOTE_EXECUTION_HINT_2} push.{PUBLIC_NOTE} - push.{aux_2} push.{tag_2} - call.output_note::create + exec.output_note::create # => [note_idx] - push.{asset_2} - call.output_note::add_asset + dup push.{asset_2} + exec.output_note::add_asset + # => [note_idx] + + push.{ATTACHMENT2} + push.{attachment_scheme2} + movup.5 + # => [note_idx, attachment_scheme, ATTACHMENT] + exec.output_note::set_array_attachment # => [] # compute the output notes commitment @@ -340,20 +330,18 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { end ", PUBLIC_NOTE = NoteType::Public as u8, - NOTE_EXECUTION_HINT_1 = Felt::from(output_note_1.metadata().execution_hint()), recipient_1 = output_note_1.recipient().digest(), tag_1 = output_note_1.metadata().tag(), - aux_1 = output_note_1.metadata().aux(), asset_1 = Word::from( **output_note_1.assets().iter().take(1).collect::>().first().unwrap() ), recipient_2 = output_note_2.recipient().digest(), - NOTE_EXECUTION_HINT_2 = Felt::from(output_note_2.metadata().execution_hint()), tag_2 = output_note_2.metadata().tag(), - aux_2 = output_note_2.metadata().aux(), asset_2 = Word::from( **output_note_2.assets().iter().take(1).collect::>().first().unwrap() ), + ATTACHMENT2 = output_note_2.metadata().to_attachment_word(), + attachment_scheme2 = output_note_2.metadata().attachment().attachment_scheme().as_u32(), ); let exec_output = &tx_context.execute_code(&code).await?; @@ -364,21 +352,30 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { "The test creates two notes", ); assert_eq!( - NoteMetadata::try_from( - exec_output - .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_OFFSET) - ) - .unwrap(), - *output_note_1.metadata(), - "Validate the output note 1 metadata", + exec_output + .get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET), + output_note_1.metadata().to_header_word(), + "Validate the output note 1 metadata header", + ); + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET), + output_note_1.metadata().to_attachment_word(), + "Validate the output note 1 attachment", + ); + + assert_eq!( + exec_output.get_kernel_mem_word( + OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET + NOTE_MEM_SIZE + ), + output_note_2.metadata().to_header_word(), + "Validate the output note 2 metadata header", ); assert_eq!( - NoteMetadata::try_from(exec_output.get_kernel_mem_word( - OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_OFFSET + NOTE_MEM_SIZE - )) - .unwrap(), - *output_note_2.metadata(), - "Validate the output note 1 metadata", + exec_output.get_kernel_mem_word( + OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET + NOTE_MEM_SIZE + ), + output_note_2.metadata().to_attachment_word(), + "Validate the output note 2 attachment", ); assert_eq!(exec_output.get_stack_word_be(0), expected_output_notes_commitment); @@ -391,26 +388,23 @@ async fn test_create_note_and_add_asset() -> anyhow::Result<()> { let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; let recipient = Word::from([0, 1, 2, 3u32]); - let aux = Felt::new(27); - let tag = NoteTag::from_account_id(faucet_id); + let tag = NoteTag::with_account_target(faucet_id); let asset = Word::from(FungibleAsset::new(faucet_id, 10)?); let code = format!( " - use.miden::output_note + use miden::protocol::output_note - use.$kernel::prologue + use $kernel::prologue begin exec.prologue::prepare_transaction push.{recipient} - push.{NOTE_EXECUTION_HINT} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - call.output_note::create + exec.output_note::create # => [note_idx] # assert that the index of the created note equals zero @@ -429,7 +423,6 @@ async fn test_create_note_and_add_asset() -> anyhow::Result<()> { ", recipient = recipient, PUBLIC_NOTE = NoteType::Public as u8, - NOTE_EXECUTION_HINT = Felt::from(NoteExecutionHint::always()), tag = tag, asset = asset, ); @@ -453,8 +446,7 @@ async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { let faucet_2 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2)?; let recipient = Word::from([0, 1, 2, 3u32]); - let aux = Felt::new(27); - let tag = NoteTag::from_account_id(faucet_2); + let tag = NoteTag::with_account_target(faucet_2); let asset = Word::from(FungibleAsset::new(faucet, 10)?); let asset_2 = Word::from(FungibleAsset::new(faucet_2, 20)?); @@ -466,18 +458,16 @@ async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { let code = format!( " - use.miden::output_note - use.$kernel::prologue + use miden::protocol::output_note + use $kernel::prologue begin exec.prologue::prepare_transaction push.{recipient} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - - call.output_note::create + exec.output_note::create # => [note_idx] # assert that the index of the created note equals zero @@ -541,44 +531,38 @@ async fn test_create_note_and_add_same_nft_twice() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let recipient = Word::from([0, 1, 2, 3u32]); - let tag = NoteTag::for_public_use_case(999, 777, NoteExecutionMode::Local).unwrap(); + let tag = NoteTag::new(999 << 16 | 777); let non_fungible_asset = NonFungibleAsset::mock(&[1, 2, 3]); let encoded = Word::from(non_fungible_asset); let code = format!( " - use.$kernel::prologue - use.miden::output_note + use $kernel::prologue + use miden::protocol::output_note begin exec.prologue::prepare_transaction # => [] - padw padw push.{recipient} - push.{execution_hint_always} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - - call.output_note::create + exec.output_note::create # => [note_idx] dup push.{nft} # => [NFT, note_idx, note_idx] - call.output_note::add_asset + exec.output_note::add_asset # => [note_idx] push.{nft} - call.output_note::add_asset + exec.output_note::add_asset # => [] end ", recipient = recipient, PUBLIC_NOTE = NoteType::Public as u8, - execution_hint_always = Felt::from(NoteExecutionHint::always()), - aux = Felt::new(0), tag = tag, nft = encoded, ); @@ -630,8 +614,7 @@ async fn test_build_recipient_hash() -> anyhow::Result<()> { // create output note let output_serial_no = Word::from([0, 1, 2, 3u32]); - let aux = Felt::new(27); - let tag = NoteTag::for_public_use_case(42, 42, NoteExecutionMode::Network).unwrap(); + let tag = NoteTag::new(42 << 16 | 42); let single_input = 2; let inputs = NoteInputs::new(vec![Felt::new(single_input)]).unwrap(); let input_commitment = inputs.commitment(); @@ -639,46 +622,40 @@ async fn test_build_recipient_hash() -> anyhow::Result<()> { let recipient = NoteRecipient::new(output_serial_no, input_note_1.script().clone(), inputs); let code = format!( " - use.miden::output_note - use.miden::note - use.$kernel::prologue + use $kernel::prologue + use miden::protocol::output_note + use miden::protocol::note + use miden::core::sys begin exec.prologue::prepare_transaction - # pad the stack before call - padw - # input push.{input_commitment} # SCRIPT_ROOT push.{script_root} # SERIAL_NUM push.{output_serial_no} - # => [SERIAL_NUM, SCRIPT_ROOT, INPUT_COMMITMENT, pad(4)] + # => [SERIAL_NUM, SCRIPT_ROOT, INPUT_COMMITMENT] exec.note::build_recipient_hash # => [RECIPIENT, pad(12)] - push.{execution_hint} push.{PUBLIC_NOTE} - push.{aux} push.{tag} - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(12)] + # => [tag, note_type, RECIPIENT] - call.output_note::create - # => [note_idx, pad(19)] + exec.output_note::create + # => [note_idx] # clean the stack - dropw dropw dropw dropw dropw + exec.sys::truncate_stack end ", script_root = input_note_1.script().clone().root(), output_serial_no = output_serial_no, PUBLIC_NOTE = NoteType::Public as u8, tag = tag, - execution_hint = Felt::from(NoteExecutionHint::after_block(2.into()).unwrap()), - aux = aux, ); let exec_output = &tx_context.execute_code(&code).await?; @@ -702,7 +679,7 @@ async fn test_build_recipient_hash() -> anyhow::Result<()> { /// This test creates an output note and then adds some assets into it checking the assets info on /// each stage. /// -/// Namely, we invoke the `miden::output_notes::get_assets_info` procedure: +/// Namely, we invoke the `miden::protocol::output_notes::get_assets_info` procedure: /// - After adding the first `asset_0` to the note. /// - Right after the previous check to make sure it returns the same commitment from the cached /// data. @@ -739,7 +716,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![fungible_asset_0], NoteType::Public, - Felt::new(0), + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), )?; @@ -748,28 +725,26 @@ async fn test_get_asset_info() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![fungible_asset_0, fungible_asset_1], NoteType::Public, - Felt::new(0), + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([4, 3, 2, 1u32])), )?; let tx_script_src = &format!( r#" - use.miden::output_note - use.std::sys + use miden::protocol::output_note + use miden::core::sys begin # create an output note with fungible asset 0 push.{RECIPIENT} - push.{note_execution_hint} push.{note_type} - push.0 # aux push.{tag} - call.output_note::create + exec.output_note::create # => [note_idx] # move the asset 0 to the note push.{asset_0} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw # => [note_idx] @@ -798,7 +773,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { # add asset_1 to the note push.{asset_1} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw # => [note_idx] @@ -822,7 +797,6 @@ async fn test_get_asset_info() -> anyhow::Result<()> { "#, // output note RECIPIENT = output_note_1.recipient().digest(), - note_execution_hint = Felt::from(output_note_1.metadata().execution_hint()), note_type = NoteType::Public as u8, tag = output_note_1.metadata().tag(), asset_0 = Word::from(fungible_asset_0), @@ -835,7 +809,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> { assets_number_1 = output_note_1.assets().num_assets(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; let tx_context = mock_chain .build_tx_context(account.id(), &[], &[])? @@ -864,14 +838,14 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![FungibleAsset::mock(5)], NoteType::Public, - Felt::new(0), + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), )?; let tx_script_src = &format!( r#" - use.miden::output_note - use.std::sys + use miden::protocol::output_note + use miden::core::sys begin # create an output note with one asset @@ -891,11 +865,14 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { # get the metadata (the only existing note has 0'th index) push.0 exec.output_note::get_metadata - # => [METADATA] + # => [NOTE_ATTACHMENT, METADATA_HEADER] - # assert the correctness of the metadata - push.{METADATA} - assert_eqw.err="requested note has incorrect metadata" + push.{NOTE_ATTACHMENT} + assert_eqw.err="requested note has incorrect note attachment" + # => [METADATA_HEADER] + + push.{METADATA_HEADER} + assert_eqw.err="requested note has incorrect metadata header" # => [] # truncate the stack @@ -904,10 +881,11 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { "#, output_note = create_output_note(&output_note), RECIPIENT = output_note.recipient().digest(), - METADATA = Word::from(output_note.metadata()), + METADATA_HEADER = output_note.metadata().to_header_word(), + NOTE_ATTACHMENT = output_note.metadata().to_attachment_word(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; let tx_context = mock_chain .build_tx_context(account.id(), &[], &[])? @@ -984,8 +962,8 @@ async fn test_get_assets() -> anyhow::Result<()> { let tx_script_src = &format!( " - use.miden::output_note - use.std::sys + use miden::protocol::output_note + use miden::core::sys begin {create_note_0} @@ -1009,7 +987,7 @@ async fn test_get_assets() -> anyhow::Result<()> { check_note_2 = check_assets_code(2, 8, &p2id_note_2_assets), ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; let tx_context = mock_chain .build_tx_context(account.id(), &[], &[])? @@ -1026,6 +1004,202 @@ async fn test_get_assets() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_set_none_attachment() -> anyhow::Result<()> { + let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); + let rng = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])); + let attachment = NoteAttachment::default(); + let output_note = + OutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); + + let tx_script = format!( + " + use miden::protocol::output_note + + begin + push.{RECIPIENT} + push.{note_type} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{ATTACHMENT} + push.{attachment_kind} + push.{attachment_scheme} + movup.6 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + exec.output_note::set_attachment + # => [] + + # truncate the stack + swapdw dropw dropw + end + ", + RECIPIENT = output_note.recipient().unwrap().digest(), + note_type = output_note.metadata().note_type() as u8, + tag = output_note.metadata().tag().as_u32(), + ATTACHMENT = output_note.metadata().to_attachment_word(), + attachment_kind = output_note.metadata().attachment().content().attachment_kind().as_u8(), + attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(), + ); + + let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; + + let tx = TransactionContextBuilder::new(account) + .extend_expected_output_notes(vec![output_note.clone()]) + .tx_script(tx_script) + .build()? + .execute() + .await?; + + let actual_note = tx.output_notes().get_note(0); + assert_eq!(actual_note.header(), output_note.header()); + assert_eq!(actual_note.assets(), output_note.assets()); + + Ok(()) +} + +#[tokio::test] +async fn test_set_word_attachment() -> anyhow::Result<()> { + let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); + let rng = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])); + let attachment = + NoteAttachment::new_word(NoteAttachmentScheme::new(u32::MAX), Word::from([3, 4, 5, 6u32])); + let output_note = + OutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); + + let tx_script = format!( + " + use miden::protocol::output_note + + begin + push.{RECIPIENT} + push.{note_type} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{ATTACHMENT} + push.{attachment_scheme} + movup.5 + # => [note_idx, attachment_scheme, ATTACHMENT] + exec.output_note::set_word_attachment + # => [] + + # truncate the stack + swapdw dropw dropw + end + ", + RECIPIENT = output_note.recipient().unwrap().digest(), + note_type = output_note.metadata().note_type() as u8, + tag = output_note.metadata().tag().as_u32(), + attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(), + ATTACHMENT = output_note.metadata().to_attachment_word(), + ); + + let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; + + let tx = TransactionContextBuilder::new(account) + .extend_expected_output_notes(vec![output_note.clone()]) + .tx_script(tx_script) + .build()? + .execute() + .await?; + + let actual_note = tx.output_notes().get_note(0); + assert_eq!(actual_note.header(), output_note.header()); + assert_eq!(actual_note.assets(), output_note.assets()); + + Ok(()) +} + +#[tokio::test] +async fn test_set_array_attachment() -> anyhow::Result<()> { + let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); + let rng = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])); + let elements = [3, 4, 5, 6, 7, 8, 9u32].map(Felt::from).to_vec(); + let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42), elements.clone())?; + let output_note = + OutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?); + + let tx_script = format!( + " + use miden::protocol::output_note + + begin + push.{RECIPIENT} + push.{note_type} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{ATTACHMENT} + push.{attachment_scheme} + movup.5 + # => [note_idx, attachment_scheme, ATTACHMENT] + exec.output_note::set_array_attachment + # => [] + + # truncate the stack + swapdw dropw dropw + end + ", + RECIPIENT = output_note.recipient().unwrap().digest(), + note_type = output_note.metadata().note_type() as u8, + tag = output_note.metadata().tag().as_u32(), + attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(), + ATTACHMENT = output_note.metadata().to_attachment_word(), + ); + + let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?; + + let tx = TransactionContextBuilder::new(account) + .extend_expected_output_notes(vec![output_note.clone()]) + .tx_script(tx_script) + .extend_advice_map(vec![(output_note.metadata().to_attachment_word(), elements)]) + .build()? + .execute() + .await?; + + let actual_note = tx.output_notes().get_note(0); + assert_eq!(actual_note.header(), output_note.header()); + assert_eq!(actual_note.assets(), output_note.assets()); + + Ok(()) +} + +/// Tests creating an output note with an attachment of type NetworkAccountTarget. +#[tokio::test] +async fn test_set_network_target_account_attachment() -> anyhow::Result<()> { + let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce); + let rng = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])); + let attachment = NetworkAccountTarget::new( + ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET.try_into()?, + NoteExecutionHint::on_block_slot(5, 32, 3), + )?; + let output_note = NoteBuilder::new(account.id(), rng) + .note_type(NoteType::Private) + .attachment(attachment) + .build()?; + let spawn_note = create_spawn_note([&output_note])?; + + let tx = TransactionContextBuilder::new(account) + .extend_input_notes([spawn_note].to_vec()) + .build()? + .execute() + .await?; + + let actual_note = tx.output_notes().get_note(0); + assert_eq!(actual_note.header(), output_note.header()); + assert_eq!(actual_note.assets().unwrap(), output_note.assets()); + + // Make sure we can deserialize the attachment back into its original type. + let actual_attachment = NetworkAccountTarget::try_from(actual_note.metadata().attachment())?; + assert_eq!(actual_attachment, attachment); + + Ok(()) +} + // HELPER FUNCTIONS // ================================================================================================ @@ -1037,15 +1211,12 @@ fn create_output_note(note: &Note) -> String { " # create an output note push.{RECIPIENT} - push.{note_execution_hint} push.{note_type} - push.0 # aux push.{tag} - call.output_note::create + exec.output_note::create # => [note_idx] ", RECIPIENT = note.recipient().digest(), - note_execution_hint = Felt::from(note.metadata().execution_hint()), note_type = note.metadata().note_type() as u8, tag = Felt::from(note.metadata().tag()), ); @@ -1055,7 +1226,7 @@ fn create_output_note(note: &Note) -> String { " # move the asset to the note push.{asset} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw # => [note_idx] ", 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 b3d65aa2f3..07ad25bf3d 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -2,22 +2,38 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; use anyhow::Context; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::errors::tx_kernel_errors::{ +use miden_processor::fast::ExecutionOutput; +use miden_processor::{AdviceInputs, Word}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountId, + AccountIdVersion, + AccountProcedureRoot, + AccountStorage, + AccountStorageMode, + AccountType, + StorageMap, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; +use miden_protocol::errors::tx_kernel::{ ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH, ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY, ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPTY_SMT, }; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::transaction::TransactionKernel; -use miden_lib::transaction::memory::{ +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, + ACCOUNT_ID_SENDER, +}; +use miden_protocol::testing::noop_auth_component::NoopAuthComponent; +use miden_protocol::transaction::memory::{ ACCT_DB_ROOT_PTR, BLOCK_COMMITMENT_PTR, BLOCK_METADATA_PTR, BLOCK_NUMBER_IDX, CHAIN_COMMITMENT_PTR, - FAUCET_STORAGE_DATA_SLOT, FEE_PARAMETERS_PTR, INIT_ACCT_COMMITMENT_PTR, INIT_NATIVE_ACCT_STORAGE_COMMITMENT_PTR, @@ -26,9 +42,10 @@ use miden_lib::transaction::memory::{ INPUT_NOTE_ARGS_OFFSET, INPUT_NOTE_ASSETS_COMMITMENT_OFFSET, INPUT_NOTE_ASSETS_OFFSET, + INPUT_NOTE_ATTACHMENT_OFFSET, INPUT_NOTE_ID_OFFSET, INPUT_NOTE_INPUTS_COMMITMENT_OFFSET, - INPUT_NOTE_METADATA_OFFSET, + INPUT_NOTE_METADATA_HEADER_OFFSET, INPUT_NOTE_NULLIFIER_SECTION_PTR, INPUT_NOTE_NUM_ASSETS_OFFSET, INPUT_NOTE_RECIPIENT_OFFSET, @@ -54,36 +71,20 @@ use miden_lib::transaction::memory::{ PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR, PARTIAL_BLOCKCHAIN_PEAKS_PTR, PREV_BLOCK_COMMITMENT_PTR, - PROOF_COMMITMENT_PTR, PROTOCOL_VERSION_IDX, TIMESTAMP_IDX, TX_COMMITMENT_PTR, TX_KERNEL_COMMITMENT_PTR, TX_SCRIPT_ROOT_PTR, + VALIDATOR_KEY_COMMITMENT_PTR, VERIFICATION_BASE_FEE_IDX, }; -use miden_objects::account::{ - Account, - AccountBuilder, - AccountId, - AccountIdVersion, - AccountProcedureInfo, - AccountStorage, - AccountStorageMode, - AccountType, - StorageMap, - StorageSlot, -}; -use miden_objects::asset::{FungibleAsset, NonFungibleAsset}; -use miden_objects::testing::account_id::{ - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_SENDER, -}; -use miden_objects::testing::noop_auth_component::NoopAuthComponent; -use miden_objects::transaction::{ExecutedTransaction, TransactionArgs, TransactionScript}; -use miden_objects::{EMPTY_WORD, ONE, WORD_SIZE}; -use miden_processor::fast::ExecutionOutput; -use miden_processor::{AdviceInputs, Word}; +use miden_protocol::transaction::{ExecutedTransaction, TransactionArgs, TransactionKernel}; +use miden_protocol::{EMPTY_WORD, ONE, WORD_SIZE}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockAccountComponent; +use miden_standards::testing::mock_account::MockAccountExt; use miden_tx::TransactionExecutorError; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -123,7 +124,7 @@ async fn test_transaction_prologue() -> anyhow::Result<()> { }; let code = " - use.$kernel::prologue + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -136,12 +137,7 @@ async fn test_transaction_prologue() -> anyhow::Result<()> { end "; - let mock_tx_script_program = TransactionKernel::assembler() - .with_debug_mode(true) - .assemble_program(mock_tx_script_code) - .unwrap(); - - let tx_script = TransactionScript::new(mock_tx_script_program); + let tx_script = CodeBuilder::default().compile_tx_script(mock_tx_script_code).unwrap(); let note_args = [Word::from([91u32; 4]), Word::from([92u32; 4])]; @@ -199,7 +195,7 @@ fn global_input_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transa assert_eq!( exec_output.get_kernel_mem_word(INIT_NATIVE_ACCT_STORAGE_COMMITMENT_PTR), - inputs.account().storage().commitment(), + inputs.account().storage().to_commitment(), "The initial native account storage commitment should be stored at the INIT_ACCT_STORAGE_COMMITMENT_PTR" ); @@ -266,9 +262,9 @@ fn block_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transact ); assert_eq!( - exec_output.get_kernel_mem_word(PROOF_COMMITMENT_PTR), - inputs.tx_inputs().block_header().proof_commitment(), - "The proof commitment should be stored at the PROOF_COMMITMENT_PTR" + exec_output.get_kernel_mem_word(VALIDATOR_KEY_COMMITMENT_PTR), + inputs.tx_inputs().block_header().validator_key().to_commitment(), + "The public key commitment should be stored at the VALIDATOR_KEY_COMMITMENT_PTR" ); assert_eq!( @@ -332,7 +328,7 @@ fn partial_blockchain_memory_assertions( // update the partial blockchain to point to the block against which this transaction is being // executed let mut partial_blockchain = prepared_tx.tx_inputs().blockchain().clone(); - partial_blockchain.add_block(prepared_tx.tx_inputs().block_header().clone(), true); + partial_blockchain.add_block(prepared_tx.tx_inputs().block_header(), true); assert_eq!( exec_output.get_kernel_mem_word(PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR)[0], @@ -393,7 +389,7 @@ fn account_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transa assert_eq!( exec_output.get_kernel_mem_word(NATIVE_ACCT_STORAGE_COMMITMENT_PTR), - inputs.account().storage().commitment(), + inputs.account().storage().to_commitment(), "The account storage commitment should be stored at NATIVE_ACCT_STORAGE_COMMITMENT_PTR" ); @@ -412,8 +408,8 @@ fn account_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transa for (i, elements) in inputs .account() .storage() - .as_elements() - .chunks(StorageSlot::NUM_ELEMENTS_PER_STORAGE_SLOT / 2) + .to_elements() + .chunks(StorageSlot::NUM_ELEMENTS / 2) .enumerate() { assert_eq!( @@ -435,14 +431,14 @@ fn account_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &Transa .account() .code() .as_elements() - .chunks(AccountProcedureInfo::NUM_ELEMENTS_PER_PROC / 2) + .chunks(AccountProcedureRoot::NUM_ELEMENTS) .enumerate() { assert_eq!( exec_output .get_kernel_mem_word(NATIVE_ACCT_PROCEDURES_SECTION_PTR + (i * WORD_SIZE) as u32), Word::try_from(elements).unwrap(), - "The account procedures and storage offsets should be stored starting at NATIVE_ACCT_PROCEDURES_SECTION_PTR" + "The account procedures should be stored starting at NATIVE_ACCT_PROCEDURES_SECTION_PTR" ); } } @@ -506,9 +502,15 @@ fn input_notes_memory_assertions( ); assert_eq!( - exec_output.get_note_mem_word(note_idx, INPUT_NOTE_METADATA_OFFSET), - Word::from(note.metadata()), - "note metadata should be stored at the correct offset" + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_METADATA_HEADER_OFFSET), + note.metadata().to_header_word(), + "note metadata header should be stored at the correct offset" + ); + + assert_eq!( + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_ATTACHMENT_OFFSET), + note.metadata().to_attachment_word(), + "note attachment should be stored at the correct offset" ); assert_eq!( @@ -588,11 +590,14 @@ pub async fn create_multiple_accounts_test(storage_mode: AccountStorageMode) -> .account_type(account_type) .storage_mode(storage_mode) .with_auth_component(Auth::IncrNonce) - .with_component(MockAccountComponent::with_slots(vec![StorageSlot::Value(Word::from( - [255u32; WORD_SIZE], - ))])) + .with_component(MockAccountComponent::with_slots(vec![StorageSlot::with_value( + StorageSlotName::mock(0), + Word::from([255u32; WORD_SIZE]), + )])) .build() - .context("account build failed")?; + .with_context(|| { + format!("account build for {account_type} and {storage_mode} failed") + })?; accounts.push(account); } @@ -627,7 +632,7 @@ fn compute_valid_account_id(account: Account) -> Account { AccountStorageMode::Public, AccountIdVersion::Version0, account.code().commitment(), - account.storage().commitment(), + account.storage().to_commitment(), ) .unwrap(); @@ -635,7 +640,7 @@ fn compute_valid_account_id(account: Account) -> Account { seed, AccountIdVersion::Version0, account.code().commitment(), - account.storage().commitment(), + account.storage().to_commitment(), ) .unwrap(); @@ -660,7 +665,9 @@ pub async fn create_account_fungible_faucet_invalid_initial_balance() -> anyhow: // Set the initial balance to a non-zero value manually, since the builder would not allow us to // do that. let faucet_data_slot = Word::from([0, 0, 0, 100u32]); - storage.set_item(FAUCET_STORAGE_DATA_SLOT, faucet_data_slot).unwrap(); + storage + .set_item(AccountStorage::faucet_sysdata_slot(), faucet_data_slot) + .unwrap(); // The compute account ID function will set the nonce to zero so this is considered a new // account. @@ -686,7 +693,11 @@ pub async fn create_account_non_fungible_faucet_invalid_initial_reserved_slot() let asset = NonFungibleAsset::mock(&[1, 2, 3, 4]); let non_fungible_storage_map = StorageMap::with_entries([(asset.vault_key().into(), asset.into())]).unwrap(); - let storage = AccountStorage::new(vec![StorageSlot::Map(non_fungible_storage_map)]).unwrap(); + let storage = AccountStorage::new(vec![StorageSlot::with_map( + AccountStorage::faucet_sysdata_slot().clone(), + non_fungible_storage_map, + )]) + .unwrap(); let account = AccountBuilder::new([1; 32]) .account_type(AccountType::NonFungibleFaucet) @@ -738,7 +749,7 @@ pub async fn create_account_invalid_seed() -> anyhow::Result<()> { .build()?; let code = " - use.$kernel::prologue + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -756,8 +767,8 @@ pub async fn create_account_invalid_seed() -> anyhow::Result<()> { async fn test_get_blk_version() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = " - use.$kernel::memory - use.$kernel::prologue + use $kernel::memory + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -782,8 +793,8 @@ async fn test_get_blk_version() -> anyhow::Result<()> { async fn test_get_blk_timestamp() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = " - use.$kernel::memory - use.$kernel::prologue + use $kernel::memory + use $kernel::prologue begin exec.prologue::prepare_transaction diff --git a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs index dfee592916..6ff0536b97 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -2,15 +2,8 @@ use alloc::sync::Arc; use anyhow::Context; use assert_matches::assert_matches; -use miden_lib::AuthScheme; -use miden_lib::account::interface::AccountInterface; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::note::create_p2id_note; -use miden_lib::testing::account_component::IncrNonceAuthComponent; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_lib::transaction::TransactionKernel; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{ +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::{ Account, AccountBuilder, AccountCode, @@ -19,16 +12,18 @@ use miden_objects::account::{ AccountStorageMode, AccountType, StorageSlot, + StorageSlotName, }; -use miden_objects::assembly::DefaultSourceManager; -use miden_objects::assembly::diagnostics::NamedSource; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; -use miden_objects::block::BlockNumber; -use miden_objects::note::{ +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::assembly::diagnostics::NamedSource; +use miden_protocol::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, - NoteExecutionMode, + NoteAttachment, + NoteAttachmentContent, + NoteAttachmentScheme, NoteHeader, NoteId, NoteInputs, @@ -37,7 +32,7 @@ use miden_objects::note::{ NoteTag, NoteType, }; -use miden_objects::testing::account_id::{ +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, @@ -45,17 +40,24 @@ use miden_objects::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; -use miden_objects::testing::note::DEFAULT_NOTE_CODE; -use miden_objects::transaction::{ +use miden_protocol::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; +use miden_protocol::testing::note::DEFAULT_NOTE_CODE; +use miden_protocol::transaction::{ InputNotes, OutputNote, OutputNotes, TransactionArgs, + TransactionKernel, TransactionSummary, }; -use miden_objects::{Felt, FieldElement, Hasher, ONE, Word}; -use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::{Felt, Hasher, ONE, Word}; +use miden_standards::AuthScheme; +use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::note::create_p2id_note; +use miden_standards::testing::account_component::IncrNonceAuthComponent; +use miden_standards::testing::mock_account::MockAccountExt; use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError}; @@ -132,8 +134,8 @@ async fn test_block_procedures() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = " - use.miden::tx - use.$kernel::prologue + use miden::protocol::tx + use $kernel::prologue begin exec.prologue::prepare_transaction @@ -197,85 +199,67 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { .expect("asset is valid"), ); - let tag1 = NoteTag::from_account_id( + let tag1 = NoteTag::with_account_target( ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), ); - let tag2 = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap(); - let tag3 = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap(); - let aux1 = Felt::new(27); - let aux2 = Felt::new(28); - let aux3 = Felt::new(29); + let tag2 = NoteTag::default(); + let tag3 = NoteTag::default(); + + let attachment2 = + NoteAttachment::new_word(NoteAttachmentScheme::new(28), Word::from([2, 3, 4, 5u32])); + let attachment3 = NoteAttachment::new_array( + NoteAttachmentScheme::new(29), + [6, 7, 8, 9u32].map(Felt::from).to_vec(), + )?; let note_type1 = NoteType::Private; let note_type2 = NoteType::Public; let note_type3 = NoteType::Public; - tag1.validate(note_type1).expect("note tag 1 should support private notes"); - tag2.validate(note_type2).expect("note tag 2 should support public notes"); - tag3.validate(note_type3).expect("note tag 3 should support public notes"); - // In this test we create 3 notes. Note 1 is private, Note 2 is public and Note 3 is public // without assets. // Create the expected output note for Note 2 which is public let serial_num_2 = Word::from([1, 2, 3, 4u32]); - let note_script_2 = ScriptBuilder::default().compile_note_script(DEFAULT_NOTE_CODE)?; + let note_script_2 = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_CODE)?; let inputs_2 = NoteInputs::new(vec![ONE])?; let metadata_2 = - NoteMetadata::new(account_id, note_type2, tag2, NoteExecutionHint::none(), aux2)?; + NoteMetadata::new(account_id, note_type2, tag2).with_attachment(attachment2.clone()); let vault_2 = NoteAssets::new(vec![removed_asset_3, removed_asset_4])?; let recipient_2 = NoteRecipient::new(serial_num_2, note_script_2, inputs_2); let expected_output_note_2 = Note::new(vault_2, metadata_2, recipient_2); // Create the expected output note for Note 3 which is public let serial_num_3 = Word::from([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]); - let note_script_3 = ScriptBuilder::default().compile_note_script(DEFAULT_NOTE_CODE)?; + let note_script_3 = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_CODE)?; let inputs_3 = NoteInputs::new(vec![ONE, Felt::new(2)])?; - let metadata_3 = NoteMetadata::new( - account_id, - note_type3, - tag3, - NoteExecutionHint::on_block_slot(1, 2, 3), - aux3, - )?; + let metadata_3 = + NoteMetadata::new(account_id, note_type3, tag3).with_attachment(attachment3.clone()); let vault_3 = NoteAssets::new(vec![])?; let recipient_3 = NoteRecipient::new(serial_num_3, note_script_3, inputs_3); let expected_output_note_3 = Note::new(vault_3, metadata_3, recipient_3); let tx_script_src = format!( "\ - use.miden::contracts::wallets::basic->wallet - use.miden::output_note - - # Inputs: [tag, aux, note_type, execution_hint, RECIPIENT] - # Outputs: [note_idx] - proc.create_note - # pad the stack before the call to prevent accidental modification of the deeper stack - # elements - padw padw swapdw - # => [tag, aux, execution_hint, note_type, RECIPIENT, pad(8)] - - call.output_note::create - # => [note_idx, pad(15)] - - # remove excess PADs from the stack - swapdw dropw dropw movdn.7 dropw drop drop drop - # => [note_idx] - end - - # Inputs: [ASSET, note_idx] - # Outputs: [ASSET, note_idx] - proc.move_asset_to_note + use miden::standards::wallets::basic->wallet + use miden::protocol::output_note + + #! Wrapper around move_asset_to_note for use with exec. + #! + #! Inputs: [ASSET, note_idx] + #! Outputs: [note_idx] + proc move_asset_to_note # pad the stack before call push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw # => [ASSET, note_idx, pad(11)] call.wallet::move_asset_to_note - # => [ASSET, note_idx, pad(11)] + dropw + # => [note_idx, pad(11)] # remove excess PADs from the stack - swapdw dropw dropw swapw movdn.7 drop drop drop - # => [ASSET, note_idx] + repeat.11 swap drop end + # => [note_idx] end ## TRANSACTION SCRIPT @@ -285,47 +269,54 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { ## ------------------------------------------------------------------------------------ # partially deplete fungible asset balance push.0.1.2.3 # recipient - push.{EXECUTION_HINT_1} # note execution hint push.{NOTETYPE1} # note_type - push.{aux1} # aux push.{tag1} # tag - exec.create_note - # => [note_idx] + exec.output_note::create + # => [note_idx = 0] push.{REMOVED_ASSET_1} # asset_1 # => [ASSET, note_idx] - exec.move_asset_to_note dropw + exec.move_asset_to_note # => [note_idx] push.{REMOVED_ASSET_2} # asset_2 - exec.move_asset_to_note dropw drop + exec.move_asset_to_note + drop # => [] # send non-fungible asset push.{RECIPIENT2} # recipient - push.{EXECUTION_HINT_2} # note execution hint push.{NOTETYPE2} # note_type - push.{aux2} # aux push.{tag2} # tag - exec.create_note - # => [note_idx] + exec.output_note::create + # => [note_idx = 1] push.{REMOVED_ASSET_3} # asset_3 - exec.move_asset_to_note dropw + exec.move_asset_to_note # => [note_idx] push.{REMOVED_ASSET_4} # asset_4 - exec.move_asset_to_note dropw drop + exec.move_asset_to_note + # => [note_idx] + + push.{ATTACHMENT2} + push.{attachment_scheme2} + movup.5 + exec.output_note::set_word_attachment # => [] # create a public note without assets push.{RECIPIENT3} # recipient - push.{EXECUTION_HINT_3} # note execution hint push.{NOTETYPE3} # note_type - push.{aux3} # aux push.{tag3} # tag - exec.create_note drop + exec.output_note::create + # => [note_idx = 2] + + push.{ATTACHMENT3} + push.{attachment_scheme3} + movup.5 + exec.output_note::set_array_attachment # => [] end ", @@ -338,19 +329,25 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { NOTETYPE1 = note_type1 as u8, NOTETYPE2 = note_type2 as u8, NOTETYPE3 = note_type3 as u8, - EXECUTION_HINT_1 = Felt::from(NoteExecutionHint::always()), - EXECUTION_HINT_2 = Felt::from(NoteExecutionHint::none()), - EXECUTION_HINT_3 = Felt::from(NoteExecutionHint::on_block_slot(11, 22, 33)), + attachment_scheme2 = attachment2.attachment_scheme().as_u32(), + ATTACHMENT2 = attachment2.content().to_word(), + attachment_scheme3 = attachment3.attachment_scheme().as_u32(), + ATTACHMENT3 = attachment3.content().to_word(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; // expected delta // -------------------------------------------------------------------------------------------- // execute the transaction and get the witness + let NoteAttachmentContent::Array(array) = attachment3.content() else { + panic!("expected array attachment"); + }; + let tx_context = TransactionContextBuilder::new(executor_account) .tx_script(tx_script) + .extend_advice_map(vec![(attachment3.content().to_word(), array.as_slice().to_vec())]) .extend_expected_output_notes(vec![ OutputNote::Full(expected_output_note_2.clone()), OutputNote::Full(expected_output_note_3.clone()), @@ -378,10 +375,10 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { let resulting_output_note_2 = executed_transaction.output_notes().get_note(1); let expected_note_id_2 = expected_output_note_2.id(); - let expected_note_metadata_2 = expected_output_note_2.metadata(); + let expected_note_metadata_2 = expected_output_note_2.metadata().clone(); assert_eq!( - NoteHeader::from(resulting_output_note_2), - NoteHeader::new(expected_note_id_2, *expected_note_metadata_2) + *resulting_output_note_2.header(), + NoteHeader::new(expected_note_id_2, expected_note_metadata_2) ); // assert that the expected output note 3 is present and has no assets @@ -413,17 +410,17 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { #[tokio::test] async fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { let source_code = r#" - use.miden::auth - use.miden::tx - const.AUTH_UNAUTHORIZED_EVENT=event("miden::auth::unauthorized") + use miden::standards::auth + use miden::protocol::tx + const AUTH_UNAUTHORIZED_EVENT=event("miden::auth::unauthorized") #! Inputs: [AUTH_ARGS, pad(12)] #! Outputs: [pad(16)] - export.auth_abort_tx + pub proc auth_abort_tx dropw # => [pad(16)] push.0.0 exec.tx::get_block_number - exec.::miden::native_account::incr_nonce + exec.::miden::protocol::native_account::incr_nonce # => [[final_nonce, block_num, 0, 0], pad(16)] # => [SALT, pad(16)] @@ -439,10 +436,12 @@ async fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { end "#; - let auth_component = - AccountComponent::compile(source_code, TransactionKernel::assembler(), vec![]) - .context("failed to compile auth component")? - .with_supports_all_types(); + let auth_code = CodeBuilder::default() + .compile_component_code("test::auth_component", source_code) + .context("failed to parse auth component")?; + let auth_component = AccountComponent::new(auth_code, vec![]) + .context("failed to parse auth component")? + .with_supports_all_types(); let account = AccountBuilder::new([42; 32]) .storage_mode(AccountStorageMode::Private) @@ -458,7 +457,7 @@ async fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { account.id(), vec![], NoteType::Private, - Felt::ZERO, + NoteAttachment::default(), &mut rng, )?; let input_note = create_spawn_note(vec![&output_note])?; @@ -501,7 +500,7 @@ async fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> account.id(), vec![], NoteType::Private, - Felt::ZERO, + NoteAttachment::default(), &mut rng, )?; let spawn_note = builder.add_spawn_note([&p2id_note])?; @@ -526,19 +525,19 @@ async fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> ); let summary_commitment = summary.to_commitment(); - let account_interface = AccountInterface::from(&account); + let account_interface = AccountInterface::from_account(&account); let pub_key = match account_interface.auth().first().unwrap() { - AuthScheme::RpoFalcon512 { pub_key } => pub_key, - AuthScheme::NoAuth => panic!("Expected RpoFalcon512 auth scheme, got NoAuth"), - AuthScheme::RpoFalcon512Multisig { .. } => { - panic!("Expected RpoFalcon512 auth scheme, got RpoFalcon512Multisig") + AuthScheme::Falcon512Rpo { pub_key } => pub_key, + AuthScheme::NoAuth => panic!("Expected Falcon512Rpo auth scheme, got NoAuth"), + AuthScheme::Falcon512RpoMultisig { .. } => { + panic!("Expected Falcon512Rpo auth scheme, got Falcon512RpoMultisig") }, - AuthScheme::Unknown => panic!("Expected RpoFalcon512 auth scheme, got Unknown"), + AuthScheme::Unknown => panic!("Expected Falcon512Rpo auth scheme, got Unknown"), AuthScheme::EcdsaK256Keccak { .. } => { - panic!("Expected RpoFalcon512 auth scheme, got EcdsaK256Keccak") + panic!("Expected Falcon512Rpo auth scheme, got EcdsaK256Keccak") }, AuthScheme::EcdsaK256KeccakMultisig { .. } => { - panic!("Expected RpoFalcon512 auth scheme, got EcdsaK256KeccakMultisig") + panic!("Expected Falcon512Rpo auth scheme, got EcdsaK256KeccakMultisig") }, }; @@ -565,7 +564,7 @@ async fn tx_summary_commitment_is_signed_by_ecdsa_auth() -> anyhow::Result<()> { account.id(), vec![], NoteType::Private, - Felt::ZERO, + NoteAttachment::default(), &mut rng, )?; let spawn_note = builder.add_spawn_note([&p2id_note])?; @@ -590,19 +589,19 @@ async fn tx_summary_commitment_is_signed_by_ecdsa_auth() -> anyhow::Result<()> { ); let summary_commitment = summary.to_commitment(); - let account_interface = AccountInterface::from(&account); + let account_interface = AccountInterface::from_account(&account); let pub_key = match account_interface.auth().first().unwrap() { AuthScheme::EcdsaK256Keccak { pub_key } => pub_key, AuthScheme::EcdsaK256KeccakMultisig { .. } => { panic!("Expected EcdsaK256Keccak auth scheme, got EcdsaK256KeccakMultisig") }, AuthScheme::NoAuth => panic!("Expected EcdsaK256Keccak auth scheme, got NoAuth"), - AuthScheme::RpoFalcon512Multisig { .. } => { - panic!("Expected EcdsaK256Keccak auth scheme, got RpoFalcon512Multisig") + AuthScheme::Falcon512RpoMultisig { .. } => { + panic!("Expected EcdsaK256Keccak auth scheme, got Falcon512RpoMultisig") }, AuthScheme::Unknown => panic!("Expected EcdsaK256Keccak auth scheme, got Unknown"), - AuthScheme::RpoFalcon512 { .. } => { - panic!("Expected EcdsaK256Keccak auth scheme, got RpoFalcon512") + AuthScheme::Falcon512Rpo { .. } => { + panic!("Expected EcdsaK256Keccak auth scheme, got Falcon512Rpo") }, }; @@ -621,7 +620,7 @@ async fn tx_summary_commitment_is_signed_by_ecdsa_auth() -> anyhow::Result<()> { #[tokio::test] async fn execute_tx_view_script() -> anyhow::Result<()> { let test_module_source = " - export.foo + pub proc foo push.3.4 add swapw dropw @@ -635,8 +634,8 @@ async fn execute_tx_view_script() -> anyhow::Result<()> { let library = assembler.assemble_library([source]).unwrap(); let source = " - use.test::module_1 - use.std::sys + use test::module_1 + use miden::core::sys begin push.1.2 @@ -645,7 +644,7 @@ async fn execute_tx_view_script() -> anyhow::Result<()> { end "; - let tx_script = ScriptBuilder::new(false) + let tx_script = CodeBuilder::new() .with_statically_linked_library(&library)? .compile_tx_script(source)?; let tx_context = TransactionContextBuilder::with_existing_mock_account() @@ -677,9 +676,7 @@ async fn test_tx_script_inputs() -> anyhow::Result<()> { let tx_script_input_key = Word::from([9999, 8888, 9999, 8888u32]); let tx_script_input_value = Word::from([9, 8, 7, 6u32]); let tx_script_src = format!( - " - use.miden::account - + r#" begin # push the tx script input key onto the stack push.{tx_script_input_key} @@ -688,12 +685,12 @@ async fn test_tx_script_inputs() -> anyhow::Result<()> { adv.push_mapval adv_loadw # assert that the value is correct - push.{tx_script_input_value} assert_eqw + push.{tx_script_input_value} assert_eqw.err="tx script input value mismatch" end - " + "#, ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; let tx_context = TransactionContextBuilder::with_existing_mock_account() .tx_script(tx_script) @@ -711,8 +708,6 @@ async fn test_tx_script_args() -> anyhow::Result<()> { let tx_script_args = Word::from([1, 2, 3, 4u32]); let tx_script_src = r#" - use.miden::account - begin # => [TX_SCRIPT_ARGS] # `TX_SCRIPT_ARGS` value is a user provided word, which could be used during the @@ -731,9 +726,9 @@ async fn test_tx_script_args() -> anyhow::Result<()> { push.5.6.7.8 assert_eqw.err="obtained advice map value doesn't match the expected one" end"#; - let tx_script = ScriptBuilder::default() + let tx_script = CodeBuilder::default() .compile_tx_script(tx_script_src) - .context("failed to compile transaction script")?; + .context("failed to parse transaction script")?; // extend the advice map with the entry that is accessed using the provided transaction script // argument @@ -755,10 +750,10 @@ async fn test_tx_script_args() -> anyhow::Result<()> { // part of the transaction advice inputs #[tokio::test] async fn inputs_created_correctly() -> anyhow::Result<()> { - let account_code_script = r#" - adv_map.A([6,7,8,9])=[10,11,12,13] + let account_component_masm = r#" + adv_map A([6,7,8,9]) = [10,11,12,13] - export.assert_adv_map + pub proc assert_adv_map # test tx script advice map push.[1,2,3,4] adv.push_mapval adv_loadw @@ -766,11 +761,12 @@ async fn inputs_created_correctly() -> anyhow::Result<()> { assert_eqw.err="script adv map not found" end "#; + let component_code = CodeBuilder::default() + .compile_component_code("test::adv_map_component", account_component_masm)?; - let component = AccountComponent::compile( - account_code_script, - TransactionKernel::assembler(), - vec![StorageSlot::Value(Word::default())], + let component = AccountComponent::new( + component_code.clone(), + vec![StorageSlot::with_value(StorageSlotName::mock(0), Word::default())], )? .with_supports_all_types(); @@ -779,27 +775,23 @@ async fn inputs_created_correctly() -> anyhow::Result<()> { AccountType::RegularAccountUpdatableCode, )?; - let script = format!( - r#" - use.miden::account - - adv_map.A([1,2,3,4])=[5,6,7,8] + let script = r#" + adv_map A([1,2,3,4]) = [5,6,7,8] begin + call.::test::adv_map_component::assert_adv_map + # test account code advice map push.[6,7,8,9] adv.push_mapval adv_loadw push.[10,11,12,13] assert_eqw.err="account code adv map not found" - - call.{assert_adv_map_proc_root} end - "#, - assert_adv_map_proc_root = - component.library().get_procedure_root_by_name("$anon::assert_adv_map").unwrap() - ); + "#; - let tx_script = ScriptBuilder::default().compile_tx_script(script)?; + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(component_code.as_library())? + .compile_tx_script(script)?; assert!(tx_script.mast().advice_map().get(&Word::try_from([1u64, 2, 3, 4])?).is_some()); assert!( diff --git a/crates/miden-testing/src/mock_chain/auth.rs b/crates/miden-testing/src/mock_chain/auth.rs index 7de10857f8..c65dbfd5e6 100644 --- a/crates/miden-testing/src/mock_chain/auth.rs +++ b/crates/miden-testing/src/mock_chain/auth.rs @@ -2,23 +2,26 @@ // ================================================================================================ use alloc::vec::Vec; -use miden_lib::account::auth::{ +use miden_protocol::Word; +use miden_protocol::account::AccountComponent; +use miden_protocol::account::auth::{AuthSecretKey, PublicKeyCommitment}; +use miden_protocol::testing::noop_auth_component::NoopAuthComponent; +use miden_standards::account::auth::{ AuthEcdsaK256Keccak, AuthEcdsaK256KeccakAcl, AuthEcdsaK256KeccakAclConfig, AuthEcdsaK256KeccakMultisig, AuthEcdsaK256KeccakMultisigConfig, - AuthRpoFalcon512, - AuthRpoFalcon512Acl, - AuthRpoFalcon512AclConfig, - AuthRpoFalcon512Multisig, - AuthRpoFalcon512MultisigConfig, + AuthFalcon512Rpo, + AuthFalcon512RpoAcl, + AuthFalcon512RpoAclConfig, + AuthFalcon512RpoMultisig, + AuthFalcon512RpoMultisigConfig, +}; +use miden_standards::testing::account_component::{ + ConditionalAuthComponent, + IncrNonceAuthComponent, }; -use miden_lib::testing::account_component::{ConditionalAuthComponent, IncrNonceAuthComponent}; -use miden_objects::Word; -use miden_objects::account::AccountComponent; -use miden_objects::account::auth::{AuthSecretKey, PublicKeyCommitment}; -use miden_objects::testing::noop_auth_component::NoopAuthComponent; use miden_tx::auth::BasicAuthenticator; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -27,7 +30,7 @@ use rand_chacha::ChaCha20Rng; #[derive(Debug, Clone)] pub enum Auth { /// Creates a secret key for the account and creates a [BasicAuthenticator] used to - /// authenticate the account with [AuthRpoFalcon512]. + /// authenticate the account with [AuthFalcon512Rpo]. BasicAuth, /// Creates a secret key for the account and creates a [BasicAuthenticator] used to @@ -58,7 +61,7 @@ pub enum Auth { }, /// Creates a secret key for the account, and creates a [BasicAuthenticator] used to - /// authenticate the account with [AuthRpoFalcon512Acl]. Authentication will only be + /// authenticate the account with [AuthFalcon512RpoAcl]. Authentication will only be /// triggered if any of the procedures specified in the list are called during execution. Acl { auth_trigger_procedures: Vec, @@ -88,10 +91,10 @@ impl Auth { match self { Auth::BasicAuth => { let mut rng = ChaCha20Rng::from_seed(Default::default()); - let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let sec_key = AuthSecretKey::new_falcon512_rpo_with_rng(&mut rng); let pub_key = sec_key.public_key().to_commitment(); - let component = AuthRpoFalcon512::new(pub_key).into(); + let component = AuthFalcon512Rpo::new(pub_key).into(); let authenticator = BasicAuthenticator::new(&[sec_key]); (component, Some(authenticator)) @@ -123,10 +126,10 @@ impl Auth { let pub_keys: Vec<_> = approvers.iter().map(|word| PublicKeyCommitment::from(*word)).collect(); - let config = AuthRpoFalcon512MultisigConfig::new(pub_keys, *threshold) + let config = AuthFalcon512RpoMultisigConfig::new(pub_keys, *threshold) .and_then(|cfg| cfg.with_proc_thresholds(proc_threshold_map.clone())) .expect("invalid multisig config"); - let component = AuthRpoFalcon512Multisig::new(config) + let component = AuthFalcon512RpoMultisig::new(config) .expect("multisig component creation failed") .into(); @@ -138,12 +141,12 @@ impl Auth { allow_unauthorized_input_notes, } => { let mut rng = ChaCha20Rng::from_seed(Default::default()); - let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let sec_key = AuthSecretKey::new_falcon512_rpo_with_rng(&mut rng); let pub_key = sec_key.public_key().to_commitment(); - let component = AuthRpoFalcon512Acl::new( + let component = AuthFalcon512RpoAcl::new( pub_key, - AuthRpoFalcon512AclConfig::new() + AuthFalcon512RpoAclConfig::new() .with_auth_trigger_procedures(auth_trigger_procedures.clone()) .with_allow_unauthorized_output_notes(*allow_unauthorized_output_notes) .with_allow_unauthorized_input_notes(*allow_unauthorized_input_notes), diff --git a/crates/miden-testing/src/mock_chain/chain.rs b/crates/miden-testing/src/mock_chain/chain.rs index 39b5a7d688..fe75c97fc4 100644 --- a/crates/miden-testing/src/mock_chain/chain.rs +++ b/crates/miden-testing/src/mock_chain/chain.rs @@ -2,25 +2,26 @@ use alloc::collections::{BTreeMap, BTreeSet}; use alloc::vec::Vec; use anyhow::Context; -use miden_block_prover::{LocalBlockProver, ProvenBlockError}; -use miden_objects::account::auth::AuthSecretKey; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{Account, AccountId, PartialAccount}; -use miden_objects::batch::{ProposedBatch, ProvenBatch}; -use miden_objects::block::account_tree::AccountTree; -use miden_objects::block::{ - AccountWitness, +use miden_block_prover::LocalBlockProver; +use miden_processor::DeserializationError; +use miden_protocol::MIN_PROOF_SECURITY_LEVEL; +use miden_protocol::account::auth::{AuthSecretKey, PublicKey}; +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{Account, AccountId, PartialAccount}; +use miden_protocol::batch::{ProposedBatch, ProvenBatch}; +use miden_protocol::block::account_tree::{AccountTree, AccountWitness}; +use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness}; +use miden_protocol::block::{ BlockHeader, BlockInputs, BlockNumber, Blockchain, - NullifierTree, - NullifierWitness, ProposedBlock, ProvenBlock, }; -use miden_objects::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier}; -use miden_objects::transaction::{ +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier}; +use miden_protocol::transaction::{ ExecutedTransaction, InputNote, InputNotes, @@ -29,7 +30,6 @@ use miden_objects::transaction::{ ProvenTransaction, TransactionInputs, }; -use miden_processor::DeserializationError; use miden_tx::LocalTransactionProver; use miden_tx::auth::BasicAuthenticator; use miden_tx::utils::{ByteReader, Deserializable, Serializable}; @@ -63,7 +63,7 @@ use crate::{MockChainBuilder, TransactionContextBuilder}; /// ## Executing a simple transaction /// ``` /// # use anyhow::Result; -/// # use miden_objects::{ +/// # use miden_protocol::{ /// # asset::{Asset, FungibleAsset}, /// # note::NoteType, /// # }; @@ -127,7 +127,7 @@ use crate::{MockChainBuilder, TransactionContextBuilder}; /// /// ``` /// # use anyhow::Result; -/// # use miden_objects::{Felt, asset::{Asset, FungibleAsset}, note::NoteType}; +/// # use miden_protocol::{Felt, asset::{Asset, FungibleAsset}, note::NoteType}; /// # use miden_testing::{Auth, MockChain, TransactionContextBuilder}; /// # /// # #[tokio::main(flavor = "current_thread")] @@ -183,6 +183,9 @@ pub struct MockChain { /// AccountId |-> AccountAuthenticator mapping to store the authenticator for accounts to /// simplify transaction creation. account_authenticators: BTreeMap, + + /// Validator secret key used for signing blocks. + validator_secret_key: SecretKey, } impl MockChain { @@ -214,6 +217,7 @@ impl MockChain { genesis_block: ProvenBlock, account_tree: AccountTree, account_authenticators: BTreeMap, + secret_key: SecretKey, ) -> anyhow::Result { let mut chain = MockChain { chain: Blockchain::default(), @@ -224,6 +228,7 @@ impl MockChain { committed_notes: BTreeMap::new(), committed_accounts: BTreeMap::new(), account_authenticators, + validator_secret_key: secret_key, }; // We do not have to apply the tree changes, because the account tree is already initialized @@ -370,6 +375,13 @@ impl MockChain { self.blocks[chain_tip.as_usize()].header().clone() } + /// Returns the latest [`ProvenBlock`] in the chain. + pub fn latest_block(&self) -> ProvenBlock { + let chain_tip = + self.chain.chain_tip().expect("chain should contain at least the genesis block"); + self.blocks[chain_tip.as_usize()].clone() + } + /// Returns the [`BlockHeader`] with the specified `block_number`. /// /// # Panics @@ -511,16 +523,6 @@ impl MockChain { self.propose_block_at(batches, timestamp) } - /// Mock-proves a proposed block into a proven block and returns it. - /// - /// This method does not modify the chain state. - pub fn prove_block( - &self, - proposed_block: ProposedBlock, - ) -> Result { - LocalBlockProver::new(0).prove_dummy(proposed_block) - } - // TRANSACTION APIS // ---------------------------------------------------------------------------------------- @@ -711,9 +713,8 @@ impl MockChain { /// Gets foreign account inputs to execute FPI transactions. /// - /// Only used internally and so does not need to be public. - #[cfg(test)] - pub(crate) fn get_foreign_account_inputs( + /// Used in tests to get foreign account inputs for FPI calls. + pub fn get_foreign_account_inputs( &self, account_id: AccountId, ) -> anyhow::Result<(Account, AccountWitness)> { @@ -847,13 +848,13 @@ impl MockChain { /// - Consumed notes are removed from the committed notes. /// - The block is appended to the [`BlockChain`] and the list of proven blocks. fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> { - for account_update in proven_block.updated_accounts() { + for account_update in proven_block.body().updated_accounts() { self.account_tree .insert(account_update.account_id(), account_update.final_state_commitment()) .context("failed to insert account update into account tree")?; } - for nullifier in proven_block.created_nullifiers() { + for nullifier in proven_block.body().created_nullifiers() { self.nullifier_tree .mark_spent(*nullifier, proven_block.header().block_num()) .context("failed to mark block nullifier as spent")?; @@ -863,7 +864,7 @@ impl MockChain { // nullifiers, so we'll have to create a second index to do this. } - for account_update in proven_block.updated_accounts() { + for account_update in proven_block.body().updated_accounts() { match account_update.details() { AccountUpdateDetails::Delta(account_delta) => { if account_delta.is_full_state() { @@ -888,8 +889,8 @@ impl MockChain { } } - let notes_tree = proven_block.build_output_note_tree(); - for (block_note_index, created_note) in proven_block.output_notes() { + let notes_tree = proven_block.body().compute_block_note_tree(); + for (block_note_index, created_note) in proven_block.body().output_notes() { let note_path = notes_tree.open(block_note_index); let note_inclusion_proof = NoteInclusionProof::new( proven_block.header().block_num(), @@ -906,7 +907,7 @@ impl MockChain { created_note.id(), MockChainNote::Private( created_note.id(), - *created_note.metadata(), + created_note.metadata().clone(), note_inclusion_proof, ), ); @@ -969,9 +970,9 @@ impl MockChain { timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS); let proposed_block = self - .propose_block_at(batches, block_timestamp) + .propose_block_at(batches.clone(), block_timestamp) .context("failed to create proposed block")?; - let proven_block = self.prove_block(proposed_block).context("failed to prove block")?; + let proven_block = self.prove_block(proposed_block.clone())?; // Apply block. // ---------------------------------------------------------------------------------------- @@ -980,6 +981,19 @@ impl MockChain { Ok(proven_block) } + + /// Proves proposed block alongside a corresponding list of batches. + pub fn prove_block(&self, proposed_block: ProposedBlock) -> anyhow::Result { + let (header, body) = proposed_block.clone().into_header_and_body()?; + let inputs = self.get_block_inputs(proposed_block.batches().as_slice())?; + let block_proof = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL).prove_dummy( + proposed_block.batches().clone(), + header.clone(), + inputs, + )?; + let signature = self.validator_secret_key.sign(header.commitment()); + Ok(ProvenBlock::new_unchecked(header, body, signature, block_proof)) + } } impl Default for MockChain { @@ -1001,6 +1015,7 @@ impl Serializable for MockChain { self.committed_accounts.write_into(target); self.committed_notes.write_into(target); self.account_authenticators.write_into(target); + self.validator_secret_key.write_into(target); } } @@ -1015,6 +1030,7 @@ impl Deserializable for MockChain { let committed_notes = BTreeMap::::read_from(source)?; let account_authenticators = BTreeMap::::read_from(source)?; + let secret_key = SecretKey::read_from(source)?; Ok(Self { chain, @@ -1025,6 +1041,7 @@ impl Deserializable for MockChain { committed_notes, committed_accounts, account_authenticators, + validator_secret_key: secret_key, }) } } @@ -1084,9 +1101,9 @@ impl Serializable for AccountAuthenticator { impl Deserializable for AccountAuthenticator { fn read_from(source: &mut R) -> Result { - let authenticator = Option::>::read_from(source)?; + let authenticator = Option::>::read_from(source)?; - let authenticator = authenticator.map(|keys| BasicAuthenticator::new(&keys)); + let authenticator = authenticator.map(|keys| BasicAuthenticator::from_key_pairs(&keys)); Ok(Self { authenticator }) } @@ -1131,15 +1148,15 @@ impl From for TxContextInput { #[cfg(test)] mod tests { - use miden_lib::account::wallets::BasicWallet; - use miden_objects::account::{AccountBuilder, AccountStorageMode}; - use miden_objects::asset::{Asset, FungibleAsset}; - use miden_objects::note::NoteType; - use miden_objects::testing::account_id::{ + use miden_protocol::account::{AccountBuilder, AccountStorageMode}; + use miden_protocol::asset::{Asset, FungibleAsset}; + use miden_protocol::note::NoteType; + use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_SENDER, }; + use miden_standards::account::wallets::BasicWallet; use super::*; use crate::Auth; @@ -1256,4 +1273,36 @@ mod tests { assert_eq!(chain.committed_notes, deserialized.committed_notes); assert_eq!(chain.account_authenticators, deserialized.account_authenticators); } + + #[test] + fn mock_chain_block_signature() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + builder.add_existing_mock_account(Auth::IncrNonce)?; + let mut chain = builder.build()?; + + // Verify the genesis block signature. + let genesis_block = chain.latest_block(); + assert!( + genesis_block.signature().verify( + genesis_block.header().commitment(), + genesis_block.header().validator_key() + ) + ); + + // Add another block. + chain.prove_next_block()?; + + // Verify the next block signature. + let next_block = chain.latest_block(); + assert!( + next_block + .signature() + .verify(next_block.header().commitment(), next_block.header().validator_key()) + ); + + // Public keys should be carried through from the genesis header to the next. + assert_eq!(next_block.header().validator_key(), next_block.header().validator_key()); + + Ok(()) + } } diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 93aeafa53a..19f014e18c 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -13,39 +13,45 @@ const DEFAULT_FAUCET_DECIMALS: u8 = 10; // ================================================================================================ use itertools::Itertools; -use miden_lib::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::note::{create_p2id_note, create_p2ide_note, create_swap_note}; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::transaction::{TransactionKernel, memory}; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{ +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{ Account, AccountBuilder, AccountDelta, AccountId, + AccountStorage, AccountStorageMode, AccountType, StorageSlot, }; -use miden_objects::asset::{Asset, FungibleAsset, TokenSymbol}; -use miden_objects::block::account_tree::AccountTree; -use miden_objects::block::{ +use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol}; +use miden_protocol::block::account_tree::AccountTree; +use miden_protocol::block::nullifier_tree::NullifierTree; +use miden_protocol::block::{ BlockAccountUpdate, + BlockBody, BlockHeader, BlockNoteTree, BlockNumber, + BlockProof, Blockchain, FeeParameters, - NullifierTree, OutputNoteBatch, ProvenBlock, }; -use miden_objects::note::{Note, NoteDetails, NoteType}; -use miden_objects::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; -use miden_objects::transaction::{OrderedTransactionHeaders, OutputNote}; -use miden_objects::{Felt, FieldElement, MAX_OUTPUT_NOTES_PER_BATCH, NoteError, Word, ZERO}; -use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; +use miden_protocol::crypto::merkle::smt::Smt; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{Note, NoteAttachment, NoteDetails, NoteType}; +use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; +use miden_protocol::testing::random_signer::RandomBlockSigner; +use miden_protocol::transaction::{OrderedTransactionHeaders, OutputNote, TransactionKernel}; +use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word, ZERO}; +use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::note::{create_p2id_note, create_p2ide_note, create_swap_note}; +use miden_standards::testing::account_component::MockAccountComponent; use rand::Rng; use crate::mock_chain::chain::AccountAuthenticator; @@ -58,7 +64,7 @@ use crate::{AccountState, Auth, MockChain}; /// /// ``` /// # use anyhow::Result; -/// # use miden_objects::{ +/// # use miden_protocol::{ /// # asset::{Asset, FungibleAsset}, /// # note::NoteType, /// # }; @@ -209,14 +215,15 @@ impl MockChainBuilder { let block_num = BlockNumber::from(0u32); let chain_commitment = Blockchain::new().commitment(); let account_root = account_tree.root(); - let nullifier_root = NullifierTree::new().root(); + let nullifier_root = NullifierTree::::default().root(); let note_root = note_tree.root(); let tx_commitment = transactions.commitment(); let tx_kernel_commitment = TransactionKernel.to_commitment(); - let proof_commitment = Word::empty(); let timestamp = MockChain::TIMESTAMP_START_SECS; let fee_parameters = FeeParameters::new(self.native_asset_id, self.verification_base_fee) .context("failed to construct fee parameters")?; + let validator_secret_key = SecretKey::random(); + let validator_public_key = validator_secret_key.public_key(); let header = BlockHeader::new( version, @@ -228,20 +235,28 @@ impl MockChainBuilder { note_root, tx_commitment, tx_kernel_commitment, - proof_commitment, + validator_public_key, fee_parameters, timestamp, ); - let genesis_block = ProvenBlock::new_unchecked( - header, + let body = BlockBody::new_unchecked( block_account_updates, output_note_batches, created_nullifiers, transactions, ); - MockChain::from_genesis_block(genesis_block, account_tree, self.account_authenticators) + let signature = validator_secret_key.sign(header.commitment()); + let block_proof = BlockProof::new_dummy(); + let genesis_block = ProvenBlock::new_unchecked(header, body, signature, block_proof); + + MockChain::from_genesis_block( + genesis_block, + account_tree, + self.account_authenticators, + validator_secret_key, + ) } // ACCOUNT METHODS @@ -333,13 +348,13 @@ impl MockChainBuilder { let mut account = self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)?; - // The faucet's reserved slot is initialized to an empty word by default. + // The faucet's sysdata slot is initialized to an empty word by default. // If total_issuance is set, overwrite it and reinsert the account. if let Some(issuance) = total_issuance { account .storage_mut() .set_item( - memory::FAUCET_STORAGE_DATA_SLOT, + AccountStorage::faucet_sysdata_slot(), Word::from([ZERO, ZERO, ZERO, Felt::new(issuance)]), ) .context("failed to set faucet storage")?; @@ -377,13 +392,13 @@ impl MockChainBuilder { let mut account = self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)?; - // The faucet's reserved slot is initialized to an empty word by default. + // The faucet's sysdata slot is initialized to an empty word by default. // If total_issuance is set, overwrite it and reinsert the account. if let Some(issuance) = total_issuance { account .storage_mut() .set_item( - memory::FAUCET_STORAGE_DATA_SLOT, + AccountStorage::faucet_sysdata_slot(), Word::from([ZERO, ZERO, ZERO, Felt::new(issuance)]), ) .context("failed to set faucet storage")?; @@ -539,7 +554,7 @@ impl MockChainBuilder { target_account_id, asset.to_vec(), note_type, - Felt::ZERO, + NoteAttachment::default(), &mut self.rng, )?; self.add_output_note(OutputNote::Full(note.clone())); @@ -590,9 +605,9 @@ impl MockChainBuilder { offered_asset, requested_asset, NoteType::Public, - Felt::ZERO, + NoteAttachment::default(), payback_note_type, - Felt::ZERO, + NoteAttachment::default(), &mut self.rng, )?; diff --git a/crates/miden-testing/src/mock_chain/note.rs b/crates/miden-testing/src/mock_chain/note.rs index 799131c6ad..759ef257ea 100644 --- a/crates/miden-testing/src/mock_chain/note.rs +++ b/crates/miden-testing/src/mock_chain/note.rs @@ -1,6 +1,6 @@ -use miden_objects::note::{Note, NoteId, NoteInclusionProof, NoteMetadata}; -use miden_objects::transaction::InputNote; use miden_processor::DeserializationError; +use miden_protocol::note::{Note, NoteId, NoteInclusionProof, NoteMetadata}; +use miden_protocol::transaction::InputNote; use miden_tx::utils::{ByteReader, Deserializable, Serializable}; use winterfell::ByteWriter; diff --git a/crates/miden-testing/src/mock_host.rs b/crates/miden-testing/src/mock_host.rs index ded9a6e663..0bcb1d766f 100644 --- a/crates/miden-testing/src/mock_host.rs +++ b/crates/miden-testing/src/mock_host.rs @@ -2,9 +2,6 @@ use alloc::collections::BTreeSet; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::StdLibrary; -use miden_lib::transaction::{EventId, TransactionEvent}; -use miden_objects::Word; use miden_processor::{ AdviceMutation, AsyncHost, @@ -14,6 +11,9 @@ use miden_processor::{ MastForest, ProcessState, }; +use miden_protocol::transaction::TransactionEventId; +use miden_protocol::vm::EventId; +use miden_protocol::{CoreLibrary, Word}; use miden_tx::TransactionExecutorHost; use miden_tx::auth::UnreachableAuth; @@ -50,24 +50,24 @@ impl<'store> MockHost<'store> { pub fn new( exec_host: TransactionExecutorHost<'store, 'static, TransactionContext, UnreachableAuth>, ) -> Self { - // StdLibrary events are always handled. - let stdlib_handlers = StdLibrary::default() + // CoreLibrary events are always handled. + let core_lib_handlers = CoreLibrary::default() .handlers() .into_iter() .map(|(handler_event_name, _)| handler_event_name.to_event_id()); - let mut handled_events = BTreeSet::from_iter(stdlib_handlers); + let mut handled_events = BTreeSet::from_iter(core_lib_handlers); // The default set of transaction events that are always handled. handled_events.extend( [ - &TransactionEvent::AccountPushProcedureIndex, - &TransactionEvent::LinkMapSet, - &TransactionEvent::LinkMapGet, + &TransactionEventId::AccountPushProcedureIndex, + &TransactionEventId::LinkMapSet, + &TransactionEventId::LinkMapGet, // TODO: It should be possible to remove this after implementing // https://github.com/0xMiden/miden-base/issues/1852. - &TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount, + &TransactionEventId::EpilogueBeforeTxFeeRemovedFromAccount, ] - .map(TransactionEvent::event_id), + .map(TransactionEventId::event_id), ); Self { exec_host, handled_events } @@ -77,15 +77,15 @@ impl<'store> MockHost<'store> { pub fn enable_lazy_loading(&mut self) { self.handled_events.extend( [ - &TransactionEvent::AccountBeforeForeignLoad, - &TransactionEvent::AccountVaultBeforeGetBalance, - &TransactionEvent::AccountVaultBeforeHasNonFungibleAsset, - &TransactionEvent::AccountVaultBeforeAddAsset, - &TransactionEvent::AccountVaultBeforeRemoveAsset, - &TransactionEvent::AccountStorageBeforeSetMapItem, - &TransactionEvent::AccountStorageBeforeGetMapItem, + &TransactionEventId::AccountBeforeForeignLoad, + &TransactionEventId::AccountVaultBeforeGetBalance, + &TransactionEventId::AccountVaultBeforeHasNonFungibleAsset, + &TransactionEventId::AccountVaultBeforeAddAsset, + &TransactionEventId::AccountVaultBeforeRemoveAsset, + &TransactionEventId::AccountStorageBeforeSetMapItem, + &TransactionEventId::AccountStorageBeforeGetMapItem, ] - .map(TransactionEvent::event_id), + .map(TransactionEventId::event_id), ); } } @@ -93,10 +93,10 @@ impl<'store> MockHost<'store> { impl<'store> BaseHost for MockHost<'store> { fn get_label_and_source_file( &self, - location: &miden_objects::assembly::debuginfo::Location, + location: &miden_protocol::assembly::debuginfo::Location, ) -> ( - miden_objects::assembly::debuginfo::SourceSpan, - Option>, + miden_protocol::assembly::debuginfo::SourceSpan, + Option>, ) { self.exec_host.get_label_and_source_file(location) } diff --git a/crates/miden-testing/src/tx_context/builder.rs b/crates/miden-testing/src/tx_context/builder.rs index 8e524171f9..453bb2c73e 100644 --- a/crates/miden-testing/src/tx_context/builder.rs +++ b/crates/miden-testing/src/tx_context/builder.rs @@ -6,24 +6,24 @@ use alloc::sync::Arc; use alloc::vec::Vec; use anyhow::Context; -use miden_lib::testing::account_component::IncrNonceAuthComponent; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_objects::EMPTY_WORD; -use miden_objects::account::auth::{PublicKeyCommitment, Signature}; -use miden_objects::account::{Account, AccountHeader, AccountId}; -use miden_objects::assembly::DefaultSourceManager; -use miden_objects::assembly::debuginfo::SourceManagerSync; -use miden_objects::block::AccountWitness; -use miden_objects::note::{Note, NoteId, NoteScript}; -use miden_objects::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; -use miden_objects::testing::noop_auth_component::NoopAuthComponent; -use miden_objects::transaction::{ +use miden_processor::{AdviceInputs, Felt, Word}; +use miden_protocol::EMPTY_WORD; +use miden_protocol::account::auth::{PublicKeyCommitment, Signature}; +use miden_protocol::account::{Account, AccountHeader, AccountId}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::assembly::debuginfo::SourceManagerSync; +use miden_protocol::block::account_tree::AccountWitness; +use miden_protocol::note::{Note, NoteId, NoteScript}; +use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; +use miden_protocol::testing::noop_auth_component::NoopAuthComponent; +use miden_protocol::transaction::{ OutputNote, TransactionArgs, TransactionInputs, TransactionScript, }; -use miden_processor::{AdviceInputs, Felt, Word}; +use miden_standards::testing::account_component::IncrNonceAuthComponent; +use miden_standards::testing::mock_account::MockAccountExt; use miden_tx::TransactionMastStore; use miden_tx::auth::BasicAuthenticator; @@ -43,16 +43,16 @@ use crate::{MockChain, MockChainNote}; /// ``` /// # use anyhow::Result; /// # use miden_testing::TransactionContextBuilder; -/// # use miden_objects::{account::AccountBuilder,Felt, FieldElement}; -/// # use miden_lib::transaction::TransactionKernel; +/// # use miden_protocol::{account::AccountBuilder,Felt, FieldElement}; +/// # use miden_protocol::transaction::TransactionKernel; /// # /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() -> Result<()> { /// let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; /// /// let code = " -/// use.$kernel::prologue -/// use.mock::account +/// use $kernel::prologue +/// use mock::account /// /// begin /// exec.prologue::prepare_transaction @@ -109,11 +109,11 @@ impl TransactionContextBuilder { /// /// The wallet: /// - /// - Includes a series of mocked assets ([miden_objects::asset::AssetVault::mock()]). + /// - Includes a series of mocked assets ([miden_protocol::asset::AssetVault::mock()]). /// - Has a nonce of `1` (so it does not imply seed validation). /// - Has an ID of [`ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE`]. /// - Has an account code based on an - /// [miden_lib::testing::account_component::MockAccountComponent]. + /// [miden_standards::testing::account_component::MockAccountComponent]. pub fn with_existing_mock_account() -> Self { Self::new(Account::mock( ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, diff --git a/crates/miden-testing/src/tx_context/context.rs b/crates/miden-testing/src/tx_context/context.rs index d09ffc069c..274b16672b 100644 --- a/crates/miden-testing/src/tx_context/context.rs +++ b/crates/miden-testing/src/tx_context/context.rs @@ -3,14 +3,22 @@ use alloc::collections::{BTreeMap, BTreeSet}; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::TransactionKernel; -use miden_objects::account::{Account, AccountId, PartialAccount, StorageMapWitness, StorageSlot}; -use miden_objects::assembly::debuginfo::{SourceLanguage, Uri}; -use miden_objects::assembly::{SourceManager, SourceManagerSync}; -use miden_objects::asset::{AssetVaultKey, AssetWitness}; -use miden_objects::block::{AccountWitness, BlockHeader, BlockNumber}; -use miden_objects::note::{Note, NoteScript}; -use miden_objects::transaction::{ +use miden_processor::fast::ExecutionOutput; +use miden_processor::{ExecutionError, FutureMaybeSend, MastForest, MastForestStore, Word}; +use miden_protocol::account::{ + Account, + AccountId, + PartialAccount, + StorageMapWitness, + StorageSlotContent, +}; +use miden_protocol::assembly::debuginfo::{SourceLanguage, Uri}; +use miden_protocol::assembly::{Assembler, SourceManager, SourceManagerSync}; +use miden_protocol::asset::{Asset, AssetVaultKey, AssetWitness}; +use miden_protocol::block::account_tree::AccountWitness; +use miden_protocol::block::{BlockHeader, BlockNumber}; +use miden_protocol::note::{Note, NoteScript}; +use miden_protocol::transaction::{ AccountInputs, ExecutedTransaction, InputNote, @@ -18,9 +26,9 @@ use miden_objects::transaction::{ PartialBlockchain, TransactionArgs, TransactionInputs, + TransactionKernel, }; -use miden_processor::fast::ExecutionOutput; -use miden_processor::{ExecutionError, FutureMaybeSend, MastForest, MastForestStore, Word}; +use miden_standards::code_builder::CodeBuilder; use miden_tx::auth::{BasicAuthenticator, UnreachableAuth}; use miden_tx::{ AccountProcedureIndexMap, @@ -59,15 +67,14 @@ impl TransactionContext { /// Executes arbitrary code within the context of a mocked transaction environment and returns /// the resulting [`ExecutionOutput`]. /// - /// The code is compiled with the assembler returned by - /// [`TransactionKernel::with_mock_libraries`] and executed with advice inputs constructed from - /// the data stored in the context. The program is run on a modified [`TransactionExecutorHost`] - /// which is loaded with the procedures exposed by the transaction kernel, and also - /// individual kernel functions (not normally exposed). + /// The code is compiled with the assembler built by [`CodeBuilder::with_mock_libraries`] + /// and executed with advice inputs constructed from the data stored in the context. The program + /// is run on a modified [`TransactionExecutorHost`] which is loaded with the procedures exposed + /// by the transaction kernel, and also individual kernel functions (not normally exposed). /// /// To improve the error message quality, convert the returned [`ExecutionError`] into a - /// [`Report`](miden_objects::assembly::diagnostics::Report) or use `?` with - /// [`miden_objects::assembly::diagnostics::Result`]. + /// [`Report`](miden_protocol::assembly::diagnostics::Report) or use `?` with + /// [`miden_protocol::assembly::diagnostics::Result`]. /// /// # Errors /// @@ -77,8 +84,42 @@ impl TransactionContext { /// /// - If the provided `code` is not a valid program. pub async fn execute_code(&self, code: &str) -> Result { - let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&self.tx_inputs) - .expect("error initializing transaction inputs"); + // Fetch all witnesses for note assets and the fee asset. + let mut asset_vault_keys = self + .tx_inputs + .input_notes() + .iter() + .flat_map(|note| note.note().assets().iter().map(Asset::vault_key)) + .collect::>(); + let fee_asset_vault_key = AssetVaultKey::from_account_id( + self.tx_inputs().block_header().fee_parameters().native_asset_id(), + ) + .expect("fee asset should be a fungible asset"); + asset_vault_keys.extend([fee_asset_vault_key]); + + let (account, block_header, _blockchain) = self + .get_transaction_inputs( + self.tx_inputs.account().id(), + BTreeSet::from_iter([self.tx_inputs.block_header().block_num()]), + ) + .await + .expect("failed to fetch transaction inputs"); + + // Add the vault key for the fee asset to the list of asset vault keys which may need to be + // accessed at the end of the transaction. + let fee_asset_vault_key = + AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id()) + .expect("fee asset should be a fungible asset"); + asset_vault_keys.insert(fee_asset_vault_key); + + // Fetch the witnesses for all asset vault keys. + let asset_witnesses = self + .get_vault_asset_witnesses(account.id(), account.vault().root(), asset_vault_keys) + .await + .expect("failed to fetch asset witnesses"); + + let tx_inputs = self.tx_inputs.clone().with_asset_witnesses(asset_witnesses); + let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs); // Virtual file name should be unique. let virtual_source_file = self.source_manager.load( @@ -87,10 +128,11 @@ impl TransactionContext { code.to_owned(), ); - let assembler = TransactionKernel::with_mock_libraries(self.source_manager.clone()) - .with_debug_mode(true); + let assembler: Assembler = + CodeBuilder::with_mock_libraries_with_source_manager(self.source_manager.clone()) + .into(); + let program = assembler - .with_debug_mode(true) .assemble_program(virtual_source_file) .expect("code was not well formed"); @@ -101,23 +143,25 @@ impl TransactionContext { self.mast_store.insert(program.mast_forest().clone()); let account_procedure_idx_map = AccountProcedureIndexMap::new( - [self.tx_inputs().account().code()] + [tx_inputs.account().code()] .into_iter() .chain(self.foreign_account_inputs.values().map(|(account, _)| account.code())), - ) - .expect("constructing account procedure index map should work"); + ); // The ref block is unimportant when using execute_code so we can set it to any value. - let ref_block = self.tx_inputs().block_header().block_num(); + let ref_block = tx_inputs.block_header().block_num(); let exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new( &PartialAccount::from(self.account()), - self.tx_inputs().input_notes().clone(), + tx_inputs.input_notes().clone(), self, ScriptMastForestStore::default(), account_procedure_idx_map, None, ref_block, + // We don't need to set the initial balance in this context under the assumption that + // fees are zero. + 0u64, self.source_manager(), ); @@ -190,15 +234,25 @@ impl DataStore for TransactionContext { fn get_transaction_inputs( &self, account_id: AccountId, - _ref_blocks: BTreeSet, + ref_blocks: BTreeSet, ) -> impl FutureMaybeSend> { + // Sanity checks assert_eq!(account_id, self.account().id()); assert_eq!(account_id, self.tx_inputs.account().id()); + assert_eq!( + ref_blocks + .last() + .copied() + .expect("at least the tx ref block should be provided"), + self.tx_inputs().blockchain().chain_length(), + "tx reference block should match partial blockchain length" + ); let account = self.tx_inputs.account().clone(); let block_header = self.tx_inputs.block_header().clone(); let blockchain = self.tx_inputs.blockchain().clone(); + async move { Ok((account, block_header, blockchain)) } } @@ -224,22 +278,21 @@ impl DataStore for TransactionContext { } } - fn get_vault_asset_witness( + fn get_vault_asset_witnesses( &self, account_id: AccountId, vault_root: Word, - asset_key: AssetVaultKey, - ) -> impl FutureMaybeSend> { + vault_keys: BTreeSet, + ) -> impl FutureMaybeSend, DataStoreError>> { async move { - if account_id == self.account().id() { + let asset_vault = if account_id == self.account().id() { if self.account().vault().root() != vault_root { return Err(DataStoreError::other(format!( "native account {account_id} has vault root {} but {vault_root} was requested", self.account().vault().root() ))); } - - Ok(self.account().vault().open(asset_key)) + self.account().vault() } else { let (foreign_account, _witness) = self .foreign_account_inputs @@ -261,9 +314,10 @@ impl DataStore for TransactionContext { foreign_account.vault().root() ))); } + foreign_account.vault() + }; - Ok(foreign_account.vault().open(asset_key)) - } + Ok(vault_keys.into_iter().map(|vault_key| asset_vault.open(vault_key)).collect()) } } @@ -281,8 +335,8 @@ impl DataStore for TransactionContext { .storage() .slots() .iter() - .find_map(|slot| match slot { - StorageSlot::Map(storage_map) if storage_map.root() == map_root => { + .find_map(|slot| match slot.content() { + StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => { Some(storage_map) }, _ => None, @@ -313,8 +367,8 @@ impl DataStore for TransactionContext { .storage() .slots() .iter() - .find_map(|slot| match slot { - StorageSlot::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)}, + .find_map(|slot| match slot.content() { + StorageSlotContent::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)}, _ => None, }) .ok_or_else(|| { @@ -331,13 +385,8 @@ impl DataStore for TransactionContext { fn get_note_script( &self, script_root: Word, - ) -> impl FutureMaybeSend> { - async move { - self.note_scripts - .get(&script_root) - .cloned() - .ok_or_else(|| DataStoreError::NoteScriptNotFound(script_root)) - } + ) -> impl FutureMaybeSend, DataStoreError>> { + async move { Ok(self.note_scripts.get(&script_root).cloned()) } } } @@ -352,9 +401,9 @@ impl MastForestStore for TransactionContext { #[cfg(test)] mod tests { - use miden_objects::Felt; - use miden_objects::assembly::Assembler; - use miden_objects::note::NoteScript; + use miden_protocol::Felt; + use miden_protocol::assembly::Assembler; + use miden_protocol::note::NoteScript; use super::*; use crate::TransactionContextBuilder; @@ -366,7 +415,7 @@ mod tests { let script1_code = "begin push.1 end"; let program1 = assembler1 .assemble_program(script1_code) - .expect("Failed to assemble note script 1"); + .expect("failed to assemble note script 1"); let note_script1 = NoteScript::new(program1); let script_root1 = note_script1.root(); @@ -374,7 +423,7 @@ mod tests { let script2_code = "begin push.2 push.3 add end"; let program2 = assembler2 .assemble_program(script2_code) - .expect("Failed to assemble note script 2"); + .expect("failed to assemble note script 2"); let note_script2 = NoteScript::new(program2); let script_root2 = note_script2.root(); @@ -383,25 +432,27 @@ mod tests { .add_note_script(note_script1.clone()) .add_note_script(note_script2.clone()) .build() - .expect("Failed to build transaction context"); + .expect("failed to build transaction context"); // Assert that fetching both note scripts works let retrieved_script1 = tx_context .get_note_script(script_root1) .await - .expect("Failed to get note script 1"); + .expect("failed to get note script 1") + .expect("note script 1 should exist"); assert_eq!(retrieved_script1, note_script1); let retrieved_script2 = tx_context .get_note_script(script_root2) .await - .expect("Failed to get note script 2"); + .expect("failed to get note script 2") + .expect("note script 2 should exist"); assert_eq!(retrieved_script2, note_script2); - // Fetching a non-existent one fails + // Fetching a non-existent one returns None let non_existent_root = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); let result = tx_context.get_note_script(non_existent_root).await; - assert!(matches!(result, Err(DataStoreError::NoteScriptNotFound(_)))); + assert!(matches!(result, Ok(None))); } } diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index f6b5b1c7d3..30aeb8fe76 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -1,15 +1,14 @@ use alloc::string::String; use alloc::vec::Vec; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::transaction::TransactionKernel; -use miden_objects::account::AccountId; -use miden_objects::asset::Asset; -use miden_objects::crypto::rand::FeltRng; -use miden_objects::note::{Note, NoteType}; -use miden_objects::testing::storage::prepare_assets; -use miden_processor::Felt; use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::AccountId; +use miden_protocol::asset::Asset; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::note::{Note, NoteType}; +use miden_protocol::testing::storage::prepare_assets; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::note::NoteBuilder; use rand::SeedableRng; use rand::rngs::SmallRng; @@ -20,7 +19,7 @@ use rand::rngs::SmallRng; macro_rules! assert_execution_error { ($execution_result:expr, $expected_err:expr) => { match $execution_result { - Err(miden_processor::ExecutionError::FailedAssertion { label: _, source_file: _, clk: _, err_code, err_msg }) => { + Err(miden_processor::ExecutionError::FailedAssertion { label: _, source_file: _, clk: _, err_code, err_msg, err: _ }) => { if let Some(ref msg) = err_msg { assert_eq!(msg.as_ref(), $expected_err.message(), "error messages did not match"); } @@ -48,6 +47,7 @@ macro_rules! assert_transaction_executor_error { clk: _, err_code, err_msg, + err: _, }, )) => { if let Some(ref msg) = err_msg { @@ -127,22 +127,22 @@ pub fn create_p2any_note( code_body.push_str("dropw dropw dropw dropw"); let code = format!( - " - use.mock::account - use.miden::active_note - use.miden::contracts::wallets::basic->wallet + r#" + use mock::account + use miden::protocol::active_note + use miden::standards::wallets::basic->wallet begin # fetch pointer & number of assets push.0 exec.active_note::get_assets # [num_assets, dest_ptr] # runtime-check we got the expected count - push.{num_assets} assert_eq # [dest_ptr] + push.{num_assets} assert_eq.err="unexpected number of assets" # [dest_ptr] {code_body} dropw dropw dropw dropw end - ", + "#, num_assets = assets.len(), ); @@ -151,7 +151,7 @@ pub fn create_p2any_note( .note_type(note_type) .serial_number(serial_number) .code(code) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) + .dynamically_linked_libraries(CodeBuilder::mock_libraries()) .build() .expect("generated note script should compile") } @@ -187,7 +187,7 @@ where let note = NoteBuilder::new(sender_id, SmallRng::from_os_rng()) .code(note_code) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) + .dynamically_linked_libraries(CodeBuilder::mock_libraries()) .build()?; Ok(note) @@ -198,7 +198,7 @@ fn note_script_that_creates_notes<'note>( sender_id: AccountId, output_notes: impl Iterator, ) -> anyhow::Result { - let mut out = String::from("use.miden::output_note\n\nbegin\n"); + let mut out = String::from("use miden::protocol::output_note\n\nbegin\n"); for (idx, note) in output_notes.into_iter().enumerate() { anyhow::ensure!( @@ -208,7 +208,7 @@ fn note_script_that_creates_notes<'note>( // Make sure that the transaction's native account matches the note sender. out.push_str(&format!( - r#"exec.::miden::native_account::get_id + r#"exec.::miden::protocol::native_account::get_id # => [native_account_id_prefix, native_account_id_suffix] push.{sender_prefix} assert_eq.err="sender ID prefix does not match native account ID's prefix" # => [native_account_id_suffix] @@ -227,23 +227,34 @@ fn note_script_that_creates_notes<'note>( out.push_str(&format!( " push.{recipient} - push.{hint} push.{note_type} - push.{aux} push.{tag} - call.output_note::create\n", + exec.output_note::create\n", recipient = note.recipient().digest(), - hint = Felt::from(note.metadata().execution_hint()), note_type = note.metadata().note_type() as u8, - aux = note.metadata().aux(), tag = note.metadata().tag(), )); + out.push_str(&format!( + " + push.{ATTACHMENT} + push.{attachment_scheme} + push.{attachment_kind} + dup.6 + # => [note_idx, attachment_kind, attachment_scheme, ATTACHMENT, note_idx] + exec.output_note::set_attachment + # => [note_idx] + ", + ATTACHMENT = note.metadata().to_attachment_word(), + attachment_scheme = note.metadata().attachment().attachment_scheme().as_u32(), + attachment_kind = note.metadata().attachment().content().attachment_kind().as_u8(), + )); + let assets_str = prepare_assets(note.assets()); for asset in assets_str { out.push_str(&format!( " push.{asset} - call.::miden::contracts::wallets::basic::move_asset_to_note\n", + call.::miden::standards::wallets::basic::move_asset_to_note\n", )); } } diff --git a/crates/miden-testing/tests/agglayer/asset_conversion.rs b/crates/miden-testing/tests/agglayer/asset_conversion.rs new file mode 100644 index 0000000000..6cec09d255 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/asset_conversion.rs @@ -0,0 +1,248 @@ +extern crate alloc; + +use alloc::sync::Arc; + +use miden_agglayer::{agglayer_library, utils}; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_processor::fast::{ExecutionOutput, FastProcessor}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; +use miden_protocol::Felt; +use miden_protocol::transaction::TransactionKernel; +use primitive_types::U256; + +/// Convert a Vec to a U256 +fn felts_to_u256(felts: Vec) -> U256 { + assert_eq!(felts.len(), 8, "expected exactly 8 felts"); + let array: [Felt; 8] = + [felts[0], felts[1], felts[2], felts[3], felts[4], felts[5], felts[6], felts[7]]; + let bytes = utils::felts_to_u256_bytes(array); + U256::from_little_endian(&bytes) +} + +/// Convert the top 8 u32 values from the execution stack to a U256 +fn stack_to_u256(exec_output: &ExecutionOutput) -> U256 { + let felts: Vec = exec_output.stack[0..8].to_vec(); + felts_to_u256(felts) +} + +/// Execute a program with default host +async fn execute_program_with_default_host( + program: Program, +) -> Result { + let mut host = DefaultHost::default(); + + let test_lib = TransactionKernel::library(); + host.load_library(test_lib.mast_forest()).unwrap(); + + let std_lib = CoreLibrary::default(); + host.load_library(std_lib.mast_forest()).unwrap(); + + let asset_conversion_lib = agglayer_library(); + host.load_library(asset_conversion_lib.mast_forest()).unwrap(); + + let stack_inputs = StackInputs::new(vec![]).unwrap(); + let advice_inputs = AdviceInputs::default(); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); + processor.execute(&program, &mut host).await +} + +/// Helper function to test convert_felt_to_u256_scaled with given parameters +async fn test_convert_to_u256_helper( + miden_amount: Felt, + scale_exponent: Felt, + expected_result_array: [u32; 8], + expected_result_u256: U256, +) -> anyhow::Result<()> { + let asset_conversion_lib = agglayer_library(); + + let script_code = format!( + " + use miden::core::sys + use miden::agglayer::asset_conversion + + begin + push.{}.{} + exec.asset_conversion::scale_native_amount_to_u256 + exec.sys::truncate_stack + end + ", + scale_exponent, miden_amount, + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(asset_conversion_lib.clone()) + .unwrap() + .assemble_program(&script_code) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + + // Extract the first 8 u32 values from the stack (the U256 representation) + let actual_result: [u32; 8] = [ + exec_output.stack[0].as_int() as u32, + exec_output.stack[1].as_int() as u32, + exec_output.stack[2].as_int() as u32, + exec_output.stack[3].as_int() as u32, + exec_output.stack[4].as_int() as u32, + exec_output.stack[5].as_int() as u32, + exec_output.stack[6].as_int() as u32, + exec_output.stack[7].as_int() as u32, + ]; + + let actual_result_u256 = stack_to_u256(&exec_output); + + assert_eq!(actual_result, expected_result_array); + assert_eq!(actual_result_u256, expected_result_u256); + + Ok(()) +} + +#[tokio::test] +async fn test_convert_to_u256_basic_examples() -> anyhow::Result<()> { + // Test case 1: amount=1, no scaling (scale_exponent=0) + test_convert_to_u256_helper( + Felt::new(1), + Felt::new(0), + [1, 0, 0, 0, 0, 0, 0, 0], + U256::from(1u64), + ) + .await?; + + // Test case 2: amount=1, scale to 1e18 (scale_exponent=18) + test_convert_to_u256_helper( + Felt::new(1), + Felt::new(18), + [2808348672, 232830643, 0, 0, 0, 0, 0, 0], + U256::from_dec_str("1000000000000000000").unwrap(), + ) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_convert_to_u256_scaled_eth() -> anyhow::Result<()> { + // 100 units base 1e6 + let miden_amount = Felt::new(100_000_000); + + // scale to 1e18 + let target_scale = Felt::new(12); + + let asset_conversion_lib = agglayer_library(); + + let script_code = format!( + " + use miden::core::sys + use miden::agglayer::asset_conversion + + begin + push.{}.{} + exec.asset_conversion::scale_native_amount_to_u256 + exec.sys::truncate_stack + end + ", + target_scale, miden_amount, + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(asset_conversion_lib.clone()) + .unwrap() + .assemble_program(&script_code) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + + let expected_result = U256::from_dec_str("100000000000000000000").unwrap(); + let actual_result = stack_to_u256(&exec_output); + + assert_eq!(actual_result, expected_result); + + Ok(()) +} + +#[tokio::test] +async fn test_convert_to_u256_scaled_large_amount() -> anyhow::Result<()> { + // 100,000,000 units (base 1e10) + let miden_amount = Felt::new(1000000000000000000); + + // scale to base 1e18 + let scale_exponent = Felt::new(8); + + let asset_conversion_lib = agglayer_library(); + + let script_code = format!( + " + use miden::core::sys + use miden::agglayer::asset_conversion + + begin + push.{}.{} + + exec.asset_conversion::scale_native_amount_to_u256 + exec.sys::truncate_stack + end + ", + scale_exponent, miden_amount, + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(asset_conversion_lib.clone()) + .unwrap() + .assemble_program(&script_code) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + + let expected_result = U256::from_dec_str("100000000000000000000000000").unwrap(); + let actual_result = stack_to_u256(&exec_output); + + assert_eq!(actual_result, expected_result); + + Ok(()) +} + +#[test] +fn test_felts_to_u256_bytes_sequential_values() { + let limbs = [ + Felt::new(1), + Felt::new(2), + Felt::new(3), + Felt::new(4), + Felt::new(5), + Felt::new(6), + Felt::new(7), + Felt::new(8), + ]; + let result = utils::felts_to_u256_bytes(limbs); + assert_eq!(result.len(), 32); + + // Verify the byte layout: limbs are processed in little-endian order, each as little-endian u32 + // First byte should be 1 (limbs[0] = 1, least significant limb, least significant byte) + assert_eq!(result[0], 1); + // Byte at position 28 should be 8 (limbs[7] = 8, most significant limb, least significant + // byte) + assert_eq!(result[28], 8); +} + +#[test] +fn test_felts_to_u256_bytes_edge_cases() { + // Test case 1: All zeros (minimum) + let limbs = [Felt::new(0); 8]; + let result = utils::felts_to_u256_bytes(limbs); + assert_eq!(result.len(), 32); + assert!(result.iter().all(|&b| b == 0)); + + // Test case 2: All max u32 values (maximum) + let limbs = [Felt::new(u32::MAX as u64); 8]; + let result = utils::felts_to_u256_bytes(limbs); + assert_eq!(result.len(), 32); + assert!(result.iter().all(|&b| b == 255)); +} diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs new file mode 100644 index 0000000000..81392b584d --- /dev/null +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -0,0 +1,196 @@ +extern crate alloc; + +use core::slice; + +use miden_agglayer::{ + ClaimNoteParams, + claim_note_test_inputs, + create_claim_note, + create_existing_agglayer_faucet, + create_existing_bridge_account, +}; +use miden_protocol::Felt; +use miden_protocol::account::Account; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteTag, + NoteType, +}; +use miden_protocol::transaction::OutputNote; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::note::WellKnownNote; +use miden_testing::{AccountState, Auth, MockChain}; +use rand::Rng; + +/// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created. +#[tokio::test] +async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ACCOUNT (with bridge_out component for MMR validation) + // -------------------------------------------------------------------------------------------- + let bridge_seed = builder.rng_mut().draw_word(); + let bridge_account = create_existing_bridge_account(bridge_seed); + builder.add_account(bridge_account.clone())?; + + // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) + // -------------------------------------------------------------------------------------------- + let token_symbol = "AGG"; + let decimals = 8u8; + let max_supply = Felt::new(1000000); + let agglayer_faucet_seed = builder.rng_mut().draw_word(); + + let agglayer_faucet = create_existing_agglayer_faucet( + agglayer_faucet_seed, + token_symbol, + decimals, + max_supply, + bridge_account.id(), + ); + builder.add_account(agglayer_faucet.clone())?; + + // CREATE USER ACCOUNT TO RECEIVE P2ID NOTE + // -------------------------------------------------------------------------------------------- + let user_account_builder = + Account::builder(builder.rng_mut().random()).with_component(BasicWallet); + let user_account = builder.add_account_from_builder( + Auth::IncrNonce, + user_account_builder, + AccountState::Exists, + )?; + + // CREATE CLAIM NOTE WITH P2ID OUTPUT NOTE DETAILS + // -------------------------------------------------------------------------------------------- + + // Define amount values for the test + let amount_felt = Felt::new(100); + + // Create CLAIM note using the helper function with new agglayer claimAsset inputs + let ( + smt_proof_local_exit_root, + smt_proof_rollup_exit_root, + global_index, + mainnet_exit_root, + rollup_exit_root, + origin_network, + origin_token_address, + destination_network, + destination_address, + amount_u256, + metadata, + ) = claim_note_test_inputs(amount_felt, user_account.id()); + + // Generate a serial number for the P2ID note + let serial_num = builder.rng_mut().draw_word(); + + let claim_params = ClaimNoteParams { + smt_proof_local_exit_root, + smt_proof_rollup_exit_root, + global_index, + mainnet_exit_root: &mainnet_exit_root, + rollup_exit_root: &rollup_exit_root, + origin_network, + origin_token_address: &origin_token_address, + destination_network, + destination_address: &destination_address, + amount: amount_u256, + metadata, + claim_note_creator_account_id: user_account.id(), + agglayer_faucet_account_id: agglayer_faucet.id(), + output_note_tag: NoteTag::with_account_target(user_account.id()), + p2id_serial_number: serial_num, + destination_account_id: user_account.id(), + rng: builder.rng_mut(), + }; + + // Create P2ID note for the user account (similar to network faucet test) + let p2id_script = WellKnownNote::P2ID.script(); + let p2id_inputs = vec![user_account.id().suffix(), user_account.id().prefix().as_felt()]; + let note_inputs = NoteInputs::new(p2id_inputs)?; + let p2id_recipient = NoteRecipient::new(serial_num, p2id_script.clone(), note_inputs); + + let claim_note = create_claim_note(claim_params)?; + + // Add the claim note to the builder before building the mock chain + builder.add_output_note(OutputNote::Full(claim_note.clone())); + + // BUILD MOCK CHAIN WITH ALL ACCOUNTS + // -------------------------------------------------------------------------------------------- + let mut mock_chain = builder.clone().build()?; + mock_chain.prove_next_block()?; + + // CREATE EXPECTED P2ID NOTE FOR VERIFICATION + // -------------------------------------------------------------------------------------------- + let mint_asset: Asset = FungibleAsset::new(agglayer_faucet.id(), amount_felt.into())?.into(); + let output_note_tag = NoteTag::with_account_target(user_account.id()); + let expected_p2id_note = Note::new( + NoteAssets::new(vec![mint_asset])?, + NoteMetadata::new(agglayer_faucet.id(), NoteType::Public, output_note_tag), + p2id_recipient, + ); + + // EXECUTE CLAIM NOTE AGAINST AGGLAYER FAUCET (with FPI to Bridge) + // -------------------------------------------------------------------------------------------- + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(bridge_account.id())?; + + let tx_context = mock_chain + .build_tx_context(agglayer_faucet.id(), &[], &[claim_note])? + .add_note_script(p2id_script) + .foreign_accounts(vec![foreign_account_inputs]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // VERIFY P2ID NOTE WAS CREATED + // -------------------------------------------------------------------------------------------- + + // Check that exactly one P2ID note was created by the faucet + assert_eq!(executed_transaction.output_notes().num_notes(), 1); + let output_note = executed_transaction.output_notes().get_note(0); + + // Verify the output note contains the minted fungible asset + let expected_asset = FungibleAsset::new(agglayer_faucet.id(), amount_felt.into())?; + + // Verify note metadata properties + assert_eq!(output_note.metadata().sender(), agglayer_faucet.id()); + assert_eq!(output_note.metadata().note_type(), NoteType::Public); + assert_eq!(output_note.id(), expected_p2id_note.id()); + + // Extract the full note from the OutputNote enum for detailed verification + let full_note = match output_note { + OutputNote::Full(note) => note, + _ => panic!("Expected OutputNote::Full variant for public note"), + }; + + // Verify note structure and asset content + let expected_asset_obj = Asset::from(expected_asset); + assert_eq!(full_note, &expected_p2id_note); + assert!(full_note.assets().iter().any(|asset| asset == &expected_asset_obj)); + + // Apply the transaction to the mock chain + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + // CONSUME THE OUTPUT NOTE WITH TARGET ACCOUNT + // -------------------------------------------------------------------------------------------- + // Consume the output note with target account + let mut user_account_mut = user_account.clone(); + let consume_tx_context = mock_chain + .build_tx_context(user_account_mut.clone(), &[], slice::from_ref(&expected_p2id_note))? + .build()?; + let consume_executed_transaction = consume_tx_context.execute().await?; + + user_account_mut.apply_delta(consume_executed_transaction.account_delta())?; + + // Verify the account's vault now contains the expected fungible asset + let balance = user_account_mut.vault().get_balance(agglayer_faucet.id())?; + assert_eq!(balance, expected_asset.amount()); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs new file mode 100644 index 0000000000..ab832bb50a --- /dev/null +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -0,0 +1,298 @@ +extern crate alloc; + +use miden_agglayer::{EthAddressFormat, b2agg_script, bridge_out_component}; +use miden_protocol::account::{ + Account, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteTag, + NoteType, +}; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::FungibleFaucetExt; +use miden_standards::note::WellKnownNote; +use miden_testing::{AccountState, Auth, MockChain}; +use rand::Rng; + +/// Tests the B2AGG (Bridge to AggLayer) note script with bridge_out account component. +/// +/// This test flow: +/// 1. Creates a network faucet to provide assets +/// 2. Creates a bridge account with the bridge_out component (using network storage) +/// 3. Creates a B2AGG note with assets from the network faucet +/// 4. Executes the B2AGG note consumption via network transaction +/// 5. Consumes the BURN note +#[tokio::test] +async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // Create a network faucet owner account + let faucet_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + // Create a network faucet to provide assets for the B2AGG note + let faucet = + builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + + // Create a bridge account with the bridge_out component using network (public) storage + // Add a storage map for the bridge component to store MMR frontier data + let storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap(); + let storage_slots = vec![StorageSlot::with_empty_map(storage_slot_name)]; + let bridge_component = bridge_out_component(storage_slots); + let account_builder = Account::builder(builder.rng_mut().random()) + .storage_mode(AccountStorageMode::Public) + .with_component(bridge_component); + let mut bridge_account = + builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)?; + + // CREATE B2AGG NOTE WITH ASSETS + // -------------------------------------------------------------------------------------------- + + let amount = Felt::new(100); + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); + let tag = NoteTag::new(0); + let note_type = NoteType::Public; // Use Public note type for network transaction + + // Get the B2AGG note script + let b2agg_script = b2agg_script(); + + // Create note inputs with destination network and address + // destination_network: u32 (AggLayer-assigned network ID) + // destination_address: 20 bytes (Ethereum address) split into 5 u32 values + let destination_network = Felt::new(1); // Example network ID + let destination_address = "0x1234567890abcdef1122334455667788990011aa"; + let eth_address = + EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + let address_felts = eth_address.to_elements().to_vec(); + + // Combine network ID and address felts into note inputs (6 felts total) + let mut input_felts = vec![destination_network]; + input_felts.extend(address_felts); + + let inputs = NoteInputs::new(input_felts.clone())?; + + // Create the B2AGG note with assets from the faucet + let b2agg_note_metadata = NoteMetadata::new(faucet.id(), note_type, tag); + let b2agg_note_assets = NoteAssets::new(vec![bridge_asset])?; + let serial_num = Word::from([1, 2, 3, 4u32]); + let b2agg_note_script = NoteScript::new(b2agg_script); + let b2agg_note_recipient = NoteRecipient::new(serial_num, b2agg_note_script, inputs); + let b2agg_note = Note::new(b2agg_note_assets, b2agg_note_metadata, b2agg_note_recipient); + + // Add the B2AGG note to the mock chain + builder.add_output_note(OutputNote::Full(b2agg_note.clone())); + let mut mock_chain = builder.build()?; + + // Get BURN note script to add to the transaction context + let burn_note_script: NoteScript = WellKnownNote::BURN.script(); + + // EXECUTE B2AGG NOTE AGAINST BRIDGE ACCOUNT (NETWORK TRANSACTION) + // -------------------------------------------------------------------------------------------- + let tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])? + .add_note_script(burn_note_script.clone()) + .build()?; + let executed_transaction = tx_context.execute().await?; + + // VERIFY PUBLIC BURN NOTE WAS CREATED + // -------------------------------------------------------------------------------------------- + // The bridge_out component should create a PUBLIC BURN note addressed to the faucet + assert_eq!( + executed_transaction.output_notes().num_notes(), + 1, + "Expected one BURN note to be created" + ); + + let output_note = executed_transaction.output_notes().get_note(0); + + // Extract the full note from the OutputNote enum + let burn_note = match output_note { + OutputNote::Full(note) => note, + _ => panic!("Expected OutputNote::Full variant for BURN note"), + }; + + // Verify the BURN note is public + assert_eq!(burn_note.metadata().note_type(), NoteType::Public, "BURN note should be public"); + + // Verify the BURN note contains the bridged asset + let expected_asset = FungibleAsset::new(faucet.id(), amount.into())?; + let expected_asset_obj = Asset::from(expected_asset); + assert!( + burn_note.assets().iter().any(|asset| asset == &expected_asset_obj), + "BURN note should contain the bridged asset" + ); + + assert_eq!( + burn_note.metadata().tag(), + NoteTag::with_account_target(faucet.id()), + "BURN note should have the correct tag" + ); + + // Verify the BURN note uses the correct script + assert_eq!( + burn_note.recipient().script().root(), + burn_note_script.root(), + "BURN note should use the BURN script" + ); + + // Apply the delta to the bridge account + bridge_account.apply_delta(executed_transaction.account_delta())?; + + // Apply the transaction to the mock chain + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + // CONSUME THE BURN NOTE WITH THE NETWORK FAUCET + // -------------------------------------------------------------------------------------------- + // Check the initial token issuance before burning + let initial_issuance = faucet.get_token_issuance().unwrap(); + assert_eq!(initial_issuance, Felt::new(100), "Initial issuance should be 100"); + + // Execute the BURN note against the network faucet + let burn_tx_context = + mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?; + let burn_executed_transaction = burn_tx_context.execute().await?; + + // Verify the burn transaction was successful - no output notes should be created + assert_eq!( + burn_executed_transaction.output_notes().num_notes(), + 0, + "Burn transaction should not create output notes" + ); + + // Apply the delta to the faucet account and verify the token issuance decreased + let mut faucet = faucet; + faucet.apply_delta(burn_executed_transaction.account_delta())?; + let final_issuance = faucet.get_token_issuance().unwrap(); + assert_eq!( + final_issuance, + Felt::new(initial_issuance.as_int() - amount.as_int()), + "Token issuance should decrease by the burned amount" + ); + + Ok(()) +} + +/// Tests the B2AGG (Bridge to AggLayer) note script reclaim functionality. +/// +/// This test covers the "reclaim" branch where the note creator consumes their own B2AGG note. +/// In this scenario, the assets are simply added back to the account without creating a BURN note. +/// +/// Test flow: +/// 1. Creates a network faucet to provide assets +/// 2. Creates a user account that will create and consume the B2AGG note +/// 3. Creates a B2AGG note with the user account as sender +/// 4. The same user account consumes the B2AGG note (triggering reclaim branch) +/// 5. Verifies that assets are added back to the account and no BURN note is created +#[tokio::test] +async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // Create a network faucet owner account + let faucet_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + // Create a network faucet to provide assets for the B2AGG note + let faucet = + builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + + // Create a user account that will create and consume the B2AGG note + let mut user_account = builder.add_existing_wallet(Auth::BasicAuth)?; + + // CREATE B2AGG NOTE WITH USER ACCOUNT AS SENDER + // -------------------------------------------------------------------------------------------- + + let amount = Felt::new(50); + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); + let tag = NoteTag::new(0); + let note_type = NoteType::Public; + + // Get the B2AGG note script + let b2agg_script = b2agg_script(); + + // Create note inputs with destination network and address + let destination_network = Felt::new(1); + let destination_address = "0x1234567890abcdef1122334455667788990011aa"; + let eth_address = + EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + let address_felts = eth_address.to_elements().to_vec(); + + // Combine network ID and address felts into note inputs (6 felts total) + let mut input_felts = vec![destination_network]; + input_felts.extend(address_felts); + + let inputs = NoteInputs::new(input_felts.clone())?; + + // Create the B2AGG note with the USER ACCOUNT as the sender + // This is the key difference - the note sender will be the same as the consuming account + let b2agg_note_metadata = NoteMetadata::new(user_account.id(), note_type, tag); + let b2agg_note_assets = NoteAssets::new(vec![bridge_asset])?; + let serial_num = Word::from([1, 2, 3, 4u32]); + let b2agg_note_script = NoteScript::new(b2agg_script); + let b2agg_note_recipient = NoteRecipient::new(serial_num, b2agg_note_script, inputs); + let b2agg_note = Note::new(b2agg_note_assets, b2agg_note_metadata, b2agg_note_recipient); + + // Add the B2AGG note to the mock chain + builder.add_output_note(OutputNote::Full(b2agg_note.clone())); + let mut mock_chain = builder.build()?; + + // Store the initial asset balance of the user account + let initial_balance = user_account.vault().get_balance(faucet.id()).unwrap_or(0u64); + + // EXECUTE B2AGG NOTE WITH THE SAME USER ACCOUNT (RECLAIM SCENARIO) + // -------------------------------------------------------------------------------------------- + let tx_context = mock_chain + .build_tx_context(user_account.id(), &[b2agg_note.id()], &[])? + .build()?; + let executed_transaction = tx_context.execute().await?; + + // VERIFY NO BURN NOTE WAS CREATED (RECLAIM BRANCH) + // -------------------------------------------------------------------------------------------- + // In the reclaim scenario, no BURN note should be created + assert_eq!( + executed_transaction.output_notes().num_notes(), + 0, + "Reclaim scenario should not create any output notes" + ); + + // Apply the delta to the user account + user_account.apply_delta(executed_transaction.account_delta())?; + + // VERIFY ASSETS WERE ADDED BACK TO THE ACCOUNT + // -------------------------------------------------------------------------------------------- + let final_balance = user_account.vault().get_balance(faucet.id()).unwrap_or(0u64); + let expected_balance = initial_balance + amount.as_int(); + + assert_eq!( + final_balance, expected_balance, + "User account should have received the assets back from the B2AGG note" + ); + + // Apply the transaction to the mock chain + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs new file mode 100644 index 0000000000..2a6d344c67 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -0,0 +1,4 @@ +pub mod asset_conversion; +mod bridge_in; +mod bridge_out; +mod solidity_miden_address_conversion; diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs new file mode 100644 index 0000000000..2083a9dd36 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -0,0 +1,174 @@ +extern crate alloc; + +use alloc::sync::Arc; + +use miden_agglayer::{EthAddressFormat, agglayer_library}; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_processor::fast::{ExecutionOutput, FastProcessor}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; +use miden_protocol::Felt; +use miden_protocol::account::AccountId; +use miden_protocol::address::NetworkId; +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_PRIVATE_SENDER, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + AccountIdBuilder, +}; +use miden_protocol::transaction::TransactionKernel; + +/// Execute a program with default host +async fn execute_program_with_default_host( + program: Program, +) -> Result { + let mut host = DefaultHost::default(); + + let test_lib = TransactionKernel::library(); + host.load_library(test_lib.mast_forest()).unwrap(); + + let std_lib = CoreLibrary::default(); + host.load_library(std_lib.mast_forest()).unwrap(); + + for (event_name, handler) in std_lib.handlers() { + host.register_handler(event_name, handler)?; + } + + let asset_conversion_lib = agglayer_library(); + host.load_library(asset_conversion_lib.mast_forest()).unwrap(); + + let stack_inputs = StackInputs::new(vec![]).unwrap(); + let advice_inputs = AdviceInputs::default(); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); + processor.execute(&program, &mut host).await +} + +#[test] +fn test_account_id_to_ethereum_roundtrip() { + let original_account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let eth_address = EthAddressFormat::from_account_id(original_account_id); + let recovered_account_id = eth_address.to_account_id().unwrap(); + assert_eq!(original_account_id, recovered_account_id); +} + +#[test] +fn test_bech32_to_ethereum_roundtrip() { + let test_addresses = [ + "mtst1azcw08rget79fqp8ymr0zqkv5v5lj466", + "mtst1arxmxavamh7lqyp79mexktt4vgxv40mp", + "mtst1ar2phe0pa0ln75plsczxr8ryws4s8zyp", + ]; + + let evm_addresses = [ + "0x00000000b0e79c68cafc54802726c6f102cca300", + "0x00000000cdb3759dddfdf0103e2ef26b2d756200", + "0x00000000d41be5e1ebff3f503f8604619c647400", + ]; + + for (bech32, expected_evm) in test_addresses.iter().zip(evm_addresses.iter()) { + let (network_id, account_id) = AccountId::from_bech32(bech32).unwrap(); + + let eth = EthAddressFormat::from_account_id(account_id); + let recovered = eth.to_account_id().unwrap(); + let recovered_bech32 = recovered.to_bech32(network_id); + + assert_eq!(&account_id, &recovered); + assert_eq!(*expected_evm, eth.to_string()); + assert_eq!(*bech32, recovered_bech32); + } +} + +#[test] +fn test_random_bech32_to_ethereum_roundtrip() { + let mut rng = rand::rng(); + let network_id = NetworkId::Testnet; + + for _ in 0..3 { + let account_id = AccountIdBuilder::new().build_with_rng(&mut rng); + let bech32_address = account_id.to_bech32(network_id.clone()); + let eth_address = EthAddressFormat::from_account_id(account_id); + let recovered_account_id = eth_address.to_account_id().unwrap(); + let recovered_bech32 = recovered_account_id.to_bech32(network_id.clone()); + + assert_eq!(account_id, recovered_account_id); + assert_eq!(bech32_address, recovered_bech32); + } +} + +#[tokio::test] +async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { + let test_account_ids = [ + AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?, + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?, + AccountIdBuilder::new().build_with_rng(&mut rand::rng()), + AccountIdBuilder::new().build_with_rng(&mut rand::rng()), + AccountIdBuilder::new().build_with_rng(&mut rand::rng()), + ]; + + for (idx, original_account_id) in test_account_ids.iter().enumerate() { + let eth_address = EthAddressFormat::from_account_id(*original_account_id); + + let address_felts = eth_address.to_elements().to_vec(); + let le: Vec = address_felts + .iter() + .map(|f| { + let val = f.as_int(); + assert!(val <= u32::MAX as u64, "felt value {} exceeds u32::MAX", val); + val as u32 + }) + .collect(); + + assert_eq!(le[4], 0, "test {}: expected msw limb (le[4]) to be zero", idx); + + let addr0 = le[0]; + let addr1 = le[1]; + let addr2 = le[2]; + let addr3 = le[3]; + let addr4 = le[4]; + + let account_id_felts: [Felt; 2] = (*original_account_id).into(); + let expected_prefix = account_id_felts[0].as_int(); + let expected_suffix = account_id_felts[1].as_int(); + + let script_code = format!( + r#" + use miden::core::sys + use miden::agglayer::eth_address + + begin + push.{}.{}.{}.{}.{} + exec.eth_address::to_account_id + exec.sys::truncate_stack + end + "#, + addr4, addr3, addr2, addr1, addr0 + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_library()) + .unwrap() + .assemble_program(&script_code) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + + let actual_prefix = exec_output.stack[0].as_int(); + let actual_suffix = exec_output.stack[1].as_int(); + + assert_eq!(actual_prefix, expected_prefix, "test {}: prefix mismatch", idx); + assert_eq!(actual_suffix, expected_suffix, "test {}: suffix mismatch", idx); + + let reconstructed_account_id = + AccountId::try_from([Felt::new(actual_prefix), Felt::new(actual_suffix)])?; + + assert_eq!( + reconstructed_account_id, *original_account_id, + "test {}: accountId roundtrip failed", + idx + ); + } + + Ok(()) +} diff --git a/crates/miden-testing/tests/auth/ecdsa_acl.rs b/crates/miden-testing/tests/auth/ecdsa_acl.rs index f5e21c282c..9bdaf80ebc 100644 --- a/crates/miden-testing/tests/auth/ecdsa_acl.rs +++ b/crates/miden-testing/tests/auth/ecdsa_acl.rs @@ -1,10 +1,7 @@ use core::slice; use assert_matches::assert_matches; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{ +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, @@ -12,9 +9,14 @@ use miden_objects::account::{ AccountStorageMode, AccountType, }; -use miden_objects::note::Note; -use miden_objects::transaction::OutputNote; -use miden_objects::{Felt, FieldElement, Word}; +use miden_protocol::note::Note; +use miden_protocol::testing::storage::MOCK_VALUE_SLOT0; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, FieldElement, Word}; +use miden_standards::account::auth::AuthEcdsaK256KeccakAcl; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockAccountComponent; +use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain}; use miden_tx::TransactionExecutorError; @@ -24,7 +26,7 @@ use crate::prove_and_verify_transaction; // ================================================================================================ const TX_SCRIPT_NO_TRIGGER: &str = r#" - use.mock::account + use mock::account begin call.account::account_procedure_1 drop @@ -44,10 +46,10 @@ fn setup_ecdsa_acl_test( MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()).into(); let get_item_proc_root = component - .get_procedure_root_by_name("mock::account::get_item") + .get_procedure_root_by_path("mock::account::get_item") .expect("get_item procedure should exist"); let set_item_proc_root = component - .get_procedure_root_by_name("mock::account::set_item") + .get_procedure_root_by_path("mock::account::set_item") .expect("set_item procedure should exist"); let auth_trigger_procedures = vec![get_item_proc_root, set_item_proc_root]; @@ -86,10 +88,10 @@ async fn test_ecdsa_acl() -> anyhow::Result<()> { MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()).into(); let get_item_proc_root = component - .get_procedure_root_by_name("mock::account::get_item") + .get_procedure_root_by_path("mock::account::get_item") .expect("get_item procedure should exist"); let set_item_proc_root = component - .get_procedure_root_by_name("mock::account::set_item") + .get_procedure_root_by_path("mock::account::set_item") .expect("set_item procedure should exist"); let auth_trigger_procedures = vec![get_item_proc_root, set_item_proc_root]; @@ -100,34 +102,45 @@ async fn test_ecdsa_acl() -> anyhow::Result<()> { } .build_component(); - let tx_script_with_trigger_1 = r#" - use.mock::account + let tx_script_with_trigger_1 = format!( + r#" + use mock::account + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin - push.0 + push.MOCK_VALUE_SLOT0[0..2] call.account::get_item dropw end - "#; + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + ); - let tx_script_with_trigger_2 = r#" - use.mock::account + let tx_script_with_trigger_2 = format!( + r#" + use mock::account + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin - push.1.2.3.4 push.0 + push.1.2.3.4 + push.MOCK_VALUE_SLOT0[0..2] call.account::set_item dropw dropw end - "#; + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + ); let tx_script_trigger_1 = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(tx_script_with_trigger_1)?; + CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_with_trigger_1)?; let tx_script_trigger_2 = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(tx_script_with_trigger_2)?; + CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_with_trigger_2)?; let tx_script_no_trigger = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; + CodeBuilder::with_mock_libraries().compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; // Test 1: Transaction WITH authenticator calling trigger procedure 1 (should succeed) let tx_context_with_auth_1 = mock_chain @@ -190,15 +203,18 @@ async fn test_ecdsa_acl_with_allow_unauthorized_output_notes() -> anyhow::Result let (account, mock_chain, note) = setup_ecdsa_acl_test(true, true)?; // Verify the storage layout includes both authorization flags - let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed"); - // Slot 1 should be [num_tracked_procs, allow_unauthorized_output_notes, + let config_slot = account + .storage() + .get_item(AuthEcdsaK256KeccakAcl::config_slot()) + .expect("config storage slot access failed"); + // Config Slot should be [num_trigger_procs, allow_unauthorized_output_notes, // allow_unauthorized_input_notes, 0] With 2 procedures, // allow_unauthorized_output_notes=true, and allow_unauthorized_input_notes=true, this should be // [2, 1, 1, 0] - assert_eq!(slot_1, Word::from([2u32, 1, 1, 0])); + assert_eq!(config_slot, Word::from([2u32, 1, 1, 0])); let tx_script_no_trigger = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; + CodeBuilder::with_mock_libraries().compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; // Test: Transaction WITHOUT authenticator calling non-trigger procedure (should succeed) // This tests that when allow_unauthorized_output_notes=true, transactions without @@ -227,15 +243,18 @@ async fn test_ecdsa_acl_with_disallow_unauthorized_input_notes() -> anyhow::Resu let (account, mock_chain, note) = setup_ecdsa_acl_test(true, false)?; // Verify the storage layout includes both flags - let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed"); - // Slot 1 should be [num_tracked_procs, allow_unauthorized_output_notes, + let config_slot = account + .storage() + .get_item(AuthEcdsaK256KeccakAcl::config_slot()) + .expect("config storage slot access failed"); + // Config Slot should be [num_trigger_procs, allow_unauthorized_output_notes, // allow_unauthorized_input_notes, 0] With 2 procedures, // allow_unauthorized_output_notes=true, and allow_unauthorized_input_notes=false, this should // be [2, 1, 0, 0] - assert_eq!(slot_1, Word::from([2u32, 1, 0, 0])); + assert_eq!(config_slot, Word::from([2u32, 1, 0, 0])); let tx_script_no_trigger = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; + CodeBuilder::with_mock_libraries().compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; // Test: Transaction WITHOUT authenticator calling non-trigger procedure but consuming input // notes This should FAIL because allow_unauthorized_input_notes=false and we're consuming diff --git a/crates/miden-testing/tests/auth/ecdsa_multisig.rs b/crates/miden-testing/tests/auth/ecdsa_multisig.rs index f75e249173..c2ff6fad66 100644 --- a/crates/miden-testing/tests/auth/ecdsa_multisig.rs +++ b/crates/miden-testing/tests/auth/ecdsa_multisig.rs @@ -1,23 +1,30 @@ -use miden_lib::account::components::ecdsa_k256_keccak_multisig_library; -use miden_lib::account::interface::AccountInterface; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::errors::tx_kernel_errors::ERR_TX_ALREADY_EXECUTED; -use miden_lib::note::create_p2id_note; -use miden_lib::testing::account_interface::get_public_keys_from_account; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::auth::{AuthSecretKey, PublicKey}; -use miden_objects::account::{Account, AccountBuilder, AccountId, AccountStorageMode, AccountType}; -use miden_objects::asset::FungibleAsset; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::{ +use miden_processor::AdviceInputs; +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::auth::{AuthSecretKey, PublicKey}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountId, + AccountStorageMode, + AccountType, +}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::note::NoteType; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, }; -use miden_objects::transaction::OutputNote; -use miden_objects::vm::AdviceMap; -use miden_objects::{Felt, Hasher, Word}; -use miden_processor::AdviceInputs; -use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::transaction::OutputNote; +use miden_protocol::vm::AdviceMap; +use miden_protocol::{Felt, Hasher, Word}; +use miden_standards::account::auth::AuthEcdsaK256KeccakMultisig; +use miden_standards::account::components::ecdsa_k256_keccak_multisig_library; +use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::ERR_TX_ALREADY_EXECUTED; +use miden_standards::note::create_p2id_note; +use miden_standards::testing::account_interface::get_public_keys_from_account; use miden_testing::utils::create_spawn_note; use miden_testing::{Auth, MockChainBuilder, assert_transaction_executor_error}; use miden_tx::TransactionExecutorError; @@ -136,7 +143,7 @@ async fn test_multisig_2_of_2_with_note_creation() -> anyhow::Result<()> { let tx_summary = match tx_context_init.execute().await.unwrap_err() { TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, - error => panic!("expected abort with tx effects: {error:?}"), + error => anyhow::bail!("expected abort with tx effects: {error}"), }; // Get signatures from both approvers @@ -398,12 +405,12 @@ async fn test_multisig_update_signers() -> anyhow::Result<()> { // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::update_signers_and_threshold + call.::ecdsa_k256_keccak_multisig::update_signers_and_threshold end "; - let tx_script = ScriptBuilder::new(true) - .with_dynamically_linked_library(&ecdsa_k256_keccak_multisig_library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(ecdsa_k256_keccak_multisig_library())? .compile_tx_script(tx_script_code)?; let advice_inputs = AdviceInputs { @@ -466,15 +473,21 @@ async fn test_multisig_update_signers() -> anyhow::Result<()> { // Verify that the public keys were actually updated in storage for (i, expected_key) in new_public_keys.iter().enumerate() { let storage_key = [Felt::new(i as u64), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let storage_item = updated_multisig_account.storage().get_map_item(1, storage_key).unwrap(); + let storage_item = updated_multisig_account + .storage() + .get_map_item(AuthEcdsaK256KeccakMultisig::approver_public_keys_slot(), storage_key) + .unwrap(); let expected_word: Word = expected_key.to_commitment().into(); assert_eq!(storage_item, expected_word, "Public key {} doesn't match expected value", i); } - // Verify the threshold was updated by checking storage slot 0 - let threshold_config_storage = updated_multisig_account.storage().get_item(0).unwrap(); + // Verify the threshold was updated by checking the config storage slot + let threshold_config_storage = updated_multisig_account + .storage() + .get_item(AuthEcdsaK256KeccakMultisig::threshold_config_slot()) + .unwrap(); assert_eq!( threshold_config_storage[0], @@ -635,9 +648,11 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { advice_map.insert(multisig_config_hash, config_and_pubkeys_vector); // Create transaction script - let tx_script = ScriptBuilder::new(true) - .with_dynamically_linked_library(&ecdsa_k256_keccak_multisig_library())? - .compile_tx_script("begin\n call.::update_signers_and_threshold\nend")?; + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(ecdsa_k256_keccak_multisig_library())? + .compile_tx_script( + "begin\n call.::ecdsa_k256_keccak_multisig::update_signers_and_threshold\nend", + )?; let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; @@ -703,13 +718,19 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { // Verify public keys were updated for (i, expected_key) in new_public_keys.iter().enumerate() { let storage_key = [Felt::new(i as u64), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let storage_item = updated_multisig_account.storage().get_map_item(1, storage_key).unwrap(); + let storage_item = updated_multisig_account + .storage() + .get_map_item(AuthEcdsaK256KeccakMultisig::approver_public_keys_slot(), storage_key) + .unwrap(); let expected_word: Word = expected_key.to_commitment().into(); assert_eq!(storage_item, expected_word, "Public key {} doesn't match", i); } // Verify threshold and num_approvers - let threshold_config = updated_multisig_account.storage().get_item(0).unwrap(); + let threshold_config = updated_multisig_account + .storage() + .get_item(AuthEcdsaK256KeccakMultisig::threshold_config_slot()) + .unwrap(); assert_eq!(threshold_config[0], Felt::new(threshold), "Threshold not updated"); assert_eq!(threshold_config[1], Felt::new(num_of_approvers), "Num approvers not updated"); @@ -729,8 +750,13 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { for removed_idx in 2..5 { let removed_owner_key = [Felt::new(removed_idx), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let removed_owner_slot = - updated_multisig_account.storage().get_map_item(1, removed_owner_key).unwrap(); + let removed_owner_slot = updated_multisig_account + .storage() + .get_map_item( + AuthEcdsaK256KeccakMultisig::approver_public_keys_slot(), + removed_owner_key, + ) + .unwrap(); assert_eq!( removed_owner_slot, Word::empty(), @@ -743,7 +769,10 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { let mut non_empty_count = 0; for i in 0..5 { let storage_key = [Felt::new(i as u64), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let storage_item = updated_multisig_account.storage().get_map_item(1, storage_key).unwrap(); + let storage_item = updated_multisig_account + .storage() + .get_map_item(AuthEcdsaK256KeccakMultisig::approver_public_keys_slot(), storage_key) + .unwrap(); if storage_item != Word::empty() { non_empty_count += 1; @@ -825,12 +854,12 @@ async fn test_multisig_new_approvers_cannot_sign_before_update() -> anyhow::Resu // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::update_signers_and_threshold + call.::ecdsa_k256_keccak_multisig::update_signers_and_threshold end "; - let tx_script = ScriptBuilder::new(true) - .with_dynamically_linked_library(&ecdsa_k256_keccak_multisig_library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(ecdsa_k256_keccak_multisig_library())? .compile_tx_script(tx_script_code)?; let advice_inputs = AdviceInputs { @@ -978,12 +1007,9 @@ async fn test_multisig_proc_threshold_overrides() -> anyhow::Result<()> { Default::default(), &mut RpoRandomCoin::new(Word::from([Felt::new(42); 4])), )?; - let multisig_account_interface = AccountInterface::from(&multisig_account); - let send_note_transaction_script = multisig_account_interface.build_send_notes_script( - &[output_note.clone().into()], - None, - false, - )?; + let multisig_account_interface = AccountInterface::from_account(&multisig_account); + let send_note_transaction_script = + multisig_account_interface.build_send_notes_script(&[output_note.clone().into()], None)?; // Execute transaction without signatures to get tx summary let tx_context_init = mock_chain diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index 4d20657bc5..e205cbfe3e 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -1,23 +1,30 @@ -use miden_lib::account::components::rpo_falcon_512_multisig_library; -use miden_lib::account::interface::AccountInterface; -use miden_lib::account::wallets::BasicWallet; -use miden_lib::errors::tx_kernel_errors::ERR_TX_ALREADY_EXECUTED; -use miden_lib::note::create_p2id_note; -use miden_lib::testing::account_interface::get_public_keys_from_account; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::auth::{AuthSecretKey, PublicKey}; -use miden_objects::account::{Account, AccountBuilder, AccountId, AccountStorageMode, AccountType}; -use miden_objects::asset::FungibleAsset; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::{ +use miden_processor::AdviceInputs; +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::auth::{AuthSecretKey, PublicKey}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountId, + AccountStorageMode, + AccountType, +}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::note::NoteType; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, }; -use miden_objects::transaction::OutputNote; -use miden_objects::vm::AdviceMap; -use miden_objects::{Felt, Hasher, Word}; -use miden_processor::AdviceInputs; -use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::transaction::OutputNote; +use miden_protocol::vm::AdviceMap; +use miden_protocol::{Felt, Hasher, Word}; +use miden_standards::account::auth::AuthFalcon512RpoMultisig; +use miden_standards::account::components::falcon_512_rpo_multisig_library; +use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::ERR_TX_ALREADY_EXECUTED; +use miden_standards::note::create_p2id_note; +use miden_standards::testing::account_interface::get_public_keys_from_account; use miden_testing::utils::create_spawn_note; use miden_testing::{Auth, MockChainBuilder, assert_transaction_executor_error}; use miden_tx::TransactionExecutorError; @@ -44,7 +51,7 @@ fn setup_keys_and_authenticators( let mut authenticators = Vec::new(); for _ in 0..num_approvers { - let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let sec_key = AuthSecretKey::new_falcon512_rpo_with_rng(&mut rng); let pub_key = sec_key.public_key(); secret_keys.push(sec_key); @@ -394,12 +401,12 @@ async fn test_multisig_update_signers() -> anyhow::Result<()> { // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::update_signers_and_threshold + call.::falcon_512_rpo_multisig::update_signers_and_threshold end "; - let tx_script = ScriptBuilder::new(true) - .with_dynamically_linked_library(&rpo_falcon_512_multisig_library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(falcon_512_rpo_multisig_library())? .compile_tx_script(tx_script_code)?; let advice_inputs = AdviceInputs { @@ -462,15 +469,20 @@ async fn test_multisig_update_signers() -> anyhow::Result<()> { // Verify that the public keys were actually updated in storage for (i, expected_key) in new_public_keys.iter().enumerate() { let storage_key = [Felt::new(i as u64), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let storage_item = updated_multisig_account.storage().get_map_item(1, storage_key).unwrap(); + let storage_item = updated_multisig_account + .storage() + .get_map_item(AuthFalcon512RpoMultisig::approver_public_keys_slot(), storage_key) + .unwrap(); let expected_word: Word = expected_key.to_commitment().into(); assert_eq!(storage_item, expected_word, "Public key {} doesn't match expected value", i); } - // Verify the threshold was updated by checking storage slot 0 - let threshold_config_storage = updated_multisig_account.storage().get_item(0).unwrap(); + // Verify the threshold was updated by checking the config storage slot + let threshold_config_storage = updated_multisig_account + .storage() + .get_item(AuthFalcon512RpoMultisig::threshold_config_slot())?; assert_eq!( threshold_config_storage[0], @@ -631,9 +643,11 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { advice_map.insert(multisig_config_hash, config_and_pubkeys_vector); // Create transaction script - let tx_script = ScriptBuilder::new(true) - .with_dynamically_linked_library(&rpo_falcon_512_multisig_library())? - .compile_tx_script("begin\n call.::update_signers_and_threshold\nend")?; + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(falcon_512_rpo_multisig_library())? + .compile_tx_script( + "begin\n call.::falcon_512_rpo_multisig::update_signers_and_threshold\nend", + )?; let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; @@ -699,13 +713,17 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { // Verify public keys were updated for (i, expected_key) in new_public_keys.iter().enumerate() { let storage_key = [Felt::new(i as u64), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let storage_item = updated_multisig_account.storage().get_map_item(1, storage_key).unwrap(); + let storage_item = updated_multisig_account + .storage() + .get_map_item(AuthFalcon512RpoMultisig::approver_public_keys_slot(), storage_key)?; let expected_word: Word = expected_key.to_commitment().into(); assert_eq!(storage_item, expected_word, "Public key {} doesn't match", i); } // Verify threshold and num_approvers - let threshold_config = updated_multisig_account.storage().get_item(0).unwrap(); + let threshold_config = updated_multisig_account + .storage() + .get_item(AuthFalcon512RpoMultisig::threshold_config_slot())?; assert_eq!(threshold_config[0], Felt::new(threshold), "Threshold not updated"); assert_eq!(threshold_config[1], Felt::new(num_of_approvers), "Num approvers not updated"); @@ -725,8 +743,10 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { for removed_idx in 2..5 { let removed_owner_key = [Felt::new(removed_idx), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let removed_owner_slot = - updated_multisig_account.storage().get_map_item(1, removed_owner_key).unwrap(); + let removed_owner_slot = updated_multisig_account + .storage() + .get_map_item(AuthFalcon512RpoMultisig::approver_public_keys_slot(), removed_owner_key) + .unwrap(); assert_eq!( removed_owner_slot, Word::empty(), @@ -739,7 +759,10 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { let mut non_empty_count = 0; for i in 0..5 { let storage_key = [Felt::new(i as u64), Felt::new(0), Felt::new(0), Felt::new(0)].into(); - let storage_item = updated_multisig_account.storage().get_map_item(1, storage_key).unwrap(); + let storage_item = updated_multisig_account + .storage() + .get_map_item(AuthFalcon512RpoMultisig::approver_public_keys_slot(), storage_key) + .unwrap(); if storage_item != Word::empty() { non_empty_count += 1; @@ -821,12 +844,12 @@ async fn test_multisig_new_approvers_cannot_sign_before_update() -> anyhow::Resu // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::update_signers_and_threshold + call.::falcon_512_rpo_multisig::update_signers_and_threshold end "; - let tx_script = ScriptBuilder::new(true) - .with_dynamically_linked_library(&rpo_falcon_512_multisig_library())? + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(falcon_512_rpo_multisig_library())? .compile_tx_script(tx_script_code)?; let advice_inputs = AdviceInputs { @@ -974,12 +997,9 @@ async fn test_multisig_proc_threshold_overrides() -> anyhow::Result<()> { Default::default(), &mut RpoRandomCoin::new(Word::from([Felt::new(42); 4])), )?; - let multisig_account_interface = AccountInterface::from(&multisig_account); - let send_note_transaction_script = multisig_account_interface.build_send_notes_script( - &[output_note.clone().into()], - None, - false, - )?; + let multisig_account_interface = AccountInterface::from_account(&multisig_account); + let send_note_transaction_script = + multisig_account_interface.build_send_notes_script(&[output_note.clone().into()], None)?; // Execute transaction without signatures to get tx summary let tx_context_init = mock_chain diff --git a/crates/miden-testing/tests/auth/rpo_falcon_acl.rs b/crates/miden-testing/tests/auth/rpo_falcon_acl.rs index 2b8d51fe19..d6305848fb 100644 --- a/crates/miden-testing/tests/auth/rpo_falcon_acl.rs +++ b/crates/miden-testing/tests/auth/rpo_falcon_acl.rs @@ -1,10 +1,8 @@ use core::slice; +use anyhow::Context; use assert_matches::assert_matches; -use miden_lib::testing::account_component::MockAccountComponent; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{ +use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, @@ -12,9 +10,14 @@ use miden_objects::account::{ AccountStorageMode, AccountType, }; -use miden_objects::note::Note; -use miden_objects::transaction::OutputNote; -use miden_objects::{Felt, FieldElement, Word}; +use miden_protocol::note::Note; +use miden_protocol::testing::storage::MOCK_VALUE_SLOT0; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, FieldElement, Word}; +use miden_standards::account::auth::AuthFalcon512RpoAcl; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockAccountComponent; +use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain}; use miden_tx::TransactionExecutorError; @@ -22,7 +25,7 @@ use miden_tx::TransactionExecutorError; // ================================================================================================ const TX_SCRIPT_NO_TRIGGER: &str = r#" - use.mock::account + use mock::account begin call.account::account_procedure_1 drop @@ -32,7 +35,7 @@ const TX_SCRIPT_NO_TRIGGER: &str = r#" // HELPER FUNCTIONS // ================================================================================================ -/// Sets up the basic components needed for RPO Falcon ACL tests. +/// Sets up the basic components needed for Falcon RPO ACL tests. /// Returns (account, mock_chain, note). fn setup_rpo_falcon_acl_test( allow_unauthorized_output_notes: bool, @@ -42,10 +45,10 @@ fn setup_rpo_falcon_acl_test( MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()).into(); let get_item_proc_root = component - .get_procedure_root_by_name("mock::account::get_item") + .get_procedure_root_by_path("mock::account::get_item") .expect("get_item procedure should exist"); let set_item_proc_root = component - .get_procedure_root_by_name("mock::account::set_item") + .get_procedure_root_by_path("mock::account::set_item") .expect("set_item procedure should exist"); let auth_trigger_procedures = vec![get_item_proc_root, set_item_proc_root]; @@ -84,10 +87,10 @@ async fn test_rpo_falcon_acl() -> anyhow::Result<()> { MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()).into(); let get_item_proc_root = component - .get_procedure_root_by_name("mock::account::get_item") + .get_procedure_root_by_path("mock::account::get_item") .expect("get_item procedure should exist"); let set_item_proc_root = component - .get_procedure_root_by_name("mock::account::set_item") + .get_procedure_root_by_path("mock::account::set_item") .expect("set_item procedure should exist"); let auth_trigger_procedures = vec![get_item_proc_root, set_item_proc_root]; @@ -98,34 +101,45 @@ async fn test_rpo_falcon_acl() -> anyhow::Result<()> { } .build_component(); - let tx_script_with_trigger_1 = r#" - use.mock::account + let tx_script_with_trigger_1 = format!( + r#" + use mock::account + + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin - push.0 + push.MOCK_VALUE_SLOT0[0..2] call.account::get_item dropw end - "#; + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + ); + + let tx_script_with_trigger_2 = format!( + r#" + use mock::account - let tx_script_with_trigger_2 = r#" - use.mock::account + const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}") begin - push.1.2.3.4 push.0 + push.1.2.3.4 + push.MOCK_VALUE_SLOT0[0..2] call.account::set_item dropw dropw end - "#; + "#, + mock_value_slot0 = &*MOCK_VALUE_SLOT0, + ); let tx_script_trigger_1 = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(tx_script_with_trigger_1)?; + CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_with_trigger_1)?; let tx_script_trigger_2 = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(tx_script_with_trigger_2)?; + CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_with_trigger_2)?; let tx_script_no_trigger = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; + CodeBuilder::with_mock_libraries().compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; // Test 1: Transaction WITH authenticator calling trigger procedure 1 (should succeed) let tx_context_with_auth_1 = mock_chain @@ -137,7 +151,7 @@ async fn test_rpo_falcon_acl() -> anyhow::Result<()> { tx_context_with_auth_1 .execute() .await - .expect("trigger 1 with auth should succeed"); + .context("trigger 1 with auth should succeed")?; // Test 2: Transaction WITH authenticator calling trigger procedure 2 (should succeed) let tx_context_with_auth_2 = mock_chain @@ -149,7 +163,7 @@ async fn test_rpo_falcon_acl() -> anyhow::Result<()> { tx_context_with_auth_2 .execute() .await - .expect("trigger 2 with auth should succeed"); + .context("trigger 2 with auth should succeed")?; // Test 3: Transaction WITHOUT authenticator calling trigger procedure (should fail) let tx_context_no_auth = mock_chain @@ -172,7 +186,7 @@ async fn test_rpo_falcon_acl() -> anyhow::Result<()> { let executed = tx_context_no_trigger .execute() .await - .expect("no trigger, no auth should succeed"); + .context("no trigger, no auth should succeed")?; assert_eq!( executed.account_delta().nonce_delta(), Felt::ZERO, @@ -187,15 +201,18 @@ async fn test_rpo_falcon_acl_with_allow_unauthorized_output_notes() -> anyhow::R let (account, mock_chain, note) = setup_rpo_falcon_acl_test(true, true)?; // Verify the storage layout includes both authorization flags - let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed"); - // Slot 1 should be [num_tracked_procs, allow_unauthorized_output_notes, + let config_slot = account + .storage() + .get_item(AuthFalcon512RpoAcl::config_slot()) + .expect("config storage slot access failed"); + // Config Slot should be [num_trigger_procs, allow_unauthorized_output_notes, // allow_unauthorized_input_notes, 0] With 2 procedures, // allow_unauthorized_output_notes=true, and allow_unauthorized_input_notes=true, this should be // [2, 1, 1, 0] - assert_eq!(slot_1, Word::from([2u32, 1, 1, 0])); + assert_eq!(config_slot, Word::from([2u32, 1, 1, 0])); let tx_script_no_trigger = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; + CodeBuilder::with_mock_libraries().compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; // Test: Transaction WITHOUT authenticator calling non-trigger procedure (should succeed) // This tests that when allow_unauthorized_output_notes=true, transactions without @@ -224,15 +241,18 @@ async fn test_rpo_falcon_acl_with_disallow_unauthorized_input_notes() -> anyhow: let (account, mock_chain, note) = setup_rpo_falcon_acl_test(true, false)?; // Verify the storage layout includes both flags - let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed"); - // Slot 1 should be [num_tracked_procs, allow_unauthorized_output_notes, + let config_slot = account + .storage() + .get_item(AuthFalcon512RpoAcl::config_slot()) + .expect("config storage slot access failed"); + // Config Slot should be [num_trigger_procs, allow_unauthorized_output_notes, // allow_unauthorized_input_notes, 0] With 2 procedures, // allow_unauthorized_output_notes=true, and allow_unauthorized_input_notes=false, this should // be [2, 1, 0, 0] - assert_eq!(slot_1, Word::from([2u32, 1, 0, 0])); + assert_eq!(config_slot, Word::from([2u32, 1, 0, 0])); let tx_script_no_trigger = - ScriptBuilder::with_mock_libraries()?.compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; + CodeBuilder::with_mock_libraries().compile_tx_script(TX_SCRIPT_NO_TRIGGER)?; // Test: Transaction WITHOUT authenticator calling non-trigger procedure but consuming input // notes This should FAIL because allow_unauthorized_input_notes=false and we're consuming diff --git a/crates/miden-testing/tests/lib.rs b/crates/miden-testing/tests/lib.rs index c5992e03d5..04df2369c4 100644 --- a/crates/miden-testing/tests/lib.rs +++ b/crates/miden-testing/tests/lib.rs @@ -1,18 +1,19 @@ extern crate alloc; +mod agglayer; mod auth; mod scripts; mod wallet; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::AccountId; -use miden_objects::asset::FungibleAsset; -use miden_objects::crypto::utils::Serializable; -use miden_objects::note::{Note, NoteAssets, NoteInputs, NoteMetadata, NoteRecipient, NoteType}; -use miden_objects::testing::account_id::ACCOUNT_ID_SENDER; -use miden_objects::transaction::{ExecutedTransaction, ProvenTransaction}; -use miden_objects::{Word, ZERO}; use miden_processor::utils::Deserializable; +use miden_protocol::Word; +use miden_protocol::account::AccountId; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::crypto::utils::Serializable; +use miden_protocol::note::{Note, NoteAssets, NoteInputs, NoteMetadata, NoteRecipient, NoteType}; +use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; +use miden_protocol::transaction::{ExecutedTransaction, ProvenTransaction}; +use miden_standards::code_builder::CodeBuilder; use miden_tx::{ LocalTransactionProver, ProvingOptions, @@ -27,21 +28,26 @@ use miden_tx::{ pub fn prove_and_verify_transaction( executed_transaction: ExecutedTransaction, ) -> Result<(), TransactionVerifierError> { + use miden_protocol::transaction::TransactionHeader; + let executed_transaction_id = executed_transaction.id(); + let executed_tx_header = TransactionHeader::from(&executed_transaction); // Prove the transaction let proof_options = ProvingOptions::default(); let prover = LocalTransactionProver::new(proof_options); let proven_transaction = prover.prove(executed_transaction).unwrap(); + let proven_tx_header = TransactionHeader::from(&proven_transaction); assert_eq!(proven_transaction.id(), executed_transaction_id); + assert_eq!(proven_tx_header, executed_tx_header); // Serialize & deserialize the ProvenTransaction let serialised_transaction = proven_transaction.to_bytes(); let proven_transaction = ProvenTransaction::read_from_bytes(&serialised_transaction).unwrap(); // Verify that the generated proof is valid - let verifier = TransactionVerifier::new(miden_objects::MIN_PROOF_SECURITY_LEVEL); + let verifier = TransactionVerifier::new(miden_protocol::MIN_PROOF_SECURITY_LEVEL); verifier.verify(&proven_transaction) } @@ -51,16 +57,12 @@ pub fn get_note_with_fungible_asset_and_script( fungible_asset: FungibleAsset, note_script: &str, ) -> Note { - use miden_objects::note::NoteExecutionHint; - - let note_script = ScriptBuilder::default().compile_note_script(note_script).unwrap(); + let note_script = CodeBuilder::default().compile_note_script(note_script).unwrap(); let serial_num = Word::from([1, 2, 3, 4u32]); let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); let vault = NoteAssets::new(vec![fungible_asset.into()]).unwrap(); - let metadata = - NoteMetadata::new(sender_id, NoteType::Public, 1.into(), NoteExecutionHint::Always, ZERO) - .unwrap(); + let metadata = NoteMetadata::new(sender_id, NoteType::Public, 1.into()); let inputs = NoteInputs::new(vec![]).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, inputs); diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 6cbf19857d..aaca772c55 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -1,27 +1,22 @@ extern crate alloc; +use alloc::sync::Arc; use core::slice; -use std::sync::Arc; - -use miden_lib::account::faucets::{BasicFungibleFaucet, FungibleFaucetExt, NetworkFungibleFaucet}; -use miden_lib::errors::tx_kernel_errors::ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED; -use miden_lib::note::{create_burn_note, create_mint_note}; -use miden_lib::testing::note::NoteBuilder; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{ + +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::{ Account, AccountId, AccountIdVersion, AccountStorageMode, AccountType, }; -use miden_objects::assembly::DefaultSourceManager; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::note::{ +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, - NoteExecutionMode, + NoteAttachment, NoteId, NoteInputs, NoteMetadata, @@ -29,10 +24,21 @@ use miden_objects::note::{ NoteTag, NoteType, }; -use miden_objects::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER; -use miden_objects::transaction::{ExecutedTransaction, OutputNote}; -use miden_objects::{Felt, Word}; -use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER; +use miden_protocol::transaction::{ExecutedTransaction, OutputNote}; +use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::{ + BasicFungibleFaucet, + FungibleFaucetExt, + NetworkFungibleFaucet, +}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::{ + ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED, + ERR_SENDER_NOT_OWNER, +}; +use miden_standards::note::{MintNoteInputs, WellKnownNote, create_burn_note, create_mint_note}; +use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use crate::scripts::swap::create_p2id_note_exact; @@ -45,8 +51,6 @@ use crate::{get_note_with_fungible_asset_and_script, prove_and_verify_transactio pub struct FaucetTestParams { pub recipient: Word, pub tag: NoteTag, - pub aux: Felt, - pub note_execution_hint: NoteExecutionHint, pub note_type: NoteType, pub amount: Felt, } @@ -57,17 +61,15 @@ pub fn create_mint_script_code(params: &FaucetTestParams) -> String { " begin # pad the stack before call - push.0.0.0 padw + padw padw push.0 push.{recipient} - push.{note_execution_hint} push.{note_type} - push.{aux} push.{tag} push.{amount} - # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] + # => [amount, tag, note_type, RECIPIENT, pad(9)] - call.::miden::contracts::faucets::basic_fungible::distribute + call.::miden::standards::faucets::basic_fungible::distribute # => [note_idx, pad(15)] # truncate the stack @@ -76,9 +78,7 @@ pub fn create_mint_script_code(params: &FaucetTestParams) -> String { ", note_type = params.note_type as u8, recipient = params.recipient, - aux = params.aux, tag = u32::from(params.tag), - note_execution_hint = Felt::from(params.note_execution_hint), amount = params.amount, ) } @@ -91,7 +91,7 @@ pub async fn execute_mint_transaction( ) -> anyhow::Result { let source_manager = Arc::new(DefaultSourceManager::default()); let tx_script_code = create_mint_script_code(params); - let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) .compile_tx_script(tx_script_code)?; let tx_context = mock_chain .build_tx_context(faucet, &[], &[])? @@ -117,13 +117,7 @@ pub fn verify_minted_output_note( assert_eq!(output_note.id(), id); assert_eq!( output_note.metadata(), - &NoteMetadata::new( - faucet.id(), - params.note_type, - params.tag, - params.note_execution_hint, - params.aux - )? + &NoteMetadata::new(faucet.id(), params.note_type, params.tag) ); Ok(()) @@ -141,18 +135,11 @@ async fn minting_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result< let params = FaucetTestParams { recipient: Word::from([0, 1, 2, 3u32]), - tag: NoteTag::for_local_use_case(0, 0).unwrap(), - aux: Felt::new(27), - note_execution_hint: NoteExecutionHint::on_block_slot(5, 6, 7), + tag: NoteTag::default(), note_type: NoteType::Private, amount: Felt::new(100), }; - params - .tag - .validate(params.note_type) - .expect("note tag should support private notes"); - let executed_transaction = execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?; verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?; @@ -169,7 +156,6 @@ async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyho let mock_chain = builder.build()?; let recipient = Word::from([0, 1, 2, 3u32]); - let aux = Felt::new(27); let tag = Felt::new(4); let amount = Felt::new(250); @@ -177,16 +163,15 @@ async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyho " begin # pad the stack before call - push.0.0.0 padw + padw padw push.0 push.{recipient} push.{note_type} - push.{aux} push.{tag} push.{amount} - # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] + # => [amount, tag, note_type, RECIPIENT, pad(9)] - call.::miden::contracts::faucets::basic_fungible::distribute + call.::miden::standards::faucets::basic_fungible::distribute # => [note_idx, pad(15)] # truncate the stack @@ -198,7 +183,7 @@ async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyho recipient = recipient, ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_code)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_code)?; let tx = mock_chain .build_tx_context(faucet.id(), &[], &[])? .tx_script(tx_script) @@ -226,18 +211,11 @@ async fn minting_fungible_asset_on_new_faucet_succeeds() -> anyhow::Result<()> { let params = FaucetTestParams { recipient: Word::from([0, 1, 2, 3u32]), - tag: NoteTag::for_local_use_case(0, 0).unwrap(), - aux: Felt::new(27), - note_execution_hint: NoteExecutionHint::on_block_slot(5, 6, 7), + tag: NoteTag::default(), note_type: NoteType::Private, amount: Felt::new(100), }; - params - .tag - .validate(params.note_type) - .expect("note tag should support private notes"); - let executed_transaction = execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?; verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?; @@ -263,7 +241,7 @@ async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::R dropw # => [] - call.::miden::contracts::faucets::basic_fungible::burn + call.::miden::standards::faucets::basic_fungible::burn # => [ASSET] # truncate the stack @@ -276,10 +254,12 @@ async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::R builder.add_output_note(OutputNote::Full(note.clone())); let mock_chain = builder.build()?; - // The Fungible Faucet component is added as the second component after auth, so it's storage - // slot offset will be 2. Check that max_supply at the word's index 0 is 200. The remainder of - // the word is initialized with the metadata of the faucet which we don't need to check. - assert_eq!(faucet.storage().get_item(2).unwrap()[0], Felt::new(200)); + // Check that max_supply at the word's index 0 is 200. The remainder of the word is initialized + // with the metadata of the faucet which we don't need to check. + assert_eq!( + faucet.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap()[0], + Felt::new(200) + ); // Check that the faucet reserved slot has been correctly initialized. // The already issued amount should be 100. @@ -318,15 +298,13 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul // Parameters for the PUBLIC note that will be created by the faucet let recipient_account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?; let amount = Felt::new(75); - let tag = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local)?; - let aux = Felt::new(27); - let note_execution_hint = NoteExecutionHint::on_block_slot(5, 6, 7); + let tag = NoteTag::default(); let note_type = NoteType::Public; // Create a simple output note script let output_note_script_code = "begin push.1 drop end"; let source_manager = Arc::new(DefaultSourceManager::default()); - let output_note_script = ScriptBuilder::with_source_manager(source_manager.clone()) + let output_note_script = CodeBuilder::with_source_manager(source_manager.clone()) .compile_note_script(output_note_script_code)?; let serial_num = Word::default(); @@ -351,12 +329,12 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul let output_script_root = note_recipient.script().root(); let asset = FungibleAsset::new(faucet.id(), amount.into())?; - let metadata = NoteMetadata::new(faucet.id(), note_type, tag, note_execution_hint, aux)?; + let metadata = NoteMetadata::new(faucet.id(), note_type, tag); let expected_note = Note::new(NoteAssets::new(vec![asset.into()])?, metadata, note_recipient); let trigger_note_script_code = format!( " - use.miden::note + use miden::protocol::note begin # Build recipient hash from SERIAL_NUM, SCRIPT_ROOT, and INPUTS_COMMITMENT @@ -382,14 +360,12 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul # => [RECIPIENT] # Now call distribute with the computed recipient - push.{note_execution_hint} push.{note_type} - push.{aux} push.{tag} push.{amount} - # => [amount, tag, aux, note_type, execution_hint, RECIPIENT] + # => [amount, tag, note_type, RECIPIENT] - call.::miden::contracts::faucets::basic_fungible::distribute + call.::miden::standards::faucets::basic_fungible::distribute # => [note_idx, pad(15)] # Truncate the stack @@ -406,9 +382,7 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul input6 = note_inputs.values()[6], script_root = output_script_root, serial_num = serial_num, - aux = aux, tag = u32::from(tag), - note_execution_hint = Felt::from(note_execution_hint), amount = amount, ); @@ -416,9 +390,7 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul let mut rng = RpoRandomCoin::new([Felt::from(1u32); 4].into()); let trigger_note = NoteBuilder::new(faucet.id(), &mut rng) .note_type(NoteType::Private) - .tag(NoteTag::for_local_use_case(0, 0)?.into()) - .note_execution_hint(NoteExecutionHint::always()) - .aux(Felt::new(0)) + .tag(NoteTag::default().into()) .serial_number(Word::from([1, 2, 3, 4u32])) .code(trigger_note_script_code) .build()?; @@ -502,11 +474,15 @@ async fn network_faucet_mint() -> anyhow::Result<()> { // The Network Fungible Faucet component is added as the second component after auth, so its // storage slot offset will be 2. Check that max_supply at the word's index 0 is 200. - assert_eq!(faucet.storage().get_item(1).unwrap()[0], Felt::new(1000)); + assert_eq!( + faucet.storage().get_item(NetworkFungibleFaucet::metadata_slot()).unwrap()[0], + Felt::new(1000) + ); // Check that the creator account ID is stored in slot 2 (second storage slot of the component) // The owner_account_id is stored as Word [0, 0, suffix, prefix] - let stored_owner_id = faucet.storage().get_item(2).unwrap(); + let stored_owner_id = + faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot()).unwrap(); assert_eq!(stored_owner_id[3], faucet_owner_account_id.prefix().as_felt()); assert_eq!(stored_owner_id[2], Felt::new(faucet_owner_account_id.suffix().as_int())); @@ -519,31 +495,28 @@ async fn network_faucet_mint() -> anyhow::Result<()> { let amount = Felt::new(75); let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - let aux = Felt::new(27); let serial_num = Word::default(); - let output_note_tag = NoteTag::from_account_id(target_account.id()); + let output_note_tag = NoteTag::with_account_target(target_account.id()); let p2id_mint_output_note = create_p2id_note_exact( faucet.id(), target_account.id(), vec![mint_asset], NoteType::Private, - aux, serial_num, ) .unwrap(); let recipient = p2id_mint_output_note.recipient().digest(); // Create the MINT note using the helper function + let mint_inputs = MintNoteInputs::new_private(recipient, amount, output_note_tag.into()); + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); let mint_note = create_mint_note( faucet.id(), faucet_owner_account_id, - recipient, - output_note_tag.into(), - amount, - aux, - aux, + mint_inputs, + NoteAttachment::default(), &mut rng, )?; @@ -590,6 +563,487 @@ async fn network_faucet_mint() -> anyhow::Result<()> { Ok(()) } +// TESTS FOR NETWORK FAUCET OWNERSHIP +// ================================================================================================ + +/// Tests that the owner can mint assets on network faucet. +#[tokio::test] +async fn test_network_faucet_owner_can_mint() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet("NET", 1000, owner_account_id, Some(50))?; + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + let mock_chain = builder.build()?; + + let amount = Felt::new(75); + let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.into())?.into(); + + let output_note_tag = NoteTag::with_account_target(target_account.id()); + let p2id_note = create_p2id_note_exact( + faucet.id(), + target_account.id(), + vec![mint_asset], + NoteType::Private, + Word::default(), + )?; + let recipient = p2id_note.recipient().digest(); + + let mint_inputs = MintNoteInputs::new_private(recipient, amount, output_note_tag.into()); + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let mint_note = create_mint_note( + faucet.id(), + owner_account_id, + mint_inputs, + NoteAttachment::default(), + &mut rng, + )?; + + let tx_context = mock_chain.build_tx_context(faucet.id(), &[], &[mint_note])?.build()?; + let executed_transaction = tx_context.execute().await?; + + assert_eq!(executed_transaction.output_notes().num_notes(), 1); + + Ok(()) +} + +/// Tests that a non-owner cannot mint assets on network faucet. +#[tokio::test] +async fn test_network_faucet_non_owner_cannot_mint() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let non_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet("NET", 1000, owner_account_id, Some(50))?; + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + let mock_chain = builder.build()?; + + let amount = Felt::new(75); + let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.into())?.into(); + + let output_note_tag = NoteTag::with_account_target(target_account.id()); + let p2id_note = create_p2id_note_exact( + faucet.id(), + target_account.id(), + vec![mint_asset], + NoteType::Private, + Word::default(), + )?; + let recipient = p2id_note.recipient().digest(); + + let mint_inputs = MintNoteInputs::new_private(recipient, amount, output_note_tag.into()); + + // Create mint note from NON-OWNER + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let mint_note = create_mint_note( + faucet.id(), + non_owner_account_id, + mint_inputs, + NoteAttachment::default(), + &mut rng, + )?; + + let tx_context = mock_chain.build_tx_context(faucet.id(), &[], &[mint_note])?.build()?; + let result = tx_context.execute().await; + + // The distribute function uses ERR_ONLY_OWNER, which is "note sender is not the owner" + let expected_error = ERR_SENDER_NOT_OWNER; + assert_transaction_executor_error!(result, expected_error); + + Ok(()) +} + +/// Tests that the owner is correctly stored and can be read from storage. +#[tokio::test] +async fn test_network_faucet_owner_storage() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet("NET", 1000, owner_account_id, Some(50))?; + let _mock_chain = builder.build()?; + + // Verify owner is stored correctly + let stored_owner = faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot())?; + + // Storage format: [0, 0, suffix, prefix] + assert_eq!(stored_owner[3], owner_account_id.prefix().as_felt()); + assert_eq!(stored_owner[2], Felt::new(owner_account_id.suffix().as_int())); + assert_eq!(stored_owner[1], Felt::new(0)); + assert_eq!(stored_owner[0], Felt::new(0)); + + Ok(()) +} + +/// Tests that transfer_ownership updates the owner correctly. +#[tokio::test] +async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // Setup: Create initial owner and new owner accounts + let initial_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let new_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = + builder.add_existing_network_faucet("NET", 1000, initial_owner_account_id, Some(50))?; + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let amount = Felt::new(75); + let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.into())?.into(); + + let output_note_tag = NoteTag::with_account_target(target_account.id()); + let p2id_note = create_p2id_note_exact( + faucet.id(), + target_account.id(), + vec![mint_asset], + NoteType::Private, + Word::default(), + )?; + let recipient = p2id_note.recipient().digest(); + + // Sanity Check: Prove that the initial owner can mint assets + let mint_inputs = MintNoteInputs::new_private(recipient, amount, output_note_tag.into()); + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let mint_note = create_mint_note( + faucet.id(), + initial_owner_account_id, + mint_inputs.clone(), + NoteAttachment::default(), + &mut rng, + )?; + + // Action: Create transfer_ownership note script + let transfer_note_script_code = format!( + r#" + use miden::standards::faucets::network_fungible->network_faucet + + begin + repeat.14 push.0 end + push.{new_owner_suffix} + push.{new_owner_prefix} + call.network_faucet::transfer_ownership + dropw dropw dropw dropw + end + "#, + new_owner_prefix = new_owner_account_id.prefix().as_felt(), + new_owner_suffix = Felt::new(new_owner_account_id.suffix().as_int()), + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let transfer_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(transfer_note_script_code.clone())?; + + // Create the transfer note and add it to the builder so it exists on-chain + let mut rng = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let transfer_note = NoteBuilder::new(initial_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([11, 22, 33, 44u32])) + .code(transfer_note_script_code.clone()) + .build()?; + + // Add the transfer note to the builder before building the chain + builder.add_output_note(OutputNote::Full(transfer_note.clone())); + let mut mock_chain = builder.build()?; + + // Prove the block to make the transfer note exist on-chain + mock_chain.prove_next_block()?; + + // Sanity Check: Execute mint transaction to verify initial owner can mint + let tx_context = mock_chain.build_tx_context(faucet.id(), &[], &[mint_note])?.build()?; + let executed_transaction = tx_context.execute().await?; + assert_eq!(executed_transaction.output_notes().num_notes(), 1); + + // Action: Execute transfer_ownership via note script + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[transfer_note.id()], &[])? + .add_note_script(transfer_note_script.clone()) + .with_source_manager(source_manager.clone()) + .build()?; + let executed_transaction = tx_context.execute().await?; + + // Persistence: Apply the transaction to update the faucet state + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + // Apply the delta to the faucet account to reflect the ownership change + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed_transaction.account_delta())?; + + // Validation 1: Try to mint using the old owner - should fail + let mut rng = RpoRandomCoin::new([Felt::from(300u32); 4].into()); + let mint_note_old_owner = create_mint_note( + updated_faucet.id(), + initial_owner_account_id, + mint_inputs.clone(), + NoteAttachment::default(), + &mut rng, + )?; + + // Use the note as an unauthenticated note (full note object) - it will be created in this + // transaction + let tx_context = mock_chain + .build_tx_context(updated_faucet.id(), &[], &[mint_note_old_owner])? + .build()?; + let result = tx_context.execute().await; + + // The distribute function uses ERR_ONLY_OWNER, which is "note sender is not the owner" + let expected_error = ERR_SENDER_NOT_OWNER; + assert_transaction_executor_error!(result, expected_error); + + // Validation 2: Try to mint using the new owner - should succeed + let mut rng = RpoRandomCoin::new([Felt::from(400u32); 4].into()); + let mint_note_new_owner = create_mint_note( + updated_faucet.id(), + new_owner_account_id, + mint_inputs, + NoteAttachment::default(), + &mut rng, + )?; + + let tx_context = mock_chain + .build_tx_context(updated_faucet.id(), &[], &[mint_note_new_owner])? + .build()?; + let executed_transaction = tx_context.execute().await?; + + // Verify that minting succeeded + assert_eq!(executed_transaction.output_notes().num_notes(), 1); + + Ok(()) +} + +/// Tests that only the owner can transfer ownership. +#[tokio::test] +async fn test_network_faucet_only_owner_can_transfer() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let non_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let new_owner_account_id = AccountId::dummy( + [3; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet("NET", 1000, owner_account_id, Some(50))?; + let mock_chain = builder.build()?; + + // Create transfer ownership note script + let transfer_note_script_code = format!( + r#" + use miden::standards::faucets::network_fungible->network_faucet + + begin + repeat.14 push.0 end + push.{new_owner_suffix} + push.{new_owner_prefix} + call.network_faucet::transfer_ownership + dropw dropw dropw dropw + end + "#, + new_owner_prefix = new_owner_account_id.prefix().as_felt(), + new_owner_suffix = Felt::new(new_owner_account_id.suffix().as_int()), + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let transfer_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(transfer_note_script_code.clone())?; + + // Create a note from NON-OWNER that tries to transfer ownership + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let transfer_note = NoteBuilder::new(non_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([10, 20, 30, 40u32])) + .code(transfer_note_script_code.clone()) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[transfer_note])? + .add_note_script(transfer_note_script.clone()) + .with_source_manager(source_manager.clone()) + .build()?; + let result = tx_context.execute().await; + + // Verify that the transaction failed with ERR_ONLY_OWNER + let expected_error = ERR_SENDER_NOT_OWNER; + assert_transaction_executor_error!(result, expected_error); + + Ok(()) +} + +/// Tests that renounce_ownership clears the owner correctly. +#[tokio::test] +async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let new_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet("NET", 1000, owner_account_id, Some(50))?; + + // Check stored value before renouncing + let stored_owner_before = + faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot())?; + assert_eq!(stored_owner_before[3], owner_account_id.prefix().as_felt()); + assert_eq!(stored_owner_before[2], Felt::new(owner_account_id.suffix().as_int())); + + // Create renounce_ownership note script + let renounce_note_script_code = r#" + use miden::standards::faucets::network_fungible->network_faucet + + begin + repeat.16 push.0 end + call.network_faucet::renounce_ownership + dropw dropw dropw dropw + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let renounce_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(renounce_note_script_code)?; + + // Create transfer note script (will be used after renounce) + let transfer_note_script_code = format!( + r#" + use miden::standards::faucets::network_fungible->network_faucet + + begin + repeat.14 push.0 end + push.{new_owner_suffix} + push.{new_owner_prefix} + call.network_faucet::transfer_ownership + dropw dropw dropw dropw + end + "#, + new_owner_prefix = new_owner_account_id.prefix().as_felt(), + new_owner_suffix = Felt::new(new_owner_account_id.suffix().as_int()), + ); + + let transfer_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(transfer_note_script_code.clone())?; + + let mut rng = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let renounce_note = NoteBuilder::new(owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([11, 22, 33, 44u32])) + .code(renounce_note_script_code) + .build()?; + + let mut rng = RpoRandomCoin::new([Felt::from(300u32); 4].into()); + let transfer_note = NoteBuilder::new(owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([50, 60, 70, 80u32])) + .code(transfer_note_script_code.clone()) + .build()?; + + builder.add_output_note(OutputNote::Full(renounce_note.clone())); + builder.add_output_note(OutputNote::Full(transfer_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // Execute renounce_ownership + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[renounce_note.id()], &[])? + .add_note_script(renounce_note_script.clone()) + .with_source_manager(source_manager.clone()) + .build()?; + let executed_transaction = tx_context.execute().await?; + + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed_transaction.account_delta())?; + + // Check stored value after renouncing - should be zero + let stored_owner_after = + updated_faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot())?; + assert_eq!(stored_owner_after[0], Felt::new(0)); + assert_eq!(stored_owner_after[1], Felt::new(0)); + assert_eq!(stored_owner_after[2], Felt::new(0)); + assert_eq!(stored_owner_after[3], Felt::new(0)); + + // Try to transfer ownership - should fail because there's no owner + // The transfer note was already added to the builder, so we need to prove another block + // to make it available on-chain after the renounce transaction + mock_chain.prove_next_block()?; + + let tx_context = mock_chain + .build_tx_context(updated_faucet.id(), &[transfer_note.id()], &[])? + .add_note_script(transfer_note_script.clone()) + .with_source_manager(source_manager.clone()) + .build()?; + let result = tx_context.execute().await; + + let expected_error = ERR_SENDER_NOT_OWNER; + assert_transaction_executor_error!(result, expected_error); + + Ok(()) +} + // TESTS FOR FAUCET PROCEDURE COMPATIBILITY // ================================================================================================ @@ -631,7 +1085,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { faucet_owner_account_id, faucet.id(), fungible_asset.into(), - Felt::new(0), + NoteAttachment::default(), &mut rng, )?; @@ -662,3 +1116,123 @@ async fn network_faucet_burn() -> anyhow::Result<()> { Ok(()) } + +// TESTS FOR MINT NOTE WITH PRIVATE AND PUBLIC OUTPUT MODES +// ================================================================================================ + +/// Tests creating a MINT note with different output note types (private/public) +/// The MINT note can create output notes with variable-length inputs for public notes. +#[rstest::rstest] +#[case::private(NoteType::Private)] +#[case::public(NoteType::Public)] +#[tokio::test] +async fn test_mint_note_output_note_types(#[case] note_type: NoteType) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let faucet_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = + builder.add_existing_network_faucet("NET", 1000, faucet_owner_account_id, Some(50))?; + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let amount = Felt::new(75); + let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); + let serial_num = Word::from([1, 2, 3, 4u32]); + + // Create the expected P2ID output note + let p2id_mint_output_note = create_p2id_note_exact( + faucet.id(), + target_account.id(), + vec![mint_asset], + note_type, + serial_num, + ) + .unwrap(); + + // Create MINT note based on note type + let mint_inputs = match note_type { + NoteType::Private => { + let output_note_tag = NoteTag::with_account_target(target_account.id()); + let recipient = p2id_mint_output_note.recipient().digest(); + MintNoteInputs::new_private(recipient, amount, output_note_tag.into()) + }, + NoteType::Public => { + let output_note_tag = NoteTag::with_account_target(target_account.id()); + let p2id_script = WellKnownNote::P2ID.script(); + let p2id_inputs = + vec![target_account.id().suffix(), target_account.id().prefix().as_felt()]; + let note_inputs = NoteInputs::new(p2id_inputs)?; + let recipient = NoteRecipient::new(serial_num, p2id_script, note_inputs); + MintNoteInputs::new_public(recipient, amount, output_note_tag.into())? + }, + NoteType::Encrypted => unreachable!("Encrypted note type not used in this test"), + }; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let mint_note = create_mint_note( + faucet.id(), + faucet_owner_account_id, + mint_inputs.clone(), + NoteAttachment::default(), + &mut rng, + )?; + + builder.add_output_note(OutputNote::Full(mint_note.clone())); + let mut mock_chain = builder.build()?; + + let mut tx_context_builder = + mock_chain.build_tx_context(faucet.id(), &[mint_note.id()], &[])?; + + if note_type == NoteType::Public { + let p2id_script = WellKnownNote::P2ID.script(); + tx_context_builder = tx_context_builder.add_note_script(p2id_script); + } + + let tx_context = tx_context_builder.build()?; + let executed_transaction = tx_context.execute().await?; + + assert_eq!(executed_transaction.output_notes().num_notes(), 1); + let output_note = executed_transaction.output_notes().get_note(0); + + match note_type { + NoteType::Private => { + // For private notes, we can only compare basic properties since we get + // OutputNote::Partial + assert_eq!(output_note.id(), p2id_mint_output_note.id()); + assert_eq!(output_note.metadata(), p2id_mint_output_note.metadata()); + }, + NoteType::Public => { + // For public notes, we get OutputNote::Full and can compare key properties + let created_note = match output_note { + OutputNote::Full(note) => note, + _ => panic!("Expected OutputNote::Full variant for public note"), + }; + + assert_eq!(created_note, &p2id_mint_output_note); + }, + NoteType::Encrypted => unreachable!("Encrypted note type not used in this test"), + } + + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + // Consume the output note with target account + let mut target_account_mut = target_account.clone(); + let consume_tx_context = mock_chain + .build_tx_context(target_account.id(), &[], slice::from_ref(&p2id_mint_output_note))? + .build()?; + let consume_executed_transaction = consume_tx_context.execute().await?; + + target_account_mut.apply_delta(consume_executed_transaction.account_delta())?; + + let expected_asset = FungibleAsset::new(faucet.id(), amount.into())?; + let balance = target_account_mut.vault().get_balance(faucet.id())?; + assert_eq!(balance, expected_asset.amount()); + + Ok(()) +} diff --git a/crates/miden-testing/tests/scripts/fee.rs b/crates/miden-testing/tests/scripts/fee.rs index 70622e4819..3fd41d7b78 100644 --- a/crates/miden-testing/tests/scripts/fee.rs +++ b/crates/miden-testing/tests/scripts/fee.rs @@ -1,6 +1,6 @@ use anyhow::Context; -use miden_objects::asset::FungibleAsset; -use miden_objects::{self, Felt, Word}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::{self, Felt, Word}; use miden_testing::{Auth, MockChain}; use crate::prove_and_verify_transaction; diff --git a/crates/miden-testing/tests/scripts/p2id.rs b/crates/miden-testing/tests/scripts/p2id.rs index dda0578692..7d5e438a2a 100644 --- a/crates/miden-testing/tests/scripts/p2id.rs +++ b/crates/miden-testing/tests/scripts/p2id.rs @@ -1,19 +1,19 @@ -use miden_lib::errors::note_script_errors::ERR_P2ID_TARGET_ACCT_MISMATCH; -use miden_lib::note::create_p2id_note; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::Account; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset}; -use miden_objects::crypto::rand::RpoRandomCoin; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::{ +use miden_protocol::account::Account; +use miden_protocol::asset::{Asset, AssetVault, FungibleAsset}; +use miden_protocol::crypto::rand::RpoRandomCoin; +use miden_protocol::note::{NoteAttachment, NoteType}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, ACCOUNT_ID_SENDER, }; -use miden_objects::transaction::OutputNote; -use miden_objects::{Felt, Word}; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, Word}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::ERR_P2ID_TARGET_ACCT_MISMATCH; +use miden_standards::note::create_p2id_note; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use crate::prove_and_verify_transaction; @@ -207,7 +207,7 @@ async fn test_create_consume_multiple_notes() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into()?, vec![FungibleAsset::mock(10)], NoteType::Public, - Felt::new(0), + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), )?; @@ -216,34 +216,30 @@ async fn test_create_consume_multiple_notes() -> anyhow::Result<()> { ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?, vec![FungibleAsset::mock(5)], NoteType::Public, - Felt::new(0), + NoteAttachment::default(), &mut RpoRandomCoin::new(Word::from([4, 3, 2, 1u32])), )?; let tx_script_src = &format!( " - use.miden::output_note + use miden::protocol::output_note begin push.{recipient_1} - push.{note_execution_hint_1} push.{note_type_1} - push.0 # aux push.{tag_1} - call.output_note::create + exec.output_note::create push.{asset_1} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw dropw dropw dropw push.{recipient_2} - push.{note_execution_hint_2} push.{note_type_2} - push.0 # aux push.{tag_2} - call.output_note::create + exec.output_note::create push.{asset_2} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw dropw dropw dropw end ", @@ -251,15 +247,13 @@ async fn test_create_consume_multiple_notes() -> anyhow::Result<()> { note_type_1 = NoteType::Public as u8, tag_1 = Felt::from(output_note_1.metadata().tag()), asset_1 = Word::from(FungibleAsset::mock(10)), - note_execution_hint_1 = Felt::from(output_note_1.metadata().execution_hint()), recipient_2 = output_note_2.recipient().digest(), note_type_2 = NoteType::Public as u8, tag_2 = Felt::from(output_note_2.metadata().tag()), asset_2 = Word::from(FungibleAsset::mock(5)), - note_execution_hint_2 = Felt::from(output_note_2.metadata().execution_hint()) ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; let tx_context = mock_chain .build_tx_context(account.id(), &[input_note_1.id(), input_note_2.id()], &[])? diff --git a/crates/miden-testing/tests/scripts/p2ide.rs b/crates/miden-testing/tests/scripts/p2ide.rs index d149ced65f..a7ac7aa0a1 100644 --- a/crates/miden-testing/tests/scripts/p2ide.rs +++ b/crates/miden-testing/tests/scripts/p2ide.rs @@ -1,17 +1,17 @@ use core::slice; use anyhow::Context; -use miden_lib::errors::note_script_errors::{ +use miden_protocol::Felt; +use miden_protocol::account::Account; +use miden_protocol::asset::{Asset, AssetVault, FungibleAsset}; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::{Note, NoteType}; +use miden_standards::errors::standards::{ ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER, ERR_P2IDE_RECLAIM_DISABLED, ERR_P2IDE_RECLAIM_HEIGHT_NOT_REACHED, ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED, }; -use miden_objects::Felt; -use miden_objects::account::Account; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset}; -use miden_objects::block::BlockNumber; -use miden_objects::note::{Note, NoteType}; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; /// Test that the P2IDE note works like a regular P2ID note diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index e18f44942d..f80a4c5a06 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -1,15 +1,13 @@ use core::slice; use std::collections::BTreeMap; -use miden_lib::account::interface::AccountInterface; -use miden_lib::utils::ScriptBuilder; -use miden_objects::Word; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin}; -use miden_objects::note::{ +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; +use miden_protocol::note::{ Note, NoteAssets, - NoteExecutionHint, + NoteAttachment, + NoteAttachmentScheme, NoteInputs, NoteMetadata, NoteRecipient, @@ -17,13 +15,16 @@ use miden_objects::note::{ NoteType, PartialNote, }; -use miden_objects::transaction::OutputNote; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, Word}; +use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; +use miden_standards::code_builder::CodeBuilder; use miden_testing::{Auth, MockChain}; /// Tests the execution of the generated send_note transaction script in case the sending account /// has the [`BasicWallet`][wallet] interface. /// -/// [wallet]: miden_lib::account::interface::AccountComponentInterface::BasicWallet +/// [wallet]: miden_standards::account::interface::AccountComponentInterface::BasicWallet #[tokio::test] async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { let sent_asset = FungibleAsset::mock(10); @@ -33,18 +34,15 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { builder.add_existing_wallet_with_assets(Auth::BasicAuth, [FungibleAsset::mock(100)])?; let mock_chain = builder.build()?; - let sender_account_interface = AccountInterface::from(&sender_basic_wallet_account); + let sender_account_interface = AccountInterface::from_account(&sender_basic_wallet_account); - let tag = NoteTag::from_account_id(sender_basic_wallet_account.id()); - let metadata = NoteMetadata::new( - sender_basic_wallet_account.id(), - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - )?; + let tag = NoteTag::with_account_target(sender_basic_wallet_account.id()); + let elements = [9, 8, 7, 6, 5u32].map(Felt::from).to_vec(); + let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42), elements.clone())?; + let metadata = NoteMetadata::new(sender_basic_wallet_account.id(), NoteType::Public, tag) + .with_attachment(attachment.clone()); let assets = NoteAssets::new(vec![sent_asset]).unwrap(); - let note_script = ScriptBuilder::default().compile_note_script("begin nop end").unwrap(); + let note_script = CodeBuilder::default().compile_note_script("begin nop end").unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); @@ -52,17 +50,17 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { let partial_note: PartialNote = note.clone().into(); let expiration_delta = 10u16; - let send_note_transaction_script = sender_account_interface.build_send_notes_script( - slice::from_ref(&partial_note), - Some(expiration_delta), - false, - )?; + let send_note_transaction_script = sender_account_interface + .build_send_notes_script(slice::from_ref(&partial_note), Some(expiration_delta))?; let executed_transaction = mock_chain .build_tx_context(sender_basic_wallet_account.id(), &[], &[]) .expect("failed to build tx context") + // TODO: This shouldn't be necessary. The attachment should be included in the tx + // script's mast forest's advice map. + .extend_advice_map(vec![(attachment.content().to_word(), elements)]) .tx_script(send_note_transaction_script) - .extend_expected_output_notes(vec![OutputNote::Full(note)]) + .extend_expected_output_notes(vec![OutputNote::Full(note.clone())]) .build()? .execute() .await?; @@ -80,6 +78,7 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { sent_asset, "sent asset should be in removed assets" ); + assert_eq!(executed_transaction.output_notes().get_note(0), &OutputNote::Full(note)); Ok(()) } @@ -87,7 +86,7 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { /// Tests the execution of the generated send_note transaction script in case the sending account /// has the [`BasicFungibleFaucet`][faucet] interface. /// -/// [faucet]: miden_lib::account::interface::AccountComponentInterface::BasicFungibleFaucet +/// [faucet]: miden_standards::account::interface::AccountComponentInterface::BasicFungibleFaucet #[tokio::test] async fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -95,20 +94,18 @@ async fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { builder.add_existing_basic_faucet(Auth::BasicAuth, "POL", 200, None)?; let mock_chain = builder.build()?; - let sender_account_interface = AccountInterface::from(&sender_basic_fungible_faucet_account); + let sender_account_interface = + AccountInterface::from_account(&sender_basic_fungible_faucet_account); - let tag = NoteTag::from_account_id(sender_basic_fungible_faucet_account.id()); - let metadata = NoteMetadata::new( - sender_basic_fungible_faucet_account.id(), - NoteType::Public, - tag, - NoteExecutionHint::always(), - Default::default(), - )?; + let tag = NoteTag::with_account_target(sender_basic_fungible_faucet_account.id()); + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::new(100), Word::empty()); + let metadata = + NoteMetadata::new(sender_basic_fungible_faucet_account.id(), NoteType::Public, tag) + .with_attachment(attachment); let assets = NoteAssets::new(vec![Asset::Fungible( FungibleAsset::new(sender_basic_fungible_faucet_account.id(), 10).unwrap(), )])?; - let note_script = ScriptBuilder::default().compile_note_script("begin nop end").unwrap(); + let note_script = CodeBuilder::default().compile_note_script("begin nop end").unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); let recipient = NoteRecipient::new(serial_num, note_script, NoteInputs::default()); @@ -116,19 +113,19 @@ async fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { let partial_note: PartialNote = note.clone().into(); let expiration_delta = 10u16; - let send_note_transaction_script = sender_account_interface.build_send_notes_script( - slice::from_ref(&partial_note), - Some(expiration_delta), - false, - )?; + let send_note_transaction_script = sender_account_interface + .build_send_notes_script(slice::from_ref(&partial_note), Some(expiration_delta))?; - let _executed_transaction = mock_chain + let executed_transaction = mock_chain .build_tx_context(sender_basic_fungible_faucet_account.id(), &[], &[]) .expect("failed to build tx context") .tx_script(send_note_transaction_script) - .extend_expected_output_notes(vec![OutputNote::Full(note)]) + .extend_expected_output_notes(vec![OutputNote::Full(note.clone())]) .build()? .execute() .await?; + + assert_eq!(executed_transaction.output_notes().get_note(0), &OutputNote::Full(note)); + Ok(()) } diff --git a/crates/miden-testing/tests/scripts/swap.rs b/crates/miden-testing/tests/scripts/swap.rs index f5aa7ebe25..748b9110c2 100644 --- a/crates/miden-testing/tests/scripts/swap.rs +++ b/crates/miden-testing/tests/scripts/swap.rs @@ -1,24 +1,17 @@ use anyhow::Context; -use miden_lib::note::utils; -use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{Account, AccountId, AccountStorageMode, AccountType}; -use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset}; -use miden_objects::note::{ - Note, - NoteAssets, - NoteDetails, - NoteExecutionHint, - NoteMetadata, - NoteTag, - NoteType, -}; -use miden_objects::testing::account_id::{ +use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountType}; +use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{Note, NoteAssets, NoteDetails, NoteMetadata, NoteTag, NoteType}; +use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, AccountIdBuilder, }; -use miden_objects::transaction::OutputNote; -use miden_objects::{Felt, NoteError, Word}; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, Word}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::note::utils; use miden_testing::{Auth, MockChain}; use crate::prove_and_verify_transaction; @@ -40,17 +33,15 @@ pub async fn prove_send_swap_note() -> anyhow::Result<()> { let tx_script_src = &format!( " - use.miden::output_note + use miden::protocol::output_note begin push.{recipient} - push.{note_execution_hint} push.{note_type} - push.0 # aux push.{tag} - call.output_note::create + exec.output_note::create push.{asset} - call.::miden::contracts::wallets::basic::move_asset_to_note + call.::miden::standards::wallets::basic::move_asset_to_note dropw dropw dropw dropw end ", @@ -58,10 +49,9 @@ pub async fn prove_send_swap_note() -> anyhow::Result<()> { note_type = NoteType::Public as u8, tag = Felt::from(swap_note.metadata().tag()), asset = Word::from(offered_asset), - note_execution_hint = Felt::from(swap_note.metadata().execution_hint()) ); - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; + let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?; let create_swap_note_tx = mock_chain .build_tx_context(sender_account.id(), &[], &[]) @@ -138,7 +128,7 @@ async fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { let full_payback_note = Note::new( payback_note.assets().clone(), - *output_payback_note.metadata(), + output_payback_note.metadata().clone(), payback_note.recipient().clone(), ); @@ -190,7 +180,6 @@ async fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { sender_account.id(), vec![requested_asset], payback_note_type, - Felt::new(0), payback_note.serial_num(), ) .unwrap(); @@ -217,7 +206,7 @@ async fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { let full_payback_note = Note::new( payback_note.assets().clone(), - *output_payback_note.metadata(), + output_payback_note.metadata().clone(), payback_note.recipient().clone(), ); @@ -353,14 +342,13 @@ pub fn create_p2id_note_exact( target: AccountId, assets: Vec, note_type: NoteType, - aux: Felt, serial_num: Word, ) -> Result { let recipient = utils::build_p2id_recipient(target, serial_num)?; - let tag = NoteTag::from_account_id(target); + let tag = NoteTag::with_account_target(target); - let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag); let vault = NoteAssets::new(assets)?; Ok(Note::new(vault, metadata, recipient)) diff --git a/crates/miden-testing/tests/wallet/mod.rs b/crates/miden-testing/tests/wallet/mod.rs index 450611693b..12386a14e3 100644 --- a/crates/miden-testing/tests/wallet/mod.rs +++ b/crates/miden-testing/tests/wallet/mod.rs @@ -1,24 +1,24 @@ -use miden_lib::AuthScheme; -use miden_lib::account::wallets::create_basic_wallet; -use miden_objects::Word; -use miden_objects::account::auth::AuthSecretKey; +use miden_protocol::Word; +use miden_protocol::account::auth::AuthSecretKey; +use miden_standards::AuthScheme; +use miden_standards::account::wallets::create_basic_wallet; use rand_chacha::ChaCha20Rng; use rand_chacha::rand_core::SeedableRng; #[cfg(not(target_arch = "wasm32"))] #[test] fn wallet_creation() { - use miden_lib::account::auth::AuthRpoFalcon512; - use miden_lib::account::wallets::BasicWallet; - use miden_objects::account::{AccountCode, AccountStorageMode, AccountType}; + use miden_protocol::account::{AccountCode, AccountStorageMode, AccountType}; + use miden_standards::account::auth::AuthFalcon512Rpo; + use miden_standards::account::wallets::BasicWallet; // we need a Falcon Public Key to create the wallet account let seed = [0_u8; 32]; let mut rng = ChaCha20Rng::from_seed(seed); - let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let sec_key = AuthSecretKey::new_falcon512_rpo_with_rng(&mut rng); let pub_key = sec_key.public_key().to_commitment(); - let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key }; + let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key }; // we need to use an initial seed to create the wallet account let init_seed: [u8; 32] = [ @@ -32,7 +32,7 @@ fn wallet_creation() { let wallet = create_basic_wallet(init_seed, auth_scheme, account_type, storage_mode).unwrap(); let expected_code = AccountCode::from_components( - &[AuthRpoFalcon512::new(pub_key).into(), BasicWallet.into()], + &[AuthFalcon512Rpo::new(pub_key).into(), BasicWallet.into()], AccountType::RegularAccountUpdatableCode, ) .unwrap(); @@ -40,15 +40,18 @@ fn wallet_creation() { assert!(wallet.is_regular_account()); assert_eq!(wallet.code().commitment(), expected_code_commitment); - assert_eq!(wallet.storage().get_item(0).unwrap(), Word::from(pub_key)); + assert_eq!( + wallet.storage().get_item(AuthFalcon512Rpo::public_key_slot()).unwrap(), + Word::from(pub_key) + ); } #[cfg(not(target_arch = "wasm32"))] #[test] fn wallet_creation_2() { - use miden_lib::account::auth::AuthEcdsaK256Keccak; - use miden_lib::account::wallets::BasicWallet; - use miden_objects::account::{AccountCode, AccountStorageMode, AccountType}; + use miden_protocol::account::{AccountCode, AccountStorageMode, AccountType}; + use miden_standards::account::auth::AuthEcdsaK256Keccak; + use miden_standards::account::wallets::BasicWallet; // we need a ECDSA Public Key to create the wallet account let seed = [0_u8; 32]; @@ -77,5 +80,8 @@ fn wallet_creation_2() { assert!(wallet.is_regular_account()); assert_eq!(wallet.code().commitment(), expected_code_commitment); - assert_eq!(wallet.storage().get_item(0).unwrap(), Word::from(pub_key)); + assert_eq!( + wallet.storage().get_item(AuthEcdsaK256Keccak::public_key_slot()).unwrap(), + Word::from(pub_key) + ); } diff --git a/crates/miden-tx-batch-prover/Cargo.toml b/crates/miden-tx-batch-prover/Cargo.toml index 6b214a120b..d664da1d37 100644 --- a/crates/miden-tx-batch-prover/Cargo.toml +++ b/crates/miden-tx-batch-prover/Cargo.toml @@ -17,9 +17,9 @@ bench = false [features] default = ["std"] -std = ["miden-objects/std", "miden-tx/std"] +std = ["miden-protocol/std", "miden-tx/std"] testing = [] [dependencies] -miden-objects = { workspace = true } -miden-tx = { workspace = true } +miden-protocol = { workspace = true } +miden-tx = { workspace = true } 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 3636f61ac5..4e7ccfffc7 100644 --- a/crates/miden-tx-batch-prover/src/local_batch_prover.rs +++ b/crates/miden-tx-batch-prover/src/local_batch_prover.rs @@ -1,7 +1,7 @@ use alloc::boxed::Box; -use miden_objects::ProvenBatchError; -use miden_objects::batch::{ProposedBatch, ProvenBatch}; +use miden_protocol::batch::{ProposedBatch, ProvenBatch}; +use miden_protocol::errors::ProvenBatchError; use miden_tx::TransactionVerifier; // LOCAL BATCH PROVER diff --git a/crates/miden-tx/Cargo.toml b/crates/miden-tx/Cargo.toml index 86c3c5006b..e78138760f 100644 --- a/crates/miden-tx/Cargo.toml +++ b/crates/miden-tx/Cargo.toml @@ -15,13 +15,13 @@ version.workspace = true [features] concurrent = ["miden-prover/concurrent", "std"] default = ["std"] -std = ["miden-lib/std", "miden-objects/std", "miden-processor/std", "miden-prover/std", "miden-verifier/std"] -testing = ["miden-lib/testing", "miden-objects/testing", "miden-processor/testing"] +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"] [dependencies] # Workspace dependencies -miden-lib = { workspace = true } -miden-objects = { workspace = true } +miden-protocol = { workspace = true } +miden-standards = { workspace = true } # Miden dependencies miden-processor = { workspace = true } @@ -29,9 +29,7 @@ miden-prover = { workspace = true } miden-verifier = { workspace = true } # External dependencies -rand = { workspace = true } thiserror = { workspace = true } -tokio = { features = ["rt"], workspace = true } [dev-dependencies] anyhow = { features = ["backtrace", "std"], workspace = true } diff --git a/crates/miden-tx/src/auth/tx_authenticator.rs b/crates/miden-tx/src/auth/tx_authenticator.rs index 848bcf2b21..4845421445 100644 --- a/crates/miden-tx/src/auth/tx_authenticator.rs +++ b/crates/miden-tx/src/auth/tx_authenticator.rs @@ -3,11 +3,11 @@ use alloc::collections::BTreeMap; use alloc::string::ToString; use alloc::vec::Vec; -use miden_objects::account::auth::{AuthSecretKey, PublicKeyCommitment, Signature}; -use miden_objects::crypto::SequentialCommit; -use miden_objects::transaction::TransactionSummary; -use miden_objects::{Felt, Hasher, Word}; use miden_processor::FutureMaybeSend; +use miden_protocol::account::auth::{AuthSecretKey, PublicKey, PublicKeyCommitment, Signature}; +use miden_protocol::crypto::SequentialCommit; +use miden_protocol::transaction::TransactionSummary; +use miden_protocol::{Felt, Hasher, Word}; use crate::errors::AuthenticationError; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; @@ -121,24 +121,31 @@ impl Deserializable for SigningInputs { /// a key managed by the authenticator. That is, the authenticator maintains a set of public- /// private key pairs, and can be requested to generate signatures against any of the managed keys. /// -/// The public keys are defined by [Word]'s which are the hashes of the actual public keys. +/// The public keys are defined by [PublicKeyCommitment]'s which are the hashes of the actual +/// public keys. pub trait TransactionAuthenticator { /// Retrieves a signature for a specific message as a list of [Felt]. /// /// The request is initiated by the VM as a consequence of the SigToStack advice /// injector. /// - /// - `pub_key_hash`: The hash of the public key used for signature generation. - /// - `message`: The message to sign, usually a commitment to the transaction data. - /// - `account_delta`: An informational parameter describing the changes made to the account up - /// to the point of calling `get_signature()`. This allows the authenticator to review any - /// alterations to the account prior to signing. It should not be directly used in the - /// signature computation. + /// - `pub_key_commitment`: the hash of the public key used for signature generation. + /// - `signing_inputs`: description of the message to be singed. The inputs could contain + /// arbitrary data or a [TransactionSummary] which would describe the changes made to the + /// account up to the point of calling `get_signature()`. This allows the authenticator to + /// review any alterations to the account prior to signing. It should not be directly used in + /// the signature computation. fn get_signature( &self, pub_key_commitment: PublicKeyCommitment, signing_inputs: &SigningInputs, ) -> impl FutureMaybeSend>; + + /// Retrieves a public key for a specific public key commitment. + fn get_public_key( + &self, + pub_key_commitment: PublicKeyCommitment, + ) -> impl FutureMaybeSend>; } /// A placeholder type for the generic trait bound of `TransactionAuthenticator<'_,'_,_,T>` @@ -160,6 +167,13 @@ impl TransactionAuthenticator for UnreachableAuth { ) -> impl FutureMaybeSend> { async { unreachable!("Type `UnreachableAuth` must not be instantiated") } } + + fn get_public_key( + &self, + _pub_key_commitment: PublicKeyCommitment, + ) -> impl FutureMaybeSend> { + async { unreachable!("Type `UnreachableAuth` must not be instantiated") } + } } // BASIC AUTHENTICATOR @@ -168,16 +182,25 @@ impl TransactionAuthenticator for UnreachableAuth { /// Represents a signer for [AuthSecretKey] keys. #[derive(Clone, Debug)] pub struct BasicAuthenticator { - /// pub_key |-> secret_key mapping - keys: BTreeMap, + /// pub_key |-> (secret_key, public_key) mapping + keys: BTreeMap, } impl BasicAuthenticator { pub fn new(keys: &[AuthSecretKey]) -> Self { let mut key_map = BTreeMap::new(); for secret_key in keys { - let pub_key = secret_key.public_key().to_commitment(); - key_map.insert(pub_key, secret_key.clone()); + let pub_key = secret_key.public_key(); + key_map.insert(pub_key.to_commitment(), (secret_key.clone(), pub_key)); + } + + BasicAuthenticator { keys: key_map } + } + + pub fn from_key_pairs(keys: &[(AuthSecretKey, PublicKey)]) -> Self { + let mut key_map = BTreeMap::new(); + for (secret_key, public_key) in keys { + key_map.insert(public_key.to_commitment(), (secret_key.clone(), public_key.clone())); } BasicAuthenticator { keys: key_map } @@ -185,9 +208,9 @@ impl BasicAuthenticator { /// Returns a reference to the keys map. /// - /// Map keys represent the public key commitments, and values represent the secret keys that - /// the authenticator would use to sign messages. - pub fn keys(&self) -> &BTreeMap { + /// Map keys represent the public key commitments, and values represent the (secret_key, + /// public_key) pair that the authenticator would use to sign messages. + pub fn keys(&self) -> &BTreeMap { &self.keys } } @@ -197,10 +220,6 @@ impl TransactionAuthenticator for BasicAuthenticator { /// /// The key should be included in the `keys` map and should be a variant of [AuthSecretKey]. /// - /// Supported signature schemes: - /// - RpoFalcon512 - /// - EcdsaK256Keccak - /// /// # Errors /// If the public key is not contained in the `keys` map, /// [`AuthenticationError::UnknownPublicKey`] is returned. @@ -213,11 +232,21 @@ impl TransactionAuthenticator for BasicAuthenticator { async move { match self.keys.get(&pub_key_commitment) { - Some(key) => Ok(key.sign(message)), + Some((auth_key, _)) => Ok(auth_key.sign(message)), None => Err(AuthenticationError::UnknownPublicKey(pub_key_commitment)), } } } + + /// Returns the public key associated with the given public key commitment. + /// + /// If the public key commitment is not contained in the `keys` map, `None` is returned. + fn get_public_key( + &self, + pub_key_commitment: PublicKeyCommitment, + ) -> impl FutureMaybeSend> { + async move { self.keys.get(&pub_key_commitment).map(|(_, pub_key)| pub_key) } + } } // HELPER FUNCTIONS @@ -236,19 +265,26 @@ impl TransactionAuthenticator for () { )) } } + + fn get_public_key( + &self, + _pub_key_commitment: PublicKeyCommitment, + ) -> impl FutureMaybeSend> { + async { None } + } } #[cfg(test)] mod test { - use miden_lib::utils::{Deserializable, Serializable}; - use miden_objects::account::auth::AuthSecretKey; - use miden_objects::{Felt, Word}; + use miden_protocol::account::auth::AuthSecretKey; + use miden_protocol::utils::{Deserializable, Serializable}; + use miden_protocol::{Felt, Word}; use super::SigningInputs; #[test] fn serialize_auth_key() { - let auth_key = AuthSecretKey::new_rpo_falcon512(); + let auth_key = AuthSecretKey::new_falcon512_rpo(); let serialized = auth_key.to_bytes(); let deserialized = AuthSecretKey::read_from_bytes(&serialized).unwrap(); diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index 7bb66698bd..0e0cc1dda2 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -3,27 +3,25 @@ use alloc::string::String; use alloc::vec::Vec; use core::error::Error; -use miden_lib::transaction::TransactionAdviceMapMismatch; -use miden_objects::account::AccountId; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::assembly::diagnostics::reporting::PrintDiagnostic; -use miden_objects::asset::AssetVaultKey; -use miden_objects::block::BlockNumber; -use miden_objects::crypto::merkle::SmtProofError; -use miden_objects::note::{NoteId, NoteMetadata}; -use miden_objects::transaction::TransactionSummary; -use miden_objects::{ +use miden_processor::{DeserializationError, ExecutionError}; +use miden_protocol::account::AccountId; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::assembly::diagnostics::reporting::PrintDiagnostic; +use miden_protocol::asset::AssetVaultKey; +use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::merkle::smt::SmtProofError; +use miden_protocol::errors::{ AccountDeltaError, AccountError, AssetError, - Felt, NoteError, ProvenTransactionError, TransactionInputError, TransactionOutputError, - Word, }; -use miden_processor::{DeserializationError, ExecutionError}; +use miden_protocol::note::{NoteId, NoteMetadata}; +use miden_protocol::transaction::TransactionSummary; +use miden_protocol::{Felt, Word}; use miden_verifier::VerificationError; use thiserror::Error; @@ -74,10 +72,12 @@ impl From for TransactionExecutorError { #[derive(Debug, Error)] pub enum TransactionExecutorError { - #[error("the advice map contains conflicting map entries")] - ConflictingAdviceMapEntry(#[source] TransactionAdviceMapMismatch), #[error("failed to fetch transaction inputs from the data store")] FetchTransactionInputsFailed(#[source] DataStoreError), + #[error("failed to fetch asset witnesses from the data store")] + FetchAssetWitnessFailed(#[source] DataStoreError), + #[error("fee asset must be fungible but was non-fungible")] + FeeAssetMustBeFungible, #[error("foreign account inputs for ID {0} are not anchored on reference block")] ForeignAccountNotAnchoredInReference(AccountId), #[error( @@ -118,8 +118,6 @@ pub enum TransactionExecutorError { "input note {0} was created in a block past the transaction reference block number ({1})" )] NoteBlockPastReferenceBlock(NoteId, BlockNumber), - #[error("failed to create transaction host")] - TransactionHostCreationFailed(#[source] TransactionHostError), #[error("failed to construct transaction outputs")] TransactionOutputConstructionFailed(#[source] TransactionOutputError), // Print the diagnostic directly instead of returning the source error. In the source error @@ -149,16 +147,10 @@ pub enum TransactionProverError { TransactionOutputConstructionFailed(#[source] TransactionOutputError), #[error("failed to build proven transaction")] ProvenTransactionBuildFailed(#[source] ProvenTransactionError), - #[error("the advice map contains conflicting map entries")] - ConflictingAdviceMapEntry(#[source] TransactionAdviceMapMismatch), // Print the diagnostic directly instead of returning the source error. In the source error // case, the diagnostic is lost if the execution error is not explicitly unwrapped. #[error("failed to execute transaction kernel program:\n{}", PrintDiagnostic::new(.0))] TransactionProgramExecutionFailed(ExecutionError), - #[error("failed to create account procedure index map")] - CreateAccountProcedureIndexMap(#[source] TransactionHostError), - #[error("failed to create transaction host")] - TransactionHostCreationFailed(#[source] TransactionHostError), /// Custom error variant for errors not covered by the other variants. #[error("{error_msg}")] Other { @@ -201,17 +193,6 @@ pub enum TransactionVerifierError { InsufficientProofSecurityLevel { actual: u32, expected_minimum: u32 }, } -// TRANSACTION HOST ERROR -// ================================================================================================ - -#[derive(Debug, Error)] -pub enum TransactionHostError { - #[error("{0}")] - AccountProcedureIndexMapError(String), - #[error("failed to create account procedure info")] - AccountProcedureInfoCreationFailed(#[source] AccountError), -} - // TRANSACTION KERNEL ERROR // ================================================================================================ @@ -225,10 +206,6 @@ pub enum TransactionKernelError { FailedToAddAssetToNote(#[source] NoteError), #[error("note input data has hash {actual} but expected hash {expected}")] InvalidNoteInputs { expected: Word, actual: Word }, - #[error( - "storage slot index {actual} is invalid, must be smaller than the number of account storage slots {max}" - )] - InvalidStorageSlotIndex { max: u64, actual: u64 }, #[error( "failed to respond to signature requested since no authenticator is assigned to the host" )] @@ -248,8 +225,6 @@ pub enum TransactionKernelError { "note inputs data extracted from the advice map by the event handler is not well formed" )] MalformedNoteInputs(#[source] NoteError), - #[error("note metadata created by the event handler is not well formed")] - MalformedNoteMetadata(#[source] NoteError), #[error( "note script data `{data:?}` extracted from the advice map by the event handler is not well formed" )] @@ -260,11 +235,17 @@ pub enum TransactionKernelError { #[error("recipient data `{0:?}` in the advice provider is not well formed")] MalformedRecipientData(Vec), #[error("cannot add asset to note with index {0}, note does not exist in the advice provider")] - MissingNote(u64), + MissingNote(usize), #[error( "public note with metadata {0:?} and recipient digest {1} is missing details in the advice provider" )] PublicNoteMissingDetails(NoteMetadata, Word), + #[error("attachment provided to set_attachment must be empty when attachment kind is None")] + NoteAttachmentNoneIsNotEmpty, + #[error( + "commitment of note attachment {actual} does not match attachment {provided} provided to set_attachment" + )] + NoteAttachmentArrayMismatch { actual: Word, provided: Word }, #[error( "note input data in advice provider contains fewer elements ({actual}) than specified ({specified}) by its inputs length" )] @@ -354,8 +335,6 @@ pub enum DataStoreError { AccountNotFound(AccountId), #[error("block with number {0} not found in data store")] BlockNotFound(BlockNumber), - #[error("note script with root {0} not found in data store")] - NoteScriptNotFound(Word), /// Custom error variant for implementors of the [`DataStore`](crate::executor::DataStore) /// trait. #[error("{error_msg}")] diff --git a/crates/miden-tx/src/executor/data_store.rs b/crates/miden-tx/src/executor/data_store.rs index c9b18f08f5..050aeb636d 100644 --- a/crates/miden-tx/src/executor/data_store.rs +++ b/crates/miden-tx/src/executor/data_store.rs @@ -1,11 +1,12 @@ use alloc::collections::BTreeSet; +use alloc::vec::Vec; -use miden_objects::account::{AccountId, PartialAccount, StorageMapWitness}; -use miden_objects::asset::{AssetVaultKey, AssetWitness}; -use miden_objects::block::{BlockHeader, BlockNumber}; -use miden_objects::note::NoteScript; -use miden_objects::transaction::{AccountInputs, PartialBlockchain}; use miden_processor::{FutureMaybeSend, MastForestStore, Word}; +use miden_protocol::account::{AccountId, PartialAccount, StorageMapWitness}; +use miden_protocol::asset::{AssetVaultKey, AssetWitness}; +use miden_protocol::block::{BlockHeader, BlockNumber}; +use miden_protocol::note::NoteScript; +use miden_protocol::transaction::{AccountInputs, PartialBlockchain}; use crate::DataStoreError; @@ -42,17 +43,17 @@ pub trait DataStore: MastForestStore { ref_block: BlockNumber, ) -> impl FutureMaybeSend>; - /// Returns a witness for an asset in the requested account's vault with the requested vault - /// root. + /// Returns witnesses for the asset vault keys in the requested account's vault with the + /// requested vault root. /// - /// This is the witness that needs to be added to the advice provider's merkle store and advice - /// map to make access to the specified asset possible. - fn get_vault_asset_witness( + /// These are the witnesses that need to be added to the advice provider's merkle store and + /// advice map to make access to the corresponding assets possible. + fn get_vault_asset_witnesses( &self, account_id: AccountId, vault_root: Word, - vault_key: AssetVaultKey, - ) -> impl FutureMaybeSend>; + vault_keys: BTreeSet, + ) -> impl FutureMaybeSend, DataStoreError>>; /// Returns a witness for a storage map item identified by `map_key` in the requested account's /// storage with the requested storage `map_root`. @@ -69,17 +70,17 @@ pub trait DataStore: MastForestStore { map_key: Word, ) -> impl FutureMaybeSend>; - /// Returns a note script with the specified root. + /// Returns a note script with the specified root, or `None` if not found. /// - /// This method will try to find a note script with the specified root in the data store, - /// and if not found, return an error. + /// This method will try to find a note script with the specified root in the data store. + /// If the script is not found, it returns `Ok(None)` rather than an error, as "not found" + /// is a valid, expected outcome. /// /// # Errors - /// Returns an error if: - /// - The note script with the specified root could not be found in the data store. - /// - The data store encountered some internal error. + /// Returns an error if the data store encountered an internal error while attempting to + /// retrieve the script. fn get_note_script( &self, script_root: Word, - ) -> impl FutureMaybeSend>; + ) -> impl FutureMaybeSend, DataStoreError>>; } diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index db8de203e0..1679190c92 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -1,19 +1,8 @@ -use alloc::collections::BTreeMap; +use alloc::boxed::Box; +use alloc::collections::{BTreeMap, BTreeSet}; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::{EventId, TransactionAdviceInputs}; -use miden_objects::account::auth::PublicKeyCommitment; -use miden_objects::account::{AccountCode, AccountDelta, AccountId, PartialAccount}; -use miden_objects::assembly::debuginfo::Location; -use miden_objects::assembly::{SourceFile, SourceManagerSync, SourceSpan}; -use miden_objects::asset::{Asset, AssetVaultKey, AssetWitness, FungibleAsset}; -use miden_objects::block::BlockNumber; -use miden_objects::crypto::merkle::SmtProof; -use miden_objects::note::{NoteInputs, NoteMetadata, NoteRecipient}; -use miden_objects::transaction::{InputNote, InputNotes, OutputNote}; -use miden_objects::vm::AdviceMap; -use miden_objects::{Felt, Hasher, Word}; use miden_processor::{ AdviceMutation, AsyncHost, @@ -23,18 +12,42 @@ use miden_processor::{ MastForest, ProcessState, }; +use miden_protocol::account::auth::PublicKeyCommitment; +use miden_protocol::account::{ + AccountCode, + AccountDelta, + AccountId, + PartialAccount, + StorageSlotId, + StorageSlotName, +}; +use miden_protocol::assembly::debuginfo::Location; +use miden_protocol::assembly::{SourceFile, SourceManagerSync, SourceSpan}; +use miden_protocol::asset::{AssetVaultKey, AssetWitness, FungibleAsset}; +use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::merkle::smt::SmtProof; +use miden_protocol::note::{NoteInputs, NoteMetadata, NoteRecipient}; +use miden_protocol::transaction::{ + InputNote, + InputNotes, + OutputNote, + TransactionAdviceInputs, + TransactionSummary, +}; +use miden_protocol::vm::AdviceMap; +use miden_protocol::{Felt, Hasher, Word}; use crate::auth::{SigningInputs, TransactionAuthenticator}; use crate::errors::TransactionKernelError; -use crate::host::note_builder::OutputNoteBuilder; use crate::host::{ + RecipientData, ScriptMastForestStore, TransactionBaseHost, - TransactionEventData, - TransactionEventHandling, + TransactionEvent, TransactionProgress, + TransactionProgressEvent, }; -use crate::{AccountProcedureIndexMap, DataStore, DataStoreError}; +use crate::{AccountProcedureIndexMap, DataStore}; // TRANSACTION EXECUTOR HOST // ================================================================================================ @@ -54,6 +67,11 @@ where /// The underlying base transaction host. base_host: TransactionBaseHost<'store, STORE>, + /// Tracks the number of cycles for each of the transaction execution stages. + /// + /// The progress is updated event handlers. + tx_progress: TransactionProgress, + /// Serves signature generation requests from the transaction runtime for signatures which are /// not present in the `generated_signatures` field. authenticator: Option<&'auth AUTH>, @@ -66,6 +84,9 @@ where /// This is required for re-executing the transaction, e.g. as part of transaction proving. accessed_foreign_account_code: Vec, + /// Storage slot names for foreign accounts accessed during transaction execution. + foreign_account_slot_names: BTreeMap, + /// Contains generated signatures (as a message |-> signature map) required for transaction /// execution. Once a signature was created for a given message, it is inserted into this map. /// After transaction execution, these can be inserted into the advice inputs to re-execute the @@ -73,6 +94,9 @@ where /// authenticator that produced it. generated_signatures: BTreeMap>, + /// The initial balance of the fee asset in the native account's vault. + initial_fee_asset_balance: u64, + /// The source manager to track source code file span information, improving any MASM related /// error messages. source_manager: Arc, @@ -87,6 +111,7 @@ where // -------------------------------------------------------------------------------------------- /// Creates a new [`TransactionExecutorHost`] instance from the provided inputs. + #[allow(clippy::too_many_arguments)] pub fn new( account: &PartialAccount, input_notes: InputNotes, @@ -95,6 +120,7 @@ where acct_procedure_index_map: AccountProcedureIndexMap, authenticator: Option<&'auth AUTH>, ref_block: BlockNumber, + initial_fee_asset_balance: u64, source_manager: Arc, ) -> Self { let base_host = TransactionBaseHost::new( @@ -107,10 +133,13 @@ where Self { base_host, + tx_progress: TransactionProgress::default(), authenticator, ref_block, accessed_foreign_account_code: Vec::new(), + foreign_account_slot_names: BTreeMap::new(), generated_signatures: BTreeMap::new(), + initial_fee_asset_balance, source_manager, } } @@ -120,7 +149,12 @@ where /// Returns a reference to the `tx_progress` field of this transaction host. pub fn tx_progress(&self) -> &TransactionProgress { - self.base_host.tx_progress() + &self.tx_progress + } + + /// Returns a reference to the foreign account slot names collected during execution. + pub fn foreign_account_slot_names(&self) -> &BTreeMap { + &self.foreign_account_slot_names } // EVENT HANDLERS @@ -143,29 +177,14 @@ where })?; let mut tx_advice_inputs = TransactionAdviceInputs::default(); - tx_advice_inputs - .add_foreign_accounts([&foreign_account_inputs]) - .map_err(|err| { - TransactionKernelError::other_with_source( - format!( - "failed to construct advice inputs for foreign account {}", - foreign_account_inputs.id() - ), - err, - ) - })?; + tx_advice_inputs.add_foreign_accounts([&foreign_account_inputs]); - self.base_host - .load_foreign_account_code(foreign_account_inputs.code()) - .map_err(|err| { - TransactionKernelError::other_with_source( - format!( - "failed to insert account procedures for foreign account {}", - foreign_account_inputs.id() - ), - err, - ) - })?; + // Extract and store slot names for this foreign account and store. + foreign_account_inputs.storage().header().slots().for_each(|slot| { + self.foreign_account_slot_names.insert(slot.id(), slot.name().clone()); + }); + + self.base_host.load_foreign_account_code(foreign_account_inputs.code()); // Add the foreign account's code to the list of accessed code. self.accessed_foreign_account_code.push(foreign_account_inputs.code().clone()); @@ -179,8 +198,10 @@ where pub async fn on_auth_requested( &mut self, pub_key_hash: Word, - signing_inputs: SigningInputs, + tx_summary: TransactionSummary, ) -> Result, TransactionKernelError> { + let signing_inputs = SigningInputs::TransactionSummary(Box::new(tx_summary)); + let authenticator = self.authenticator.ok_or(TransactionKernelError::MissingAuthenticator)?; @@ -205,32 +226,10 @@ where &self, fee_asset: FungibleAsset, ) -> Result, TransactionKernelError> { - let asset_witness = self - .base_host - .store() - .get_vault_asset_witness( - self.base_host.initial_account_header().id(), - self.base_host.initial_account_header().vault_root(), - fee_asset.vault_key(), - ) - .await - .map_err(|err| TransactionKernelError::GetVaultAssetWitness { - vault_root: self.base_host.initial_account_header().vault_root(), - asset_key: fee_asset.vault_key(), - source: err, - })?; - - // Find fee asset in the witness or default to 0 if it isn't present. - let initial_fee_asset = asset_witness - .find(fee_asset.vault_key()) - .and_then(|asset| match asset { - Asset::Fungible(fungible_asset) => Some(fungible_asset), - _ => None, - }) - .unwrap_or( - FungibleAsset::new(fee_asset.faucet_id(), 0) - .expect("fungible asset created from fee asset should be valid"), - ); + // Construct initial fee asset. + let initial_fee_asset = + FungibleAsset::new(fee_asset.faucet_id(), self.initial_fee_asset_balance) + .expect("fungible asset created from fee asset should be valid"); // Compute the current balance of the native asset in the account based on the initial value // and the delta. @@ -272,7 +271,7 @@ where }); } - Ok(asset_witness_to_advice_mutation(asset_witness)) + Ok(Vec::new()) } /// Handles a request for a storage map witness by querying the data store for a merkle path. @@ -281,14 +280,14 @@ where /// [`Self::on_account_vault_asset_witness_requested`] for more on this topic. async fn on_account_storage_map_witness_requested( &self, - current_account_id: AccountId, + active_account_id: AccountId, map_root: Word, map_key: Word, ) -> Result, TransactionKernelError> { let storage_map_witness = self .base_host .store() - .get_storage_map_witness(current_account_id, map_root, map_key) + .get_storage_map_witness(active_account_id, map_root, map_key) .await .map_err(|err| TransactionKernelError::GetStorageMapWitness { map_root, @@ -343,14 +342,18 @@ where /// witnesses for the initial vault root. async fn on_account_vault_asset_witness_requested( &self, - current_account_id: AccountId, + active_account_id: AccountId, vault_root: Word, asset_key: AssetVaultKey, ) -> Result, TransactionKernelError> { - let asset_witness = self + let asset_witnesses = self .base_host .store() - .get_vault_asset_witness(current_account_id, vault_root, asset_key) + .get_vault_asset_witnesses( + active_account_id, + vault_root, + BTreeSet::from_iter([asset_key]), + ) .await .map_err(|err| TransactionKernelError::GetVaultAssetWitness { vault_root, @@ -358,7 +361,7 @@ where source: err, })?; - Ok(asset_witness_to_advice_mutation(asset_witness)) + Ok(asset_witnesses.into_iter().flat_map(asset_witness_to_advice_mutation).collect()) } /// Handles a request for a [`NoteScript`] by querying the [`DataStore`]. @@ -368,46 +371,51 @@ where /// where the script is not already available in the advice provider. async fn on_note_script_requested( &mut self, + note_idx: usize, + recipient_digest: Word, script_root: Word, metadata: NoteMetadata, - recipient_digest: Word, - note_idx: usize, note_inputs: NoteInputs, serial_num: Word, ) -> Result, TransactionKernelError> { let note_script_result = self.base_host.store().get_note_script(script_root).await; - let (recipient, mutations) = match note_script_result { - Ok(note_script) => { + match note_script_result { + Ok(Some(note_script)) => { let script_felts: Vec = (¬e_script).into(); let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); - let mutations = vec![AdviceMutation::extend_map(AdviceMap::from_iter([( + + if recipient.digest() != recipient_digest { + return Err(TransactionKernelError::other(format!( + "recipient digest is {recipient_digest}, but recipient constructed from raw inputs has digest {}", + recipient.digest() + ))); + } + + self.base_host.output_note_from_recipient(note_idx, metadata, recipient)?; + + Ok(vec![AdviceMutation::extend_map(AdviceMap::from_iter([( script_root, script_felts, - )]))]; - - (Some(recipient), mutations) - }, - Err(DataStoreError::NoteScriptNotFound(_)) if metadata.is_private() => { - (None, Vec::new()) + )]))]) }, - Err(DataStoreError::NoteScriptNotFound(_)) => { - return Err(TransactionKernelError::other(format!( - "note script with root {script_root} not found in data store for public note" - ))); - }, - Err(err) => { - return Err(TransactionKernelError::other_with_source( - "failed to retrieve note script from data store", - err, - )); - }, - }; - - let note_builder = OutputNoteBuilder::new(metadata, recipient_digest, recipient)?; - self.base_host.insert_output_note_builder(note_idx, note_builder)?; + Ok(None) if metadata.is_private() => { + self.base_host.output_note_from_recipient_digest( + note_idx, + metadata, + recipient_digest, + )?; - Ok(mutations) + Ok(Vec::new()) + }, + Ok(None) => Err(TransactionKernelError::other(format!( + "note script with root {script_root} not found in data store for public note" + ))), + Err(err) => Err(TransactionKernelError::other_with_source( + "failed to retrieve note script from data store", + err, + )), + } } /// Consumes `self` and returns the account delta, output notes, generated signatures and @@ -422,8 +430,9 @@ where Vec, BTreeMap>, TransactionProgress, + BTreeMap, ) { - let (account_delta, input_notes, output_notes, tx_progress) = self.base_host.into_parts(); + let (account_delta, input_notes, output_notes) = self.base_host.into_parts(); ( account_delta, @@ -431,7 +440,8 @@ where output_notes, self.accessed_foreign_account_code, self.generated_signatures, - tx_progress, + self.tx_progress, + self.foreign_account_slot_names, ) } } @@ -469,72 +479,208 @@ where &mut self, process: &ProcessState, ) -> impl FutureMaybeSend, EventError>> { - let event_id = EventId::from_felt(process.get_stack_item(0)); - - // TODO: Eventually, refactor this to let TransactionEvent contain the data directly, which - // should be cleaner. - let event_handling_result = self.base_host.handle_event(process, event_id); + let core_lib_event_result = self.base_host.handle_core_lib_events(process); + + // If the event was handled by a core lib handler (Ok(Some)), we will return the result from + // within the async block below. So, we only need to extract th tx event if the event was + // not yet handled (Ok(None)). + let tx_event_result = match core_lib_event_result { + Ok(None) => Some(TransactionEvent::extract(&self.base_host, process)), + _ => None, + }; async move { - let event_handling = event_handling_result?; - let event_data = match event_handling { - TransactionEventHandling::Unhandled(event) => event, - TransactionEventHandling::Handled(mutations) => { - return Ok(mutations); - }, + if let Some(mutations) = core_lib_event_result? { + return Ok(mutations); + } + + // The outer None means the event was handled by core lib handlers. + let Some(tx_event_result) = tx_event_result else { + return Ok(Vec::new()); + }; + // The inner None means the transaction event ID does not need to be handled. + let Some(tx_event) = tx_event_result? else { + return Ok(Vec::new()); }; - match event_data { - TransactionEventData::AuthRequest { pub_key_hash, signing_inputs } => self - .on_auth_requested(pub_key_hash, signing_inputs) - .await - .map_err(EventError::from), - TransactionEventData::TransactionFeeComputed { fee_asset } => self - .on_before_tx_fee_removed_from_account(fee_asset) - .await - .map_err(EventError::from), - TransactionEventData::ForeignAccount { account_id } => { - self.on_foreign_account_requested(account_id).await.map_err(EventError::from) + let result = match tx_event { + TransactionEvent::AccountBeforeForeignLoad { foreign_account_id: account_id } => { + self.on_foreign_account_requested(account_id).await + }, + + TransactionEvent::AccountVaultAfterRemoveAsset { asset } => { + self.base_host.on_account_vault_after_remove_asset(asset) + }, + TransactionEvent::AccountVaultAfterAddAsset { asset } => { + self.base_host.on_account_vault_after_add_asset(asset) }, - TransactionEventData::AccountVaultAssetWitness { - current_account_id, + + TransactionEvent::AccountStorageAfterSetItem { slot_name, new_value } => { + self.base_host.on_account_storage_after_set_item(slot_name, new_value) + }, + + TransactionEvent::AccountStorageAfterSetMapItem { + slot_name, + key, + old_value: prev_map_value, + new_value, + } => self.base_host.on_account_storage_after_set_map_item( + slot_name, + key, + prev_map_value, + new_value, + ), + + TransactionEvent::AccountVaultBeforeAssetAccess { + active_account_id, vault_root, asset_key, - } => self - .on_account_vault_asset_witness_requested( - current_account_id, + } => { + self.on_account_vault_asset_witness_requested( + active_account_id, vault_root, asset_key, ) .await - .map_err(EventError::from), - TransactionEventData::AccountStorageMapWitness { - current_account_id, + }, + + TransactionEvent::AccountStorageBeforeMapItemAccess { + active_account_id, map_root, map_key, - } => self - .on_account_storage_map_witness_requested(current_account_id, map_root, map_key) - .await - .map_err(EventError::from), - TransactionEventData::NoteData { - note_idx, - metadata, - script_root, - recipient_digest, - note_inputs, - serial_num, - } => self - .on_note_script_requested( - script_root, - metadata, - recipient_digest, - note_idx, - note_inputs, - serial_num, + } => { + self.on_account_storage_map_witness_requested( + active_account_id, + map_root, + map_key, ) .await - .map_err(EventError::from), - } + }, + + TransactionEvent::AccountAfterIncrementNonce => { + self.base_host.on_account_after_increment_nonce() + }, + + TransactionEvent::AccountPushProcedureIndex { code_commitment, procedure_root } => { + self.base_host.on_account_push_procedure_index(code_commitment, procedure_root) + }, + + TransactionEvent::NoteBeforeCreated { note_idx, metadata, recipient_data } => { + match recipient_data { + RecipientData::Digest(recipient_digest) => { + self.base_host.output_note_from_recipient_digest( + note_idx, + metadata, + recipient_digest, + ) + }, + RecipientData::Recipient(note_recipient) => self + .base_host + .output_note_from_recipient(note_idx, metadata, note_recipient), + RecipientData::ScriptMissing { + recipient_digest, + serial_num, + script_root, + note_inputs, + } => { + self.on_note_script_requested( + note_idx, + recipient_digest, + script_root, + metadata, + note_inputs, + serial_num, + ) + .await + }, + } + }, + + TransactionEvent::NoteBeforeAddAsset { note_idx, asset } => { + self.base_host.on_note_before_add_asset(note_idx, asset) + }, + + TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment } => self + .base_host + .on_note_before_set_attachment(note_idx, attachment) + .map(|_| Vec::new()), + + TransactionEvent::AuthRequest { pub_key_hash, tx_summary, signature } => { + if let Some(signature) = signature { + Ok(self.base_host.on_auth_requested(signature)) + } else { + self.on_auth_requested(pub_key_hash, tx_summary).await + } + }, + + // This always returns an error to abort the transaction. + TransactionEvent::Unauthorized { tx_summary } => { + Err(TransactionKernelError::Unauthorized(Box::new(tx_summary))) + }, + + TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount { fee_asset } => { + self.on_before_tx_fee_removed_from_account(fee_asset).await + }, + + TransactionEvent::LinkMapSet { advice_mutation } => Ok(advice_mutation), + TransactionEvent::LinkMapGet { advice_mutation } => Ok(advice_mutation), + TransactionEvent::Progress(tx_progress) => match tx_progress { + TransactionProgressEvent::PrologueStart(clk) => { + self.tx_progress.start_prologue(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::PrologueEnd(clk) => { + self.tx_progress.end_prologue(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::NotesProcessingStart(clk) => { + self.tx_progress.start_notes_processing(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::NotesProcessingEnd(clk) => { + self.tx_progress.end_notes_processing(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::NoteExecutionStart { note_id, clk } => { + self.tx_progress.start_note_execution(clk, note_id); + Ok(Vec::new()) + }, + TransactionProgressEvent::NoteExecutionEnd(clk) => { + self.tx_progress.end_note_execution(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::TxScriptProcessingStart(clk) => { + self.tx_progress.start_tx_script_processing(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::TxScriptProcessingEnd(clk) => { + self.tx_progress.end_tx_script_processing(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::EpilogueStart(clk) => { + self.tx_progress.start_epilogue(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::EpilogueEnd(clk) => { + self.tx_progress.end_epilogue(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::EpilogueAuthProcStart(clk) => { + self.tx_progress.start_auth_procedure(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::EpilogueAuthProcEnd(clk) => { + self.tx_progress.end_auth_procedure(clk); + Ok(Vec::new()) + }, + TransactionProgressEvent::EpilogueAfterTxCyclesObtained(clk) => { + self.tx_progress.epilogue_after_tx_cycles_obtained(clk); + Ok(Vec::new()) + }, + }, + }; + + result.map_err(EventError::from) } } } @@ -544,7 +690,7 @@ where /// Converts an [`AssetWitness`] into the set of advice mutations that need to be inserted in order /// to access the asset. -fn asset_witness_to_advice_mutation(asset_witness: AssetWitness) -> Vec { +fn asset_witness_to_advice_mutation(asset_witness: AssetWitness) -> [AdviceMutation; 2] { // Get the nodes in the proof and insert them into the merkle store. let merkle_store_ext = AdviceMutation::extend_merkle_store(asset_witness.authenticated_nodes()); @@ -554,5 +700,5 @@ fn asset_witness_to_advice_mutation(asset_witness: AssetWitness) -> Vec, tx_args: TransactionArgs, ) -> Result { - let mut ref_blocks = validate_input_notes(&input_notes, block_ref)?; + let (mut asset_vault_keys, mut ref_blocks) = validate_input_notes(&input_notes, block_ref)?; ref_blocks.insert(block_ref); let (account, block_header, blockchain) = self @@ -263,9 +263,24 @@ where .await .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?; + // Add the vault key for the fee asset to the list of asset vault keys which will need to be + // accessed at the end of the transaction. + let fee_asset_vault_key = + AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id()) + .expect("fee asset should be a fungible asset"); + asset_vault_keys.insert(fee_asset_vault_key); + + // Fetch the witnesses for all asset vault keys. + let asset_witnesses = self + .data_store + .get_vault_asset_witnesses(account_id, account.vault().root(), asset_vault_keys) + .await + .map_err(TransactionExecutorError::FetchAssetWitnessFailed)?; + let tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes) .map_err(TransactionExecutorError::InvalidTransactionInputs)? - .with_tx_args(tx_args); + .with_tx_args(tx_args) + .with_asset_witnesses(asset_witnesses); Ok(tx_inputs) } @@ -281,8 +296,7 @@ where (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs), TransactionExecutorError, > { - let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs) - .map_err(TransactionExecutorError::ConflictingAdviceMapEntry)?; + let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs); // This reverses the stack inputs (even though it doesn't look like it does) because the // fast processor expects the reverse order. @@ -301,8 +315,27 @@ where // To start executing the transaction, the procedure index map only needs to contain the // native account's procedures. Foreign accounts are inserted into the map on first access. let account_procedure_index_map = - AccountProcedureIndexMap::new([tx_inputs.account().code()]) - .map_err(TransactionExecutorError::TransactionHostCreationFailed)?; + AccountProcedureIndexMap::new([tx_inputs.account().code()]); + + let initial_fee_asset_balance = { + let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id(); + let fee_asset_vault_key = AssetVaultKey::from_account_id(native_asset_id) + .expect("fee asset should be a fungible asset"); + + let fee_asset_witness = tx_inputs + .asset_witnesses() + .iter() + .find_map(|witness| witness.find(fee_asset_vault_key)); + + match fee_asset_witness { + Some(Asset::Fungible(fee_asset)) => fee_asset.amount(), + Some(Asset::NonFungible(_)) => { + return Err(TransactionExecutorError::FeeAssetMustBeFungible); + }, + // If the witness does not contain the asset, its balance is zero. + None => 0, + } + }; let host = TransactionExecutorHost::new( tx_inputs.account(), @@ -312,6 +345,7 @@ where account_procedure_index_map, self.authenticator, tx_inputs.block_header().block_num(), + initial_fee_asset_balance, self.source_manager.clone(), ); @@ -333,6 +367,7 @@ fn build_executed_transaction Result { // Note that the account delta does not contain the removed transaction fee, so it is the // "pre-fee" delta of the transaction. + let ( pre_fee_account_delta, _input_notes, @@ -340,6 +375,7 @@ fn build_executed_transaction, block_ref: BlockNumber, -) -> Result, TransactionExecutorError> { - // Validate that notes were not created after the reference, and build the set of required - // block numbers +) -> Result<(BTreeSet, BTreeSet), TransactionExecutorError> { let mut ref_blocks: BTreeSet = BTreeSet::new(); - for note in notes.iter() { - if let Some(location) = note.location() { + let mut asset_vault_keys: BTreeSet = BTreeSet::new(); + + for input_note in notes.iter() { + // Validate that notes were not created after the reference, and build the set of required + // block numbers + if let Some(location) = input_note.location() { if location.block_num() > block_ref { return Err(TransactionExecutorError::NoteBlockPastReferenceBlock( - note.id(), + input_note.id(), block_ref, )); } ref_blocks.insert(location.block_num()); } + + asset_vault_keys.extend(input_note.note().assets().iter().map(Asset::vault_key)); } - Ok(ref_blocks) + Ok((asset_vault_keys, ref_blocks)) } /// Validates that the number of cycles specified is within the allowed range. diff --git a/crates/miden-tx/src/executor/notes_checker.rs b/crates/miden-tx/src/executor/notes_checker.rs index e6a033cb60..9741e79824 100644 --- a/crates/miden-tx/src/executor/notes_checker.rs +++ b/crates/miden-tx/src/executor/notes_checker.rs @@ -1,14 +1,19 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; -use miden_lib::note::{NoteConsumptionStatus, WellKnownNote}; -use miden_lib::transaction::TransactionKernel; -use miden_objects::account::AccountId; -use miden_objects::block::BlockNumber; -use miden_objects::note::Note; -use miden_objects::transaction::{InputNote, InputNotes, TransactionArgs, TransactionInputs}; use miden_processor::fast::FastProcessor; +use miden_protocol::account::AccountId; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::Note; +use miden_protocol::transaction::{ + InputNote, + InputNotes, + TransactionArgs, + TransactionInputs, + TransactionKernel, +}; use miden_prover::AdviceInputs; +use miden_standards::note::{NoteConsumptionStatus, WellKnownNote}; use super::TransactionExecutor; use crate::auth::TransactionAuthenticator; diff --git a/crates/miden-tx/src/host/account_delta_tracker.rs b/crates/miden-tx/src/host/account_delta_tracker.rs index 279d806f05..f62e7996e8 100644 --- a/crates/miden-tx/src/host/account_delta_tracker.rs +++ b/crates/miden-tx/src/host/account_delta_tracker.rs @@ -1,11 +1,11 @@ -use miden_objects::account::{ +use miden_protocol::account::{ AccountCode, AccountDelta, AccountId, AccountVaultDelta, PartialAccount, }; -use miden_objects::{Felt, FieldElement, ZERO}; +use miden_protocol::{Felt, FieldElement, ZERO}; use crate::host::storage_delta_tracker::StorageDeltaTracker; diff --git a/crates/miden-tx/src/host/account_procedures.rs b/crates/miden-tx/src/host/account_procedures.rs index c4c664977f..0a74d232e6 100644 --- a/crates/miden-tx/src/host/account_procedures.rs +++ b/crates/miden-tx/src/host/account_procedures.rs @@ -1,8 +1,7 @@ -use miden_lib::transaction::memory::{ACCOUNT_STACK_TOP_PTR, ACCT_CODE_COMMITMENT_OFFSET}; -use miden_objects::account::AccountCode; +use miden_protocol::account::AccountCode; -use super::{BTreeMap, ProcessState, Word}; -use crate::errors::{TransactionHostError, TransactionKernelError}; +use super::{BTreeMap, Word}; +use crate::errors::TransactionKernelError; // ACCOUNT PROCEDURE INDEX MAP // ================================================================================================ @@ -16,19 +15,17 @@ pub struct AccountProcedureIndexMap(BTreeMap>); impl AccountProcedureIndexMap { /// Returns a new [`AccountProcedureIndexMap`] instantiated with account procedures from the /// provided iterator of [`AccountCode`]. - pub fn new<'code>( - account_codes: impl IntoIterator, - ) -> Result { + pub fn new<'code>(account_codes: impl IntoIterator) -> Self { let mut index_map = Self::default(); for account_code in account_codes { // Insert each account procedures only once. if !index_map.0.contains_key(&account_code.commitment()) { - index_map.insert_code(account_code)?; + index_map.insert_code(account_code); } } - Ok(index_map) + index_map } /// Inserts the procedures from the provided [`AccountCode`] into the advice inputs, using @@ -37,58 +34,36 @@ impl AccountProcedureIndexMap { /// The resulting instance will map the account code commitment to a mapping of /// `proc_root |-> proc_index` for any account that is expected to be involved in the /// transaction, enabling fast procedure index lookups at runtime. - pub fn insert_code(&mut self, code: &AccountCode) -> Result<(), TransactionHostError> { + pub fn insert_code(&mut self, code: &AccountCode) { let mut procedure_map = BTreeMap::new(); - for (proc_idx, proc_info) in code.procedures().iter().enumerate() { - let proc_idx = u8::try_from(proc_idx).map_err(|_| { - TransactionHostError::AccountProcedureIndexMapError( - "procedure index out of bounds".into(), - ) - })?; - - procedure_map.insert(*proc_info.mast_root(), proc_idx); + for (proc_idx, proc_root) in code.procedures().iter().enumerate() { + // SAFETY: AccountCode::MAX_NUM_PROCEDURES is 256 and so the highest possible index is + // 255. + let proc_idx = + u8::try_from(proc_idx).expect("account code should contain at most 256 procedures"); + procedure_map.insert(*proc_root.mast_root(), proc_idx); } self.0.insert(code.commitment(), procedure_map); - - Ok(()) } - /// Returns index of the procedure whose root is currently at the top of the operand stack in - /// the provided process. + /// Returns the index of the requested procedure root in the account code identified by the + /// provided commitment. /// /// # Errors - /// Returns an error if the procedure at the top of the operand stack is not present in this - /// map. - pub fn get_proc_index(&self, process: &ProcessState) -> Result { - // get active account code commitment - let code_commitment = { - let account_stack_top_ptr = process - .get_mem_value(process.ctx(), ACCOUNT_STACK_TOP_PTR) - .expect("Account stack top pointer was not initialized") - .as_int(); - let curr_data_ptr = process - .get_mem_value( - process.ctx(), - account_stack_top_ptr - .try_into() - .expect("account stack top pointer should be less than u32::MAX"), - ) - .expect("active account pointer was not initialized") - .as_int(); - process - .get_mem_word(process.ctx(), curr_data_ptr as u32 + ACCT_CODE_COMMITMENT_OFFSET) - .expect("failed to read a word from memory") - .expect("active account code commitment was not initialized") - }; - - let proc_root = process.get_stack_word_be(1); - + /// + /// Returns an error if: + /// - the requested procedure is not present in this map. + pub fn get_proc_index( + &self, + code_commitment: Word, + procedure_root: Word, + ) -> Result { self.0 .get(&code_commitment) .ok_or(TransactionKernelError::UnknownCodeCommitment(code_commitment))? - .get(&proc_root) + .get(&procedure_root) .cloned() - .ok_or(TransactionKernelError::UnknownAccountProcedure(proc_root)) + .ok_or(TransactionKernelError::UnknownAccountProcedure(procedure_root)) } } diff --git a/crates/miden-tx/src/host/kernel_process.rs b/crates/miden-tx/src/host/kernel_process.rs index 5280c811d3..5d948de35a 100644 --- a/crates/miden-tx/src/host/kernel_process.rs +++ b/crates/miden-tx/src/host/kernel_process.rs @@ -1,12 +1,18 @@ -use miden_lib::transaction::memory::{ +use miden_processor::{ExecutionError, Felt, ProcessState}; +use miden_protocol::account::{AccountId, StorageSlotId, StorageSlotType}; +use miden_protocol::note::{NoteId, NoteInputs}; +use miden_protocol::transaction::memory::{ ACCOUNT_STACK_TOP_PTR, + ACCT_CODE_COMMITMENT_OFFSET, + ACCT_STORAGE_SLOT_ID_PREFIX_OFFSET, + ACCT_STORAGE_SLOT_ID_SUFFIX_OFFSET, + ACCT_STORAGE_SLOT_TYPE_OFFSET, + ACCT_STORAGE_SLOT_VALUE_OFFSET, ACTIVE_INPUT_NOTE_PTR, NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR, + NUM_OUTPUT_NOTES_PTR, }; -use miden_objects::account::AccountId; -use miden_objects::note::{NoteId, NoteInputs}; -use miden_objects::{Hasher, Word}; -use miden_processor::{EventError, ExecutionError, Felt, ProcessState}; +use miden_protocol::{Hasher, Word}; use crate::errors::TransactionKernelError; @@ -14,13 +20,29 @@ use crate::errors::TransactionKernelError; // ================================================================================================ pub(super) trait TransactionKernelProcess { + /// Returns the pointer to the active account. + fn get_active_account_ptr(&self) -> Result; + + /// Returns the [`AccountId`] of the active account. fn get_active_account_id(&self) -> Result; + /// Returns the account code commitment of the active account. + fn get_active_account_code_commitment(&self) -> Result; + + #[allow(dead_code)] fn get_num_storage_slots(&self) -> Result; + /// Returns the current number of output notes. + fn get_num_output_notes(&self) -> u64; + fn get_vault_root(&self, vault_root_ptr: Felt) -> Result; - fn get_active_note_id(&self) -> Result, EventError>; + fn get_active_note_id(&self) -> Result, TransactionKernelError>; + + fn get_storage_slot( + &self, + slot_ptr: Felt, + ) -> Result<(StorageSlotId, StorageSlotType, Word), TransactionKernelError>; fn read_note_recipient_info_from_adv_map( &self, @@ -33,11 +55,18 @@ pub(super) trait TransactionKernelProcess { ) -> Result; fn has_advice_map_entry(&self, key: Word) -> bool; + + /// Returns `true` if the advice provider has a merkle path for the provided root and leaf + /// index, `false` otherwise. + fn has_merkle_path( + &self, + root: Word, + leaf_index: Felt, + ) -> Result; } impl<'a> TransactionKernelProcess for ProcessState<'a> { - /// Returns the ID of the currently active account. - fn get_active_account_id(&self) -> Result { + fn get_active_account_ptr(&self) -> Result { let account_stack_top_ptr = self.get_mem_value(self.ctx(), ACCOUNT_STACK_TOP_PTR).ok_or_else(|| { TransactionKernelError::other("account stack top ptr should be initialized") @@ -49,10 +78,12 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { let active_account_ptr = self .get_mem_value(self.ctx(), account_stack_top_ptr) .ok_or_else(|| TransactionKernelError::other("account id should be initialized"))?; - let active_account_ptr = u32::try_from(active_account_ptr).map_err(|_| { - TransactionKernelError::other("active account ptr should fit into a u32") - })?; + u32::try_from(active_account_ptr) + .map_err(|_| TransactionKernelError::other("active account ptr should fit into a u32")) + } + fn get_active_account_id(&self) -> Result { + let active_account_ptr = self.get_active_account_ptr()?; let active_account_id_and_nonce = self .get_mem_word(self.ctx(), active_account_ptr) .map_err(|_| { @@ -70,6 +101,23 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { }) } + fn get_active_account_code_commitment(&self) -> Result { + let active_account_ptr = self.get_active_account_ptr()?; + let code_commitment = self + .get_mem_word(self.ctx(), active_account_ptr + ACCT_CODE_COMMITMENT_OFFSET) + .map_err(|err| { + TransactionKernelError::other_with_source( + "failed to read code commitment from memory", + err, + ) + })? + .ok_or_else(|| { + TransactionKernelError::other("active account code commitment was not initialized") + })?; + + Ok(code_commitment) + } + /// Returns the number of storage slots initialized for the active account. /// /// # Errors @@ -85,13 +133,21 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { Ok(num_storage_slots_felt.as_int()) } + fn get_num_output_notes(&self) -> u64 { + // Read the number from memory or default to 0 if the location hasn't been accessed + // previously (e.g. when no notes have been created yet). + self.get_mem_value(self.ctx(), NUM_OUTPUT_NOTES_PTR) + .map(|num_output_notes| num_output_notes.as_int()) + .unwrap_or(0) + } + /// Returns the ID of the active note, or None if the note execution hasn't started yet or has /// already ended. /// /// # Errors /// Returns an error if the address of the active note is invalid (e.g., greater than /// `u32::MAX`). - fn get_active_note_id(&self) -> Result, EventError> { + fn get_active_note_id(&self) -> Result, TransactionKernelError> { // get the note address in `Felt` or return `None` if the address hasn't been accessed // previously. let note_address_felt = match self.get_mem_value(self.ctx(), ACTIVE_INPUT_NOTE_PTR) { @@ -100,7 +156,7 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { }; // convert note address into u32 let note_address = u32::try_from(note_address_felt).map_err(|_| { - EventError::from(format!( + TransactionKernelError::other(format!( "failed to convert {note_address_felt} into a memory address (u32)" )) })?; @@ -110,8 +166,13 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { } else { Ok(self .get_mem_word(self.ctx(), note_address) - .map_err(ExecutionError::MemoryError)? - .map(NoteId::from)) + .map_err(|err| { + TransactionKernelError::other_with_source( + "failed to read note address", + ExecutionError::MemoryError(err), + ) + })? + .map(NoteId::from_raw)) } } @@ -135,6 +196,61 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { }) } + fn get_storage_slot( + &self, + slot_ptr: Felt, + ) -> Result<(StorageSlotId, StorageSlotType, Word), TransactionKernelError> { + let slot_ptr = u32::try_from(slot_ptr).map_err(|_err| { + TransactionKernelError::other(format!( + "slot ptr should fit into a u32, but was {slot_ptr}" + )) + })?; + + let slot_metadata = self + .get_mem_word(self.ctx(), slot_ptr) + .map_err(|err| { + TransactionKernelError::other_with_source( + format!("misaligned slot ptr {slot_ptr}"), + err, + ) + })? + .ok_or_else(|| { + TransactionKernelError::other(format!("slot ptr {slot_ptr} is uninitialized")) + })?; + + let slot_value_ptr = slot_ptr + ACCT_STORAGE_SLOT_VALUE_OFFSET as u32; + let slot_value = self + .get_mem_word(self.ctx(), slot_value_ptr) + .map_err(|err| { + TransactionKernelError::other_with_source( + format!("misaligned slot value ptr {slot_value_ptr}"), + err, + ) + })? + .ok_or_else(|| { + TransactionKernelError::other(format!( + "slot value ptr {slot_value_ptr} is uninitialized" + )) + })?; + + let slot_type = slot_metadata[ACCT_STORAGE_SLOT_TYPE_OFFSET as usize]; + let slot_type = u8::try_from(slot_type).map_err(|err| { + TransactionKernelError::other(format!("failed to convert {slot_type} into u8: {err}")) + })?; + let slot_type = StorageSlotType::try_from(slot_type).map_err(|err| { + TransactionKernelError::other_with_source( + format!("failed to convert {slot_type} into storage slot type",), + err, + ) + })?; + + let suffix = slot_metadata[ACCT_STORAGE_SLOT_ID_SUFFIX_OFFSET as usize]; + let prefix = slot_metadata[ACCT_STORAGE_SLOT_ID_PREFIX_OFFSET as usize]; + let slot_id = StorageSlotId::new(suffix, prefix); + + Ok((slot_id, slot_type, slot_value)) + } + fn read_note_recipient_info_from_adv_map( &self, recipient_digest: Word, @@ -193,6 +309,21 @@ impl<'a> TransactionKernelProcess for ProcessState<'a> { fn has_advice_map_entry(&self, key: Word) -> bool { self.advice_provider().get_mapped_values(&key).is_some() } + + fn has_merkle_path( + &self, + root: Word, + leaf_index: Felt, + ) -> Result { + self.advice_provider() + .has_merkle_path(root, Felt::from(TREE_DEPTH), leaf_index) + .map_err(|err| { + TransactionKernelError::other_with_source( + "failed to check for merkle path presence in advice provider", + err, + ) + }) + } } // HELPER FUNCTIONS diff --git a/crates/miden-tx/src/host/link_map.rs b/crates/miden-tx/src/host/link_map.rs index de96421edf..5024c86dfd 100644 --- a/crates/miden-tx/src/host/link_map.rs +++ b/crates/miden-tx/src/host/link_map.rs @@ -1,9 +1,9 @@ use alloc::vec::Vec; use core::cmp::Ordering; -use miden_objects::{Felt, LexicographicWord, Word, ZERO}; use miden_processor::fast::ExecutionOutput; -use miden_processor::{AdviceMutation, ContextId, EventError, ProcessState}; +use miden_processor::{AdviceMutation, ContextId, ProcessState}; +use miden_protocol::{Felt, LexicographicWord, Word, ZERO}; // LINK MAP // ================================================================================================ @@ -42,7 +42,7 @@ impl<'process> LinkMap<'process> { /// /// Expected operand stack state before: [map_ptr, KEY, NEW_VALUE] /// Advice stack state after: [set_operation, entry_ptr] - pub fn handle_set_event(process: &ProcessState<'_>) -> Result, EventError> { + pub fn handle_set_event(process: &ProcessState<'_>) -> Vec { let map_ptr = process.get_stack_item(1); let map_key = process.get_stack_word_be(2); @@ -51,17 +51,14 @@ impl<'process> LinkMap<'process> { let (set_op, entry_ptr) = link_map.compute_set_operation(LexicographicWord::from(map_key)); - Ok(vec![AdviceMutation::extend_stack([ - Felt::from(set_op as u8), - Felt::from(entry_ptr), - ])]) + vec![AdviceMutation::extend_stack([Felt::from(set_op as u8), Felt::from(entry_ptr)])] } /// Handles a `LINK_MAP_GET_EVENT` emitted from a VM. /// /// Expected operand stack state before: [map_ptr, KEY] /// Advice stack state after: [get_operation, entry_ptr] - pub fn handle_get_event(process: &ProcessState<'_>) -> Result, EventError> { + pub fn handle_get_event(process: &ProcessState<'_>) -> Vec { let map_ptr = process.get_stack_item(1); let map_key = process.get_stack_word_be(2); @@ -69,10 +66,7 @@ impl<'process> LinkMap<'process> { let link_map = LinkMap::new(map_ptr, &mem_viewer); let (get_op, entry_ptr) = link_map.compute_get_operation(LexicographicWord::from(map_key)); - Ok(vec![AdviceMutation::extend_stack([ - Felt::from(get_op as u8), - Felt::from(entry_ptr), - ])]) + vec![AdviceMutation::extend_stack([Felt::from(get_op as u8), Felt::from(entry_ptr)])] } /// Returns `true` if the map is empty, `false` otherwise. @@ -322,7 +316,7 @@ impl<'mem> MemoryViewer<'mem> { // https://github.com/0xMiden/miden-vm/issues/2237 // Copy of how Memory::read_element is implemented in Miden VM. - let idx = addr % miden_objects::WORD_SIZE as u32; + let idx = addr % miden_protocol::WORD_SIZE as u32; let word_addr = addr - idx; Some(self.get_kernel_mem_word(word_addr)?[idx as usize]) diff --git a/crates/miden-tx/src/host/mod.rs b/crates/miden-tx/src/host/mod.rs index d31a8fb161..019a3a0291 100644 --- a/crates/miden-tx/src/host/mod.rs +++ b/crates/miden-tx/src/host/mod.rs @@ -10,7 +10,8 @@ mod account_procedures; pub use account_procedures::AccountProcedureIndexMap; pub(crate) mod note_builder; -use miden_lib::StdLibrary; +use miden_protocol::CoreLibrary; +use miden_protocol::vm::EventId; use note_builder::OutputNoteBuilder; mod kernel_process; @@ -21,25 +22,36 @@ pub use script_mast_forest_store::ScriptMastForestStore; mod tx_progress; +mod tx_event; use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::{EventId, TransactionEvent, TransactionEventError}; -use miden_objects::account::{ +use miden_processor::{ + AdviceMutation, + EventError, + EventHandlerRegistry, + Felt, + MastForest, + MastForestStore, + ProcessState, +}; +use miden_protocol::Word; +use miden_protocol::account::{ AccountCode, AccountDelta, AccountHeader, AccountId, AccountStorageHeader, PartialAccount, - StorageMap, - StorageSlotType, + StorageSlotHeader, + StorageSlotId, + StorageSlotName, }; -use miden_objects::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; -use miden_objects::note::{NoteId, NoteInputs, NoteMetadata, NoteRecipient, NoteScript}; -use miden_objects::transaction::{ +use miden_protocol::asset::Asset; +use miden_protocol::note::{NoteAttachment, NoteId, NoteMetadata, NoteRecipient}; +use miden_protocol::transaction::{ InputNote, InputNotes, OutputNote, @@ -47,23 +59,11 @@ use miden_objects::transaction::{ TransactionMeasurements, TransactionSummary, }; -use miden_objects::vm::RowIndex; -use miden_objects::{Hasher, Word}; -use miden_processor::{ - AdviceError, - AdviceMutation, - ContextId, - EventError, - EventHandlerRegistry, - Felt, - MastForest, - MastForestStore, - ProcessState, -}; +use miden_protocol::vm::RowIndex; +pub(crate) use tx_event::{RecipientData, TransactionEvent, TransactionProgressEvent}; pub use tx_progress::TransactionProgress; -use crate::auth::SigningInputs; -use crate::errors::{TransactionHostError, TransactionKernelError}; +use crate::errors::TransactionKernelError; // TRANSACTION BASE HOST // ================================================================================================ @@ -100,19 +100,11 @@ pub struct TransactionBaseHost<'store, STORE> { /// map. output_notes: BTreeMap, - /// Tracks the number of cycles for each of the transaction execution stages. - /// - /// The progress is updated event handlers. - tx_progress: TransactionProgress, - /// Handle the VM default events _before_ passing it to user defined ones. - stdlib_handlers: EventHandlerRegistry, + core_lib_handlers: EventHandlerRegistry, } -impl<'store, STORE> TransactionBaseHost<'store, STORE> -where - STORE: MastForestStore, -{ +impl<'store, STORE> TransactionBaseHost<'store, STORE> { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -124,14 +116,14 @@ where scripts_mast_store: ScriptMastForestStore, acct_procedure_index_map: AccountProcedureIndexMap, ) -> Self { - let stdlib_handlers = { + let core_lib_handlers = { let mut registry = EventHandlerRegistry::new(); - let stdlib = StdLibrary::default(); - for (event_id, handler) in stdlib.handlers() { + let core_lib = CoreLibrary::default(); + for (event_id, handler) in core_lib.handlers() { registry .register(event_id, handler) - .expect("There are no duplicates in the stdlibrary handlers"); + .expect("There are no duplicates in the core library handlers"); } registry }; @@ -144,26 +136,16 @@ where acct_procedure_index_map, output_notes: BTreeMap::default(), input_notes, - tx_progress: TransactionProgress::default(), - stdlib_handlers, + core_lib_handlers, } } // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`MastForest`] that contains the procedure with the given `procedure_root`. - pub fn get_mast_forest(&self, procedure_root: &Word) -> Option> { - // Search in the note MAST forest store, otherwise fall back to the user-provided store - match self.scripts_mast_store.get(procedure_root) { - Some(forest) => Some(forest), - None => self.mast_store.get(procedure_root), - } - } - - /// Returns a reference to the `tx_progress` field of this transaction host. - pub fn tx_progress(&self) -> &TransactionProgress { - &self.tx_progress + /// Returns the ID of the native account. + pub fn native_account_id(&self) -> AccountId { + self.initial_account_header().id() } /// Returns a reference to the initial account header of the native account, which represents @@ -178,6 +160,21 @@ where &self.initial_account_storage_header } + /// Returns the initial storage slot of the native account identified by [`StorageSlotId`], + /// which represents the state at the beginning of the transaction. + pub fn initial_account_storage_slot( + &self, + slot_id: StorageSlotId, + ) -> Result<&StorageSlotHeader, TransactionKernelError> { + self.initial_account_storage_header() + .find_slot_header_by_id(slot_id) + .ok_or_else(|| { + TransactionKernelError::other(format!( + "failed to find storage map with name {slot_id} in storage header" + )) + }) + } + /// Returns a reference to the account delta tracker of this transaction host. pub fn account_delta_tracker(&self) -> &AccountDeltaTracker { &self.account_delta @@ -199,18 +196,11 @@ where self.output_notes.values().cloned().map(|builder| builder.build()).collect() } - /// Consumes `self` and returns the account delta, output notes and transaction progress. - pub fn into_parts( - self, - ) -> (AccountDelta, InputNotes, Vec, TransactionProgress) { + /// Consumes `self` and returns the account delta, input and output notes. + pub fn into_parts(self) -> (AccountDelta, InputNotes, Vec) { let output_notes = self.output_notes.into_values().map(|builder| builder.build()).collect(); - ( - self.account_delta.into_delta(), - self.input_notes, - output_notes, - self.tx_progress, - ) + (self.account_delta.into_delta(), self.input_notes, output_notes) } // MUTATORS @@ -219,7 +209,9 @@ where /// Inserts an output note builder at the specified index. /// /// # Errors - /// Returns an error if a note builder already exists at the given index. + /// + /// Returns an error if: + /// - a note builder already exists at the given index. pub(super) fn insert_output_note_builder( &mut self, note_idx: usize, @@ -235,767 +227,189 @@ where Ok(()) } - /// Returns a mutable reference to the [`AccountProcedureIndexMap`]. - pub fn load_foreign_account_code( + /// Inserts an [`OutputNoteBuilder`] into the output notes created from only the recipient + /// digest. + pub(super) fn output_note_from_recipient_digest( &mut self, - account_code: &AccountCode, - ) -> Result<(), TransactionHostError> { - self.acct_procedure_index_map.insert_code(account_code) - } + note_idx: usize, + metadata: NoteMetadata, + recipient_digest: Word, + ) -> Result, TransactionKernelError> { + let note_builder = OutputNoteBuilder::from_recipient_digest(metadata, recipient_digest)?; + self.insert_output_note_builder(note_idx, note_builder)?; - // EVENT HANDLERS - // -------------------------------------------------------------------------------------------- + Ok(Vec::new()) + } - /// Handles the given [`TransactionEvent`], for example by updating the account delta or pushing - /// requested advice to the advice stack. - pub(super) fn handle_event( + /// Inserts an [`OutputNoteBuilder`] into the output notes created from the full + /// [`NoteRecipient`] object. + pub(super) fn output_note_from_recipient( &mut self, - process: &ProcessState, - event_id: EventId, - ) -> Result { - if let Some(mutations) = self.stdlib_handlers.handle_event(event_id, process)? { - return Ok(TransactionEventHandling::Handled(mutations)); - } - - let transaction_event = TransactionEvent::try_from(event_id).map_err(EventError::from)?; - - // Privileged events can only be emitted from the root context. - if process.ctx() != ContextId::root() && transaction_event.is_privileged() { - return Err(Box::new(TransactionEventError::NotRootContext(transaction_event))); - } - - let advice_mutations = match transaction_event { - TransactionEvent::AccountBeforeForeignLoad => { - self.on_account_before_foreign_load(process) - } - - TransactionEvent::AccountVaultBeforeAddAsset => { - self.on_account_vault_before_add_or_remove_asset(process) - }, - TransactionEvent::AccountVaultAfterAddAsset => { - self.on_account_vault_after_add_asset(process).map(|_| TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::AccountVaultBeforeRemoveAsset => { - self.on_account_vault_before_add_or_remove_asset(process) - }, - TransactionEvent::AccountVaultAfterRemoveAsset => { - self.on_account_vault_after_remove_asset(process).map(|_| TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::AccountVaultBeforeGetBalance => { - self.on_account_vault_before_get_balance(process) - }, - - TransactionEvent::AccountVaultBeforeHasNonFungibleAsset => { - self.on_account_vault_before_has_non_fungible_asset(process) - } - - TransactionEvent::AccountStorageBeforeGetMapItem => { - self.on_account_storage_before_get_map_item(process) - } - - TransactionEvent::AccountStorageBeforeSetItem => Ok(TransactionEventHandling::Handled(Vec::new())), - TransactionEvent::AccountStorageAfterSetItem => { - self.on_account_storage_after_set_item(process).map(|_| TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::AccountStorageBeforeSetMapItem => { - self.on_account_storage_before_set_map_item(process) - }, - TransactionEvent::AccountStorageAfterSetMapItem => { - self.on_account_storage_after_set_map_item(process).map(|_| TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::AccountBeforeIncrementNonce => { - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - TransactionEvent::AccountAfterIncrementNonce => { - self.on_account_after_increment_nonce().map(|_| TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::AccountPushProcedureIndex => { - self.on_account_push_procedure_index(process).map(TransactionEventHandling::Handled) - }, - - TransactionEvent::NoteBeforeCreated => Ok(TransactionEventHandling::Handled(Vec::new())), - TransactionEvent::NoteAfterCreated => self.on_note_after_created(process), - - TransactionEvent::NoteBeforeAddAsset => self.on_note_before_add_asset(process).map(|_| TransactionEventHandling::Handled(Vec::new())), - TransactionEvent::NoteAfterAddAsset => Ok(TransactionEventHandling::Handled(Vec::new())), - - TransactionEvent::AuthRequest => self.on_auth_requested(process), - - TransactionEvent::PrologueStart => { - self.tx_progress.start_prologue(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - TransactionEvent::PrologueEnd => { - self.tx_progress.end_prologue(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::NotesProcessingStart => { - self.tx_progress.start_notes_processing(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - TransactionEvent::NotesProcessingEnd => { - self.tx_progress.end_notes_processing(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::NoteExecutionStart => { - let note_id = process.get_active_note_id()?.expect( - "Note execution interval measurement is incorrect: check the placement of the start and the end of the interval", - ); - self.tx_progress.start_note_execution(process.clk(), note_id); - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - TransactionEvent::NoteExecutionEnd => { - self.tx_progress.end_note_execution(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - }, - - TransactionEvent::TxScriptProcessingStart => { - self.tx_progress.start_tx_script_processing(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - } - TransactionEvent::TxScriptProcessingEnd => { - self.tx_progress.end_tx_script_processing(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - } - - TransactionEvent::EpilogueStart => { - self.tx_progress.start_epilogue(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - } - TransactionEvent::EpilogueAuthProcStart => { - self.tx_progress.start_auth_procedure(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - } - TransactionEvent::EpilogueAuthProcEnd => { - self.tx_progress.end_auth_procedure(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - } - TransactionEvent::EpilogueAfterTxCyclesObtained => { - self.tx_progress.epilogue_after_tx_cycles_obtained(process.clk()); - Ok(TransactionEventHandling::Handled(vec![])) - } - TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount => self.on_before_tx_fee_removed_from_account(process), - TransactionEvent::EpilogueEnd => { - self.tx_progress.end_epilogue(process.clk()); - Ok(TransactionEventHandling::Handled(Vec::new())) - } - TransactionEvent::LinkMapSet => { - return LinkMap::handle_set_event(process).map(TransactionEventHandling::Handled); - }, - TransactionEvent::LinkMapGet => { - return LinkMap::handle_get_event(process).map(TransactionEventHandling::Handled); - }, - TransactionEvent::Unauthorized => { - // Note: This always returns an error to abort the transaction. - Err(self.on_unauthorized(process)) - } - } - .map_err(EventError::from)?; + note_idx: usize, + metadata: NoteMetadata, + recipient: NoteRecipient, + ) -> Result, TransactionKernelError> { + let note_builder = OutputNoteBuilder::from_recipient(metadata, recipient); + self.insert_output_note_builder(note_idx, note_builder)?; - Ok(advice_mutations) + Ok(Vec::new()) } - /// Extract all necessary data for requesting the data to access the foreign account that is - /// being loaded. - /// - /// Expected stack state: `[event, account_id_prefix, account_id_suffix]` - pub fn on_account_before_foreign_load( - &self, - process: &ProcessState, - ) -> Result { - let account_id_word = process.get_stack_word_be(1); - let account_id = - AccountId::try_from([account_id_word[3], account_id_word[2]]).map_err(|err| { - TransactionKernelError::other_with_source( - "failed to convert account ID word into account ID", - err, - ) - })?; - - Ok(TransactionEventHandling::Unhandled(TransactionEventData::ForeignAccount { - account_id, - })) + /// Loads the provided [`AccountCode`] into the host's [`AccountProcedureIndexMap`]. + pub fn load_foreign_account_code(&mut self, account_code: &AccountCode) { + self.acct_procedure_index_map.insert_code(account_code) } - /// Pushes a signature to the advice stack as a response to the `AuthRequest` event. - /// - /// Expected stack state: `[event, MESSAGE, PUB_KEY]` + // EVENT HANDLERS + // -------------------------------------------------------------------------------------------- + + /// Handles the event if the core lib event handler registry contains a handler with the emitted + /// event ID. /// - /// The signature is fetched from the advice map using `hash(PUB_KEY, MESSAGE)` as the key. If - /// not present in the advice map [`TransactionEventHandling::Unhandled`] is returned with the - /// data required to request a signature from a - /// [`TransactionAuthenticator`](crate::auth::TransactionAuthenticator). - fn on_auth_requested( + /// Returns `Some` if the event was handled, `None` otherwise. + pub fn handle_core_lib_events( &self, process: &ProcessState, - ) -> Result { - let message = process.get_stack_word_be(1); - let pub_key_hash = process.get_stack_word_be(5); - let signature_key = Hasher::merge(&[pub_key_hash, message]); - - let tx_summary = self.build_tx_summary(process, message)?; - - if message != tx_summary.to_commitment() { - return Err(TransactionKernelError::TransactionSummaryConstructionFailed( - "transaction summary doesn't commit to the expected message".into(), - )); - } - - let signing_inputs = SigningInputs::TransactionSummary(Box::new(tx_summary)); - - if let Some(signature) = process - .advice_provider() - .get_mapped_values(&signature_key) - .map(|slice| slice.to_vec()) - { - return Ok(TransactionEventHandling::Handled(vec![AdviceMutation::extend_stack( - signature, - )])); - } - - // If the signature is not present in the advice map, extract the necessary data to handle - // the event at a later point. - Ok(TransactionEventHandling::Unhandled(TransactionEventData::AuthRequest { - pub_key_hash, - signing_inputs, - })) - } - - /// Aborts the transaction by building the - /// [`TransactionSummary`](miden_objects::transaction::TransactionSummary) based on elements on - /// the operand stack and advice map. - /// - /// Expected stack state: - /// - /// `[event, MESSAGE]` - /// - /// Expected advice map state: - /// - /// ```text - /// MESSAGE -> [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT] - /// ``` - fn on_unauthorized(&self, process: &ProcessState) -> TransactionKernelError { - let msg = process.get_stack_word_be(1); - - let tx_summary = match self.build_tx_summary(process, msg) { - Ok(s) => s, - Err(err) => return err, - }; - - if msg != tx_summary.to_commitment() { - return TransactionKernelError::TransactionSummaryConstructionFailed( - "transaction summary doesn't commit to the expected message".into(), - ); + ) -> Result>, EventError> { + let event_id = EventId::from_felt(process.get_stack_item(0)); + if let Some(mutations) = self.core_lib_handlers.handle_event(event_id, process)? { + Ok(Some(mutations)) + } else { + Ok(None) } - - TransactionKernelError::Unauthorized(Box::new(tx_summary)) } - /// Extracts all necessary data to handle - /// [`TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount`]. - /// - /// Expected stack state: - /// - /// ```text - /// `[event, FEE_ASSET]` - /// ``` - fn on_before_tx_fee_removed_from_account( - &self, - process: &ProcessState, - ) -> Result { - let fee_asset = process.get_stack_word_be(1); - let fee_asset = FungibleAsset::try_from(fee_asset) - .map_err(TransactionKernelError::FailedToConvertFeeAsset)?; - - Ok(TransactionEventHandling::Unhandled( - TransactionEventData::TransactionFeeComputed { fee_asset }, - )) + /// Converts the provided signature into an advice mutation that pushes it onto the advice stack + /// as a response to an `AuthRequest` event. + pub fn on_auth_requested(&self, signature: Vec) -> Vec { + vec![AdviceMutation::extend_stack(signature)] } - /// Handles the note creation event by extracting note data from the stack and advice provider. - /// - /// If the recipient data and note script are present in the advice provider, creates a new - /// [`OutputNoteBuilder`] and stores it in the `output_notes` field of this - /// [`TransactionBaseHost`]. Otherwise, returns [`TransactionEventHandling::Unhandled`] to - /// request the missing note script from the data store. - /// - /// Expected stack state: `[event, NOTE_METADATA, note_ptr, RECIPIENT, note_idx]` - fn on_note_after_created( + /// Adds an asset to the output note identified by the note index. + pub fn on_note_before_add_asset( &mut self, - process: &ProcessState, - ) -> Result { - let metadata_word = process.get_stack_word_be(1); - let metadata = NoteMetadata::try_from(metadata_word) - .map_err(TransactionKernelError::MalformedNoteMetadata)?; - - let recipient_digest = process.get_stack_word_be(6); - let note_idx = process.get_stack_item(10).as_int() as usize; - - // try to read the full recipient from the advice provider - let recipient = if process.has_advice_map_entry(recipient_digest) { - let (inputs, script_root, serial_num) = - process.read_note_recipient_info_from_adv_map(recipient_digest)?; - - if let Some(script_data) = process.advice_provider().get_mapped_values(&script_root) { - let script = NoteScript::try_from(script_data).map_err(|source| { - TransactionKernelError::MalformedNoteScript { - data: script_data.to_vec(), - source, - } - })?; - - Some(NoteRecipient::new(serial_num, script, inputs)) - } else { - // we couldn't build the full recipient because script root was missing; return the - // info that we did read so that we could request the script from the data store - return Ok(TransactionEventHandling::Unhandled(TransactionEventData::NoteData { - note_idx, - metadata, - script_root, - recipient_digest, - note_inputs: inputs, - serial_num, - })); - } - } else { - None - }; + note_idx: usize, + asset: Asset, + ) -> Result, TransactionKernelError> { + let note_builder = self.output_notes.get_mut(¬e_idx).ok_or_else(|| { + TransactionKernelError::other(format!("failed to find output note {note_idx}")) + })?; - let note_builder = OutputNoteBuilder::new(metadata, recipient_digest, recipient)?; - self.insert_output_note_builder(note_idx, note_builder)?; + note_builder.add_asset(asset)?; - Ok(TransactionEventHandling::Handled(Vec::new())) + Ok(Vec::new()) } - /// Adds an asset at the top of the [OutputNoteBuilder] identified by the note pointer. - /// - /// Expected stack state: `[event, ASSET, note_ptr, num_of_assets, note_idx]` - fn on_note_before_add_asset( + /// Sets the attachment on the output note identified by the note index. + pub fn on_note_before_set_attachment( &mut self, - process: &ProcessState, - ) -> Result<(), TransactionKernelError> { - let stack = process.get_stack_state(); - //# => [ASSET, note_ptr, num_of_assets, note_idx] - - let note_idx = stack[7].as_int(); - assert!(note_idx < self.output_notes.len() as u64); - let node_idx = note_idx as usize; - - let asset_word = process.get_stack_word_be(1); - let asset = Asset::try_from(asset_word).map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_note_before_add_asset", - source, - } + note_idx: usize, + attachment: NoteAttachment, + ) -> Result, TransactionKernelError> { + let note_builder = self.output_notes.get_mut(¬e_idx).ok_or_else(|| { + TransactionKernelError::other(format!("failed to find output note {note_idx}")) })?; - let note_builder = self - .output_notes - .get_mut(&node_idx) - .ok_or_else(|| TransactionKernelError::MissingNote(note_idx))?; + note_builder.set_attachment(attachment); - note_builder.add_asset(asset)?; - - Ok(()) + Ok(Vec::new()) } - /// Loads the index of the procedure root onto the advice stack. - /// - /// Expected stack state: `[event, PROC_ROOT, ...]` - fn on_account_push_procedure_index( + /// Pushes the index of the procedure root in the code identified by the commitment onto the + /// advice stack. + pub fn on_account_push_procedure_index( &mut self, - process: &ProcessState, + code_commitment: Word, + procedure_root: Word, ) -> Result, TransactionKernelError> { - let proc_idx = self.acct_procedure_index_map.get_proc_index(process)?; + let proc_idx = + self.acct_procedure_index_map.get_proc_index(code_commitment, procedure_root)?; Ok(vec![AdviceMutation::extend_stack([Felt::from(proc_idx)])]) } /// Handles the increment nonce event by incrementing the nonce delta by one. - pub fn on_account_after_increment_nonce(&mut self) -> Result<(), TransactionKernelError> { + pub fn on_account_after_increment_nonce( + &mut self, + ) -> Result, TransactionKernelError> { if self.account_delta.was_nonce_incremented() { return Err(TransactionKernelError::NonceCanOnlyIncrementOnce); } self.account_delta.increment_nonce(); - Ok(()) + + Ok(Vec::new()) } // ACCOUNT STORAGE UPDATE HANDLERS // -------------------------------------------------------------------------------------------- - /// Extracts information from the process state about the storage slot being updated and - /// records the latest value of this storage slot. - /// - /// Expected stack state: `[event, slot_index, NEW_SLOT_VALUE]` + /// Tracks the insertion of an item in the account delta. pub fn on_account_storage_after_set_item( &mut self, - process: &ProcessState, - ) -> Result<(), TransactionKernelError> { - // get slot index from the stack and make sure it is valid - let slot_index = process.get_stack_item(1); - - // get number of storage slots initialized by the account - let num_storage_slot = process.get_num_storage_slots()?; - - if slot_index.as_int() >= num_storage_slot { - return Err(TransactionKernelError::InvalidStorageSlotIndex { - max: num_storage_slot, - actual: slot_index.as_int(), - }); - } - - // get the value to which the slot is being updated - let new_slot_value = process.get_stack_word_be(2); - - self.account_delta.storage().set_item(slot_index.as_int() as u8, new_slot_value); - - Ok(()) - } - - /// Checks if the necessary witness for accessing the map item is already in the merkle store, - /// and if not, extracts all necessary data for requesting it. - /// - /// Expected stack state: `[event, KEY, ROOT, index]` - pub fn on_account_storage_before_get_map_item( - &self, - process: &ProcessState, - ) -> Result { - let map_key = process.get_stack_word_be(1); - let current_map_root = process.get_stack_word_be(5); - let slot_index = process.get_stack_item(9); - - self.on_account_storage_before_get_or_set_map_item( - slot_index, - current_map_root, - map_key, - process, - ) - } - - /// Checks if the necessary witness for accessing the map item is already in the merkle store, - /// and if not, extracts all necessary data for requesting it. - /// - /// Expected stack state: `[event, index, KEY, NEW_VALUE, OLD_ROOT]` - pub fn on_account_storage_before_set_map_item( - &self, - process: &ProcessState, - ) -> Result { - let slot_index = process.get_stack_item(1); - let map_key = process.get_stack_word_be(2); - let current_map_root = process.get_stack_word_be(10); - - self.on_account_storage_before_get_or_set_map_item( - slot_index, - current_map_root, - map_key, - process, - ) - } + slot_name: StorageSlotName, + new_value: Word, + ) -> Result, TransactionKernelError> { + self.account_delta.storage().set_item(slot_name, new_value); - /// Checks if the necessary witness for accessing the map item is already in the merkle store, - /// and if not, extracts all necessary data for requesting it. - fn on_account_storage_before_get_or_set_map_item( - &self, - slot_index: Felt, - current_map_root: Word, - map_key: Word, - process: &ProcessState, - ) -> Result { - let current_account_id = process.get_active_account_id()?; - let hashed_map_key = StorageMap::hash_key(map_key); - let leaf_index = StorageMap::hashed_map_key_to_leaf_index(hashed_map_key); - - if advice_provider_has_merkle_path::<{ StorageMap::DEPTH }>( - process, - current_map_root, - leaf_index, - )? { - // If the merkle path is already in the store there is nothing to do. - Ok(TransactionEventHandling::Handled(Vec::new())) - } else { - // For the native account we need to explicitly request the initial map root, while for - // foreign accounts the current map root is always the initial one. - let map_root = if current_account_id == self.initial_account_header().id() { - // For native accounts, we have to request witnesses against the initial root - // instead of the _current_ one, since the data store only has - // witnesses for initial one. - let (slot_type, slot_value) = self - .initial_account_storage_header() - // Slot index should always fit into a usize. - .slot(slot_index.as_int() as usize) - .map_err(|err| { - TransactionKernelError::other_with_source( - "failed to access storage map in storage header", - err, - ) - })?; - if *slot_type != StorageSlotType::Map { - return Err(TransactionKernelError::other(format!( - "expected map slot type at slot index {slot_index}" - ))); - } - *slot_value - } else { - current_map_root - }; - - // If the merkle path is not in the store return the data to request it. - Ok(TransactionEventHandling::Unhandled( - TransactionEventData::AccountStorageMapWitness { - current_account_id, - map_root, - map_key, - }, - )) - } + Ok(Vec::new()) } - /// Extracts information from the process state about the storage map being updated and - /// records the latest values of this storage map. - /// - /// Expected stack state: `[event, slot_index, KEY, PREV_MAP_VALUE, NEW_MAP_VALUE]` + /// Tracks the insertion of a storage map item in the account delta. pub fn on_account_storage_after_set_map_item( &mut self, - process: &ProcessState, - ) -> Result<(), TransactionKernelError> { - // get slot index from the stack and make sure it is valid - let slot_index = process.get_stack_item(1); - - // get number of storage slots initialized by the account - let num_storage_slot = process.get_num_storage_slots()?; - - if slot_index.as_int() >= num_storage_slot { - return Err(TransactionKernelError::InvalidStorageSlotIndex { - max: num_storage_slot, - actual: slot_index.as_int(), - }); - } - - // get the KEY to which the slot is being updated - let key = process.get_stack_word_be(2); - - // get the previous VALUE of the slot - let prev_map_value = process.get_stack_word_be(6); - - // get the VALUE to which the slot is being updated - let new_map_value = process.get_stack_word_be(10); - - self.account_delta.storage().set_map_item( - slot_index.as_int() as u8, - key, - prev_map_value, - new_map_value, - ); + slot_name: StorageSlotName, + key: Word, + old_map_value: Word, + new_map_value: Word, + ) -> Result, TransactionKernelError> { + self.account_delta + .storage() + .set_map_item(slot_name, key, old_map_value, new_map_value); - Ok(()) + Ok(Vec::new()) } // ACCOUNT VAULT UPDATE HANDLERS // -------------------------------------------------------------------------------------------- - /// Extracts the asset that is being added to the account's vault from the process state and - /// updates the appropriate fungible or non-fungible asset map. - /// - /// Expected stack state: `[event, ASSET, ...]` + /// Tracks the addition of an asset to the account vault in the account delta. pub fn on_account_vault_after_add_asset( &mut self, - process: &ProcessState, - ) -> Result<(), TransactionKernelError> { - let asset: Asset = process.get_stack_word_be(1).try_into().map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_account_vault_after_add_asset", - source, - } - })?; - + asset: Asset, + ) -> Result, TransactionKernelError> { self.account_delta .vault_delta_mut() .add_asset(asset) .map_err(TransactionKernelError::AccountDeltaAddAssetFailed)?; - Ok(()) - } - - /// Checks if the necessary witness for accessing the asset is already in the merkle store, - /// and if not, extracts all necessary data for requesting it. - /// - /// Expected stack state: `[event, ASSET, account_vault_root_ptr]` - pub fn on_account_vault_before_add_or_remove_asset( - &self, - process: &ProcessState, - ) -> Result { - let asset_word = process.get_stack_word_be(1); - let asset = Asset::try_from(asset_word).map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_account_vault_before_add_or_remove_asset", - source, - } - })?; - let vault_root_ptr = process.get_stack_item(5); - let vault_root_ptr = u32::try_from(vault_root_ptr).map_err(|_err| { - TransactionKernelError::other(format!( - "vault root ptr should fit into a u32, but was {vault_root_ptr}" - )) - })?; - let current_vault_root = process - .get_mem_word(process.ctx(), vault_root_ptr) - .map_err(|_err| { - TransactionKernelError::other(format!( - "vault root ptr {vault_root_ptr} is not word-aligned" - )) - })? - .ok_or_else(|| { - TransactionKernelError::other(format!( - "vault root ptr {vault_root_ptr} was not initialized" - )) - })?; - - self.on_account_vault_asset_accessed(process, asset.vault_key(), current_vault_root) + Ok(Vec::new()) } - /// Extracts the asset that is being removed from the account's vault from the process state - /// and updates the appropriate fungible or non-fungible asset map. - /// - /// Expected stack state: `[event, ASSET, ...]` + /// Tracks the removal of an asset from the account vault in the account delta. pub fn on_account_vault_after_remove_asset( &mut self, - process: &ProcessState, - ) -> Result<(), TransactionKernelError> { - let asset: Asset = process.get_stack_word_be(1).try_into().map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_account_vault_after_remove_asset", - source, - } - })?; - + asset: Asset, + ) -> Result, TransactionKernelError> { self.account_delta .vault_delta_mut() .remove_asset(asset) .map_err(TransactionKernelError::AccountDeltaRemoveAssetFailed)?; - Ok(()) - } - - /// Checks if the necessary witness for accessing the asset is already in the merkle store, - /// and if not, extracts all necessary data for requesting it. - /// - /// Expected stack state: `[event, faucet_id_prefix, faucet_id_suffix, vault_root_ptr]` - pub fn on_account_vault_before_get_balance( - &self, - process: &ProcessState, - ) -> Result { - let stack_top = process.get_stack_word_be(1); - let faucet_id = AccountId::try_from([stack_top[3], stack_top[2]]).map_err(|err| { - TransactionKernelError::other_with_source( - "failed to convert faucet ID word into faucet ID", - err, - ) - })?; - let vault_root_ptr = stack_top[1]; - let vault_root = process.get_vault_root(vault_root_ptr)?; - - let vault_key = AssetVaultKey::from_account_id(faucet_id).ok_or_else(|| { - TransactionKernelError::other(format!( - "provided faucet ID {faucet_id} is not valid for fungible assets" - )) - })?; - self.on_account_vault_asset_accessed(process, vault_key, vault_root) - } - - /// Checks if the necessary witness for accessing the asset is already in the merkle store, - /// and if not, extracts all necessary data for requesting it. - /// - /// Expected stack state: `[event, ASSET, vault_root_ptr]` - pub fn on_account_vault_before_has_non_fungible_asset( - &self, - process: &ProcessState, - ) -> Result { - let asset_word = process.get_stack_word_be(1); - let asset = Asset::try_from(asset_word).map_err(|err| { - TransactionKernelError::other_with_source("provided asset is not a valid asset", err) - })?; - - let vault_root_ptr = process.get_stack_item(5); - let vault_root = process.get_vault_root(vault_root_ptr)?; - self.on_account_vault_asset_accessed(process, asset.vault_key(), vault_root) - } - - /// Checks if the necessary witness for accessing the provided asset is already in the merkle - /// store, and if not, extracts all necessary data for requesting it. - fn on_account_vault_asset_accessed( - &self, - process: &ProcessState, - vault_key: AssetVaultKey, - current_vault_root: Word, - ) -> Result { - let leaf_index = Felt::new(vault_key.to_leaf_index().value()); - let active_account_id = process.get_active_account_id()?; - - // Note that we check whether a merkle path for the current vault root is present, not - // necessarily for the root we are going to request. This is because the end goal is to - // enable access to an asset against the current vault root, and so if this - // condition is already satisfied, there is nothing to request. - if advice_provider_has_merkle_path::<{ AssetVault::DEPTH }>( - process, - current_vault_root, - leaf_index, - )? { - // If the merkle path is already in the store there is nothing to do. - Ok(TransactionEventHandling::Handled(Vec::new())) - } else { - // For the native account we need to explicitly request the initial vault root, while - // for foreign accounts the current vault root is always the initial one. - let vault_root = if active_account_id == self.initial_account_header().id() { - self.initial_account_header().vault_root() - } else { - current_vault_root - }; - - // If the merkle path is not in the store return the data to request it. - Ok(TransactionEventHandling::Unhandled( - TransactionEventData::AccountVaultAssetWitness { - current_account_id: active_account_id, - vault_root, - asset_key: vault_key, - }, - )) - } + Ok(Vec::new()) } // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- - /// Builds a [TransactionSummary] by extracting data from the advice provider and validating - /// commitments against the host's state. + /// Builds a [`TransactionSummary`] from the current host's state and validates it against the + /// provided commitments. pub(crate) fn build_tx_summary( &self, - process: &ProcessState, - msg: Word, + salt: Word, + output_notes_commitment: Word, + input_notes_commitment: Word, + account_delta_commitment: Word, ) -> Result { - let Some(commitments) = process.advice_provider().get_mapped_values(&msg) else { - return Err(TransactionKernelError::TransactionSummaryConstructionFailed( - "Expected message to exist in advice provider".into(), - )); - }; - - if commitments.len() != 16 { - return Err(TransactionKernelError::TransactionSummaryConstructionFailed( - "Expected 4 words for transaction summary commitments".into(), - )); - } - - let salt = extract_word(commitments, 0); - let output_notes_commitment = extract_word(commitments, 4); - let input_notes_commitment = extract_word(commitments, 8); - let account_delta_commitment = extract_word(commitments, 12); - let account_delta = self.build_account_delta(); let input_notes = self.input_notes(); let output_notes_vec = self.build_output_notes(); @@ -1036,116 +450,23 @@ where Ok(TransactionSummary::new(account_delta, input_notes, output_notes, salt)) } -} -impl<'store, STORE> TransactionBaseHost<'store, STORE> { /// Returns the underlying store of the base host. pub fn store(&self) -> &'store STORE { self.mast_store } } -// TRANSACTION EVENT HANDLING -// ================================================================================================ - -/// Indicates whether a [`TransactionEvent`] was handled or not. -/// -/// If it is unhandled, the necessary data to handle it is returned. -#[derive(Debug)] -pub(super) enum TransactionEventHandling { - Unhandled(TransactionEventData), - Handled(Vec), -} - -/// The data necessary to handle an [`TransactionEvent`]. -#[derive(Debug, Clone)] -pub(super) enum TransactionEventData { - /// The data necessary to handle an auth request. - AuthRequest { - /// The hash of the public key for which a signature was requested. - pub_key_hash: Word, - /// The signing inputs that summarize what is being signed. The commitment to these inputs - /// is the message that is being signed. - signing_inputs: SigningInputs, - }, - /// The data necessary to handle a transaction fee computed event. - TransactionFeeComputed { - /// The fee asset extracted from the stack. - fee_asset: FungibleAsset, - }, - /// The data necessary to request a foreign account's data from the data store. - ForeignAccount { - /// The foreign account's ID. - account_id: AccountId, - }, - /// The data necessary to request an asset witness from the data store. - AccountVaultAssetWitness { - /// The account ID for whose vault a witness is requested. - current_account_id: AccountId, - /// The vault root identifying the asset vault from which a witness is requested. - vault_root: Word, - /// The asset for which a witness is requested. - asset_key: AssetVaultKey, - }, - /// The data necessary to request a storage map witness from the data store. - AccountStorageMapWitness { - /// The account ID for whose storage a witness is requested. - current_account_id: AccountId, - /// The root of the storage map in the account at the beginning of the transaction. - map_root: Word, - /// The raw map key for which a witness is requested. - map_key: Word, - }, - /// The data necessary to request a note script from the data store. - NoteData { - /// The note index extracted from the stack. - note_idx: usize, - /// The note metadata extracted from the stack. - metadata: NoteMetadata, - /// The root of the note script being requested. - script_root: Word, - /// The recipient digest extracted from the stack. - recipient_digest: Word, - /// The note inputs extracted from the advice provider. - note_inputs: NoteInputs, - /// The serial number extracted from the advice provider. - serial_num: Word, - }, -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Returns `true` if the advice provider has a merkle path for the provided root and leaf -/// index, `false` otherwise. -fn advice_provider_has_merkle_path( - process: &ProcessState, - root: Word, - leaf_index: Felt, -) -> Result { - match process - .advice_provider() - .get_merkle_path(root, Felt::from(TREE_DEPTH), leaf_index) - { - // Merkle path is already in the store; consider the event handled. - Ok(_) => Ok(true), - // This means the merkle path is missing in the advice provider. - Err(AdviceError::MerkleStoreLookupFailed(_)) => Ok(false), - // We should never encounter this as long as our inputs to get_merkle_path are correct. - Err(err) => Err(TransactionKernelError::other_with_source( - "unexpected get_merkle_path error", - err, - )), +impl<'store, STORE> TransactionBaseHost<'store, STORE> +where + STORE: MastForestStore, +{ + /// Returns the [`MastForest`] that contains the procedure with the given `procedure_root`. + pub fn get_mast_forest(&self, procedure_root: &Word) -> Option> { + // Search in the note MAST forest store, otherwise fall back to the user-provided store + match self.scripts_mast_store.get(procedure_root) { + Some(forest) => Some(forest), + None => self.mast_store.get(procedure_root), + } } } - -/// Extracts a word from a slice of field elements. -#[inline(always)] -fn extract_word(commitments: &[Felt], start: usize) -> Word { - Word::from([ - commitments[start], - commitments[start + 1], - commitments[start + 2], - commitments[start + 3], - ]) -} diff --git a/crates/miden-tx/src/host/note_builder.rs b/crates/miden-tx/src/host/note_builder.rs index d4016f6e65..eac4f8a006 100644 --- a/crates/miden-tx/src/host/note_builder.rs +++ b/crates/miden-tx/src/host/note_builder.rs @@ -1,5 +1,12 @@ -use miden_objects::asset::Asset; -use miden_objects::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, PartialNote}; +use miden_protocol::asset::Asset; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteMetadata, + NoteRecipient, + PartialNote, +}; use super::{OutputNote, Word}; use crate::errors::TransactionKernelError; @@ -24,39 +31,39 @@ impl OutputNoteBuilder { /// recipient. /// /// # Errors - /// Returns an error if the note is public but no recipient is provided. - pub fn new( + /// + /// Returns an error if: + /// - the note is public. + pub fn from_recipient_digest( metadata: NoteMetadata, recipient_digest: Word, - recipient: Option, ) -> Result { - // For public notes, we must have a recipient - if !metadata.is_private() && recipient.is_none() { + // For public notes, we must have a recipient. + if !metadata.is_private() { return Err(TransactionKernelError::PublicNoteMissingDetails( metadata, recipient_digest, )); } - // If recipient is present, verify its digest matches the provided recipient_digest - if let Some(ref recipient) = recipient - && recipient.digest() != recipient_digest - { - return Err(TransactionKernelError::other(format!( - "recipient digest mismatch: expected {}, but recipient has digest {}", - recipient_digest, - recipient.digest() - ))); - } - Ok(Self { metadata, recipient_digest, - recipient, + recipient: None, assets: NoteAssets::default(), }) } + /// Returns a new [`OutputNoteBuilder`] from the provided metadata and recipient. + pub fn from_recipient(metadata: NoteMetadata, recipient: NoteRecipient) -> Self { + Self { + metadata, + recipient_digest: recipient.digest(), + recipient: Some(recipient), + assets: NoteAssets::default(), + } + } + // STATE MUTATORS // -------------------------------------------------------------------------------------------- @@ -77,6 +84,11 @@ impl OutputNoteBuilder { Ok(()) } + /// Overwrites the attachment in the note's metadata. + pub fn set_attachment(&mut self, attachment: NoteAttachment) { + self.metadata.set_attachment(attachment); + } + /// Converts this builder to an [OutputNote]. /// /// Depending on the available information, this may result in [OutputNote::Full] or diff --git a/crates/miden-tx/src/host/script_mast_forest_store.rs b/crates/miden-tx/src/host/script_mast_forest_store.rs index 071f84208b..6a8e82054d 100644 --- a/crates/miden-tx/src/host/script_mast_forest_store.rs +++ b/crates/miden-tx/src/host/script_mast_forest_store.rs @@ -1,12 +1,12 @@ use alloc::collections::BTreeMap; use alloc::sync::Arc; -use miden_objects::Word; -use miden_objects::assembly::mast::MastForest; -use miden_objects::note::NoteScript; -use miden_objects::transaction::TransactionScript; -use miden_objects::vm::AdviceMap; use miden_processor::MastForestStore; +use miden_protocol::Word; +use miden_protocol::assembly::mast::MastForest; +use miden_protocol::note::NoteScript; +use miden_protocol::transaction::TransactionScript; +use miden_protocol::vm::AdviceMap; /// Stores the MAST forests for a set of scripts (both note scripts and transaction scripts). /// diff --git a/crates/miden-tx/src/host/storage_delta_tracker.rs b/crates/miden-tx/src/host/storage_delta_tracker.rs index 81da62182b..6270612130 100644 --- a/crates/miden-tx/src/host/storage_delta_tracker.rs +++ b/crates/miden-tx/src/host/storage_delta_tracker.rs @@ -1,11 +1,14 @@ use alloc::collections::BTreeMap; +use alloc::vec::Vec; -use miden_objects::Word; -use miden_objects::account::{ +use miden_protocol::Word; +use miden_protocol::account::{ AccountStorageDelta, AccountStorageHeader, PartialAccount, - StorageMap, + StorageSlotDelta, + StorageSlotHeader, + StorageSlotName, StorageSlotType, }; @@ -27,9 +30,9 @@ pub struct StorageDeltaTracker { /// executed. This is only used to look up the initial values of storage _value_ slots, while /// the map slots are unused. storage_header: AccountStorageHeader, - /// A map from slot index to a map of key-value pairs where the key is a storage map key and + /// A map from slot name to a map of key-value pairs where the key is a storage map key and /// the value represents the value of that key at the beginning of transaction execution. - init_maps: BTreeMap>, + init_maps: BTreeMap>, /// The account storage delta. delta: AccountStorageDelta, } @@ -58,34 +61,37 @@ impl StorageDeltaTracker { // Insert account storage into delta if it is new to match the kernel behavior. if account.is_new() { - (0..u8::MAX).zip(account.storage().header().slots()).for_each( - |(slot_idx, (slot_type, value))| match slot_type { + account.storage().header().slots().for_each(|slot_header| { + match slot_header.slot_type() { StorageSlotType::Value => { // For new accounts, all values should be added to the delta, even empty // words, so that the final delta includes the storage slot. - storage_delta_tracker.set_item(slot_idx, *value); + storage_delta_tracker + .set_item(slot_header.name().clone(), slot_header.value()); }, StorageSlotType::Map => { let storage_map = account .storage() .maps() - .find(|map| map.root() == *value) + .find(|map| map.root() == slot_header.value()) .expect("storage map should be present in partial storage"); // Make sure each map is represented by at least an empty storage map delta. - storage_delta_tracker.delta.insert_empty_map_delta(slot_idx); + storage_delta_tracker + .delta + .insert_empty_map_delta(slot_header.name().clone()); storage_map.entries().for_each(|(key, value)| { storage_delta_tracker.set_map_item( - slot_idx, + slot_header.name().clone(), *key, Word::empty(), *value, ); }); }, - }, - ); + } + }); } storage_delta_tracker @@ -95,16 +101,26 @@ impl StorageDeltaTracker { // -------------------------------------------------------------------------------------------- /// Updates a value slot. - pub fn set_item(&mut self, slot_index: u8, new_value: Word) { - self.delta.set_item(slot_index, new_value); + pub fn set_item(&mut self, slot_name: StorageSlotName, new_value: Word) { + self.delta + .set_item(slot_name, new_value) + .expect("transaction kernel should not change slot types"); } /// Updates a map slot. - pub fn set_map_item(&mut self, slot_index: u8, key: Word, prev_value: Word, new_value: Word) { + pub fn set_map_item( + &mut self, + slot_name: StorageSlotName, + key: Word, + prev_value: Word, + new_value: Word, + ) { // Don't update the delta if the new value matches the old one. if prev_value != new_value { - self.set_init_map_item(slot_index, key, prev_value); - self.delta.set_map_item(slot_index, key, new_value); + self.set_init_map_item(slot_name.clone(), key, prev_value); + self.delta + .set_map_item(slot_name, key, new_value) + .expect("transaction kernel should not change slot types"); } } @@ -118,8 +134,8 @@ impl StorageDeltaTracker { /// Sets the initial value of the given key in the given slot to the given value, if no value is /// already tracked for that key. - fn set_init_map_item(&mut self, slot_index: u8, key: Word, prev_value: Word) { - let slot_map = self.init_maps.entry(slot_index).or_default(); + fn set_init_map_item(&mut self, slot_name: StorageSlotName, key: Word, prev_value: Word) { + let slot_map = self.init_maps.entry(slot_name).or_default(); slot_map.entry(key).or_insert(prev_value); } @@ -136,59 +152,64 @@ impl StorageDeltaTracker { init_maps, delta, } = self; - let (mut value_slots, mut map_slots) = delta.into_parts(); - - // Skip normalization of value slots for new accounts. Since the initial value for - // normalization defaults to Word::empty, this prevents slots being removed that are validly - // created with an empty value. - if !is_account_new { - // Keep only the values whose new value is different from the initial value. - value_slots.retain(|slot_idx, new_value| { - // SAFETY: The header in the initial storage is the one from the account against - // which the transaction is executed, so accessing that slot index - // should be fine. - let (_, initial_value) = - storage_header.slot(*slot_idx as usize).expect("index should be in bounds"); - new_value != initial_value - }); - } + let mut deltas = delta.into_map(); + + deltas.retain(|slot_name, slot_delta| { + match slot_delta { + StorageSlotDelta::Value(new_value) => { + // SAFETY: The header in the initial storage is the one from the account + // against which the transaction is executed, so accessing that slot name + // should be fine. + let slot_header = storage_header + .find_slot_header_by_name(slot_name) + .expect("slot name should exist"); + + // Only retain the value if the account is new or if it has changed. + // New accounts must contain all slots, even empty ones, to represent the full + // storage state. + is_account_new || *new_value != slot_header.value() + }, - // On the key-value level: Keep only the key-value pairs whose new value is different from - // the initial value. - // On the map level: Keep only the maps that are non-empty after its key-value pairs have - // been normalized, or if the account is new. - map_slots.retain(|slot_idx, map_delta| { - let init_map = init_maps.get(slot_idx); - - if let Some(init_map) = init_map { - map_delta.as_map_mut().retain(|key, new_value| { - let initial_value = init_map.get(key.inner()).expect( - "the initial value should be present for every value that was updated", - ); - new_value != initial_value - }); - } + // On the key-value level: Keep only the key-value pairs whose new value is + // different from the initial value. + // On the map level: Keep only the maps that are non-empty after its key-value + // pairs have been normalized, or if the account is new. + StorageSlotDelta::Map(map_delta) => { + let init_map = init_maps.get(slot_name); + + if let Some(init_map) = init_map { + map_delta.as_map_mut().retain(|key, new_value| { + let initial_value = init_map.get(key.inner()).expect( + "the initial value should be present for every value that was updated", + ); + new_value != initial_value + }); + } - // Only retain the map delta if the account is new or if it still contains values after - // normalization. - self.is_account_new || !map_delta.is_empty() + // Only retain the map delta if the account is new or if it still contains + // values after normalization. + is_account_new || !map_delta.is_empty() + }, + } }); - AccountStorageDelta::from_parts(value_slots, map_slots) - .expect("storage delta should still be valid since no new values were added") + AccountStorageDelta::from_raw(deltas) } } /// Creates empty slots of the same slot types as the to-be-created account. fn empty_storage_header_from_account(account: &PartialAccount) -> AccountStorageHeader { - let slots = account + let slots: Vec = account .storage() .header() .slots() - .map(|(slot_type, _)| match slot_type { - StorageSlotType::Value => (*slot_type, Word::empty()), - StorageSlotType::Map => (*slot_type, StorageMap::new().root()), + .map(|slot_header| match slot_header.slot_type() { + StorageSlotType::Value => { + StorageSlotHeader::with_empty_value(slot_header.name().clone()) + }, + StorageSlotType::Map => StorageSlotHeader::with_empty_map(slot_header.name().clone()), }) .collect(); - AccountStorageHeader::new(slots) + + AccountStorageHeader::new(slots).expect("storage header should be valid") } diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs new file mode 100644 index 0000000000..760a6aff4e --- /dev/null +++ b/crates/miden-tx/src/host/tx_event.rs @@ -0,0 +1,819 @@ +use alloc::vec::Vec; + +use miden_processor::{AdviceMutation, AdviceProvider, ProcessState, RowIndex}; +use miden_protocol::account::{AccountId, StorageMap, StorageSlotName, StorageSlotType}; +use miden_protocol::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; +use miden_protocol::note::{ + NoteAttachment, + NoteAttachmentArray, + NoteAttachmentContent, + NoteAttachmentKind, + NoteAttachmentScheme, + NoteId, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteTag, + NoteType, +}; +use miden_protocol::transaction::memory::{NOTE_MEM_SIZE, OUTPUT_NOTE_SECTION_OFFSET}; +use miden_protocol::transaction::{TransactionEventId, TransactionSummary}; +use miden_protocol::vm::EventId; +use miden_protocol::{Felt, Hasher, Word}; + +use crate::host::{TransactionBaseHost, TransactionKernelProcess}; +use crate::{LinkMap, TransactionKernelError}; + +// TRANSACTION PROGRESS EVENT +// ================================================================================================ +#[derive(Debug)] +pub(crate) enum TransactionProgressEvent { + PrologueStart(RowIndex), + PrologueEnd(RowIndex), + + NotesProcessingStart(RowIndex), + NotesProcessingEnd(RowIndex), + + NoteExecutionStart { note_id: NoteId, clk: RowIndex }, + NoteExecutionEnd(RowIndex), + + TxScriptProcessingStart(RowIndex), + TxScriptProcessingEnd(RowIndex), + + EpilogueStart(RowIndex), + EpilogueEnd(RowIndex), + + EpilogueAuthProcStart(RowIndex), + EpilogueAuthProcEnd(RowIndex), + + EpilogueAfterTxCyclesObtained(RowIndex), +} + +// TRANSACTION EVENT +// ================================================================================================ + +/// The data necessary to handle a [`TransactionEventId`]. +#[derive(Debug)] +pub(crate) enum TransactionEvent { + /// The data necessary to request a foreign account's data from the data store. + AccountBeforeForeignLoad { + /// The foreign account's ID. + foreign_account_id: AccountId, + }, + + AccountVaultAfterRemoveAsset { + asset: Asset, + }, + + AccountVaultAfterAddAsset { + asset: Asset, + }, + + AccountStorageAfterSetItem { + slot_name: StorageSlotName, + new_value: Word, + }, + + AccountStorageAfterSetMapItem { + slot_name: StorageSlotName, + key: Word, + old_value: Word, + new_value: Word, + }, + + /// The data necessary to request a storage map witness from the data store. + AccountStorageBeforeMapItemAccess { + /// The account ID for whose storage a witness is requested. + active_account_id: AccountId, + /// The root of the storage map for which a witness is requested. + map_root: Word, + /// The raw map key for which a witness is requested. + map_key: Word, + }, + + /// The data necessary to request an asset witness from the data store. + AccountVaultBeforeAssetAccess { + /// The account ID for whose vault a witness is requested. + active_account_id: AccountId, + /// The vault root identifying the asset vault from which a witness is requested. + vault_root: Word, + /// The asset for which a witness is requested. + asset_key: AssetVaultKey, + }, + + AccountAfterIncrementNonce, + + AccountPushProcedureIndex { + /// The code commitment of the active account. + code_commitment: Word, + /// The procedure root whose index is requested. + procedure_root: Word, + }, + + NoteBeforeCreated { + /// The note index extracted from the stack. + note_idx: usize, + /// The note metadata extracted from the stack. + metadata: NoteMetadata, + /// The recipient data extracted from the advice inputs. + recipient_data: RecipientData, + }, + + NoteBeforeAddAsset { + /// The note index to which the asset is added. + note_idx: usize, + /// The asset that is added to the output note. + asset: Asset, + }, + + NoteBeforeSetAttachment { + /// The note index on which the attachment is set. + note_idx: usize, + /// The attachment that is set. + attachment: NoteAttachment, + }, + + /// The data necessary to handle an auth request. + AuthRequest { + pub_key_hash: Word, + tx_summary: TransactionSummary, + signature: Option>, + }, + + Unauthorized { + tx_summary: TransactionSummary, + }, + + EpilogueBeforeTxFeeRemovedFromAccount { + fee_asset: FungibleAsset, + }, + + LinkMapSet { + advice_mutation: Vec, + }, + LinkMapGet { + advice_mutation: Vec, + }, + + Progress(TransactionProgressEvent), +} + +impl TransactionEvent { + /// Extracts the [`TransactionEventId`] from the stack as well as the data necessary to handle + /// it. + /// + /// Returns `Some` if the extracted [`TransactionEventId`] resulted in an event that needs to be + /// handled, `None` otherwise. + pub fn extract<'store, STORE>( + base_host: &TransactionBaseHost<'store, STORE>, + process: &ProcessState, + ) -> Result, TransactionKernelError> { + let event_id = EventId::from_felt(process.get_stack_item(0)); + let tx_event_id = TransactionEventId::try_from(event_id).map_err(|err| { + TransactionKernelError::other_with_source( + "failed to convert event ID into transaction event ID", + err, + ) + })?; + + let tx_event = match tx_event_id { + TransactionEventId::AccountBeforeForeignLoad => { + // Expected stack state: [event, account_id_prefix, account_id_suffix] + let account_id_word = process.get_stack_word_be(1); + let account_id = AccountId::try_from([account_id_word[3], account_id_word[2]]) + .map_err(|err| { + TransactionKernelError::other_with_source( + "failed to convert account ID word into account ID", + err, + ) + })?; + + Some(TransactionEvent::AccountBeforeForeignLoad { foreign_account_id: account_id }) + }, + TransactionEventId::AccountVaultBeforeAddAsset + | TransactionEventId::AccountVaultBeforeRemoveAsset => { + // Expected stack state: [event, ASSET, account_vault_root_ptr] + let asset_word = process.get_stack_word_be(1); + let asset = Asset::try_from(asset_word).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "on_account_vault_before_add_or_remove_asset", + source, + } + })?; + + let vault_root_ptr = process.get_stack_item(5); + let current_vault_root = process.get_vault_root(vault_root_ptr)?; + + on_account_vault_asset_accessed( + base_host, + process, + asset.vault_key(), + current_vault_root, + )? + }, + TransactionEventId::AccountVaultAfterRemoveAsset => { + // Expected stack state: [event, ASSET] + let asset: Asset = process.get_stack_word_be(1).try_into().map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "on_account_vault_after_remove_asset", + source, + } + })?; + + Some(TransactionEvent::AccountVaultAfterRemoveAsset { asset }) + }, + TransactionEventId::AccountVaultAfterAddAsset => { + // Expected stack state: [event, ASSET] + let asset: Asset = process.get_stack_word_be(1).try_into().map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "on_account_vault_after_add_asset", + source, + } + })?; + + Some(TransactionEvent::AccountVaultAfterAddAsset { asset }) + }, + TransactionEventId::AccountVaultBeforeGetBalance => { + // Expected stack state: + // [event, faucet_id_prefix, faucet_id_suffix, vault_root_ptr] + let stack_top = process.get_stack_word_be(1); + let faucet_id = + AccountId::try_from([stack_top[3], stack_top[2]]).map_err(|err| { + TransactionKernelError::other_with_source( + "failed to convert faucet ID word into faucet ID", + err, + ) + })?; + let vault_root_ptr = stack_top[1]; + let vault_root = process.get_vault_root(vault_root_ptr)?; + + let vault_key = AssetVaultKey::from_account_id(faucet_id).ok_or_else(|| { + TransactionKernelError::other(format!( + "provided faucet ID {faucet_id} is not valid for fungible assets" + )) + })?; + + on_account_vault_asset_accessed(base_host, process, vault_key, vault_root)? + }, + TransactionEventId::AccountVaultBeforeHasNonFungibleAsset => { + // Expected stack state: [event, ASSET, vault_root_ptr] + let asset_word = process.get_stack_word_be(1); + let asset = Asset::try_from(asset_word).map_err(|err| { + TransactionKernelError::other_with_source( + "provided asset is not a valid asset", + err, + ) + })?; + + let vault_root_ptr = process.get_stack_item(5); + let vault_root = process.get_vault_root(vault_root_ptr)?; + + on_account_vault_asset_accessed(base_host, process, asset.vault_key(), vault_root)? + }, + + TransactionEventId::AccountStorageBeforeSetItem => None, + + TransactionEventId::AccountStorageAfterSetItem => { + // Expected stack state: [event, slot_ptr, VALUE] + let slot_ptr = process.get_stack_item(1); + let new_value = process.get_stack_word_be(2); + + let (slot_id, slot_type, _old_value) = process.get_storage_slot(slot_ptr)?; + + let slot_header = base_host.initial_account_storage_slot(slot_id)?; + let slot_name = slot_header.name().clone(); + + if !slot_type.is_value() { + return Err(TransactionKernelError::other(format!( + "expected slot to be of type value, found {slot_type}" + ))); + } + + Some(TransactionEvent::AccountStorageAfterSetItem { slot_name, new_value }) + }, + + TransactionEventId::AccountStorageBeforeGetMapItem => { + // Expected stack state: [event, slot_ptr, KEY] + let slot_ptr = process.get_stack_item(1); + let map_key = process.get_stack_word_be(2); + + on_account_storage_map_item_accessed(base_host, process, slot_ptr, map_key)? + }, + + TransactionEventId::AccountStorageBeforeSetMapItem => { + // Expected stack state: [event, slot_ptr, KEY] + let slot_ptr = process.get_stack_item(1); + let map_key = process.get_stack_word_be(2); + + on_account_storage_map_item_accessed(base_host, process, slot_ptr, map_key)? + }, + + TransactionEventId::AccountStorageAfterSetMapItem => { + // Expected stack state: [event, slot_ptr, KEY, OLD_VALUE, NEW_VALUE] + let slot_ptr = process.get_stack_item(1); + let key = process.get_stack_word_be(2); + let old_value = process.get_stack_word_be(6); + let new_value = process.get_stack_word_be(10); + + // Resolve slot ID to slot name. + let (slot_id, ..) = process.get_storage_slot(slot_ptr)?; + let slot_header = base_host.initial_account_storage_slot(slot_id)?; + let slot_name = slot_header.name().clone(); + + Some(TransactionEvent::AccountStorageAfterSetMapItem { + slot_name, + key, + old_value, + new_value, + }) + }, + + TransactionEventId::AccountBeforeIncrementNonce => None, + + TransactionEventId::AccountAfterIncrementNonce => { + Some(TransactionEvent::AccountAfterIncrementNonce) + }, + + TransactionEventId::AccountPushProcedureIndex => { + // Expected stack state: [event, PROC_ROOT] + let procedure_root = process.get_stack_word_be(1); + let code_commitment = process.get_active_account_code_commitment()?; + + Some(TransactionEvent::AccountPushProcedureIndex { + code_commitment, + procedure_root, + }) + }, + + TransactionEventId::NoteBeforeCreated => { + // Expected stack state: [event, tag, note_type, RECIPIENT] + let tag = process.get_stack_item(1); + let note_type = process.get_stack_item(2); + let recipient_digest = process.get_stack_word_be(3); + + let sender = base_host.native_account_id(); + let metadata = build_note_metadata(sender, note_type, tag)?; + + let note_idx = process.get_num_output_notes() as usize; + + // try to read the full recipient from the advice provider + let recipient_data = if process.has_advice_map_entry(recipient_digest) { + let (note_inputs, script_root, serial_num) = + process.read_note_recipient_info_from_adv_map(recipient_digest)?; + + let note_script = process + .advice_provider() + .get_mapped_values(&script_root) + .map(|script_data| { + NoteScript::try_from(script_data).map_err(|source| { + TransactionKernelError::MalformedNoteScript { + data: script_data.to_vec(), + source, + } + }) + }) + .transpose()?; + + match note_script { + Some(note_script) => { + let recipient = + NoteRecipient::new(serial_num, note_script, note_inputs); + + if recipient.digest() != recipient_digest { + return Err(TransactionKernelError::other(format!( + "recipient digest is {recipient_digest}, but recipient constructed from raw inputs has digest {}", + recipient.digest() + ))); + } + + RecipientData::Recipient(recipient) + }, + None => RecipientData::ScriptMissing { + recipient_digest, + serial_num, + script_root, + note_inputs, + }, + } + } else { + RecipientData::Digest(recipient_digest) + }; + + Some(TransactionEvent::NoteBeforeCreated { note_idx, metadata, recipient_data }) + }, + + TransactionEventId::NoteAfterCreated => None, + + TransactionEventId::NoteBeforeAddAsset => { + // Expected stack state: [event, ASSET, note_ptr, num_of_assets, note_idx] + let note_idx = process.get_stack_item(7).as_int() as usize; + + let asset_word = process.get_stack_word_be(1); + let asset = Asset::try_from(asset_word).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "on_note_before_add_asset", + source, + } + })?; + + Some(TransactionEvent::NoteBeforeAddAsset { note_idx, asset }) + }, + + TransactionEventId::NoteAfterAddAsset => None, + + TransactionEventId::NoteBeforeSetAttachment => { + // Expected stack state: [ + // event, attachment_scheme, attachment_kind, + // note_ptr, note_ptr, ATTACHMENT + // ] + + let attachment_scheme = process.get_stack_item(1); + let attachment_kind = process.get_stack_item(2); + let note_ptr = process.get_stack_item(3); + let attachment = process.get_stack_word_be(5); + + let (note_idx, attachment) = extract_note_attachment( + attachment_scheme, + attachment_kind, + attachment, + note_ptr, + process.advice_provider(), + )?; + + Some(TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment }) + }, + + TransactionEventId::AuthRequest => { + // Expected stack state: [event, MESSAGE, PUB_KEY] + let message = process.get_stack_word_be(1); + let pub_key_hash = process.get_stack_word_be(5); + let signature_key = Hasher::merge(&[pub_key_hash, message]); + + let signature = process + .advice_provider() + .get_mapped_values(&signature_key) + .map(|slice| slice.to_vec()); + + let tx_summary = extract_tx_summary(base_host, process, message)?; + + Some(TransactionEvent::AuthRequest { pub_key_hash, tx_summary, signature }) + }, + + TransactionEventId::Unauthorized => { + // Expected stack state: [event, MESSAGE] + let message = process.get_stack_word_be(1); + let tx_summary = extract_tx_summary(base_host, process, message)?; + + Some(TransactionEvent::Unauthorized { tx_summary }) + }, + + TransactionEventId::EpilogueBeforeTxFeeRemovedFromAccount => { + // Expected stack state: [event, FEE_ASSET] + let fee_asset = process.get_stack_word_be(1); + let fee_asset = FungibleAsset::try_from(fee_asset) + .map_err(TransactionKernelError::FailedToConvertFeeAsset)?; + + Some(TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount { fee_asset }) + }, + + TransactionEventId::LinkMapSet => Some(TransactionEvent::LinkMapSet { + advice_mutation: LinkMap::handle_set_event(process), + }), + TransactionEventId::LinkMapGet => Some(TransactionEvent::LinkMapGet { + advice_mutation: LinkMap::handle_get_event(process), + }), + + TransactionEventId::PrologueStart => Some(TransactionEvent::Progress( + TransactionProgressEvent::PrologueStart(process.clk()), + )), + TransactionEventId::PrologueEnd => Some(TransactionEvent::Progress( + TransactionProgressEvent::PrologueEnd(process.clk()), + )), + + TransactionEventId::NotesProcessingStart => Some(TransactionEvent::Progress( + TransactionProgressEvent::NotesProcessingStart(process.clk()), + )), + TransactionEventId::NotesProcessingEnd => Some(TransactionEvent::Progress( + TransactionProgressEvent::NotesProcessingEnd(process.clk()), + )), + + TransactionEventId::NoteExecutionStart => { + let note_id = process.get_active_note_id()?.ok_or_else(|| TransactionKernelError::other( + "note execution interval measurement is incorrect: check the placement of the start and the end of the interval", + ))?; + + Some(TransactionEvent::Progress(TransactionProgressEvent::NoteExecutionStart { + note_id, + clk: process.clk(), + })) + }, + TransactionEventId::NoteExecutionEnd => Some(TransactionEvent::Progress( + TransactionProgressEvent::NoteExecutionEnd(process.clk()), + )), + + TransactionEventId::TxScriptProcessingStart => Some(TransactionEvent::Progress( + TransactionProgressEvent::TxScriptProcessingStart(process.clk()), + )), + TransactionEventId::TxScriptProcessingEnd => Some(TransactionEvent::Progress( + TransactionProgressEvent::TxScriptProcessingEnd(process.clk()), + )), + + TransactionEventId::EpilogueStart => Some(TransactionEvent::Progress( + TransactionProgressEvent::EpilogueStart(process.clk()), + )), + TransactionEventId::EpilogueEnd => Some(TransactionEvent::Progress( + TransactionProgressEvent::EpilogueEnd(process.clk()), + )), + + TransactionEventId::EpilogueAuthProcStart => Some(TransactionEvent::Progress( + TransactionProgressEvent::EpilogueAuthProcStart(process.clk()), + )), + TransactionEventId::EpilogueAuthProcEnd => Some(TransactionEvent::Progress( + TransactionProgressEvent::EpilogueAuthProcEnd(process.clk()), + )), + + TransactionEventId::EpilogueAfterTxCyclesObtained => Some(TransactionEvent::Progress( + TransactionProgressEvent::EpilogueAfterTxCyclesObtained(process.clk()), + )), + }; + + Ok(tx_event) + } +} + +// RECIPIENT DATA +// ================================================================================================ + +/// The partial data to construct a note recipient. +#[derive(Debug)] +pub(crate) enum RecipientData { + /// Only the recipient digest is available. + Digest(Word), + /// The full [`NoteRecipient`] is available. + Recipient(NoteRecipient), + /// Everything but the note script is available. + ScriptMissing { + recipient_digest: Word, + serial_num: Word, + script_root: Word, + note_inputs: NoteInputs, + }, +} + +/// Checks if the necessary witness for accessing the asset identified by the vault key is already +/// in the merkle store, and: +/// - If so, returns `None`. +/// - If not, returns `Some` with all necessary data for requesting it. +fn on_account_vault_asset_accessed<'store, STORE>( + base_host: &TransactionBaseHost<'store, STORE>, + process: &ProcessState, + vault_key: AssetVaultKey, + vault_root: Word, +) -> Result, TransactionKernelError> { + let leaf_index = Felt::new(vault_key.to_leaf_index().value()); + let active_account_id = process.get_active_account_id()?; + + // For the native account we need to explicitly request the initial vault root, while for + // foreign accounts the current vault root is always the initial one. + let vault_root = if active_account_id == base_host.native_account_id() { + base_host.initial_account_header().vault_root() + } else { + vault_root + }; + + // Note that we check whether a merkle path for the current vault root is present, not + // necessarily for the root we are going to request. This is because the end goal is to + // enable access to an asset against the current vault root, and so if this + // condition is already satisfied, there is nothing to request. + if process.has_merkle_path::<{ AssetVault::DEPTH }>(vault_root, leaf_index)? { + // If the witness already exists, the event does not need to be handled. + Ok(None) + } else { + Ok(Some(TransactionEvent::AccountVaultBeforeAssetAccess { + active_account_id, + vault_root, + asset_key: vault_key, + })) + } +} + +/// Checks if the necessary witness for accessing the map item identified by the map key is already +/// in the merkle store, and: +/// - If so, returns `None`. +/// - If not, returns `Some` with all necessary data for requesting it. +fn on_account_storage_map_item_accessed<'store, STORE>( + base_host: &TransactionBaseHost<'store, STORE>, + process: &ProcessState, + slot_ptr: Felt, + map_key: Word, +) -> Result, TransactionKernelError> { + let (slot_id, slot_type, current_map_root) = process.get_storage_slot(slot_ptr)?; + + if !slot_type.is_map() { + return Err(TransactionKernelError::other(format!( + "expected slot to be of type map, found {slot_type}" + ))); + } + + let active_account_id = process.get_active_account_id()?; + let leaf_index: Felt = StorageMap::map_key_to_leaf_index(map_key) + .value() + .try_into() + .expect("expected key index to be a felt"); + + // For the native account we need to explicitly request the initial map root, + // while for foreign accounts the current map root is always the initial one. + let map_root = if active_account_id == base_host.native_account_id() { + // For native accounts, we have to request witnesses against the initial + // root instead of the _current_ one, since the data + // store only has witnesses for initial one. + let slot_header = base_host.initial_account_storage_slot(slot_id)?; + + if slot_header.slot_type() != StorageSlotType::Map { + return Err(TransactionKernelError::other(format!( + "expected slot {slot_id} to be of type map" + ))); + } + slot_header.value() + } else { + current_map_root + }; + + if process.has_merkle_path::<{ StorageMap::DEPTH }>(current_map_root, leaf_index)? { + // If the witness already exists, the event does not need to be handled. + Ok(None) + } else { + Ok(Some(TransactionEvent::AccountStorageBeforeMapItemAccess { + active_account_id, + map_root, + map_key, + })) + } +} + +/// Extracts the transaction summary from the advice map using the provided `message` as the +/// key. +/// +/// ```text +/// Expected advice map state: { +/// MESSAGE: [ +/// SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT +/// ] +/// } +/// ``` +fn extract_tx_summary<'store, STORE>( + base_host: &TransactionBaseHost<'store, STORE>, + process: &ProcessState, + message: Word, +) -> Result { + let Some(commitments) = process.advice_provider().get_mapped_values(&message) else { + return Err(TransactionKernelError::TransactionSummaryConstructionFailed( + "expected message to exist in advice provider".into(), + )); + }; + + if commitments.len() != 16 { + return Err(TransactionKernelError::TransactionSummaryConstructionFailed( + "expected 4 words for transaction summary commitments".into(), + )); + } + + let salt = extract_word(commitments, 0); + let output_notes_commitment = extract_word(commitments, 4); + let input_notes_commitment = extract_word(commitments, 8); + let account_delta_commitment = extract_word(commitments, 12); + + let tx_summary = base_host.build_tx_summary( + salt, + output_notes_commitment, + input_notes_commitment, + account_delta_commitment, + )?; + + if tx_summary.to_commitment() != message { + return Err(TransactionKernelError::TransactionSummaryConstructionFailed( + "transaction summary doesn't commit to the expected message".into(), + )); + } + + Ok(tx_summary) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Builds the note metadata from sender, note type and tag if all inputs are valid. +fn build_note_metadata( + sender: AccountId, + note_type: Felt, + tag: Felt, +) -> Result { + let note_type = u8::try_from(note_type) + .map_err(|_| TransactionKernelError::other("failed to decode note_type into u8")) + .and_then(|note_type_byte| { + NoteType::try_from(note_type_byte).map_err(|source| { + TransactionKernelError::other_with_source( + "failed to decode note_type from u8", + source, + ) + }) + })?; + + let tag = u32::try_from(tag) + .map_err(|_| TransactionKernelError::other("failed to decode note tag into u32")) + .map(NoteTag::new)?; + + Ok(NoteMetadata::new(sender, note_type, tag)) +} + +fn extract_note_attachment( + attachment_scheme: Felt, + attachment_kind: Felt, + attachment: Word, + note_ptr: Felt, + advice_provider: &AdviceProvider, +) -> Result<(usize, NoteAttachment), TransactionKernelError> { + let note_idx = note_ptr_to_idx(note_ptr)?; + + let attachment_kind = u8::try_from(attachment_kind) + .map_err(|_| TransactionKernelError::other("failed to convert attachment kind to u8")) + .and_then(|attachment_kind| { + NoteAttachmentKind::try_from(attachment_kind).map_err(|source| { + TransactionKernelError::other_with_source( + "failed to convert u8 to attachment kind", + source, + ) + }) + })?; + + let attachment_scheme = u32::try_from(attachment_scheme) + .map_err(|_| TransactionKernelError::other("failed to convert attachment scheme to u32")) + .map(NoteAttachmentScheme::new)?; + + let attachment_content = match attachment_kind { + NoteAttachmentKind::None => { + if !attachment.is_empty() { + return Err(TransactionKernelError::NoteAttachmentNoneIsNotEmpty); + } + NoteAttachmentContent::None + }, + NoteAttachmentKind::Word => NoteAttachmentContent::Word(attachment), + NoteAttachmentKind::Array => { + let elements = advice_provider.get_mapped_values(&attachment).ok_or_else(|| { + TransactionKernelError::other( + "elements of a note attachment commitment must be present in the advice provider", + ) + })?; + + let commitment_attachment = + NoteAttachmentArray::new(elements.to_vec()).map_err(|source| { + TransactionKernelError::other_with_source( + "failed to construct note attachment commitment", + source, + ) + })?; + + if commitment_attachment.commitment() != attachment { + return Err(TransactionKernelError::NoteAttachmentArrayMismatch { + actual: commitment_attachment.commitment(), + provided: attachment, + }); + } + + NoteAttachmentContent::Array(commitment_attachment) + }, + }; + + let attachment = + NoteAttachment::new(attachment_scheme, attachment_content).map_err(|source| { + TransactionKernelError::other_with_source("failed to extract note attachment", source) + })?; + + Ok((note_idx as usize, attachment)) +} + +/// Extracts a word from a slice of field elements. +#[inline(always)] +fn extract_word(commitments: &[Felt], start: usize) -> Word { + Word::from([ + commitments[start], + commitments[start + 1], + commitments[start + 2], + commitments[start + 3], + ]) +} + +/// Converts the provided note ptr into the corresponding note index. +fn note_ptr_to_idx(note_ptr: Felt) -> Result { + u32::try_from(note_ptr) + .map_err(|_| TransactionKernelError::other("failed to convert note_ptr to u32")) + .and_then(|note_ptr| { + note_ptr + .checked_sub(OUTPUT_NOTE_SECTION_OFFSET) + .ok_or_else(|| { + TransactionKernelError::other("failed to calculate note_idx from note_ptr") + }) + .map(|note_ptr| note_ptr / NOTE_MEM_SIZE) + }) +} diff --git a/crates/miden-tx/src/lib.rs b/crates/miden-tx/src/lib.rs index 8b3f225574..a756df72b7 100644 --- a/crates/miden-tx/src/lib.rs +++ b/crates/miden-tx/src/lib.rs @@ -48,4 +48,4 @@ pub mod auth; // RE-EXPORTS // ================================================================================================ -pub use miden_objects::utils; +pub use miden_protocol::utils; diff --git a/crates/miden-tx/src/prover/mast_store.rs b/crates/miden-tx/src/prover/mast_store.rs index 3ee9d7d5dc..e92984cc66 100644 --- a/crates/miden-tx/src/prover/mast_store.rs +++ b/crates/miden-tx/src/prover/mast_store.rs @@ -1,13 +1,13 @@ use alloc::collections::BTreeMap; use alloc::sync::Arc; -use miden_lib::transaction::TransactionKernel; -use miden_lib::{MidenLib, StdLibrary}; -use miden_objects::Word; -use miden_objects::account::AccountCode; -use miden_objects::assembly::mast::MastForest; -use miden_objects::utils::sync::RwLock; use miden_processor::MastForestStore; +use miden_protocol::account::AccountCode; +use miden_protocol::assembly::mast::MastForest; +use miden_protocol::transaction::TransactionKernel; +use miden_protocol::utils::sync::RwLock; +use miden_protocol::{CoreLibrary, ProtocolLib, Word}; +use miden_standards::StandardsLib; // TRANSACTION MAST STORE // ================================================================================================ @@ -28,9 +28,10 @@ impl TransactionMastStore { /// Returns a new [TransactionMastStore] instantiated with the default libraries. /// /// The default libraries include: - /// - Miden standard library (miden-stdlib). - /// - Miden protocol library (miden-lib). - /// - Transaction kernel. + /// - Miden core library [`CoreLibrary`]. + /// - Miden protocol library [`ProtocolLib`]. + /// - Miden standards library [`StandardsLib`]. + /// - Transaction kernel [`TransactionKernel::kernel`]. pub fn new() -> Self { let mast_forests = RwLock::new(BTreeMap::new()); let store = Self { mast_forests }; @@ -39,13 +40,17 @@ impl TransactionMastStore { let kernels_forest = TransactionKernel::kernel().mast_forest().clone(); store.insert(kernels_forest); - // load miden-stdlib MAST forest - let miden_stdlib_forest = StdLibrary::default().mast_forest().clone(); - store.insert(miden_stdlib_forest); + // load miden-core-lib MAST forest + let miden_core_lib_forest = CoreLibrary::default().mast_forest().clone(); + store.insert(miden_core_lib_forest); - // load miden lib MAST forest - let miden_lib_forest = MidenLib::default().mast_forest().clone(); - store.insert(miden_lib_forest); + // load protocol lib MAST forest + let protocol_lib_forest = ProtocolLib::default().mast_forest().clone(); + store.insert(protocol_lib_forest); + + // load standards lib MAST forest + let standards_lib_forest = StandardsLib::default().mast_forest().clone(); + store.insert(standards_lib_forest); store } diff --git a/crates/miden-tx/src/prover/mod.rs b/crates/miden-tx/src/prover/mod.rs index 29c3d5f576..60d13b4c52 100644 --- a/crates/miden-tx/src/prover/mod.rs +++ b/crates/miden-tx/src/prover/mod.rs @@ -1,18 +1,18 @@ use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::TransactionKernel; -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{AccountDelta, PartialAccount}; -use miden_objects::asset::Asset; -use miden_objects::block::BlockNumber; -use miden_objects::transaction::{ +use miden_protocol::account::delta::AccountUpdateDetails; +use miden_protocol::account::{AccountDelta, PartialAccount}; +use miden_protocol::asset::Asset; +use miden_protocol::block::BlockNumber; +use miden_protocol::transaction::{ InputNote, InputNotes, OutputNote, ProvenTransaction, ProvenTransactionBuilder, TransactionInputs, + TransactionKernel, TransactionOutputs, }; pub use miden_prover::ProvingOptions; @@ -99,8 +99,7 @@ impl LocalTransactionProver { tx_inputs: impl Into, ) -> Result { let tx_inputs = tx_inputs.into(); - let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs) - .map_err(TransactionProverError::ConflictingAdviceMapEntry)?; + let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs(&tx_inputs); self.mast_store.load_account_code(tx_inputs.account().code()); for account_code in tx_inputs.foreign_account_code() { @@ -114,8 +113,7 @@ impl LocalTransactionProver { let account_procedure_index_map = AccountProcedureIndexMap::new( tx_inputs.foreign_account_code().iter().chain([tx_inputs.account().code()]), - ) - .map_err(TransactionProverError::CreateAccountProcedureIndexMap)?; + ); let (partial_account, ref_block, _, input_notes, _) = tx_inputs.into_parts(); let mut host = TransactionProverHost::new( @@ -140,7 +138,7 @@ impl LocalTransactionProver { // Extract transaction outputs and process transaction data. // Note that the account delta does not contain the removed transaction fee, so it is the // "pre-fee" delta of the transaction. - let (pre_fee_account_delta, input_notes, output_notes, _tx_progress) = host.into_parts(); + let (pre_fee_account_delta, input_notes, output_notes) = host.into_parts(); let tx_outputs = TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes) .map_err(TransactionProverError::TransactionOutputConstructionFailed)?; @@ -170,7 +168,7 @@ impl Default for LocalTransactionProver { impl LocalTransactionProver { pub fn prove_dummy( &self, - executed_transaction: miden_objects::transaction::ExecutedTransaction, + executed_transaction: miden_protocol::transaction::ExecutedTransaction, ) -> Result { let (tx_inputs, tx_outputs, account_delta, _) = executed_transaction.into_parts(); diff --git a/crates/miden-tx/src/prover/prover_host.rs b/crates/miden-tx/src/prover/prover_host.rs index 6f948217eb..db00cdf2d0 100644 --- a/crates/miden-tx/src/prover/prover_host.rs +++ b/crates/miden-tx/src/prover/prover_host.rs @@ -1,12 +1,6 @@ use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::EventId; -use miden_objects::Word; -use miden_objects::account::{AccountDelta, PartialAccount}; -use miden_objects::assembly::debuginfo::Location; -use miden_objects::assembly::{SourceFile, SourceSpan}; -use miden_objects::transaction::{InputNote, InputNotes, OutputNote}; use miden_processor::{ AdviceMutation, BaseHost, @@ -16,15 +10,14 @@ use miden_processor::{ ProcessState, SyncHost, }; +use miden_protocol::Word; +use miden_protocol::account::{AccountDelta, PartialAccount}; +use miden_protocol::assembly::debuginfo::Location; +use miden_protocol::assembly::{SourceFile, SourceSpan}; +use miden_protocol::transaction::{InputNote, InputNotes, OutputNote}; -use crate::AccountProcedureIndexMap; -use crate::host::{ - ScriptMastForestStore, - TransactionBaseHost, - TransactionEventData, - TransactionEventHandling, - TransactionProgress, -}; +use crate::host::{RecipientData, ScriptMastForestStore, TransactionBaseHost, TransactionEvent}; +use crate::{AccountProcedureIndexMap, TransactionKernelError}; /// The transaction prover host is responsible for handling [`SyncHost`] requests made by the /// transaction kernel during proving. @@ -65,15 +58,8 @@ where // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns a reference to the `tx_progress` field of this transaction host. - pub fn tx_progress(&self) -> &TransactionProgress { - self.base_host.tx_progress() - } - - /// Consumes `self` and returns the account delta, output notes and transaction progress. - pub fn into_parts( - self, - ) -> (AccountDelta, InputNotes, Vec, TransactionProgress) { + /// Consumes `self` and returns the account delta, input and output notes. + pub fn into_parts(self) -> (AccountDelta, InputNotes, Vec) { self.base_host.into_parts() } } @@ -105,34 +91,107 @@ where } fn on_event(&mut self, process: &ProcessState) -> Result, EventError> { - let event_id = EventId::from_felt(process.get_stack_item(0)); - - match self.base_host.handle_event(process, event_id)? { - TransactionEventHandling::Unhandled(event_data) => { - // We match on the event_data here so that if a new - // variant is added to the enum, this fails compilation and we can adapt - // accordingly. - match event_data { - // The base host should have handled this event since the signature should be - // present in the advice map. - TransactionEventData::AuthRequest { .. } => { - Err(EventError::from("base host should have handled auth request event")) - }, - // Foreign account data and witnesses should be in the advice provider at - // proving time, so there is nothing to do. - TransactionEventData::ForeignAccount { .. } => Ok(Vec::new()), - TransactionEventData::AccountVaultAssetWitness { .. } => Ok(Vec::new()), - TransactionEventData::AccountStorageMapWitness { .. } => Ok(Vec::new()), - // Note scripts should be in the advice provider at proving time, so there is - // nothing to do. - TransactionEventData::NoteData { .. } => Ok(Vec::new()), - // We don't track enough information to handle this event. Since this just - // improves error messages for users and the error should not be relevant during - // proving, we ignore it. - TransactionEventData::TransactionFeeComputed { .. } => Ok(Vec::new()), + if let Some(advice_mutations) = self.base_host.handle_core_lib_events(process)? { + return Ok(advice_mutations); + } + + let tx_event = + TransactionEvent::extract(&self.base_host, process).map_err(EventError::from)?; + + // None means the event ID does not need to be handled. + let Some(tx_event) = tx_event else { + return Ok(Vec::new()); + }; + + let result = match tx_event { + // Foreign account data and witnesses should be in the advice provider at + // proving time, so there is nothing to do. + TransactionEvent::AccountBeforeForeignLoad { .. } => Ok(Vec::new()), + + TransactionEvent::AccountVaultAfterRemoveAsset { asset } => { + self.base_host.on_account_vault_after_remove_asset(asset) + }, + TransactionEvent::AccountVaultAfterAddAsset { asset } => { + self.base_host.on_account_vault_after_add_asset(asset) + }, + + TransactionEvent::AccountStorageAfterSetItem { slot_name, new_value } => { + self.base_host.on_account_storage_after_set_item(slot_name, new_value) + }, + + TransactionEvent::AccountStorageAfterSetMapItem { + slot_name, + key, + old_value, + new_value, + } => self + .base_host + .on_account_storage_after_set_map_item(slot_name, key, old_value, new_value), + + // Access witnesses should be in the advice provider at proving time. + TransactionEvent::AccountVaultBeforeAssetAccess { .. } => Ok(Vec::new()), + TransactionEvent::AccountStorageBeforeMapItemAccess { .. } => Ok(Vec::new()), + + TransactionEvent::AccountAfterIncrementNonce => { + self.base_host.on_account_after_increment_nonce() + }, + + TransactionEvent::AccountPushProcedureIndex { code_commitment, procedure_root } => { + self.base_host.on_account_push_procedure_index(code_commitment, procedure_root) + }, + + TransactionEvent::NoteBeforeCreated { note_idx, metadata, recipient_data } => { + match recipient_data { + RecipientData::Digest(recipient_digest) => self + .base_host + .output_note_from_recipient_digest(note_idx, metadata, recipient_digest), + RecipientData::Recipient(note_recipient) => self + .base_host + .output_note_from_recipient(note_idx, metadata, note_recipient), + RecipientData::ScriptMissing { .. } => Err(TransactionKernelError::other( + "note script should be in the advice provider at proving time", + )), } }, - TransactionEventHandling::Handled(mutations) => Ok(mutations), - } + + TransactionEvent::NoteBeforeAddAsset { note_idx, asset } => { + self.base_host.on_note_before_add_asset(note_idx, asset).map(|_| Vec::new()) + }, + + TransactionEvent::NoteBeforeSetAttachment { note_idx, attachment } => self + .base_host + .on_note_before_set_attachment(note_idx, attachment) + .map(|_| Vec::new()), + + TransactionEvent::AuthRequest { signature, .. } => { + if let Some(signature) = signature { + Ok(self.base_host.on_auth_requested(signature)) + } else { + Err(TransactionKernelError::other( + "signatures should be in the advice provider at proving time", + )) + } + }, + + TransactionEvent::Unauthorized { tx_summary } => { + Err(TransactionKernelError::other(format!( + "unexpected unauthorized event during proving with tx summary commitment {}", + tx_summary.to_commitment() + ))) + }, + + // We don't track enough information to handle this event. Since this just improves + // error messages for users and the error should not be relevant during proving, we + // ignore it. + TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount { .. } => Ok(Vec::new()), + + TransactionEvent::LinkMapSet { advice_mutation } => Ok(advice_mutation), + TransactionEvent::LinkMapGet { advice_mutation } => Ok(advice_mutation), + + // We do not track tx progress during proving. + TransactionEvent::Progress(_) => Ok(Vec::new()), + }; + + result.map_err(EventError::from) } } diff --git a/crates/miden-tx/src/verifier/mod.rs b/crates/miden-tx/src/verifier/mod.rs index 062b3f8ccc..9855151dec 100644 --- a/crates/miden-tx/src/verifier/mod.rs +++ b/crates/miden-tx/src/verifier/mod.rs @@ -1,7 +1,6 @@ -use miden_lib::StdLibrary; -use miden_lib::transaction::TransactionKernel; -use miden_objects::transaction::ProvenTransaction; -use miden_objects::vm::ProgramInfo; +use miden_protocol::CoreLibrary; +use miden_protocol::transaction::{ProvenTransaction, TransactionKernel}; +use miden_protocol::vm::ProgramInfo; use miden_verifier::verify_with_precompiles; use super::TransactionVerifierError; @@ -50,7 +49,7 @@ impl TransactionVerifier { ); // verify transaction proof - let precompile_verifiers = StdLibrary::default().verifier_registry(); + let precompile_verifiers = CoreLibrary::default().verifier_registry(); let proof_security_level = verify_with_precompiles( self.tx_program_info.clone(), stack_inputs, diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000000..3679142018 --- /dev/null +++ b/deny.toml @@ -0,0 +1,72 @@ +# cargo-deny configuration, see https://github.com/EmbarkStudios/cargo-deny + +# Graph configuration +[graph] +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu"] + +# Advisory database configuration +[advisories] +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +ignore = [ + "RUSTSEC-2024-0436", # paste is unmaintained but no alternative available + "RUSTSEC-2025-0055", # tracing-subscriber vulnerability - will be fixed by upgrade + "RUSTSEC-2025-0056", # adler is unmaintained but used by miniz_oxide +] +yanked = "warn" + +# License configuration +[licenses] +allow = [ + "Apache-2.0 WITH LLVM-exception", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MIT", + "Unicode-3.0", + "Zlib", +] +exceptions = [ + # Each entry is the crate and version constraint, and its specific allowed licenses + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Bans configuration +[bans] +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +deny = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +highlight = "all" +multiple-versions = "deny" +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +skip-tree = [ + # Allow getrandom v0.2.x - legacy version used by nanorand + { name = "getrandom", version = "=0.2.*" }, + # Allow rand_core v0.6.x - legacy version used by winterfell crates + { name = "rand_core", version = "=0.6.*" }, + # Allow rustc_version v0.2.x - build dependency version + { name = "rustc_version", version = "=0.2.*" }, + # Allow unicode-width v0.1.x - used by miden-formatting vs textwrap conflict + { name = "unicode-width", version = "=0.1.*" }, + # Allow windows-targets v0.48.x - older Windows target version + { name = "windows-targets", version = "=0.48.*" }, + # Allow windows-sys v0.48.x/v0.59.x - multiple Windows system libraries + { name = "windows-sys", version = "=0.48.*" }, + { name = "windows-sys", version = "=0.59.*" }, + # Allow syn v1.x and v2.x - our derive macros need v1.x while ecosystem uses v2.x + { name = "syn", version = "=1.0.109" }, + { name = "syn", version = "=2.0.111" }, +] +wildcards = "allow" + +# Sources configuration +[sources] +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +unknown-git = "deny" +unknown-registry = "deny" diff --git a/docs/src/account/address.md b/docs/src/account/address.md index 1287ffcedf..fe979e730e 100644 --- a/docs/src/account/address.md +++ b/docs/src/account/address.md @@ -17,11 +17,11 @@ The receiver can choose to disclose various pieces of information that control h Consider a few examples that use different address mechanisms: - The [Pay-to-ID note](../note#p2id-pay-to-id): the note itself can only be consumed if the account ID encoded in the note details matches the ID of the account that tries to consume it. To receive a P2ID note, the receiver should communicate an `AddressId::AccountId` type to the sender. -- A "Pay-to-PoW" note that can only be consumed if the receiver can provide a valid seed such that the hash of the seed results in a value with n leading zero bits. The receiver communicates an `AddressId::PoW` type to the sender, which encodes the target number of leading zero bits (and a salt to avoid re-use of the same seed).* -- A "Pay-to-Public-Key" note that stores a public (signature) key and checks if the receiver can provide a valid cryptographic signature for that key. The `AddressId::PublicKey` type must encode the public key.* +- A "Pay-to-PoW" note that can only be consumed if the receiver can provide a valid seed such that the hash of the seed results in a value with n leading zero bits. The receiver communicates an `AddressId::PoW` type to the sender, which encodes the target number of leading zero bits (and a salt to avoid re-use of the same seed). +- A "Pay-to-Public-Key" note that stores a public (signature) key and checks if the receiver can provide a valid cryptographic signature for that key. The `AddressId::PublicKey` type must encode the public key. These different address mechanisms provide different levels of privacy and security: -- `AddressId::AccountId`: the receiver is uniquely identifiable, but they are the only ones who can consume the note. +- `AddressId::AccountId`: the receiver is uniquely identifiable, but they are the only one who can consume the note. - `AddressId::PoW`: the receiver is not revealed publicly, but potentially many entities can consume the note. The receiver has an advantage by specifying the salt. - `AddressId::PublicKey`: the receiver `AccountId` is not revealed publicly, only their public key. A fresh `AddressId::PublicKey` can be used for receiving each note, resulting in increased privacy. @@ -35,13 +35,13 @@ For notes which are sent privately, the sender needs to communicate the full not Instead, our Miden client connects to a _Note Transport Layer_, which stores encrypted note details together with the associated public metadata for each note. The receiver can query the Note Transport Layer for `NoteTag`s they are interested in. Typically, a `NoteTag` encodes a few leading bits (14 by default) of the receiver's `AccountId`. Querying the Note Transport Layer for 14-bit `NoteTag`s reduces the receiver's privacy, but at the same time allows them to perform less work downloading and trial-decrypting the notes than if fewer bits were encoded. -With an `Address`, e.g. the [`AddressId::AccountId`](./address#addressaccountid) variant, the receiver could specify how many bits of their `AccountId` they want to disclose to the sender and thus choose their level of privacy. +With an `Address`, e.g. the [`AddressId::AccountId`](./address#address-types) variant, the receiver could specify how many bits of their `AccountId` they want to disclose to the sender and thus choose their level of privacy. ### Account interface discovery An address allows the sender of the note to easily discover the interface of the receiving account. As explained in the [account interface](./code#interface) section, every account can have a different set of procedures that note scripts can call, which is the _interface_ of the account. In order for the sender of a note to create a note that the receiver can consume, the sender needs to know the interface of the receiving account. This can be communicated via the address, which encodes a mapping of standard interfaces like the basic wallet. -If a sender wants to create a note, it is up to them to check whether the receiver account has an interface that it compatible with that note. The notion of an address doesn't exist at protocol level and so it is up to wallets or clients to implement this interface compatibility check. +If a sender wants to create a note, it is up to them to check whether the receiver account has an interface that is compatible with that note. The notion of an address doesn't exist at protocol level and so it is up to wallets or clients to implement this interface compatibility check. ### Note encryption diff --git a/docs/src/account/code.md b/docs/src/account/code.md index 2f5eedf9e5..ed6d60ab1e 100644 --- a/docs/src/account/code.md +++ b/docs/src/account/code.md @@ -17,7 +17,7 @@ Every Miden `Account` is essentially a smart contract. The `Code` defines the ac ## Interface -An account's code is typically the result of merging multiple [account components](./components). This results in a set of procedures that make up the _interface_ of the account. As an example, a typical wallet uses the so-called _basic wallet_ interface, which is defined in `miden::contracts::wallets::basic`. It consists of the `receive_asset` and `move_asset_to_note` procedures. If an account has this interface, i.e. this set of procedures, it can consume standard [P2ID notes](../note#p2id-pay-to-id). If it doesn't, it can't consume this type of note. So, adhering to standard interfaces such as the basic wallet will generally make an account more interoperable. +An account's code is typically the result of merging multiple [account components](./components). This results in a set of procedures that make up the _interface_ of the account. As an example, a typical wallet uses the so-called _basic wallet_ interface, which is defined in `miden::standards::wallets::basic`. It consists of the `receive_asset` and `move_asset_to_note` procedures. If an account has this interface, i.e. this set of procedures, it can consume standard [P2ID notes](../note#p2id-pay-to-id). If it doesn't, it can't consume this type of note. So, adhering to standard interfaces such as the basic wallet will generally make an account more interoperable. ## Authentication @@ -35,6 +35,6 @@ Such an authentication procedure typically inspects the transaction and then dec Recall that an [account's nonce](index.md#nonce) must be incremented whenever its state changes. Only authentication procedures are allowed to do so, to prevent accidental or unintended authorization of state changes. -### Procedure tracking +### Procedure invocation checks -The authentication procedure can base its authentication decision on whether a specific account procedure was called during the transaction (procedure tracking). A procedure is considered "tracked" 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`). Procedures that execute only local instructions (e.g., a noop `push.0 drop`) will not be marked as tracked. +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. diff --git a/docs/src/account/components.md b/docs/src/account/components.md index b304910423..300524086f 100644 --- a/docs/src/account/components.md +++ b/docs/src/account/components.md @@ -7,27 +7,30 @@ title: "Components" Account components are reusable units of functionality that define a part of an account's code and storage. Multiple account components can be merged together to form an account's final [code](./code) and [storage](./storage). -As an example, consider a typical wallet account, capable of holding a user's assets and requiring authentication whenever assets are added or removed. Such an account can be created by merging a `BasicWallet` component with an `RpoFalcon512` authentication component. The basic wallet does not need any storage, but contains the code to move assets in and out of the account vault. The authentication component holds a user's public key in storage and additionally contains the code to verify a signature against that public key. Together, these components form a fully functional wallet account. +As an example, consider a typical wallet account, capable of holding a user's assets and requiring authentication whenever assets are added or removed. Such an account can be created by merging a `BasicWallet` component with an `Falcon512Rpo` authentication component. The basic wallet does not need any storage, but contains the code to move assets in and out of the account vault. The authentication component holds a user's public key in storage and additionally contains the code to verify a signature against that public key. Together, these components form a fully functional wallet account. -## Account Component templates +## Account Component schemas -An account component template is a description of an account component and contains all the -information needed to initialize it. +An account component schema describes a reusable piece of account functionality and captures +everything required to initialize it. The schema encapsulates the component's **metadata**, its +code, and how its storage should be laid out and typed. -Specifically, a template specifies a component's **metadata** and its **code**. - -Once defined, a component template can be instantiated to an account component, which can then be +Once defined, a component schema can be instantiated to an account component. Multiple components can then be merged to form the account's `Code` and `Storage`. ## Component code -The template's code defines a library of functions that can read and write to the storage slots of the component. +The component's code defines a library of functions that can perform arbitrary computations, as well as read and write to account storage. ## Component metadata The component metadata describes the account component entirely: its name, description, version, and storage layout. -The storage layout must specify a contiguous list of slot values that starts at index `0`, and can optionally specify initial values for each of the slots. Alternatively, placeholders can be utilized to identify values that should be provided at the moment of instantiation. +The storage layout is described as a set of named storage slots. Each slot name must be a valid `StorageSlotName`, and its slot ID is +derived deterministically from the name. + +Each slot has a type and an optional default value. If there is no default value defined as part of the schema, the value needs to be +provided at instantiation time. Default values can also be overridden ### TOML specification @@ -35,154 +38,170 @@ The component metadata can be defined using TOML. Below is an example specificat ```toml name = "Fungible Faucet" -description = "This component showcases the component template format, and the different ways of -providing valid values to it." +description = "This component showcases the component schema format, and the different ways of providing valid values to it." version = "1.0.0" supported-types = ["FungibleFaucet"] -[[storage]] -name = "token_metadata" -description = "Contains metadata about the token associated to the faucet account. The metadata -is formed by three fields: max supply, the token symbol and the asset's decimals" -slot = 0 -value = [ - { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, - { type = "token_symbol", value = "TST" }, - { type = "u8", name = "decimals", description = "Number of decimal places for converting to absolute units", value = "10" }, - { value = "0x0" } +[[storage.slots]] +name = "demo::token_metadata" +description = "Contains token metadata (max supply, symbol, decimals)." +type = [ + { type = "u32", name = "max_supply", description = "Maximum supply of the token in base units" }, + { type = "miden::standards::fungible_faucets::metadata::token_symbol", name = "symbol", description = "Token symbol", default-value = "TST" }, + { type = "u8", name = "decimals", description = "Number of decimal places for converting to absolute units" }, + { type = "void" } ] -[[storage]] -name = "owner_public_key" -description = "This is a value placeholder that will be interpreted as a Falcon public key" -slot = 1 -type = "auth::rpo_falcon512::pub_key" - -[[storage]] -name = "map_storage_entry" -slot = 2 -values = [ - { key = "0x1", value = ["0x0", "249381274", "998123581", "124991023478"] }, - { - key = "0xDE0B1140012A9FD912F18AD9EC85E40F4CB697AE", - value = { - name = "value_placeholder", - description = "This value will be defined at the moment of instantiation" - } - } +[[storage.slots]] +name = "demo::owner_public_key" +description = "This is a typed value supplied at instantiation and interpreted as a Falcon public key" +type = "miden::standards::auth::falcon512_rpo::pub_key" + +[[storage.slots]] +name = "demo::protocol_version" +description = "A whole-word init-supplied value typed as a felt (stored as [0,0,0,])." +type = "u8" + +[[storage.slots]] +name = "demo::static_map" +description = "A map slot with statically defined entries" +type = { key = "word", value = "word" } +default-values = [ + { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = ["0x0", "249381274", "998123581", "124991023478"] }, + { key = ["0", "0", "0", "2"], value = "0x0000000000000000000000000000000000000000000000000000000000000010" } ] -[[storage]] -name = "procedure_thresholds" +[[storage.slots]] +name = "demo::procedure_thresholds" description = "Map which stores procedure thresholds (PROC_ROOT -> signature threshold)" -slot = 3 -type = "map" - -[[storage]] -name = "multislot_entry" -slots = [4,5] -values = [ - ["0x1","0x2","0x3","0x4"], - ["50000","60000","70000","80000"] -] +type = { key = "word", value = "u16" } ``` -#### Specifying values and their types +#### Header + +The metadata header specifies four fields: -In the TOML format, any value that is one word long can be written as a single value, or as exactly four field elements. In turn, a field element is a number within Miden's finite field. +- `name`: The component schema's name +- `description` (optional): A brief description of the component schema and its functionality +- `version`: A semantic version of this component schema +- `supported-types`: Specifies the types of accounts on which the component can be used. Valid values are `FungibleFaucet`, `NonFungibleFaucet`, `RegularAccountUpdatableCode` and `RegularAccountImmutableCode` -A word can be written as a hexadecimal value, and field elements can be written either as hexadecimal or decimal numbers. In all cases, numbers should be input as strings. +#### Storage entries -In our example, the `token_metadata` single-slot entry is defined as four elements, where the first element is a placeholder, and the second, third and fourth are hardcoded values. +An account component schema can contain multiple storage entries, each describing either a +**single-slot value** or a **storage map**. -##### Word types +In TOML, these are declared using dotted array keys: -Valid word types are `word` (default type) and `auth::rpo_falcon512::pub_key` (represents a Falcon public key). Both can be written and interpreted as hexadecimal strings. +- **Value slots**: `[[storage.slots]]` with `type = "..."` or `type = [ ... ]` +- **Map slots**: `[[storage.slots]]` with `type = { ... }` -##### Felt types +**Value-slot** entries describe their schema via `WordSchema`. A value type can be either: -Valid field element types are `u8`, `u16`, `u32`, `felt` (default type) and `token_symbol`: +- **Simple**: defined through the `type = ""` field, indicating the expected `SchemaTypeId` for the entire word. The value is supplied at instantiation time via `InitStorageData`. Felt types are stored as full words in the following layout: `[0, 0, 0, ]`. +- **Composite**: provided through `type = [ ... ]`, which contains exactly four `FeltSchema` descriptors. Each element is either a named typed field (optionally with `default-value`) or a `void` element for reserved/padding zeros. -- `u8`, `u16` and `u32` values can be parsed as decimal numbers and represent 8-bit, 16-bit and 32-bit unsigned integers -- `felt` values represent a field element, and can be parsed as decimal or hexadecimal values -- `token_symbol` values represent the symbol for basic fungible tokens, and are parsed as strings made of four uppercase characters +Composite schema entries reuse the existing TOML structure for four-element words, while simple schemas rely on `type`. In our example, the `token_metadata` slot uses a composite schema (`type = [...]`) mixing typed fields (`max_supply`, `decimals`) with defaults (`symbol`) and a reserved/padding `void` element. -#### Header +Every entry carries: -The metadata header specifies four fields: +- `name`: Identifies the storage entry. +- `description` (optional): Explains the entry's purpose within the component. -- `name`: The component template's name -- `description` (optional): A brief description of the component template and its functionality -- `version`: A semantic version of this component template -- `supported-types`: Specifies the types of accounts on which the component can be used. Valid values are `FungibleFaucet`, `NonFungibleFaucet`, `RegularAccountUpdatableCode` and `RegularAccountImmutableCode` +The remaining fields depend on whether the entry is a value slot or a map slot, as inferred by the +shape of the `type` field. -#### Storage entries +##### Word types + +Simple schemas accept `word` (default) and word-shaped types such as `miden::standards::auth::falcon512_rpo::pub_key` or `miden::standards::auth::ecdsa_k256_keccak::pub_key` (parsed from hexadecimal strings). -An account component template can have multiple storage entries. A storage entry can specify either a **single-slot value**, a **multi-slot value**, or a **storage map**. +Simple schemas can also use any felt type (e.g. `u8`, `u16`, `u32`, `felt`, `miden::standards::fungible_faucets::metadata::token_symbol`, `void`). The value is parsed as a felt and stored as a word with the parsed felt in the last element and the remaining elements set to `0`. -Each of these storage entries contain the following fields: +##### Word schema example + +```toml +[[storage.slots]] +name = "demo::faucet_id" +description = "Account ID of the registered faucet" +type = [ + { type = "felt", name = "prefix", description = "Faucet ID prefix" }, + { type = "felt", name = "suffix", description = "Faucet ID suffix" }, + { type = "void" }, + { type = "void" }, +] +``` -- `name`: A name for identifying the storage entry -- `description` (optional): Describes the intended function of the storage slot within the component definition +##### Felt types -Additionally, based on the type of the storage entry, there are specific fields that should be specified. +Valid field element types are `void`, `u8`, `u16`, `u32`, `felt` (default) and `miden::standards::fungible_faucets::metadata::token_symbol`: -##### Single-slot value +- `void` is a special type which always evaluates to `0` and does not produce an init requirement; it is intended for reserved or padding elements. +- `u8`, `u16` and `u32` values can be parsed as decimal numbers and represent 8-bit, 16-bit and 32-bit unsigned integers. +- `felt` values represent a field element, and can be parsed as decimal or hexadecimal numbers. +- `miden::standards::fungible_faucets::metadata::token_symbol` values represent basic fungible token symbols, parsed as 1–6 uppercase ASCII characters. -A single-slot value fits within one slot (i.e., one word). +##### Value slots -For a single-slot entry, the following fields are expected: +Single-slot entries are represented by `ValueSlotSchema` and occupy one slot (one word). They use the fields: -- `slot`: Specifies the slot index in which the value will be placed -- `value` (optional): Contains the initial storage value for this slot. Will be interpreted as a `word` unless another `type` is specified -- `type` (optional): Describes the expected type for the slot +- `type` (required): Describes the schema for this slot. It can be either: + - a string type identifier (simple init-supplied slot), or + - an array of 4 felt schema descriptors (composite slot schema). +- `default-value` (optional): An overridable default for simple slots. If omitted, the slot is required at instantiation (unless `type = "void"`). -If no `value` is provided, the entry acts as a placeholder, requiring a value to be passed at instantiation. In this case, specifying a `type` is mandatory to ensure the input is correctly parsed. So the rule is that at least one of `value` and `type` has to be specified. -Valid types for a single-slot value are `word` or `auth::rpo_falcon512::pub_key`. +In our TOML example, the first entry defines a composite schema, while the second is an init-supplied value typed as `miden::standards::auth::falcon512_rpo::pub_key`. -In the above example, the first and second storage entries are single-slot values. +##### Storage map slots -##### Storage map entries +[Storage maps](./storage#map-slots) use `MapSlotSchema` and describe key-value pairs where each key and value is itself a `WordSchema`. Map slots support: -[Storage maps](./storage#map-slots) consist of key-value pairs, where both keys and values are single words. +- `type` (required): Declares the slot as a map via a map type table (`type = { ... }`), with: + - `type.key` (required): Declares the schema/type of keys stored in the map. + - `type.value` (required): Declares the schema/type of values stored in the map. +- `default-values` (optional): Lists default map entries defined by nested `key` and `value` descriptors. Each entry must be fully specified and cannot contain typed fields. -Storage map entries can specify the following fields: +`type.key` / `type.value` accept either a string type identifier (e.g. `"word"`) or a 4-element array of felt schema descriptors. -- `slot`: Specifies the slot index in which the root of the map will be placed -- `values` (optional): Contains a list of map entries, defined by a `key` and `value`. Each entry is - interpreted as a word, and keys or values may themselves be expressed via placeholders. -- `type = "map"` (optional): When provided without `values`, the entry is treated as a templated map - whose contents must be provided at instantiation time through [`InitStorageData`](#initializing-placeholder-values). - If `values` are present, the entry is interpreted as a static map regardless of the `type` field, so - specifying `type = "map"` becomes purely descriptive in that case. +If `default-values` is omitted, the map is populated at instantiation via [`InitStorageData`](#providing-init-values). When `default-values` are present, they act as defaults: init data can override existing values and optionally add new key-value pairs. -In the example, the third storage entry defines a static storage map with two initial entries, while -the fourth entry (`procedure_thresholds`) is a templated map whose contents are supplied at -instantiation time. +In the example, the third storage entry defines a static map and the fourth entry (`procedure_thresholds`) is populated at instantiation. -##### Multi-slot value +##### Typed map -Multi-slot values are composite values that exceed the size of a single slot (i.e., more than one `word`). +You can type maps at the slot level via `type.key` and `type.value` (each a `WordSchema`): -For multi-slot values, the following fields are expected: +```toml +[[storage.slots]] +name = "demo::typed_map" +type = { key = "word", value = "miden::standards::auth::falcon512_rpo::pub_key" } +``` + +This declares that all keys are `word` and all values are `miden::standards::auth::falcon512_rpo::pub_key`, regardless of whether the map contents come from `default-values = [...]` (static) or are supplied at instantiation via `InitStorageData`. + +`type.key` / `type.value` are validated when building map entries from `InitStorageData` (and when validating `default-values`). -- `slots`: Specifies the list of contiguous slots that the value comprises -- `values`: Contains the initial storage value for the specified slots +##### Multi-slot value -Placeholders can currently not be defined for multi-slot values. In our example, the fifth entry defines a two-slot value. +Multi-slot values are currently unsupported by component schemas. -#### Initializing placeholder values +#### Providing init values -When a storage entry introduces placeholders, an implementation must provide their concrete values -at instantiation time. This is done through `InitStorageData` (available as `miden_objects::account::InitStorageData`), which can be created programmatically or loaded from TOML using `InitStorageData::from_toml()`. +When a storage entry requires init-supplied values, an implementation must provide their concrete values at instantiation time. This is done through `InitStorageData` (available as `miden_protocol::account::component::InitStorageData`), which can be created programmatically or loaded from TOML using `InitStorageData::from_toml()`. -For example, the templated map entry above can be populated from TOML as follows: +For example, the init-populated map entry above can be populated from TOML as follows: ```toml -procedure_thresholds = [ +"demo::owner_public_key" = "0x1234" +"demo::protocol_version" = "1" + +["demo::token_metadata"] +max_supply = "1000000000" +decimals = "10" + +"demo::procedure_thresholds" = [ { key = "0xd2d1b6229d7cfb9f2ada31c5cb61453cf464f91828e124437c708eec55b9cd07", - value = "0x00000000000000000000000000000000000000000000000000000000000001" + value = ["0", "0", "0", "1"] }, { key = "0x2217cd9963f742fc2d131d86df08f8a2766ed17b73f1519b8d3143ad1c71d32d", @@ -191,4 +210,6 @@ procedure_thresholds = [ ] ``` -Each element in the array is a fully specified key/value pair. Keys and values can be written either as hexadecimal words or as an array of four field elements (decimal or hexadecimal strings). This syntax complements the existing `values = [...]` form used for static maps, and mirrors how map entries are provided in component metadata. +All init values must be provided as TOML strings (including numeric values), and are parsed/validated against the schema at instantiation time. + +Each element in the array is a fully specified key/value pair. Note that slot names include `::`, so they must be quoted in TOML. This syntax complements the existing `default-values = [...]` form used for static maps, and mirrors how map entries are provided in component metadata. If an init-populated map slot is omitted from `InitStorageData`, it defaults to an empty map. diff --git a/docs/src/account/storage.md b/docs/src/account/storage.md index 06146ef588..622a8f54bd 100644 --- a/docs/src/account/storage.md +++ b/docs/src/account/storage.md @@ -9,18 +9,56 @@ title: "Storage" A flexible, arbitrary data store within the `Account`. ::: -The [storage](https://docs.rs/miden-objects/latest/miden_objects/account/struct.AccountStorage.html) is divided into a maximum of 255 indexed [storage slots](https://docs.rs/miden-objects/latest/miden_objects/account/enum.StorageSlot.html). Each slot can either store a 32-byte value or serve as the cryptographic root to a key-value store with the capacity to store large amounts of data. +The [storage](https://docs.rs/miden-protocol/latest/miden_protocol/account/struct.AccountStorage.html) consists of up to 256 individual [storage slots](https://docs.rs/miden-protocol/latest/miden_protocol/account/enum.StorageSlot.html), where each slot consists of: +- Key: Slot name. +- Value: either a single [`Word`](#storage-units) or a key-value map of [`Word`](#storage-units)s. -- **Value slots:** Contains 32 bytes of arbitrary data. -- **Map slots:** Contains a [StorageMap](#map-slots), a key-value store where both keys and values are 32 bytes. The slot's value is a commitment to the entire map. +So, account storage can be thought of as a map from slot name to slot value. + +#### Storage units + +The basic storage unit in account storage is a `Word`. A `Word` consists of 4 elements where each element is slightly less than a 64-bit value. Specifically, a maximum value of an element is $2^{64} - 2^{32}$. Thus, a single `Word` can contain almost 32 bytes of data. + +Since a `Word` cannot store exactly 32 bytes of data, we may need to use multiple words to store such binary data as Keccak or SHA256 hashes. At the minimum, a 32-byte hash would require 5 elements to store, but it is recommended to encode such hashes into 8 elements with each element containing 32 bits of data. In both cases, two words would be used to store the hash. + +## Slot Name + +Each slot has a name associated with it that uniquely identifies it in the account's storage. + +One example for a slot name is: + +```text +miden::standards::fungible_faucets::metadata +``` + +Slot names are intended to be _globally unique_. This means `miden::standards::fungible_faucets::metadata` should always identify a slot that contains the standardized metadata for a fungible faucet. [Account components](components.md) that define their own storage slots should therefore choose names that avoid collisions. + +To reduce the chance of slot name collisions, it is recommended that slot names have at least three components separated by `::`. A recommended pattern is: + +```text +project_name::component_name::slot_name +``` + +### Slot ID + +Because slot names are too large to be used directly in the transaction kernel, a _slot ID_ is used instead. The slot ID is derived from the hash of the slot name. Miden Assembly APIs will always work with the slot ID instead of the slot name. + +## Slot types + +Each slot has one of the following types: + +- **Value slot:** Contains a single `Word` of arbitrary data. +- **Map slot:** Contains a [StorageMap](#map-slots), a key-value store where both keys and values are `Word`s. The slot's value is set to the root of the map. An account's storage is typically the result of merging multiple [account components](./components). -## Value Slots +### Value Slots + +A value slot can be used whenever a single `Word` (almost 32 bytes) of data is enough, e.g. for storing a single public key commitment for use in [authentication procedures](code#authentication). -A value slot can be used whenever 32 bytes of data is enough, e.g. for storing a single public key for use in [authentication procedures](code#authentication). +Value slots can be used with `set_item`, `get_item` and `get_initial_item` APIs. -## Map Slots +### Map Slots A map slot contains a `StorageMap` which is a key-value store implemented as a sparse Merkle tree (SMT). This allows an account to store a much larger amount of data than would be possible using only the account's storage slots. The root of the underlying SMT is stored in a single account storage slot, and each map entry is a leaf in the tree. When retrieving an entry (e.g., via `active_account::get_map_item`), its inclusion is proven using a Merkle proof. @@ -31,3 +69,7 @@ Key properties of `StorageMap`: - **Key hashing:** Since map keys are user-chosen and may not be uniformly distributed, keys are hashed before being inserted into the SMT. This ensures a more balanced tree and mitigates efficiency issues due to key clustering. The original keys are retained in a separate map, allowing for introspection (e.g., querying the set of stored original keys for debugging or explorer scenarios). This introduces some redundancy, but enables useful features such as listing all stored keys. This design allows for flexible, scalable, and privacy-preserving storage within accounts, supporting both large datasets and efficient proof generation. + +Map slots can be used with `set_map_item`, `get_map_item` and `get_initial_map_item` APIs. + +Additionally, `get_item` and `get_initial_item` can also be used to access the root of the storage map. diff --git a/docs/src/index.md b/docs/src/index.md index 947d63c334..bf7c554b86 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -41,7 +41,7 @@ An [Asset](asset) can be fungible and non-fungible. They are stored in the owner ### Transactions -A [Transactions](transaction) describe the production and consumption of notes by a single account. +A [Transaction](transaction) describes the production and consumption of notes by a single account. Executing a transaction always results in a STARK proof. diff --git a/docs/src/note.md b/docs/src/note.md index 785133e78e..b9ee08d714 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -61,10 +61,28 @@ The serial number has two main purposes. Firstly by adding some randomness to th ### Metadata :::note -Additional `Note` information. +Additional public `Note` information. ::: -Notes include metadata such as the sender’s account ID and a [tag](#note-discovery) that aids in discovery. Regardless of [storage mode](#note-storage-mode), these metadata fields remain public. +Every note includes metadata: +- the account ID of the sender, i.e. the creator of the note. +- its note type, i.e. private or public. +- the [note tag](#note-discovery) that aids in discovery of the note. +- an optional [note attachment](#attachment). + +Regardless of [storage mode](#note-storage-mode), these metadata fields are always public. + +### Attachment + +An attachment is a variable-size part of a note's metadata: +- It can either be absent (`None`), store a single `Word` or an `Array` of field elements. These are the three _kinds_ of attachments. +- The _scheme_ of an attachment is an optional, 32-bit user-defined value that can be used to detect the presence of certain standardized attachments. + +Example use cases for attachments are: +- Communicate the note details of a private note in encrypted form. This means the encrypted note is attached publicly to the otherwise private note. +- For [network transactions](./transaction.md#network-transaction), encode the ID of the network account that should + consume the note. This is a standardized attachment scheme in miden-standards called `NetworkAccountTarget`. +- Communicate the details of a _private_ note to the receiver so they can derive the note. For example, the payback note of a partially fillable swap note can be private and the receiver already knows a few details: It is a P2ID note, the serial number is derived from the SWAP note's serial number and the note inputs are the account ID of the receiver. The receiver only needs to now the exact amount that was filled to derive the full note for consumption. This amount can be encoded in the public attachment of the payback note, which allows this use case to work with private notes and still not require a side-channel. ## Note Lifecycle @@ -98,7 +116,26 @@ After validation notes become "live" and eligible for consumption. If creation a Clients often need to find specific notes of interest. Miden allows clients to query the `Note` database using `Note` tags. These lightweight, 32-bit data fields serve as best-effort filters, enabling quick lookups for notes related to particular use cases, scripts, or account prefixes. -Using `Note` tags strikes a balance between privacy and efficiency. Without tags, querying a specific `Note` ID reveals a user’s interest to the operator. Conversely, downloading and filtering all registered notes locally is highly inefficient. Tags allow users to adjust their level of privacy by choosing how broadly or narrowly they define their search criteria, letting them find the right balance between revealing too much information and incurring excessive computational overhead. +While note tags can be arbitrarily constructed from 32 bits of data, there are two categories of tags that many notes fit into. + +#### Account Targets + +A note targeted at an account is a note that is intended or even enforced to be consumed by a specific account. One example is a P2ID note that can only be consumed by a specific account ID. The tag for such a note should make it easy for the receiver to find the note. Therefore, the tag encodes a certain number of bits of the receiver account's ID, by convention. Notably, it may not encode the full 32 bits of the target account's ID to preserve the receiver's privacy. See also the section on privacy below. + +#### Use Case Tags + +Use case notes are notes that are not intended to be consumed by a specific account, but by anyone willing to fulfill the note's contract. One example is a SWAP note that trades one asset against another. Such a use case note can define the structure of their note tags. A sensible structure for a SWAP note could be: +- encoding the 2 bits of the note's type. +- encoding the note script root, i.e. making it identifiable as a SWAP note, for example by + using 16 bits of the SWAP script root. +- encoding the SWAP pair, for example by using 8 bits of the offered asset faucet ID and 8 bits + of the requested asset faucet ID. + +This allows clients to search for a public SWAP note that trades USDC against ETH only through the note tag. Since tags are not validated in any way and only act as best-effort filters, further local filtering is almost always necessary. For example, there could easily be a collision on the 8 bits used in SWAP tag's faucet IDs. + +#### Privacy vs Efficiency + +Using `Note` tags strikes a balance between privacy and efficiency. Without tags, querying a specific `Note` ID reveals a user's interest to the operator. Conversely, downloading and filtering all registered notes locally is highly inefficient. Tags allow users to adjust their level of privacy by choosing how broadly or narrowly they define their search criteria, letting them find the right balance between revealing too much information and incurring excessive computational overhead. ### Note consumption @@ -121,7 +158,7 @@ Only those who know the RECIPIENT’s pre-image can consume the `Note`. For priv The [transaction prologue](transaction) requires all necessary data to compute the `Note` hash. This setup allows scenario-specific restrictions on who may consume a `Note`. -For a practical example, refer to the [SWAP note script](https://github.com/0xMiden/miden-base/blob/next/crates/miden-lib/asm/note_scripts/SWAP.masm), where the RECIPIENT ensures that only a defined target can consume the swapped asset. +For a practical example, refer to the [SWAP note script](https://github.com/0xMiden/miden-base/blob/next/crates/miden-standards/asm/standards/notes/swap.masm), where the RECIPIENT ensures that only a defined target can consume the swapped asset. #### Note nullifier ensuring private consumption @@ -156,7 +193,7 @@ The P2ID note script implements a simple pay-to-account-ID pattern. It adds all - **Purpose:** Direct asset transfer to a specific account ID - **Inputs:** Requires exactly 2 note inputs containing the target account ID - **Validation:** Ensures the consuming account's ID matches the target account ID specified in the note -- **Requirements:** Target account must expose the `miden::contracts::wallets::basic::receive_asset` procedure +- **Requirements:** Target account must expose the `miden::standards::wallets::basic::receive_asset` procedure **Use case:** Simple, direct payments where you want to send assets to a known account ID. @@ -174,7 +211,7 @@ The P2IDE note script extends P2ID with additional features including time-locki - **Time-lock:** Note cannot be consumed until the specified block height is reached - **Reclaim:** Original sender can reclaim the note after the reclaim block height if not consumed by target - **Validation:** Complex logic to handle both target consumption and sender reclaim scenarios -- **Requirements:** Account must expose the `miden::contracts::wallets::basic::receive_asset` procedure +- **Requirements:** Account must expose the `miden::standards::wallets::basic::receive_asset` procedure **Use cases:** @@ -189,17 +226,17 @@ The SWAP note script implements atomic asset swapping functionality. **Key characteristics:** - **Purpose:** Atomic asset exchange between two parties -- **Inputs:** Requires exactly 12 note inputs specifying: +- **Inputs:** Requires exactly 16 note inputs specifying: - Requested asset details - Payback note recipient information - - Note creation parameters (execution hint, type, aux data, tag) + - Note creation parameters (type, tag, attachment) - **Assets:** Must contain exactly 1 asset to be swapped - **Mechanism:** 1. Creates a payback note containing the requested asset for the original note issuer 2. Adds the note's asset to the consuming account's vault - **Requirements:** Account must expose both: - - `miden::contracts::wallets::basic::receive_asset` procedure - - `miden::contracts::wallets::basic::move_asset_to_note` procedure + - `miden::standards::wallets::basic::receive_asset` procedure + - `miden::standards::wallets::basic::move_asset_to_note` procedure **Use case:** Decentralized asset trading where two parties want to exchange different assets atomically. diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index aa49c87157..d9cef7f02e 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -27,7 +27,7 @@ Most procedures in the Miden protocol library are implemented as wrappers around The procedures maintain the same security and context restrictions as the underlying kernel procedures. When invoking these procedures, ensure that the calling context matches the requirements. -## Active account Procedures (`miden::active_account`) +## Active account Procedures (`miden::protocol::active_account`) Active account procedures can be used to read from storage, fetch or compute commitments or obtain other internal data of the active account. @@ -40,10 +40,10 @@ Active account procedures can be used to read from storage, fetch or compute com | `get_code_commitment` | Gets the account code commitment of the active account.

**Inputs:** `[]`
**Outputs:** `[CODE_COMMITMENT]` | Account | | `get_initial_storage_commitment` | Returns the storage commitment of the active account at the beginning of the transaction.

**Inputs:** `[]`
**Outputs:** `[INIT_STORAGE_COMMITMENT]` | Any | | `compute_storage_commitment` | Computes the latest account storage commitment of the active account.

**Inputs:** `[]`
**Outputs:** `[STORAGE_COMMITMENT]` | Account | -| `get_item` | Gets an item from the account storage.

**Inputs:** `[index]`
**Outputs:** `[VALUE]` | Account | -| `get_initial_item` | Gets the initial item from the account storage slot as it was at the beginning of the transaction.

**Inputs:** `[index]`
**Outputs:** `[VALUE]` | Account | -| `get_map_item` | Returns the VALUE located under the specified KEY within the map contained in the given account storage slot.

**Inputs:** `[index, KEY]`
**Outputs:** `[VALUE]` | Account | -| `get_initial_map_item` | Gets the initial VALUE from the account storage map as it was at the beginning of the transaction.

**Inputs:** `[index, KEY]`
**Outputs:** `[VALUE]` | Account | +| `get_item` | Gets an item from the account storage.

**Inputs:** `[slot_id_prefix, slot_id_suffix]`
**Outputs:** `[VALUE]` | Account | +| `get_initial_item` | Gets the initial item from the account storage slot as it was at the beginning of the transaction.

**Inputs:** `[slot_id_prefix, slot_id_suffix]`
**Outputs:** `[VALUE]` | Account | +| `get_map_item` | Returns the VALUE located under the specified KEY within the map contained in the given account storage slot.

**Inputs:** `[slot_id_prefix, slot_id_suffix, KEY]`
**Outputs:** `[VALUE]` | Account | +| `get_initial_map_item` | Gets the initial VALUE from the account storage map as it was at the beginning of the transaction.

**Inputs:** `[slot_id_prefix, slot_id_suffix, KEY]`
**Outputs:** `[VALUE]` | Account | | `get_balance` | Returns the balance of the fungible asset associated with the provided faucet_id in the active account's vault.

**Inputs:** `[faucet_id_prefix, faucet_id_suffix]`
**Outputs:** `[balance]` | Any | | `get_initial_balance` | Returns the balance of the fungible asset associated with the provided faucet_id in the active account's vault at the beginning of the transaction.

**Inputs:** `[faucet_id_prefix, faucet_id_suffix]`
**Outputs:** `[init_balance]` | Any | | `has_non_fungible_asset` | Returns a boolean indicating whether the non-fungible asset is present in the active account's vault.

**Inputs:** `[ASSET]`
**Outputs:** `[has_asset]` | Any | @@ -53,7 +53,7 @@ Active account procedures can be used to read from storage, fetch or compute com | `get_procedure_root` | Returns the procedure root for the procedure at the specified index.

**Inputs:** `[index]`
**Outputs:** `[PROC_ROOT]` | Any | | `has_procedure` | Returns the binary flag indicating whether the procedure with the provided root is available on the active account.

**Inputs:** `[PROC_ROOT]`
**Outputs:** `[is_procedure_available]` | Any | -## Native account Procedures (`miden::native_account`) +## Native account Procedures (`miden::protocol::native_account`) Native account procedures can be used to write to storage, add or remove assets from the vault and compute delta commitment of the native account. @@ -62,13 +62,13 @@ Native account procedures can be used to write to storage, add or remove assets | `get_id` | Returns the ID of the native account of the transaction.

**Inputs:** `[]`
**Outputs:** `[account_id_prefix, account_id_suffix]` | Any | | `incr_nonce` | Increments the nonce of the native account by one and returns the new nonce. Can only be called from auth procedures.

**Inputs:** `[]`
**Outputs:** `[final_nonce]` | Auth | | `compute_delta_commitment` | Computes the commitment to the native account's delta. Can only be called from auth procedures.

**Inputs:** `[]`
**Outputs:** `[DELTA_COMMITMENT]` | Auth | -| `set_item` | Sets an item in the native account storage.

**Inputs:** `[index, VALUE]`
**Outputs:** `[OLD_VALUE]` | Native & Account | -| `set_map_item` | Sets VALUE under the specified KEY within the map contained in the given native account storage slot.

**Inputs:** `[index, KEY, VALUE]`
**Outputs:** `[OLD_MAP_ROOT, OLD_MAP_VALUE]` | Native & Account | +| `set_item` | Sets an item in the native account storage.

**Inputs:** `[slot_id_prefix, slot_id_suffix, VALUE]`
**Outputs:** `[OLD_VALUE]` | Native & Account | +| `set_map_item` | Sets VALUE under the specified KEY within the map contained in the given native account storage slot.

**Inputs:** `[slot_id_prefix, slot_id_suffix, KEY, VALUE]`
**Outputs:** `[OLD_VALUE]` | Native & Account | | `add_asset` | Adds the specified asset to the vault. For fungible assets, returns the total after addition.

**Inputs:** `[ASSET]`
**Outputs:** `[ASSET']` | Native & Account | | `remove_asset` | Removes the specified asset from the vault.

**Inputs:** `[ASSET]`
**Outputs:** `[ASSET]` | Native & Account | | `was_procedure_called` | Returns 1 if a native account procedure was called during transaction execution, and 0 otherwise.

**Inputs:** `[PROC_ROOT]`
**Outputs:** `[was_called]` | Any | -## Active Note Procedures (`miden::active_note`) +## Active Note Procedures (`miden::protocol::active_note`) Active note procedures can be used to fetch data from the note that is currently being processed by the transaction kernel. @@ -81,9 +81,8 @@ Active note procedures can be used to fetch data from the note that is currently | `get_sender` | Returns the sender of the active note.

**Inputs:** `[]`
**Outputs:** `[sender_id_prefix, sender_id_suffix]` | Note | | `get_serial_number` | Returns the [serial number](note.md#serial-number) of the active note.

**Inputs:** `[]`
**Outputs:** `[SERIAL_NUMBER]` | Note | | `get_script_root` | Returns the [script root](note.md#script) of the active note.

**Inputs:** `[]`
**Outputs:** `[SCRIPT_ROOT]` | Note | -| `add_assets_to_account` | Adds all assets from the active note to the account vault.

**Inputs:** `[]`
**Outputs:** `[]` | Note | -## Input Note Procedures (`miden::input_note`) +## Input Note Procedures (`miden::protocol::input_note`) Input note procedures can be used to fetch data on input notes consumed by the transaction. @@ -98,33 +97,35 @@ Input note procedures can be used to fetch data on input notes consumed by the t | `get_script_root` | Returns the [script root](note.md#script) of the input note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[SCRIPT_ROOT]` | Any | | `get_serial_number` | Returns the [serial number](note.md#serial-number) of the input note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[SERIAL_NUMBER]` | Any | -## Output Note Procedures (`miden::output_note`) +## Output Note Procedures (`miden::protocol::output_note`) Output note procedures can be used to fetch data on output notes created by the transaction. | Procedure | Description | Context | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -| `create` | Creates a new output note and returns its index.

**Inputs:** `[tag, aux, note_type, execution_hint, RECIPIENT]`
**Outputs:** `[note_idx]` | Native & Account | +| `create` | Creates a new output note and returns its index.

**Inputs:** `[tag, note_type, RECIPIENT]`
**Outputs:** `[note_idx]` | Native & Account | | `get_assets_info` | Returns the information about assets in the output note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[ASSETS_COMMITMENT, num_assets]` | Any | | `get_assets` | Writes the assets of the output note with the specified index into memory starting at the specified address.

**Inputs:** `[dest_ptr, note_index]`
**Outputs:** `[num_assets, dest_ptr, note_index]` | Any | | `add_asset` | Adds the `ASSET` to the output note specified by the index.

**Inputs:** `[ASSET, note_idx]`
**Outputs:** `[]` | Native | +| `set_attachment` | Sets the attachment of the note specified by the index.

If attachment_kind == Array, there must be an advice map entry for ATTACHMENT.

**Inputs:**
`Operand Stack: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT]`
`Advice map: { ATTACHMENT?: [[ATTACHMENT_ELEMENTS]] }`
**Outputs:** `[]` | Native | +| `set_array_attachment` | Sets the attachment of the note specified by the note index to the provided ATTACHMENT which commits to an array of felts.

**Inputs:**
`Operand Stack: [note_idx, attachment_scheme, ATTACHMENT]`
`Advice map: { ATTACHMENT: [[ATTACHMENT_ELEMENTS]] }`
**Outputs:** `[]` | Native | +| `set_word_attachment` | Sets the attachment of the note specified by the note index to the provided word.

**Inputs:** `[note_idx, attachment_scheme, ATTACHMENT]`
**Outputs:** `[]` | | `get_recipient` | Returns the [recipient](note#note-recipient-restricting-consumption) of the output note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[RECIPIENT]` | Any | | `get_metadata` | Returns the [metadata](note#metadata) of the output note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[METADATA]` | Any | -## Note Utility Procedures (`miden::note`) +## Note Utility Procedures (`miden::protocol::note`) Note utility procedures can be used to compute the required utility data or write note data to memory. | Procedure | Description | Context | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `compute_inputs_commitment` | Computes the commitment to the output note inputs starting at the specified memory address.

**Inputs:** `[inputs_ptr, num_inputs]`
**Outputs:** `[INPUTS_COMMITMENT]` | Any | -| `get_max_inputs_per_note` | Returns the max allowed number of input values per note.

**Inputs:** `[]`
**Outputs:** `[max_inputs_per_note]` | Any | | `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 inputs commitment.

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

**Inputs:** `[inputs_ptr, num_inputs, 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_prefix, sender_id_suffix]` | Any | -## Transaction Procedures (`miden::tx`) +## Transaction Procedures (`miden::protocol::tx`) Transaction procedures manage transaction-level operations including note creation, context switching, and reading transaction metadata. @@ -141,7 +142,7 @@ Transaction procedures manage transaction-level operations including note creati | `get_expiration_block_delta` | Returns the transaction expiration delta, or 0 if not set.

**Inputs:** `[]`
**Outputs:** `[block_height_delta]` | Any | | `update_expiration_block_delta` | Updates the transaction expiration delta.

**Inputs:** `[block_height_delta]`
**Outputs:** `[]` | Any | -## Faucet Procedures (`miden::faucet`) +## Faucet Procedures (`miden::protocol::faucet`) Faucet procedures allow reading and writing to faucet accounts to mint and burn assets. @@ -154,7 +155,7 @@ Faucet procedures allow reading and writing to faucet accounts to mint and burn | `get_total_issuance` | Returns the total issuance of the fungible faucet the transaction is being executed against.

**Inputs:** `[]`
**Outputs:** `[total_issuance]` | Faucet | | `is_non_fungible_asset_issued` | Returns a boolean indicating whether the provided non-fungible asset has been already issued by this faucet.

**Inputs:** `[ASSET]`
**Outputs:** `[is_issued]` | Faucet | -## Asset Procedures (`miden::asset`) +## Asset Procedures (`miden::protocol::asset`) Asset procedures provide utilities for creating fungible and non-fungible assets. diff --git a/docs/src/state.md b/docs/src/state.md index 6257959f54..02233cf6d3 100644 --- a/docs/src/state.md +++ b/docs/src/state.md @@ -49,7 +49,7 @@ This is done using an authenticated data structure, a sparse Merkle tree. As described in the [account ID section](account/id#account-storage-mode), accounts can have different storage modes: - **Public & Network accounts:** where all account data is stored on-chain. -- **Private accounts:** where only the commitments to the account is stored on-chain. +- **Private accounts:** where only the commitments to the account are stored on-chain. Private accounts significantly reduce storage overhead. A private account contributes only 40 bytes to the global `State` (15 bytes for the account ID + 32 bytes for the account commitment + 4 bytes for the block number). For example, 1 billion private accounts take up only 47.47 GB of `State`. diff --git a/docs/src/transaction.md b/docs/src/transaction.md index 72fe743cf5..c4c9477cf5 100644 --- a/docs/src/transaction.md +++ b/docs/src/transaction.md @@ -66,9 +66,9 @@ To illustrate the `Transaction` protocol, we provide two examples for a basic `T Let's assume account A wants to create a P2ID note. P2ID notes are pay-to-ID notes that can only be consumed by a specified target account ID. Note creators can provide the target account ID using the [note inputs](note#inputs). -In this example, account A uses the basic wallet and the authentication component provided by `miden-lib`. The basic wallet component defines the methods `wallets::basic::create_note` and `wallets::basic::move_asset_to_note` to create notes with assets, and `wallets::basic::receive_asset` to receive assets. The authentication component exposes `auth::basic::auth_tx_rpo_falcon512` which allows for signing a transaction. Some account methods like `active_account::get_id` are always exposed. +In this example, account A uses the basic wallet and the authentication component provided by `miden-standards`. The basic wallet component defines the methods `wallets::basic::create_note` and `wallets::basic::move_asset_to_note` to create notes with assets, and `wallets::basic::receive_asset` to receive assets. The authentication component exposes `auth::basic::auth_tx_falcon512_rpo` which allows for signing a transaction. Some account methods like `active_account::get_id` are always exposed. -The executor inputs to the Miden VM a `Transaction` script in which he places on the stack the data (tag, aux, note_type, execution_hint, RECIPIENT) of the note(s) that he wants to create using `wallets::basic::create_note` during the said `Transaction`. The [`NoteRecipient`](https://github.com/0xMiden/miden-base/blob/main/crates/miden-objects/src/note/recipient.rs) is a value that describes under which condition a note can be consumed and is built using a `serial_number`, the `note_script` (in this case P2ID script) and the `note_inputs`. The Miden VM will execute the `Transaction` script and create the note(s). After having been created, the executor can use `wallets::basic::move_asset_to_note` to move assets from the account's vault to the notes vault. +The executor inputs to the Miden VM a `Transaction` script in which it places on the stack the data (tag, aux, note_type, execution_hint, RECIPIENT) of the note(s) that it wants to create using `wallets::basic::create_note` during the said `Transaction`. The [`NoteRecipient`](https://github.com/0xMiden/miden-base/blob/main/crates/miden-protocol/src/note/recipient.rs) is a value that describes under which condition a note can be consumed and is built using a `serial_number`, the `note_script` (in this case P2ID script) and the `note_inputs`. The Miden VM will execute the `Transaction` script and create the note(s). After having been created, the executor can use `wallets::basic::move_asset_to_note` to move assets from the account's vault to the notes vault. After finalizing the `Transaction` the updated state and created note(s) can now be submitted to the Miden operator to be recorded on-chain. @@ -84,7 +84,7 @@ Then the P2ID note script is being executed. The script starts by reading the no If the check passes, the note script pushes the assets it holds into the account's vault. For every asset the note contains, the script calls the `wallets::basic::receive_asset` method exposed by the account's wallet component. The `wallets::basic::receive_asset` procedure calls `native_account::add_asset`, which cannot be called from the note itself. This allows accounts to control what functionality to expose, e.g. whether the account supports receiving assets or not, and the note cannot bypass that. -After the assets are stored in the account's vault, the transaction script is being executed. The script calls `auth::basic::auth_tx_rpo_falcon512` which is explicitly exposed in the account interface. The method is used to verify a provided signature against a public key stored in the account's storage and a commitment to this specific transaction. If the signature can be verified, the method increments the nonce. +After the assets are stored in the account's vault, the transaction script is being executed. The script calls `auth::basic::auth_tx_falcon512_rpo` which is explicitly exposed in the account interface. The method is used to verify a provided signature against a public key stored in the account's storage and a commitment to this specific transaction. If the signature can be verified, the method increments the nonce. The Epilogue finalizes the transaction by computing the final account hash, asserting the nonce increment and checking that no assets were created or destroyed in the transaction — that means the net sum of all assets must stay the same. @@ -128,7 +128,7 @@ The ability to facilitate both, local and network transactions, **is one of the - Not all transactions require notes. For example, the owner of a faucet can mint new tokens using only a `Transaction` script, without interacting with external notes. -- In Miden executors can choose arbitrary reference blocks to execute against their state. Hence it is possible to set `Transaction` expiration heights and in doing so, to define a block height until a `Transaction` should be included into a block. If the `Transaction` is expired, the resulting account state change is not valid and the `Transaction` cannot be verified anymore. +- In Miden, executors can choose arbitrary reference blocks to execute against their state. Hence it is possible to set `Transaction` expiration heights and in doing so, to define a block height until a `Transaction` should be included into a block. If the `Transaction` is expired, the resulting account state change is not valid and the `Transaction` cannot be verified anymore. - Note and `Transaction` scripts can read the state of foreign accounts during execution. This is called foreign procedure invocation. For example, the price of an asset for the **Swap** script might depend on a certain value stored in the oracle account. diff --git a/scripts/check-features.sh b/scripts/check-features.sh new file mode 100755 index 0000000000..7eae778e4b --- /dev/null +++ b/scripts/check-features.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +# Script to check all feature combinations compile without warnings +# This script ensures that warnings are treated as errors for CI + +echo "Checking all feature combinations with cargo-hack..." + +# Set environment variables to treat warnings as errors +export RUSTFLAGS="-D warnings" + +# Enable file generation in the `src` directory for miden-protocol and miden-standards build scripts +export BUILD_GENERATED_FILES_IN_SRC=1 + +# Run cargo-hack with comprehensive feature checking +# Focus on library packages that have significant feature matrices +for package in miden-protocol miden-standards miden-agglayer miden-tx miden-testing miden-block-prover miden-tx-batch-prover; do + echo "Checking package: $package" + cargo hack check -p "$package" --each-feature --all-targets +done + +echo "All feature combinations compiled successfully!"