diff --git a/.github/ISSUE_TEMPLATE/1-bugreport.yml b/.github/ISSUE_TEMPLATE/1-bugreport.yml deleted file mode 100644 index 7bcf0aa30a..0000000000 --- a/.github/ISSUE_TEMPLATE/1-bugreport.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: "Bug Report" -description: "File a bug report" -type: "Bug" -body: - - type: markdown - attributes: - value: | - Thank you for taking the time to fill out this bug report! - - type: textarea - id: version - attributes: - label: "Packages versions" - description: "Let us know the versions of any other packages used. For example, which version of the protocol are you using?" - placeholder: "miden-base: 0.1.0" - validations: - required: true - - type: textarea - id: bug-description - attributes: - label: "Bug description" - description: "Describe the behavior you are experiencing." - placeholder: "Tell us what happened and what should have happened." - validations: - required: true - - type: textarea - id: reproduce-steps - attributes: - label: "How can this be reproduced?" - description: "If possible, describe how to replicate the unexpected behavior that you see." - placeholder: "Steps!" - validations: - required: false - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This is automatically formatted as code, no need for backticks. - render: shell diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml deleted file mode 100644 index bf545356b3..0000000000 --- a/.github/ISSUE_TEMPLATE/2-feature-request.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: "Feature request" -description: "Request new goodies" -type: "Feature" -body: - - type: markdown - attributes: - value: | - Thank you for taking the time to fill a feature request! - - type: textarea - id: scenario-why - attributes: - label: "Feature description" - validations: - required: true - - type: textarea - id: scenario-how - attributes: - label: "Why is this feature needed?" - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/3-task.yml b/.github/ISSUE_TEMPLATE/3-task.yml deleted file mode 100644 index 9aa8cfe5e0..0000000000 --- a/.github/ISSUE_TEMPLATE/3-task.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "Task" -description: "Work item" -type: "Task" -body: - - type: markdown - attributes: - value: | - A task should be less than a week worth of work! - - type: textarea - id: task-what - attributes: - label: "What should be done?" - placeholder: "Refactor how account storage is loaded into transaction executor" - validations: - required: true - - type: textarea - id: task-how - attributes: - label: "How should it be done?" - placeholder: "Only the data required for transaction execution should be loaded from account storage (i.e., lazy-loading)" - validations: - required: true - - type: textarea - id: task-done - attributes: - label: "When is this task done?" - placeholder: "The task is done when lazy-loading of account storage is implemented" - validations: - required: true - - type: textarea - id: task-related - attributes: - label: "Additional context" - description: "Add context to the tasks. E.g. other related tasks or relevant discussions on PRs/chats." - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 0086358db1..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: true diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml deleted file mode 100644 index 1cdd372b45..0000000000 --- a/.github/workflows/book.yml +++ /dev/null @@ -1,75 +0,0 @@ -# Performs checks, builds and deploys the documentation mdbook. -# -# Flow is based on the official github and mdbook documentation: -# -# https://github.com/actions/starter-workflows/blob/main/pages/mdbook.yml -# https://github.com/rust-lang/mdBook/wiki/Automated-Deployment%3A-GitHub-Actions - -name: book - -# Documentation should be built and tested on every pull-request, and additionally deployed on push onto next. -on: - workflow_dispatch: - pull_request: - path: ['docs/**'] - push: - branches: [next] - path: ['docs/**'] - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Always build and test the mdbook documentation whenever the docs folder is changed. - # - # The documentation is uploaded as a github artifact IFF it is required for deployment i.e. on push into next. - build: - name: Build documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@main - - # Installation from source takes a fair while, so we install the binaries directly instead. - - name: Install mdbook and plugins - uses: taiki-e/install-action@v2 - with: - tool: mdbook, mdbook-linkcheck, mdbook-alerts, mdbook-katex - - - name: Build book - run: mdbook build docs/ - - # Only Upload documentation if we want to deploy (i.e. push to next). - - name: Setup Pages - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - id: pages - uses: actions/configure-pages@v5 - - - name: Upload book artifact - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - uses: actions/upload-pages-artifact@v3 - with: - # We specify multiple [output] sections in our book.toml which causes mdbook to create separate folders for each. This moves the generated `html` into its own `html` subdirectory. - path: ./docs/book/html - - # Deployment job only runs on push to next. - deploy: - name: Deploy documentation - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000000..8400212bc1 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,46 @@ +name: build-docs + +# 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 + +on: + push: + branches: [main, next] + paths: + - "docs/**" + - ".github/workflows/build-docs.yml" + pull_request: + types: [opened, reopened, synchronize] + paths: + - "docs/**" + - ".github/workflows/build-docs.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-docs: + name: Build Documentation + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: docs/package-lock.json + + - name: Install dependencies + working-directory: ./docs + run: npm ci + + - name: Build documentation + working-directory: ./docs + run: npm run build:dev diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml new file mode 100644 index 0000000000..2f39de1937 --- /dev/null +++ b/.github/workflows/msrv.yml @@ -0,0 +1,31 @@ +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/trigger-deploy-docs.yml b/.github/workflows/trigger-deploy-docs.yml new file mode 100644 index 0000000000..dd8349db2d --- /dev/null +++ b/.github/workflows/trigger-deploy-docs.yml @@ -0,0 +1,21 @@ +name: Trigger Aggregator Docs Rebuild + +on: + push: + branches: [next] + +jobs: + notify: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Send repository_dispatch to aggregator + uses: peter-evans/repository-dispatch@a628c95fd17070f003ea24579a56e6bc89b25766 + with: + # PAT that can access the central aggregator repository + token: ${{ secrets.DOCS_REPO_TOKEN }} + repository: ${{ vars.DOCS_AGGREGATOR_REPO }} + event-type: rebuild diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b38a68508..6e3060ba83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,99 @@ # Changelog +## 0.12.0 (11-05-2025) + +### Features + +- Added `prove_dummy` APIs on `LocalTransactionProver` ([#1674](https://github.com/0xMiden/miden-base/pull/1674)). +- Added `update_signers_and_threshold` procedure to update owner public keys and threshold config in multisig authentication component ([#1707](https://github.com/0xMiden/miden-base/issues/1707)). +- Added `add_signature` helper to simplify loading signatures into advice map ([#1725](https://github.com/0xMiden/miden-base/pull/1725)). +- Added `build_recipient` procedure to `miden::note` module ([#1807](https://github.com/0xMiden/miden-base/pull/1807)). +- Added `prove_dummy` APIs on `LocalBatchProver` and `LocalBlockProver` ([#1811](https://github.com/0xMiden/miden-base/pull/1811)). +- Added `get_native_id` and `get_native_nonce` procedures to the `miden` library ([#1844](https://github.com/0xMiden/miden-base/pull/1844)). +- Enabled lazy loading of assets during transaction execution ([#1848](https://github.com/0xMiden/miden-base/pull/1848)). +- Added lazy loading of the native asset ([#1855](https://github.com/0xMiden/miden-base/pull/1855)). +- [BREAKING] Enabled lazy loading of storage map entries during transaction execution ([#1857](https://github.com/0xMiden/miden-base/pull/1857)). +- [BREAKING] Enabled lazy loading of foreign accounts during transaction execution ([#1873](https://github.com/0xMiden/miden-base/pull/1873)). +- [BREAKING] Move account seed into `PartialAccount` ([#1875](https://github.com/0xMiden/miden-base/pull/1875), [#2003](https://github.com/0xMiden/miden-base/pull/2003)). +- Added `get_initial_item` and `get_map_item_init` procedures to `miden::account` module for accessing initial storage state ([#1883](https://github.com/0xMiden/miden-base/pull/1883)). +- Updated `rpo_falcon512::verify_signatures` to use `account::get_map_item_init` ([#1885](https://github.com/0xMiden/miden-base/issues/1885)). +- [BREAKING] Enabled lazy loading of assets and storage map items for foreign accounts during transaction execution ([#1888](https://github.com/0xMiden/miden-base/pull/1888)). +- [BREAKING] Represent new accounts as account deltas ([#1896](https://github.com/0xMiden/miden-base/pull/1896)). +- Implement `SlotName` for named storage slots ([#1932](https://github.com/0xMiden/miden-base/issues/1932)) +- [BREAKING] Removed `get_falcon_signature` from `miden-tx` crate ([#1924](https://github.com/0xMiden/miden-base/pull/1924)). +- Created a `Signature` wrapper to simplify the preparation of "native" signatures for use in the VM ([#1924](https://github.com/0xMiden/miden-base/pull/1924)). +- Added per-procedure approval thresholds to `AuthRpoFalcon512Multisig` auth component ([#1968](https://github.com/0xMiden/miden-base/pull/1968)). +- Implemented `input_note::get_sender` and `active_note::get_metadata` procedures in `miden` lib ([#1933](https://github.com/0xMiden/miden-base/pull/1933)). +- Added `Address` serialization and deserialization ([#1937](https://github.com/0xMiden/miden-base/issues/1937)). +- Added `StorageMap::{num_entries, num_leaves}` to retrieve the number of entries in a storage map ([#1935](https://github.com/0xMiden/miden-base/pull/1935)). +- Added `AssetVault::{num_assets, num_leaves, inner_nodes}` ([#1939](https://github.com/0xMiden/miden-base/pull/1939)). +- [BREAKING] Enabled computing the transaction ID from the data in a `TransactionHeader` ([#1973](https://github.com/0xMiden/miden-base/pull/1973)). +- 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] 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)). +- [BREAKING] Added `get_note_script()` method to `DataStore` trait to enable lazy loading of note scripts during transaction execution ([#1995](https://github.com/0xMiden/miden-base/pull/1995)). +- Added `AccountTree::apply_mutations_with_reversions` ([#2002](https://github.com/0xMiden/miden-base/pull/2002)). +- [BREAKING] Change `AccountTree` to be generic over `trait AccountTreeBackend` implementations ([#2006](https://github.com/0xMiden/miden-base/pull/2006)). +- Added `Display` trait for `AddressInterface` ([#2016](https://github.com/0xMiden/miden-base/pull/2016)). +- Added `has_procedure` procedure to the `miden::account` module ([#2017](https://github.com/0xMiden/miden-base/pull/2017)). +- Re-add bech32 encoding for `AccountId` ([#2018](https://github.com/0xMiden/miden-base/pull/2018)). +- [BREAKING] Separate account APIs in `miden::account` into `active_account` and `native_account` ([#2026](https://github.com/0xMiden/miden-base/pull/2026)). +- [BREAKING] Remove `miden::account::get_native_nonce` procedure ([#2026](https://github.com/0xMiden/miden-base/pull/2026)). +- [BREAKING] Refactor `Address` to make routing parameters optional ([#2032](https://github.com/0xMiden/miden-base/pull/2032), [#2047](https://github.com/0xMiden/miden-base/pull/2047)). +- [BREAKING] Refactor `PartialVault`, `PartialStorageMap`, `PartialAccountTree` and `PartialNullifierTree` to allow construction from a root ([#2042](https://github.com/0xMiden/miden-base/pull/2042)). +- [BREAKING] Refactor `Address` to make routing parameters optional ([#2032](https://github.com/0xMiden/miden-base/pull/2032)). +- Added `encryption_key` to `RoutingParameters` ([#2050](https://github.com/0xMiden/miden-base/pull/2050)). +- [BREAKING] Added `EcdsaK256Keccak` variant to auth enums ([#2052](https://github.com/0xMiden/miden-base/pull/2052)). +- Implemented storage map templates, which can be initialized through key/value lists provided via `InitStorageData` TOML ([#2053](https://github.com/0xMiden/miden-base/pull/2053)). + +### Changes + +- [BREAKING] Incremented MSRV to 1.90. +- [BREAKING] Migrated to `miden-vm` v0.18 and `miden-crypto` v0.17 ([#1832](https://github.com/0xMiden/miden-base/pull/1832)). +- [BREAKING] Removed `MockChain::add_pending_p2id_note` in favor of using `MockChainBuilder` ([#1842](https://github.com/0xMiden/miden-base/pull/#1842)). +- [BREAKING] Removed versioning of the transaction kernel, leaving only one latest version ([#1793](https://github.com/0xMiden/miden-base/pull/1793)). +- [BREAKING] Moved `miden::asset::{create_fungible_asset, create_non_fungible_asset}` procedures to `miden::faucet` ([#1850](https://github.com/0xMiden/miden-base/pull/1850)). +- [BREAKING] Removed versioning of the transaction kernel, leaving only one latest version ([#1793](https://github.com/0xMiden/miden-base/pull/1793)). +- Added `AccountComponent::from_package()` method to create components from `miden-mast-package::Package` ([#1802](https://github.com/0xMiden/miden-base/pull/1802)). +- [BREAKING] Removed some of the `note` kernel procedures and use `input_note` procedures instead ([#1834](https://github.com/0xMiden/miden-base/pull/1834)). +- [BREAKING] Replaced `Account` with `PartialAccount` in `TransactionInputs` ([#1840](https://github.com/0xMiden/miden-base/pull/1840)). +- [BREAKING] Renamed `Account::init_commitment` to `Account::initial_commitment` ([#1840](https://github.com/0xMiden/miden-base/pull/1840)). +- [BREAKING] Renamed the `is_onchain` method to `has_public_state` for `AccountId`, `AccountIdPrefix`, `Account`, `AccountInterface` and `AccountStorageMode` ([#1846](https://github.com/0xMiden/miden-base/pull/1846)). +- [BREAKING] Moved `NetworkId` from account ID to address module ([#1851](https://github.com/0xMiden/miden-base/pull/1851)). +- Removed `ProvenTransactionExt`([#1867](https://github.com/0xMiden/miden-base/pull/1867)). +- [BREAKING] Renamed the `is_onchain` method to `has_public_state` for `AccountId`, `AccountIdPrefix`, `Account`, `AccountInterface` and `AccountStorageMode` ([#1846](https://github.com/0xMiden/miden-base/pull/1846)). +- [BREAKING] Moved `miden::asset::{create_fungible_asset, create_non_fungible_asset}` procedures to `miden::faucet` ([#1850](https://github.com/0xMiden/miden-base/pull/1850)). +- [BREAKING] Moved `NetworkId` from account ID to address module ([#1851](https://github.com/0xMiden/miden-base/pull/1851)). +- [BREAKING] Moved `TransactionKernelError` to miden-tx ([#1859](https://github.com/0xMiden/miden-base/pull/1859)). +- [BREAKING] Changed `PartialStorageMap` to track the correct set of key+value pairings ([#1878](https://github.com/0xMiden/miden-base/pull/1878), [#1921](https://github.com/0xMiden/miden-base/pull/1921)). +- Change terminology of "current note" to "active note" ([#1863](https://github.com/0xMiden/miden-base/issues/1863)). +- [BREAKING] Moved and rename `miden::tx::{add_asset_to_note, create_note}` procedures to `miden::output_note::{add_asset, create}` ([#1874](https://github.com/0xMiden/miden-base/pull/1874)). +- Merge `bench-prover` into `bench-tx` crate ([#1894](https://github.com/0xMiden/miden-base/pull/1894)). +- Replace `eqw` usages with `exec.word::test_eq` and `exec.word::eq`, remove `is_key_greater` and `is_key_less` from `link_map` module ([#1897](https://github.com/0xMiden/miden-base/pull/1897)). +- [BREAKING] Make AssetVault and PartialVault APIs more type safe ([#1916](https://github.com/0xMiden/miden-base/pull/1916)). +- [BREAKING] Remove `MockChain::add_pending_note` to simplify mock chain internals ([#1903](https://github.com/0xMiden/miden-base/pull/1903)). +- [BREAKING] Moved active note procedures from `miden::note` to `miden::active_note` module ([#1901](https://github.com/0xMiden/miden-base/pull/1901)). +- [BREAKING] Removed account_seed from AccountFile ([#1917](https://github.com/0xMiden/miden-base/pull/1917)). +- [BREAKING] Renamed `TransactionInputs` to `TransactionExecutionInputs` and make a new `TransactionInputs` struct which does not contain `InputNotes` ([#1934](https://github.com/0xMiden/miden-base/pull/1934)). +- [BREAKING] Refactored `TransactionInputs` and remove `TransactionWitness` ([#1934](https://github.com/0xMiden/miden-base/pull/1934)). +- Simplify `MockChain` internals and rework its documentation ([#1942](https://github.com/0xMiden/miden-base/pull/1942)). +- [BREAKING] Changed the signature of TransactionAuthenticator to return the native signature ([#1945](https://github.com/0xMiden/miden-base/pull/1945)). +- [BREAKING] Renamed `MockChainBuilder::add_note` to `add_output_note` ([#1946](https://github.com/0xMiden/miden-base/pull/1946)). +- Dynamically lookup all masm `EventId`s from source ([#1954](https://github.com/0xMiden/miden-base/pull/1954)). +- [BREAKING] Return `ExecutionOutput` from `TransactionContext::execute_code` ([#1955](https://github.com/0xMiden/miden-base/pull/1955)). +- [BREAKING] Renamed `get_item_init` and `get_map_item_init` to `get_initial_item` and `get_initial_map_item` respectively ([#1959](https://github.com/0xMiden/miden-base/pull/1959)). +- Update the type signature syntax in the `account_components` module ([#1971](https://github.com/0xMiden/miden-base/pull/1971)). +- [BREAKING] Assert nonce is non-zero after the auth procedure ([#1982](https://github.com/0xMiden/miden-base/pull/1982)). +- [BREAKING] Removed `Rng` from `BasicAuthenticator` ([#1994](https://github.com/0xMiden/miden-base/pull/1994)). +- [BREAKING] Changed the outputs of the `output_note::add_asset` procedure: now the values that are the same as the passed parameters are dropped ([#2031](https://github.com/0xMiden/miden-base/pull/2031)). +- [BREAKING] Upgraded VM to 0.19 ([#2042](https://github.com/0xMiden/miden-base/pull/2042)). + ## 0.11.5 (2025-10-02) - Add new `can_consume` method to the `NoteConsumptionChecker` ([#1928](https://github.com/0xMiden/miden-base/pull/1928)). @@ -10,7 +104,7 @@ ## 0.11.3 (2025-09-15) -- Added Serialize and Deserialize Traits on `SigningInputs` ([#1858](https://github.com/0xMiden/miden-base/pull/1858)) +- Added Serialize and Deserialize Traits on `SigningInputs` ([#1858](https://github.com/0xMiden/miden-base/pull/1858)). ## 0.11.2 (2025-09-08) @@ -35,15 +129,14 @@ - Added `PartialBlockchain::num_tracked_blocks()` ([#1643](https://github.com/0xMiden/miden-base/pull/1643)). - Removed `TransactionScript::compile` & `NoteScript::compile` methods in favor of `ScriptBuilder` ([#1665](https://github.com/0xMiden/miden-base/pull/1665)). - Added `get_initial_code_commitment`, `get_initial_storage_commitment` and `get_initial_vault_root` procedures to `miden::account` module ([#1667](https://github.com/0xMiden/miden-base/pull/1667)). -- Added `FeeParameters` to `BlockHeader` ([#1652](https://github.com/0xMiden/miden-base/pull/1652)). - Added `input_note_get_recipient`, `output_note_get_recipient`, `input_note_get_metadata`, `output_note_get_metadata` procedures to the transaction kernel ([#1648](https://github.com/0xMiden/miden-base/pull/1648)). - Added `input_notes::get_assets` and `output_notes::get_assets` procedures to `miden` library ([#1648](https://github.com/0xMiden/miden-base/pull/1648)). - Added issuance accessor for fungible faucet accounts. ([#1660](https://github.com/0xMiden/miden-base/pull/1660)). -- Added `FeeParameters` to `BlockHeader`, implement `compute_fee` and output `FEE_ASSET` on the transaction stack ([#1652](https://github.com/0xMiden/miden-base/pull/1652), [#1654](https://github.com/0xMiden/miden-base/pull/1654), [#1659](https://github.com/0xMiden/miden-base/pull/1659)). +- Added multi-signature authentication component as standard authentication component ([#1599](https://github.com/0xMiden/miden-base/issues/1599)). - Added `FeeParameters` to `BlockHeader` and automatically compute and remove fees from account in the transaction kernel epilogue ([#1652](https://github.com/0xMiden/miden-base/pull/1652), [#1654](https://github.com/0xMiden/miden-base/pull/1654), [#1659](https://github.com/0xMiden/miden-base/pull/1659), [#1664](https://github.com/0xMiden/miden-base/pull/1664), [#1775](https://github.com/0xMiden/miden-base/pull/1775)). +- Added `Address` type to represent account-id based addresses ([#1713](https://github.com/0xMiden/miden-base/pull/1713), [#1750](https://github.com/0xMiden/miden-base/pull/1750)). - [BREAKING] Consolidated to a single async interface and drop `#[maybe_async]` usage ([#1666](https://github.com/0xMiden/miden-base/pull/#1666)). - [BREAKING] Made transaction execution and transaction authentication asynchronous ([#1699](https://github.com/0xMiden/miden-base/pull/1699)). -- Added `Address` type to represent account-id based addresses ([#1713](https://github.com/0xMiden/miden-base/pull/1713), [#1750](https://github.com/0xMiden/miden-base/pull/1750)). - [BREAKING] Return dedicated insufficient fee error from transaction host if account balance is too low ([#1744](https://github.com/0xMiden/miden-base/pull/#1744)). - Added `asset_vault::peek_balance` ([#1745](https://github.com/0xMiden/miden-base/pull/1745)). - Added `get_auth_scheme` method to `AccountComponentInterface` and `AccountInterface` for better authentication scheme extraction ([#1759](https://github.com/0xMiden/miden-base/pull/1759)). @@ -53,6 +146,7 @@ - Document `Address` in Miden book ([#1792](https://github.com/0xMiden/miden-base/pull/1792)). - Add `asset_vault::peek_balance` ([#1745](https://github.com/0xMiden/miden-base/pull/1745)). - Add `get_auth_scheme` method to `AccountComponentInterface` and `AccountInterface` for better authentication scheme extraction ([#1759](https://github.com/0xMiden/miden-base/pull/1759)). +- Add `CustomNetworkId` in `NetworkID` ([#1787](https://github.com/0xMiden/miden-base/pull/1787)). ### Changes @@ -162,6 +256,7 @@ - Add a new auth component `RpoFalcon512Acl` ([#1531](https://github.com/0xMiden/miden-base/pull/1531)). - [BREAKING] Change `BasicFungibleFaucet` to use `RpoFalcon512Acl` for authentication ([#1531](https://github.com/0xMiden/miden-base/pull/1531)). - Introduce `MockChain` methods for executing at an older block (#1541). +- [BREAKING] Change authentication component procedure name prefix from `auth__*` to `auth_*` ([#1861](https://github.com/0xMiden/miden-base/issues/1861)). ### Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d1d3971cc5..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,134 +0,0 @@ -# Contributing to Miden Base - -#### First off, thanks for taking the time to contribute! - -We want to make contributing to this project as easy and transparent as possible, whether it's: - -- Reporting a [bug](https://github.com/0xMiden/miden-base/issues/new?assignees=&labels=bug&projects=&template=1-bugreport.yml) -- Taking part in [discussions](https://github.com/0xMiden/miden-base/discussions) -- Submitting a [fix](https://github.com/0xMiden/miden-base/pulls) -- Proposing new [features](https://github.com/0xMiden/miden-base/issues/new?assignees=&labels=enhancement&projects=&template=2-feature-request.yml) - -  - -## Flow - -We are using [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), so all code changes happen through pull requests from a [forked repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo). - -### Branching - -- The current active branch is `next`. Every branch with a fix/feature must be forked from `next`. - -- The branch name should contain a short issue/feature description separated with hyphens [(kebab-case)](https://en.wikipedia.org/wiki/Letter_case#Kebab_case). - - For example, if the issue title is `Fix functionality X in component Y` then the branch name will be something like: `fix-x-in-y`. - -- New branch should be rebased from `next` before submitting a PR in case there have been changes to avoid merge commits. - i.e. this branches state: - - ``` - A---B---C fix-x-in-y - / - D---E---F---G next - | | - (F, G) changes happened after `fix-x-in-y` forked - ``` - - should become this after rebase: - - ``` - A'--B'--C' fix-x-in-y - / - D---E---F---G next - ``` - - More about rebase [here](https://git-scm.com/docs/git-rebase) and [here](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase#:~:text=What%20is%20git%20rebase%3F,of%20a%20feature%20branching%20workflow.) - -### Commit messages - -- Commit messages should be written in a short, descriptive manner and be prefixed with tags for the change type and scope (if possible) according to the [semantic commit](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) scheme. - For example, a new change to the `miden-objects` crate might have the following message: `feat(miden-objects): Added Account deserialization` - -- Also squash commits to logically separated, distinguishable stages to keep git log clean: - - ``` - 7hgf8978g9... Added A to X \ - \ (squash) - gh354354gh... oops, typo --- * ---------> 9fh1f51gh7... feat(X): add A && B - / - 85493g2458... Added B to X / - - - 789fdfffdf... Fixed D in Y \ - \ (squash) - 787g8fgf78... blah blah --- * ---------> 4070df6f00... fix(Y): fixed D && C - / - 9080gf6567... Fixed C in Y / - ``` - -### Code Style and Documentation - -- For documentation in the codebase, we follow the [rustdoc](https://doc.rust-lang.org/rust-by-example/meta/doc.html) convention with no more than 100 characters per line. -- For code sections, we use code separators like the following to a width of 100 characters:: - - ``` - // CODE SECTION HEADER - // ================================================================================ - ``` - -- [Rustfmt](https://github.com/rust-lang/rustfmt), [Clippy](https://github.com/rust-lang/rust-clippy), [Rustdoc](https://doc.rust-lang.org/rustdoc/index.html), [Typos](https://github.com/crate-ci/typos) and [Taplo](https://github.com/tamasfe/taplo) linting is included in CI pipeline. Anyways it's preferable to run linting locally before push. To simplify running these commands in a reproducible manner we use `make` commands, you can install the required tools by running: - - ``` - make install-tools - ``` - - and then run: - - ``` - make lint - ``` - -You can find more information about the `make` commands in the [Makefile](Makefile) - -### Testing - -After writing code different types of tests (unit, integration, end-to-end) are required to make sure that the correct behavior has been achieved and that no bugs have been introduced. You can run tests using the following command: - -``` -make test -``` - -### Versioning - -We use [semver](https://semver.org/) naming convention. - -  - -## Pre-PR checklist - -To make sure all commits adhere to our programming standards we use [pre-commit](https://pre-commit.com/) ([file](.pre-commit-config.yaml)) to run automatic commands on each commit. Please install it and follow the setup instructions for your machine. - -1. Repo forked and branch created from `next` according to the naming convention. -2. Commit messages and code style follow conventions. -3. Tests added for new functionality. -4. Documentation/comments updated for all changes according to our documentation convention. -5. Rustfmt, Clippy, Rustdoc, Typos and TOML-formatting linting passed (Will be run automatically by pre-commit). -6. New branch rebased from `next`. - -  - -## Write bug reports with detail, background, and sample code - -**Great Bug Reports** tend to have: - -- A quick summary and/or background -- Steps to reproduce -- What you expected would happen -- What actually happens -- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) - -  - -## Any contributions you make will be under the MIT Software License - -In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. diff --git a/Cargo.lock b/Cargo.lock index b45d8db323..4b4cc93993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,40 @@ # 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.25.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] -name = "adler2" -version = "2.0.1" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] [[package]] name = "ahash" @@ -24,7 +44,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -32,9 +52,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -48,6 +68,12 @@ dependencies = [ "equator", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -170,17 +196,17 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.0", ] [[package]] @@ -192,6 +218,18 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bech32" version = "0.11.0" @@ -214,17 +252,20 @@ dependencies = [ ] [[package]] -name = "bench-prover" +name = "bench-transaction" version = "0.1.0" dependencies = [ "anyhow", "criterion 0.6.0", + "miden-lib", "miden-objects", "miden-processor", "miden-testing", "miden-tx", + "rand_chacha", "serde", "serde_json", + "tokio", ] [[package]] @@ -250,9 +291,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake3" @@ -284,9 +325,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "bytes" @@ -302,9 +343,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.39" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "jobserver", @@ -314,9 +355,33 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] [[package]] name = "ciborium" @@ -345,20 +410,31 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" -version = "4.5.48" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstyle", "clap_lex", @@ -366,9 +442,36 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "color-eyre" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors 1.3.0", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +dependencies = [ + "once_cell", + "owo-colors 1.3.0", + "tracing-core", + "tracing-error", +] [[package]] name = "colorchoice" @@ -376,6 +479,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -482,6 +591,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -489,9 +610,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "debugid" version = "0.8.0" @@ -501,6 +650,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -528,7 +687,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -537,12 +698,70 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.3" @@ -552,11 +771,23 @@ 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.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -608,7 +839,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", ] [[package]] @@ -617,11 +858,27 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "findshlibs" @@ -641,6 +898,33 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs-err" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.31" @@ -737,33 +1021,47 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.32.3" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -771,14 +1069,26 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -786,6 +1096,13 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "rayon", + "serde", +] [[package]] name = "hermit-abi" @@ -793,6 +1110,24 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "indenter" version = "0.3.4" @@ -801,9 +1136,9 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown", @@ -828,25 +1163,23 @@ dependencies = [ ] [[package]] -name = "io-uring" -version = "0.7.10" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "generic-array", ] [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -857,9 +1190,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -924,20 +1257,34 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "keccak" version = "0.1.5" @@ -985,9 +1332,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libm" @@ -1009,11 +1356,10 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1033,7 +1379,7 @@ dependencies = [ "generator", "scoped-tls", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.20", ] [[package]] @@ -1053,44 +1399,45 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] [[package]] name = "miden-air" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2871bc4f392053cd115d4532e4b0fb164791829cc94b35641ed72480547dfd1" +checksum = "02cfe0ccecbd2e7a8a2cd97544d4cef438cb495e84e1a5769371dc822ff67268" dependencies = [ "miden-core", - "thiserror 2.0.17", + "miden-utils-indexing", + "thiserror", "winter-air", "winter-prover", ] [[package]] name = "miden-assembly" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345ae47710bbb4c6956dcc669a537c5cf2081879b6238c0df6da50e84a77ea3f" +checksum = "95978901bbe5cfc5632a85776d122ae3376a0728e5a4b4211c478d0a87c9e715" dependencies = [ "log", "miden-assembly-syntax", "miden-core", "miden-mast-package", "smallvec", - "thiserror 2.0.17", + "thiserror", ] [[package]] name = "miden-assembly-syntax" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab186ac7061b47415b923cf2da88df505f25c333f7caace80fb7760cf9c0590" +checksum = "f6e4ae33c9801102e32e04d51917333b0576d245e996f5ff43b0e2820f156876" dependencies = [ "aho-corasick", "lalrpop", @@ -1100,88 +1447,99 @@ dependencies = [ "miden-debug-types", "miden-utils-diagnostics", "midenc-hir-type", + "proptest", "regex", "rustc_version 0.4.1", "semver 1.0.27", "smallvec", - "thiserror 2.0.17", -] - -[[package]] -name = "miden-bench-tx" -version = "0.1.0" -dependencies = [ - "anyhow", - "miden-lib", - "miden-objects", - "miden-processor", - "miden-testing", - "miden-tx", - "rand_chacha", - "serde", - "serde_json", + "thiserror", ] [[package]] name = "miden-block-prover" -version = "0.11.5" +version = "0.12.0" dependencies = [ "miden-lib", "miden-objects", - "thiserror 2.0.17", + "thiserror", ] [[package]] name = "miden-core" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ad0b07592486e02de3ff7f3bff1d7fa81b1a7120f7f0b1027d650d810bbab" +checksum = "6806a8fd613e996b6d818a973ca40c484787848f14bea0a0c7aa1158ba9b26c2" dependencies = [ + "enum_dispatch", "miden-crypto", "miden-debug-types", "miden-formatting", + "miden-utils-indexing", "num-derive", "num-traits", - "thiserror 2.0.17", + "thiserror", "winter-math", "winter-utils", ] [[package]] name = "miden-crypto" -version = "0.15.9" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4329275a11c7d8328b14a7129b21d40183056dcd0d871c3069be6e550d6ca40" +checksum = "ffd4233803234f287596d9a60c67c4e5762f792eb0dd20969a41c8db093d6126" dependencies = [ "blake3", "cc", + "chacha20poly1305", + "ed25519-dalek", + "flume", "glob", + "hashbrown", + "hkdf", + "k256", + "miden-crypto-derive", "num", "num-complex", "rand", - "rand_core", + "rand_chacha", + "rand_core 0.9.3", + "rand_hc", + "rayon", "sha3", - "thiserror 2.0.17", + "subtle", + "thiserror", "winter-crypto", "winter-math", "winter-utils", + "x25519-dalek", +] + +[[package]] +name = "miden-crypto-derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f70dccd82a2b2787b9bfc64687cd224dfe0adc0d21ae9b241b0c6edc4a23335" +dependencies = [ + "quote", + "syn", ] [[package]] name = "miden-debug-types" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84e5b15ea6fe0f80688fde2d6e4f5a3b66417d2541388b7a6cf967c6a8a2bc0" +checksum = "43b37453feb8f4392af5eef29a11ac099b76cbcc47ffc034944a573d06a1cc1e" dependencies = [ "memchr", "miden-crypto", "miden-formatting", "miden-miette", + "miden-utils-indexing", "miden-utils-sync", "paste", "serde", - "serde_spanned 1.0.2", - "thiserror 2.0.17", + "serde_spanned", + "thiserror", ] [[package]] @@ -1195,29 +1553,33 @@ dependencies = [ [[package]] name = "miden-lib" -version = "0.11.5" +version = "0.12.0" dependencies = [ + "Inflector", "anyhow", "assert_matches", + "fs-err", "miden-assembly", + "miden-core", "miden-objects", "miden-processor", "miden-stdlib", "rand", "regex", - "thiserror 2.0.17", + "thiserror", "walkdir", ] [[package]] name = "miden-mast-package" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9d87c128f467874b272fec318e792385e35b14d200408a10d30909228db864" +checksum = "bb106cd425958cb2f6fab03741d4d8905b31dca48c0942dfd61adfe6970c695f" dependencies = [ "derive_more", "miden-assembly-syntax", "miden-core", + "thiserror", ] [[package]] @@ -1233,7 +1595,7 @@ dependencies = [ "indenter", "lazy_static", "miden-miette-derive", - "owo-colors", + "owo-colors 4.2.3", "regex", "rustc_version 0.2.3", "rustversion", @@ -1246,7 +1608,7 @@ dependencies = [ "syn", "terminal_size", "textwrap", - "thiserror 2.0.17", + "thiserror", "trybuild", "unicode-width 0.1.14", ] @@ -1264,17 +1626,21 @@ dependencies = [ [[package]] name = "miden-objects" -version = "0.11.5" +version = "0.12.0" dependencies = [ "anyhow", "assert_matches", "bech32", + "color-eyre", "criterion 0.5.1", - "getrandom", + "getrandom 0.3.4", "log", + "miden-air", "miden-assembly", + "miden-assembly-syntax", "miden-core", "miden-crypto", + "miden-mast-package", "miden-objects", "miden-processor", "miden-utils-sync", @@ -1286,23 +1652,27 @@ dependencies = [ "semver 1.0.27", "serde", "tempfile", - "thiserror 2.0.17", - "toml 0.8.23", + "thiserror", + "toml", "winter-air", "winter-rand-utils", ] [[package]] name = "miden-processor" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64304339292cc4e50a7b534197326824eeb21f3ffaadd955be63cc57093854ed" +checksum = "e97fbbc6911627d1fd25ad6f598a7ccd79721e9c89c4890a105f0a906ea3baf7" dependencies = [ + "itertools 0.14.0", "miden-air", "miden-core", "miden-debug-types", "miden-utils-diagnostics", - "thiserror 2.0.17", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", "tokio", "tracing", "winter-prover", @@ -1310,9 +1680,9 @@ dependencies = [ [[package]] name = "miden-prover" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21637f9dcca045f7bf65da20d42f37f86373b43092ae9df9a230cffb86f4d6d" +checksum = "3e07a8cb521312c8454544453b212b6dee3e7a983467f18ef8e296754ed7fe79" dependencies = [ "miden-air", "miden-debug-types", @@ -1324,21 +1694,23 @@ dependencies = [ [[package]] name = "miden-stdlib" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7308c32ec08dd75c29653315a0f395786d391c2fe55a5318ec39662a31de6a26" +checksum = "79bafb0b8715b8323eecef2b118a1dfa7112cc6cc247df742ccb8f8026c4da37" dependencies = [ "env_logger", + "fs-err", "miden-assembly", "miden-core", + "miden-crypto", "miden-processor", "miden-utils-sync", - "thiserror 2.0.17", + "thiserror", ] [[package]] name = "miden-testing" -version = "0.11.5" +version = "0.12.0" dependencies = [ "anyhow", "assert_matches", @@ -1348,10 +1720,11 @@ dependencies = [ "miden-objects", "miden-processor", "miden-tx", + "miden-tx-batch-prover", "rand", "rand_chacha", "rstest", - "thiserror 2.0.17", + "thiserror", "tokio", "winter-rand-utils", "winterfell", @@ -1359,7 +1732,7 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.11.5" +version = "0.12.0" dependencies = [ "anyhow", "assert_matches", @@ -1372,13 +1745,13 @@ dependencies = [ "miden-verifier", "rand", "rstest", - "thiserror 2.0.17", + "thiserror", "tokio", ] [[package]] name = "miden-tx-batch-prover" -version = "0.11.5" +version = "0.12.0" dependencies = [ "miden-objects", "miden-tx", @@ -1386,9 +1759,9 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9bf9a2c1cf3e3694d0eb347695a291602a57f817dd001ac838f300799576a69" +checksum = "a7d2d3d0909fa09ba58c6965f0dfafefeb299822dc417893786c119211cac89c" dependencies = [ "miden-crypto", "miden-debug-types", @@ -1397,11 +1770,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "miden-utils-indexing" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f35669a21d99c0d5f96720ba80205438f5e63a4132dfa4d1089bc95efce7760" +dependencies = [ + "thiserror", +] + [[package]] name = "miden-utils-sync" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb026e69ae937b2a83bf69ea86577e0ec2450cb22cce163190b9fd42f2972b63" +checksum = "0d9f2e82680ceff6618373c61d5947eda6eaf95f3503cf8310d3fb12a1dbdf9f" dependencies = [ "lock_api", "loom", @@ -1410,46 +1792,44 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a305e5e2c68d10bcb8e2ed3dc38e54bc3a538acc9eadc154b713d5bb47af22" +checksum = "a7e25b94000d5eaa24375c1c0a6fb49e1f349a35b59f51717359f06bc56fe14f" dependencies = [ "miden-air", "miden-core", - "thiserror 2.0.17", + "thiserror", "tracing", "winter-verifier", ] [[package]] name = "midenc-hir-type" -version = "0.1.5" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e381ba23e4f57ffa0d6039113f6d6004e5b8c7ae6cb909329b48f2ab525e8680" +checksum = "7798671ffbf6596de00619a9abaec67dc26965b891328c9d65c4cb6007597d50" dependencies = [ "miden-formatting", "smallvec", - "thiserror 2.0.17", + "thiserror", ] [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ - "adler2", + "adler", ] [[package]] -name = "mio" -version = "1.0.4" +name = "nanorand" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "getrandom 0.2.16", ] [[package]] @@ -1471,11 +1851,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1575,9 +1955,9 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1590,9 +1970,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -1600,6 +1980,18 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "owo-colors" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" + [[package]] name = "owo-colors" version = "4.2.3" @@ -1608,9 +2000,9 @@ checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1618,15 +2010,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -1666,6 +2058,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "plotters" version = "0.3.7" @@ -1694,6 +2096,17 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1711,9 +2124,9 @@ dependencies = [ [[package]] name = "pprof" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afad4d4df7b31280028245f152d5a575083e2abb822d05736f5e47653e77689f" +checksum = "38a01da47675efa7673b032bf8efd8214f1917d89685e07e395ab125ea42b187" dependencies = [ "aligned-vec", "backtrace", @@ -1729,7 +2142,7 @@ dependencies = [ "spin 0.10.0", "symbolic-demangle", "tempfile", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1753,18 +2166,33 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bitflags 2.10.0", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "quick-xml" version = "0.26.0" @@ -1796,7 +2224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -1806,7 +2234,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -1815,7 +2252,25 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b363d4f6370f88d62bf586c80405657bde0f0e1b8945d47d2ad59b906cb4f54" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", ] [[package]] @@ -1824,7 +2279,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -1849,18 +2304,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1870,9 +2325,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1881,9 +2336,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" @@ -1891,6 +2346,16 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.52" @@ -1959,7 +2424,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1972,11 +2437,11 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2012,6 +2477,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "0.9.0" @@ -2083,20 +2562,22 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "serde_spanned" -version = "1.0.2" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "serde_core", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -2124,6 +2605,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -2153,6 +2644,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spin" @@ -2163,11 +2657,21 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "str_stack" @@ -2196,6 +2700,12 @@ dependencies = [ "vte", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -2241,9 +2751,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -2252,9 +2762,9 @@ dependencies = [ [[package]] name = "target-triple" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" @@ -2263,10 +2773,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2275,7 +2785,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2305,16 +2815,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.1", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", + "unicode-width 0.2.2", ] [[package]] @@ -2323,18 +2824,7 @@ version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2369,24 +2859,19 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", - "io-uring", - "libc", - "mio", "pin-project-lite", - "slab", "tokio-macros", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2419,26 +2904,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned", + "toml_datetime", "toml_parser", "toml_writer", "winnow", @@ -2446,68 +2919,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime 0.7.2", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tracing" @@ -2541,6 +2985,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +dependencies = [ + "tracing", + "tracing-subscriber 0.2.25", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -2552,6 +3006,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" @@ -2572,9 +3037,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ded9fdb81f30a5708920310bfcd9ea7482ff9cba5f54601f7a19a877d5c2392" +checksum = "559b6a626c0815c942ac98d434746138b4f89ddd6a1b8cbb168c6845fb3376c5" dependencies = [ "dissimilar", "glob", @@ -2583,7 +3048,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 0.9.7", + "toml", ] [[package]] @@ -2592,11 +3057,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" @@ -2612,9 +3083,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -2622,6 +3093,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -2675,15 +3156,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -2695,9 +3167,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -2706,25 +3178,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2732,31 +3190,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -2784,7 +3242,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2841,9 +3299,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -2852,9 +3310,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -2869,9 +3327,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -2910,15 +3368,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -2934,16 +3383,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -2979,19 +3428,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.0", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3017,9 +3466,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3035,9 +3484,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3053,9 +3502,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3065,9 +3514,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3083,9 +3532,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3101,9 +3550,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3119,9 +3568,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3137,9 +3586,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -3269,6 +3718,16 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -3288,3 +3747,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml index 79943d84d1..a4f5124b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,7 @@ [workspace] members = [ "bin/bench-note-checker", - "bin/bench-prover", - "bin/bench-tx", + "bin/bench-transaction", "crates/miden-block-prover", "crates/miden-lib", "crates/miden-objects", @@ -19,7 +18,8 @@ exclude = [".github/"] homepage = "https://miden.xyz" license = "MIT" repository = "https://github.com/0xMiden/miden-base" -rust-version = "1.88" +rust-version = "1.90" +version = "0.12.0" [profile.release] codegen-units = 1 @@ -40,24 +40,28 @@ lto = true [workspace.dependencies] # Workspace crates -miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.11" } -miden-lib = { default-features = false, path = "crates/miden-lib", version = "0.11" } -miden-objects = { default-features = false, path = "crates/miden-objects", version = "0.11" } -miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.11" } -miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.11" } -miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.11" } +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 dependencies -miden-assembly = { default-features = false, version = "0.17" } -miden-core = { default-features = false, version = "0.17" } -miden-crypto = { default-features = false, version = "0.15.6" } -miden-processor = { default-features = false, version = "0.17" } -miden-prover = { default-features = false, version = "0.17" } -miden-stdlib = { default-features = false, version = "0.17" } -miden-utils-sync = { default-features = false, version = "0.17" } -miden-verifier = { default-features = false, version = "0.17" } +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" } +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" } +miden-utils-sync = { default-features = false, version = "0.19" } +miden-verifier = { default-features = false, version = "0.19" } # 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" } diff --git a/Makefile b/Makefile index 0f5e7a3d86..2af8b9b5ab 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ clippy: ## Runs Clippy with configs .PHONY: clippy-no-std clippy-no-std: ## Runs Clippy with configs - cargo clippy --no-default-features --target wasm32-unknown-unknown --workspace --lib --exclude bench-prover -- -D warnings + cargo clippy --no-default-features --target wasm32-unknown-unknown --workspace --lib -- -D warnings .PHONY: fix @@ -68,9 +68,9 @@ doc: ## Generates & checks documentation $(WARNINGS) cargo doc --all-features --keep-going --release -.PHONY: book -book: ## Builds the book & serves documentation site - mdbook serve --open docs +.PHONY: serve-docs +serve-docs: ## Serves the docs + cd docs && npm run start:dev # --- testing ------------------------------------------------------------------------------------- @@ -116,23 +116,19 @@ build: ## By default we should build in release mode .PHONY: build-no-std build-no-std: ## Build without the standard library - $(BUILD_GENERATED_FILES_IN_SRC) cargo build --no-default-features --target wasm32-unknown-unknown --workspace --lib --exclude bench-prover + $(BUILD_GENERATED_FILES_IN_SRC) cargo build --no-default-features --target wasm32-unknown-unknown --workspace --lib .PHONY: build-no-std-testing build-no-std-testing: ## Build without the standard library. Includes the `testing` feature - $(BUILD_GENERATED_FILES_IN_SRC) cargo build --no-default-features --target wasm32-unknown-unknown --workspace --exclude miden-bench-tx --features testing --exclude bench-prover + $(BUILD_GENERATED_FILES_IN_SRC) cargo build --no-default-features --target wasm32-unknown-unknown --workspace --exclude bench-transaction --features testing # --- benchmarking -------------------------------------------------------------------------------- .PHONY: bench-tx bench-tx: ## Run transaction benchmarks - cargo run --bin bench-tx - -.PHONY: bench-prover -bench-prover: ## Run prover benchmarks and consolidate results. - cargo bench --bin bench-prover --bench benches - cargo run --bin bench-prover + cargo run --bin bench-transaction --features concurrent + cargo bench --bin bench-transaction --bench time_counting_benchmarks --features concurrent .PHONY: bench-note-checker bench-note-checker: ## Run note checker benchmarks @@ -143,16 +139,24 @@ bench-note-checker: ## Run note checker benchmarks .PHONY: check-tools check-tools: ## Checks if development tools are installed @echo "Checking development tools..." - @command -v mdbook >/dev/null 2>&1 && echo "[OK] mdbook is installed" || echo "[MISSING] mdbook is not installed (run: make install-tools)" + @command -v npm >/dev/null 2>&1 && echo "[OK] npm is installed" || echo "[MISSING] npm is not installed (run: make install-tools)" @command -v typos >/dev/null 2>&1 && echo "[OK] typos is installed" || echo "[MISSING] typos is not installed (run: make install-tools)" - @command -v nextest >/dev/null 2>&1 && echo "[OK] nextest is installed" || echo "[MISSING] nextest is not installed (run: make install-tools)" + @command -v cargo nextest >/dev/null 2>&1 && echo "[OK] cargo-nextest is installed" || echo "[MISSING] cargo-nextest is not installed (run: make install-tools)" @command -v taplo >/dev/null 2>&1 && echo "[OK] taplo is installed" || echo "[MISSING] taplo is not installed (run: make install-tools)" + @command -v cargo-machete >/dev/null 2>&1 && echo "[OK] cargo-machete is installed" || echo "[MISSING] cargo-machete is not installed (run: make install-tools)" .PHONY: install-tools install-tools: ## Installs development tools required by the Makefile (mdbook, typos, nextest, taplo) - @echo "Installing development tools..." - cargo install mdbook --locked + @echo "Installing development tools..."" + @if ! command -v node >/dev/null 2>&1; then \ + echo "Node.js not found. Please install Node.js from https://nodejs.org/ or using your package manager"; \ + echo "On macOS: brew install node"; \ + echo "On Ubuntu/Debian: sudo apt install nodejs npm"; \ + echo "On Windows: Download from https://nodejs.org/"; \ + exit 1; \ + fi cargo install typos-cli --locked cargo install cargo-nextest --locked cargo install taplo-cli --locked + cargo install cargo-machete --locked @echo "Development tools installation complete!" diff --git a/README.md b/README.md index bcd2db18fc..394bf72716 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/0xMiden/miden-base/blob/main/LICENSE) [![test](https://github.com/0xMiden/miden-base/actions/workflows/test.yml/badge.svg)](https://github.com/0xMiden/miden-base/actions/workflows/test.yml) [![build](https://github.com/0xMiden/miden-base/actions/workflows/build.yml/badge.svg)](https://github.com/0xMiden/miden-base/actions/workflows/build.yml) -[![RUST_VERSION](https://img.shields.io/badge/rustc-1.88+-lightgray.svg)](https://www.rust-lang.org/tools/install) +[![RUST_VERSION](https://img.shields.io/badge/rustc-1.90+-lightgray.svg)](https://www.rust-lang.org/tools/install) [![GitHub Release](https://img.shields.io/github/release/0xMiden/miden-base)](https://github.com/0xMiden/miden-base/releases/) Description and core structures for the Miden Rollup protocol. @@ -16,14 +16,14 @@ Miden is a zero-knowledge rollup for high-throughput and private applications. M If you want to join the technical discussion or learn more about the project, please check out -* the [Documentation](https://0xMiden.github.io/miden-docs). -* the [Telegram](https://t.me/BuildOnMiden) -* the [Repo](https://github.com/0xMiden) -* the [Roadmap](https://miden.xyz/roadmap) +- the [Documentation](https://0xMiden.github.io/miden-docs). +- the [Telegram](https://t.me/BuildOnMiden) +- the [Repo](https://github.com/0xMiden) +- the [Roadmap](https://miden.xyz/roadmap) ## Status and features -Miden is currently on release v0.11. 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.12. 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 @@ -44,12 +44,12 @@ Miden is currently on release v0.11. This is an early version of the protocol an ## Project structure -| 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-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. | +| 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-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. | ## Make commands @@ -69,6 +69,10 @@ make test Some of the functions in this project are computationally intensive and may take a significant amount of time to compile and complete during testing. To ensure optimal results we use the `make test` command. It enables the running of tests in release mode and using specific configurations replicates the test conditions of the development mode and verifies all debug assertions. +## Documentation + +The documentation in the `docs/` folder is built using Docusaurus and is automatically absorbed into the main [miden-docs](https://github.com/0xMiden/miden-docs) repository for the main documentation website. Changes to the `next` branch trigger an automated deployment workflow. The docs folder requires npm packages to be installed before building. + ## License This project is [MIT licensed](./LICENSE) diff --git a/bin/bench-note-checker/Cargo.toml b/bin/bench-note-checker/Cargo.toml index 9c81202e8c..9496b74ecb 100644 --- a/bin/bench-note-checker/Cargo.toml +++ b/bin/bench-note-checker/Cargo.toml @@ -18,7 +18,7 @@ miden-testing = { workspace = true } miden-tx = { workspace = true } # External dependencies -anyhow = "1.0" +anyhow = { workspace = true } serde = { features = ["derive"], version = "1.0" } tokio = { features = ["macros", "rt"], version = "1.0" } diff --git a/bin/bench-note-checker/benches/benchmarks.rs b/bin/bench-note-checker/benches/benchmarks.rs index 754f368df8..b09a138f72 100644 --- a/bin/bench-note-checker/benches/benchmarks.rs +++ b/bin/bench-note-checker/benches/benchmarks.rs @@ -4,6 +4,7 @@ use std::time::Duration; use bench_note_checker::benchmark_names::{BENCH_GROUP, BENCH_MIXED_NOTES}; use bench_note_checker::{MixedNotesConfig, run_mixed_notes_check, setup_mixed_notes_benchmark}; use criterion::{Criterion, SamplingMode, criterion_group, criterion_main}; +use miden_tx::MAX_NUM_CHECKER_NOTES; fn note_checker_benchmarks(c: &mut Criterion) { let mut group = c.benchmark_group(BENCH_GROUP); @@ -14,19 +15,15 @@ fn note_checker_benchmarks(c: &mut Criterion) { .warm_up_time(Duration::from_millis(500)) .measurement_time(Duration::from_secs(10)); - // Benchmark with different numbers of failing notes (staying under 1024 total note limit). - for failing_count in [1, 10, 100, 1000] { + // Benchmark with different numbers of failing notes. + for failing_count in [1, 10, MAX_NUM_CHECKER_NOTES] { group.bench_function(format!("{BENCH_MIXED_NOTES}_{failing_count}_failing"), |b| { let setup = setup_mixed_notes_benchmark(MixedNotesConfig { failing_note_count: failing_count }) - .expect("Failed to set up mixed notes benchmark"); + .expect("failed to set up mixed notes benchmark"); - b.iter(|| { - let runtime = - tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - - runtime.block_on(async { black_box(run_mixed_notes_check(&setup).await) }) - }); + b.to_async(tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap()) + .iter(|| async { black_box(run_mixed_notes_check(&setup).await) }); }); } diff --git a/bin/bench-note-checker/src/lib.rs b/bin/bench-note-checker/src/lib.rs index 9fffee1c31..36761b03e6 100644 --- a/bin/bench-note-checker/src/lib.rs +++ b/bin/bench-note-checker/src/lib.rs @@ -8,6 +8,7 @@ use miden_objects::testing::account_id::{ ACCOUNT_ID_SENDER, }; use miden_testing::{Auth, MockChain, TxContextInput}; +use miden_tx::auth::UnreachableAuth; use miden_tx::{NoteConsumptionChecker, TransactionExecutor}; use serde::{Deserialize, Serialize}; @@ -64,7 +65,7 @@ pub struct MixedNotesSetup { pub fn setup_mixed_notes_benchmark(config: MixedNotesConfig) -> anyhow::Result { // Create a mock chain with an account. let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let account = builder.add_existing_wallet(Auth::IncrNonce)?; let target_account_id = account.id(); // Create the first successful note (P2ID note that the account can consume). @@ -87,7 +88,6 @@ pub fn setup_mixed_notes_benchmark(config: MixedNotesConfig) -> anyhow::Result anyhow::Result<() .build_tx_context(TxContextInput::AccountId(setup.target_account_id), &[], &setup.notes)? .build()?; - let input_notes = tx_context.input_notes().clone(); let block_ref = tx_context.tx_inputs().block_header().block_num(); let tx_args = tx_context.tx_args().clone(); // Create executor and checker. - let executor = TransactionExecutor::new(&tx_context) - .with_authenticator(tx_context.authenticator().unwrap()); + let executor = TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context); let checker = NoteConsumptionChecker::new(&executor); let result = checker - .check_notes_consumability(setup.target_account_id, block_ref, input_notes, tx_args) + .check_notes_consumability(setup.target_account_id, block_ref, setup.notes.clone(), tx_args) .await?; // Validate that we got the expected number of successful notes. assert_eq!( - result.successful.len(), setup.expected_successful_count, + result.successful.len(), "Expected {} successful notes, got {}", setup.expected_successful_count, result.successful.len() diff --git a/bin/bench-prover/Cargo.toml b/bin/bench-prover/Cargo.toml deleted file mode 100644 index f8a705fffb..0000000000 --- a/bin/bench-prover/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -authors.workspace = true -edition.workspace = true -exclude.workspace = true -homepage.workspace = true -license.workspace = true -name = "bench-prover" -publish = false -repository.workspace = true -rust-version.workspace = true -version = "0.1.0" - -[dependencies] -# Workspace dependencies -miden-objects = { features = ["testing"], workspace = true } -miden-testing = { workspace = true } -miden-tx = { workspace = true } - -# Miden dependencies -miden-processor = { workspace = true } - -# External dependencies -anyhow = "1.0" -serde = { features = ["derive"], version = "1.0" } -serde_json = "1.0" - -[dev-dependencies] -criterion = { features = ["html_reports"], version = "0.6" } - -[[bench]] -harness = false -name = "benches" -path = "benches/benchmarks.rs" diff --git a/bin/bench-prover/README.md b/bin/bench-prover/README.md deleted file mode 100644 index d5df280557..0000000000 --- a/bin/bench-prover/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Miden Prover Benchmarking - -This document describes how to run and analyze benchmarks for the Miden prover. - -## Running Benchmarks - -You can run the benchmarks in two ways: - -### Option 1: Using Make (from miden-base directory) - -```bash -make bench-prover -``` - -### Option 2: Running Directly (from bench-prover directory) - -```bash -# Run the benchmarks -cargo bench - -# Process the results -cargo run -``` - -## How It Works - -1. `cargo bench` uses [Criterion.rs](https://github.com/bheisler/criterion.rs) to run performance benchmarks -2. By default, Criterion stores raw benchmark results in `target/criterion/` -3. `cargo run` parses these results and creates a consolidated summary in `consolidated_benchmarks.json` - -## Viewing Results - -### HTML Reports - -Criterion automatically generates HTML reports with its built-in reporting feature. After running the benchmarks, you can find these reports in the Criterion directory by default under `target/criterion/{BENCHMARK_GROUP}/index.html` - - -### Consolidated JSON Summary - -The `consolidated_benchmarks.json` file contains a summary of all proving benchmarks in a structured format: - -Example `consolidated_benchmarks.json` structure: -```json -{ - "prove_consume_note_with_new_account": { - "id": "miden_proving/prove_consume_note_with_new_account", - // average time per trial in seconds - "mean_sec": 2.9489723874, - // lower bound of 95% confidence interval for mean - "mean_lower_bound_sec": 2.924891996, - // upper bound of 95% confidence interval for mean - "mean_upper_bound_sec": 2.9777331873, - // standard deviation of time per trial - "std_dev_sec": 0.04551027448900068, - // denotes time for each trial - "times_sec": [ - 2.98336025, - 3.051340166, - 2.972870583, - 2.943372125, - 2.923954958, - 2.939220542, - 2.945244416, - 2.890069959, - 2.9041745, - 2.936116375 - ], - // total number of trials benchmark completed - "trial_count": 10 - }, - "prove_consume_multiple_notes": { - "id": "miden_proving/prove_consume_multiple_notes", - "mean_sec": 2.0523832292, - "mean_lower_bound_sec": 2.0268005916, - "mean_upper_bound_sec": 2.0808349876, - "std_dev_sec": 0.04648980316499867, - "times_sec": [ - 2.118166333, - 2.121326834, - 2.112017625, - 2.028088083, - 2.014474333, - 2.000334667, - 2.018519542, - 2.024895417, - 2.043308542, - 2.042700916 - ], - "trial_count": 10 - } -} -``` diff --git a/bin/bench-prover/benches/benchmarks.rs b/bin/bench-prover/benches/benchmarks.rs deleted file mode 100644 index 0c58590eb4..0000000000 --- a/bin/bench-prover/benches/benchmarks.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::hint::black_box; -use std::time::Duration; - -use bench_prover::bench_functions::{ - prove_transaction, - setup_consume_multiple_notes, - setup_consume_note_with_new_account, -}; -use bench_prover::benchmark_names::{ - BENCH_CONSUME_MULTIPLE_NOTES, - BENCH_CONSUME_NOTE_NEW_ACCOUNT, - BENCH_GROUP, -}; -use criterion::{Criterion, SamplingMode, criterion_group, criterion_main}; - -fn core_benchmarks(c: &mut Criterion) { - let mut group = c.benchmark_group(BENCH_GROUP); - - group - .sampling_mode(SamplingMode::Flat) - .sample_size(10) - .warm_up_time(Duration::from_millis(1000)); - - group.bench_function(BENCH_CONSUME_NOTE_NEW_ACCOUNT, |b| { - let executed_transaction = setup_consume_note_with_new_account() - .expect("Failed to set up transaction for consuming note with new account"); - - // Only benchmark proving and verification - b.iter(|| black_box(prove_transaction(executed_transaction.clone()))); - }); - - group.bench_function(BENCH_CONSUME_MULTIPLE_NOTES, |b| { - let executed_transaction = setup_consume_multiple_notes() - .expect("Failed to set up transaction for consuming multiple notes"); - - // Only benchmark the proving and verification - b.iter(|| black_box(prove_transaction(executed_transaction.clone()))); - }); - - group.finish(); -} -criterion_group!(benches, core_benchmarks); -criterion_main!(benches); diff --git a/bin/bench-prover/src/bench_functions.rs b/bin/bench-prover/src/bench_functions.rs deleted file mode 100644 index 4c547849a3..0000000000 --- a/bin/bench-prover/src/bench_functions.rs +++ /dev/null @@ -1,107 +0,0 @@ -use anyhow::Result; -use miden_objects::Felt; -use miden_objects::account::Account; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset}; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::ACCOUNT_ID_SENDER; -use miden_objects::transaction::ExecutedTransaction; -use miden_testing::{Auth, MockChain}; -use miden_tx::{LocalTransactionProver, ProvingOptions}; - -pub fn setup_consume_note_with_new_account() -> Result { - // Create assets - let fungible_asset: Asset = FungibleAsset::mock(123); - - let mut builder = MockChain::builder(); - - // Create target account - let target_account = builder.create_new_wallet(Auth::BasicAuth)?; - - // Create the note - let note = builder - .add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - target_account.id(), - &[fungible_asset], - NoteType::Public, - ) - .unwrap(); - - let mock_chain = builder.build()?; - - // CONSTRUCT AND EXECUTE TX (Success) - // -------------------------------------------------------------------------------------------- - - // Execute the transaction and get the witness - let executed_transaction = mock_chain - .build_tx_context(target_account.clone(), &[note.id()], &[])? - .build()? - .execute_blocking()?; - - // Apply delta to the target account to verify it is no longer new - let target_account_after: Account = Account::from_parts( - target_account.id(), - AssetVault::new(&[fungible_asset]).unwrap(), - target_account.storage().clone(), - target_account.code().clone(), - Felt::new(1), - ); - - assert_eq!( - executed_transaction.final_account().commitment(), - target_account_after.commitment() - ); - - Ok(executed_transaction) -} - -pub fn setup_consume_multiple_notes() -> Result { - let mut builder = MockChain::builder(); - - let mut account = builder.add_existing_wallet(Auth::BasicAuth)?; - let fungible_asset_1: Asset = FungibleAsset::mock(100); - let fungible_asset_2: Asset = FungibleAsset::mock(23); - - let note_1 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[fungible_asset_1], - NoteType::Private, - )?; - let note_2 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[fungible_asset_2], - NoteType::Private, - )?; - - let mock_chain = builder.build()?; - - let tx_context = mock_chain - .build_tx_context(account.id(), &[note_1.id(), note_2.id()], &[])? - .build()?; - - let executed_transaction = tx_context.execute_blocking().unwrap(); - - account.apply_delta(executed_transaction.account_delta()).unwrap(); - let resulting_asset = account.vault().assets().next().unwrap(); - if let Asset::Fungible(asset) = resulting_asset { - assert_eq!(asset.amount(), 123u64); - } else { - panic!("Resulting asset should be fungible"); - } - - Ok(executed_transaction) -} - -pub fn prove_transaction(executed_transaction: ExecutedTransaction) -> Result<()> { - let executed_transaction_id = executed_transaction.id(); - - let proof_options = ProvingOptions::default(); - let prover = LocalTransactionProver::new(proof_options); - let proven_transaction: miden_objects::transaction::ProvenTransaction = - prover.prove(executed_transaction.into()).unwrap(); - - assert_eq!(proven_transaction.id(), executed_transaction_id); - Ok(()) -} diff --git a/bin/bench-prover/src/lib.rs b/bin/bench-prover/src/lib.rs deleted file mode 100644 index 15cd947eea..0000000000 --- a/bin/bench-prover/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod bench_functions; -pub mod utils; - -pub mod benchmark_names { - pub const BENCH_CONSUME_NOTE_NEW_ACCOUNT: &str = "prove_consume_note_with_new_account"; - pub const BENCH_CONSUME_MULTIPLE_NOTES: &str = "prove_consume_multiple_notes"; - pub const BENCH_GROUP: &str = "miden_proving"; -} diff --git a/bin/bench-prover/src/main.rs b/bin/bench-prover/src/main.rs deleted file mode 100644 index 91974e0bc4..0000000000 --- a/bin/bench-prover/src/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use bench_prover::benchmark_names::{ - BENCH_CONSUME_MULTIPLE_NOTES, - BENCH_CONSUME_NOTE_NEW_ACCOUNT, - BENCH_GROUP, -}; -use bench_prover::utils::{cargo_target_directory, process_benchmark_data, save_json_to_file}; -use serde_json::json; - -fn main() -> Result<()> { - let target_dir = cargo_target_directory().unwrap_or_else(|| PathBuf::from("target")); - let base_path = target_dir.join("criterion").join(BENCH_GROUP); - - println!("Looking for benchmark results in: {}", base_path.display()); - - let benchmarks = [BENCH_CONSUME_NOTE_NEW_ACCOUNT, BENCH_CONSUME_MULTIPLE_NOTES]; - - let mut consolidated_results = json!({}); - - for benchmark in benchmarks { - let benchmark_path = base_path.join(benchmark).join("new"); - - println!("\nProcessing benchmark: {benchmark}"); - - if !benchmark_path.exists() { - return Err(anyhow::anyhow!( - "Benchmark directory does not exist: {}", - benchmark_path.display() - )); - } - - match process_benchmark_data(&benchmark_path) { - Ok(benchmark_data) => { - consolidated_results[benchmark] = benchmark_data; - }, - Err(err) => { - return Err(err) - .with_context(|| format!("Failed to process benchmark: {benchmark}")); - }, - } - } - - // Rest of the code remains the same - let output_path = target_dir.join("criterion").join("consolidated_benchmarks.json"); - println!("Writing consolidated file to {}", output_path.display()); - - save_json_to_file(&consolidated_results, &output_path) - .with_context(|| format!("Failed to save results to {}", output_path.display()))?; - - Ok(()) -} diff --git a/bin/bench-prover/src/utils.rs b/bin/bench-prover/src/utils.rs deleted file mode 100644 index 71a8f437e0..0000000000 --- a/bin/bench-prover/src/utils.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::env; -use std::fs::{self}; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; - -use anyhow::{Context, Result}; -use serde::Deserialize; -use serde_json::{Value, json}; - -// replicating this function from the criterion lib: -// https://github.com/bheisler/criterion.rs/blob/ccccbcc15237233af22af4c76751a7aa184609b3/src/lib.rs#L366. -pub fn cargo_target_directory() -> Option { - #[derive(Deserialize)] - struct Metadata { - target_directory: PathBuf, - } - - env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).or_else(|| { - let output = Command::new(env::var_os("CARGO")?) - .args(["metadata", "--format-version", "1"]) - .output() - .ok()?; - let metadata: Metadata = serde_json::from_slice(&output.stdout).ok()?; - Some(metadata.target_directory) - }) -} -const NANOS_PER_SEC: f64 = 1_000_000_000.0; - -/// Processes Criterion benchmark output files (benchmark.json, estimates.json, sample.json) -/// and extracts relevant performance metrics into a single JSON structure. -/// Converts nanosecond measurements to seconds and includes mean, confidence intervals, -/// standard deviation, and individual sample times. -pub fn process_benchmark_data(benchmark_path: &Path) -> Result { - let mut benchmark_data = json!({}); - - // Process benchmark.json - let benchmark_file = benchmark_path.join("benchmark.json"); - let benchmark_content = fs::read_to_string(&benchmark_file).with_context(|| { - format!("Failed to read benchmark file at {}", benchmark_file.display()) - })?; - - let json: Value = serde_json::from_str(&benchmark_content).with_context(|| { - format!("Failed to parse benchmark.json at {}", benchmark_file.display()) - })?; - benchmark_data["id"] = json["full_id"].clone(); - - // Process estimates.json - let estimates_file = benchmark_path.join("estimates.json"); - let estimates_content = fs::read_to_string(&estimates_file).with_context(|| { - format!("Failed to read estimates file at {}", estimates_file.display()) - })?; - - let json: Value = serde_json::from_str(&estimates_content).with_context(|| { - format!("Failed to parse estimates.json at {}", estimates_file.display()) - })?; - - // Extract metrics - let mean = json["mean"]["point_estimate"] - .as_f64() - .with_context(|| "Missing or invalid mean point estimate in estimates.json")?; - benchmark_data["mean_sec"] = json!(mean / NANOS_PER_SEC); - - let lower = json["mean"]["confidence_interval"]["lower_bound"] - .as_f64() - .with_context(|| "Missing or invalid lower bound in estimates.json")?; - benchmark_data["mean_lower_bound_sec"] = json!(lower / NANOS_PER_SEC); - - let upper = json["mean"]["confidence_interval"]["upper_bound"] - .as_f64() - .with_context(|| "Missing or invalid upper bound in estimates.json")?; - benchmark_data["mean_upper_bound_sec"] = json!(upper / NANOS_PER_SEC); - - let std_dev = json["std_dev"]["point_estimate"] - .as_f64() - .with_context(|| "Missing or invalid std_dev point estimate in estimates.json")?; - benchmark_data["std_dev_sec"] = json!(std_dev / NANOS_PER_SEC); - - // Process sample.json - let sample_file = benchmark_path.join("sample.json"); - let sample_content = fs::read_to_string(&sample_file) - .with_context(|| format!("Failed to read sample file at {}", sample_file.display()))?; - - let json: Value = serde_json::from_str(&sample_content) - .with_context(|| format!("Failed to parse sample.json at {}", sample_file.display()))?; - - let times_array = json["times"] - .as_array() - .with_context(|| "Missing or invalid time values in sample.json")?; - - let times_sec: Vec = times_array - .iter() - .map(|v| v.as_f64().ok_or_else(|| anyhow::anyhow!("Invalid time values"))) - .collect::>>()? - .into_iter() - .map(|t| t / NANOS_PER_SEC) - .collect(); - - benchmark_data["times_sec"] = json!(times_sec); - - // Do the same for trials - let trials_array = json["iters"] - .as_array() - .with_context(|| "Missing or invalid iters array in sample.json")?; - benchmark_data["trial_count"] = json!(trials_array.len()); - - Ok(benchmark_data) -} - -// Update signature for save_json_to_file to use anyhow -pub fn save_json_to_file(data: &Value, file_path: &Path) -> Result<()> { - let mut file = fs::File::create(file_path) - .with_context(|| format!("Failed to create file at {}", file_path.display()))?; - let json_string = - serde_json::to_string_pretty(data).context("Failed to convert data to JSON string")?; - file.write_all(json_string.as_bytes()) - .with_context(|| format!("Failed to write JSON to file at {}", file_path.display()))?; - Ok(()) -} diff --git a/bin/bench-tx/Cargo.toml b/bin/bench-transaction/Cargo.toml similarity index 60% rename from bin/bench-tx/Cargo.toml rename to bin/bench-transaction/Cargo.toml index 12b29e2e9e..a269825112 100644 --- a/bin/bench-tx/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -4,28 +4,33 @@ edition.workspace = true exclude.workspace = true homepage.workspace = true license.workspace = true -name = "miden-bench-tx" +name = "bench-transaction" publish = false repository.workspace = true rust-version.workspace = true version = "0.1.0" -[[bin]] -name = "bench-tx" -path = "src/main.rs" +[[bench]] +harness = false +name = "time_counting_benchmarks" +path = "src/time_counting_benchmarks/prove.rs" [dependencies] # Workspace dependencies miden-lib = { workspace = true } -miden-objects = { workspace = true } +miden-objects = { features = ["testing"], workspace = true } miden-testing = { workspace = true } -miden-tx = { features = ["testing"], workspace = true } +miden-tx = { workspace = true } # Miden dependencies miden-processor = { workspace = true } # External dependencies -anyhow = { features = ["backtrace", "std"], version = "1.0" } +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 } + +[dev-dependencies] +criterion = { features = ["async_tokio", "html_reports"], version = "0.6" } diff --git a/bin/bench-transaction/README.md b/bin/bench-transaction/README.md new file mode 100644 index 0000000000..6d229c8c78 --- /dev/null +++ b/bin/bench-transaction/README.md @@ -0,0 +1,67 @@ +# Miden Transaction Benchmarking + +Below we describe how to benchmark Miden transactions. + +Benchmarks consist of two groups: +- Benchmarking the transaction execution. + + For each transaction, data is collected on the number of cycles required to complete: + - Prologue + - All notes processing + - Each note execution + - Transaction script processing + - Epilogue: + - Total number of cycles + - Authentication procedure + - After tx cycles were obtained (The number of cycles the epilogue took to execute after the number of transaction cycles were obtained) + + Results of this benchmark will be stored in the [`bin/bench-tx/bench-tx.json`](bench-tx.json) file. +- Benchmarking the transaction execution and proving. + For each transaction in this group we measure how much time it takes to execute the transaction and to execute and prove the transaction. + + This group uses the [Criterion.rs](https://github.com/bheisler/criterion.rs) to collect the elapsed time. Results of this benchmark group are printed to the terminal and look like so: + ```zsh + Execute transaction/Execute transaction which consumes single P2ID note + time: [7.2611 ms 7.2772 ms 7.2929 ms] + change: [−0.9131% −0.5837% −0.3058%] (p = 0.00 < 0.05) + Change within noise threshold. + Execute transaction/Execute transaction which consumes two P2ID notes + time: [8.8279 ms 8.8442 ms 8.8633 ms] + change: [−1.2256% −0.7611% −0.3355%] (p = 0.00 < 0.05) + Change within noise threshold. + + Execute and prove transaction/Execute and prove transaction which consumes single P2ID note + time: [698.96 ms 703.92 ms 708.70 ms] + change: [−2.3061% −0.4274% +0.9653%] (p = 0.70 > 0.05) + No change in performance detected. + Execute and prove transaction/Execute and prove transaction which consumes two P2ID notes + time: [706.52 ms 710.91 ms 715.66 ms] + change: [−7.4641% −5.0278% −2.9437%] (p = 0.00 < 0.05) + Performance has improved. + ``` + +## Running Benchmarks + +You can run the benchmarks in two ways: + +### Option 1: Using Make (from miden-base directory) + +```bash +make bench-tx +``` + +This command will run both the cycle counting and the time counting benchmarks. + +### Option 2: Running each benchmark individually (from miden-base directory) + +```bash +# Run the cycle counting benchmarks +cargo run --bin bench-transaction --features concurrent + +# Run the time counting benchmarks +cargo bench --bin bench-transaction --bench time_counting_benchmarks --features concurrent +``` + +## License + +This project is [MIT licensed](../../LICENSE). \ No newline at end of file diff --git a/bin/bench-transaction/bench-tx.json b/bin/bench-transaction/bench-tx.json new file mode 100644 index 0000000000..dddf80ff3f --- /dev/null +++ b/bin/bench-transaction/bench-tx.json @@ -0,0 +1,40 @@ +{ + "consume single P2ID note": { + "prologue": 3182, + "notes_processing": 1736, + "note_execution": { + "0xa988111a1c72fba94d4d1d5a5c301440d524c5edd6446babc6e09c51da0c92af": 1698 + }, + "tx_script_processing": 42, + "epilogue": { + "total": 63605, + "auth_procedure": 62403, + "after_tx_cycles_obtained": 517 + } + }, + "consume two P2ID notes": { + "prologue": 3779, + "notes_processing": 3478, + "note_execution": { + "0xd4a5cbccaeb197a18b8377d46c9737ed10c7049b619c1441f0414cb7c3f06af9": 1732, + "0xedc22bcd96965d66d045b2b78ecc3116bebc688619801d3ba2749484eb5eac3e": 1698 + }, + "tx_script_processing": 42, + "epilogue": { + "total": 63585, + "auth_procedure": 62393, + "after_tx_cycles_obtained": 517 + } + }, + "create single P2ID note": { + "prologue": 1575, + "notes_processing": 29, + "note_execution": {}, + "tx_script_processing": 1516, + "epilogue": { + "total": 64309, + "auth_procedure": 62574, + "after_tx_cycles_obtained": 517 + } + } +} \ No newline at end of file diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs new file mode 100644 index 0000000000..d784ef52ae --- /dev/null +++ b/bin/bench-transaction/src/context_setups.rs @@ -0,0 +1,123 @@ +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_testing::{Auth, MockChain, TransactionContext}; + +/// Returns the transaction context which could be used to run the transaction which creates a +/// single P2ID note. +pub fn tx_create_single_p2id_note() -> Result { + let mut builder = MockChain::builder(); + let fungible_asset = FungibleAsset::mock(150); + let account = builder.add_existing_wallet_with_assets(Auth::BasicAuth, [fungible_asset])?; + + let output_note = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[fungible_asset], + NoteType::Public, + )?; + + let mock_chain = builder.build()?; + + let tx_note_creation_script = format!( + " + use.miden::output_note + use.std::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 + # => [note_idx] + + # move the asset to the note + push.{asset} + call.::miden::contracts::wallets::basic::move_asset_to_note + dropw + # => [note_idx] + + # truncate the stack + exec.sys::truncate_stack + 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)?; + + // construct the transaction context + mock_chain + .build_tx_context(account.id(), &[], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note)]) + .tx_script(tx_script) + .build() +} + +/// Returns the transaction context which could be used to run the transaction which consumes a +/// single P2ID note into a new basic wallet. +pub fn tx_consume_single_p2id_note() -> Result { + // Create assets + let fungible_asset: Asset = FungibleAsset::mock(123); + + let mut builder = MockChain::builder(); + + // Create target account + let target_account = builder.create_new_wallet(Auth::BasicAuth)?; + + // Create the note + let note = builder + .add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + target_account.id(), + &[fungible_asset], + NoteType::Public, + ) + .unwrap(); + + let mock_chain = builder.build()?; + + // construct the transaction context + mock_chain.build_tx_context(target_account.clone(), &[note.id()], &[])?.build() +} + +/// Returns the transaction context which could be used to run the transaction which consumes two +/// P2ID notes into an existing basic wallet. +pub fn tx_consume_two_p2id_notes() -> Result { + let mut builder = MockChain::builder(); + + let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let fungible_asset_1: Asset = FungibleAsset::mock(100); + let fungible_asset_2: Asset = FungibleAsset::mock(23); + + let note_1 = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[fungible_asset_1], + NoteType::Private, + )?; + let note_2 = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[fungible_asset_2], + NoteType::Private, + )?; + + let mock_chain = builder.build()?; + + // construct the transaction context + mock_chain + .build_tx_context(account.id(), &[note_1.id(), note_2.id()], &[])? + .build() +} diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/mod.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/mod.rs new file mode 100644 index 0000000000..24801e09ba --- /dev/null +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/mod.rs @@ -0,0 +1,20 @@ +use core::fmt; + +pub mod utils; + +/// Indicates the type of the transaction execution benchmark +pub enum ExecutionBenchmark { + ConsumeSingleP2ID, + ConsumeTwoP2ID, + CreateSingleP2ID, +} + +impl fmt::Display for ExecutionBenchmark { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExecutionBenchmark::ConsumeSingleP2ID => write!(f, "consume single P2ID note"), + ExecutionBenchmark::ConsumeTwoP2ID => write!(f, "consume two P2ID notes"), + ExecutionBenchmark::CreateSingleP2ID => write!(f, "create single P2ID note"), + } + } +} diff --git a/bin/bench-tx/src/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs similarity index 51% rename from bin/bench-tx/src/utils.rs rename to bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index df99354322..1a5865ad1f 100644 --- a/bin/bench-tx/src/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -2,46 +2,27 @@ extern crate alloc; pub use alloc::collections::BTreeMap; pub use alloc::string::String; use std::fs::{read_to_string, write}; +use std::path::Path; use anyhow::Context; -use miden_lib::account::auth::AuthRpoFalcon512; -use miden_lib::account::wallets::BasicWallet; -use miden_objects::account::{ - Account, - AccountBuilder, - AccountStorageMode, - AccountType, - AuthSecretKey, -}; -use miden_objects::asset::Asset; -use miden_objects::crypto::dsa::rpo_falcon512::{PublicKey, SecretKey}; use miden_objects::transaction::TransactionMeasurements; -use miden_tx::auth::BasicAuthenticator; -use rand_chacha::ChaCha20Rng; -use rand_chacha::rand_core::SeedableRng; use serde::Serialize; use serde_json::{Value, from_str, to_string_pretty}; -use super::{Benchmark, Path}; - -// CONSTANTS -// ================================================================================================ - -// Copied from miden_objects::testing::account_id. -pub const ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET: u128 = 0x00aa00000000bc200000bc000000de00; -pub const ACCOUNT_ID_SENDER: u128 = 0x00fa00000000bb800000cc000000de00; +use super::ExecutionBenchmark; // MEASUREMENTS PRINTER // ================================================================================================ +/// Helper structure holding the cycle count of each transaction stage which could be easily +/// converted to the JSON file. #[derive(Debug, Clone, Serialize)] pub struct MeasurementsPrinter { prologue: usize, notes_processing: usize, note_execution: BTreeMap, tx_script_processing: usize, - epilogue: usize, - after_tx_cycles_obtained: usize, + epilogue: EpilogueMeasurements, } impl From for MeasurementsPrinter { @@ -57,48 +38,47 @@ impl From for MeasurementsPrinter { notes_processing: tx_measurements.notes_processing, note_execution: note_execution_map, tx_script_processing: tx_measurements.tx_script_processing, - epilogue: tx_measurements.epilogue, - after_tx_cycles_obtained: tx_measurements.after_tx_cycles_obtained, + epilogue: EpilogueMeasurements::from_parts( + tx_measurements.epilogue, + tx_measurements.auth_procedure, + tx_measurements.after_tx_cycles_obtained, + ), } } } -// HELPER FUNCTIONS -// ================================================================================================ - -pub fn get_account_with_basic_authenticated_wallet( - init_seed: [u8; 32], - account_type: AccountType, - storage_mode: AccountStorageMode, - public_key: PublicKey, - assets: Option, -) -> Account { - AccountBuilder::new(init_seed) - .account_type(account_type) - .storage_mode(storage_mode) - .with_assets(assets) - .with_component(BasicWallet) - .with_auth_component(AuthRpoFalcon512::new(public_key)) - .build_existing() - .unwrap() +/// Helper structure holding the cycle count for different intervals in the epilogue, namely: +/// - `total` interval holds the total number of cycles required to execute the epilogue +/// - `auth_procedure` interval holds the number of cycles required to execute the authentication +/// procedure +/// - `after_tx_cycles_obtained` holds the number of cycles which was executed from the moment of +/// the cycle count obtainment in the `epilogue::compute_fee` procedure to the end of the +/// epilogue. +#[derive(Debug, Clone, Serialize)] +struct EpilogueMeasurements { + total: usize, + auth_procedure: usize, + after_tx_cycles_obtained: usize, } -pub fn get_new_pk_and_authenticator() -> (PublicKey, BasicAuthenticator) { - let mut rng = ChaCha20Rng::from_seed(Default::default()); - let sec_key = SecretKey::with_rng(&mut rng); - let pub_key = sec_key.public_key(); - - let authenticator = BasicAuthenticator::::new_with_rng( - &[(pub_key.into(), AuthSecretKey::RpoFalcon512(sec_key))], - rng, - ); - - (pub_key, authenticator) +impl EpilogueMeasurements { + pub fn from_parts( + total: usize, + auth_procedure: usize, + after_tx_cycles_obtained: usize, + ) -> Self { + Self { + total, + auth_procedure, + after_tx_cycles_obtained, + } + } } +/// Writes the provided benchmark results to the JSON file at the provided path. pub fn write_bench_results_to_json( path: &Path, - tx_benchmarks: Vec<(Benchmark, MeasurementsPrinter)>, + tx_benchmarks: Vec<(ExecutionBenchmark, MeasurementsPrinter)>, ) -> anyhow::Result<()> { // convert benchmark file internals to the JSON Value let benchmark_file = read_to_string(path).context("failed to read benchmark file")?; diff --git a/bin/bench-transaction/src/lib.rs b/bin/bench-transaction/src/lib.rs new file mode 100644 index 0000000000..882a85de80 --- /dev/null +++ b/bin/bench-transaction/src/lib.rs @@ -0,0 +1 @@ +pub mod context_setups; diff --git a/bin/bench-transaction/src/main.rs b/bin/bench-transaction/src/main.rs new file mode 100644 index 0000000000..c036936eea --- /dev/null +++ b/bin/bench-transaction/src/main.rs @@ -0,0 +1,58 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use anyhow::{Context, Result}; +use miden_objects::transaction::TransactionMeasurements; + +mod context_setups; +use context_setups::{ + tx_consume_single_p2id_note, + tx_consume_two_p2id_notes, + tx_create_single_p2id_note, +}; + +mod cycle_counting_benchmarks; +use cycle_counting_benchmarks::ExecutionBenchmark; +use cycle_counting_benchmarks::utils::write_bench_results_to_json; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + // create a template file for benchmark results + let path = Path::new("bin/bench-transaction/bench-tx.json"); + let mut file = File::create(path).context("failed to create file")?; + file.write_all(b"{}").context("failed to write to file")?; + + // run all available benchmarks + let benchmark_results = vec![ + ( + ExecutionBenchmark::ConsumeSingleP2ID, + tx_consume_single_p2id_note()? + .execute() + .await + .map(TransactionMeasurements::from)? + .into(), + ), + ( + ExecutionBenchmark::ConsumeTwoP2ID, + tx_consume_two_p2id_notes()? + .execute() + .await + .map(TransactionMeasurements::from)? + .into(), + ), + ( + ExecutionBenchmark::CreateSingleP2ID, + tx_create_single_p2id_note()? + .execute() + .await + .map(TransactionMeasurements::from)? + .into(), + ), + ]; + + // store benchmark results in the JSON file + write_bench_results_to_json(path, benchmark_results)?; + + Ok(()) +} diff --git a/bin/bench-transaction/src/time_counting_benchmarks/prove.rs b/bin/bench-transaction/src/time_counting_benchmarks/prove.rs new file mode 100644 index 0000000000..857fb57121 --- /dev/null +++ b/bin/bench-transaction/src/time_counting_benchmarks/prove.rs @@ -0,0 +1,137 @@ +use std::hint::black_box; +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_tx::LocalTransactionProver; + +// BENCHMARK NAMES +// ================================================================================================ + +const BENCH_GROUP_EXECUTE: &str = "Execute transaction"; +const BENCH_EXECUTE_TX_CONSUME_SINGLE_P2ID: &str = + "Execute transaction which consumes single P2ID note"; +const BENCH_EXECUTE_TX_CONSUME_TWO_P2ID: &str = "Execute transaction which consumes two P2ID notes"; + +const BENCH_GROUP_EXECUTE_AND_PROVE: &str = "Execute and prove transaction"; +const BENCH_EXECUTE_AND_PROVE_TX_CONSUME_SINGLE_P2ID: &str = + "Execute and prove transaction which consumes single P2ID note"; +const BENCH_EXECUTE_AND_PROVE_TX_CONSUME_TWO_P2ID: &str = + "Execute and prove transaction which consumes two P2ID notes"; + +// CORE PROVING BENCHMARKS +// ================================================================================================ + +fn core_benchmarks(c: &mut Criterion) { + // EXECUTE GROUP + // -------------------------------------------------------------------------------------------- + + let mut execute_group = c.benchmark_group(BENCH_GROUP_EXECUTE); + + execute_group + .sampling_mode(SamplingMode::Flat) + .sample_size(10) + .warm_up_time(Duration::from_millis(1000)); + + execute_group.bench_function(BENCH_EXECUTE_TX_CONSUME_SINGLE_P2ID, |b| { + b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) + .iter_batched( + || { + // prepare the transaction context + tx_consume_single_p2id_note() + .expect("failed to create a context which consumes single P2ID note") + }, + |tx_context| async move { + // benchmark the transaction execution + black_box(tx_context.execute().await) + }, + BatchSize::SmallInput, + ); + }); + + execute_group.bench_function(BENCH_EXECUTE_TX_CONSUME_TWO_P2ID, |b| { + b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) + .iter_batched( + || { + // prepare the transaction context + tx_consume_two_p2id_notes() + .expect("failed to create a context which consumes two P2ID notes") + }, + |tx_context| async move { + // benchmark the transaction execution + black_box(tx_context.execute().await) + }, + BatchSize::SmallInput, + ); + }); + + execute_group.finish(); + + // EXECUTE AND PROVE GROUP + // -------------------------------------------------------------------------------------------- + + let mut execute_and_prove_group = c.benchmark_group(BENCH_GROUP_EXECUTE_AND_PROVE); + + execute_and_prove_group + .sampling_mode(SamplingMode::Flat) + .sample_size(10) + .warm_up_time(Duration::from_millis(1000)); + + execute_and_prove_group.bench_function(BENCH_EXECUTE_AND_PROVE_TX_CONSUME_SINGLE_P2ID, |b| { + b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) + .iter_batched( + || { + // prepare the transaction context + tx_consume_single_p2id_note() + .expect("failed to create a context which consumes single P2ID note") + }, + |tx_context| async move { + // benchmark the transaction execution and proving + black_box(prove_transaction( + tx_context + .execute() + .await + .expect("execution of the single P2ID note consumption tx failed"), + )) + }, + BatchSize::SmallInput, + ); + }); + + execute_and_prove_group.bench_function(BENCH_EXECUTE_AND_PROVE_TX_CONSUME_TWO_P2ID, |b| { + b.to_async(tokio::runtime::Builder::new_current_thread().build().unwrap()) + .iter_batched( + || { + // prepare the transaction context + tx_consume_two_p2id_notes() + .expect("failed to create a context which consumes two P2ID notes") + }, + |tx_context| async move { + // benchmark the transaction execution and proving + black_box(prove_transaction( + tx_context + .execute() + .await + .expect("execution of the two P2ID note consumption tx failed"), + )) + }, + BatchSize::SmallInput, + ); + }); + + execute_and_prove_group.finish(); +} + +fn prove_transaction(executed_transaction: ExecutedTransaction) -> Result<()> { + let executed_transaction_id = executed_transaction.id(); + let proven_transaction: ProvenTransaction = + LocalTransactionProver::default().prove(executed_transaction)?; + + assert_eq!(proven_transaction.id(), executed_transaction_id); + Ok(()) +} + +criterion_group!(benches, core_benchmarks); +criterion_main!(benches); diff --git a/bin/bench-tx/README.md b/bin/bench-tx/README.md deleted file mode 100644 index a9b14cb555..0000000000 --- a/bin/bench-tx/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Miden transactions benchmark - -This crate contains an executable used for benchmarking transactions. - -For each transaction, data is collected on the number of cycles required to complete: - -- Prologue -- All notes processing -- Each note execution -- Transaction script processing -- Epilogue - -## Usage - -To run the benchmarks you can run the following command: - -```shell -make bench-tx -``` - -Results of the benchmark are stored in the [bench-tx.json](bench-tx.json) file. - -## License - -This project is [MIT licensed](../../LICENSE). diff --git a/bin/bench-tx/bench-tx.json b/bin/bench-tx/bench-tx.json deleted file mode 100644 index 2a00818d71..0000000000 --- a/bin/bench-tx/bench-tx.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "simple": { - "prologue": 4178, - "notes_processing": 2624, - "note_execution": { - "0x2a96867a0f22d54d1e3530c074c3ad9ba9542b5060edf2f8398202f5d2e3f557": 1308, - "0xc52584ba513fd5eaaec47234263a1b20ce4d848488430e577dbdd6d9254d5bae": 1275 - }, - "tx_script_processing": 39, - "epilogue": 1663, - "after_tx_cycles_obtained": 500 - }, - "p2id": { - "prologue": 2779, - "notes_processing": 1653, - "note_execution": { - "0xb3044cb915c500d7678047e7f6cdfc9581efd8ebd07ff1977ef86f7072c51463": 1620 - }, - "tx_script_processing": 39, - "epilogue": 62466, - "after_tx_cycles_obtained": 500 - } -} \ No newline at end of file diff --git a/bin/bench-tx/src/main.rs b/bin/bench-tx/src/main.rs deleted file mode 100644 index a5ee64217a..0000000000 --- a/bin/bench-tx/src/main.rs +++ /dev/null @@ -1,124 +0,0 @@ -use core::fmt; -use std::fs::File; -use std::io::Write; -use std::path::Path; - -use anyhow::Context; -use miden_lib::note::create_p2id_note; -use miden_lib::testing::account_component::IncrNonceAuthComponent; -use miden_lib::testing::mock_account::MockAccountExt; -use miden_objects::account::{Account, AccountId, AccountStorageMode, AccountType}; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::crypto::rand::RpoRandomCoin; -use miden_objects::note::NoteType; -use miden_objects::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; -use miden_objects::transaction::TransactionMeasurements; -use miden_objects::{Felt, Word}; -use miden_testing::TransactionContextBuilder; -use miden_testing::utils::create_p2any_note; - -mod utils; -use utils::{ - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, - ACCOUNT_ID_SENDER, - get_account_with_basic_authenticated_wallet, - get_new_pk_and_authenticator, - write_bench_results_to_json, -}; -pub enum Benchmark { - Simple, - P2ID, -} - -impl fmt::Display for Benchmark { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Benchmark::Simple => write!(f, "simple"), - Benchmark::P2ID => write!(f, "p2id"), - } - } -} - -fn main() -> anyhow::Result<()> { - // create a template file for benchmark results - let path = Path::new("bin/bench-tx/bench-tx.json"); - let mut file = File::create(path).context("failed to create file")?; - file.write_all(b"{}").context("failed to write to file")?; - - // run all available benchmarks - let benchmark_results = vec![ - (Benchmark::Simple, benchmark_default_tx()?.into()), - (Benchmark::P2ID, benchmark_p2id()?.into()), - ]; - - // store benchmark results in the JSON file - write_bench_results_to_json(path, benchmark_results)?; - - Ok(()) -} - -// BENCHMARKS -// ================================================================================================ - -/// Runs the default transaction with empty transaction script and two default notes. -#[allow(clippy::arc_with_non_send_sync)] -pub fn benchmark_default_tx() -> anyhow::Result { - let tx_context = { - let account = - Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, IncrNonceAuthComponent); - - let input_note_1 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); - - let input_note_2 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(150)]); - TransactionContextBuilder::new(account) - .extend_input_notes(vec![input_note_1, input_note_2]) - .build()? - }; - let executed_transaction = - tx_context.execute_blocking().context("failed to execute transaction")?; - - Ok(executed_transaction.into()) -} - -/// Runs the transaction which consumes a P2ID note into a basic wallet. -#[allow(clippy::arc_with_non_send_sync)] -pub fn benchmark_p2id() -> anyhow::Result { - // Create assets - let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let fungible_asset: Asset = FungibleAsset::new(faucet_id, 100).unwrap().into(); - - // Create sender and target account - let sender_account_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); - - let (target_pub_key, falcon_auth) = get_new_pk_and_authenticator(); - - let target_account = get_account_with_basic_authenticated_wallet( - [10; 32], - AccountType::RegularAccountUpdatableCode, - AccountStorageMode::Private, - target_pub_key, - None, - ); - - // Create the note - let note = create_p2id_note( - sender_account_id, - target_account.id(), - vec![fungible_asset], - NoteType::Public, - Felt::new(0), - &mut RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])), - ) - .unwrap(); - - let tx_context = TransactionContextBuilder::new(target_account.clone()) - .extend_input_notes(vec![note]) - .authenticator(Some(falcon_auth)) - .build()?; - - let executed_transaction = tx_context.execute_blocking()?; - - Ok(executed_transaction.into()) -} diff --git a/crates/miden-block-prover/Cargo.toml b/crates/miden-block-prover/Cargo.toml index 8f20d95040..d774870d03 100644 --- a/crates/miden-block-prover/Cargo.toml +++ b/crates/miden-block-prover/Cargo.toml @@ -10,7 +10,7 @@ name = "miden-block-prover" readme = "README.md" repository.workspace = true rust-version.workspace = true -version = "0.11.5" +version.workspace = true [lib] bench = false diff --git a/crates/miden-block-prover/README.md b/crates/miden-block-prover/README.md index 4227291049..fe59be6504 100644 --- a/crates/miden-block-prover/README.md +++ b/crates/miden-block-prover/README.md @@ -4,4 +4,4 @@ This crate contains tools for executing and proving Miden blocks. ## License -This project is [MIT licensed](../LICENSE). +This project is [MIT licensed](../../LICENSE). diff --git a/crates/miden-block-prover/src/local_block_prover.rs b/crates/miden-block-prover/src/local_block_prover.rs index de55f3dd62..5637009e29 100644 --- a/crates/miden-block-prover/src/local_block_prover.rs +++ b/crates/miden-block-prover/src/local_block_prover.rs @@ -63,7 +63,7 @@ impl LocalBlockProver { /// /// This is exposed for testing purposes. #[cfg(any(feature = "testing", test))] - pub fn prove_without_batch_verification( + pub fn prove_dummy( &self, proposed_block: ProposedBlock, ) -> Result { @@ -158,7 +158,7 @@ impl LocalBlockProver { // Currently undefined and reserved for future use. // See miden-base/1155. let version = 0; - let tx_kernel_commitment = TransactionKernel::kernel_commitment(); + let tx_kernel_commitment = TransactionKernel.to_commitment(); // For now, we're not actually proving the block. let proof_commitment = Word::empty(); @@ -208,17 +208,13 @@ fn compute_nullifiers( let nullifiers: Vec = created_nullifiers.keys().copied().collect(); - let mut partial_nullifier_tree = PartialNullifierTree::new(); - // 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. - for witness in created_nullifiers.into_values() { - partial_nullifier_tree - .track_nullifier(witness) + 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. diff --git a/crates/miden-lib/Cargo.toml b/crates/miden-lib/Cargo.toml index a3c1ed5f97..288d2fb02b 100644 --- a/crates/miden-lib/Cargo.toml +++ b/crates/miden-lib/Cargo.toml @@ -10,7 +10,7 @@ name = "miden-lib" readme = "README.md" repository.workspace = true rust-version.workspace = true -version = "0.11.5" +version.workspace = true [lib] @@ -22,6 +22,7 @@ with-debug-info = ["miden-stdlib/with-debug-info"] [dependencies] # Miden dependencies +miden-core = { workspace = true } miden-objects = { workspace = true } miden-processor = { workspace = true } miden-stdlib = { workspace = true } @@ -31,7 +32,10 @@ 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 } regex = { version = "1.11" } walkdir = { version = "2.5" } diff --git a/crates/miden-lib/README.md b/crates/miden-lib/README.md index 37c37ec5c3..918fa2c761 100644 --- a/crates/miden-lib/README.md +++ b/crates/miden-lib/README.md @@ -8,4 +8,4 @@ At this point, all implementations listed above are considered to be experimenta ## License -This project is [MIT licensed](../LICENSE). +This project is [MIT licensed](../../LICENSE). diff --git a/crates/miden-lib/asm/account_components/basic_fungible_faucet.masm b/crates/miden-lib/asm/account_components/basic_fungible_faucet.masm index 238c6ea2ab..f436d1b541 100644 --- a/crates/miden-lib/asm/account_components/basic_fungible_faucet.masm +++ b/crates/miden-lib/asm/account_components/basic_fungible_faucet.masm @@ -2,5 +2,5 @@ # # See the `BasicFungibleFaucet` Rust type's documentation for more details. -export.::miden::contracts::faucets::basic_fungible::distribute -export.::miden::contracts::faucets::basic_fungible::burn +pub proc ::miden::contracts::faucets::basic_fungible::distribute +pub proc ::miden::contracts::faucets::basic_fungible::burn diff --git a/crates/miden-lib/asm/account_components/basic_wallet.masm b/crates/miden-lib/asm/account_components/basic_wallet.masm index 091a03c062..36fbc7d7b9 100644 --- a/crates/miden-lib/asm/account_components/basic_wallet.masm +++ b/crates/miden-lib/asm/account_components/basic_wallet.masm @@ -2,5 +2,5 @@ # # See the `BasicWallet` Rust type's documentation for more details. -export.::miden::contracts::wallets::basic::receive_asset -export.::miden::contracts::wallets::basic::move_asset_to_note +pub proc ::miden::contracts::wallets::basic::receive_asset +pub proc ::miden::contracts::wallets::basic::move_asset_to_note diff --git a/crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm b/crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm index 4631247258..92a0cf0a21 100644 --- a/crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm +++ b/crates/miden-lib/asm/account_components/multisig_rpo_falcon_512.masm @@ -1,8 +1,12 @@ # The MASM code of the Multi-Signature RPO Falcon 512 Authentication Component. +# +# See the `AuthRpoFalcon512Multisig` Rust type's documentation for more details. + +use miden::active_account +use miden::native_account +use miden::auth -use.miden::account -use.miden::auth -use.miden::tx +type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # CONSTANTS # ================================================================================================= @@ -10,35 +14,46 @@ use.miden::tx # Auth Request Constants # The event emitted when a signature is not found for a required signer. -const.UNAUTHORIZED_EVENT=131102 +const AUTH_UNAUTHORIZED_EVENT = event("miden::auth::unauthorized") # Storage Layout Constants # -# ┌─────────────────────────────┬──────────┬──────────────┐ -# │ THRESHOLD & APPROVERS │ PUB KEYS │ EXECUTED TXS │ -# │ (slot) │ (map) │ (map) │ -# ├─────────────────────────────┼──────────┼──────────────┤ -# │ 0 │ 1 │ 2 │ -# └─────────────────────────────┴──────────┴──────────────┘ - -# The slot in this component's storage layout where both the signature threshold -# and number of approvers are stored as [threshold, num_approvers, 0, 0]. +# ┌───────────────────────────────┬──────────┬──────────────┬───────────────────┐ +# │ THRESHOLD & APPROVERS CONFIG │ PUB KEYS │ EXECUTED TXS │ PROC THRESHOLDS │ +# │ (slot) │ (map) │ (map) │ (map) │ +# ├───────────────────────────────┼──────────┼──────────────┼───────────────────┤ +# │ 0 │ 1 │ 2 │ 3 │ +# └───────────────────────────────┴──────────┴──────────────┴───────────────────┘ + +# The slot in this component's storage layout where the default signature threshold and +# 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 = 0 # The slot in this component's storage layout where the public keys map is stored. -# Map entries: [key_index, 0, 0, 0] => owner_public_key -const.PUBLIC_KEYS_MAP_SLOT=1 +# Map entries: [key_index, 0, 0, 0] => APPROVER_PUBLIC_KEY +const PUBLIC_KEYS_MAP_SLOT = 1 # 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 = 2 + +# 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 # Executed Transaction Flag Constant -const.IS_EXECUTED_FLAG=[1, 0, 0, 0] +const IS_EXECUTED_FLAG = [1, 0, 0, 0] # ERRORS -const.ERR_TX_ALREADY_EXECUTED="failed to approve multisig transaction as it was already executed" +# ================================================================================================= + +const ERR_TX_ALREADY_EXECUTED = "failed to approve multisig transaction as it was already executed" + +const ERR_MALFORMED_MULTISIG_CONFIG = "number of approvers must be equal to or greater than threshold" + +const ERR_ZERO_IN_MULTISIG_CONFIG = "number of approvers or threshold must not be zero" #! Check if transaction has already been executed and add it to executed transactions for replay protection. #! @@ -47,7 +62,7 @@ const.ERR_TX_ALREADY_EXECUTED="failed to approve multisig transaction as it was #! #! Panics if: #! - the same transaction has already been executed -proc.assert_new_tx +proc assert_new_tx(msg: BeWord) push.IS_EXECUTED_FLAG # => [[0, 0, 0, is_executed], MSG] @@ -58,7 +73,7 @@ proc.assert_new_tx # => [index, MSG, IS_EXECUTED_FLAG] # Set the key value pair in the map to mark transaction as executed - exec.account::set_map_item + exec.native_account::set_map_item # => [OLD_MAP_ROOT, [0, 0, 0, is_executed]] dropw drop drop drop @@ -68,6 +83,245 @@ proc.assert_new_tx # => [] end +#! Remove old approver public keys from the approver public key mapping. +#! +#! This procedure cleans up the storage by removing public keys of approvers that are no longer +#! part of the multisig configuration. This procedure assumes that init_num_of_approvers and +#! new_num_of_approvers are u32 values. +#! +#! Inputs: [init_num_of_approvers, new_num_of_approvers] +#! Outputs: [] +#! +#! Where: +#! - init_num_of_approvers is the original number of approvers before the update +#! - new_num_of_approvers is the new number of approvers after the update +proc cleanup_pubkey_mapping(init_num_of_approvers: u32, new_num_of_approvers: u32) + dup.1 dup.1 + u32assert2 u32lt + # => [should_loop, i = init_num_of_approvers, new_num_of_approvers] + + while.true + # => [i, new_num_of_approvers] + + sub.1 + # => [i-1, new_num_of_approvers] + + dup + # => [i-1, i-1, new_num_of_approvers] + + push.0.0.0 + # => [[0, 0, 0, i-1], i-1, new_num_of_approvers] + + 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] + + exec.native_account::set_map_item + # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, new_num_of_approvers] + + dropw dropw + # => [i-1, new_num_of_approvers] + + dup.1 dup.1 + u32lt + # => [should_loop, i-1, new_num_of_approvers] + end + + drop drop + # => [] +end + +#! Update threshold config and add / remove approvers +#! +#! Inputs: +#! Operand stack: [MULTISIG_CONFIG_HASH, pad(12)] +#! Advice map: { +#! MULTISIG_CONFIG_HASH => [CONFIG, PUB_KEY_N, PUB_KEY_N-1, ..., PUB_KEY_0] +#! } +#! Outputs: +#! Operand stack: [] +#! +#! Where: +#! - MULTISIG_CONFIG_HASH is the hash of the threshold and new public key vector +#! - MULTISIG_CONFIG is [threshold, num_approvers, 0, 0] +#! - PUB_KEY_i is the public key of the i-th signer +#! +#! Locals: +#! 0: new_num_of_approvers +#! 1: init_num_of_approvers +pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord) + adv.push_mapval + # => [MULTISIG_CONFIG_HASH, pad(12)] + + adv_loadw + # => [MULTISIG_CONFIG, pad(12)] + + # store new_num_of_approvers for later + dup.2 loc_store.0 + # => [MULTISIG_CONFIG, pad(12)] + + dup.3 dup.3 + # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] + + # make sure that the threshold is smaller than the number of approvers + u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG + u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG + # => [MULTISIG_CONFIG, pad(12)] + + dup.3 dup.3 + # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] + + # make sure that threshold or num_approvers are not zero + eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG + eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG + # => [MULTISIG_CONFIG, pad(12)] + + push.THRESHOLD_CONFIG_SLOT + # => [slot, MULTISIG_CONFIG, pad(12)] + + exec.native_account::set_item + # => [OLD_THRESHOLD_CONFIG, pad(12)] + + # store init_num_of_approvers for later + drop drop loc_store.1 drop + # => [pad(12)] + + loc_load.0 + # => [num_approvers] + + dup neq.0 + while.true + sub.1 + # => [i-1, pad(12)] + + dup push.0.0.0 + # => [[0, 0, 0, i-1], i-1, pad(12)] + + padw adv_loadw + # => [PUB_KEY, [0, 0, 0, i-1], i-1, pad(12)] + + 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)] + + exec.native_account::set_map_item + # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, pad(12)] + + dropw dropw + # => [i-1, pad(12)] + + dup neq.0 + # => [is_non_zero, i-1, pad(12)] + end + # => [pad(13)] + + drop + # => [pad(12)] + + # compare initial vs current multisig config + + # load init_num_of_approvers & new_num_of_approvers + loc_load.0 loc_load.1 + # => [init_num_of_approvers, new_num_of_approvers, pad(12)] + + exec.cleanup_pubkey_mapping + # => [pad(12)] +end + +# Computes the effective transaction threshold based on called procedures and per-procedure +# overrides stored in PROC_THRESHOLD_ROOTS_SLOT. Falls back to default_threshold if no +# overrides apply. +# +#! Inputs: [default_threshold] +#! Outputs: [transaction_threshold] +proc compute_transaction_threshold.1(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 + # b. if called, get the override threshold of that procedure from the config map + # c. if proc_threshold > transaction_threshold, set transaction_threshold = proc_threshold + # 3. if transaction_threshold == 0 at the end, revert to using default_threshold + + # store default_threshold for later + loc_store.0 + # => [] + + # 1. initialize transaction_threshold = 0 + push.0 + # => [transaction_threshold] + + # get the number of account procedures + exec.active_account::get_num_procedures + # => [num_procedures, transaction_threshold] + + # 2. iterate through all account procedures + dup neq.0 + # => [should_continue, num_procedures, transaction_threshold] + while.true + sub.1 dup + # => [num_procedures-1, num_procedures-1, transaction_threshold] + + # get procedure root of the procedure with index i + exec.active_account::get_procedure_root dupw + # => [PROC_ROOT, PROC_ROOT, num_procedures-1, transaction_threshold] + + # 2a. check if this procedure has been called in the transaction + exec.native_account::was_procedure_called + # => [was_called, PROC_ROOT, num_procedures-1, transaction_threshold] + + # if it has been called, get the override threshold of that procedure + 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] + + # 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] + exec.active_account::get_initial_map_item + # => [[0, 0, 0, proc_threshold], num_procedures-1, transaction_threshold] + + drop drop drop dup dup.3 + # => [transaction_threshold, proc_threshold, proc_threshold, num_procedures-1, transaction_threshold] + + u32assert2.err="transaction threshold or procedure threshold are not u32" + u32gt + # => [is_gt, proc_threshold, num_procedures-1, transaction_threshold] + # 2c. if proc_threshold > transaction_threshold, update transaction_threshold + movup.2 movdn.3 + # => [is_gt, proc_threshold, transaction_threshold, num_procedures-1] + cdrop + # => [updated_transaction_threshold, num_procedures-1] + swap + # => [num_procedures-1, updated_transaction_threshold] + # if it has not been called during this transaction, nothing to do, move to the next procedure + else + dropw + # => [num_procedures-1, transaction_threshold] + end + + dup neq.0 + # => [should_continue, num_procedures-1, transaction_threshold] + end + + drop + # => [transaction_threshold] + + loc_load.0 + # => [default_threshold, transaction_threshold] + + # 3. if transaction_threshold == 0 at the end, revert to using default_threshold + dup.1 eq.0 + # => [is_zero, default_threshold, transaction_threshold] + + cdrop + # => [effective_transaction_threshold] +end + #! Authenticate a transaction using the Falcon signature scheme with multi-signature support. #! #! This procedure implements multi-signature authentication by: @@ -99,8 +353,8 @@ end #! - the same transaction has already been executed (replay protection). #! #! Invocation: call -export.auth__tx_rpo_falcon512_multisig - exec.account::incr_nonce drop +pub proc auth_tx_rpo_falcon512_multisig.1(salt: BeWord) + exec.native_account::incr_nonce drop # => [SALT] # ------ Computing transaction summary ------ @@ -116,37 +370,40 @@ export.auth__tx_rpo_falcon512_multisig exec.auth::hash_tx_summary # => [TX_SUMMARY_COMMITMENT] - # ------ Verifying owner signatures ------ + # ------ Verifying approver signatures ------ push.THRESHOLD_CONFIG_SLOT # => [index, TX_SUMMARY_COMMITMENT] - exec.account::get_item - # => [0, 0, num_of_approvers, threshold, TX_SUMMARY_COMMITMENT] + exec.active_account::get_initial_item + # => [0, 0, num_of_approvers, default_threshold, TX_SUMMARY_COMMITMENT] drop drop - # => [num_of_approvers, threshold, TX_SUMMARY_COMMITMENT] + # => [num_of_approvers, default_threshold, TX_SUMMARY_COMMITMENT] swap movdn.5 - # => [num_of_approvers, TX_SUMMARY_COMMITMENT, threshold] + # => [num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] push.PUBLIC_KEYS_MAP_SLOT - # => [pub_key_slot_idx, num_of_approvers, TX_SUMMARY_COMMITMENT, threshold] + # => [pub_key_slot_idx, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] exec.::miden::auth::rpo_falcon512::verify_signatures - # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, threshold] + # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, default_threshold] # ------ Checking threshold is >= num_verified_signatures ------ movup.5 - # => [threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] + # => [default_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] + + exec.compute_transaction_threshold + # => [transaction_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] u32assert2 u32lt # => [is_unauthorized, TX_SUMMARY_COMMITMENT] # If signatures are non-existent the tx will fail here. if.true - emit.UNAUTHORIZED_EVENT + emit.AUTH_UNAUTHORIZED_EVENT push.0 assert.err="insufficient number of signatures" end diff --git a/crates/miden-lib/asm/account_components/network_fungible_faucet.masm b/crates/miden-lib/asm/account_components/network_fungible_faucet.masm new file mode 100644 index 0000000000..b88db104ce --- /dev/null +++ b/crates/miden-lib/asm/account_components/network_fungible_faucet.masm @@ -0,0 +1,6 @@ +# 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/account_components/no_auth.masm b/crates/miden-lib/asm/account_components/no_auth.masm index b1476fb769..d651f68179 100644 --- a/crates/miden-lib/asm/account_components/no_auth.masm +++ b/crates/miden-lib/asm/account_components/no_auth.masm @@ -1,5 +1,6 @@ -use.miden::account -use.std::word +use miden::active_account +use miden::native_account +use std::word #! Increment the nonce only if the account commitment has changed #! @@ -11,13 +12,13 @@ use.std::word #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] -export.auth__no_auth +pub proc auth_no_auth # check if the account state has changed by comparing initial and final commitments - exec.account::get_initial_commitment + exec.active_account::get_initial_commitment # => [INITIAL_COMMITMENT, pad(16)] - exec.account::compute_current_commitment + exec.active_account::compute_commitment # => [CURRENT_COMMITMENT, INITIAL_COMMITMENT, pad(16)] exec.word::eq not @@ -25,6 +26,6 @@ export.auth__no_auth # if the account has been updated, increment the nonce if.true - exec.account::incr_nonce drop + exec.native_account::incr_nonce drop end end diff --git a/crates/miden-lib/asm/account_components/rpo_falcon_512.masm b/crates/miden-lib/asm/account_components/rpo_falcon_512.masm index 3bd833aca5..7e68c230ee 100644 --- a/crates/miden-lib/asm/account_components/rpo_falcon_512.masm +++ b/crates/miden-lib/asm/account_components/rpo_falcon_512.masm @@ -2,14 +2,16 @@ # # See the `AuthRpoFalcon512` Rust type's documentation for more details. -use.miden::auth::rpo_falcon512 -use.miden::account +use miden::auth::rpo_falcon512 +use miden::active_account + +type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # CONSTANTS # ================================================================================================= # The slot in this component's storage layout where the public key is stored. -const.PUBLIC_KEY_SLOT=0 +const PUBLIC_KEY_SLOT = 0 #! Authenticate a transaction using the Falcon signature scheme. #! @@ -26,13 +28,13 @@ const.PUBLIC_KEY_SLOT=0 #! Outputs: [pad(16)] #! #! Invocation: call -export.auth__tx_rpo_falcon512 +pub proc auth_tx_rpo_falcon512(auth_args: BeWord) dropw # => [pad(16)] # Fetch public key from storage. # --------------------------------------------------------------------------------------------- - push.PUBLIC_KEY_SLOT exec.account::get_item + push.PUBLIC_KEY_SLOT exec.active_account::get_item # => [PUB_KEY, pad(16)] exec.rpo_falcon512::authenticate_transaction diff --git a/crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm b/crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm index 609f9b3f4b..f349738d84 100644 --- a/crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm +++ b/crates/miden-lib/asm/account_components/rpo_falcon_512_acl.masm @@ -2,21 +2,24 @@ # # See the `AuthRpoFalcon512Acl` Rust type's documentation for more details. -use.miden::account -use.miden::tx -use.std::word +use miden::active_account +use miden::native_account +use miden::tx +use std::word + +type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } # CONSTANTS # ================================================================================================ # The slot in this component's storage layout where the public key is stored. -const.PUBLIC_KEY_SLOT=0 +const PUBLIC_KEY_SLOT = 0 # The slot where the authentication configuration is stored. -const.AUTH_CONFIG_SLOT=1 +const AUTH_CONFIG_SLOT = 1 # 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 = 2 #! Authenticate a transaction using the Falcon signature scheme based on procedure calls and note usage. #! @@ -32,13 +35,13 @@ const.AUTH_TRIGGER_PROCS_MAP_SLOT=2 #! Outputs: [pad(16)] #! #! Invocation: call -export.auth__tx_rpo_falcon512_acl.2 +pub proc auth_tx_rpo_falcon512_acl.2(auth_args: BeWord) dropw # => [pad(16)] # Get the authentication configuration - push.AUTH_CONFIG_SLOT exec.account::get_item - # => [0, num_auth_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, pad(16)] + push.AUTH_CONFIG_SLOT exec.active_account::get_item + # => [0, allow_unauthorized_input_notes, allow_unauthorized_output_notes, num_auth_trigger_procs, pad(16)] drop # => [allow_unauthorized_input_notes, allow_unauthorized_output_notes, num_auth_trigger_procs, pad(16)] @@ -62,10 +65,10 @@ export.auth__tx_rpo_falcon512_acl.2 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)] - exec.account::get_map_item + exec.active_account::get_map_item # => [AUTH_TRIGGER_PROC_ROOT, require_acl_auth, i, pad(16)] - exec.account::was_procedure_called + exec.native_account::was_procedure_called # => [was_called, require_acl_auth, i, pad(16)] # Update require_acl_auth @@ -121,24 +124,24 @@ export.auth__tx_rpo_falcon512_acl.2 # If authentication is required, perform signature verification if.true # Fetch public key from storage. - push.PUBLIC_KEY_SLOT exec.account::get_item + push.PUBLIC_KEY_SLOT exec.active_account::get_item # => [PUB_KEY, pad(16)] exec.::miden::auth::rpo_falcon512::authenticate_transaction else # ------ Check if initial account commitment differs from current commitment ------ - exec.account::get_initial_commitment + exec.active_account::get_initial_commitment # => [INITIAL_COMMITMENT, pad(16)] - exec.account::compute_current_commitment + exec.active_account::compute_commitment # => [CURRENT_COMMITMENT, INITIAL_COMMITMENT, pad(16)] exec.word::eq not # => [has_account_state_changed, pad(16)] if.true - exec.account::incr_nonce drop + exec.native_account::incr_nonce drop end end # => [pad(16)] diff --git a/crates/miden-lib/asm/kernels/transaction/api.masm b/crates/miden-lib/asm/kernels/transaction/api.masm index c9061019b4..48b764e071 100644 --- a/crates/miden-lib/asm/kernels/transaction/api.masm +++ b/crates/miden-lib/asm/kernels/transaction/api.masm @@ -1,10 +1,6 @@ -use.std::sys - use.$kernel::account use.$kernel::account_delta use.$kernel::account_id -use.$kernel::asset_vault -use.$kernel::constants use.$kernel::faucet use.$kernel::input_note use.$kernel::memory @@ -32,6 +28,18 @@ const.ERR_FAUCET_IS_NF_ASSET_ISSUED_PROC_CAN_ONLY_BE_CALLED_ON_NON_FUNGIBLE_FAUC 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_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_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_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROCESSED="failed to access note serial number of active note because no note is currently being processed" + # AUTHENTICATION # ================================================================================================= @@ -78,9 +86,10 @@ end # KERNEL PROCEDURES # ================================================================================================= -### ACCOUNT ##################################### +# ACCOUNT +# ------------------------------------------------------------------------------------------------- -#! Returns the native account commitment at the beginning of the transaction. +#! Returns the active account commitment at the beginning of the transaction. #! #! Inputs: [pad(16)] #! Outputs: [INIT_COMMITMENT, pad(12)] @@ -99,7 +108,7 @@ export.account_get_initial_commitment # => [INIT_COMMITMENT, pad(12)] end -#! Computes the account commitment of the current account. +#! Computes commitment to the state of the active account. #! #! Inputs: [pad(16)] #! Outputs: [ACCOUNT_COMMITMENT, pad(12)] @@ -108,9 +117,9 @@ end #! - ACCOUNT_COMMITMENT is the commitment of the account data. #! #! Invocation: dynexec -export.account_compute_current_commitment - # compute the current account commitment - exec.account::compute_current_commitment +export.account_compute_commitment + # compute the active account commitment + exec.account::compute_commitment # => [ACCOUNT_COMMITMENT, pad(16)] # truncate the stack @@ -120,6 +129,15 @@ end #! Computes the commitment to the native account's delta. #! +#! The commitment to an empty delta is defined as the empty word. +#! +#! During an account-creating transaction (when the initial nonce is 0), this procedure may not +#! return the empty word even if the initial storage commitment and the current storage commitment +#! are identical (storage hasn't changed). This is because the delta for a new account must +#! represent its entire newly created state, and the initial storage in a transaction is +#! initialized to the the storage that the account ID commits to, which may be non-empty. This +#! does not have any consequences other than being inconsistent in this edge case. +#! #! Inputs: [pad(16)] #! Outputs: [DELTA_COMMITMENT, pad(12)] #! @@ -140,33 +158,61 @@ export.account_compute_delta_commitment # => [ACCOUNT_COMMITMENT, pad(12)] end -#! Returns the account ID of the current account. +#! Returns the ID of the specified account. #! -#! Inputs: [pad(16)] +#! Inputs: [is_native, pad(15)] #! Outputs: [account_id_prefix, account_id_suffix, pad(14)] #! #! Where: -#! - account_id_{prefix,suffix} are the prefix and suffix felts of the account ID of the currently -#! accessing account. +#! - is_native is a boolean flag that indicates whether the account ID was requested for the native +#! or the active account. +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. #! #! Invocation: dynexec export.account_get_id - # get the account ID - exec.account::get_id - # => [account_id_prefix, account_id_suffix, pad(16)] + # get the native account ID + exec.memory::get_native_account_id + # => [native_account_id_prefix, native_account_id_suffix, is_native, pad(15)] - # truncate the stack - movup.2 drop movup.2 drop + # get the active account ID + exec.account::get_id + # => [ + # active_account_id_prefix, active_account_id_suffix, + # native_account_id_prefix, native_account_id_suffix, + # is_native, pad(15) + # ] + + # prepare the stack for the first cdrop + movup.2 dup.4 + # => [ + # is_native, native_account_id_prefix, active_account_id_prefix, + # active_account_id_suffix, native_account_id_suffix, is_native, pad(15) + # ] + + # drop the prefix corresponding to the is_native flag + cdrop + # => [account_id_prefix, active_account_id_suffix, native_account_id_suffix, is_native, pad(15)] + + # prepare the stack for the second cdrop + movdn.3 swap movup.2 + # => [is_native, native_account_id_suffix, active_account_id_suffix, account_id_prefix, pad(15)] + + # drop the suffix corresponding to the is_native flag + cdrop + # => [account_id_suffix, account_id_prefix, pad(15)] + + # rearrange the ID parts and truncate the stack + swap movup.2 drop # => [account_id_prefix, account_id_suffix, pad(14)] end -#! Returns the nonce of the current account. +#! Returns the active account nonce. #! #! Inputs: [pad(16)] #! Outputs: [nonce, pad(15)] #! #! Where: -#! - nonce is the current account's nonce. +#! - nonce is the active account's nonce. #! #! Invocation: dynexec export.account_get_nonce @@ -213,7 +259,7 @@ export.account_incr_nonce # => [final_nonce, pad(15)] end -#! Gets the account code commitment of the current account. +#! Gets the account code commitment of the active account. #! #! Inputs: [pad(16)] #! Outputs: [CODE_COMMITMENT, pad(12)] @@ -236,7 +282,7 @@ export.account_get_code_commitment # => [CODE_COMMITMENT, pad(12)] end -#! Returns the storage commitment of the native account at the beginning of the transaction. +#! Returns the storage commitment of the active account at the beginning of the transaction. #! #! Inputs: [pad(16)] #! Outputs: [INIT_STORAGE_COMMITMENT, pad(12)] @@ -255,7 +301,7 @@ export.account_get_initial_storage_commitment # => [INIT_STORAGE_COMMITMENT, pad(12)] end -#! Computes the latest account storage commitment of the current account. +#! Computes the latest account storage commitment of the active account. #! #! Inputs: [pad(16)] #! Outputs: [STORAGE_COMMITMENT, pad(12)] @@ -378,7 +424,68 @@ export.account_get_map_item # => [VALUE, pad(12)] end -#! Stores NEW_VALUE under the specified KEY within the map contained in the given account storage slot. +#! Gets an item from the account storage at its initial state (beginning of transaction). +#! +#! Inputs: [index, pad(15)] +#! Outputs: [INIT_VALUE, pad(12)] +#! +#! Where: +#! - index is the index of the item to get. +#! - INIT_VALUE is the initial value of the item at the beginning of the transaction. +#! +#! Panics if: +#! - the index is out of bounds. +#! +#! Invocation: dynexec +export.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)] + + # fetch the initial account storage item + exec.account::get_initial_item + # => [INIT_VALUE, pad(15)] + + # truncate the stack + movup.4 drop movup.4 drop movup.4 drop + # => [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. +#! +#! Inputs: [index, KEY, pad(11)] +#! Outputs: [INIT_VALUE, pad(12)] +#! +#! Where: +#! - index is the index of the storage slot that contains the map root. +#! - 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). +#! - the requested storage slot type is not map. +#! +#! Invocation: dynexec +export.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)] + + # fetch the initial map item from account storage + exec.account::get_initial_map_item + # => [INIT_VALUE, pad(12)] +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)] @@ -415,7 +522,7 @@ export.account_set_map_item # => [OLD_MAP_ROOT, OLD_VALUE, pad(8)] end -#! Returns the vault root of the native account at the beginning of the transaction. +#! Returns the vault root of the active account at the beginning of the transaction. #! #! Inputs: [pad(16)] #! Outputs: [INIT_VAULT_ROOT, pad(12)] @@ -434,7 +541,7 @@ export.account_get_initial_vault_root # => [INIT_VAULT_ROOT, pad(12)] end -#! Returns the vault root of the current account. +#! Returns the vault root of the active account. #! #! Inputs: [pad(16)] #! Outputs: [VAULT_ROOT, pad(12)] @@ -445,7 +552,7 @@ end #! Invocation: dynexec export.account_get_vault_root # fetch the account vault root - exec.memory::get_acct_vault_root + exec.memory::get_account_vault_root # => [VAULT_ROOT, pad(16)] # truncate the stack @@ -516,7 +623,8 @@ export.account_remove_asset # => [ASSET, pad(12)] end -#! Returns the balance of the fungible asset associated with the provided faucet_id in the current account's vault. +#! 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, pad(14)] #! Outputs: [balance, pad(15)] @@ -527,20 +635,36 @@ end #! - balance is the vault balance of the fungible asset. #! #! Panics if: -#! - the asset is not a fungible asset. +#! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: dynexec export.account_get_balance - # get the vault root - exec.memory::get_acct_vault_root_ptr movdn.2 - # => [faucet_id_prefix, faucet_id_suffix, acct_vault_root_ptr, pad(14)] - - # get the asset balance - exec.asset_vault::get_balance + exec.account::get_balance # => [balance, pad(15)] end -#! Returns a boolean indicating whether the non-fungible asset is present in the current account's vault. +#! 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, pad(14)] +#! Outputs: [init_balance, pad(15)] +#! +#! Where: +#! - faucet_id_{prefix,suffix} are the prefix and suffix felts of the faucet id of the fungible +#! asset of interest. +#! - init_balance is the vault balance of the fungible asset at the beginning of the transaction. +#! +#! Panics if: +#! - the provided faucet ID is not an ID of a fungible faucet. +#! +#! Invocation: dynexec +export.account_get_initial_balance + exec.account::get_initial_balance + # => [init_balance, pad(15)] +end + +#! Returns a boolean indicating whether the non-fungible asset is present in the active account's +#! vault. #! #! Inputs: [ASSET, pad(12)] #! Outputs: [has_asset, pad(15)] @@ -554,16 +678,99 @@ end #! #! Invocation: dynexec export.account_has_non_fungible_asset - # get the vault root - exec.memory::get_acct_vault_root_ptr movdn.4 - # => [ASSET, vault_root_ptr, pad(12)] - - # check if the account vault has the non-fungible asset - exec.asset_vault::has_non_fungible_asset + exec.account::has_non_fungible_asset # => [has_asset, pad(15)] end -### FAUCET ##################################### +#! Returns 1 if a native account procedure was called during transaction execution, and 0 otherwise. +#! +#! Inputs: [PROC_ROOT, pad(12)] +#! Outputs: [was_called, pad(15)] +#! +#! Where: +#! - PROC_ROOT is the hash of the procedure to check. +#! - was_called is 1 if the procedure was called at least once during tx execution, 0 otherwise. +#! +#! Panics if: +#! - the procedure root is not part of the account code. +#! +#! Invocation: dynexec +export.account_was_procedure_called + # check that this procedure was executed against the native account + exec.memory::assert_native_account + # => [PROC_ROOT, pad(12)] + + # check if the procedure was called + exec.account::was_procedure_called + # => [was_called, pad(15)] +end + +#! Returns the number of procedures in the active account. +#! +#! Inputs: [pad(16)] +#! Outputs: [num_procedures, pad(15)] +#! +#! Where: +#! - num_procedures is the number of procedures in the active account. +#! +#! Invocation: dynexec +export.account_get_num_procedures + # get the number of procedures + exec.memory::get_num_account_procedures + # => [num_procedures, pad(16)] + + # truncate the stack + swap drop + # => [num_procedures, pad(15)] +end + +#! Returns the procedure root for the active account procedure at the specified index. +#! +#! Inputs: [index, pad(15)] +#! Outputs: [PROC_ROOT, pad(12)] +#! +#! Where: +#! - index is the index of the procedure. +#! - PROC_ROOT is the hash of the procedure. +#! +#! Panics if: +#! - 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)] + + movup.4 drop + # => [PROC_ROOT, pad(12)] +end + +#! Returns the binary flag indicating whether the procedure with the provided root is available on +#! the active account. +#! +#! Returns 1 if the procedure is available on the active account and 0 otherwise. +#! +#! Inputs: [PROC_ROOT, pad(12)] +#! Outputs: [is_procedure_available, pad(15)] +#! +#! Where: +#! - 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. +#! +#! Invocation: dynexec +export.account_has_procedure + # check if the procedure was called + exec.account::has_procedure + # => [is_procedure_available, pad(15)] +end + +# FAUCET +# ------------------------------------------------------------------------------------------------- #! Mint an asset from the faucet the transaction is being executed against. #! @@ -692,149 +899,295 @@ export.faucet_is_non_fungible_asset_issued # => [is_issued, pad(15)] end -### NOTE ######################################## +# INPUT NOTE +# ------------------------------------------------------------------------------------------------- -#! Returns the number of assets and the assets commitment of the note currently being processed. +#! Returns the assets information of the specified input note. #! -#! Inputs: [pad(16)] +#! Inputs: [is_active_note, note_index, pad(14)] #! Outputs: [ASSETS_COMMITMENT, num_assets, pad(11)] #! #! Where: -#! - num_assets is the number of assets in the note currently being processed. -#! - ASSETS_COMMITMENT is a sequential hash of the assets in the note currently being processed. +#! - is_active_note is the boolean flag indicating whether we should return the assets data from +#! the active note or from the note with the specified index. +#! - note_index is the index of the input note whose assets info should be returned. Notice that if +#! is_active_note is 1, note_index is ignored. +#! - ASSETS_COMMITMENT is a sequential hash of the assets in the specified input note. +#! - num_assets is the number of assets in the specified input note. #! #! Panics if: -#! - a note is not being processed. +#! - the note index is greater or equal to the total number of input notes. +#! - is_active_note is 1 and no input note is not being processed (attempted to access note inputs +#! from incorrect context). #! #! Invocation: dynexec -export.note_get_assets_info +export.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 + # => [input_note_ptr, pad(15)] + + # assert the pointer is not zero - this would suggest the procedure has been called from an + # incorrect context + dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_WHILE_NO_NOTE_BEING_PROCESSED + # => [input_note_ptr, pad(15)] + # get the assets info - exec.note::get_assets_info - # => [ASSETS_COMMITMENT, num_assets, pad(16)] + exec.input_note::get_assets_info + # => [ASSETS_COMMITMENT, num_assets, pad(15)] # truncate the stack - repeat.5 - movup.5 drop - end + movupw.2 dropw # => [ASSETS_COMMITMENT, num_assets, pad(11)] end -#! Adds the ASSET to the note specified by the index. +#! Returns the recipient of the specified input note. #! -#! Inputs: [note_idx, ASSET, pad(11)] -#! Outputs: [note_idx, ASSET, pad(11)] +#! Inputs: [is_active_note, note_index, pad(15)] +#! Outputs: [RECIPIENT, pad(12)] #! #! Where: -#! - note_idx is the index of the note to which the asset is added. -#! - ASSET can be a fungible or non-fungible asset. +#! - is_active_note is the boolean flag indicating whether we should return the assets data from +#! the active note or from the note with the specified index. +#! - note_index is the index of the input note whose assets info should be returned. Notice that if +#! is_active_note is 1, note_index is ignored. +#! - RECIPIENT is the commitment to the input note's script, inputs, the serial number. #! #! Panics if: -#! - the procedure is called when the current account is not the native one. +#! - the note index is greater or equal to the total number of input notes. +#! - is_active_note is 1 and no input note is not being processed (attempted to access note inputs +#! from incorrect context). #! #! Invocation: dynexec -export.note_add_asset - # check that this procedure was executed against the native account - exec.memory::assert_native_account - # => [note_idx, ASSET, pad(11)] +export.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 + # => [input_note_ptr, pad(15)] - # duplicate the asset word to be able to return it - movdn.4 dupw movup.8 - # => [note_idx, ASSET, ASSET, pad(11)] + # assert the pointer is not zero - this would suggest the procedure has been called from an + # incorrect context + dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_RECIPIENT_WHILE_NO_NOTE_BEING_PROCESSED + # => [input_note_ptr, pad(15)] - exec.tx::add_asset_to_note - # => [note_idx, ASSET, pad(11)] + # get the recipient + exec.memory::get_input_note_recipient + # => [RECIPIENT, pad(15)] + + # truncate the stack + swapw drop drop drop movdn.4 + # => [RECIPIENT, pad(12)] end -#! Returns the information about assets in the input note with the specified index. +#! Returns the metadata of the specified input note. #! -#! Inputs: [note_index, pad(15)] -#! Outputs: [ASSETS_COMMITMENT, num_assets, pad(11)] +#! Inputs: [is_active_note, note_index, pad(14)] +#! Outputs: [METADATA, pad(12)] #! #! Where: -#! - note_index is the index of the input 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. +#! - 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. #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. +#! - is_active_note is 1 and no input note is not being processed (attempted to access note inputs +#! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_assets_info - # assert that the provided note index is less than the total number of input notes - exec.input_note::assert_note_index_in_bounds - # => [note_index, pad(15)] +export.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 + # => [input_note_ptr, pad(15)] - # get the assets info - exec.input_note::get_assets_info - # => [ASSETS_COMMITMENT, num_assets, pad(16)] + # assert the pointer is not zero - this would suggest the procedure has been called from an + # incorrect context + dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED + # => [input_note_ptr, pad(15)] + + # get the metadata + exec.memory::get_input_note_metadata + # => [METADATA, pad(15)] # truncate the stack - repeat.5 - movup.5 drop - end - # => [ASSETS_COMMITMENT, num_assets, pad(11)] + swapw drop drop drop movdn.4 + # => [METADATA, pad(12)] end -#! Returns the recipient of the input note with the specified index. +#! Returns the serial number of the specified input note. #! -#! Inputs: [note_index, pad(15)] -#! Outputs: [RECIPIENT, pad(12)] +#! Inputs: [is_active_note, note_index, pad(14)] +#! Outputs: [SERIAL_NUMBER, pad(12)] #! #! Where: -#! - note_index is the index of the input note whose recipient should be returned. -#! - RECIPIENT is the commitment to the input note's script, inputs, the serial number. +#! - is_active_note is the boolean flag indicating whether we should return the serial number +#! of the active note or of the note with the specified index. +#! - note_index is the index of the input note whose serial number should be returned. Notice that +#! if is_active_note is 1, note_index is ignored. +#! - SERIAL_NUMBER is the serial number of the specified input note. #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. +#! - is_active_note is 1 and no input note is not being processed (attempted to access note inputs +#! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_recipient - # assert that the provided note index is less than the total number of input notes - exec.input_note::assert_note_index_in_bounds - # => [note_index, pad(15)] +export.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 + # => [input_note_ptr, pad(15)] - # get the note data pointer by the provided index - exec.memory::get_input_note_ptr - # => [note_ptr, pad(15)] + # assert the pointer is not zero - this would suggest the procedure has been called from an + # incorrect context + dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROCESSED + # => [input_note_ptr, pad(15)] - # get the recipient - exec.memory::get_input_note_recipient - # => [RECIPIENT, pad(15)] + # get the serial number using the note pointer + exec.memory::get_input_note_serial_num + # => [SERIAL_NUMBER, pad(15)] # truncate the stack - swapw drop drop drop movdn.4 - # => [RECIPIENT, pad(12)] + repeat.3 + movup.4 drop + end + # => [SERIAL_NUMBER, pad(12)] end -#! Returns the metadata of the input note with the specified index. +#! Returns the inputs commitment and length of the specified input note. #! -#! Inputs: [note_index, pad(15)] -#! Outputs: [METADATA, pad(12)] +#! Inputs: [is_active_note, note_index, pad(14)] +#! Outputs: [NOTE_INPUTS_COMMITMENT, num_inputs, pad(11)] #! #! Where: -#! - note_index is the index of the input note whose metadata should be returned. -#! - METADATA is the metadata of the input note. +#! - is_active_note is the boolean flag indicating whether we should return the inputs commitment +#! and length from the active note or from the note with the specified index. +#! - note_index is the index of the input note whose data should be returned. Notice that if +#! is_active_note is 1, note_index is ignored. +#! - NOTE_INPUTS_COMMITMENT is the inputs commitment of the specified input note. +#! - num_inputs is the number of inputs of the specified input note. #! #! Panics if: #! - the note index is greater or equal to the total number of input notes. +#! - is_active_note is 1 and no input note is not being processed (attempted to access note inputs +#! from incorrect context). #! #! Invocation: dynexec -export.input_note_get_metadata - # assert that the provided note index is less than the total number of input notes - exec.input_note::assert_note_index_in_bounds - # => [note_index, pad(15)] +export.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 + # => [input_note_ptr, pad(15)] + + # assert the pointer is not zero - this would suggest the procedure has been called from an + # incorrect context + dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_WHILE_NO_NOTE_BEING_PROCESSED + # => [input_note_ptr, pad(15)] + + # get the note inputs length + dup exec.memory::get_input_note_num_inputs swap + # => [input_note_ptr, num_inputs, pad(16)] + + # get the inputs commitment + exec.memory::get_input_note_inputs_commitment + # => [NOTE_INPUTS_COMMITMENT, num_inputs, pad(16)] - # get the note data pointer by the provided index - exec.memory::get_input_note_ptr - # => [note_ptr, pad(15)] + # truncate the stack + repeat.5 + movup.5 drop + end + # => [NOTE_INPUTS_COMMITMENT, num_inputs, pad(11)] +end - # get the metadata - exec.memory::get_input_note_metadata - # => [METADATA, pad(15)] +#! Returns the script root of the specified input note. +#! +#! Inputs: [is_active_note, note_index, pad(14)] +#! Outputs: [SCRIPT_ROOT, pad(12)] +#! +#! Where: +#! - is_active_note is the boolean flag indicating whether we should return the inputs commitment +#! and length from the active note or from the note with the specified index. +#! - note_index is the index of the input note whose data should be returned. Notice that if +#! is_active_note is 1, note_index is ignored. +#! - SCRIPT_ROOT is the script root of the specified input note. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! - is_active_note is 1 and no input note is not being processed (attempted to access note inputs +#! from incorrect context). +#! +#! Invocation: dynexec +export.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 + # => [input_note_ptr, pad(15)] + + # assert the pointer is not zero - this would suggest the procedure has been called from an + # incorrect context + dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SCRIPT_ROOT_WHILE_NO_NOTE_BEING_PROCESSED + # => [input_note_ptr, pad(15)] + + # get the script root using the note pointer + exec.memory::get_input_note_script_root + # => [SCRIPT_ROOT, pad(15)] # truncate the stack - swapw drop drop drop movdn.4 - # => [METADATA, pad(12)] + repeat.3 + movup.4 drop + end + # => [SCRIPT_ROOT, pad(12)] +end + +# OUTPUT NOTE +# ------------------------------------------------------------------------------------------------- + +#! Creates a new note and returns its index. +#! +#! Inputs: [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] +#! 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 + # check that this procedure was executed against the native account + exec.memory::assert_native_account + # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] + + exec.output_note::create + # => [note_idx, pad(15)] +end + +#! Adds the ASSET to the note specified by the index. +#! +#! Inputs: [note_idx, ASSET, pad(11)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - note_idx is the index of the note to which the asset is added. +#! - ASSET can be a fungible or non-fungible asset. +#! +#! Panics if: +#! - the procedure is called when the active account is not the native one. +#! +#! Invocation: dynexec +export.output_note_add_asset + # check that this procedure was executed against the native account + exec.memory::assert_native_account + # => [note_idx, ASSET, pad(11)] + + exec.output_note::add_asset + # => [pad(16)] end #! Returns the information about assets in the output note with the specified index. @@ -929,124 +1282,16 @@ export.output_note_get_metadata # => [METADATA, pad(12)] end -#! Returns the serial number of the note currently being processed. -#! -#! Inputs: [pad(16)] -#! Outputs: [SERIAL_NUMBER, pad(12)] -#! -#! Where: -#! - SERIAL_NUMBER is the serial number of the note currently being processed. -#! -#! Panics if: -#! - no note is not being processed. -#! -#! Invocation: dynexec -export.note_get_serial_number - exec.note::get_serial_number - # => [SERIAL_NUMBER, pad(16)] - - # truncate the stack - swapw dropw - # => [SERIAL_NUMBER, pad(12)] -end - -#! Returns the current note's inputs commitment and length. -#! -#! Inputs: [pad(16)] -#! Outputs: [NOTE_INPUTS_COMMITMENT, num_inputs, pad(11)] -#! -#! Where: -#! - NOTE_INPUTS_COMMITMENT is the current note's inputs commitment. -#! - num_inputs is the number of input values of the current note. -#! -#! Invocation: dynexec -export.note_get_inputs_commitment_and_len - exec.memory::get_input_note_num_inputs - # => [num_inputs, pad(16)] - - exec.note::get_note_inputs_commitment - # => [NOTE_INPUTS_COMMITMENT, num_inputs, pad(16)] - - # truncate the stack - swapdw dropw dropw - # => [NOTE_INPUTS_COMMITMENT, num_inputs, pad(11)] -end - -#! Returns the sender of the note currently being processed. -#! -#! Inputs: [pad(16)] -#! Outputs: [sender_id_prefix, sender_id_suffix, pad(14)] -#! -#! Where: -#! - sender_{prefix,suffix} are the prefix and suffix felts of the sender account ID of the note -#! currently being processed. -#! -#! Panics if: -#! - a note is not being processed. -#! -#! Invocation: dynexec -export.note_get_sender - exec.note::get_sender - # => [sender_id_prefix, sender_id_suffix, pad(16)] - - # truncate the stack - movup.2 drop movup.2 drop - # => [sender_id_prefix, sender_id_suffix, pad(14)] -end - -#! Returns the script root of the note currently being processed. -#! -#! Inputs: [pad(16)] -#! Outputs: [script_root, pad(12)] -#! -#! Where: -#! - SCRIPT_ROOT is the script root of the note currently being processed. -#! -#! Panics if: -#! - no note is not being processed. -#! -#! Invocation: dynexec -export.note_get_script_root - exec.note::get_script_root - # => [SCRIPT_ROOT, pad(16)] - - # truncate the stack - swapw dropw - # => [SCRIPT_ROOT, pad(12)] -end - -### TRANSACTION ################################# - -#! Creates a new note and returns the index of the note. -#! -#! Inputs: [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] -#! 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.tx_create_note - # check that this procedure was executed against the native account - exec.memory::assert_native_account - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] - - exec.tx::create_note - # => [note_idx, pad(15)] -end +# TRANSACTION +# ------------------------------------------------------------------------------------------------- #! Returns the input notes commitment. #! -#! This is computed as a sequential hash of `(NULLIFIER, EMPTY_WORD_OR_NOTE_COMMITMENT)` over all input -#! notes. The data `EMPTY_WORD_OR_NOTE_COMMITMENT` functions as a flag, if the value is set to zero, then -#! the notes are authenticated by the transaction kernel. If the value is non-zero, then note -#! authentication will be delayed to the batch/block kernel. The delayed authentication allows a -#! transaction to consume a public note that is not yet included to a block. +#! This is computed as a sequential hash of `(NULLIFIER, EMPTY_WORD_OR_NOTE_COMMITMENT)` over all +#! input notes. The data `EMPTY_WORD_OR_NOTE_COMMITMENT` functions as a flag, if the value is set to +#! zero, then the notes are authenticated by the transaction kernel. If the value is non-zero, then +#! note authentication will be delayed to the batch/block kernel. The delayed authentication allows +#! a transaction to consume a public note that is not yet included to a block. #! #! Inputs: [pad(16)] #! Outputs: [INPUT_NOTES_COMMITMENT, pad(12)] @@ -1180,7 +1425,7 @@ end #! #! This allows calling procedures on an account different from the native account. It loads the #! foreign account into memory, unless already loaded. It pushes the foreign account onto the -#! account stack, which makes the foreign account the current account. +#! account stack, which makes the foreign account the active account. #! #! Inputs: #! Operand stack: [foreign_account_id_prefix, foreign_account_id_suffix, pad(14)] @@ -1223,67 +1468,25 @@ export.tx_start_foreign_context exec.memory::push_ptr_to_account_stack # OS => [foreign_account_id_prefix, foreign_account_id_suffix, pad(14)] - # construct the word with account ID to load the core account data from the advice map - push.0.0 - # OS => [0, 0, foreign_account_id_prefix, foreign_account_id_suffix, pad(14)] - - # move the core account data to the advice stack - adv.push_mapval - # OS => [0, 0, foreign_account_id_prefix, foreign_account_id_suffix, pad(14)] - # AS => [[foreign_account_id_prefix, foreign_account_id_suffix, 0, account_nonce], VAULT_ROOT, STORAGE_ROOT, CODE_ROOT] - - # store the id and nonce of the foreign account to the memory - dropw adv_loadw - exec.memory::set_acct_id_and_nonce dropw - # OS => [pad(16)] - # AS => [VAULT_ROOT, STORAGE_ROOT, CODE_ROOT] - - # store the vault root of the foreign account to the memory - adv_loadw exec.memory::set_acct_vault_root dropw - # OS => [pad(16)] - # AS => [STORAGE_ROOT, CODE_ROOT] - - # move the storage root and the code root to the operand stack - adv_loadw padw adv_loadw - # OS => [CODE_ROOT, STORAGE_ROOT, pad(16)] - # AS => [] - - # store the code root into the memory - exec.memory::set_acct_code_commitment - # OS => [CODE_ROOT, STORAGE_ROOT, pad(16)] - # AS => [] - - # save the account procedure data into the memory - exec.account::save_account_procedure_data - # OS => [STORAGE_ROOT, pad(16)] - # AS => [] - - # store the storage root to the memory - exec.memory::set_acct_storage_commitment - # OS => [STORAGE_ROOT, pad(16)] - # AS => [] - - # save the storage slots data into the memory - exec.account::save_account_storage_data - # OS => [pad(16)] - # AS => [] + # load the advice data into the active account memory section + exec.account::load_foreign_account end # make sure that the state of the loaded foreign account corresponds to this commitment in the # account database - exec.account::validate_current_foreign_account + exec.account::validate_active_foreign_account # => [pad(16)] end #! Ends a foreign account context. #! -#! This pops the top of the account stack, making the previous account the current account. +#! This pops the top of the account stack, making the previous account the active account. #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] #! #! Panics if: -#! - the current account is the native account. +#! - the active account is the native account. #! #! Invocation: dynexec export.tx_end_foreign_context @@ -1291,7 +1494,8 @@ export.tx_end_foreign_context # => [pad(16)] end -#! Updates the transaction expiration time delta. +#! Updates the transaction expiration block delta. +#! #! Once set, the delta can be decreased but not increased. #! #! The input block height delta is added to the reference block in order to output an upper limit @@ -1304,8 +1508,8 @@ end #! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). #! #! Invocation: dynexec -export.tx_update_expiration_block_num - exec.tx::update_expiration_block_num +export.tx_update_expiration_block_delta + exec.tx::update_expiration_block_delta # => [pad(16)] end @@ -1327,25 +1531,6 @@ export.tx_get_expiration_delta # => [block_height_delta, pad(15)] end -#! Returns 1 if a procedure was called during transaction execution, and 0 otherwise. -#! -#! Inputs: [PROC_ROOT, pad(12)] -#! Outputs: [was_called, pad(15)] -#! -#! Where: -#! - PROC_ROOT is the hash of the procedure to check. -#! - was_called is 1 if the procedure was called at least once during tx execution, 0 otherwise. -#! -#! Panics if: -#! - the procedure root is not part of the account code. -#! -#! Invocation: dynexec -export.account_was_procedure_called - # check if the procedure was called - exec.account::was_procedure_called - # => [was_called, pad(15)] -end - #! Executes a kernel procedure specified by its offset. #! #! Inputs: [procedure_offset, , ] @@ -1377,3 +1562,39 @@ export.exec_kernel_proc dynexec # => [, ] end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Returns the memory pointer to the input note, depending on whether the requested note is current +#! or it was requested by index. +#! +#! If the pointer to the active note was requested, but no note is being executed, 0 +#! is returned. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [input_note_ptr] +#! +#! Where: +#! - is_active_note is the boolean flag indicating whether we should return requested data from +#! the active note or from the note with the specified index. +#! - note_index is the index of the input note whose data should be returned. Notice that if +#! is_active_note is 1, note_index is ignored. +#! - input_note_ptr is the pointer to the correct input note. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +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] + + # get the memory pointer to the currently processed input note + exec.memory::get_active_input_note_ptr swap + # => [is_active_note, active_input_note_ptr, indexed_input_note_ptr] + + # If is_active_note flag is true (active note processing case), active_input_note_ptr remains + # on the stack. If it is false, indexed_input_note_ptr remains instead. + cdrop + # => [input_note_ptr] +end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/account.masm b/crates/miden-lib/asm/kernels/transaction/lib/account.masm index 2d864cacf2..d4f6b69e42 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/account.masm @@ -1,15 +1,14 @@ -use.std::collections::mmr -use.std::collections::smt -use.std::crypto::hashes::rpo -use.std::mem - +use.$kernel::account_delta use.$kernel::account_id use.$kernel::asset_vault -use.$kernel::asset -use.$kernel::account_delta use.$kernel::constants use.$kernel::memory +use.std::collections::smt +use.std::crypto::hashes::rpo +use.std::mem +use.std::word + # ERRORS # ================================================================================================= @@ -43,6 +42,8 @@ const.ERR_ACCOUNT_TOO_MANY_STORAGE_SLOTS="number of account storage slots exceed const.ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH="computed account storage commitment does not match recorded account storage commitment" +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_INVALID_STORAGE_OFFSET_FOR_SIZE="storage size can only be zero if storage offset is also zero" const.ERR_FOREIGN_ACCOUNT_ID_IS_ZERO="ID of the provided foreign account equals zero" @@ -122,34 +123,46 @@ const.ACCOUNT_PROCEDURE_DATA_LENGTH=8 # 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") + # Event emitted before an asset is added to the account vault. -const.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT=131072 +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=131073 +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=131074 +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=131075 +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") + +# 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") # Event emitted before an account storage item is updated. -const.ACCOUNT_STORAGE_BEFORE_SET_ITEM_EVENT=131076 +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=131077 +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") # Event emitted before an account storage map item is updated. -const.ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM_EVENT=131078 +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=131079 +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=131080 +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=131081 +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=131082 +const.ACCOUNT_PUSH_PROCEDURE_INDEX_EVENT=event("miden::account::push_procedure_index") # CONSTANT ACCESSORS # ================================================================================================= @@ -192,45 +205,30 @@ end # PROCEDURES # ================================================================================================= -#! Computes the account commitment of the current account. -#! -#! Notice that there is no caching (and, hence, dirty flag) for the commitment of the entire -#! account. Assuming that the storage commitment is current, computing account commitment is -#! relatively cheap — essentially is consists of just 2 permutations of the hash function and takes -#! relatively small number of cycles, so it would not be worth adding a separate caching mechanism -#! for this. +# ID AND NONCE +# ------------------------------------------------------------------------------------------------- + +#! Returns the id of the active account. #! #! Inputs: [] -#! Outputs: [ACCOUNT_COMMITMENT] +#! Outputs: [act_acct_id_prefix, act_acct_id_suffix] #! #! Where: -#! - ACCOUNT_COMMITMENT is the commitment of the account data. -export.compute_current_commitment - # if outdated, recompute the storage commitment and store it in the memory - exec.refresh_storage_commitment - # => [] - - # prepare the stack for computing the account commitment - exec.memory::get_current_account_data_ptr padw padw padw - # => [RATE, RATE, PERM, account_data_ptr] - - # 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 - # => [RATE, RATE, PERM, account_data_ptr'] - - # extract account commitment - exec.rpo::squeeze_digest - # => [ACCOUNT_COMMITMENT, account_data_ptr'] +#! - 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 - # drop account_data_ptr - movup.4 drop - # => [ACCOUNT_COMMITMENT] -end +#! Returns the nonce of the active account. +#! +#! Inputs: [] +#! Outputs: [nonce] +#! +#! Where: +#! - nonce is the account nonce. +export.memory::get_account_nonce->get_nonce #! Increments the account nonce by one and returns the new nonce. #! -#! Assumes that it is executed only when the current account is the native account. +#! Assumes that it is executed only when the active account is the native account. #! #! Inputs: [] #! Outputs: [new_nonce] @@ -248,7 +246,7 @@ export.incr_nonce # emit event to signal that account nonce is being incremented emit.ACCOUNT_BEFORE_INCREMENT_NONCE_EVENT - exec.memory::get_acct_nonce + exec.memory::get_account_nonce # => [current_nonce] # if the current nonce is the maximum felt value, then incrementing the nonce would overflow @@ -259,191 +257,163 @@ export.incr_nonce add.1 # => [new_nonce] - dup exec.memory::set_acct_nonce + dup exec.memory::set_account_nonce # => [new_nonce] emit.ACCOUNT_AFTER_INCREMENT_NONCE_EVENT # => [new_nonce] end -#! Returns the id of the current account. -#! -#! Inputs: [] -#! Outputs: [curr_acct_id_prefix, curr_acct_id_suffix] -#! -#! Where: -#! - curr_acct_id_{prefix,suffix} are the prefix and suffix felts of the account ID of the currently -#! accessing account. -export.memory::get_account_id->get_id +# COMMITMENTS +# ------------------------------------------------------------------------------------------------- -#! Returns the account nonce. -#! -#! Inputs: [] -#! Outputs: [nonce] -#! -#! Where: -#! - nonce is the account nonce. -export.memory::get_acct_nonce->get_nonce +### ACCOUNT COMMITMENT ################################################### -#! Returns the native account commitment at the beginning of the transaction. +#! Returns the commitment of the active account at the beginning of the transaction. #! #! Inputs: [] #! Outputs: [INIT_COMMITMENT] #! #! Where: #! - INIT_COMMITMENT is the initial account commitment. -export.memory::get_init_account_commitment->get_initial_commitment +export.get_initial_commitment + # determine whether the active account is native + exec.memory::is_native_account + # => [is_native_account] -#! Returns the vault root of the native account at the beginning of the transaction. + if.true + exec.memory::get_init_account_commitment + else + # for the foreign account current commitment equals to initial + exec.compute_commitment + end + # => [INIT_COMMITMENT] +end + +#! Computes commitment to the state of the active account. +#! +#! Notice that there is no caching (and, hence, dirty flag) for the commitment of the entire +#! account. If the storage commitment is up-to-date (which is ensured by this procedure), computing +#! account commitment is relatively cheap — essentially is consists of just 2 permutations of the +#! hash function and takes relatively small number of cycles, so it would not be worth adding a +#! separate caching mechanism for this. #! #! Inputs: [] -#! Outputs: [INIT_ACCOUNT_VAULT_ROOT] +#! Outputs: [ACCOUNT_COMMITMENT] #! #! Where: -#! - INIT_ACCOUNT_VAULT_ROOT is the initial account vault root. -export.memory::get_init_account_vault_root->get_initial_vault_root +#! - ACCOUNT_COMMITMENT is the commitment of the account data. +export.compute_commitment + # if outdated, recompute the storage commitment and store it in the memory + exec.refresh_storage_commitment + # => [] -#! Returns the storage commitment of the native account at the beginning of the transaction. + # prepare the stack for computing the account commitment + exec.memory::get_active_account_data_ptr padw padw padw + # => [RATE, RATE, PERM, account_data_ptr] + + # 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 + # => [RATE, RATE, PERM, account_data_ptr'] + + # extract account commitment + exec.rpo::squeeze_digest + # => [ACCOUNT_COMMITMENT, account_data_ptr'] + + # drop account_data_ptr + movup.4 drop + # => [ACCOUNT_COMMITMENT] +end + +### CODE COMMITMENT ###################################################### + +#! Gets the code commitment of the active account. #! #! Inputs: [] -#! Outputs: [INIT_ACCOUNT_STORAGE_COMMITMENT] +#! Outputs: [CODE_COMMITMENT] #! #! Where: -#! - INIT_ACCOUNT_STORAGE_COMMITMENT is the initial account storage commitment. -export.memory::get_init_account_storage_commitment->get_initial_storage_commitment +#! - CODE_COMMITMENT is the commitment of the account code. +export.memory::get_account_code_commitment->get_code_commitment + +### STORAGE COMMITMENT ################################################### -#! Gets the code commitment of the current account. +#! Returns the storage commitment of the active account at the beginning of the transaction. #! #! Inputs: [] -#! Outputs: [CODE_COMMITMENT] +#! Outputs: [INIT_ACCOUNT_STORAGE_COMMITMENT] #! #! Where: -#! - CODE_COMMITMENT is the commitment of the account code. -export.memory::get_acct_code_commitment->get_code_commitment +#! - INIT_ACCOUNT_STORAGE_COMMITMENT is the initial account storage commitment. +export.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 + # => [ACTIVE_ACCOUNT_STORAGE_COMMITMENT] + + # get the initial storage commitment of the native account + exec.memory::get_init_account_storage_commitment + # => [INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT, ACTIVE_ACCOUNT_STORAGE_COMMITMENT] + + # check whether the active account is native + exec.memory::is_native_account + # => [is_native_account, INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT, ACTIVE_ACCOUNT_STORAGE_COMMITMENT] + + # if the active account is native, keep the INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT, or + # ACTIVE_ACCOUNT_STORAGE_COMMITMENT otherwise + cdropw + # => [INIT_ACCOUNT_STORAGE_COMMITMENT] +end -#! Computes the storage commitment of the current account. +#! Computes the storage commitment of the active account. #! #! Inputs: [] #! Outputs: [STORAGE_COMMITMENT] #! #! Where: -#! - STORAGE_COMMITMENT is the current commitment of the account storage. +#! - STORAGE_COMMITMENT is the commitment of the active account storage. export.compute_storage_commitment # if outdated, recompute the storage commitment and store it in the memory exec.refresh_storage_commitment # return the storage commitment - exec.memory::get_acct_storage_commitment + exec.memory::get_account_storage_commitment # => [STORAGE_COMMITMENT] end -#! Applies storage offset to provided storage slot index for storage access. -#! -#! 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 +### VAULT ROOT ########################################################### -#! Validates all account procedure's storage metadata. +#! Returns the vault root of the active account at the beginning of the transaction. #! #! Inputs: [] -#! Outputs: [] +#! Outputs: [INIT_ACCOUNT_VAULT_ROOT] #! -#! 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 - # => [] +#! Where: +#! - INIT_ACCOUNT_VAULT_ROOT is the initial account vault root. +export.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 + # => [ACTIVE_ACCOUNT_VAULT_ROOT] + + # get the initial vault root of the native account + exec.memory::get_init_native_account_vault_root + # => [INIT_NATIVE_ACCOUNT_VAULT_ROOT, ACTIVE_ACCOUNT_VAULT_ROOT] + + # check whether the active account is native + exec.memory::is_native_account + # => [is_native_account, INIT_NATIVE_ACCOUNT_VAULT_ROOT, ACTIVE_ACCOUNT_VAULT_ROOT] + + # if the active account is native, keep the INIT_NATIVE_ACCOUNT_VAULT_ROOT, or + # ACTIVE_ACCOUNT_VAULT_ROOT otherwise + cdropw + # => [INIT_ACCOUNT_VAULT_ROOT] end +# STORAGE +# ------------------------------------------------------------------------------------------------- + #! Gets an item from the account storage. #! #! Note: @@ -457,14 +427,35 @@ end #! - VALUE is the value of the item. export.get_item # get account storage slots section offset - exec.memory::get_acct_storage_slots_section_ptr + exec.memory::get_account_storage_slots_section_ptr # => [acct_storage_slots_section_offset, index] # get the item from storage - swap mul.8 add padw movup.4 mem_loadw + exec.get_item_raw # => [VALUE] end +#! Gets an item from the account storage at its initial state (beginning of transaction). +#! +#! Note: +#! - We assume that index has been validated and is within bounds. +#! +#! Inputs: [index] +#! Outputs: [INIT_VALUE] +#! +#! Where: +#! - index is the index of the item to get. +#! - INIT_VALUE is the initial value of the item at the beginning of the transaction. +export.get_initial_item + # get account initial storage slots section offset + exec.memory::get_account_initial_storage_slots_ptr + # => [account_initial_storage_slots_ptr, index] + + # get the item from initial storage + exec.get_item_raw + # => [INIT_VALUE] +end + #! Sets an item in the account storage. #! #! Note: @@ -524,30 +515,42 @@ end #! Panics if: #! - the requested storage slot type is not map. export.get_map_item - # get the storage slot type - dup exec.get_storage_slot_type - # => [slot_type, index, KEY] - - # check if storage slot type is map - exec.constants::get_storage_slot_type_map eq - assert.err=ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT - # => [index, KEY] + # 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] + # => [KEY, ROOT, index] - # see hash_map_key's docs for why this is done - exec.hash_map_key - # => [HASHED_KEY, ROOT] + exec.get_map_item_raw +end - # fetch the VALUE located under HASHED_KEY in the tree - exec.smt::get - # => [VALUE, ROOT] +#! 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] +#! 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. +#! - INIT_VALUE is the initial value of the map item at KEY at the beginning of the transaction. +#! +#! Panics if: +#! - the requested storage slot type is not map. +export.get_initial_map_item + # duplicate index for later use + dup movdn.5 + # => [index, KEY, index] - # remove the ROOT from the stack - swapw dropw - # => [VALUE] + # fetch the initial account storage item, which is ROOT of the map + exec.get_initial_item swapw + # => [KEY, INIT_ROOT, index] + + exec.get_map_item_raw end #! Stores NEW_VALUE under the specified KEY within the map contained in the given account storage slot. @@ -576,6 +579,9 @@ export.set_map_item.12 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, ...] + # check if storage type is map exec.get_storage_slot_type # => [slot_type, KEY, NEW_VALUE, OLD_ROOT] @@ -585,9 +591,6 @@ export.set_map_item.12 assert.err=ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT # => [KEY, NEW_VALUE, OLD_ROOT] - emit.ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM_EVENT - # => [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 @@ -603,13 +606,13 @@ export.set_map_item.12 # => [OLD_MAP_VALUE, NEW_ROOT, KEY, NEW_VALUE] # store OLD_MAP_VALUE and NEW_ROOT until the end of the procedure - loc_storew.4 dropw loc_storew.8 dropw + loc_storew_be.4 dropw loc_storew_be.8 dropw # => [KEY, NEW_VALUE] dupw.1 # => [NEW_VALUE, KEY, NEW_VALUE] - padw loc_loadw.4 + padw loc_loadw_be.4 # => [OLD_MAP_VALUE, NEW_VALUE, KEY, NEW_VALUE] dupw.2 @@ -619,42 +622,312 @@ export.set_map_item.12 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] + 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, ...] +end + +#! Applies storage offset to provided storage slot index for storage access. +#! +#! 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. +#! +#! Inputs: [index] +#! Outputs: [slot_type] +#! +#! 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 + # => [slot_type] +end + +# VAULT +# ------------------------------------------------------------------------------------------------- + +#! Adds the specified asset to the account vault. +#! +#! Inputs: [ASSET] +#! Outputs: [ASSET'] +#! +#! Where: +#! - ASSET is the asset that is added to the vault. +#! - ASSET' final asset in the account vault defined as follows: +#! - If ASSET is a non-fungible asset, then ASSET' is the same as ASSET. +#! - If ASSET is a fungible asset, then ASSET' is the total fungible asset in the account vault +#! after ASSET was added to it. +#! +#! Panics if: +#! - the asset is not valid. +#! - 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 + # duplicate the ASSET to be able to emit an event after an asset is being added + dupw + # => [ASSET, ASSET] + + # fetch the account vault root + exec.memory::get_account_vault_root_ptr movdn.4 + # => [ASSET, acct_vault_root_ptr, ASSET] + + # emit event to signal that an asset is going to be added to the account vault + emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT + + # add the asset to the account vault + exec.asset_vault::add_asset + # => [ASSET', ASSET] + + swapw + # => [ASSET, ASSET'] + + dupw exec.account_delta::add_asset + # => [ASSET, ASSET'] + + # emit event to signal that an asset is being added to the account vault + emit.ACCOUNT_VAULT_AFTER_ADD_ASSET_EVENT + dropw + # => [ASSET'] +end + +#! Removes the specified asset from the account vault. +#! +#! Inputs: [ASSET] +#! Outputs: [ASSET] +#! +#! Where: +#! - ASSET is the asset to remove from the vault. +#! +#! 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. +export.remove_asset_from_vault + # fetch the vault root + exec.memory::get_account_vault_root_ptr movdn.4 + # => [ASSET, acct_vault_root_ptr] + + # emit event to signal that an asset is going to be removed from the account vault + emit.ACCOUNT_VAULT_BEFORE_REMOVE_ASSET_EVENT + + # remove the asset from the account vault + exec.asset_vault::remove_asset + # => [ASSET] + + dupw exec.account_delta::remove_asset + # => [ASSET] + + # emit event to signal that an asset is being removed from the account vault + emit.ACCOUNT_VAULT_AFTER_REMOVE_ASSET_EVENT + # => [ASSET] +end + +#! 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] +#! +#! Where: +#! - faucet_id_{prefix, suffix} are the prefix and suffix felts of the faucet id of the fungible +#! asset of interest. +#! - balance is the vault balance of the fungible asset. +#! +#! Panics if: +#! - the provided faucet ID is not an ID of a fungible faucet. +export.get_balance + # get the vault root + exec.memory::get_account_vault_root_ptr movdn.2 + # => [faucet_id_prefix, faucet_id_suffix, vault_root_ptr] + + # emit event to signal that an asset's balance is requested + emit.ACCOUNT_VAULT_BEFORE_GET_BALANCE_EVENT + # => [faucet_id_prefix, faucet_id_suffix, vault_root_ptr] + + # get the asset balance + exec.asset_vault::get_balance + # => [balance] +end + +#! 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] +#! +#! Where: +#! - faucet_id_{prefix, suffix} are the prefix and suffix felts of the faucet id of the fungible +#! asset of interest. +#! - init_balance is the vault balance of the fungible asset at the beginning of the transaction. +#! +#! Panics if: +#! - the provided faucet ID is not an ID of a fungible faucet. +export.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] + + # emit event to signal that an asset's balance is requested + emit.ACCOUNT_VAULT_BEFORE_GET_BALANCE_EVENT + # => [faucet_id_prefix, faucet_id_suffix, init_native_vault_root_ptr] + + # get the asset balance + exec.asset_vault::get_balance + # => [init_balance] +end + +#! Returns a boolean indicating whether the non-fungible asset is present in the active account's +#! vault. +#! +#! Inputs: [ASSET] +#! Outputs: [has_asset] +#! +#! Where: +#! - ASSET is the non-fungible asset of interest. +#! - has_asset is a boolean indicating whether the account vault has the asset of interest. +#! +#! Panics if: +#! - the ASSET is a fungible asset. +export.has_non_fungible_asset + # get the vault root + exec.memory::get_account_vault_root_ptr movdn.4 + # => [ASSET, vault_root_ptr] + + # emit event to signal that an asset's presence is being checked + emit.ACCOUNT_VAULT_BEFORE_HAS_NON_FUNGIBLE_ASSET_EVENT + # => [ASSET, vault_root_ptr] + + # check if the account vault has the non-fungible asset + exec.asset_vault::has_non_fungible_asset + # => [has_asset] +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] - exec.account_delta::set_map_item - # => [KEY, NEW_VALUE] + # 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] - # load OLD_MAP_VALUE and NEW_ROOT on the top of the stack - loc_loadw.8 swapw loc_loadw.4 swapw - # => [NEW_ROOT, OLD_MAP_VALUE, ...] + # 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] - # 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, ...] -end + # 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] -#! Returns the type of the requested storage slot. -#! -#! Inputs: [index] -#! Outputs: [slot_type] -#! -#! 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] + # 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] - exec.memory::get_acct_storage_slots_section_ptr - # => [curr_account_storage_slots_section_ptr, index] + # check if we should continue looping + add.1 dup dup.3 lt + # => [should_loop, index, num_storage_slots, num_account_procedures] + end + end - exec.memory::get_storage_slot_type - # => [slot_type] + # clean stack + drop drop drop + # => [] end #! Returns the procedure information. @@ -676,7 +949,7 @@ export.get_procedure_info # => [index] # get procedure pointer - exec.memory::get_acct_procedure_ptr + exec.memory::get_account_procedure_ptr # => [proc_ptr] # get metadata pointer @@ -684,7 +957,7 @@ export.get_procedure_info # => [proc_ptr, metadata_ptr] # load procedure information from memory - padw movup.4 mem_loadw padw movup.8 mem_loadw + 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] @@ -759,6 +1032,9 @@ export.assert_auth_procedure # => [] end +# VALIDATION +# ------------------------------------------------------------------------------------------------- + #! Validates that the account seed, provided via the advice map, satisfies the seed requirements. #! #! Validation is performed via the following steps: @@ -789,7 +1065,7 @@ export.validate_seed # => [SEED, 0, 0, 0, 0, EMPTY_WORD] # populate last four elements of the hasher rate with the code commitment - exec.memory::get_acct_code_commitment + exec.memory::get_account_code_commitment # => [CODE_COMMITMENT, SEED, 0, 0, 0, 0, EMPTY_WORD] # perform first permutation of seed and code_commitment (from advice stack) @@ -802,7 +1078,7 @@ export.validate_seed # => [PERM, EMPTY_WORD] # perform second permutation perm(storage_commitment, 0, 0, 0, 0) - swapw exec.memory::get_acct_storage_commitment swapw + swapw exec.memory::get_account_storage_commitment swapw # => [EMPTY_WORD, STORAGE_COMMITMENT, PERM] hperm @@ -831,87 +1107,90 @@ export.validate_seed # => [] end -#! Adds the specified asset to the account vault. +# DATA LOADERS +# ------------------------------------------------------------------------------------------------- + +#! Loads account data from the advice inputs into the _active_ account's memory section. #! -#! Inputs: [ASSET] -#! Outputs: [ASSET'] +#! Inputs: +#! Operand stack: [account_id_prefix, account_id_suffix] +#! Advice map: { +#! ACCOUNT_ID: [[account_id_suffix, account_id_prefix, 0, account_nonce], +#! VAULT_ROOT, STORAGE_COMMITMENT, CODE_COMMITMENT], +#! STORAGE_COMMITMENT: [[STORAGE_SLOT_DATA]], +#! CODE_COMMITMENT: [[ACCOUNT_PROCEDURE_DATA]], +#! } +#! Outputs: +#! Operand stack: [] #! #! Where: -#! - ASSET is the asset that is added to the vault. -#! - ASSET' final asset in the account vault defined as follows: -#! - If ASSET is a non-fungible asset, then ASSET' is the same as ASSET. -#! - If ASSET is a fungible asset, then ASSET' is the total fungible asset in the account vault -#! after ASSET was added to it. +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the account. +#! - ACCOUNT_ID is the word constructed from the account_id as follows: +#! [account_id_suffix, account_id_prefix, 0, 0]. +#! - account_nonce is the nonce of the account. +#! - VAULT_ROOT is the commitment of the account's vault. +#! - STORAGE_COMMITMENT is the commitment to the account's storage. +#! - 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]. #! #! Panics if: -#! - the asset is not valid. -#! - 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 - # emit event to signal that an asset is going to be added to the account vault - emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT - # => [ASSET] - - # duplicate the ASSET to be able to emit an event after an asset is being added - dupw - # => [ASSET, ASSET] - - # fetch the account vault root - exec.memory::get_acct_vault_root_ptr movdn.4 - # => [ASSET, acct_vault_root_ptr, ASSET] - - # add the asset to the account vault - exec.asset_vault::add_asset - # => [ASSET', ASSET] - - swapw - # => [ASSET, ASSET'] - - dupw exec.account_delta::add_asset - # => [ASSET, ASSET'] +#! - 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 + emit.ACCOUNT_BEFORE_FOREIGN_LOAD_EVENT + # => [account_id_prefix, account_id_suffix] + + # construct the word with account ID to load the core account data from the advice map + push.0.0 + # OS => [0, 0, account_id_prefix, account_id_suffix] + + # move the core account data to the advice stack + adv.push_mapval + # OS => [0, 0, account_id_prefix, account_id_suffix] + # AS => [[account_id_prefix, account_id_suffix, 0, account_nonce], VAULT_ROOT, STORAGE_COMMITMENT, CODE_COMMITMENT] + + # store the id and nonce of the foreign account to the memory + adv_loadw + exec.memory::set_account_id_and_nonce + # OS => [] + # AS => [VAULT_ROOT, STORAGE_COMMITMENT, CODE_COMMITMENT] - # emit event to signal that an asset is being added to the account vault - emit.ACCOUNT_VAULT_AFTER_ADD_ASSET_EVENT dropw - # => [ASSET'] -end + # store the vault root of the foreign account to the memory + adv_loadw exec.memory::set_account_vault_root + # OS => [] + # AS => [STORAGE_COMMITMENT, CODE_COMMITMENT] -#! Removes the specified asset from the account vault. -#! -#! Inputs: [ASSET] -#! Outputs: [ASSET] -#! -#! Where: -#! - ASSET is the asset to remove from the vault. -#! -#! 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. -export.remove_asset_from_vault - # emit event to signal that an asset is going to be removed from the account vault - emit.ACCOUNT_VAULT_BEFORE_REMOVE_ASSET_EVENT - # => [ASSET] + # move the storage root and the code root to the operand stack + adv_loadw padw adv_loadw + # OS => [CODE_COMMITMENT, STORAGE_COMMITMENT] + # AS => [] - # fetch the vault root - exec.memory::get_acct_vault_root_ptr movdn.4 - # => [ASSET, acct_vault_root_ptr] + # store the code root into the memory + exec.memory::set_account_code_commitment + # OS => [CODE_COMMITMENT, STORAGE_COMMITMENT] + # AS => [] - # remove the asset from the account vault - exec.asset_vault::remove_asset - # => [ASSET] + # save the account procedure data into the memory + exec.save_account_procedure_data + # OS => [STORAGE_COMMITMENT] + # AS => [] - dupw exec.account_delta::remove_asset - # => [ASSET] + # store the storage root to the memory + exec.memory::set_account_storage_commitment + # OS => [STORAGE_COMMITMENT] + # AS => [] - # emit event to signal that an asset is being removed from the account vault - emit.ACCOUNT_VAULT_AFTER_REMOVE_ASSET_EVENT - # => [ASSET] + # save the storage slots data into the memory + exec.save_account_storage_data + # OS => [] + # AS => [] end -# DATA LOADERS -# ================================================================================================= - #! Saves storage slots data into memory and validates that the storage commitment matches the #! sequential storage hash. #! @@ -924,13 +1203,13 @@ end #! Operand stack: [] #! #! Where: -#! - STORAGE_COMMITMENT is the commitment of the current account's storage. +#! - STORAGE_COMMITMENT is the commitment of the active account's storage. #! - STORAGE_SLOT_DATA is the data contained in the storage slot which is constructed as follows: #! [SLOT_VALUE, slot_type, 0, 0, 0] #! #! 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 +#! - the computed account storage commitment does not match the provided account storage commitment. export.save_account_storage_data # move storage slot data from the advice map to the advice stack adv.push_mapvaln @@ -954,7 +1233,7 @@ 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_acct_storage_slots_section_ptr dup movdn.2 add swap + mul.8 exec.memory::get_account_storage_slots_section_ptr dup movdn.2 add swap # OS => [acct_storage_slots_ptr, end_ptr, STORAGE_COMMITMENT] # AS => [[STORAGE_SLOT_DATA]] @@ -979,11 +1258,6 @@ export.save_account_storage_data # verify hashed account storage slots match account storage commitment assert_eqw.err=ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH # OS => [] - - # duplicate the initial storage slots in memory to enable a diff computation - # for the account delta - exec.memory::mem_copy_initial_storage_slots - # OS => [] end #! Saves account procedure data into memory and validates that the code commitment matches the @@ -998,13 +1272,13 @@ end #! Operand stack: [] #! #! Where: -#! - CODE_COMMITMENT is the commitment of the current account's code. +#! - 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] #! #! 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 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 # move procedure data from the advice map to the advice stack adv.push_mapvaln @@ -1028,8 +1302,8 @@ export.save_account_procedure_data # AS => [[ACCOUNT_PROCEDURE_DATA]] # setup acct_proc_offset and end_ptr for reading from advice stack - exec.memory::get_acct_procedure_ptr - push.0 exec.memory::get_acct_procedure_ptr + exec.memory::get_account_procedure_ptr + push.0 exec.memory::get_account_procedure_ptr # OS => [acct_proc_offset, end_ptr, CODE_COMMITMENT] # AS => [[ACCOUNT_PROCEDURE_DATA]] @@ -1056,9 +1330,207 @@ export.save_account_procedure_data # OS => [] end -# HELPER PROCEDURES +#! Writes the initial storage values from the advice inputs into the account delta and validates +#! that they match what the account commits to. +#! +#! This is only relevant for new accounts and only does actual work for storage maps, since +#! value slots aren't explicitly tracked by the delta. +#! +#! Inputs: [] +#! Outputs: [] +export.insert_new_storage + exec.memory::get_num_storage_slots + # => [num_slots] + + # loop if there are storage slots + dup neq.0 + # => [should_loop, num_slots] + + while.true + sub.1 + # => [slot_idx] + + dup exec.get_storage_slot_type + # => [slot_type, slot_idx] + + exec.constants::get_storage_slot_type_map eq + # => [is_map_slot_type, slot_idx] + + if.true + # add map entries to account storage + dup exec.insert_and_validate_storage_map + # => [slot_idx] + end + # => [slot_idx] + + dup neq.0 + # => [should_continue, slot_idx] + end + # => [slot_idx] + + drop + # => [] +end + +#! Inserts the entries of the provided storage map root into the account. +#! +#! These entries must be present in the advice provider with the map root as the key. Each entry is +#! is inserted using set_map_item on an initially empty SMT root. This does two important things: +#! - It allows checking whether the root of the SMT with all entries inserted matches the root of +#! the map the account commits to. +#! - It inserts all entries into the in-kernel delta, where the initial value of each entry will +#! be set to the empty word so the delta for this map is computed as if the map had initially +#! been empty. +#! +#! Inputs: +#! 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] + + exec.get_item_raw + # => [MAP_ROOT, slot_idx] + + # 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 + dup.8 + # => [slot_idx, EMPTY_SMT_ROOT, MAP_ROOT, slot_idx] + + exec.set_item_raw dropw + # => [MAP_ROOT, slot_idx] + + adv.push_mapvaln + # OS => [MAP_ROOT, slot_idx] + # AS => [num_elements, [MAP_ENTRIES]] + + movup.4 + # OS => [slot_idx, MAP_ROOT] + # AS => [num_elements, [MAP_ENTRIES]] + + adv_push.1 + # OS => [num_elements, slot_idx, 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] + # 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] + # 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] + # AS => [[MAP_ENTRIES]] + + while.true + sub.1 + # => [remaining_entries, slot_idx, MAP_ROOT] + + # push a key-value pair (8 felts) to the operand stack + adv_push.8 + # => [KEY, VALUE, remaining_entries, slot_idx, MAP_ROOT] + + dup.9 + # => [slot_idx, KEY, VALUE, remaining_entries, slot_idx, 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] + + dup neq.0 + # => [should_continue, remaining_entries, slot_idx, MAP_ROOT] + end + # OS => [remaining_entries, slot_idx, MAP_ROOT] + # AS => [] + + drop + # => [slot_idx, 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] + + # after inserting all entries, the storage map root must match the map root that was committed + # to as part of account creation + assert_eqw.err=ERR_ACCOUNT_STORAGE_MAP_ENTRIES_DO_NOT_MATCH_MAP_ROOT + # => [] +end + +# HELPER PROCEDURES # ================================================================================================= +#! Gets an item from storage using the provided storage slots pointer and index. +#! +#! Note: +#! - We assume that index has been validated and is within bounds. +#! +#! Inputs: [storage_slots_ptr, index] +#! Outputs: [VALUE] +#! +#! Where: +#! - storage_slots_ptr is the pointer to the storage slots section. +#! - index is the index of the item to get. +#! - 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 + # => [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. +#! +#! Inputs: [KEY, ROOT, index] +#! 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. +#! - VALUE is the value of the map item at KEY. +#! +#! Panics if: +#! - 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] + + # check if storage slot type is map + exec.constants::get_storage_slot_type_map eq + assert.err=ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT + # => [KEY, ROOT, index] + + emit.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT + # => [KEY, ROOT, index] + + # see hash_map_key's docs for why this is done + exec.hash_map_key + # => [HASHED_KEY, ROOT, index] + + # fetch the VALUE located under HASHED_KEY in the tree + exec.smt::get + # => [VALUE, ROOT, index] + + # 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. #! #! Inputs: [index, NEW_VALUE] @@ -1078,11 +1550,11 @@ proc.set_item_raw # => [index, NEW_VALUE, OLD_VALUE] # get account storage slots section offset - exec.memory::get_acct_storage_slots_section_ptr + exec.memory::get_account_storage_slots_section_ptr # => [acct_storage_slots_section_offset, index, NEW_VALUE, OLD_VALUE] # update storage - swap mul.8 add mem_storew + swap mul.8 add mem_storew_be # => [NEW_VALUE, OLD_VALUE] # update the storage commitment dirty flag, indicating that the commitment is outdated @@ -1111,11 +1583,11 @@ proc.get_procedure_root # => [index] # get procedure pointer - exec.memory::get_acct_procedure_ptr + exec.memory::get_account_procedure_ptr # => [proc_ptr] # load procedure root from memory - padw movup.4 mem_loadw + padw movup.4 mem_loadw_be # => [PROC_ROOT] end @@ -1132,11 +1604,11 @@ end #! - 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_acct_procedure_ptr add.4 + 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 drop drop swap + mem_loadw_be drop drop swap # => [storage_offset, storage_size] end @@ -1181,7 +1653,7 @@ export.get_account_data_ptr # => [curr_account_ptr', foreign_account_id_prefix, foreign_account_id_suffix] # load the first data word at the current account pointer - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be # => [FIRST_DATA_WORD, curr_account_ptr', foreign_account_id_prefix, foreign_account_id_suffix] # check whether the last value in the word equals zero @@ -1211,30 +1683,30 @@ export.get_account_data_ptr # => [was_loaded, curr_account_ptr, foreign_account_id_prefix, foreign_account_id_suffix] end -#! Checks that the state of the current foreign account is valid. +#! Checks that the state of the active foreign account is valid. #! #! Inputs: [] #! Outputs: [] #! #! Panics if: -#! - the hash of the current account is not represented in the account database. -export.validate_current_foreign_account +#! - the hash of the active account is not represented in the account database. +export.validate_active_foreign_account # get the account database root - exec.memory::get_acct_db_root + exec.memory::get_account_db_root # => [ACCOUNT_DB_ROOT] - # get the current account ID + # get the active account ID push.0.0 exec.memory::get_account_id # => [account_id_prefix, account_id_suffix, 0, 0, ACCOUNT_DB_ROOT] - # retrieve the commitment of the foreign account from the current account tree + # retrieve the commitment of the foreign account from the active account tree # this would abort if the proof for the commitment was invalid for the account root, # so this implicitly verifies its correctness exec.smt::get # => [FOREIGN_ACCOUNT_COMMITMENT, ACCOUNT_DB_ROOT] # get the foreign account's commitment from memory and compare with the verified commitment - exec.compute_current_commitment assert_eqw.err=ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT + exec.compute_commitment assert_eqw.err=ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT # => [ACCOUNT_DB_ROOT] # clean the stack @@ -1256,22 +1728,22 @@ end #! Makes the account storage commitment up-to-date. #! -#! Notice that the account storage commitment got updated only if it is outdated: if the account +#! Notice that the account storage commitment got updated only if it is outdated: if the account #! storage changes, its commitment will be recomputed by hashing the storage slots. Then this newly #! computed storage commitment updates the storage commitment in memory. If the storage commitment -#! is up-to-date, this procedure does nothing. +#! is up-to-date, this procedure does nothing. #! #! Inputs: [] #! Outputs: [] proc.refresh_storage_commitment # First we should determine whether the storage commitment should be recomputed. We should do so - # if the current account is native and the storage commitment is outdated (the dirty flag equals + # 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. exec.memory::get_recompute_storage_commitment_flag # => [should_recompute_storage_commitment] if.true - # dirty flag being equal 1 ensures that we have at least one storage slot, so we have to + # dirty flag being equal 1 ensures that we have at least one storage slot, so we have to # hash the storage anyway # get number of storage slots @@ -1279,7 +1751,7 @@ proc.refresh_storage_commitment # => [num_storage_slots] # setup start and end ptr - mul.8 exec.memory::get_acct_storage_slots_section_ptr dup movdn.2 add swap + mul.8 exec.memory::get_account_storage_slots_section_ptr dup movdn.2 add swap # => [start_ptr, end_ptr] # pad stack to read and hash from memory @@ -1299,17 +1771,17 @@ proc.refresh_storage_commitment # => [DIGEST] # set new account storage commitment - exec.memory::set_acct_storage_commitment dropw + exec.memory::set_account_storage_commitment dropw # => [] # update the storage commitment dirty flag, indicating that the commitment is up-to-date - push.0 + push.0 exec.memory::set_native_account_storage_commitment_dirty_flag # => [] end end -#! Checks if a procedure has been called during transaction execution. +#! Checks if a native account procedure has been called during transaction execution. #! #! Note: This returns 1 only if the procedure invoked account-restricted kernel APIs (e.g., #! `exec.faucet::mint`) which trigger `authenticate_and_track_procedure`. Procedures that execute @@ -1340,7 +1812,7 @@ export.was_procedure_called assert_eqw.err=ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE # => [index] - exec.memory::get_acct_procedures_call_tracking_ptr + exec.memory::get_account_procedures_call_tracking_ptr # => [was_called_offset, index] # load the value of was_called @@ -1358,9 +1830,76 @@ end #! Inputs: [proc_idx] #! Outputs: [] export.set_was_procedure_called - exec.memory::get_acct_procedures_call_tracking_ptr + exec.memory::get_account_procedures_call_tracking_ptr # => [was_called_offset, proc_idx] # save 1 to the was_called address add push.1 swap mem_store end + +#! Returns the binary flag indicating whether the procedure with the provided root is available on +#! the active account. +#! +#! Returns 1 if the procedure is available on the active account and 0 otherwise. +#! +#! Inputs: [PROC_ROOT] +#! Outputs: [is_procedure_available] +#! +#! Where: +#! - 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 + # 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 + # => [end_ptr, PROC_ROOT] + + # get the start pointer of the procedure section (where we will start iterating) + push.0 exec.memory::get_account_procedure_ptr + # => [start_ptr, end_ptr, PROC_ROOT] + + # prepare the stack for the loop + movdn.5 movdn.5 push.0 movdn.6 + # => [PROC_ROOT, start_ptr, end_ptr, is_procedure_available] + + # push the flag to enter the loop: an account should have at least 2 procedures + push.1 + # => [should_loop, PROC_ROOT, start_ptr, end_ptr, is_procedure_available] + + while.true + # => [PROC_ROOT, curr_proc_ptr, end_ptr, is_procedure_available] + + # load the root of the current procedure + padw dup.8 mem_loadw_be + # => [CURR_PROC_ROOT, PROC_ROOT, curr_proc_ptr, end_ptr, is_procedure_available] + + # check whether the current root is equal to the provided root + dupw.1 exec.word::eq + # => [is_equal, PROC_ROOT, curr_proc_ptr, end_ptr, is_procedure_available] + + # update the is_procedure_available flag + movup.7 or movdn.6 + # => [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'] + + # 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 + # 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'] + + # rearrange the stack + swap movdn.5 + # => [should_loop, PROC_ROOT, curr_proc_ptr + 8, end_ptr, is_procedure_available'] + end + + # => [PROC_ROOT, curr_proc_ptr', end_ptr, is_procedure_available'] + + # clean the stack + dropw drop drop + # => [is_procedure_available'] +end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm b/crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm index 5d56f5d39e..d3777b9e80 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm @@ -1,11 +1,11 @@ -use.$kernel::memory -use.$kernel::link_map -use.$kernel::constants 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::math::u64 +use.std::word # ERRORS # ================================================================================================= @@ -173,11 +173,25 @@ proc.update_value_slot_delta dup exec.account::get_item # => [CURRENT_VALUE, slot_idx, RATE, RATE, PERM] - dup.4 exec.get_item_initial + dup.4 exec.account::get_initial_item # => [INIT_VALUE, CURRENT_VALUE, slot_idx, RATE, RATE, PERM] - eqw not - # => [was_changed, 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] + + exec.word::test_eq not + # => [was_changed, INIT_VALUE', CURRENT_VALUE, slot_idx, RATE, RATE, PERM] # only include in delta if the slot's value has changed if.true @@ -252,7 +266,7 @@ proc.update_map_slot_delta.4 swapw.2 # => [NEW_VALUE, INIT_VALUE, KEY, ...] - eqw not + exec.word::test_eq not # => [was_changed, NEW_VALUE, INIT_VALUE, KEY, ...] # if the key-value pair has actually changed, update the hasher @@ -389,7 +403,6 @@ proc.update_fungible_asset_delta.2 # => [RATE, RATE, PERM] end - #! Updates the given delta hasher with the non-fungible asset vault delta. #! #! Inputs: [RATE, RATE, PERM] @@ -442,9 +455,9 @@ proc.update_non_fungible_asset_delta.2 hperm # => [RATE, RATE, PERM] else - # discard the two key and value words loaded from the map - dropw dropw - # => [RATE, RATE, PERM] + # discard the two key and value words loaded from the map + dropw dropw + # => [RATE, RATE, PERM] end # => [RATE, RATE, PERM] @@ -461,27 +474,6 @@ proc.update_non_fungible_asset_delta.2 # => [RATE, RATE, PERM] end -#! Returns the initial value of a storage slot from the account storage. -#! -#! This is the value of the slot at the beginning of the transaction. -#! -#! If this this procedure is moved to the account, additional assertions are necessary to make it -#! safe to use. -#! -#! Note: Assumes the index is within bounds. -#! -#! Inputs: [index] -#! Outputs: [INIT_VALUE] -proc.get_item_initial - # get account storage slots section offset - exec.memory::get_native_account_initial_storage_slots_ptr - # => [account_delta_initial_storage_slots_ptr, index] - - # get the item from storage - swap mul.8 add padw movup.4 mem_loadw - # => [INIT_VALUE] -end - # DELTA BOOKKEEPING # ------------------------------------------------------------------------------------------------- @@ -496,7 +488,7 @@ export.was_nonce_incremented exec.memory::get_init_nonce # => [init_nonce] - exec.memory::get_acct_nonce + exec.memory::get_account_nonce # => [current_nonce, init_nonce] neq @@ -680,7 +672,6 @@ export.add_non_fungible_asset # => [] end - #! Removes the given non-fungible asset from the non-fungible asset vault delta. #! #! ASSET must be a valid non-fungible asset. @@ -740,7 +731,7 @@ export.set_map_item.8 # => [KEY, PREV_VALUE, NEW_VALUE] # store KEY in local - loc_storew.4 + loc_storew_be.4 # => [KEY, PREV_VALUE, NEW_VALUE] loc_load.0 @@ -771,7 +762,7 @@ export.set_map_item.8 # => [INITIAL_VALUE, NEW_VALUE] # load key and index from locals - padw loc_loadw.4 loc_load.0 + padw loc_loadw_be.4 loc_load.0 # => [account_delta_storage_map_ptr, KEY, INITIAL_VALUE, NEW_VALUE] exec.link_map::set drop @@ -809,8 +800,8 @@ end # Since the golidlocks modulus is 2^64 - 2^32 + 1 and therefore odd, we can represent # modulus / 2 + 1 positive and modulus / 2 negative values. Again, 0 is counted on the positive # side and so the largest representable positive value is modulus / 2 and the smallest -# representable negative value is -modulus/2. So, every negative value has a positive counterpart -# (and vice versa). +# representable negative value is -(modulus / 2). So, every negative value has a positive +# counterpart (and vice versa). #! Computes the absolute value of the given delta amount represented as a felt and returns a #! boolean flag indicating whether the value is positive (or unsigned). diff --git a/crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm b/crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm index f43beb706f..6d439c8e71 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/asset_vault.masm @@ -1,4 +1,5 @@ use.std::collections::smt +use.std::word use.$kernel::account_id use.$kernel::asset @@ -25,6 +26,14 @@ const.ERR_VAULT_REMOVE_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to re 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") + # CONSTANTS # ================================================================================================= @@ -46,7 +55,7 @@ const.INVERSE_FUNGIBLE_BITMASK_U32=0xffffffdf # last byte: 0b1101_1111 #! - balance is the vault balance of the fungible asset. #! #! Panics if: -#! - the asset is not a fungible asset. +#! - the provided faucet ID is not an ID of a fungible faucet. export.get_balance # assert that the faucet id is a fungible faucet dup exec.account_id::is_fungible_faucet @@ -54,7 +63,7 @@ export.get_balance # => [faucet_id_prefix, faucet_id_suffix, vault_root_ptr] # load the asset vault root from memory - padw movup.6 mem_loadw + padw movup.6 mem_loadw_be # => [ASSET_VAULT_ROOT, faucet_id_prefix, faucet_id_suffix] # prepare the key for fungible asset lookup (pad least significant elements with zeros) @@ -76,6 +85,10 @@ end #! manipulation from a malicious host. Therefore this should only be used when the inclusion of the #! peeked balance is verified at a later point. #! +#! WARNING: This is a generic vault procedure and so it cannot emit an event to lazy load asset +#! merkle paths from the merkle store, since this is only possible for the account vault. Ensure +#! that the merkle paths are present prior to calling. +#! #! To get the verified balance, use get_balance. #! #! Inputs: [faucet_id_prefix, faucet_id_suffix, vault_root_ptr] @@ -96,7 +109,7 @@ export.peek_balance # => [faucet_id_prefix, faucet_id_suffix, vault_root_ptr] # load the asset vault root from memory - padw movup.6 mem_loadw + padw movup.6 mem_loadw_be # => [ASSET_VAULT_ROOT, faucet_id_prefix, faucet_id_suffix] # prepare the vault key for fungible asset lookup (pad least significant elements with zeros) @@ -105,7 +118,7 @@ export.peek_balance # => [ASSET_KEY, ASSET_VAULT_ROOT] # lookup asset - adv.push_smtpeek + emit.SMT_PEEK_EVENT # OS => [ASSET_KEY, ASSET_VAULT_ROOT] # AS => [ASSET] @@ -146,7 +159,7 @@ export.has_non_fungible_asset # => [ASSET_KEY, vault_root_ptr] # prepare the stack to read non-fungible asset from vault - padw movup.8 mem_loadw swapw + padw movup.8 mem_loadw_be swapw # => [ASSET_KEY, ACCT_VAULT_ROOT] # lookup asset @@ -154,11 +167,7 @@ export.has_non_fungible_asset # => [ASSET] # compare with EMPTY_WORD to assess if the asset exists in the vault - padw eqw not - # => [has_asset, PAD, ASSET] - - # organize the stack for return - movdn.4 dropw movdn.4 dropw + exec.word::eqz not # => [has_asset] end @@ -199,10 +208,10 @@ export.add_fungible_asset # the current asset may be the empty word if it does not exist and so its faucet id would be zeroes # we therefore overwrite the faucet id with the faucet id from ASSET to account for this edge case - mem_loadw swapw + mem_loadw_be swapw # => [ASSET_KEY, VAULT_ROOT, faucet_id_prefix, faucet_id_suffix, amount, vault_root_ptr] - adv.push_smtpeek - adv_loadw + + emit.SMT_PEEK_EVENT 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] @@ -270,7 +279,7 @@ export.add_fungible_asset # => [VAULT_ROOT', ASSET', vault_root_ptr] # update the vault root - movup.8 mem_storew dropw + movup.8 mem_storew_be dropw # => [ASSET'] end @@ -297,7 +306,7 @@ export.add_non_fungible_asset padw dup.12 # => [vault_root_ptr, pad(4), ASSET_KEY, ASSET, vault_root_ptr] - mem_loadw swapw + mem_loadw_be swapw # => [ASSET_KEY, VAULT_ROOT, ASSET, vault_root_ptr] dupw.2 # => [ASSET, ASSET_KEY, VAULT_ROOT, ASSET, vault_root_ptr] @@ -311,7 +320,7 @@ export.add_non_fungible_asset # => [VAULT_ROOT', ASSET, vault_root_ptr] # update the vault root - movup.8 mem_storew dropw + movup.8 mem_storew_be dropw # => [ASSET] end @@ -389,7 +398,7 @@ export.remove_fungible_asset.4 # => [ASSET, ASSET_KEY, PEEKED_ASSET, vault_root_ptr] # store ASSET so we can return it later - loc_storew.0 + loc_storew_be.0 # => [ASSET, ASSET_KEY, PEEKED_ASSET, vault_root_ptr] dup.3 dup.12 @@ -422,7 +431,7 @@ export.remove_fungible_asset.4 cdropw # => [EMPTY_WORD_OR_ASSET', ASSET_KEY, PEEKED_ASSET, vault_root_ptr] - dup.12 padw movup.4 mem_loadw + dup.12 padw movup.4 mem_loadw_be # => [VAULT_ROOT, EMPTY_WORD_OR_ASSET', ASSET_KEY, PEEKED_ASSET, vault_root_ptr] movdnw.2 @@ -438,10 +447,10 @@ export.remove_fungible_asset.4 # => [NEW_VAULT_ROOT, vault_root_ptr] # update vault root - movup.4 mem_storew + movup.4 mem_storew_be # => [NEW_VAULT_ROOT] - loc_loadw.0 + loc_loadw_be.0 # => [ASSET] end @@ -462,7 +471,7 @@ export.remove_non_fungible_asset # => [pad(4), ASSET_KEY, ASSET, vault_root_ptr] # load vault root - dup.12 mem_loadw + dup.12 mem_loadw_be # => [VAULT_ROOT, ASSET_KEY, ASSET, vault_root_ptr] # prepare insertion of an EMPTY_WORD into the vault at the asset key to remove the asset @@ -478,7 +487,7 @@ export.remove_non_fungible_asset # => [VAULT_ROOT', ASSET, vault_root_ptr] # update the vault root - movup.8 mem_storew dropw + movup.8 mem_storew_be dropw # => [ASSET] end @@ -527,14 +536,14 @@ end #! - asset is the peeked asset from the vault. proc.peek_asset # load the asset vault root from memory - padw movup.8 mem_loadw + padw movup.8 mem_loadw_be # => [ASSET_VAULT_ROOT, ASSET_KEY] swapw # => [ASSET_KEY, ASSET_VAULT_ROOT] # lookup asset - adv.push_smtpeek + emit.SMT_PEEK_EVENT # OS => [ASSET_KEY, ASSET_VAULT_ROOT] # AS => [ASSET] diff --git a/crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm index cb5e9e62f0..f7281e5be9 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/epilogue.masm @@ -4,30 +4,34 @@ use.$kernel::asset_vault use.$kernel::constants use.$kernel::memory use.$kernel::note -use.$kernel::tx -use.std::crypto::hashes::rpo use.std::word # ERRORS # ================================================================================================= -const.ERR_ACCOUNT_NONCE_DID_NOT_INCREASE_AFTER_STATE_CHANGE="account nonce did not increase after a state changing transaction" - 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_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT="auth procedure had been called from outside the epilogue" +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=131097 +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_TX_FEE_COMPUTED=131098 +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") + +# 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") # 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. @@ -115,7 +119,7 @@ end #! Outputs: [] proc.build_output_vault # copy final account vault root to output account vault root - exec.memory::get_acct_vault_root exec.memory::set_output_vault_root dropw + exec.memory::get_account_vault_root exec.memory::set_output_vault_root dropw # => [] # get the number of output notes from memory @@ -163,7 +167,7 @@ proc.build_output_vault # num_assets, note_data_ptr, output_notes_end_ptr] # read the output note asset from memory - padw dup.5 mem_loadw + padw dup.5 mem_loadw_be # => [ASSET, output_vault_root_ptr, assets_start_ptr, assets_end_ptr, # output_vault_root_ptr, num_assets, note_data_ptr, output_notes_end_ptr] @@ -200,22 +204,24 @@ end #! Inputs: [] #! Outputs: [] proc.execute_auth_procedure + emit.EPILOGUE_AUTH_PROC_START_EVENT + padw padw padw # get the auth procedure arguments exec.memory::get_auth_args # => [AUTH_ARGS, pad(12)] # auth procedure is at index 0 within the account procedures section. - push.0 exec.memory::get_acct_procedure_ptr + push.0 exec.memory::get_account_procedure_ptr # => [auth_procedure_ptr, AUTH_ARGS, pad(12)] - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be # => [AUTH_PROC_ROOT, auth_procedure_ptr, AUTH_ARGS, pad(12)] # if auth procedure was called already, it must have been called by a user, which is disallowed exec.account::was_procedure_called - # => [was_auth_called, auth_procedure_ptr, AUTH_ARGS, pad(12)] assertz.err=ERR_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT + # => [auth_procedure_ptr, AUTH_ARGS, pad(12)] # execute the auth procedure dyncall @@ -223,6 +229,8 @@ proc.execute_auth_procedure # clean up auth procedure outputs dropw dropw dropw dropw + + emit.EPILOGUE_AUTH_PROC_END_EVENT end # FEE PROCEDURES @@ -244,7 +252,7 @@ proc.compute_fee clk # => [num_current_cycles] - emit.EPILOGUE_AFTER_TX_CYCLES_OBTAINED + emit.EPILOGUE_AFTER_TX_CYCLES_OBTAINED_EVENT # estimate the number of cycles the transaction will take add.ESTIMATED_AFTER_COMPUTE_FEE_CYCLES @@ -309,7 +317,7 @@ proc.compute_and_remove_fee exec.build_native_fee_asset # => [FEE_ASSET] - emit.EPILOGUE_TX_FEE_COMPUTED + emit.EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT_EVENT # => [FEE_ASSET] # remove the fee from the native account's vault @@ -321,7 +329,7 @@ proc.compute_and_remove_fee # are essentially ignored. # fetch the vault root - exec.memory::get_acct_vault_root_ptr movdn.4 + exec.memory::get_account_vault_root_ptr movdn.4 # => [FEE_ASSET, acct_vault_root_ptr] # remove the asset from the account vault @@ -378,6 +386,11 @@ export.finalize_transaction.12 exec.execute_auth_procedure # => [] + # nonce cannot be 0 after the first transaction + exec.memory::get_native_account_nonce + # => [nonce] + eq.0 assertz.err=ERR_EPILOGUE_NONCE_CANNOT_BE_0 + # ------ Assert assets are preserved ------ # build the output vault representing the assets that were in the account at the end of the @@ -388,7 +401,7 @@ export.finalize_transaction.12 # assert no net creation or destruction of assets over the transaction exec.memory::get_input_vault_root exec.memory::get_output_vault_root assert_eqw.err=ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME - # => [ACCOUNT_UPDATE_COMMITMENT] + # => [] # ------ Compute output notes commitment ------ @@ -400,7 +413,7 @@ export.finalize_transaction.12 # => [OUTPUT_NOTES_COMMITMENT] # store commitment in local - loc_storew.0 dropw + loc_storew_be.0 dropw # => [] # ------ Compute account delta commitment ------ @@ -409,7 +422,7 @@ export.finalize_transaction.12 # => [ACCOUNT_DELTA_COMMITMENT] # store commitment in local - loc_storew.8 + loc_storew_be.8 # => [ACCOUNT_DELTA_COMMITMENT] # ------ Assert that account was changed or notes were consumed ------ @@ -438,21 +451,21 @@ export.finalize_transaction.12 # => [FEE_ASSET] # store fee asset in local - loc_storew.4 dropw + loc_storew_be.4 dropw # => [] # ------ Insert final account data into advice provider ------ # get the offset for the end of the account data section - exec.memory::get_core_acct_data_end_ptr + exec.memory::get_core_account_data_end_ptr # => [acct_data_end_ptr] # get the offset for the start of the account data section - exec.memory::get_current_account_data_ptr + exec.memory::get_active_account_data_ptr # => [acct_data_ptr, acct_data_end_ptr] # compute the final account commitment - exec.account::compute_current_commitment + exec.account::compute_commitment # => [FINAL_ACCOUNT_COMMITMENT, acct_data_ptr, acct_data_end_ptr] # insert final account data into the advice map @@ -466,7 +479,7 @@ export.finalize_transaction.12 # ------ Compute and insert account update commitment ------ # load account delta commitment from local - padw loc_loadw.8 + padw loc_loadw_be.8 # => [ACCOUNT_DELTA_COMMITMENT, FINAL_ACCOUNT_COMMITMENT] # insert into advice map ACCOUNT_UPDATE_COMMITMENT: (FINAL_ACCOUNT_COMMITMENT, ACCOUNT_DELTA_COMMITMENT), @@ -483,10 +496,10 @@ export.finalize_transaction.12 # => [ACCOUNT_UPDATE_COMMITMENT, tx_expiration_block_num] # load fee asset from local - padw loc_loadw.4 swapw + padw loc_loadw_be.4 swapw # => [ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET, tx_expiration_block_num] # load output notes commitment from local - padw loc_loadw.0 + padw loc_loadw_be.0 # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET, tx_expiration_block_num] end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/faucet.masm b/crates/miden-lib/asm/kernels/transaction/lib/faucet.masm index 38fcffa306..7cd5f4e515 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/faucet.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/faucet.masm @@ -1,5 +1,3 @@ -use.std::collections::smt - use.$kernel::account use.$kernel::account_id use.$kernel::asset diff --git a/crates/miden-lib/asm/kernels/transaction/lib/input_note.masm b/crates/miden-lib/asm/kernels/transaction/lib/input_note.masm index 43a1d73f9b..717bc67ee0 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/input_note.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/input_note.masm @@ -8,25 +8,19 @@ const.ERR_INPUT_NOTE_INDEX_OUT_OF_BOUNDS="requested input note index should be l # INPUT NOTE PROCEDURES # ================================================================================================= -#! Returns the information about assets in the input note with the specified index. +#! Returns the information about assets in the input note with the specified memory pointer. #! -#! The provided input note index is expected to be less than the total number of input notes. -#! -#! Inputs: [note_index] +#! Inputs: [input_note_ptr] #! Outputs: [ASSETS_COMMITMENT, num_assets] #! #! Where: -#! - note_index is the index of the input note whose assets info should be returned. +#! - 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 - # get the memory pointer to the requested note - exec.memory::get_input_note_ptr - # => [ptr] - # get the number of assets in the note dup exec.memory::get_input_note_num_assets - # => [num_assets, ptr] + # => [num_assets, input_note_ptr] # get the assets commitment from the note pointer swap exec.memory::get_input_note_assets_commitment @@ -46,3 +40,25 @@ export.assert_note_index_in_bounds u32lt assert.err=ERR_INPUT_NOTE_INDEX_OUT_OF_BOUNDS # => [note_index] end + +#! Computes a pointer to the memory address at which the data associated with an input note with +#! index `idx` is stored. +#! +#! Inputs: [idx] +#! Outputs: [note_ptr] +#! +#! 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`. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +export.get_input_note_ptr + # asset that the provided input note index is valid + exec.assert_note_index_in_bounds + # => [idx] + + # get the memory pointer to the specified input note + exec.memory::get_input_note_ptr + # => [note_ptr] +end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/link_map.masm b/crates/miden-lib/asm/kernels/transaction/lib/link_map.masm index ab9cbd18c7..bef4faffff 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/link_map.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/link_map.masm @@ -1,4 +1,5 @@ use.std::collections::smt +use.std::word use.$kernel::memory # A link map is a map data structure based on a sorted linked list. @@ -166,10 +167,10 @@ const.GET_OPERATION_ABSENT_AT_HEAD=1 # ================================================================================================= # Event emitted when an entry is set. -const.LINK_MAP_SET_EVENT=131100 +const.LINK_MAP_SET_EVENT=event("miden::link_map::set") # Event emitted when an entry is fetched. -const.LINK_MAP_GET_EVENT=131101 +const.LINK_MAP_GET_EVENT=event("miden::link_map::get") # LINK MAP PROCEDURES # ================================================================================================= @@ -198,7 +199,8 @@ const.LINK_MAP_GET_EVENT=131101 #! - the host provides faulty advice. See panic sections of assert_entry_ptr_is_valid, #! update_entry, insert_at_head, insert_after_entry. export.set - emit.LINK_MAP_SET_EVENT adv_push.2 + emit.LINK_MAP_SET_EVENT + adv_push.2 # => [operation, entry_ptr, map_ptr, KEY, VALUE0, VALUE1] dup.2 dup.2 @@ -264,7 +266,8 @@ end #! - 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 - emit.LINK_MAP_GET_EVENT adv_push.2 + emit.LINK_MAP_GET_EVENT + adv_push.2 # => [get_operation, entry_ptr, map_ptr, KEY] dup.2 dup.2 @@ -720,10 +723,10 @@ proc.set_value dup movdn.5 # => [entry_ptr, VALUE0, entry_ptr, VALUE1] - add.VALUE0_OFFSET mem_storew dropw + add.VALUE0_OFFSET mem_storew_be dropw # => [entry_ptr, VALUE1] - add.VALUE1_OFFSET mem_storew dropw + add.VALUE1_OFFSET mem_storew_be dropw # => [] end @@ -747,7 +750,7 @@ proc.get_value0 padw movup.4 # => [entry_ptr, pad(4)] - add.VALUE0_OFFSET mem_loadw + add.VALUE0_OFFSET mem_loadw_be # => [VALUE0] end @@ -759,7 +762,7 @@ proc.get_value1 padw movup.4 # => [entry_ptr, pad(4)] - add.VALUE1_OFFSET mem_loadw + add.VALUE1_OFFSET mem_loadw_be # => [VALUE1] end @@ -768,7 +771,7 @@ end #! Inputs: [entry_ptr, KEY] #! Outputs: [] proc.set_key - add.KEY_OFFSET mem_storew dropw + add.KEY_OFFSET mem_storew_be dropw end #! Returns the key of the entry pointer. @@ -779,7 +782,7 @@ proc.get_key padw movup.4 # => [entry_ptr, pad(4)] - add.KEY_OFFSET mem_loadw + add.KEY_OFFSET mem_loadw_be # => [KEY] end @@ -835,11 +838,10 @@ end #! Inputs: [entry_ptr, KEY] #! Outputs: [] proc.assert_key_is_greater - exec.get_key swapw - # => [KEY, ENTRY_KEY] + exec.get_key + # => [ENTRY_KEY, KEY] - exec.is_key_greater assert.err=ERR_LINK_MAP_PROVIDED_KEY_NOT_GREATER_THAN_ENTRY_KEY - # => [] + exec.word::gt assert.err=ERR_LINK_MAP_PROVIDED_KEY_NOT_GREATER_THAN_ENTRY_KEY end #! Asserts that the KEY is less than the key in the entry pointer. @@ -847,10 +849,10 @@ end #! Inputs: [entry_ptr, KEY] #! Outputs: [] proc.assert_key_is_less - exec.get_key swapw - # => [KEY, ENTRY_KEY] + exec.get_key + # => [ENTRY_KEY, KEY] - exec.is_key_less assert.err=ERR_LINK_MAP_PROVIDED_KEY_NOT_LESS_THAN_ENTRY_KEY + exec.word::lt assert.err=ERR_LINK_MAP_PROVIDED_KEY_NOT_LESS_THAN_ENTRY_KEY # => [] end @@ -925,143 +927,3 @@ export.assert_entry_ptr_is_valid # this assertion is always true if is_empty_map is true or assert.err=ERR_LINK_MAP_MAP_PTR_IN_ENTRY_DOES_NOT_MATCH_EXPECTED_MAP_PTR end - -# COMPARISON OPERATIONS -# ------------------------------------------------------------------------------------------------- - -#! Returns true if KEY1 is strictly greater than KEY2, false otherwise. -#! -#! The implementation avoids branching for performance reasons. -#! The procedure is exported for testing purposes only. -#! -#! For reference, this is equivalent to the following Rust function: -#! -#! fn is_key_greater(key1: Word, key2: Word) -> bool { -#! let mut result = false; -#! let mut cont = true; -#! -#! for i in (0..4).rev() { -#! let gt = key1[i].as_int() > key2[i].as_int(); -#! let eq = key1[i].as_int() == key2[i].as_int(); -#! result |= gt & cont; -#! cont &= eq; -#! } -#! -#! result -#! } -#! -#! Inputs: [KEY1, KEY2] -#! Outputs: [is_key_greater] -export.is_key_greater - exec.arrange_words_adjacent - # => [2_3, 1_3, 2_2, 1_2, 2_1, 1_1, 2_0, 1_0] - - push.1.0 - # => [is_key_greater, continue, 2_3, 1_3, 2_2, 1_2, 2_1, 1_1, 2_0, 1_0] - - repeat.4 - movup.3 movup.3 - # => [2_x, 1_x, is_key_greater, continue, ] - - # check 1_x == 2_x; if so, we continue - dup dup.2 eq - # => [is_felt_eq, 2_x, 1_x, is_key_greater, continue, ] - - movdn.3 - # => [2_x, 1_x, is_key_greater, is_felt_eq, continue, ] - - # check 1_x > 2_x - gt - # => [is_felt_gt, is_key_greater, is_felt_eq, continue, ] - - dup.3 and - # => [is_felt_gt_if_continue, is_key_greater, is_felt_eq, continue, ] - - or movdn.2 - # => [is_felt_eq, continue, is_key_greater, ] - - # keeps continue at 1 if the felts are equal - # sets continue to 0 if the felts are not equal - and - # => [continue, is_key_greater, ] - - swap - # => [is_key_greater, continue, ] - end - # => [is_key_greater, continue] - - swap drop - # => [is_key_greater] -end - -#! Returns true if KEY1 is strictly less than KEY2, false otherwise. -#! -#! The implementation avoids branching for performance reasons. -#! The procedure is exported for testing purposes only. -#! -#! From an implementation standpoint this is exactly the same as `is_key_greater` except it uses -#! `lt` rather than `gt`. See its docs for details. -#! -#! Inputs: [KEY1, KEY2] -#! Outputs: [is_key_less] -export.is_key_less - exec.arrange_words_adjacent - # => [2_3, 1_3, 2_2, 1_2, 2_1, 1_1, 2_0, 1_0] - - push.1.0 - # => [is_key_less, continue, 2_3, 1_3, 2_2, 1_2, 2_1, 1_1, 2_0, 1_0] - - repeat.4 - movup.3 movup.3 - # => [2_x, 1_x, is_key_less, continue, ] - - # check 1_x == 2_x; if so, we continue - dup dup.2 eq - # => [is_felt_eq, 2_x, 1_x, is_key_less, continue, ] - - movdn.3 - # => [2_x, 1_x, is_key_less, is_felt_eq, continue, ] - - # check 1_x < 2_x - lt - # => [is_felt_lt, is_key_less, is_felt_eq, continue, ] - - dup.3 and - # => [is_felt_lt_if_continue, is_key_less, is_felt_eq, continue, ] - - or movdn.2 - # => [is_felt_eq, continue, is_key_less, ] - - # keeps continue at 1 if the felts are equal - # sets continue to 0 if the felts are not equal - and - # => [continue, is_key_less, ] - - swap - # => [is_key_less, continue, ] - end - # => [is_key_less, continue] - - swap drop - # => [is_key_less] -end - -#! Arranges the given words such that the corresponding elements are next to each other. -#! -#! Inputs: [KEY1, KEY2] -#! Outputs: [key2_3, key1_3, key2_2, key1_2, key2_1, key1_1, key2_0, key1_0] -proc.arrange_words_adjacent - # => [1_3, 1_2, 1_1, 1_0, 2_3, 2_2, 2_1, 2_0] - - movup.3 movup.7 - # => [2_0, 1_0, 1_3, 1_2, 1_1, 2_3, 2_2, 2_1] - - movup.4 movup.7 - # => [2_1, 1_1, 2_0, 1_0, 1_3, 1_2, 2_3, 2_2] - - movup.5 movup.7 - # => [2_2, 1_2, 2_1, 1_1, 2_0, 1_0, 1_3, 2_3] - - movup.6 movup.7 - # => [2_3, 1_3, 2_2, 1_2, 2_1, 1_1, 2_0, 1_0] -end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/memory.masm b/crates/miden-lib/asm/kernels/transaction/lib/memory.masm index 58ad7e2ed3..5223ca6e48 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/memory.masm @@ -1,4 +1,3 @@ -use.$kernel::account use.$kernel::constants use.std::mem @@ -7,11 +6,11 @@ use.std::mem const.ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceed 255" -const.ERR_ACCOUNT_IS_NOT_NATIVE="the current 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_UNDERFLOW="failed to end foreign context because the current 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" @@ -23,8 +22,8 @@ const.ERR_LINK_MAP_MAX_ENTRIES_EXCEEDED="number of link map entries exceeds maxi # BOOK KEEPING # ------------------------------------------------------------------------------------------------- -# The memory address at which a pointer to the input note being executed is stored. -const.CURRENT_INPUT_NOTE_PTR=0 +# The memory address at which a pointer to the currently active input note is stored. +const.ACTIVE_INPUT_NOTE_PTR=0 # The memory address at which the number of output notes is stored. const.NUM_OUTPUT_NOTES_PTR=4 @@ -46,7 +45,7 @@ const.NATIVE_ACCT_STORAGE_COMMITMENT_DIRTY_FLAG_PTR=16 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 data is stored. +# currently accessing account (active account) data is stored. const.ACCOUNT_STACK_TOP_PTR=24 # Pointer to the first element on the account stack. @@ -147,11 +146,10 @@ const.PARTIAL_BLOCKCHAIN_PEAKS_PTR=1204 # KERNEL DATA # ------------------------------------------------------------------------------------------------- -# The memory address at which the number of the procedures of the selected kernel is stored. +# The memory address at which the number of the kernel procedures is stored. const.NUM_KERNEL_PROCEDURES_PTR=1600 # The memory address at which the hashes of kernel procedures begin. -# TODO: choose the proper memory location for the kernel procedures. const.KERNEL_PROCEDURES_PTR=1604 # ACCOUNT DATA @@ -295,29 +293,29 @@ export.set_num_output_notes mem_store.NUM_OUTPUT_NOTES_PTR end -#! Returns a pointer to the input note being executed. +#! Returns the pointer to the active input note. #! #! Inputs: [] #! Outputs: [note_ptr] #! #! Where: -#! - note_ptr is the memory address of the data segment for the current input note. -export.get_current_input_note_ptr - mem_load.CURRENT_INPUT_NOTE_PTR +#! - note_ptr is the memory address of the data segment for the active note. +export.get_active_input_note_ptr + mem_load.ACTIVE_INPUT_NOTE_PTR end -#! Sets the current input note pointer to the input note being executed. +#! Sets the active note pointer to the specified value. #! #! Inputs: [note_ptr] #! Outputs: [] #! #! Where: #! - note_ptr is the new memory address of the data segment for the input note. -export.set_current_input_note_ptr - mem_store.CURRENT_INPUT_NOTE_PTR +export.set_active_input_note_ptr + mem_store.ACTIVE_INPUT_NOTE_PTR end -#! Returns a pointer to the memory address at which the input vault root is stored. +#! Returns the pointer to the memory address at which the input vault root is stored. #! #! Inputs: [] #! Outputs: [input_vault_root_ptr] @@ -337,7 +335,7 @@ end #! Where: #! - INPUT_VAULT_ROOT is the input vault root. export.get_input_vault_root - padw mem_loadw.INPUT_VAULT_ROOT_PTR + padw mem_loadw_be.INPUT_VAULT_ROOT_PTR end #! Sets the input vault root. @@ -348,10 +346,10 @@ end #! Where: #! - INPUT_VAULT_ROOT is the input vault root. export.set_input_vault_root - mem_storew.INPUT_VAULT_ROOT_PTR + mem_storew_be.INPUT_VAULT_ROOT_PTR end -#! Returns a pointer to the memory address at which the output vault root is stored. +#! Returns the pointer to the memory address at which the output vault root is stored. #! #! Inputs: [] #! Outputs: [output_vault_root_ptr] @@ -371,7 +369,7 @@ end #! Where: #! - OUTPUT_VAULT_ROOT is the output vault root. export.get_output_vault_root - padw mem_loadw.OUTPUT_VAULT_ROOT_PTR + padw mem_loadw_be.OUTPUT_VAULT_ROOT_PTR end #! Sets the output vault root. @@ -382,7 +380,7 @@ end #! Where: #! - OUTPUT_VAULT_ROOT is the output vault root. export.set_output_vault_root - mem_storew.OUTPUT_VAULT_ROOT_PTR + mem_storew_be.OUTPUT_VAULT_ROOT_PTR end # GLOBAL INPUTS @@ -396,7 +394,7 @@ end #! Where: #! - BLOCK_COMMITMENT is the commitment of the transaction reference block. export.set_block_commitment - mem_storew.BLOCK_COMMITMENT_PTR + mem_storew_be.BLOCK_COMMITMENT_PTR end #! Returns the block commitment of the reference block. @@ -407,20 +405,20 @@ end #! Where: #! - BLOCK_COMMITMENT is the commitment of the transaction reference block. export.get_block_commitment - padw mem_loadw.BLOCK_COMMITMENT_PTR + padw mem_loadw_be.BLOCK_COMMITMENT_PTR end #! Sets the ID of the native account. #! -#! Inputs: [acct_id_prefix, acct_id_suffix] +#! Inputs: [account_id_prefix, account_id_suffix] #! Outputs: [] #! #! Where: -#! - acct_id_{prefix,suffix} are the prefix and suffix felts of the account ID. -export.set_global_acct_id +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the account ID. +export.set_global_account_id push.0.0 - # => [0, 0, acct_id_prefix, acct_id_suffix] - mem_storew.NATIVE_ACCT_ID_PTR + # => [0, 0, account_id_prefix, account_id_suffix] + mem_storew_be.NATIVE_ACCT_ID_PTR dropw # => [] end @@ -428,13 +426,13 @@ end #! Returns the ID of the native account. #! #! Inputs: [] -#! Outputs: [acct_id_prefix, acct_id_suffix] +#! Outputs: [account_id_prefix, account_id_suffix] #! #! Where: -#! - acct_id_{prefix,suffix} are the prefix and suffix felts of the account ID. -export.get_global_acct_id - padw mem_loadw.NATIVE_ACCT_ID_PTR - # => [0, 0, acct_id_prefix, acct_id_suffix] +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the account ID. +export.get_global_account_id + padw mem_loadw_be.NATIVE_ACCT_ID_PTR + # => [0, 0, account_id_prefix, account_id_suffix] drop drop end @@ -446,7 +444,7 @@ end #! Where: #! - INIT_ACCOUNT_COMMITMENT is the initial account commitment. export.set_init_account_commitment - mem_storew.INIT_ACCOUNT_COMMITMENT_PTR + mem_storew_be.INIT_ACCOUNT_COMMITMENT_PTR end #! Returns the native account commitment at the beginning of the transaction. @@ -457,7 +455,7 @@ end #! Where: #! - INIT_ACCOUNT_COMMITMENT is the initial account commitment. export.get_init_account_commitment - padw mem_loadw.INIT_ACCOUNT_COMMITMENT_PTR + padw mem_loadw_be.INIT_ACCOUNT_COMMITMENT_PTR end #! Sets the initial account nonce. @@ -489,8 +487,8 @@ end #! #! Where: #! - INIT_NATIVE_ACCOUNT_VAULT_ROOT is the initial vault root of the native account. -export.set_init_account_vault_root - mem_storew.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR +export.set_init_native_account_vault_root + mem_storew_be.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR end #! Returns the vault root of the native account at the beginning of the transaction. @@ -500,8 +498,21 @@ end #! #! Where: #! - INIT_NATIVE_ACCOUNT_VAULT_ROOT is the initial vault root of the native account. -export.get_init_account_vault_root - padw mem_loadw.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR +export.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 +#! 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. +export.get_init_native_account_vault_root_ptr + push.INIT_NATIVE_ACCOUNT_VAULT_ROOT_PTR end #! Sets the storage commitment of the native account at the beginning of the transaction. @@ -512,7 +523,7 @@ end #! Where: #! - INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT is the initial storage commitment of the native account. export.set_init_account_storage_commitment - mem_storew.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR + mem_storew_be.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR end #! Returns the storage commitment of the native account at the beginning of the transaction. @@ -523,7 +534,7 @@ end #! Where: #! - INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT is the initial storage commitment of the native account. export.get_init_account_storage_commitment - padw mem_loadw.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR + padw mem_loadw_be.INIT_NATIVE_ACCOUNT_STORAGE_COMMITMENT_PTR end #! Returns the input notes commitment. @@ -536,7 +547,7 @@ end #! Where: #! - INPUT_NOTES_COMMITMENT is the input notes commitment. export.get_input_notes_commitment - padw mem_loadw.INPUT_NOTES_COMMITMENT_PTR + padw mem_loadw_be.INPUT_NOTES_COMMITMENT_PTR end #! Sets the input notes' commitment. @@ -547,10 +558,10 @@ end #! Where: #! - INPUT_NOTES_COMMITMENT is the notes' commitment. export.set_nullifier_commitment - mem_storew.INPUT_NOTES_COMMITMENT_PTR + mem_storew_be.INPUT_NOTES_COMMITMENT_PTR end -#! Returns a memory address of the transaction script root. +#! Returns the memory address of the transaction script root. #! #! Inputs: [] #! Outputs: [tx_script_root_ptr] @@ -569,7 +580,7 @@ end #! Where: #! - TX_SCRIPT_ROOT is the transaction script root. export.set_tx_script_root - mem_storew.TX_SCRIPT_ROOT_PTR + mem_storew_be.TX_SCRIPT_ROOT_PTR end #! Returns the transaction script arguments. @@ -581,7 +592,7 @@ end #! - 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 - padw mem_loadw.TX_SCRIPT_ARGS_PTR + padw mem_loadw_be.TX_SCRIPT_ARGS_PTR end #! Sets the transaction script arguments. @@ -593,7 +604,7 @@ end #! - 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 - mem_storew.TX_SCRIPT_ARGS_PTR + mem_storew_be.TX_SCRIPT_ARGS_PTR end #! Returns the auth procedure arguments. @@ -604,7 +615,7 @@ end #! Where: #! - AUTH_ARGS is the argument passed to the auth procedure. export.get_auth_args - padw mem_loadw.AUTH_ARGS_PTR + padw mem_loadw_be.AUTH_ARGS_PTR end #! Sets the auth procedure arguments. @@ -615,7 +626,7 @@ end #! Where: #! - AUTH_ARGS is the argument passed to the auth procedure. export.set_auth_args - mem_storew.AUTH_ARGS_PTR + mem_storew_be.AUTH_ARGS_PTR end # BLOCK DATA @@ -640,7 +651,7 @@ end #! Where: #! - PREV_BLOCK_COMMITMENT_PTR is the block commitment of the transaction reference block. export.get_prev_block_commitment - padw mem_loadw.PREV_BLOCK_COMMITMENT_PTR + padw mem_loadw_be.PREV_BLOCK_COMMITMENT_PTR end #! Returns the block number of the transaction reference block. @@ -687,7 +698,7 @@ end #! - 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 - padw mem_loadw.FEE_PARAMETERS_PTR drop drop + padw mem_loadw_be.FEE_PARAMETERS_PTR drop drop # => [native_asset_id_prefix, native_asset_id_suffix] end @@ -711,7 +722,7 @@ end #! Where: #! - CHAIN_COMMITMENT is the chain commitment of the transaction reference block. export.get_chain_commitment - padw mem_loadw.CHAIN_COMMITMENT_PTR + padw mem_loadw_be.CHAIN_COMMITMENT_PTR end #! Returns the account db root of the transaction reference block. @@ -721,8 +732,8 @@ end #! #! Where: #! - ACCT_DB_ROOT is the account database root of the transaction reference block. -export.get_acct_db_root - padw mem_loadw.ACCT_DB_ROOT_PTR +export.get_account_db_root + padw mem_loadw_be.ACCT_DB_ROOT_PTR end #! Returns the nullifier db root of the transaction reference block. @@ -733,7 +744,7 @@ end #! Where: #! - NULLIFIER_ROOT is the nullifier root of the transaction reference block. export.get_nullifier_db_root - padw mem_loadw.NULLIFIER_ROOT_PTR + padw mem_loadw_be.NULLIFIER_ROOT_PTR end #! Returns the tx commitment of the transaction reference block. @@ -744,7 +755,7 @@ end #! Where: #! - TX_COMMITMENT is the tx commitment of the transaction reference block. export.get_tx_commitment - padw mem_loadw.TX_COMMITMENT_PTR + padw mem_loadw_be.TX_COMMITMENT_PTR end #! Returns the transaction kernel commitment of the transaction reference block. @@ -753,9 +764,9 @@ end #! Outputs: [TX_KERNEL_COMMITMENT] #! #! Where: -#! - TX_KERNEL_COMMITMENT is an accumulative hash from all kernel commitments. +#! - TX_KERNEL_COMMITMENT is the sequential hash of the kernel procedures. export.get_tx_kernel_commitment - padw mem_loadw.TX_KERNEL_COMMITMENT_PTR + padw mem_loadw_be.TX_KERNEL_COMMITMENT_PTR end #! Returns the proof commitment of the transaction reference block. @@ -766,7 +777,7 @@ end #! Where: #! - PROOF_COMMITMENT is the proof commitment of the transaction reference block. export.get_proof_commitment - padw mem_loadw.PROOF_COMMITMENT_PTR + padw mem_loadw_be.PROOF_COMMITMENT_PTR end #! Returns the note root of the transaction reference block. @@ -777,7 +788,7 @@ end #! Where: #! - NOTE_ROOT is the note root of the transaction reference block. export.get_note_root - padw mem_loadw.NOTE_ROOT_PTR + padw mem_loadw_be.NOTE_ROOT_PTR end #! Sets the note root of the transaction reference block. @@ -788,7 +799,7 @@ end #! Where: #! - NOTE_ROOT is the note root of the transaction reference block. export.set_note_root - mem_storew.NOTE_ROOT_PTR + mem_storew_be.NOTE_ROOT_PTR end # CHAIN DATA @@ -844,10 +855,10 @@ end #! Returns the length of the memory interval that the account data occupies. #! #! Inputs: [] -#! Outputs: [acct_data_length] +#! Outputs: [account_data_length] #! #! Where: -#! - acct_data_length is the length of the memory interval that the account data occupies. +#! - account_data_length is the length of the memory interval that the account data occupies. export.get_account_data_length push.ACCOUNT_DATA_LENGTH end @@ -855,49 +866,42 @@ end #! Returns the largest memory address which can be used to load the foreign account data. #! #! Inputs: [] -#! Outputs: [max_foreign_acct_ptr] +#! Outputs: [max_foreign_account_ptr] #! #! Where: -#! - max_foreign_acct_ptr is the largest memory address which can be used to load the foreign +#! - 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 push.MAX_FOREIGN_ACCOUNT_PTR end -#! Sets the memory pointer of the current account data to the native account (8192). +#! Sets the memory pointer of the active account data to the native account (8192). #! #! Inputs: [] #! Outputs: [] -export.set_current_account_data_ptr_to_native_account - # get the pointer to the first stack element where pointer to the native account data should be - # stored - push.ACCOUNT_STACK_TOP_PTR - push.MIN_ACCOUNT_STACK_PTR +export.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] - # store the native account data pointer into the first stack element. - push.NATIVE_ACCOUNT_DATA_PTR dup.1 mem_store - # => [native_acct_stack_ptr, account_stack_top_ptr] - - # store the pointer to the first account stack element at the account stack top - # pointer. - swap mem_store + # store the pointer to the first account stack element into the account stack top pointer. + push.MIN_ACCOUNT_STACK_PTR mem_store.ACCOUNT_STACK_TOP_PTR # => [] end -#! Returns the memory pointer of the current account data. +#! Returns the memory pointer of the active account data. #! #! Inputs: [] -#! Outputs: [ptr] +#! Outputs: [active_account_data_ptr] #! #! Where: -#! - ptr is the memory address at which the data of the currently used account begins. -export.get_current_account_data_ptr +#! - active_account_data_ptr is the memory address at which the data of the active account begins. +export.get_active_account_data_ptr mem_load.ACCOUNT_STACK_TOP_PTR # => [account_stack_top_ptr] mem_load - # => [current_account_data_ptr] + # => [active_account_data_ptr] end #! Adds the pointer to the account stack. @@ -922,7 +926,7 @@ export.push_ptr_to_account_stack u32lt.MAX_ACCOUNT_STACK_PTR assert.err=ERR_ACCOUNT_STACK_OVERFLOW # => [account_stack_top_ptr, curr_account_data_ptr] - # check that the current account data pointer is not equal to the native account data pointer + # check that the active account data pointer is not equal to the native account data pointer dup.1 eq.NATIVE_ACCOUNT_DATA_PTR assertz.err=ERR_FOREIGN_ACCOUNT_CONTEXT_AGAINST_NATIVE_ACCOUNT # => [account_stack_top_ptr, curr_account_data_ptr] @@ -957,7 +961,7 @@ export.pop_ptr_from_account_stack # => [] end -#! Asserts that current account data pointer matches the data pointer of the native account (8192). +#! Asserts that active account data pointer matches the data pointer of the native account (8192). #! It is used to prevent usage of the account procedures which can mutate the account state with the #! foreign accounts. #! @@ -965,19 +969,19 @@ end #! Outputs: [] #! #! Panics if: -#! - the current account data pointer is not equal to native account data pointer (8192). +#! - the active account data pointer is not equal to native account data pointer (8192). export.assert_native_account exec.is_native_account assert.err=ERR_ACCOUNT_IS_NOT_NATIVE end -#! Returns 1 if the current account data pointer matches the data pointer of the native account, +#! Returns 1 if the active account data pointer matches the data pointer of the native account, #! 0 otherwise. #! #! Inputs: [] #! Outputs: [is_native_account] export.is_native_account - exec.get_current_account_data_ptr - # => [current_account_data_ptr] + exec.get_active_account_data_ptr + # => [active_account_data_ptr] eq.NATIVE_ACCOUNT_DATA_PTR # => [is_native_account] @@ -990,81 +994,91 @@ end #! #! Where: #! - ptr is the memory address at which the core account data ends. -export.get_core_acct_data_end_ptr - exec.get_current_account_data_ptr add.ACCT_CORE_DATA_SECTION_END_OFFSET +export.get_core_account_data_end_ptr + exec.get_active_account_data_ptr add.ACCT_CORE_DATA_SECTION_END_OFFSET end ### ACCOUNT ID AND NONCE ################################################# -#! Returns the id of the current account. +#! Returns the ID of the active account. #! #! Inputs: [] -#! Outputs: [curr_acct_id_prefix, curr_acct_id_suffix] +#! Outputs: [account_id_prefix, account_id_suffix] #! #! Where: -#! - curr_acct_id_{prefix,suffix} are the prefix and suffix felts of the account ID of the currently -#! accessing account. +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. export.get_account_id - padw exec.get_current_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET mem_loadw - # => [nonce, 0, curr_acct_id_prefix, curr_acct_id_suffix] + 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 - # => [curr_acct_id_prefix, curr_acct_id_suffix] + # => [account_id_prefix, account_id_suffix] +end + +#! Returns the ID of the native account of the transaction. +#! +#! Inputs: [] +#! Outputs: [account_id_prefix, account_id_suffix] +#! +#! 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 + 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 + # => [account_id_prefix, account_id_suffix] end #! Sets the account ID and nonce. #! -#! Inputs: [account_nonce, 0, account_id_prefix, account_id_suffix] -#! Outputs: [account_nonce, 0, account_id_prefix, account_id_suffix] +#! Inputs: [nonce, 0, account_id_prefix, account_id_suffix] +#! Outputs: [nonce, 0, account_id_prefix, account_id_suffix] #! #! Where: -#! - account_id_{prefix,suffix} are the prefix and suffix felts of the id of the currently accessing -#! account. -#! - account_nonce is the nonce of the currently accessing account. -export.set_acct_id_and_nonce - exec.get_current_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET - mem_storew +#! - 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 + exec.get_active_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET + mem_storew_be end -#! Returns the id of the native account. +#! Returns the nonce of the active account. #! #! Inputs: [] -#! Outputs: [native_acct_id_prefix, native_acct_id_suffix] +#! Outputs: [nonce] #! #! Where: -#! - native_acct_id_{prefix,suffix} are the prefix and suffix felts of the id of the native account. -export.get_native_account_id - padw push.NATIVE_ACCOUNT_DATA_PTR add.ACCT_ID_AND_NONCE_OFFSET mem_loadw - # => [nonce, 0, native_acct_id_prefix, native_acct_id_suffix] - drop drop - # => [native_acct_id_prefix, native_acct_id_suffix] +#! - nonce is the nonce of the active account. +export.get_account_nonce + exec.get_active_account_data_ptr add.ACCT_NONCE_OFFSET + mem_load end -#! Returns the account nonce. +#! Returns the nonce of the native account of the transaction. #! #! Inputs: [] -#! Outputs: [acct_nonce] +#! Outputs: [nonce] #! #! Where: -#! - acct_nonce is the account nonce. - -export.get_acct_nonce - exec.get_current_account_data_ptr add.ACCT_NONCE_OFFSET +#! - nonce is the nonce of the native account of the transaction. +export.get_native_account_nonce + push.NATIVE_ACCOUNT_DATA_PTR add.ACCT_NONCE_OFFSET mem_load end -#! Sets the account nonce. +#! Sets the nonce of the active account. #! -#! Inputs: [acct_nonce] +#! Inputs: [nonce] #! Outputs: [] #! #! Where: -#! - acct_nonce is the account nonce. -export.set_acct_nonce - exec.get_current_account_data_ptr add.ACCT_ID_AND_NONCE_OFFSET padw - # => [0, 0, 0, 0, acct_id_and_nonce_ptr, new_nonce] - dup.4 mem_loadw - # => [old_nonce, 0, old_id_prefix, old_id_suffix, acct_id_and_nonce_ptr, new_nonce] - drop movup.4 movup.4 mem_storew dropw +#! - nonce is the nonce of the active account. +export.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 + # => [old_nonce, 0, old_id_prefix, old_id_suffix, account_id_and_nonce_ptr, new_nonce] + drop movup.4 movup.4 mem_storew_be dropw # => [] end @@ -1073,12 +1087,12 @@ end #! Returns the memory pointer to the account vault root. #! #! Inputs: [] -#! Outputs: [acct_vault_root_ptr] +#! Outputs: [account_vault_root_ptr] #! #! Where: -#! - acct_vault_root_ptr is the memory pointer to the account asset vault root. -export.get_acct_vault_root_ptr - exec.get_current_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET +#! - account_vault_root_ptr is the memory pointer to the account asset vault root. +export.get_account_vault_root_ptr + exec.get_active_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET end #! Returns the account vault root. @@ -1088,10 +1102,10 @@ end #! #! Where: #! - ACCT_VAULT_ROOT is the account asset vault root. -export.get_acct_vault_root +export.get_account_vault_root padw - exec.get_current_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET - mem_loadw + exec.get_active_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET + mem_loadw_be end #! Sets the account vault root. @@ -1101,9 +1115,39 @@ end #! #! Where: #! - ACCT_VAULT_ROOT is the account vault root to be set. -export.set_acct_vault_root - exec.get_current_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET - mem_storew +export.set_account_vault_root + exec.get_active_account_data_ptr add.ACCT_VAULT_ROOT_OFFSET + mem_storew_be +end + +#! Returns the memory pointer to the initial vault root of the active account. +#! +#! For the native account, this returns the pointer to the initial vault root. +#! For foreign accounts, this returns the regular vault root pointer since foreign accounts +#! are read-only and their initial and current vault state always matches. +#! +#! Inputs: [] +#! Outputs: [account_initial_vault_root_ptr] +#! +#! 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 + # and initial == current + exec.get_account_vault_root_ptr + # => [account_vault_root_ptr] + + # For native account, use the initial vault root pointer + exec.get_init_native_account_vault_root_ptr + # => [native_account_initial_vault_root_ptr, account_vault_root_ptr] + + # get the flag indicating whether the active account is native + exec.is_native_account + # => [is_native_account, native_account_initial_vault_root_ptr, account_vault_root_ptr] + + # according to the is_native_account flag, return the corresponding pointer + cdrop + # => [account_initial_vault_root_ptr] end ### ACCOUNT CODE ################################################# @@ -1115,10 +1159,10 @@ end #! #! Where: #! - CODE_COMMITMENT is the code commitment of the account. -export.get_acct_code_commitment +export.get_account_code_commitment padw - exec.get_current_account_data_ptr add.ACCT_CODE_COMMITMENT_OFFSET - mem_loadw + exec.get_active_account_data_ptr add.ACCT_CODE_COMMITMENT_OFFSET + mem_loadw_be end #! Sets the code commitment of the account. @@ -1128,9 +1172,9 @@ end #! #! Where: #! - CODE_COMMITMENT is the code commitment to be set. -export.set_acct_code_commitment - exec.get_current_account_data_ptr add.ACCT_CODE_COMMITMENT_OFFSET - mem_storew +export.set_account_code_commitment + exec.get_active_account_data_ptr add.ACCT_CODE_COMMITMENT_OFFSET + mem_storew_be end #! Sets the transaction expiration block number. @@ -1163,7 +1207,7 @@ end #! Where: #! - num_procedures is the number of procedures contained in the account code. export.get_num_account_procedures - exec.get_current_account_data_ptr add.NUM_ACCT_PROCEDURES_OFFSET + exec.get_active_account_data_ptr add.NUM_ACCT_PROCEDURES_OFFSET mem_load end @@ -1175,7 +1219,7 @@ end #! Where: #! - num_procedures is the number of procedures contained in the account code. export.set_num_account_procedures - exec.get_current_account_data_ptr add.NUM_ACCT_PROCEDURES_OFFSET + exec.get_active_account_data_ptr add.NUM_ACCT_PROCEDURES_OFFSET mem_store end @@ -1186,8 +1230,8 @@ end #! #! Where: #! - account_procedures_section_ptr is the memory pointer to the account procedures section. -export.get_acct_procedures_section_ptr - exec.get_current_account_data_ptr add.ACCT_PROCEDURES_SECTION_OFFSET +export.get_account_procedures_section_ptr + exec.get_active_account_data_ptr add.ACCT_PROCEDURES_SECTION_OFFSET end #! Returns the memory pointer to the account procedures call tracking section. @@ -1197,8 +1241,8 @@ end #! #! Where: #! - procedures_call_tracking_ptr is the memory pointer to the procedure call tracking section. -export.get_acct_procedures_call_tracking_ptr - exec.get_current_account_data_ptr add.ACCT_PROCEDURES_CALL_TRACKING_OFFSET +export.get_account_procedures_call_tracking_ptr + exec.get_active_account_data_ptr add.ACCT_PROCEDURES_CALL_TRACKING_OFFSET end #! Returns the memory pointer to a specific account procedure. @@ -1209,8 +1253,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_acct_procedure_ptr - mul.8 exec.get_acct_procedures_section_ptr add +export.get_account_procedure_ptr + mul.8 exec.get_account_procedures_section_ptr add end ### ACCOUNT STORAGE ################################################# @@ -1222,10 +1266,10 @@ end #! #! Where: #! - STORAGE_COMMITMENT is the account storage commitment. -export.get_acct_storage_commitment +export.get_account_storage_commitment padw - exec.get_current_account_data_ptr add.ACCT_STORAGE_COMMITMENT_OFFSET - mem_loadw + exec.get_active_account_data_ptr add.ACCT_STORAGE_COMMITMENT_OFFSET + mem_loadw_be end #! Sets the account storage commitment. @@ -1235,9 +1279,9 @@ end #! #! Where: #! - STORAGE_COMMITMENT is the account storage commitment. -export.set_acct_storage_commitment - exec.get_current_account_data_ptr add.ACCT_STORAGE_COMMITMENT_OFFSET - mem_storew +export.set_account_storage_commitment + exec.get_active_account_data_ptr add.ACCT_STORAGE_COMMITMENT_OFFSET + mem_storew_be end #! Sets the dirty flag for the native account storage commitment. @@ -1257,7 +1301,7 @@ end #! Returns the flag indicating whether the account storage commitment should be recomputed. #! -#! This flag equals 1 if the current account is native and the storage commitment is outdated. +#! This flag equals 1 if the active account is native and the storage commitment is outdated. #! #! Inputs: [] #! Outputs: [should_recompute_storage_commitment] @@ -1287,7 +1331,7 @@ end #! Where: #! - num_storage_slots is the number of storage slots contained in the account storage. export.get_num_storage_slots - exec.get_current_account_data_ptr add.NUM_ACCT_STORAGE_SLOTS_OFFSET + exec.get_active_account_data_ptr add.NUM_ACCT_STORAGE_SLOTS_OFFSET mem_load end @@ -1299,13 +1343,13 @@ end #! Where: #! - num_storage_slots is the number of storage slots contained in the account storage. export.set_num_storage_slots - exec.get_current_account_data_ptr add.NUM_ACCT_STORAGE_SLOTS_OFFSET + exec.get_active_account_data_ptr add.NUM_ACCT_STORAGE_SLOTS_OFFSET mem_store end #! Returns the type of the requested storage slot. #! -#! Inputs: [acct_storage_slots_section_ptr, index] +#! Inputs: [account_storage_slots_section_ptr, index] #! Outputs: [slot_type] #! #! Where: @@ -1324,8 +1368,8 @@ end #! #! Where: #! - storage_slots_section_ptr is the memory pointer to the account storage slots section. -export.get_acct_storage_slots_section_ptr - exec.get_current_account_data_ptr add.ACCT_STORAGE_SLOTS_SECTION_OFFSET +export.get_account_storage_slots_section_ptr + exec.get_active_account_data_ptr add.ACCT_STORAGE_SLOTS_SECTION_OFFSET end #! Returns the memory pointer to the native account's storage slots section. @@ -1350,6 +1394,36 @@ export.get_native_account_initial_storage_slots_ptr exec.get_native_account_data_ptr add.ACCT_INITIAL_STORAGE_SLOTS_SECTION_OFFSET end +#! Returns the memory pointer to the initial storage slots of the active account. +#! +#! For the native account, this returns the pointer to the initial storage slots section. +#! For foreign accounts, this returns the regular storage slots pointer since foreign accounts +#! are read-only and their initial and current storage state always matches. +#! +#! Inputs: [] +#! Outputs: [account_initial_storage_slots_ptr] +#! +#! 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 + # read-only and initial == current + exec.get_account_storage_slots_section_ptr + # => [account_storage_slots_ptr] + + # For native account, use the initial storage slots pointer + exec.get_native_account_initial_storage_slots_ptr + # => [native_account_initial_storage_slots_ptr, account_storage_slots_ptr] + + # get the flag indicating whether the active account is native + exec.is_native_account + # => [is_native_account, native_account_initial_storage_slots_ptr, account_storage_slots_ptr] + + # according to the is_native_account flag, return the corresponding pointer + cdrop + # => [account_initial_storage_slots_ptr] +end + ### ACCOUNT DELTA ################################################# #! Returns the link map pointer to the fungible asset vault delta. @@ -1360,7 +1434,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 - push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR + push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR end #! Returns the link map pointer to the non-fungible asset vault delta. @@ -1390,7 +1464,7 @@ end #! #! Inputs: [] #! Outputs: [] -export.mem_copy_initial_storage_slots +export.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] @@ -1450,7 +1524,7 @@ end #! - note_ptr is the input note's the memory address. #! - NOTE_ID is the note's id. export.set_input_note_id - mem_storew + mem_storew_be end #! Computes a pointer to the memory address at which the nullifier associated a note with `idx` is @@ -1475,7 +1549,7 @@ end #! - idx is the index of the input note. #! - nullifier is the nullifier of the input note. export.get_input_note_nullifier - mul.4 padw movup.4 add.INPUT_NOTE_NULLIFIER_SECTION_PTR mem_loadw + mul.4 padw movup.4 add.INPUT_NOTE_NULLIFIER_SECTION_PTR mem_loadw_be end #! Returns a pointer to the start of the input note core data segment for the note located at the @@ -1502,7 +1576,7 @@ end export.get_input_note_script_root padw movup.4 add.INPUT_NOTE_SCRIPT_ROOT_OFFSET - mem_loadw + mem_loadw_be end #! Returns the memory address of the script root of an input note. @@ -1528,7 +1602,7 @@ end export.get_input_note_inputs_commitment padw movup.4 add.INPUT_NOTE_INPUTS_COMMITMENT_OFFSET - mem_loadw + mem_loadw_be end #! Returns the metadata of an input note located at the specified memory address. @@ -1542,7 +1616,7 @@ end export.get_input_note_metadata padw movup.4 add.INPUT_NOTE_METADATA_OFFSET - mem_loadw + mem_loadw_be end #! Sets the metadata for an input note located at the specified memory address. @@ -1555,7 +1629,7 @@ end #! - NOTE_METADATA is the metadata of the input note. export.set_input_note_metadata add.INPUT_NOTE_METADATA_OFFSET - mem_storew + mem_storew_be end #! Returns the note's args. @@ -1569,7 +1643,7 @@ end export.get_input_note_args padw movup.4 add.INPUT_NOTE_ARGS_OFFSET - mem_loadw + mem_loadw_be end #! Sets the note args for an input note located at the specified memory address. @@ -1582,23 +1656,23 @@ end #! - NOTE_ARGS are optional note args of the input note. export.set_input_note_args add.INPUT_NOTE_ARGS_OFFSET - mem_storew + mem_storew_be end -#! Returns the number of inputs of the note currently being processed. +#! Returns the number of inputs of the note located at the specified memory address. #! -#! Inputs: [] +#! Inputs: [note_ptr] #! Outputs: [num_inputs] #! #! Where: -#! - num_inputs is the number of inputs in the input note. +#! - 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 - exec.get_current_input_note_ptr add.INPUT_NOTE_NUM_INPUTS_OFFSET mem_load end -#! Sets the number of input values for an input note located at the specified memory address. +#! Sets the number of inputs for an input note located at the specified memory address. #! #! Inputs: [note_ptr, num_inputs] #! Outputs: [] @@ -1661,7 +1735,7 @@ end export.get_input_note_recipient padw movup.4 add.INPUT_NOTE_RECIPIENT_OFFSET - mem_loadw + mem_loadw_be end #! Sets the input note's recipient. @@ -1674,7 +1748,7 @@ end #! - RECIPIENT is the commitment to the note's script, inputs and the serial number. export.set_input_note_recipient add.INPUT_NOTE_RECIPIENT_OFFSET - mem_storew + mem_storew_be end #! Returns the assets commitment for the input note located at the specified memory address. @@ -1688,7 +1762,7 @@ end export.get_input_note_assets_commitment padw movup.4 add.INPUT_NOTE_ASSETS_COMMITMENT_OFFSET - mem_loadw + mem_loadw_be end #! Returns the serial number for the input note located at the specified memory address. @@ -1702,7 +1776,7 @@ end export.get_input_note_serial_num padw movup.4 add.INPUT_NOTE_SERIAL_NUM_OFFSET - mem_loadw + mem_loadw_be end #! Returns the sender for the input note located at the specified memory address. @@ -1716,7 +1790,7 @@ end export.get_input_note_sender padw movup.4 add.INPUT_NOTE_METADATA_OFFSET - mem_loadw + mem_loadw_be # => [aux, merged_tag_hint_payload, merged_sender_id_type_hint_tag, sender_id_prefix] drop drop @@ -1733,7 +1807,6 @@ export.get_input_note_sender # 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 @@ -1774,7 +1847,7 @@ end export.get_output_note_recipient padw movup.4 add.OUTPUT_NOTE_RECIPIENT_OFFSET - mem_loadw + mem_loadw_be end #! Sets the output note's recipient. @@ -1787,7 +1860,7 @@ end #! - RECIPIENT is the commitment to the note's script, inputs and the serial number. export.set_output_note_recipient add.OUTPUT_NOTE_RECIPIENT_OFFSET - mem_storew + mem_storew_be end #! Returns the output note's metadata. @@ -1800,8 +1873,11 @@ end #! - note_ptr is the memory address at which the output note data begins. export.get_output_note_metadata padw + # => [0, 0, 0, 0, note_ptr] movup.4 add.OUTPUT_NOTE_METADATA_OFFSET - mem_loadw + # => [(note_ptr + offset), 0, 0, 0, 0] + mem_loadw_be + # => [METADATA] end #! Sets the output note's metadata. @@ -1814,7 +1890,7 @@ end #! - note_ptr is the memory address at which the output note data begins. export.set_output_note_metadata add.OUTPUT_NOTE_METADATA_OFFSET - mem_storew + mem_storew_be end #! Returns the number of assets in the output note. @@ -1903,7 +1979,7 @@ end export.get_output_note_assets_commitment padw movup.4 add.OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET - mem_loadw + mem_loadw_be end #! Sets the output note assets commitment for the output note located at the specified memory @@ -1917,7 +1993,7 @@ end #! - ASSETS_COMMITMENT is the sequential hash of the padded assets of an output note. export.set_output_note_assets_commitment add.OUTPUT_NOTE_ASSETS_COMMITMENT_OFFSET - mem_storew + mem_storew_be end # KERNEL DATA diff --git a/crates/miden-lib/asm/kernels/transaction/lib/note.masm b/crates/miden-lib/asm/kernels/transaction/lib/note.masm index 4b07767651..b884a3728f 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/note.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/note.masm @@ -6,12 +6,6 @@ use.$kernel::memory # ERRORS # ================================================================================================= -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SENDER_FROM_INCORRECT_CONTEXT="attempted to access note sender from incorrect context" - -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_FROM_INCORRECT_CONTEXT="attempted to access note assets from incorrect context" - -const.ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_FROM_INCORRECT_CONTEXT="attempted to access note inputs from incorrect context" - const.ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceed 255" # CONSTANTS @@ -21,118 +15,38 @@ const.ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT="number of assets in a note exceed 255 # generating the output notes commitment. Must be NOTE_MEM_SIZE - 8; const.OUTPUT_NOTE_HASHING_MEM_DIFF=2040 -# CURRENTLY EXECUTING NOTE PROCEDURES +# ACTIVE NOTE PROCEDURES # ================================================================================================= -#! Returns the sender of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [sender_id_prefix, sender_id_suffix] -#! -#! Where: -#! - sender_{prefix,suffix} are the prefix and suffix felts of the sender of the note currently -#! being processed. -#! -#! Panics if: -#! - the note is not being processed. -export.get_sender - # get the current input note pointer - exec.memory::get_current_input_note_ptr - # => [ptr] - - # assert the pointer is not zero - this would suggest the procedure has been called from an - # incorrect context - dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SENDER_FROM_INCORRECT_CONTEXT - # => [ptr] - - # get the sender from the note pointer - exec.memory::get_input_note_sender - # => [sender_id_prefix, sender_id_suffix] -end - -#! Returns the number of assets and the assets commitment of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [ASSETS_COMMITMENT, num_assets] -#! -#! Where: -#! - num_assets is the number of assets in the note currently being processed. -#! - ASSETS_COMMITMENT is a sequential hash of the assets in the note currently being processed. -#! -#! Panics if: -#! - the note is not being processed. -export.get_assets_info - # get the current input note pointer - exec.memory::get_current_input_note_ptr - # => [ptr] - - # assert the pointer is not zero - this would suggest the procedure has been called from an - # incorrect context - dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_FROM_INCORRECT_CONTEXT - # => [ptr] - - # get the number of assets in the note - dup exec.memory::get_input_note_num_assets - # => [num_assets, ptr] - - # get the assets commitment from the note pointer - swap exec.memory::get_input_note_assets_commitment - # => [ASSETS_COMMITMENT, num_assets] -end - -#! Returns the commitment to the note's inputs. -#! -#! Inputs: [] -#! Outputs: [NOTE_INPUTS_COMMITMENT] -#! -#! Where: -#! - NOTE_INPUTS_COMMITMENT is the note inputs commitment of the note currently being processed. -#! -#! Panics if: -#! - the note is not being processed. -export.get_note_inputs_commitment - exec.memory::get_current_input_note_ptr - # => [ptr] - - # The kernel memory is initialized by prologue::process_input_notes_data, and reset by - # note_processing_teardown before running the tx_script. If the value is `0` it is likely this - # procedure is being called outside of the kernel context. - dup neq.0 assert.err=ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_FROM_INCORRECT_CONTEXT - # => [ptr] - - exec.memory::get_input_note_inputs_commitment - # => [NOTE_INPUTS_COMMITMENT] -end - -#! Move the current input note pointer to the next note and returns the pointer value. +#! Move the active input note pointer to the next note and returns the pointer value. #! #! Inputs: [] -#! Outputs: [current_input_note_ptr] +#! Outputs: [active_input_note_ptr] #! #! Where: -#! - current_input_note_ptr is the pointer to the next note to be processed. -export.increment_current_input_note_ptr - # get the current input note pointer - exec.memory::get_current_input_note_ptr +#! - active_input_note_ptr is the pointer to the next note to be processed. +export.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 - # => [current_input_note_ptr] + # => [active_input_note_ptr] - # set the current input note pointer to the incremented value - dup exec.memory::set_current_input_note_ptr - # => [current_input_note_ptr] + # set the active input note pointer to the incremented value + dup exec.memory::set_active_input_note_ptr + # => [active_input_note_ptr] end -#! Sets the current input note pointer to 0. This should be called after all input notes have +#! Sets the active input note pointer to 0. This should be called after all input notes have #! been processed. #! #! Inputs: [] #! Outputs: [] export.note_processing_teardown - # set the current input note pointer to 0 - push.0 exec.memory::set_current_input_note_ptr + # set the active input note pointer to 0 + push.0 exec.memory::set_active_input_note_ptr # => [] end @@ -150,7 +64,7 @@ export.prepare_note padw padw push.0.0.0 # => [pad(11)] - exec.memory::get_current_input_note_ptr + exec.memory::get_active_input_note_ptr # => [note_ptr, pad(11)] dup exec.memory::get_input_note_args movup.4 @@ -280,7 +194,7 @@ proc.compute_output_note_id # => [NOTE_ID, note_data_ptr] # save the output note commitment (note ID) to memory - movup.4 mem_storew + movup.4 mem_storew_be # => [NOTE_ID] end @@ -345,39 +259,3 @@ export.compute_output_notes_commitment movup.4 drop # => [OUTPUT_NOTES_COMMITMENT, ...] end - -#! Returns the serial number of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [SERIAL_NUMBER] -#! -#! Where: -#! - SERIAL_NUMBER is the serial number of the note currently being processed. -#! -#! Panics if: -#! - no note is being processed. -export.get_serial_number - exec.memory::get_current_input_note_ptr - # => [note_ptr, ...] - - exec.memory::get_input_note_serial_num - # => [SERIAL_NUMBER, ...] -end - -#! Returns the script root of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [SCRIPT_ROOT] -#! -#! Where: -#! - SCRIPT_ROOT is the serial number of the note currently being processed. -#! -#! Panics if: -#! - no note is being processed. -export.get_script_root - exec.memory::get_current_input_note_ptr - # => [note_ptr] - - exec.memory::get_input_note_script_root - # => [SCRIPT_ROOT] -end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/output_note.masm b/crates/miden-lib/asm/kernels/transaction/lib/output_note.masm index 4f8df7b317..902ea9768f 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/output_note.masm @@ -1,14 +1,126 @@ +use.$kernel::account use.$kernel::memory use.$kernel::note +use.$kernel::asset +use.$kernel::constants +use.std::word + +# CONSTANTS +# ================================================================================================= + +# Constants for different note types +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 # 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_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_INDEX="failed to find note at the given index; index must be within [0, num_of_notes]" + +const.ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807" + +const.ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="non-fungible asset that already exists in the note cannot be added again" + +# 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_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX="invalid note type for the given note tag prefix" + +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" + +# EVENTS +# ================================================================================================= + +# Event emitted before a new note is 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") + +# Event emitted before an ASSET is added to a note +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") + # OUTPUT NOTE PROCEDURES # ================================================================================================= +#! Creates a new note and returns the index of the note. +#! +#! Inputs: [tag, aux, note_type, execution_hint, 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 number of output notes exceeds the maximum limit of 1024. +export.create + emit.NOTE_BEFORE_CREATED_EVENT + + exec.build_metadata + # => [NOTE_METADATA, 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] + + # 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] + + movdn.4 + # => [NOTE_METADATA, 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, RECIPIENT, note_idx] + + # set the RECIPIENT for the output note + exec.memory::set_output_note_recipient dropw + # => [note_idx] +end + #! Returns the information about assets in the output note with the specified index. #! #! The provided output note index is expected to be less than the total number of output notes. @@ -37,23 +149,23 @@ export.get_assets_info # able to get the assets later (in the `miden::output_note::get_assets` procedure) # get the start and the end pointers of the asset data - # - # notice that if the number of assets is odd, the asset data end pointer will be shifted one - # word further to make the assets number even (the same way it is done in the + # + # notice that if the number of assets is odd, the asset data end pointer will be shifted one + # word further to make the assets number even (the same way it is done in the # `note::compute_output_note_assets_commitment` procedure) movup.4 exec.memory::get_output_note_asset_data_ptr # => [assets_data_ptr, ASSETS_COMMITMENT, num_assets] dup dup.6 dup is_odd add # => [padded_num_assets, assets_data_ptr, assets_data_ptr, ASSETS_COMMITMENT, num_assets] - + mul.4 add # => [assets_end_ptr, assets_start_ptr, ASSETS_COMMITMENT, num_assets] movdn.5 movdn.4 # => [ASSETS_COMMITMENT, assets_start_ptr, assets_end_ptr, num_assets] - # store the assets data to the advice map using ASSETS_COMMITMENT as a key + # store the assets data to the advice map using ASSETS_COMMITMENT as a key adv.insert_mem # => [ASSETS_COMMITMENT, assets_start_ptr, assets_end_ptr, num_assets] @@ -62,16 +174,361 @@ export.get_assets_info # => [ASSETS_COMMITMENT, num_assets] end +#! Adds the ASSET to the note specified by the index. +#! +#! Inputs: [note_idx, ASSET] +#! Outputs: [] +#! +#! Where: +#! - note_idx is the index of the note to which the asset is added. +#! - ASSET can be a fungible or non-fungible asset. +#! +#! Panics if: +#! - 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 + # 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] + + # get a pointer to the memory address of the note at which the asset will be stored + dup movdn.5 exec.memory::get_output_note_ptr + # => [note_ptr, ASSET, note_idx] + + # get current num of assets + dup exec.memory::get_output_note_num_assets movdn.5 + # => [note_ptr, ASSET, num_of_assets, note_idx] + + # validate the ASSET + movdn.4 exec.asset::validate_asset + # => [ASSET, note_ptr, num_of_assets, note_idx] + + # emit event to signal that a new asset is going to be added to the note. + emit.NOTE_BEFORE_ADD_ASSET_EVENT + # => [ASSET, note_ptr, num_of_assets, note_idx] + + # Check if ASSET to add is fungible + exec.asset::is_fungible_asset + # => [is_fungible_asset?, ASSET, note_ptr, num_of_assets, note_idx] + + if.true + # ASSET to add is fungible + exec.add_fungible_asset + # => [note_ptr, note_idx] + else + # ASSET to add is non-fungible + exec.add_non_fungible_asset + # => [note_ptr, note_idx] + end + # => [note_ptr, note_idx] + + # update the assets commitment dirty flag to signal that the current assets commitment is not + # valid anymore + push.1 swap exec.memory::set_output_note_dirty_flag + # => [note_idx] + + # emit event to signal that a new asset was added to the note. + emit.NOTE_AFTER_ADD_ASSET_EVENT + # => [note_idx] + + # drop the note index + drop + # => [] +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 # assert that the provided note index is less than the total number of notes - dup exec.memory::get_num_output_notes + dup exec.memory::get_num_output_notes # => [output_notes_num, note_index, note_index] - + u32assert2.err=ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS u32lt assert.err=ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS # => [note_index] end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Builds the stack into the NOTE_METADATA word, encoding the note type and execution hint into a +#! single element. +#! 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] +#! +#! 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: 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] + + # 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] + + u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 + # => [tag, note_type, public_note, note_type, tag, aux, note_type, execution_hint] + + # 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. + # -------------------------------------------------------------------------------------------- + + # 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] + + # 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] + + # Merge execution hint payload and note tag. + # -------------------------------------------------------------------------------------------- + + # 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] + + # Merge sender_id_suffix, note_type and execution_hint_tag. + # -------------------------------------------------------------------------------------------- + + exec.account::get_id + # => [sender_id_prefix, sender_id_suffix, note_tag_hint_payload, execution_hint_tag, aux, note_type] + + 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] + + # 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] + + # 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] + + # Rearrange elements to produce the final note metadata layout. + # -------------------------------------------------------------------------------------------- + + 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]] +end + +#! Increments the number of output notes by one. Returns the index of the next note to be created. +#! +#! Inputs: [] +#! Outputs: [note_idx] +#! +#! Where: +#! - note_idx is the index of the next note to be created. +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 + assert.err=ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT + # => [note_idx] + + # increment the number of output notes + dup add.1 exec.memory::set_num_output_notes + # => [note_idx] +end + +#! Adds a fungible asset to a note. If the note already holds an asset issued by the same faucet id +#! the two quantities are summed up and the new quantity is stored at the old position in the note. +#! In the other case, the asset is stored at the next available position. +#! Returns the pointer to the note the asset was stored at. +#! +#! Inputs: [ASSET, note_ptr, num_of_assets, note_idx] +#! Outputs: [note_ptr] +#! +#! Where: +#! - ASSET is the fungible asset to be added to the note. +#! - note_ptr is the pointer to the note the asset will be added to. +#! - num_of_assets is the current number of assets. +#! - note_idx is the index of the note the asset will be added to. +#! +#! Panics if +#! - the summed amounts exceed the maximum amount of fungible assets. +proc.add_fungible_asset + dup.4 exec.memory::get_output_note_asset_data_ptr + # => [asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] + + # compute the pointer at which we should stop iterating + dup dup.7 mul.4 add + # => [end_asset_ptr, asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] + + # reorganize and pad the stack, prepare for the loop + movdn.5 movdn.5 padw dup.9 + # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # compute the loop latch + dup dup.10 neq + # => [latch, asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, + # note_idx] + + while.true + mem_loadw_be + # => [STORED_ASSET, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + dup.4 eq + # => [are_equal, 0, 0, stored_amount, ASSET, end_asset_ptr, asset_ptr, note_ptr, + # num_of_assets, note_idx] + + if.true + # add the asset quantity, we don't overflow here, bc both ASSETs are valid. + movup.2 movup.6 add + # => [updated_amount, 0, 0, faucet_id, 0, 0, end_asset_ptr, asset_ptr, note_ptr, + # num_of_assets, note_idx] + + # check that we don't overflow bc we use lte + dup exec.asset::get_fungible_asset_max_amount lte + assert.err=ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED + # => [updated_amount, 0, 0, faucet_id, 0, 0, end_asset_ptr, asset_ptr, note_ptr, + # num_of_assets, note_idx] + + # prepare stack to store the "updated" ASSET'' with the new quantity + movdn.5 + # => [0, 0, ASSET'', end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # decrease num_of_assets by 1 to offset incrementing it later + movup.9 sub.1 movdn.9 + # => [0, 0, ASSET'', end_asset_ptr, asset_ptr, note_ptr, num_of_assets - 1, note_idx] + + # end the loop we add 0's to the stack to have the correct number of elements + push.0.0 dup.9 push.0 + # => [0, asset_ptr, 0, 0, 0, 0, ASSET'', end_asset_ptr, asset_ptr, note_ptr, + # num_of_assets - 1, note_idx] + else + # => [0, 0, stored_amount, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, + # note_idx] + + # drop ASSETs and increment the asset pointer + movup.2 drop push.0.0 movup.9 add.4 dup movdn.10 + # => [asset_ptr + 4, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr + 4, note_ptr, + # num_of_assets, note_idx] + + # check if we reached the end of the loop + dup dup.10 neq + end + end + # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + # prepare stack for storing the ASSET + movdn.4 dropw + # => [asset_ptr, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # Store the fungible asset, either the combined ASSET or the new ASSET + mem_storew_be dropw drop drop + # => [note_ptr, num_of_assets, note_idx] + + # increase the number of assets in the note + swap add.1 dup.1 exec.memory::set_output_note_num_assets + # => [note_ptr, note_idx] +end + +#! Adds a non-fungible asset to a note at the next available position. +#! Returns the pointer to the note the asset was stored at. +#! +#! Inputs: [ASSET, note_ptr, num_of_assets, note_idx] +#! Outputs: [note_ptr, note_idx] +#! +#! Where: +#! - ASSET is the non-fungible asset to be added to the note. +#! - note_ptr is the pointer to the note the asset will be added to. +#! - num_of_assets is the current number of assets. +#! - note_idx is the index of the note the asset will be added to. +#! +#! Panics if: +#! - the non-fungible asset already exists in the note. +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] + + # compute the pointer at which we should stop iterating + dup dup.7 mul.4 add + # => [end_asset_ptr, asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] + + # reorganize and pad the stack, prepare for the loop + movdn.5 movdn.5 padw dup.9 + # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # compute the loop latch + dup dup.10 neq + # => [latch, asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, + # note_idx] + + while.true + # load the asset and compare + mem_loadw_be exec.word::test_eq + assertz.err=ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS + # => [ASSET', ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # drop ASSET' and increment the asset pointer + dropw movup.5 add.4 dup movdn.6 padw movup.4 + # => [asset_ptr + 4, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr + 4, note_ptr, + # num_of_assets, note_idx] + + # check if we reached the end of the loop + dup dup.10 neq + end + # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # prepare stack for storing the ASSET + movdn.4 dropw + # => [asset_ptr, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] + + # end of the loop reached, no error so we can store the non-fungible asset + mem_storew_be dropw drop drop + # => [note_ptr, num_of_assets, note_idx] + + # increase the number of assets in the note + swap add.1 dup.1 exec.memory::set_output_note_num_assets + # => [note_ptr, note_idx] +end diff --git a/crates/miden-lib/asm/kernels/transaction/lib/prologue.masm b/crates/miden-lib/asm/kernels/transaction/lib/prologue.masm index 2462f7b34e..0f4848deb0 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/prologue.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/prologue.masm @@ -4,10 +4,10 @@ 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::input_note use.$kernel::memory # CONSTS @@ -16,6 +16,12 @@ use.$kernel::memory # Max U32 value, used for initializing the expiration block number 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") + # ERRORS # ================================================================================================= @@ -59,9 +65,7 @@ const.ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT="number of note inputs e const.ERR_PROLOGUE_NOTE_AUTHENTICATION_FAILED="failed to authenticate note inclusion in block" -const.ERR_PROLOGUE_KERNEL_COMMITMENT_MISMATCH="sequential hash over kernel commitments does not match tx kernel commitment from block" - -const.ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH="sequential hash over kernel procedures does not match kernel commitment" +const.ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH="sequential hash over kernel procedures does not match kernel commitment from block" # PUBLIC INPUTS # ================================================================================================= @@ -89,115 +93,62 @@ proc.process_global_inputs exec.memory::set_block_commitment dropw exec.memory::set_init_account_commitment dropw exec.memory::set_nullifier_commitment dropw - exec.memory::set_global_acct_id + exec.memory::set_global_account_id end # KERNEL DATA # ================================================================================================= -#! Saves the procedure roots of the chosen kernel to memory. Verifies that kernel commitment and kernel -#! hash match the sequential hash of all kernels and sequential hash of kernel procedures -#! respectively. +#! Saves the kernel procedure roots to the memory. Verifies that the kernel commitment match the +#! sequential hash of kernel procedures. #! #! Inputs: #! Operand stack: [] -#! Advice stack: [kernel_version] #! Advice map: { -#! TX_KERNEL_COMMITMENT: [KERNEL_COMMITMENTS] -#! KERNEL_COMMITMENT: [KERNEL_PROCEDURE_ROOTS] +#! TX_KERNEL_COMMITMENT: [KERNEL_PROCEDURE_ROOTS] #! } #! Outputs: #! Operand stack: [] #! Advice stack: [] #! #! Where: -#! - kernel_version is the index of the desired kernel in the array of all kernels available for the -#! current transaction. -#! - TX_KERNEL_COMMITMENT is the accumulative hash from all kernel commitments. -#! - [KERNEL_COMMITMENTS] is the array of each kernel commitment. -#! - [KERNEL_PROCEDURE_ROOTS] is the array of procedure roots of the current kernel. +#! - 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 - # move the kernel offset to the operand stack - adv_push.1 - # OS => [kernel_version] - # AS => [] - - # load the tx kernel commitment from memory + # load the transaction kernel commitment from memory exec.memory::get_tx_kernel_commitment - # OS => [TX_KERNEL_COMMITMENT, kernel_version] + # OS => [TX_KERNEL_COMMITMENT] # AS => [] - # push the kernel commitments from the advice map to the advice stack + # push the procedure roots of the transaction kernel from the advice map to the advice stack adv.push_mapvaln - # OS => [TX_KERNEL_COMMITMENT, kernel_version] - # AS => [len_felts, [KERNEL_COMMITMENTS]] + # OS => [TX_KERNEL_COMMITMENT] + # AS => [len_felts, [KERNEL_PROCEDURE_ROOTS]] - # move the number of felt elements in the [KERNEL_COMMITMENTS] array to the stack and get the - # number of Words from it + # move the number of felt elements in the [KERNEL_PROCEDURE_ROOTS] array to the stack and get + # the number of procedures from it (which is essentially the number of words) adv_push.1 div.4 - # OS => [len_words, TX_KERNEL_COMMITMENT, kernel_version] - # AS => [[KERNEL_COMMITMENTS]] - - # get the pointer to the memory where kernel commitments will be stored - # Note: for now we use the same address for kernel commitment and for kernel procedures since there is - # only one kernel and its hash will be overwritten by the procedures anyway. - exec.memory::get_kernel_procedures_ptr swap - # OS => [len_words, kernel_mem_ptr, TX_KERNEL_COMMITMENT, kernel_version] - # AS => [[KERNEL_COMMITMENTS]] - - # store the kernel commitments in memory - exec.mem::pipe_words_to_memory - # OS => [C, B, A, kernel_mem_ptr', TX_KERNEL_COMMITMENT, kernel_version] - # AS => [] - - # extract the resulting hash - exec.rpo::squeeze_digest - # OS => [SEQ_HASH, kernel_mem_ptr', TX_KERNEL_COMMITMENT, kernel_version] - # AS => [] - - # assert that sequential hash matches the precomputed kernel commitment - movup.4 drop assert_eqw.err=ERR_PROLOGUE_KERNEL_COMMITMENT_MISMATCH - # OS => [kernel_version] - # AS => [] - - # get the hash of the kernel which will be used in the current transaction - exec.memory::get_kernel_procedures_ptr add - # OS => [kernel_ptr] - # AS => [] - - padw movup.4 mem_loadw - # OS => [KERNEL_COMMITMENT] - # AS => [] - - # push the procedure roots of the chosen kernel from the advice map to the advice stack - adv.push_mapvaln - # OS => [KERNEL_COMMITMENT] - # AS => [len_felts, [PROC_HASHES]] - - # move the number of felt elements in the [PROC_HASHES] array to the stack and get the - # number of Words from it - adv_push.1 div.4 - # OS => [len_words, KERNEL_COMMITMENT] - # AS => [[PROC_HASHES]] + # OS => [num_kernel_procedures, TX_KERNEL_COMMITMENT] + # AS => [[KERNEL_PROCEDURE_ROOTS]] # store the number of the procedures of the chosen kernel to the memory dup exec.memory::set_num_kernel_procedures - # OS => [len_words, KERNEL_COMMITMENT] - # AS => [[PROC_HASHES]] + # OS => [num_kernel_procedures, TX_KERNEL_COMMITMENT] + # AS => [[KERNEL_PROCEDURE_ROOTS]] # get the pointer to the memory where hashes of the kernel procedures will be stored exec.memory::get_kernel_procedures_ptr swap - # OS => [len_words, kernel_procs_ptr, KERNEL_COMMITMENT] - # AS => [[PROC_HASHES]] + # OS => [num_kernel_procedures, kernel_procs_ptr, TX_KERNEL_COMMITMENT] + # AS => [[KERNEL_PROCEDURE_ROOTS]] # store the kernel procedures to the memory exec.mem::pipe_words_to_memory - # OS => [C, B, A, kernel_procs_ptr', KERNEL_COMMITMENT] + # OS => [C, B, A, kernel_procs_ptr', TX_KERNEL_COMMITMENT] # AS => [] # extract the resulting hash exec.rpo::squeeze_digest - # OS => [SEQ_HASH, kernel_procs_ptr', KERNEL_COMMITMENT] + # OS => [SEQ_KERNEL_PROC_HASH, kernel_procs_ptr', TX_KERNEL_COMMITMENT] # AS => [] # assert that the precomputed hash matches the computed one @@ -236,7 +187,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. -#! - TX_KERNEL_COMMITMENT is the accumulative hash from all kernel commitments. +#! - 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. #! - block_num is the reference block number. #! - version is the current protocol version. @@ -342,13 +293,13 @@ proc.validate_new_account # => [] # Assert the account nonce is 0 - exec.memory::get_acct_nonce eq.0 assert.err=ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO + exec.memory::get_account_nonce eq.0 assert.err=ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO # => [] # Assert the initial vault is empty # --------------------------------------------------------------------------------------------- # get the account vault root - exec.memory::get_acct_vault_root + exec.memory::get_account_vault_root # => [ACCT_VAULT_ROOT] # push empty vault root onto stack @@ -451,14 +402,14 @@ end #! - ACCOUNT_STORAGE_COMMITMENT is the account's storage commitment. #! - ACCOUNT_CODE_COMMITMENT is the account's code commitment. proc.process_account_data - # Initialize the current account data pointer in the bookkeeping section with the native offset + # Initialize the active account data pointer in the bookkeeping section with the native offset # (2048) - exec.memory::set_current_account_data_ptr_to_native_account + exec.memory::set_active_account_data_ptr_to_native_account # Copy the account data from the advice provider to memory and hash it # --------------------------------------------------------------------------------------------- - exec.memory::get_current_account_data_ptr + exec.memory::get_active_account_data_ptr # => [acct_data_ptr] # read account details and compute its digest. See `Advice stack` above for details. @@ -472,33 +423,38 @@ proc.process_account_data # => [ACCOUNT_COMMITMENT] # assert the account ID matches the account ID in global inputs - exec.memory::get_global_acct_id + exec.memory::get_global_account_id exec.memory::get_account_id exec.account_id::is_equal assert.err=ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER # => [ACCOUNT_COMMITMENT] # store a copy of the initial nonce in global inputs - exec.memory::get_acct_nonce + exec.memory::get_account_nonce exec.memory::set_init_nonce # => [ACCOUNT_COMMITMENT] # validates and stores account storage slots in memory # account storage commitment is also stored as an initial one in the global inputs - exec.memory::get_acct_storage_commitment + exec.memory::get_account_storage_commitment exec.memory::set_init_account_storage_commitment exec.account::save_account_storage_data # => [ACCOUNT_COMMITMENT] # validates and stores account procedures in memory. - exec.memory::get_acct_code_commitment + exec.memory::get_account_code_commitment exec.account::save_account_procedure_data # => [ACCOUNT_COMMITMENT] + # duplicate the initial storage slots in memory to enable a diff computation + # for the account delta + exec.memory::mem_copy_native_account_initial_storage_slots + # => [ACCOUNT_COMMITMENT] + # copy the initial account vault root to the input vault root to support transaction asset # invariant checking # this account vault root is also stored as an initial one in the global inputs - exec.memory::get_acct_vault_root - exec.memory::set_init_account_vault_root + exec.memory::get_account_vault_root + exec.memory::set_init_native_account_vault_root exec.memory::set_input_vault_root dropw # => [ACCOUNT_COMMITMENT] @@ -515,6 +471,13 @@ proc.process_account_data # process conditional logic depending on whether the account is new or existing if.true + # insert the initial storage values of the new account into the delta + # this is required only for new accounts because the delta at the end of an + # account-creating transaction needs to represent the entire account, while + # this is not a requirement for existing accounts + exec.account::insert_new_storage + # => [ACCOUNT_COMMITMENT] + # set the initial account commitment exec.memory::set_init_account_commitment dropw # => [] @@ -529,7 +492,7 @@ proc.process_account_data # => [] # assert the nonce of an existing account is non-zero - exec.memory::get_acct_nonce neq.0 + exec.memory::get_account_nonce neq.0 assert.err=ERR_PROLOGUE_EXISTING_ACCOUNT_MUST_HAVE_NON_ZERO_NONCE # => [] end @@ -592,7 +555,7 @@ proc.authenticate_note.8 # --------------------------------------------------------------------------------------------- # load the note root from memory - loc_loadw.4 swapw + loc_loadw_be.4 swapw # => [NOTE_COMMITMENT, NOTE_ROOT] # load the index of the note @@ -631,7 +594,7 @@ 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. -#! - NULLIFIER is the result of +#! - NULLIFIER is the result of #! `hash(SERIAL_NUMBER || SCRIPT_ROOT || INPUTS_COMMITMENT || ASSETS_COMMITMENT)`. proc.process_input_note_details exec.memory::get_input_note_core_ptr @@ -819,7 +782,17 @@ proc.add_input_note_assets_to_vault dup.2 # => [input_vault_root_ptr, assets_start_ptr, assets_end_ptr, input_vault_root_ptr] - padw dup.5 mem_loadw + 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] exec.asset_vault::add_asset dropw @@ -924,7 +897,7 @@ proc.process_input_note # => [NULLIFIER, note_ptr, idx, HASHER_CAPACITY] # save NULLIFIER to memory - movup.5 exec.memory::get_input_note_nullifier_ptr mem_storew + movup.5 exec.memory::get_input_note_nullifier_ptr mem_storew_be # => [NULLIFIER, note_ptr, HASHER_CAPACITY] # note metadata & args @@ -1041,7 +1014,7 @@ proc.process_input_notes_data # - On the top of the stack is the hasher state containing the input notes commitment. The # hasher state will be updated by `process_input_note`. After the loop the commitment is # extracted. - # - Below the hasher state in the stack is the current note index. This number is used for two + # - Below the hasher state in the stack is the active note index. This number is used for two # purposes: # 1. Compute the input note's memory addresses, the index works as an offset. # 2. Determine the loop condition. The loop below runs until all input notes have been @@ -1083,10 +1056,10 @@ proc.process_input_notes_data assert_eqw.err=ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH # => [idx+1, num_notes] - # set the current input note ptr to the address of the first input note + # set the active input note ptr to the address of the first input note push.0 exec.memory::get_input_note_ptr - exec.memory::set_current_input_note_ptr + exec.memory::set_active_input_note_ptr # => [idx+1, num_notes] drop drop @@ -1107,7 +1080,7 @@ end #! #! Where: #! - 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 +#! - 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 # read the transaction script root from the advice stack @@ -1205,7 +1178,7 @@ end #! - 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`. -#! - TX_KERNEL_COMMITMENT is the accumulative hash from all kernel commitments. +#! - TX_KERNEL_COMMITMENT is the sequential hash from all kernel commitments. #! - PREV_BLOCK_COMMITMENT is the commitment to the previous block. #! - PARTIAL_BLOCKCHAIN_COMMITMENT is the sequential hash of the reference MMR. #! - ACCOUNT_ROOT is the root of the tree with latest account states for all accounts. diff --git a/crates/miden-lib/asm/kernels/transaction/lib/tx.masm b/crates/miden-lib/asm/kernels/transaction/lib/tx.masm index 96c8fd3c30..e7e137c885 100644 --- a/crates/miden-lib/asm/kernels/transaction/lib/tx.masm +++ b/crates/miden-lib/asm/kernels/transaction/lib/tx.masm @@ -1,83 +1,20 @@ -use.$kernel::account -use.$kernel::asset -use.$kernel::constants use.$kernel::memory use.$kernel::note # CONSTANTS # ================================================================================================= -# Constants for different note types -const.PUBLIC_NOTE=1 # 0b01 -const.PRIVATE_NOTE=2 # 0b10 -const.ENCRYPTED_NOTE=3 # 0b11 - -# Two raised to the power of 38 (2^38), used for shifting the note type value -const.TWO_POW_38=274877906944 - # Max value for U16, used as the upper limit for expiration block delta const.EXPIRY_UPPER_LIMIT=0xFFFF+1 -# The note type must be PUBLIC, unless the high bits are `0b11`. (See the table below.) -const.LOCAL_ANY_PREFIX=3 # 0b11 - # Max U32 value, used for initializing the expiration block number const.MAX_BLOCK_NUM=0xFFFFFFFF # 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_NOTE_INVALID_TYPE="invalid note type" - -# 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_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX="invalid note type for the given note tag prefix" - -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_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807" - -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]" - -const.ERR_NOTE_NETWORK_EXECUTION_DOES_NOT_TARGET_NETWORK_ACCOUNT="network execution mode with a specific target can only target network accounts" - const.ERR_TX_INVALID_EXPIRATION_DELTA="transaction expiration block delta must be within 0x1 and 0xFFFF" -# EVENTS -# ================================================================================================= - -# Event emitted before a new note is created. -const.NOTE_BEFORE_CREATED_EVENT=131083 -# Event emitted after a new note is created. -const.NOTE_AFTER_CREATED_EVENT=131084 - -# Event emitted before an ASSET is added to a note -const.NOTE_BEFORE_ADD_ASSET_EVENT=131085 -# Event emitted after an ASSET is added to a note -const.NOTE_AFTER_ADD_ASSET_EVENT=131086 - # PROCEDURES # ================================================================================================= @@ -147,88 +84,7 @@ export.memory::get_num_input_notes #! - num_output_notes is the number of output notes created in this transaction so far. export.memory::get_num_output_notes -#! Increments the number of output notes by one. Returns the index of the next note to be created. -#! -#! Inputs: [] -#! Outputs: [note_idx] -#! -#! Where: -#! - note_idx is the index of the next note to be created. -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 - assert.err=ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT - # => [note_idx] - - # increment the number of output notes - dup add.1 exec.memory::set_num_output_notes - # => [note_idx] -end - -#! Adds a non-fungible asset to a note at the next available position. -#! Returns the pointer to the note the asset was stored at. -#! -#! Inputs: [ASSET, note_ptr, num_of_assets, note_idx] -#! Outputs: [note_ptr, note_idx] -#! -#! Where: -#! - ASSET is the non-fungible asset to be added to the note. -#! - note_ptr is the pointer to the note the asset will be added to. -#! - num_of_assets is the current number of assets. -#! - note_idx is the index of the note the asset will be added to. -#! -#! Panics if: -#! - the non-fungible asset already exists in the note. -proc.add_non_fungible_asset_to_note - dup.4 exec.memory::get_output_note_asset_data_ptr - # => [asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] - - # compute the pointer at which we should stop iterating - dup dup.7 mul.4 add - # => [end_asset_ptr, asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] - - # reorganize and pad the stack, prepare for the loop - movdn.5 movdn.5 padw dup.9 - # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # compute the loop latch - dup dup.10 neq - # => [latch, asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, - # note_idx] - - while.true - # load the asset and compare - mem_loadw eqw assertz.err=ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS - # => [ASSET', ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # drop ASSET' and increment the asset pointer - dropw movup.5 add.4 dup movdn.6 padw movup.4 - # => [asset_ptr + 4, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr + 4, note_ptr, - # num_of_assets, note_idx] - - # check if we reached the end of the loop - dup dup.10 neq - end - # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # prepare stack for storing the ASSET - movdn.4 dropw - # => [asset_ptr, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # end of the loop reached, no error so we can store the non-fungible asset - mem_storew dropw drop drop - # => [note_ptr, num_of_assets, note_idx] - - # increase the number of assets in the note - swap add.1 dup.1 exec.memory::set_output_note_num_assets - # => [note_ptr, note_idx] -end - -#! Updates the transaction expiration block number. +#! Updates the transaction expiration block delta. #! #! The input block_height_delta is added to the block reference number in order to output an upper #! limit at which the transaction will be considered valid (not expired). @@ -239,7 +95,7 @@ end #! #! Where: #! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). -export.update_expiration_block_num +export.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] @@ -287,308 +143,3 @@ export.get_expiration_delta exec.get_block_number sub end end - -#! Adds a fungible asset to a note. If the note already holds an asset issued by the same faucet id -#! the two quantities are summed up and the new quantity is stored at the old position in the note. -#! In the other case, the asset is stored at the next available position. -#! Returns the pointer to the note the asset was stored at. -#! -#! Inputs: [ASSET, note_ptr, num_of_assets, note_idx] -#! Outputs: [note_ptr] -#! -#! Where: -#! - ASSET is the fungible asset to be added to the note. -#! - note_ptr is the pointer to the note the asset will be added to. -#! - num_of_assets is the current number of assets. -#! - note_idx is the index of the note the asset will be added to. -#! -#! Panics if -#! - the summed amounts exceed the maximum amount of fungible assets. -proc.add_fungible_asset_to_note - dup.4 exec.memory::get_output_note_asset_data_ptr - # => [asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] - - # compute the pointer at which we should stop iterating - dup dup.7 mul.4 add - # => [end_asset_ptr, asset_ptr, ASSET, note_ptr, num_of_assets, note_idx] - - # reorganize and pad the stack, prepare for the loop - movdn.5 movdn.5 padw dup.9 - # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # compute the loop latch - dup dup.10 neq - # => [latch, asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, - # note_idx] - - while.true - mem_loadw - # => [STORED_ASSET, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - dup.4 eq - # => [are_equal, 0, 0, stored_amount, ASSET, end_asset_ptr, asset_ptr, note_ptr, - # num_of_assets, note_idx] - - if.true - # add the asset quantity, we don't overflow here, bc both ASSETs are valid. - movup.2 movup.6 add - # => [updated_amount, 0, 0, faucet_id, 0, 0, end_asset_ptr, asset_ptr, note_ptr, - # num_of_assets, note_idx] - - # check that we don't overflow bc we use lte - dup exec.asset::get_fungible_asset_max_amount lte - assert.err=ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED - # => [updated_amount, 0, 0, faucet_id, 0, 0, end_asset_ptr, asset_ptr, note_ptr, - # num_of_assets, note_idx] - - # prepare stack to store the "updated" ASSET'' with the new quantity - movdn.5 - # => [0, 0, ASSET'', end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # decrease num_of_assets by 1 to offset incrementing it later - movup.9 sub.1 movdn.9 - # => [0, 0, ASSET'', end_asset_ptr, asset_ptr, note_ptr, num_of_assets - 1, note_idx] - - # end the loop we add 0's to the stack to have the correct number of elements - push.0.0 dup.9 push.0 - # => [0, asset_ptr, 0, 0, 0, 0, ASSET'', end_asset_ptr, asset_ptr, note_ptr, - # num_of_assets - 1, note_idx] - else - # => [0, 0, stored_amount, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, - # note_idx] - - # drop ASSETs and increment the asset pointer - movup.2 drop push.0.0 movup.9 add.4 dup movdn.10 - # => [asset_ptr + 4, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr + 4, note_ptr, - # num_of_assets, note_idx] - - # check if we reached the end of the loop - dup dup.10 neq - end - end - # => [asset_ptr, 0, 0, 0, 0, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - # prepare stack for storing the ASSET - movdn.4 dropw - # => [asset_ptr, ASSET, end_asset_ptr, asset_ptr, note_ptr, num_of_assets, note_idx] - - # Store the fungible asset, either the combined ASSET or the new ASSET - mem_storew dropw drop drop - # => [note_ptr, num_of_assets, note_idx] - - # increase the number of assets in the note - swap add.1 dup.1 exec.memory::set_output_note_num_assets - # => [note_ptr, note_idx] -end - -#! Builds the stack into the NOTE_METADATA word, encoding the note type and execution hint into a -#! single element. -#! 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] -#! -#! 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_note_metadata - - # Validate the note type. - # -------------------------------------------------------------------------------------------- - - # 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] - - # 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] - - u32assert.err=ERR_NOTE_TAG_MUST_BE_U32 - # => [tag, note_type, public_note, note_type, tag, aux, note_type, execution_hint] - - # 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. - # -------------------------------------------------------------------------------------------- - - # 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] - - # 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] - - # Merge execution hint payload and note tag. - # -------------------------------------------------------------------------------------------- - - # 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] - - # Merge sender_id_suffix, note_type and execution_hint_tag. - # -------------------------------------------------------------------------------------------- - - exec.account::get_id - # => [sender_id_prefix, sender_id_suffix, note_tag_hint_payload, execution_hint_tag, aux, note_type] - - 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] - - # 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] - - # 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] - - # Rearrange elements to produce the final note metadata layout. - # -------------------------------------------------------------------------------------------- - - 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]] -end - -#! Creates a new note and returns the index of the note. -#! -#! Inputs: [tag, aux, note_type, execution_hint, 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 number of output notes exceeds the maximum limit of 1024. -export.create_note - emit.NOTE_BEFORE_CREATED_EVENT - - exec.build_note_metadata - # => [NOTE_METADATA, 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] - - # 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] - - movdn.4 - # => [NOTE_METADATA, 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 exec.memory::set_output_note_metadata dropw - # => [note_ptr, RECIPIENT, note_idx] - - # set the RECIPIENT for the output note - exec.memory::set_output_note_recipient dropw - # => [note_idx] -end - -#! Adds the ASSET to the note specified by the index. -#! -#! Inputs: [note_idx, ASSET] -#! Outputs: [note_idx] -#! -#! Where: -#! - note_idx is the index of the note to which the asset is added. -#! - ASSET can be a fungible or non-fungible asset. -#! -#! Panics if: -#! - 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_to_note - # 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] - - # get a pointer to the memory address of the note at which the asset will be stored - dup movdn.5 exec.memory::get_output_note_ptr - # => [note_ptr, ASSET, note_idx] - - # get current num of assets - dup exec.memory::get_output_note_num_assets movdn.5 - # => [note_ptr, ASSET, num_of_assets, note_idx] - - # validate the ASSET - movdn.4 exec.asset::validate_asset - # => [ASSET, note_ptr, num_of_assets, note_idx] - - # emit event to signal that a new asset is going to be added to the note. - emit.NOTE_BEFORE_ADD_ASSET_EVENT - # => [ASSET, note_ptr, num_of_assets, note_idx] - - # Check if ASSET to add is fungible - exec.asset::is_fungible_asset - # => [is_fungible_asset?, ASSET, note_ptr, num_of_assets, note_idx] - - if.true - # ASSET to add is fungible - exec.add_fungible_asset_to_note - # => [note_ptr, note_idx] - else - # ASSET to add is non-fungible - exec.add_non_fungible_asset_to_note - # => [note_ptr, note_idx] - end - # => [note_ptr, note_idx] - - # update the assets commitment dirty flag to signal that the current assets commitment is not - # valid anymore - push.1 swap exec.memory::set_output_note_dirty_flag - # => [note_idx] - - # emit event to signal that a new asset was added to the note. - emit.NOTE_AFTER_ADD_ASSET_EVENT - # => [note_idx] -end diff --git a/crates/miden-lib/asm/kernels/transaction/main.masm b/crates/miden-lib/asm/kernels/transaction/main.masm index cfbf5fc3fa..959f882d83 100644 --- a/crates/miden-lib/asm/kernels/transaction/main.masm +++ b/crates/miden-lib/asm/kernels/transaction/main.masm @@ -9,29 +9,29 @@ use.$kernel::prologue # ================================================================================================= # Event emitted to signal that an execution of the transaction prologue has started. -const.PROLOGUE_START=131088 +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=131089 +const.PROLOGUE_END_EVENT=event("miden::tx::prologue_end") # Event emitted to signal that the notes processing has started. -const.NOTES_PROCESSING_START=131090 +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=131091 +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=131092 +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=131093 +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=131094 +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=131095 +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=131096 +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=131099 +const.EPILOGUE_END_EVENT=event("miden::tx::epilogue_end") # MAIN # ================================================================================================= @@ -69,18 +69,18 @@ const.EPILOGUE_END=131099 proc.main.1 # Prologue # --------------------------------------------------------------------------------------------- - - emit.PROLOGUE_START + + emit.PROLOGUE_START_EVENT exec.prologue::prepare_transaction # => [pad(16)] - emit.PROLOGUE_END + emit.PROLOGUE_END_EVENT # Note Processing # --------------------------------------------------------------------------------------------- - - emit.NOTES_PROCESSING_START + + emit.NOTES_PROCESSING_START_EVENT exec.memory::get_num_input_notes # => [num_input_notes, pad(16)] @@ -93,7 +93,7 @@ proc.main.1 # => [should_loop, pad(16)] while.true - emit.NOTE_EXECUTION_START + emit.NOTE_EXECUTION_START_EVENT # => [] exec.note::prepare_note @@ -107,29 +107,29 @@ proc.main.1 dropw dropw dropw dropw # => [pad(16)] - exec.note::increment_current_input_note_ptr - # => [current_input_note_ptr, pad(16)] + exec.note::increment_active_input_note_ptr + # => [active_input_note_ptr, pad(16)] # loop condition, exit when the memory ptr is after all input notes loc_load.0 neq # => [should_loop, pad(16)] - emit.NOTE_EXECUTION_END + emit.NOTE_EXECUTION_END_EVENT end exec.note::note_processing_teardown # => [pad(16)] - emit.NOTES_PROCESSING_END + emit.NOTES_PROCESSING_END_EVENT # Transaction Script Processing # --------------------------------------------------------------------------------------------- - - emit.TX_SCRIPT_PROCESSING_START + + emit.TX_SCRIPT_PROCESSING_START_EVENT # get the memory address of the transaction script root and load it to the stack exec.memory::get_tx_script_root_ptr - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be # => [TX_SCRIPT_ROOT, tx_script_root_ptr, pad(16)] exec.word::eqz not @@ -157,12 +157,12 @@ proc.main.1 # => [pad(16)] end - emit.TX_SCRIPT_PROCESSING_END + emit.TX_SCRIPT_PROCESSING_END_EVENT # Epilogue # --------------------------------------------------------------------------------------------- - emit.EPILOGUE_START + emit.EPILOGUE_START_EVENT # execute the transaction epilogue exec.epilogue::finalize_transaction @@ -172,7 +172,7 @@ proc.main.1 repeat.13 movup.13 drop end # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET, tx_expiration_block_num, pad(3)] - emit.EPILOGUE_END + emit.EPILOGUE_END_EVENT end begin diff --git a/crates/miden-lib/asm/kernels/transaction/tx_script_main.masm b/crates/miden-lib/asm/kernels/transaction/tx_script_main.masm index c6abb37472..79c99f8549 100644 --- a/crates/miden-lib/asm/kernels/transaction/tx_script_main.masm +++ b/crates/miden-lib/asm/kernels/transaction/tx_script_main.masm @@ -46,7 +46,7 @@ proc.main # get the memory address of the transaction script root and load it to the stack exec.memory::get_tx_script_root_ptr - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be # => [TX_SCRIPT_ROOT, tx_script_root_ptr] # return an error if the transaction script was not specified diff --git a/crates/miden-lib/asm/miden/account.masm b/crates/miden-lib/asm/miden/active_account.masm similarity index 58% rename from crates/miden-lib/asm/miden/account.masm rename to crates/miden-lib/asm/miden/active_account.masm index 018bedd9d9..576e6b672c 100644 --- a/crates/miden-lib/asm/miden/account.masm +++ b/crates/miden-lib/asm/miden/active_account.masm @@ -1,28 +1,31 @@ use.miden::kernel_proc_offsets -# NATIVE ACCOUNT PROCEDURES +# ACTIVE ACCOUNT PROCEDURES # ================================================================================================= -#! Returns the account ID of the current account. +# ID AND NONCE +# ------------------------------------------------------------------------------------------------- + +#! Returns the ID of the active account. #! #! Inputs: [] #! Outputs: [account_id_prefix, account_id_suffix] #! #! Where: -#! - account_id_{prefix,suffix} are the prefix and suffix felts of the account ID of the currently -#! accessing account. +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the ID of the active account. #! #! Invocation: exec export.get_id - # start padding the stack - push.0.0.0 + # pad the stack + padw padw padw push.0.0 + # => [pad(14)] - exec.kernel_proc_offsets::account_get_id_offset - # => [offset, 0, 0, 0] + # push the flag indicating that the ID of the current account was requested + push.0 + # => [is_native = 0, pad(14)] - # pad the stack - padw swapw padw padw swapdw - # => [offset, pad(15)] + exec.kernel_proc_offsets::account_get_id_offset + # => [offset, is_native = 0, pad(14)] syscall.exec_kernel_proc # => [account_id_prefix, account_id_suffix, pad(14)] @@ -32,28 +35,25 @@ export.get_id # => [account_id_prefix, account_id_suffix] end -#! Returns the nonce of the current account. +#! Returns the nonce of the active account. #! #! This procedure always returns the initial account nonce, as the nonce can only be incremented -#! in the authentication procedure when signing the transaction after all user code has been +#! in the authentication procedure when signing the transaction after all user code has been #! executed. #! #! Inputs: [] #! Outputs: [nonce] #! #! Where: -#! - nonce is the current account's nonce. +#! - nonce is the active account's nonce. #! #! Invocation: exec export.get_nonce - # start padding the stack - push.0.0.0 + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] exec.kernel_proc_offsets::account_get_nonce_offset - # => [offset, 0, 0, 0] - - # pad the stack - padw swapw padw padw swapdw # => [offset, pad(15)] syscall.exec_kernel_proc @@ -64,7 +64,10 @@ export.get_nonce # => [nonce] end -#! Returns the native account commitment at the beginning of the transaction. +# COMMITMENTS +# ------------------------------------------------------------------------------------------------- + +#! Returns the commitment of the active account at the beginning of the transaction. #! #! Inputs: [] #! Outputs: [INIT_COMMITMENT] @@ -89,7 +92,7 @@ export.get_initial_commitment # => [INIT_COMMITMENT] end -#! Computes and returns the account commitment from account data stored in memory. +#! Computes and returns commitment to the state of the active account. #! #! Inputs: [] #! Outputs: [ACCOUNT_COMMITMENT] @@ -98,12 +101,12 @@ end #! - ACCOUNT_COMMITMENT is the commitment of the account data. #! #! Invocation: exec -export.compute_current_commitment +export.compute_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] - exec.kernel_proc_offsets::account_compute_current_commitment_offset + exec.kernel_proc_offsets::account_compute_commitment_offset # => [offset, pad(15)] syscall.exec_kernel_proc @@ -114,68 +117,162 @@ export.compute_current_commitment # => [ACCOUNT_COMMITMENT] end -#! Computes the commitment to the native account's delta. +#! Gets the code commitment of the active account. +#! +#! Notice that the account code cannot be changed during the user code execution, so the code +#! commitment doesn't change during transaction execution: commitment returned by this procedure +#! could be used as both the initial and the current. +#! +#! The commitment to an empty delta is defined as the empty word. #! -#! Note that if the account state has changed, the nonce must be incremented before this procedure -#! is called, otherwise it will panic. This means it can only be called from an auth procedure, -#! since only auth procedures are allowed to increment the nonce. +#! During an account-creating transaction (when the initial nonce is 0), this procedure will not +#! return the empty word even if the initial storage commitment and the current storage commitment +#! are identical (storage hasn't changed). This is because the delta for a new account must +#! represent its entire newly created state, and the initial storage in a transaction is +#! initialized to the the storage that the account ID commits to, which may be non-empty. This +#! does not have any consequences other than being inconsistent in this edge case. #! #! Inputs: [] -#! Outputs: [DELTA_COMMITMENT] +#! Outputs: [CODE_COMMITMENT] #! #! Where: -#! - DELTA_COMMITMENT is the commitment to the account delta. +#! - CODE_COMMITMENT is the commitment of the account code. #! -#! Panics if: -#! - the vault or storage delta is not empty but the nonce increment is zero. -export.compute_delta_commitment +#! Invocation: exec +export.get_code_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] - exec.kernel_proc_offsets::account_compute_delta_commitment_offset + exec.kernel_proc_offsets::account_get_code_commitment_offset # => [offset, pad(15)] syscall.exec_kernel_proc - # => [DELTA_COMMITMENT, pad(12)] + # => [CODE_COMMITMENT, pad(12)] # clean the stack swapdw dropw dropw swapw dropw - # => [DELTA_COMMITMENT] + # => [CODE_COMMITMENT] end -#! Increments the account nonce by one and returns the new nonce. +#! Returns the storage commitment of the active account at the beginning of the transaction. +#! +#! During an account-creating transaction (when the initial nonce is 0), this procedure and +#! compute_storage_commitment will return the same value at the beginning of the transaction +#! (before any note or transaction scripts were executed). Despite that, the account delta may +#! not be empty. See account::compute_delta_commitment for more. #! #! Inputs: [] -#! Outputs: [final_nonce] +#! Outputs: [INIT_STORAGE_COMMITMENT] #! #! Where: -#! - final_nonce is the new nonce of the account. Since it cannot be incremented again, this will -#! also be the final nonce of the account after transaction execution. +#! - INIT_STORAGE_COMMITMENT is the initial account storage commitment. #! -#! Panics if: -#! - the invocation of this procedure does not originate from the native account. -#! - the invocation of this procedure does not originate from the authentication procedure -#! of the account. -#! - the nonce has already been incremented. +#! Invocation: exec +export.get_initial_storage_commitment + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + exec.kernel_proc_offsets::account_get_initial_storage_commitment_offset + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [INIT_STORAGE_COMMITMENT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [INIT_STORAGE_COMMITMENT] +end + +#! Computes the latest storage commitment of the active account. +#! +#! Notice that this procedure always returns the latest commitment, but it doesn't actually always +#! recompute it: recomputation is performed only if the account's storage has been changed, +#! otherwise the cached value is returned. +#! +#! During an account-creating transaction (when the initial nonce is 0), this procedure and +#! get_initial_storage_commitment will return the same value at the beginning of the transaction +#! (before any note or transaction scripts were executed). Despite that, the account delta may +#! not be empty. See account::compute_delta_commitment for more. +#! +#! Inputs: [] +#! Outputs: [STORAGE_COMMITMENT] +#! +#! Where: +#! - STORAGE_COMMITMENT is the commitment of the account storage. #! #! Invocation: exec -export.incr_nonce +export.compute_storage_commitment # pad the stack padw padw padw push.0.0.0 # => [pad(15)] - exec.kernel_proc_offsets::account_incr_nonce_offset + exec.kernel_proc_offsets::account_compute_storage_commitment_offset # => [offset, pad(15)] syscall.exec_kernel_proc - # => [final_nonce, pad(15)] + # => [STORAGE_COMMITMENT, pad(12)] - swap.15 dropw dropw dropw drop drop drop - # => [final_nonce] + # clean the stack + swapdw dropw dropw swapw dropw + # => [STORAGE_COMMITMENT] end -#! Gets an item from the account storage. Panics if the index is out of bounds. +#! Returns the vault root of the active account at the beginning of the transaction. +#! +#! Inputs: [] +#! Outputs: [INIT_VAULT_ROOT] +#! +#! Where: +#! - INIT_VAULT_ROOT is the initial account vault root. +#! +#! Invocation: exec +export.get_initial_vault_root + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + exec.kernel_proc_offsets::account_get_initial_vault_root_offset + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [INIT_VAULT_ROOT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [INIT_VAULT_ROOT] +end + +#! Returns the vault root of the active account. +#! +#! Inputs: [] +#! Outputs: [VAULT_ROOT] +#! +#! Where: +#! - VAULT_ROOT is the root of the account vault. +#! +#! Invocation: exec +export.get_vault_root + # pad the stack for syscall invocation + padw padw padw push.0.0.0 + # => [pad(15)] + + exec.kernel_proc_offsets::account_get_vault_root_offset + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [VAULT_ROOT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [VAULT_ROOT] +end + +# STORAGE +# ------------------------------------------------------------------------------------------------- + +#! Gets an item from the active account storage. Panics if the index is out of bounds. #! #! Inputs: [index] #! Outputs: [VALUE] @@ -207,37 +304,40 @@ export.get_item # => [VALUE] end -#! Sets an item in the account storage. Panics if the index is out of bounds. +#! Gets the initial item from the active account storage slot as it was at the beginning of the +#! transaction. #! -#! Inputs: [index, VALUE] -#! Outputs: [OLD_VALUE] +#! Inputs: [index] +#! Outputs: [INIT_VALUE] #! #! Where: -#! - index is the index of the item to set. -#! - VALUE is the value to set. -#! - OLD_VALUE is the previous value of the item. +#! - index is the index of the item to get. +#! - INIT_VALUE is the initial value of the item at the beginning of the transaction. #! #! Panics if: -#! - the index of the item is out of bounds. +#! - the index of the requested item is out of bounds. #! #! Invocation: exec -export.set_item - exec.kernel_proc_offsets::account_set_item_offset - # => [offset, index, VALUE] +export.get_initial_item + push.0.0 movup.2 + # => [index, 0, 0] + + exec.kernel_proc_offsets::account_get_initial_item_offset + # => [offset, index, 0, 0] # pad the stack - push.0.0 movdn.7 movdn.7 padw padw swapdw - # => [offset, index, VALUE, pad(10)] + padw swapw padw padw swapdw + # => [offset, index, pad(14)] syscall.exec_kernel_proc - # => [OLD_VALUE, pad(12)] + # => [INIT_VALUE, pad(12)] # clean the stack - swapw.3 dropw dropw dropw - # => [OLD_VALUE] + swapdw dropw dropw swapw dropw + # => [INIT_VALUE] end -#! Gets a map item from the account storage. +#! Gets a map item from the active account storage. #! #! Inputs: [index, KEY] #! Outputs: [VALUE] @@ -268,134 +368,54 @@ export.get_map_item # => [VALUE] end -#! Sets a map item in the account storage. +#! Gets the initial VALUE from the active account storage map as it was at the beginning of the +#! transaction. #! -#! Inputs: [index, KEY, VALUE] -#! Outputs: [OLD_MAP_ROOT, OLD_MAP_VALUE] +#! Inputs: [index, KEY] +#! Outputs: [INIT_VALUE] #! #! Where: -#! - index is the index of the map where the KEY VALUE should be set. -#! - 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. +#! - index is the index of the map where the KEY VALUE should be read. +#! - 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. #! - the slot item at index is not a map. #! #! Invocation: exec -export.set_map_item - exec.kernel_proc_offsets::account_set_map_item_offset - # => [offset, index, KEY, VALUE] - - # pad the stack - push.0.0 movdn.11 movdn.11 padw movdnw.3 - # => [offset, index, KEY, VALUE, pad(6)] - - syscall.exec_kernel_proc - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] - - # clean the stack - swapdw dropw dropw - # => [OLD_MAP_ROOT, OLD_MAP_VALUE] -end - -#! Gets the account code commitment of the current account. -#! -#! Notice that the account code cannot be changed during the user code execution, so the code -#! commitment doesn't change during transaction execution: commitment returned by this procedure -#! could be used as both the initial and the current. -#! -#! Inputs: [] -#! Outputs: [CODE_COMMITMENT] -#! -#! Where: -#! - CODE_COMMITMENT is the commitment of the account code. -#! -#! Invocation: exec -export.get_code_commitment - exec.kernel_proc_offsets::account_get_code_commitment_offset - # => [offset] - - # pad the stack - push.0.0.0 movup.3 padw swapw padw padw swapdw - # => [offset, pad(15)] - - syscall.exec_kernel_proc - # => [CODE_COMMITMENT, pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw - # => [CODE_COMMITMENT] -end +export.get_initial_map_item + exec.kernel_proc_offsets::account_get_initial_map_item_offset + # => [offset, index, KEY] -#! Returns the storage commitment of the native account at the beginning of the transaction. -#! -#! Inputs: [] -#! Outputs: [INIT_STORAGE_COMMITMENT] -#! -#! Where: -#! - INIT_STORAGE_COMMITMENT is the initial account storage commitment. -#! -#! Invocation: exec -export.get_initial_storage_commitment # pad the stack - padw padw padw push.0.0.0 - # => [pad(15)] - - exec.kernel_proc_offsets::account_get_initial_storage_commitment_offset - # => [offset, pad(15)] + push.0.0 movdn.7 movdn.7 padw padw swapdw + # => [offset, index, KEY, pad(10)] syscall.exec_kernel_proc - # => [INIT_STORAGE_COMMITMENT, pad(12)] + # => [INIT_VALUE, pad(12)] # clean the stack swapdw dropw dropw swapw dropw - # => [INIT_STORAGE_COMMITMENT] + # => [INIT_VALUE] end -#! Computes the latest account storage commitment of the current account. -#! -#! Notice that this procedure always returns the latest commitment, but it doesn't actually always -#! recompute it: recomputation is performed only if the account's storage has been changed, -#! otherwise the cached value is returned. -#! -#! Inputs: [] -#! Outputs: [STORAGE_COMMITMENT] -#! -#! Where: -#! - STORAGE_COMMITMENT is the commitment of the account storage. -#! -#! Invocation: exec -export.compute_storage_commitment - exec.kernel_proc_offsets::account_compute_storage_commitment_offset - # => [offset] - - # pad the stack - push.0.0.0 movup.3 padw swapw padw padw swapdw - # => [offset, pad(15)] +# VAULT +# ------------------------------------------------------------------------------------------------- - syscall.exec_kernel_proc - # => [STORAGE_COMMITMENT, pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw - # => [STORAGE_COMMITMENT] -end - -#! Returns the balance of the fungible asset associated with the provided faucet_id in the current account's vault. +#! 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] #! #! Where: -#! - faucet_id_{prefix,suffix} are the prefix and suffix felts of the faucet id of the fungible +#! - faucet_id_{prefix,suffix} are the prefix and suffix felts of the faucet ID of the fungible #! asset of interest. #! - balance is the vault balance of the fungible asset. #! #! Panics if: -#! - the asset is not a fungible asset. +#! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: exec export.get_balance @@ -414,84 +434,53 @@ export.get_balance # => [balance] end -#! Returns a boolean indicating whether the non-fungible asset is present in the current account's vault. +#! 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: [ASSET] -#! Outputs: [has_asset] +#! Inputs: [faucet_id_prefix, faucet_id_suffix] +#! Outputs: [init_balance] #! #! Where: -#! - ASSET is the non-fungible asset of interest -#! - has_asset is a boolean indicating whether the account vault has the asset of interest +#! - faucet_id_{prefix, suffix} are the prefix and suffix felts of the faucet id of the fungible +#! asset of interest. +#! - init_balance is the vault balance of the fungible asset at the beginning of the transaction. #! #! Panics if: -#! - the ASSET is a fungible asset. +#! - the provided faucet ID is not an ID of a fungible faucet. #! #! Invocation: exec -export.has_non_fungible_asset - exec.kernel_proc_offsets::account_has_non_fungible_asset_offset - # => [offset, ASSET] +export.get_initial_balance + exec.kernel_proc_offsets::account_get_initial_balance_offset + # => [offset, faucet_id_prefix, faucet_id_suffix] # pad the stack - push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw - # => [offset, ASSET, pad(11)] + push.0 movdn.3 padw swapw padw padw swapdw + # => [offset, faucet_id_prefix, faucet_id_suffix, pad(13)] syscall.exec_kernel_proc - # => [has_asset, pad(15)] + # => [init_balance, pad(15)] # clean the stack swapdw dropw dropw swapw dropw movdn.3 drop drop drop - # => [has_asset] + # => [init_balance] end -#! Add the specified asset to the vault. +#! Returns a boolean indicating whether the non-fungible asset is present in the active account's +#! vault. #! #! Inputs: [ASSET] -#! Outputs: [ASSET'] -#! -#! Where: -#! - ASSET' is a final asset in the account vault defined as follows: -#! - If ASSET is a non-fungible asset, then ASSET' is the same as ASSET. -#! - If ASSET is a fungible asset, then ASSET' is the total fungible asset in the account vault -#! after ASSET was added to it. -#! -#! Panics if: -#! - 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. -#! -#! Invocation: exec -export.add_asset - exec.kernel_proc_offsets::account_add_asset_offset - # => [offset, ASSET] - - # pad the stack - push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw - # => [offset, ASSET, pad(11)] - - syscall.exec_kernel_proc - # => [ASSET', pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw - # => [ASSET'] -end - -#! Remove the specified asset from the vault. -#! -#! Inputs: [ASSET] -#! Outputs: [ASSET] +#! Outputs: [has_asset] #! #! Where: -#! - ASSET is the asset to remove from the vault. +#! - ASSET is the non-fungible asset of interest +#! - has_asset is a boolean indicating whether the account vault has the 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. +#! - the ASSET is a fungible asset. #! #! Invocation: exec -export.remove_asset - exec.kernel_proc_offsets::account_remove_asset_offset +export.has_non_fungible_asset + exec.kernel_proc_offsets::account_has_non_fungible_asset_offset # => [offset, ASSET] # pad the stack @@ -499,79 +488,86 @@ export.remove_asset # => [offset, ASSET, pad(11)] syscall.exec_kernel_proc - # => [ASSET, pad(12)] + # => [has_asset, pad(15)] # clean the stack - swapdw dropw dropw swapw dropw - # => [ASSET] + swapdw dropw dropw swapw dropw movdn.3 drop drop drop + # => [has_asset] end -#! Returns the vault root of the native account at the beginning of the transaction. +#! Returns the number of procedures in the active account. #! #! Inputs: [] -#! Outputs: [INIT_VAULT_ROOT] +#! Outputs: [num_procedures] #! #! Where: -#! - INIT_VAULT_ROOT is the initial account vault root. +#! - num_procedures is the number of procedures in the active account. #! #! Invocation: exec -export.get_initial_vault_root +export.get_num_procedures # pad the stack padw padw padw push.0.0.0 # => [pad(15)] - exec.kernel_proc_offsets::account_get_initial_vault_root_offset + exec.kernel_proc_offsets::account_get_num_procedures_offset # => [offset, pad(15)] syscall.exec_kernel_proc - # => [INIT_VAULT_ROOT, pad(12)] + # => [num_procedures, pad(15)] # clean the stack - swapdw dropw dropw swapw dropw - # => [INIT_VAULT_ROOT] + swap.15 dropw dropw dropw drop drop drop + # => [num_procedures] end -#! Returns the vault root of the current account. +#! Returns the procedure root for the procedure at the specified index in the active account. #! -#! Inputs: [] -#! Outputs: [VAULT_ROOT] +#! Inputs: [index] +#! Outputs: [PROC_ROOT] #! #! Where: -#! - VAULT_ROOT is the root of the account vault. +#! - index is the index of the procedure. +#! - PROC_ROOT is the hash of the procedure. +#! +#! Panics if: +#! - the procedure index is out of bounds. #! #! Invocation: exec -export.get_vault_root - # pad the stack for syscall invocation - padw padw padw push.0.0.0 - # => [pad(15)] +export.get_procedure_root + # => [index] - exec.kernel_proc_offsets::account_get_vault_root_offset - # => [offset, pad(15)] + push.0.0 movup.2 + exec.kernel_proc_offsets::account_get_procedure_root_offset + # => [offset, index, 0, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, index, pad(14)] syscall.exec_kernel_proc - # => [VAULT_ROOT, pad(12)] + # => [PROC_ROOT, pad(12)] # clean the stack swapdw dropw dropw swapw dropw - # => [VAULT_ROOT] + # => [PROC_ROOT] end -#! Returns 1 if a procedure was called during transaction execution, and 0 otherwise. +#! Returns the binary flag indicating whether the procedure with the provided root is available on +#! the active account. #! -#! Note: This returns 1 only if the procedure invoked account-restricted kernel APIs (e.g., -#! `exec.faucet::mint`) which trigger `authenticate_and_track_procedure`. Procedures that execute -#! only local MASM instructions will return 0 even if they were executed. +#! Returns 1 if the procedure is available on the active account and 0 otherwise. #! #! Inputs: [PROC_ROOT] -#! Outputs: [was_called] +#! Outputs: [is_procedure_available] #! #! Where: -#! - PROC_ROOT is the hash of the procedure to check. -#! - was_called is 1 if the procedure was called, 0 otherwise. +#! - 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. #! #! Invocation: exec -export.was_procedure_called - exec.kernel_proc_offsets::account_was_procedure_called_offset +export.has_procedure + exec.kernel_proc_offsets::account_has_procedure_offset # => [offset, PROC_ROOT] # pad the stack @@ -579,9 +575,9 @@ export.was_procedure_called # => [offset, PROC_ROOT, pad(11)] syscall.exec_kernel_proc - # => [was_called, pad(15)] + # => [is_procedure_available, pad(15)] # clean the stack swapdw dropw dropw swapw dropw movdn.3 drop drop drop - # => [was_called] + # => [is_procedure_available] end diff --git a/crates/miden-lib/asm/miden/active_note.masm b/crates/miden-lib/asm/miden/active_note.masm new file mode 100644 index 0000000000..69d2e6753b --- /dev/null +++ b/crates/miden-lib/asm/miden/active_note.masm @@ -0,0 +1,373 @@ +use.std::mem + +use.miden::kernel_proc_offsets +use.miden::note +use.miden::contracts::wallets::basic->wallet + +# ERRORS +# ================================================================================================= + +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" + +# ACTIVE NOTE PROCEDURES +# ================================================================================================= +# +# By the "active note" notion here we assume the note which is currently being processed by the +# transaction kernel. + +#! Writes the assets of the active note into memory starting at the specified address. +#! +#! Inputs: [dest_ptr] +#! Outputs: [num_assets, dest_ptr] +#! +#! Where: +#! - dest_ptr is the memory address to write the assets. +#! - num_assets is the number of assets in the active note. +#! +#! Panics if: +#! - no note is currently active. +#! +#! Invocation: exec +export.get_assets + # pad the stack + padw padw padw push.0.0 + # => [pad(14), dest_ptr] + + # push the flag indicating that we want to request assets info from the active note + push.1 + # => [is_active_note = 1, pad(14), dest_ptr] + + exec.kernel_proc_offsets::input_note_get_assets_info_offset + # => [offset, is_active_note = 1, pad(14), dest_ptr] + + syscall.exec_kernel_proc + # => [ASSETS_COMMITMENT, num_assets, pad(11), dest_ptr] + + # clean the stack + swapdw dropw dropw movup.7 movup.7 movup.7 drop drop drop + # => [ASSETS_COMMITMENT, num_assets, dest_ptr] + + # write the assets from the advice map into memory + exec.note::write_assets_to_memory + # => [num_assets, dest_ptr] +end + +#! Returns the recipient of the active note. +#! +#! Inputs: [] +#! Outputs: [RECIPIENT] +#! +#! Where: +#! - RECIPIENT is the commitment to the active note's script, inputs, the serial number. +#! +#! Panics if: +#! - no note is currently active. +#! +#! Invocation: exec +export.get_recipient + # pad the stack + padw padw padw push.0.0 + # => [pad(14)] + + # push the flag indicating that we want to request recipient from the active note + push.1 + # => [is_active_note = 1, pad(14)] + + exec.kernel_proc_offsets::input_note_get_recipient_offset + # => [offset, is_active_note = 1, pad(14)] + + syscall.exec_kernel_proc + # => [RECIPIENT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [RECIPIENT] +end + +#! Writes the active note's inputs to memory starting at the specified address. +#! +#! Inputs: +#! Stack: [dest_ptr] +#! Advice Map: { NOTE_INPUTS_COMMITMENT: [INPUTS] } +#! Outputs: +#! Stack: [num_inputs, dest_ptr] +#! +#! 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. +#! - INPUTS is the data corresponding to the note's inputs. +#! +#! Panics if: +#! - no note is currently active. +#! +#! Invocation: exec +export.get_inputs + # pad the stack + padw padw padw push.0.0 + # => [pad(14), dest_ptr] + + # push the flag indicating that we want to request inputs info from the active note + push.1 + # => [is_active_note = 1, pad(14), dest_ptr] + + exec.kernel_proc_offsets::input_note_get_inputs_info_offset + # => [offset, is_active_note = 1, pad(14), dest_ptr] + + syscall.exec_kernel_proc + # => [NOTE_INPUTS_COMMITMENT, num_inputs, pad(11), dest_ptr] + + # clean the stack + swapdw dropw dropw + movup.5 drop movup.5 drop movup.5 drop + # => [NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + + # write the inputs to the memory using the provided destination pointer + exec.write_inputs_to_memory + # => [num_inputs, dest_ptr] +end + +#! Returns the metadata of the active note. +#! +#! Inputs: [] +#! Outputs: [METADATA] +#! +#! Where: +#! - METADATA is the metadata of the active note. +#! +#! Panics if: +#! - no note is currently active. +#! +#! Invocation: exec +export.get_metadata + # 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)] + + exec.kernel_proc_offsets::input_note_get_metadata_offset + # => [offset, is_active_note = 1, pad(14)] + + syscall.exec_kernel_proc + # => [METADATA, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [METADATA] +end + +#! Returns the sender of the active note. +#! +#! Inputs: [] +#! Outputs: [sender_id_prefix, sender_id_suffix] +#! +#! Where: +#! - sender_{prefix,suffix} are the prefix and suffix felts of the sender of the active note. +#! +#! Panics if: +#! - 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)] + + 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 + 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 + +#! Returns the serial number of the active note. +#! +#! Inputs: [] +#! Outputs: [SERIAL_NUMBER] +#! +#! Where: +#! - SERIAL_NUMBER is the serial number of the active note. +#! +#! Panics if: +#! - no note is currently active. +#! +#! Invocation: exec +export.get_serial_number + # pad the stack + padw padw padw push.0.0 + # => [pad(14)] + + # push the flag indicating that we want to request serial number from the active note + push.1 + # => [is_active_note = 1, pad(14)] + + exec.kernel_proc_offsets::input_note_get_serial_number_offset + # => [offset, is_active_note = 1, pad(14)] + + syscall.exec_kernel_proc + # => [SERIAL_NUMBER, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [SERIAL_NUMBER] +end + +#! Returns the script root of the active note. +#! +#! Inputs: [] +#! Outputs: [SCRIPT_ROOT] +#! +#! Where: +#! - SCRIPT_ROOT is the script root of the active note. +#! +#! Panics if: +#! - no note is currently active. +#! +#! Invocation: exec +export.get_script_root + # pad the stack + padw padw padw push.0.0 + # => [pad(14)] + + # push the flag indicating that we want to request script root from the active note + push.1 + # => [is_active_note = 1, pad(14)] + + exec.kernel_proc_offsets::input_note_get_script_root_offset + # => [offset, is_active_note = 1, pad(14)] + + syscall.exec_kernel_proc + # => [SCRIPT_ROOT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [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 +# ================================================================================================= + +#! Writes the note inputs stored in the advice map to the memory specified by the provided +#! destination pointer. +#! +#! Inputs: +#! Operand stack: [NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] +#! Advice map: { +#! NOTE_INPUTS_COMMITMENT: [[INPUT_VALUES]] +#! } +#! Outputs: +#! Operand stack: [num_inputs, dest_ptr] +proc.write_inputs_to_memory + # load the inputs from the advice map to the advice stack + adv.push_mapvaln + # OS => [NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + # AS => [advice_num_inputs, [INPUT_VALUES]] + + # move the number of inputs obtained from advice map to the operand stack + adv_push.1 dup.5 + # 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]] + + # calculate the number of words required to store the inputs + dup.4 u32divmod.4 neq.0 add + # OS => [num_words, NOTE_INPUTS_COMMITMENT, num_inputs, dest_ptr] + # AS => [[INPUT_VALUES]] + + # round up the number of words to the next multiple of 2 + dup is_odd add + # 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] + # AS => [[INPUT_VALUES]] + + # write the inputs from the advice stack into memory + exec.mem::pipe_preimage_to_memory drop + # OS => [num_inputs, dest_ptr] + # AS => [] +end diff --git a/crates/miden-lib/asm/miden/asset.masm b/crates/miden-lib/asm/miden/asset.masm index 6dfcb0ab9f..6c6d8f8d6b 100644 --- a/crates/miden-lib/asm/miden/asset.masm +++ b/crates/miden-lib/asm/miden/asset.masm @@ -1,4 +1,3 @@ -use.miden::account use.miden::account_id # ERRORS @@ -24,7 +23,7 @@ const.ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! - amount is the amount of the asset to create. #! - ASSET is the built fungible asset. #! -#! Annotation hint: is not used anywhere except this file +#! Invocation: exec export.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 @@ -40,26 +39,6 @@ export.build_fungible_asset # => [ASSET] end -#! Creates a fungible asset for the faucet the transaction is being executed against. -#! -#! Inputs: [amount] -#! Outputs: [ASSET] -#! -#! Where: -#! - amount is the amount of the asset to create. -#! - ASSET is the created fungible asset. -#! -#! Invocation: exec -export.create_fungible_asset - # fetch the id of the faucet the transaction is being executed against. - exec.account::get_id - # => [id_prefix, id_suffix, amount] - - # build the fungible asset - exec.build_fungible_asset - # => [ASSET] -end - #! Builds a non fungible asset for the specified non-fungible faucet and amount. #! #! Inputs: [faucet_id_prefix, DATA_HASH] @@ -71,7 +50,7 @@ end #! - DATA_HASH is the data hash of the non-fungible asset to build. #! - ASSET is the built non-fungible asset. #! -#! Annotation hint: is not used anywhere except this file +#! Invocation: exec export.build_non_fungible_asset # assert the faucet is a non-fungible faucet dup exec.account_id::is_non_fungible_faucet @@ -84,26 +63,6 @@ export.build_non_fungible_asset # => [ASSET] end -#! Creates a non-fungible asset for the faucet the transaction is being executed against. -#! -#! Inputs: [DATA_HASH] -#! Outputs: [ASSET] -#! -#! Where: -#! - DATA_HASH is the data hash of the non-fungible asset to create. -#! - ASSET is the created non-fungible asset. -#! -#! Invocation: exec -export.create_non_fungible_asset - # get the id of the faucet the transaction is being executed against - exec.account::get_id swap drop - # => [faucet_id_prefix, DATA_HASH] - - # build the non-fungible asset - exec.build_non_fungible_asset - # => [ASSET] -end - #! Returns the maximum amount of a fungible asset. #! #! Stack: [] diff --git a/crates/miden-lib/asm/miden/auth/mod.masm b/crates/miden-lib/asm/miden/auth/mod.masm index ca3cfa7923..a62b6141cc 100644 --- a/crates/miden-lib/asm/miden/auth/mod.masm +++ b/crates/miden-lib/asm/miden/auth/mod.masm @@ -1,17 +1,17 @@ -use.miden::account +use.miden::native_account use.miden::tx use.std::crypto::hashes::rpo #! 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 - loc_storew.0 + loc_storew_be.0 movdnw.3 - loc_storew.4 + loc_storew_be.4 movdnw.3 - loc_storew.8 + loc_storew_be.8 movdnw.3 - loc_storew.12 + loc_storew_be.12 movdnw.3 # => [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT] @@ -29,10 +29,10 @@ export.adv_insert_hqword.16 drop drop # => [<4 stack elements>] - loc_loadw.12 - padw loc_loadw.8 - padw loc_loadw.4 - padw loc_loadw.0 + loc_loadw_be.12 + padw loc_loadw_be.8 + padw loc_loadw_be.4 + padw loc_loadw_be.0 # => [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT] end @@ -47,7 +47,7 @@ end #! - 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 - exec.account::compute_delta_commitment + exec.native_account::compute_delta_commitment # => [ACCOUNT_DELTA_COMMITMENT, SALT] exec.tx::get_input_notes_commitment diff --git a/crates/miden-lib/asm/miden/auth/rpo_falcon512.masm b/crates/miden-lib/asm/miden/auth/rpo_falcon512.masm index e9aca94ca6..81da11a100 100644 --- a/crates/miden-lib/asm/miden/auth/rpo_falcon512.masm +++ b/crates/miden-lib/asm/miden/auth/rpo_falcon512.masm @@ -1,4 +1,5 @@ -use.miden::account +use.miden::active_account +use.miden::native_account use.miden::auth use.miden::tx use.std::crypto::dsa::rpo_falcon512 @@ -7,7 +8,7 @@ use.std::crypto::dsa::rpo_falcon512 # ================================================================================================= # The event to request an authentication signature. -const.AUTH_REQUEST=131087 +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 @@ -38,7 +39,7 @@ export.authenticate_transaction # --------------------------------------------------------------------------------------------- # This has to happen before computing the delta commitment, otherwise that procedure will abort push.0.0 exec.tx::get_block_number - exec.account::incr_nonce + exec.native_account::incr_nonce # => [[final_nonce, ref_block_num, 0, 0], PUB_KEY] # Compute the message that is signed. @@ -56,7 +57,7 @@ export.authenticate_transaction # Fetch signature from advice provider and verify. # --------------------------------------------------------------------------------------------- # Emit the authentication request event that pushes a signature for the message to the advice stack - emit.AUTH_REQUEST + emit.AUTH_REQUEST_EVENT swapw # OS => [PUB_KEY, MESSAGE] # AS => [SIGNATURE] @@ -75,6 +76,11 @@ end #! the provided account storage map slot, verifies their signatures against the transaction message, #! and returns the number of successfully verified signatures. #! +#! Note: Calls `active_account::get_initial_map_item` to access the transaction's initial storage +#! state rather than the current state. This is crucial when validating transactions that update +#! 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] #! Outputs: [num_verified_signatures, MSG] export.verify_signatures.16 @@ -101,10 +107,11 @@ export.verify_signatures.16 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] - exec.account::get_map_item + # Get public key from initial storage state + exec.active_account::get_initial_map_item # => [OWNER_PUB_KEY, i-1, MSG] - loc_storew.CURRENT_PK_LOC + loc_storew_be.CURRENT_PK_LOC # => [OWNER_PUB_KEY, i-1, MSG] # Check if signature exists for this signer. @@ -135,14 +142,14 @@ export.verify_signatures.16 # Verify the signature against the public key and message. # ----------------------------------------------------------------------------------------- - loc_loadw.CURRENT_PK_LOC + loc_loadw_be.CURRENT_PK_LOC # => [PK, MSG, MSG, i-1] swapw # => [MSG, PK, MSG, i-1] # Emit the authentication request event that pushes a signature for the message to the advice stack. - emit.AUTH_REQUEST + emit.AUTH_REQUEST_EVENT swapw # OS => [PUB_KEY, MSG, MSG, i-1] diff --git a/crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm b/crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm index 7fb4eb10bb..130df4d663 100644 --- a/crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm +++ b/crates/miden-lib/asm/miden/contracts/faucets/basic_fungible.masm @@ -7,31 +7,30 @@ # - max_supply is the maximum supply of the token. # - decimals are the decimals of the token. # - token_symbol as three chars encoded in a Felt. -use.miden::account -use.miden::asset -use.miden::faucet -use.miden::tx -use.miden::contracts::auth::basic -# CONSTANTS +use.miden::contracts::faucets + +# CONSTANTS # ================================================================================================= + const.PRIVATE_NOTE=2 -# ERRORS +# ERRORS # ================================================================================================= - 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" + # 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. +#! 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: [note_idx, pad(15)] +#! Outputs: [pad(16)] #! #! Where: #! - amount is the amount to be minted and sent. @@ -41,74 +40,31 @@ 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 output note. -#! This cannot directly be accessed from another context. #! #! 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.4 - # get max supply of this faucet. We assume it is stored at pos 3 of slot 1 - push.METADATA_SLOT exec.account::get_item drop drop drop - # => [max_supply, amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - # 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, - # pad(7)] - - # 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, - # pad(7)] - - # 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, pad(7)] - - # creating the asset - exec.asset::create_fungible_asset - # => [ASSET, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - # mint the asset; this is needed to satisfy asset preservation logic. - exec.faucet::mint - # => [ASSET, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - # store and drop the ASSET - loc_storew.0 dropw - # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - # create a note - exec.tx::create_note - # => [note_idx, pad(15)] - - # load the ASSET and add it to the note - movdn.4 loc_loadw.0 exec.tx::add_asset_to_note movup.4 - # => [note_idx, ASSET, pad(11)] +export.distribute + exec.faucets::distribute + # => [pad(16)] end -#! Burns fungible assets. +#! Burns the fungible asset from the active note. #! -#! Inputs: [ASSET, pad(12)] -#! Outputs: [pad(16)] +#! 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. #! -#! Where: -#! - ASSET is the fungible asset to be burned. +#! This is a re-export of basic_fungible::burn. +#! +#! 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 -export.burn - # burning the asset - exec.faucet::burn - # => [ASSET, pad(12)] - - # clear the stack - dropw - # => [pad(16)] -end +export.faucets::burn diff --git a/crates/miden-lib/asm/miden/contracts/faucets/mod.masm b/crates/miden-lib/asm/miden/contracts/faucets/mod.masm new file mode 100644 index 0000000000..b16d3ad808 --- /dev/null +++ b/crates/miden-lib/asm/miden/contracts/faucets/mod.masm @@ -0,0 +1,117 @@ +use.miden::active_account +use.miden::active_note +use.miden::faucet +use.miden::output_note + +# CONSTANTS +# ================================================================================================= + +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_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] +#! Outputs: [] +#! +#! 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). +#! +#! 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 + # 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] + + # 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] + + # 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] + + # 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] + + # creating the asset + exec.faucet::create_fungible_asset + # => [ASSET, tag, aux, note_type, execution_hint, RECIPIENT] + + # mint the asset; this is needed to satisfy asset preservation logic. + exec.faucet::mint + # => [ASSET, tag, aux, note_type, execution_hint, RECIPIENT] + + # store and drop the ASSET + movdnw.2 + # => [tag, aux, note_type, execution_hint, 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 + # => [] +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)] +#! +#! Where: +#! - ASSET is the fungible asset that was burned. +#! +#! 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 +export.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)] + + # Verify we have exactly one asset + assert.err=ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS + # => [dest_ptr, pad(16)] + + mem_loadw_be + # => [ASSET, pad(16)] + + # burning the asset + exec.faucet::burn + # => [ASSET, pad(16)] + + dropw + # => [pad(16)] +end diff --git a/crates/miden-lib/asm/miden/contracts/faucets/network_fungible.masm b/crates/miden-lib/asm/miden/contracts/faucets/network_fungible.masm new file mode 100644 index 0000000000..a983169cfa --- /dev/null +++ b/crates/miden-lib/asm/miden/contracts/faucets/network_fungible.masm @@ -0,0 +1,88 @@ +use.miden::active_account +use.miden::account_id +use.miden::active_note +use.miden::contracts::faucets +use.miden::contracts::faucets::basic_fungible + +# CONSTANTS +# ================================================================================================ + +# The slot in this component's storage layout where the owner config is stored. +const.OWNER_CONFIG_SLOT=1 + +# 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. +#! +#! Inputs: [] +#! Outputs: [is_owner] +#! +#! 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] + + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, owner_prefix, owner_suffix, 0, 0] + + exec.account_id::is_equal + # => [are_equal, 0, 0] + + movdn.2 drop drop + # => [is_owner] +end + +#! 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)] +#! +#! 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. +#! +#! 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 + # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] + + exec.faucets::distribute + # => [pad(16)] +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 +export.faucets::burn diff --git a/crates/miden-lib/asm/miden/contracts/wallets/basic.masm b/crates/miden-lib/asm/miden/contracts/wallets/basic.masm index 5491215b39..0094eec5af 100644 --- a/crates/miden-lib/asm/miden/contracts/wallets/basic.masm +++ b/crates/miden-lib/asm/miden/contracts/wallets/basic.masm @@ -1,11 +1,11 @@ -use.miden::account -use.miden::tx +use.miden::native_account +use.miden::output_note # CONSTANTS # ================================================================================================= const.PUBLIC_NOTE=1 -#! Adds the provided asset to the current account. +#! Adds the provided asset to the active account. #! #! Inputs: [ASSET, pad(12)] #! Outputs: [pad(16)] @@ -20,7 +20,7 @@ const.PUBLIC_NOTE=1 #! #! Invocation: call export.receive_asset - exec.account::add_asset + exec.native_account::add_asset # => [ASSET', pad(12)] # drop the final asset @@ -50,9 +50,12 @@ end #! Invocation: call export.move_asset_to_note # remove the asset from the account - exec.account::remove_asset + exec.native_account::remove_asset # => [ASSET, note_idx, pad(11)] - exec.tx::add_asset_to_note - # => [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/miden/faucet.masm b/crates/miden-lib/asm/miden/faucet.masm index fbc52fe3a8..1332f2c3ca 100644 --- a/crates/miden-lib/asm/miden/faucet.masm +++ b/crates/miden-lib/asm/miden/faucet.masm @@ -1,5 +1,53 @@ +use.miden::asset +use.miden::active_account use.miden::kernel_proc_offsets +#! Creates a fungible asset for the faucet the transaction is being executed against. +#! +#! Inputs: [amount] +#! Outputs: [ASSET] +#! +#! Where: +#! - amount is the amount of the asset to create. +#! - ASSET is the created fungible asset. +#! +#! Panics if: +#! - the active account is not a fungible faucet. +#! +#! Invocation: exec +export.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] + + # build the fungible asset + exec.asset::build_fungible_asset + # => [ASSET] +end + +#! Creates a non-fungible asset for the faucet the transaction is being executed against. +#! +#! Inputs: [DATA_HASH] +#! Outputs: [ASSET] +#! +#! Where: +#! - DATA_HASH is the data hash of the non-fungible asset to create. +#! - ASSET is the created non-fungible asset. +#! +#! Panics if: +#! - the active account is not a non-fungible faucet. +#! +#! Invocation: exec +export.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] + + # build the non-fungible asset + exec.asset::build_non_fungible_asset + # => [ASSET] +end + #! Mint an asset from the faucet the transaction is being executed against. #! #! Inputs: [ASSET] diff --git a/crates/miden-lib/asm/miden/input_note.masm b/crates/miden-lib/asm/miden/input_note.masm index a2bb17b58f..228899e11e 100644 --- a/crates/miden-lib/asm/miden/input_note.masm +++ b/crates/miden-lib/asm/miden/input_note.masm @@ -22,15 +22,20 @@ use.miden::note #! Invocation: exec export.get_assets_info # start padding the stack - push.0.0 movup.2 - # => [note_index, 0, 0] + push.0 swap + # => [note_index, 0] + + # push the flag indicating that we want to request assets info from the note with the specified + # index + push.0 + # => [is_active_note = 0, note_index, 0] exec.kernel_proc_offsets::input_note_get_assets_info_offset - # => [offset, note_index, 0, 0] + # => [offset, is_active_note = 0, note_index, 0] # pad the stack padw swapw padw padw swapdw - # => [offset, note_index, pad(14)] + # => [offset, 0, note_index, pad(13)] syscall.exec_kernel_proc # => [ASSETS_COMMITMENT, num_assets, pad(11)] @@ -91,11 +96,16 @@ end #! Invocation: exec export.get_recipient # start padding the stack - push.0.0 movup.2 - # => [note_index, 0, 0] + push.0 swap + # => [note_index, 0] + + # push the flag indicating that we want to request assets info from the note with the specified + # index + push.0 + # => [is_active_note = 0, note_index, 0] exec.kernel_proc_offsets::input_note_get_recipient_offset - # => [offset, note_index, 0, 0] + # => [offset, is_active_note = 0, note_index, 0] # pad the stack padw swapw padw padw swapdw @@ -124,15 +134,20 @@ end #! Invocation: exec export.get_metadata # start padding the stack - push.0.0 movup.2 - # => [note_index, 0, 0] + 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, note_index, 0, 0] + # => [offset, is_active_note = 0, note_index, 0] # pad the stack padw swapw padw padw swapdw - # => [offset, note_index, pad(14)] + # => [offset, is_active_note = 0, note_index, pad(13)] syscall.exec_kernel_proc # => [METADATA, pad(12)] @@ -141,3 +156,163 @@ export.get_metadata swapdw dropw dropw swapw dropw # => [METADATA] end + +#! Returns the sender of the input note with the specified index. +#! +#! Inputs: [note_index] +#! Outputs: [sender_id_prefix, sender_id_suffix] +#! +#! Where: +#! - note_index is the index of the input note whose sender should be returned. +#! - sender_{prefix,suffix} are the prefix and suffix felts of the specified note. +#! +#! Panics if: +#! - 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)] + + # extract the sender ID from the metadata word + 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 + +#! Returns the inputs commitment and length of the input note with the specified index. +#! +#! Inputs: [note_index] +#! Outputs: [NOTE_INPUTS_COMMITMENT, num_inputs] +#! +#! Where: +#! - note_index is the index of the input note whose data should be returned. +#! - NOTE_INPUTS_COMMITMENT is the inputs commitment of the specified input note. +#! - num_inputs is the number of input values 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_inputs_info + # start padding the stack + push.0 swap + # => [note_index, 0] + + # push the flag indicating that we want to request inputs info from the note with the specified + # index + push.0 + # => [is_active_note = 0, note_index, 0] + + exec.kernel_proc_offsets::input_note_get_inputs_info_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 + # => [NOTE_INPUTS_COMMITMENT, num_inputs, pad(11)] + + # clean the stack + swapdw dropw dropw + repeat.3 + movup.5 drop + end + # => [NOTE_INPUTS_COMMITMENT, num_inputs] +end + +#! Returns the script root of the input note with the specified index. +#! +#! Inputs: [note_index] +#! Outputs: [SCRIPT_ROOT] +#! +#! Where: +#! - note_index is the index of the input note whose script root should be returned. +#! - SCRIPT_ROOT is the script root 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_script_root + # start padding the stack + push.0 swap + # => [note_index, 0] + + # push the flag indicating that we want to request script root from the note with the specified + # index + push.0 + # => [is_active_note = 0, note_index, 0] + + exec.kernel_proc_offsets::input_note_get_script_root_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 + # => [SCRIPT_ROOT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [SCRIPT_ROOT] +end + +#! Returns the serial number of the input note with the specified index. +#! +#! Inputs: [note_index] +#! Outputs: [SERIAL_NUMBER] +#! +#! Where: +#! - note_index is the index of the input note whose serial number should be returned. +#! - SERIAL_NUMBER is the serial number 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_serial_number + # start padding the stack + push.0 swap + # => [note_index, 0] + + # push the flag indicating that we want to request serial number from the note with the + # specified index + push.0 + # => [is_active_note = 0, note_index, 0] + + exec.kernel_proc_offsets::input_note_get_serial_number_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 + # => [SERIAL_NUMBER, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [SERIAL_NUMBER] +end diff --git a/crates/miden-lib/asm/miden/kernel_proc_offsets.masm b/crates/miden-lib/asm/miden/kernel_proc_offsets.masm index b998afdd1b..3f7215e494 100644 --- a/crates/miden-lib/asm/miden/kernel_proc_offsets.masm +++ b/crates/miden-lib/asm/miden/kernel_proc_offsets.masm @@ -2,9 +2,10 @@ # ------------------------------------------------------------------------------------------------- ### Account ##################################### + # Entire account commitment const.ACCOUNT_GET_INITIAL_COMMITMENT_OFFSET=0 -const.ACCOUNT_COMPUTE_CURRENT_COMMITMENT_OFFSET=1 +const.ACCOUNT_COMPUTE_COMMITMENT_OFFSET=1 # ID const.ACCOUNT_GET_ID_OFFSET=2 @@ -20,75 +21,75 @@ const.ACCOUNT_GET_CODE_COMMITMENT_OFFSET=5 const.ACCOUNT_GET_INITIAL_STORAGE_COMMITMENT_OFFSET=6 const.ACCOUNT_COMPUTE_STORAGE_COMMITMENT_OFFSET=7 const.ACCOUNT_GET_ITEM_OFFSET=8 -const.ACCOUNT_SET_ITEM_OFFSET=9 -const.ACCOUNT_GET_MAP_ITEM_OFFSET=10 -const.ACCOUNT_SET_MAP_ITEM_OFFSET=11 +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=12 -const.ACCOUNT_GET_VAULT_ROOT_OFFSET=13 -const.ACCOUNT_ADD_ASSET_OFFSET=14 -const.ACCOUNT_REMOVE_ASSET_OFFSET=15 -const.ACCOUNT_GET_BALANCE_OFFSET=16 -const.ACCOUNT_HAS_NON_FUNGIBLE_ASSET_OFFSET=17 +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=18 +const.ACCOUNT_COMPUTE_DELTA_COMMITMENT_OFFSET=21 # Procedure introspection -const.ACCOUNT_WAS_PROCEDURE_CALLED_OFFSET=19 +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=20 -const.FAUCET_BURN_ASSET_OFFSET=21 -const.FAUCET_GET_TOTAL_FUNGIBLE_ASSET_ISSUANCE_OFFSET=22 -const.FAUCET_IS_NON_FUNGIBLE_ASSET_ISSUED_OFFSET=23 +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 ######################################## -# assets -const.NOTE_GET_ASSETS_INFO_OFFSET=24 # accessor -const.NOTE_ADD_ASSET_OFFSET=25 # mutator - -# note parameters -const.NOTE_GET_SERIAL_NUMBER_OFFSET=26 -const.NOTE_GET_INPUTS_COMMITMENT_AND_LEN_OFFSET=27 -const.NOTE_GET_SENDER_OFFSET=28 -const.NOTE_GET_SCRIPT_ROOT_OFFSET=29 - -# note introspection -const.INPUT_NOTE_GET_ASSETS_INFO_OFFSET=30 -const.OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=31 - -const.INPUT_NOTE_GET_RECIPIENT_OFFSET=32 -const.OUTPUT_NOTE_GET_RECIPIENT_OFFSET=33 -const.INPUT_NOTE_GET_METADATA_OFFSET=34 -const.OUTPUT_NOTE_GET_METADATA_OFFSET=35 +# 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 + +# 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 ### Tx ########################################## -# creation -const.TX_CREATE_NOTE_OFFSET=36 -# input/output notes +# input notes +const.TX_GET_NUM_INPUT_NOTES_OFFSET=41 +const.TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=42 -const.TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=37 -const.TX_GET_NUM_INPUT_NOTES_OFFSET=38 - -const.TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=39 -const.TX_GET_NUM_OUTPUT_NOTES_OFFSET=40 +# output notes +const.TX_GET_NUM_OUTPUT_NOTES_OFFSET=43 +const.TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=44 # block info -const.TX_GET_BLOCK_COMMITMENT_OFFSET=41 -const.TX_GET_BLOCK_NUMBER_OFFSET=42 -const.TX_GET_BLOCK_TIMESTAMP_OFFSET=43 +const.TX_GET_BLOCK_COMMITMENT_OFFSET=45 +const.TX_GET_BLOCK_NUMBER_OFFSET=46 +const.TX_GET_BLOCK_TIMESTAMP_OFFSET=47 # foreign context -const.TX_START_FOREIGN_CONTEXT_OFFSET=44 -const.TX_END_FOREIGN_CONTEXT_OFFSET=45 +const.TX_START_FOREIGN_CONTEXT_OFFSET=48 +const.TX_END_FOREIGN_CONTEXT_OFFSET=49 # expiration data -const.TX_GET_EXPIRATION_DELTA_OFFSET=46 # accessor -const.TX_UPDATE_EXPIRATION_BLOCK_NUM_OFFSET=47 # mutator +const.TX_GET_EXPIRATION_DELTA_OFFSET=50 # accessor +const.TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=51 # mutator # ACCESSORS # ------------------------------------------------------------------------------------------------- @@ -101,22 +102,22 @@ const.TX_UPDATE_EXPIRATION_BLOCK_NUM_OFFSET=47 # mutator #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `account_get_initial_commitment` kernel procedure required to +#! - 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 push.ACCOUNT_GET_INITIAL_COMMITMENT_OFFSET end -#! Returns the offset of the `account_compute_current_commitment` kernel procedure. +#! Returns the offset of the `account_compute_commitment` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `account_compute_current_commitment` kernel procedure required -#! to get the address where this procedure is stored. -export.account_compute_current_commitment_offset - push.ACCOUNT_COMPUTE_CURRENT_COMMITMENT_OFFSET +#! - 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 + push.ACCOUNT_COMPUTE_COMMITMENT_OFFSET end #! Returns the offset of the `account_compute_delta_commitment` kernel procedure. @@ -125,7 +126,7 @@ end #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `account_compute_delta_commitment` kernel procedure required +#! - 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 push.ACCOUNT_COMPUTE_DELTA_COMMITMENT_OFFSET @@ -185,7 +186,7 @@ end #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `account_get_initial_storage_commitment` kernel procedure +#! - 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 push.ACCOUNT_GET_INITIAL_STORAGE_COMMITMENT_OFFSET @@ -251,6 +252,30 @@ export.account_set_map_item_offset push.ACCOUNT_SET_MAP_ITEM_OFFSET end +#! Returns the offset of the `account_get_initial_item` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! 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 + push.ACCOUNT_GET_INITIAL_ITEM_OFFSET +end + +#! Returns the offset of the `account_get_initial_map_item` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! 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 + push.ACCOUNT_GET_INITIAL_MAP_ITEM_OFFSET +end + #! Returns the offset of the `account_get_initial_vault_root` kernel procedure. #! #! Inputs: [] @@ -311,6 +336,18 @@ export.account_get_balance_offset push.ACCOUNT_GET_BALANCE_OFFSET end +#! Returns the offset of the `account_get_initial_balance` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! 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 + push.ACCOUNT_GET_INITIAL_BALANCE_OFFSET +end + #! Returns the offset of the `account_has_non_fungible_asset` kernel procedure. #! #! Inputs: [] @@ -335,6 +372,42 @@ export.account_was_procedure_called_offset push.ACCOUNT_WAS_PROCEDURE_CALLED_OFFSET end +#! Returns the offset of the `account_has_procedure` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! 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 + push.ACCOUNT_HAS_PROCEDURE_OFFSET +end + +#! Returns the offset of the `account_get_num_procedures` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! 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 + push.ACCOUNT_GET_NUM_PROCEDURES_OFFSET +end + +#! Returns the offset of the `account_get_procedure_root` kernel procedure. +#! +#! Inputs: [] +#! Outputs: [proc_offset] +#! +#! 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 + push.ACCOUNT_GET_PROCEDURE_ROOT_OFFSET +end + ### FAUCET ###################################### #! Returns the offset of the `faucet_mint_asset` kernel procedure. @@ -385,42 +458,30 @@ export.faucet_is_non_fungible_asset_issued_offset push.FAUCET_IS_NON_FUNGIBLE_ASSET_ISSUED_OFFSET end -### NOTE ######################################## +### OUTPUT NOTE ######################################## -#! Returns the offset of the `note_get_assets_info` kernel procedure. +#! Returns the offset of the `output_note_create` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `note_get_assets_info` kernel procedure required to get the +#! - proc_offset is the offset of the `output_note_create` kernel procedure required to get the #! address where this procedure is stored. -export.note_get_assets_info_offset - push.NOTE_GET_ASSETS_INFO_OFFSET +export.output_note_create_offset + push.OUTPUT_NOTE_CREATE_OFFSET end -#! Returns the offset of the `note_add_asset` kernel procedure. +#! Returns the offset of the `output_note_add_asset` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `note_add_asset` kernel procedure required to get the +#! - proc_offset is the offset of the `output_note_add_asset` kernel procedure required to get the #! address where this procedure is stored. -export.note_add_asset_offset - push.NOTE_ADD_ASSET_OFFSET -end - -#! Returns the offset of the `input_note_get_assets_info` kernel procedure. -#! -#! Inputs: [] -#! Outputs: [proc_offset] -#! -#! 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 - push.INPUT_NOTE_GET_ASSETS_INFO_OFFSET +export.output_note_add_asset_offset + push.OUTPUT_NOTE_ADD_ASSET_OFFSET end #! Returns the offset of the `output_note_get_assets_info` kernel procedure. @@ -435,18 +496,6 @@ export.output_note_get_assets_info_offset push.OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET end -#! Returns the offset of the `input_note_get_recipient` kernel procedure. -#! -#! Inputs: [] -#! Outputs: [proc_offset] -#! -#! 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 - push.INPUT_NOTE_GET_RECIPIENT_OFFSET -end - #! Returns the offset of the `output_note_get_recipient` kernel procedure. #! #! Inputs: [] @@ -459,92 +508,94 @@ export.output_note_get_recipient_offset push.OUTPUT_NOTE_GET_RECIPIENT_OFFSET end -#! Returns the offset of the `input_note_get_metadata` kernel procedure. +#! Returns the offset of the `output_note_get_metadata` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `input_note_get_metadata` kernel procedure required to get +#! - proc_offset is the offset of the `output_note_get_metadata` kernel procedure required to get #! the address where this procedure is stored. -export.input_note_get_metadata_offset - push.INPUT_NOTE_GET_METADATA_OFFSET +export.output_note_get_metadata_offset + push.OUTPUT_NOTE_GET_METADATA_OFFSET end -#! Returns the offset of the `output_note_get_metadata` kernel procedure. +### INPUT NOTE ######################################## + +#! Returns the offset of the `input_note_get_assets_info` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! 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 - push.OUTPUT_NOTE_GET_METADATA_OFFSET +#! - 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 + push.INPUT_NOTE_GET_ASSETS_INFO_OFFSET end -#! Returns the offset of the `note_get_serial_number` kernel procedure. +#! Returns the offset of the `input_note_get_recipient` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `note_get_serial_number` kernel procedure required to get the -#! address where this procedure is stored. -export.note_get_serial_number_offset - push.NOTE_GET_SERIAL_NUMBER_OFFSET +#! - 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 + push.INPUT_NOTE_GET_RECIPIENT_OFFSET end -#! Returns the offset of the `note_get_inputs_commitment_and_len` kernel procedure. +#! Returns the offset of the `input_note_get_metadata` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `note_get_inputs_commitment_and_len` kernel procedure required -#! to get the address where this procedure is stored. -export.note_get_inputs_commitment_and_len_offset - push.NOTE_GET_INPUTS_COMMITMENT_AND_LEN_OFFSET +#! - 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 + push.INPUT_NOTE_GET_METADATA_OFFSET end -#! Returns the offset of the `note_get_sender` kernel procedure. +#! Returns the offset of the `input_note_get_serial_number` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `note_get_sender` kernel procedure required to get the address -#! where this procedure is stored. -export.note_get_sender_offset - push.NOTE_GET_SENDER_OFFSET +#! - 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 + push.INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET end -#! Returns the offset of the `note_get_script_root` kernel procedure. +#! Returns the offset of the `input_note_get_inputs_info` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `note_get_script_root` kernel procedure required to get the -#! address where this procedure is stored. -export.note_get_script_root_offset - push.NOTE_GET_SCRIPT_ROOT_OFFSET +#! - 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 + push.INPUT_NOTE_GET_INPUTS_INFO_OFFSET end -### TRANSACTION ################################# - -#! Returns the offset of the `tx_create_note` kernel procedure. +#! Returns the offset of the `input_note_get_script_root` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `tx_create_note` kernel procedure required to get the address -#! where this procedure is stored. -export.tx_create_note_offset - push.TX_CREATE_NOTE_OFFSET +#! - 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 + push.INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET end +### TRANSACTION ################################# + #! Returns the offset of the `tx_get_input_notes_commitment` kernel procedure. #! #! Inputs: [] @@ -575,7 +626,7 @@ end #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `tx_get_num_input_notes` kernel procedure required to get the +#! - 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 push.TX_GET_NUM_INPUT_NOTES_OFFSET @@ -587,7 +638,7 @@ end #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `tx_get_num_output_notes` kernel procedure required to get the +#! - 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 push.TX_GET_NUM_OUTPUT_NOTES_OFFSET @@ -653,16 +704,16 @@ export.tx_end_foreign_context_offset push.TX_END_FOREIGN_CONTEXT_OFFSET end -#! Returns the offset of the `tx_update_expiration_block_num` kernel procedure. +#! Returns the offset of the `tx_update_expiration_block_delta` kernel procedure. #! #! Inputs: [] #! Outputs: [proc_offset] #! #! Where: -#! - proc_offset is the offset of the `tx_update_expiration_block_num` kernel procedure required to -#! get the address where this procedure is stored. -export.tx_update_expiration_block_num_offset - push.TX_UPDATE_EXPIRATION_BLOCK_NUM_OFFSET +#! - 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 + push.TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET end #! Returns the offset of the `tx_get_expiration_delta` kernel procedure. diff --git a/crates/miden-lib/asm/miden/native_account.masm b/crates/miden-lib/asm/miden/native_account.masm new file mode 100644 index 0000000000..f09f638c0d --- /dev/null +++ b/crates/miden-lib/asm/miden/native_account.masm @@ -0,0 +1,275 @@ +use.miden::kernel_proc_offsets + +# NATIVE ACCOUNT PROCEDURES +# ================================================================================================= + +# ID AND NONCE +# ------------------------------------------------------------------------------------------------- + +#! Returns the ID of the native account of the transaction. +#! +#! Inputs: [] +#! Outputs: [account_id_prefix, account_id_suffix] +#! +#! Where: +#! - account_id_{prefix,suffix} are the prefix and suffix felts of the native account ID of the +#! transaction. +#! +#! Invocation: exec +export.get_id + # pad the stack + padw padw padw push.0.0 + # => [pad(14)] + + # push the flag indicating that the ID of the native account was requested + push.1 + # => [is_native = 1, pad(14)] + + exec.kernel_proc_offsets::account_get_id_offset + # => [offset, is_native = 0, pad(14)] + + syscall.exec_kernel_proc + # => [account_id_prefix, account_id_suffix, pad(14)] + + # clean the stack + swapdw dropw dropw swapw dropw movdn.3 movdn.3 drop drop + # => [account_id_prefix, account_id_suffix] +end + +#! Increments the nonce of the native account by one and returns the new nonce. +#! +#! Inputs: [] +#! Outputs: [final_nonce] +#! +#! Where: +#! - final_nonce is the new nonce of the account. Since it cannot be incremented again, this will +#! also be the final nonce of the account after transaction execution. +#! +#! Panics if: +#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the authentication procedure +#! of the account. +#! - the nonce has already been incremented. +#! +#! Invocation: exec +export.incr_nonce + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + exec.kernel_proc_offsets::account_incr_nonce_offset + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [final_nonce, pad(15)] + + swap.15 dropw dropw dropw drop drop drop + # => [final_nonce] +end + +# COMMITMENTS +# ------------------------------------------------------------------------------------------------- + +#! Computes the commitment to the native account's delta. +#! +#! Note that if the account state has changed, the nonce must be incremented before this procedure +#! is called, otherwise it will panic. This means it can only be called from an auth procedure, +#! since only auth procedures are allowed to increment the nonce. +#! +#! The commitment to an empty delta is defined as the empty word. +#! +#! During an account-creating transaction (when the initial nonce is 0), this procedure will not +#! return the empty word even if the initial storage commitment and the current storage commitment +#! are identical (storage hasn't changed). This is because the delta for a new account must +#! represent its entire newly created state, and the initial storage in a transaction is initialized +#! to the storage that the account ID commits to, which may be non-empty. This does not have any +#! consequences other than being inconsistent in this edge case. +#! +#! Inputs: [] +#! Outputs: [DELTA_COMMITMENT] +#! +#! Where: +#! - DELTA_COMMITMENT is the commitment to the account delta. +#! +#! Panics if: +#! - the vault or storage delta is not empty but the nonce increment is zero. +export.compute_delta_commitment + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + exec.kernel_proc_offsets::account_compute_delta_commitment_offset + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [DELTA_COMMITMENT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [DELTA_COMMITMENT] +end + +# STORAGE +# ------------------------------------------------------------------------------------------------- + +#! Sets an item in the native account storage. +#! +#! Inputs: [index, VALUE] +#! Outputs: [OLD_VALUE] +#! +#! Where: +#! - index is the index of the item to set. +#! - 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. +#! +#! Invocation: exec +export.set_item + exec.kernel_proc_offsets::account_set_item_offset + # => [offset, index, VALUE] + + # pad the stack + push.0.0 movdn.7 movdn.7 padw padw swapdw + # => [offset, index, VALUE, pad(10)] + + syscall.exec_kernel_proc + # => [OLD_VALUE, pad(12)] + + # clean the stack + swapw.3 dropw dropw dropw + # => [OLD_VALUE] +end + +#! Sets a map item in the native account storage. +#! +#! Inputs: [index, KEY, VALUE] +#! Outputs: [OLD_MAP_ROOT, OLD_MAP_VALUE] +#! +#! Where: +#! - index is the index of the map where the KEY VALUE should be set. +#! - 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. +#! +#! Panics if: +#! - the index for the map is out of bounds, meaning > 255. +#! - the slot item at index is not a map. +#! +#! Invocation: exec +export.set_map_item + exec.kernel_proc_offsets::account_set_map_item_offset + # => [offset, index, KEY, VALUE] + + # pad the stack + push.0.0 movdn.11 movdn.11 padw movdnw.3 + # => [offset, index, KEY, VALUE, pad(6)] + + syscall.exec_kernel_proc + # => [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] + + # clean the stack + swapdw dropw dropw + # => [OLD_MAP_ROOT, OLD_MAP_VALUE] +end + +# VAULT +# ------------------------------------------------------------------------------------------------- + +#! Add the specified asset to the vault. +#! +#! Inputs: [ASSET] +#! Outputs: [ASSET'] +#! +#! Where: +#! - ASSET' is a final asset in the account vault defined as follows: +#! - If ASSET is a non-fungible asset, then ASSET' is the same as ASSET. +#! - If ASSET is a fungible asset, then ASSET' is the total fungible asset in the account vault +#! after ASSET was added to it. +#! +#! Panics if: +#! - 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. +#! +#! Invocation: exec +export.add_asset + exec.kernel_proc_offsets::account_add_asset_offset + # => [offset, ASSET] + + # pad the stack + push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw + # => [offset, ASSET, pad(11)] + + syscall.exec_kernel_proc + # => [ASSET', pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [ASSET'] +end + +#! Remove the specified asset from the vault. +#! +#! Inputs: [ASSET] +#! Outputs: [ASSET] +#! +#! Where: +#! - ASSET is the asset to remove from the vault. +#! +#! 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: exec +export.remove_asset + exec.kernel_proc_offsets::account_remove_asset_offset + # => [offset, ASSET] + + # pad the stack + push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw + # => [offset, ASSET, pad(11)] + + syscall.exec_kernel_proc + # => [ASSET, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [ASSET] +end + +# CODE +# ------------------------------------------------------------------------------------------------- + +#! Returns 1 if a native account procedure was called during transaction execution, and 0 otherwise. +#! +#! Note: This returns 1 only if the procedure invoked account-restricted kernel APIs (e.g., +#! `exec.faucet::mint`) which trigger `authenticate_and_track_procedure`. Procedures that execute +#! only local MASM instructions will return 0 even if they were executed. +#! +#! Inputs: [PROC_ROOT] +#! Outputs: [was_called] +#! +#! Where: +#! - PROC_ROOT is the hash of the procedure to check. +#! - was_called is 1 if the procedure was called, 0 otherwise. +#! +#! Invocation: exec +export.was_procedure_called + exec.kernel_proc_offsets::account_was_procedure_called_offset + # => [offset, PROC_ROOT] + + # pad the stack + push.0.0.0 movdn.7 movdn.7 movdn.7 padw padw swapdw + # => [offset, PROC_ROOT, pad(11)] + + syscall.exec_kernel_proc + # => [was_called, pad(15)] + + # clean the stack + swapdw dropw dropw swapw dropw movdn.3 drop drop drop + # => [was_called] +end diff --git a/crates/miden-lib/asm/miden/note.masm b/crates/miden-lib/asm/miden/note.masm index 712a6929f8..5ce7214497 100644 --- a/crates/miden-lib/asm/miden/note.masm +++ b/crates/miden-lib/asm/miden/note.masm @@ -1,189 +1,26 @@ -use.miden::kernel_proc_offsets +use.miden::account_id use.std::crypto::hashes::rpo use.std::mem -use.miden::contracts::wallets::basic->wallet # ERRORS # ================================================================================================= -const.ERR_NOTE_DATA_DOES_NOT_MATCH_COMMITMENT="note data does not match the commitment" +const.ERR_PROLOGUE_NOTE_INPUTS_LEN_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 128" - -const.ERR_NOTE_INVALID_NUMBER_OF_NOTE_INPUTS="the specified number of note inputs does not match the actual number" - -# PROCEDURES +# NOTE UTILITY PROCEDURES # ================================================================================================= -#! Writes the assets of the currently executing note into memory starting at the specified address. -#! -#! Inputs: [dest_ptr] -#! Outputs: [num_assets, dest_ptr] -#! -#! Where: -#! - dest_ptr is the memory address to write the assets. -#! - num_assets is the number of assets in the currently executing note. -#! -#! Invocation: exec -export.get_assets - # pad the stack - padw padw padw push.0.0.0 - # => [pad(15), dest_ptr] - - exec.kernel_proc_offsets::note_get_assets_info_offset - # => [offset, pad(15), dest_ptr] - - syscall.exec_kernel_proc - # => [ASSETS_COMMITMENT, num_assets, pad(11), dest_ptr] - - # clean the stack - swapdw dropw dropw movup.7 movup.7 movup.7 drop drop drop - # => [ASSETS_COMMITMENT, num_assets, dest_ptr] - - # write the assets from the advice map into memory - exec.write_assets_to_memory - # => [num_assets, dest_ptr] -end - -#! Loads the note's inputs to `dest_ptr`. -#! -#! Inputs: -#! Stack: [dest_ptr] -#! Advice Map: { INPUTS_COMMITMENT: [INPUTS] } -#! Outputs: -#! Stack: [num_inputs, dest_ptr] -#! -#! Where: -#! - dest_ptr is the memory address to write the inputs. -#! - INPUTS_COMMITMENT is the sequential hash of the padded note's inputs. -#! - INPUTS is the data corresponding to the note's inputs. -#! -#! Invocation: exec -export.get_inputs - # pad the stack - padw padw padw push.0.0.0 - # OS => [pad(15), dest_ptr] - - exec.kernel_proc_offsets::note_get_inputs_commitment_and_len_offset - # OS => [offset, pad(15), dest_ptr] - - syscall.exec_kernel_proc - # OS => [INPUTS_COMMITMENT, num_inputs, pad(11), dest_ptr] - - # clean the stack - swapdw dropw dropw - movup.5 drop movup.5 drop movup.5 drop - # OS => [INPUTS_COMMITMENT, num_inputs, dest_ptr] - - # load the inputs from the advice map to the advice stack - adv.push_mapvaln - # OS => [INPUTS_COMMITMENT, num_inputs, dest_ptr] - # AS => [advice_num_inputs, [INPUT_VALUES]] - - # move the number of inputs obtained from advice map to the operand stack - adv_push.1 dup.5 - # OS => [num_inputs, advice_num_inputs, 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, INPUTS_COMMITMENT, num_inputs, dest_ptr] - # AS => [[INPUT_VALUES]] - - assert_eq.err=ERR_NOTE_INVALID_NUMBER_OF_NOTE_INPUTS - # OS => [INPUTS_COMMITMENT, num_inputs, dest_ptr] - # AS => [[INPUT_VALUES]] - - # calculate the number of words required to store the inputs - dup.4 u32divmod.4 neq.0 add - # OS => [num_words, INPUTS_COMMITMENT, num_inputs, dest_ptr] - # AS => [[INPUT_VALUES]] - - # round up the number of words to the next multiple of 2 - dup is_odd add - # OS => [even_num_words, 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, INPUTS_COMMITMENT, num_inputs, dest_ptr] - # AS => [[INPUT_VALUES]] - - # write the input values from the advice stack into memory - exec.mem::pipe_preimage_to_memory drop - # OS => [num_inputs, dest_ptr] - # AS => [] -end - -#! Returns the sender of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [sender_id_prefix, sender_id_suffix] -#! -#! Where: -#! - sender_{prefix,suffix} are the prefix and suffix felts of the sender of the note currently -#! being processed. -#! -#! Panics if: -#! - no note is being processed. -#! -#! Invocation: exec -export.get_sender - # pad the stack - padw padw padw push.0.0.0 - # => [pad(15)] - - exec.kernel_proc_offsets::note_get_sender_offset - # => [offset, pad(15)] - - syscall.exec_kernel_proc - # => [sender, pad(15)] - - # clean the stack - swapdw dropw dropw swapw dropw movdn.3 movdn.3 drop drop - # => [sender_id_prefix, sender_id_suffix] -end - -#! Returns the serial number of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [SERIAL_NUMBER] -#! -#! Where: -#! - SERIAL_NUMBER is the serial number of the note currently being processed. -#! -#! Panics if: -#! - no note is being processed. -#! -#! Invocation: exec -export.get_serial_number - # pad the stack - padw padw padw push.0.0.0 - # => [pad(15)] - - exec.kernel_proc_offsets::note_get_serial_number_offset - # => [offset, pad(15)] - - syscall.exec_kernel_proc - # => [SERIAL_NUMBER, pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw - # => [SERIAL_NUMBER] -end - #! Computes the commitment to the note inputs starting at the specified memory address. #! -#! This procedure checks that the provided number of inputs is within limits and then computes the -#! commitment. +#! 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 if inputs is 0, procedure returns the empty word: [0, 0, 0, 0]. +#! If the number of note inputs is 0, procedure returns the empty word: [0, 0, 0, 0]. #! #! Inputs: [inputs_ptr, num_inputs] -#! Outputs: [COMMITMENT] +#! Outputs: [INPUTS_COMMITMENT] #! #! Cycles: #! - If number of elements divides by 8: 56 cycles + 3 * words @@ -196,8 +33,8 @@ end #! Invocation: exec export.compute_inputs_commitment # check that number of inputs is less than 128 - dup.1 push.128 u32assert2.err=ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT - u32lte assert.err=ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT + dup.1 push.128 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 @@ -206,37 +43,8 @@ export.compute_inputs_commitment # => [inputs_ptr, num_inputs, pad_inputs_flag] exec.rpo::prepare_hasher_state - exec.rpo::hash_memory_with_state - # => [COMMITMENT] -end - -#! Returns the script root of the note currently being processed. -#! -#! Inputs: [] -#! Outputs: [SCRIPT_ROOT] -#! -#! Where: -#! - SCRIPT_ROOT is the script root of the note currently being processed. -#! -#! Panics if: -#! - no note is being processed. -#! -#! Invocation: exec -export.get_script_root - # pad the stack - padw padw padw push.0.0.0 - # => [pad(15)] - - exec.kernel_proc_offsets::note_get_script_root_offset - # => [offset, pad(15)] - - syscall.exec_kernel_proc - # => [SCRIPT_ROOT, pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw - # => [SCRIPT_ROOT] + # => [INPUTS_COMMITMENT] end #! Returns the max allowed number of input values per note. @@ -247,64 +55,7 @@ end #! - max_inputs_per_note is the max inputs per note. export.::miden::util::note::get_max_inputs_per_note -#! Adds all assets from the currently executing note to the account 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 - # => [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 - -#! Writes the assets data stored in the advice map to the memory specified by the provided +#! Writes the assets data stored in the advice map to the memory specified by the provided #! destination pointer. #! #! Inputs: @@ -335,3 +86,121 @@ export.write_assets_to_memory # OS => [num_assets, dest_ptr] # AS => [] end + +#! Builds the recipient hash from note inputs, script root, and serial number. +#! +#! This procedure computes the commitment of the note inputs and then uses it to calculate the note +#! recipient by hashing this commitment, the provided script root, and the serial number. +#! +#! Inputs: +#! Operand stack: [inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] +#! Advice map: { +#! INPUTS_COMMITMENT: [INPUTS], +#! } +#! Outputs: +#! Operand stack: [RECIPIENT] +#! Advice map: { +#! INPUTS_COMMITMENT: [INPUTS], +#! RECIPIENT: [SERIAL_SCRIPT_HASH, INPUTS_COMMITMENT], +#! SERIAL_SCRIPT_HASH: [SERIAL_HASH, SCRIPT_ROOT], +#! SERIAL_HASH: [SERIAL_NUM, EMPTY_WORD], +#! } +#! +#! Where: +#! - inputs_ptr is the memory address where the note inputs are stored. +#! - num_inputs is the number of input values. +#! - SCRIPT_ROOT is the script root of the note. +#! - SERIAL_NUM is the serial number of the note. +#! - RECIPIENT is the commitment to the input note's script, inputs, and the serial number. +#! +#! Locals: +#! - 0: inputs_ptr +#! - 1: num_inputs +#! +#! Panics if: +#! - inputs_ptr is not word-aligned (i.e., is not a multiple of 4). +#! - num_inputs is greater than 128. +#! +#! Invocation: exec +export.build_recipient + dup.1 dup.1 + # => [inputs_ptr, num_inputs, inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] + + exec.compute_inputs_commitment + # => [INPUTS_COMMITMENT, inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] + + movup.5 movup.5 dup movdn.2 + # => [inputs_ptr, num_inputs, inputs_ptr, INPUTS_COMMITMENT, SERIAL_NUM, SCRIPT_ROOT] + + add swap + # => [inputs_ptr, end_ptr, INPUTS_COMMITMENT, SERIAL_NUM, SCRIPT_ROOT] + + movdn.5 movdn.5 + # => [INPUTS_COMMITMENT, inputs_ptr, end_ptr, SERIAL_NUM, SCRIPT_ROOT] + + adv.insert_mem + # => [INPUTS_COMMITMENT, inputs_ptr, end_ptr, SERIAL_NUM, SCRIPT_ROOT] + + movup.4 drop movup.4 drop + # => [INPUTS_COMMITMENT, SERIAL_NUM, SCRIPT_ROOT] + + movdnw.2 + # => [SERIAL_NUM, SCRIPT_ROOT, INPUTS_COMMITMENT] + + padw adv.insert_hdword hmerge + # => [SERIAL_HASH, SCRIPT_ROOT, INPUTS_COMMITMENT] + + swapw adv.insert_hdword hmerge + # => [SERIAL_SCRIPT_HASH, INPUTS_COMMITMENT] + + swapw adv.insert_hdword hmerge + # => [RECIPIENT] +end + +#! Returns the RECIPIENT for a specified SERIAL_NUM, SCRIPT_ROOT, and inputs commitment. +#! +#! Inputs: [SERIAL_NUM, SCRIPT_ROOT, INPUT_COMMITMENT] +#! Outputs: [RECIPIENT] +#! +#! Where: +#! - SERIAL_NUM is the serial number of the recipient. +#! - SCRIPT_ROOT is the commitment of the note script. +#! - INPUT_COMMITMENT is the commitment of the note inputs. +#! - RECIPIENT is the recipient of the note. +#! +#! Invocation: exec +export.build_recipient_hash + padw hmerge + # => [SERIAL_NUM_HASH, SCRIPT_ROOT, INPUT_COMMITMENT] + + swapw hmerge + # => [MERGE_SCRIPT, INPUT_COMMITMENT] + + swapw hmerge + # [RECIPIENT] +end + +#! Extracts the sender ID from the provided metadata word. +#! +#! Inputs: [METADATA] +#! Outputs: [sender_id_prefix, sender_id_suffix] +#! +#! Where: +#! - METADATA is the metadata of some 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] + + # drop aux felt and the felt containing tag, execution hint and payload + 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 + exec.account_id::shape_suffix + # => [sender_id_suffix, sender_id_prefix] + + # rearrange suffix and prefix + swap + # => [sender_id_prefix, sender_id_suffix] +end diff --git a/crates/miden-lib/asm/miden/output_note.masm b/crates/miden-lib/asm/miden/output_note.masm index 0054ce8281..39c900edfc 100644 --- a/crates/miden-lib/asm/miden/output_note.masm +++ b/crates/miden-lib/asm/miden/output_note.masm @@ -1,10 +1,40 @@ use.miden::kernel_proc_offsets use.miden::note -use.std::mem # PROCEDURES # ================================================================================================= +#! Creates a new note and returns the index of the note. +#! +#! Inputs: [tag, aux, note_type, execution_hint, 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. +#! +#! Invocation: exec +export.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)] + + exec.kernel_proc_offsets::output_note_create_offset + # => [offset, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] + + syscall.exec_kernel_proc + # => [note_idx, pad(15)] + + # remove excess PADs from the stack + swapdw dropw dropw movdn.7 dropw drop drop drop + # => [note_idx] +end + #! Returns the information about assets in the output note with the specified index. #! #! This information can then be used to retrieve the actual assets from the advice map. @@ -37,14 +67,14 @@ export.get_assets_info # => [ASSETS_COMMITMENT, num_assets, pad(11)] # clean the stack - swapdw dropw dropw + swapdw dropw dropw repeat.3 movup.5 drop end # => [ASSETS_COMMITMENT, num_assets] end -#! Writes the assets of the output note with the specified index into memory starting at the +#! Writes the assets of the output note with the specified index into memory starting at the #! specified address. #! #! Attention: memory starting from the `dest_ptr` should have enough space to store all the assets @@ -77,6 +107,33 @@ export.get_assets # => [num_assets, dest_ptr, note_index] end +#! Adds the ASSET to the note specified by the index. +#! +#! Inputs: [ASSET, note_idx] +#! Outputs: [] +#! +#! Where: +#! - note_idx is the index of the note to which the asset is added. +#! - ASSET can be a fungible or non-fungible asset. +#! +#! Invocation: exec +export.add_asset + movup.4 exec.kernel_proc_offsets::output_note_add_asset_offset + # => [offset, note_idx, ASSET] + + # pad the stack before the syscall to prevent accidental modification of the deeper stack + # elements + push.0.0 movdn.7 movdn.7 padw padw swapdw + # => [offset, note_idx, ASSET, pad(10)] + + syscall.exec_kernel_proc + # => [pad(16)] + + # remove excess PADs from the stack + dropw dropw dropw dropw + # => [] +end + #! Returns the recipient of the output note with the specified index. #! #! Inputs: [note_index] diff --git a/crates/miden-lib/asm/miden/tx.masm b/crates/miden-lib/asm/miden/tx.masm index 2948913dce..a6890e1fe4 100644 --- a/crates/miden-lib/asm/miden/tx.masm +++ b/crates/miden-lib/asm/miden/tx.masm @@ -199,87 +199,6 @@ export.get_num_output_notes # => [num_output_notes] end -#! Creates a new note and returns the index of the note. -#! -#! Inputs: [tag, aux, note_type, execution_hint, 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. -#! -#! Invocation: exec -export.create_note - # 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)] - - exec.kernel_proc_offsets::tx_create_note_offset - # => [offset, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] - - syscall.exec_kernel_proc - # => [note_idx, pad(15)] - - # remove excess PADs from the stack - swapdw dropw dropw movdn.7 dropw drop drop drop - # => [note_idx] -end - -#! Adds the ASSET to the note specified by the index. -#! -#! Inputs: [ASSET, note_idx] -#! Outputs: [ASSET, note_idx] -#! -#! Where: -#! - note_idx is the index of the note to which the asset is added. -#! - ASSET can be a fungible or non-fungible asset. -#! -#! Invocation: exec -export.add_asset_to_note - movup.4 exec.kernel_proc_offsets::note_add_asset_offset - # => [offset, note_idx, ASSET] - - # pad the stack before the syscall to prevent accidental modification of the deeper stack - # elements - push.0.0 movdn.7 movdn.7 padw padw swapdw - # => [offset, note_idx, ASSET, pad(10)] - - syscall.exec_kernel_proc - # => [note_idx, ASSET, pad(11)] - - # remove excess PADs from the stack - swapdw dropw dropw swapw movdn.7 drop drop drop movdn.4 - # => [ASSET, note_idx] -end - -#! Returns the RECIPIENT for a specified SERIAL_NUM, SCRIPT_ROOT, and inputs commitment. -#! -#! Inputs: [SERIAL_NUM, SCRIPT_ROOT, INPUT_COMMITMENT] -#! Outputs: [RECIPIENT] -#! -#! Where: -#! - SERIAL_NUM is the serial number of the recipient. -#! - SCRIPT_ROOT is the commitment of the note script. -#! - INPUT_COMMITMENT is the commitment of the note inputs. -#! - RECIPIENT is the recipient of the note. -#! -#! Invocation: exec -export.build_recipient_hash - padw hmerge - # => [SERIAL_NUM_HASH, SCRIPT_ROOT, INPUT_COMMITMENT] - - swapw hmerge - # => [MERGE_SCRIPT, INPUT_COMMITMENT] - - swapw hmerge - # [RECIPIENT] -end - #! Executes the provided procedure against the foreign account. #! #! WARNING: the procedure to be invoked can not have more than 15 inputs and it can not return more @@ -315,14 +234,14 @@ export.execute_foreign_procedure.4 # store the foreign procedure root to the first local memory slot and get its absolute memory # address - loc_storew.0 dropw locaddr.0 + loc_storew_be.0 dropw locaddr.0 # => [foreign_proc_root_ptr, , pad(n)] # execute the foreign procedure dyncall # => [] - # reset the current account data offset to the native offset (2048) + # reset the active account data offset to the native offset (2048) push.0.0.0 padw padw padw exec.kernel_proc_offsets::tx_end_foreign_context_offset # => [offset, pad(15), ] @@ -350,7 +269,7 @@ end #! #! Annotation hint: is not used anywhere export.update_expiration_block_delta - exec.kernel_proc_offsets::tx_update_expiration_block_num_offset + exec.kernel_proc_offsets::tx_update_expiration_block_delta_offset # => [offset, expiration_delta, ...] # pad the stack diff --git a/crates/miden-lib/asm/note_scripts/BURN.masm b/crates/miden-lib/asm/note_scripts/BURN.masm new file mode 100644 index 0000000000..42b68ed11b --- /dev/null +++ b/crates/miden-lib/asm/note_scripts/BURN.masm @@ -0,0 +1,28 @@ +use.miden::contracts::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 +#! (e.g., basic fungible faucet or network fungible faucet). +#! +#! The burn procedure in the faucet already handles all necessary validations including: +#! - Checking that the note contains exactly one asset +#! - Verifying the asset is a fungible asset issued by this faucet +#! - Ensuring the amount to burn doesn't exceed the outstanding supply +#! +#! Requires that the account exposes: +#! - burn procedure (from the faucets interface). +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - account does not expose burn procedure. +#! - any of the validations in the burn procedure fail. +begin + dropw + # => [pad(16)] + + # Call the faucet's burn procedure which handles all validations + call.faucets::burn + # => [pad(16)] +end diff --git a/crates/miden-lib/asm/note_scripts/MINT.masm b/crates/miden-lib/asm/note_scripts/MINT.masm new file mode 100644 index 0000000000..fec23f48a4 --- /dev/null +++ b/crates/miden-lib/asm/note_scripts/MINT.masm @@ -0,0 +1,61 @@ +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/note_scripts/P2ID.masm b/crates/miden-lib/asm/note_scripts/P2ID.masm index 24bfc59a0b..f5b565d223 100644 --- a/crates/miden-lib/asm/note_scripts/P2ID.masm +++ b/crates/miden-lib/asm/note_scripts/P2ID.masm @@ -1,6 +1,6 @@ -use.miden::account +use.miden::active_account use.miden::account_id -use.miden::note +use.miden::active_note # ERRORS # ================================================================================================= @@ -29,7 +29,7 @@ const.ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transacti #! greater than 2^63. begin # store the note inputs to memory starting at address 0 - padw push.0 exec.note::get_inputs + padw push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr, EMPTY_WORD] # make sure the number of inputs is 2 @@ -37,16 +37,16 @@ begin # => [inputs_ptr, EMPTY_WORD] # read the target account ID from the note inputs - mem_loadw drop drop + mem_loadw_be drop drop # => [target_account_id_prefix, target_account_id_suffix] - exec.account::get_id + exec.active_account::get_id # => [account_id_prefix, account_id_suffix, target_account_id_prefix, target_account_id_suffix, ...] # ensure account_id = target_account_id, fails otherwise exec.account_id::is_equal assert.err=ERR_P2ID_TARGET_ACCT_MISMATCH # => [] - exec.note::add_assets_to_account + exec.active_note::add_assets_to_account # => [] end diff --git a/crates/miden-lib/asm/note_scripts/P2IDE.masm b/crates/miden-lib/asm/note_scripts/P2IDE.masm index 6482825723..8e885cc02d 100644 --- a/crates/miden-lib/asm/note_scripts/P2IDE.masm +++ b/crates/miden-lib/asm/note_scripts/P2IDE.masm @@ -1,8 +1,7 @@ -use.miden::account +use.miden::active_account use.miden::account_id -use.miden::note +use.miden::active_note use.miden::tx -use.note_scripts::utils # ERRORS # ================================================================================================= @@ -41,11 +40,11 @@ end #! Outputs: [] #! #! Panics if: -#! - the reclaim of the current note is disabled. +#! - 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 - # check that the reclaim of the current note is enabled + # 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] @@ -56,16 +55,16 @@ proc.reclaim_note lte assert.err=ERR_P2IDE_RECLAIM_HEIGHT_NOT_REACHED # => [account_id_prefix, account_id_suffix] - # if current account is not the target, we need to ensure it is the sender - exec.note::get_sender + # if active account is not the target, we need to ensure it is the sender + exec.active_note::get_sender # => [sender_account_id_prefix, sender_account_id_suffix, account_id_prefix, account_id_suffix] - # ensure current account ID = sender account ID + # ensure active account ID = sender account ID exec.account_id::is_equal assert.err=ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER # => [] # add note assets to account - exec.note::add_assets_to_account + exec.active_note::add_assets_to_account # => [] end @@ -102,7 +101,7 @@ end #! greater than 2^63. begin # store the note inputs to memory starting at address 0 - push.0 exec.note::get_inputs + push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr] # make sure the number of inputs is 4 @@ -110,7 +109,7 @@ begin # => [inputs_ptr] # read the reclaim block height, timelock_block_height, and target account ID from the note inputs - mem_loadw + mem_loadw_be # => [timelock_block_height, reclaim_block_height, target_account_id_prefix, target_account_id_suffix] # read the current block number @@ -121,21 +120,21 @@ begin exec.verify_unlocked # => [current_block_height, reclaim_block_height, target_account_id_prefix, target_account_id_suffix] - # get current account id - exec.account::get_id dup.1 dup.1 + # get active account id + exec.active_account::get_id dup.1 dup.1 # => [account_id_prefix, account_id_suffix, account_id_prefix, account_id_suffix, current_block_height, reclaim_block_height, target_account_id_prefix, target_account_id_suffix] - # determine if the current account is the target account + # determine if the active account is the target account movup.7 movup.7 exec.account_id::is_equal # => [is_target, account_id_prefix, account_id_suffix, current_block_height, reclaim_block_height] if.true - # we can safely consume the note since the current account is the target of the note - dropw exec.note::add_assets_to_account + # we can safely consume the note since the active account is the target of the note + dropw exec.active_note::add_assets_to_account # => [] else - # checks if current account is sender and if reclaim is enabled + # checks if active account is sender and if reclaim is enabled exec.reclaim_note # => [] end diff --git a/crates/miden-lib/asm/note_scripts/SWAP.masm b/crates/miden-lib/asm/note_scripts/SWAP.masm index 15e13e1c32..479e328593 100644 --- a/crates/miden-lib/asm/note_scripts/SWAP.masm +++ b/crates/miden-lib/asm/note_scripts/SWAP.masm @@ -1,5 +1,5 @@ -use.miden::note -use.miden::tx +use.miden::active_note +use.miden::output_note use.miden::contracts::wallets::basic->wallet # CONSTANTS @@ -46,7 +46,7 @@ begin # --- create a payback note with the requested asset ---------------- # store note inputs into memory starting at address 0 - push.0 exec.note::get_inputs + push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr] # make sure the number of inputs is 12 @@ -54,19 +54,19 @@ begin # => [inputs_ptr] # load REQUESTED_ASSET - mem_loadw + mem_loadw_be # => [REQUESTED_ASSET] # load PAYBACK_NOTE_RECIPIENT - padw mem_loadw.4 + padw mem_loadw_be.4 # => [PAYBACK_NOTE_RECIPIENT, REQUESTED_ASSET] # load payback P2ID details - padw mem_loadw.8 + padw mem_loadw_be.8 # => [tag, aux, note_type, execution_hint, PAYBACK_NOTE_RECIPIENT, REQUESTED_ASSET] # create payback P2ID note - exec.tx::create_note + exec.output_note::create # => [note_idx, REQUESTED_ASSET] movdn.4 @@ -89,7 +89,7 @@ begin # --- move assets from the SWAP note into the account ------------------------- # store the number of note assets to memory starting at address 0 - push.0 exec.note::get_assets + push.0 exec.active_note::get_assets # => [num_assets, ptr, pad(12)] # make sure the number of assets is 1 @@ -97,7 +97,7 @@ begin # => [ptr, pad(12)] # load the ASSET - mem_loadw + mem_loadw_be # => [ASSET, pad(12)] # add the ASSET to the account diff --git a/crates/miden-lib/build.rs b/crates/miden-lib/build.rs index 4965182bda..10254ae490 100644 --- a/crates/miden-lib/build.rs +++ b/crates/miden-lib/build.rs @@ -1,12 +1,12 @@ use std::collections::{BTreeMap, BTreeSet}; use std::env; use std::fmt::Write; -use std::fs::{self}; use std::io::{self}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; +use fs_err as fs; +use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr, miette}; use miden_assembly::utils::Serializable; use miden_assembly::{ Assembler, @@ -38,7 +38,7 @@ 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_V0_RS_FILE: &str = "src/transaction/procedures/kernel_v0.rs"; +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"; @@ -115,6 +115,7 @@ fn main() -> Result<()> { generate_error_constants(&source_dir)?; + generate_event_constants(&source_dir, &target_dir)?; Ok(()) } @@ -154,13 +155,13 @@ fn compile_tx_kernel(source_dir: &Path, target_dir: &Path) -> Result let kernel_lib = assembler .assemble_kernel_from_dir(source_dir.join("api.masm"), Some(source_dir.join("lib")))?; - // generate `kernel_v0.rs` file + // 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))?.with_debug_mode(true); + 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(); @@ -218,7 +219,7 @@ fn compile_tx_script_main( tx_script_main.write_to_file(masb_file_path).into_diagnostic() } -/// Generates `kernel_v0.rs` file based on the kernel library +/// 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 @@ -255,17 +256,17 @@ fn generate_kernel_proc_hash_file(kernel: KernelLibrary) -> Result<()> { }).collect::>().join("\n"); fs::write( - KERNEL_V0_RS_FILE, + KERNEL_PROCEDURES_RS_FILE, format!( - r#"//! This file is generated by build.rs, do not modify + r#"// This file is generated by build.rs, do not modify -use miden_objects::{{word, Word}}; +use miden_objects::{{Word, word}}; -// KERNEL V0 PROCEDURES +// KERNEL PROCEDURES // ================================================================================================ -/// Hashes of all dynamically executed procedures from the kernel 0. -pub const KERNEL0_PROCEDURES: [Word; {proc_count}] = [ +/// Hashes of all dynamically executed kernel procedures. +pub const KERNEL_PROCEDURES: [Word; {proc_count}] = [ {generated_procs} ]; "#, @@ -486,7 +487,7 @@ fn get_masm_files>(dir_path: P) -> Result> { } } } else { - println!("cargo:rerun-The specified path is not a directory."); + println!("cargo:warn=The specified path is not a directory."); } Ok(files) @@ -588,7 +589,7 @@ fn extract_masm_errors( errors: &mut BTreeMap, file_contents: &str, ) -> Result<()> { - let regex = Regex::new(r#"const\.ERR_(?.*)="(?.*)""#).unwrap(); + let regex = Regex::new(r#"const(\.|\ )ERR_(?.*)\ ?=\ ?"(?.*)""#).unwrap(); for capture in regex.captures_iter(file_contents) { let error_name = capture @@ -774,3 +775,143 @@ impl TxKernelErrorCategory { } } } + +// 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/mod.rs b/crates/miden-lib/src/account/auth/mod.rs index a389176b49..f54e947405 100644 --- a/crates/miden-lib/src/account/auth/mod.rs +++ b/crates/miden-lib/src/account/auth/mod.rs @@ -1,575 +1,11 @@ -use alloc::vec::Vec; +mod no_auth; +pub use no_auth::NoAuth; -use miden_objects::account::{AccountCode, AccountComponent, StorageMap, StorageSlot}; -use miden_objects::crypto::dsa::rpo_falcon512::PublicKey; -use miden_objects::{AccountError, Word}; +mod rpo_falcon_512; +pub use rpo_falcon_512::AuthRpoFalcon512; -use crate::account::components::{ - multisig_library, - no_auth_library, - rpo_falcon_512_acl_library, - rpo_falcon_512_library, -}; +mod rpo_falcon_512_acl; +pub use rpo_falcon_512_acl::{AuthRpoFalcon512Acl, AuthRpoFalcon512AclConfig}; -/// 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 { - public_key: PublicKey, -} - -impl AuthRpoFalcon512 { - /// Creates a new [`AuthRpoFalcon512`] component with the given `public_key`. - pub fn new(public_key: PublicKey) -> Self { - Self { public_key } - } -} - -impl From for AccountComponent { - fn from(falcon: AuthRpoFalcon512) -> Self { - AccountComponent::new( - rpo_falcon_512_library(), - vec![StorageSlot::Value(falcon.public_key.into())], - ) - .expect("falcon component should satisfy the requirements of a valid account component") - .with_supports_all_types() - } -} - -/// Configuration for [`AuthRpoFalcon512Acl`] component. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuthRpoFalcon512AclConfig { - /// 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 - /// authentication. When `true`, output notes can be created without authentication. - pub allow_unauthorized_output_notes: bool, - /// When `false`, consuming input notes (processing notes sent to this account) requires - /// authentication. When `true`, input notes can be consumed without authentication. - pub allow_unauthorized_input_notes: bool, -} - -impl AuthRpoFalcon512AclConfig { - /// Creates a new configuration with no trigger procedures and both flags set to `false` (most - /// restrictive). - pub fn new() -> Self { - Self { - auth_trigger_procedures: vec![], - allow_unauthorized_output_notes: false, - allow_unauthorized_input_notes: false, - } - } - - /// Sets the list of procedure roots that require authentication when called. - pub fn with_auth_trigger_procedures(mut self, procedures: Vec) -> Self { - self.auth_trigger_procedures = procedures; - self - } - - /// Sets whether unauthorized output notes are allowed. - pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self { - self.allow_unauthorized_output_notes = allow; - self - } - - /// Sets whether unauthorized input notes are allowed. - pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self { - self.allow_unauthorized_input_notes = allow; - self - } -} - -impl Default for AuthRpoFalcon512AclConfig { - fn default() -> Self { - Self::new() - } -} - -/// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the -/// RpoFalcon512 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 -/// procedures are called during the transaction. -/// 2. **Output note authentication**: Controls whether creating output notes requires -/// authentication. Output notes are new notes created by the account and sent to other accounts -/// (e.g., when transferring assets). When `allow_unauthorized_output_notes` is `false`, any -/// transaction that creates output notes must be authenticated, ensuring account owners control -/// when their account sends assets to other accounts. -/// 3. **Input note authentication**: Controls whether consuming input notes requires -/// authentication. Input notes are notes that were sent to this account by other accounts (e.g., -/// incoming asset transfers). When `allow_unauthorized_input_notes` is `false`, any transaction -/// that consumes input notes must be authenticated, ensuring account owners control when their -/// account processes incoming notes. -/// -/// ## Authentication Logic -/// -/// Authentication is required if ANY of the following conditions are true: -/// - Any trigger procedure from the ACL was called -/// - Output notes were created AND `allow_unauthorized_output_notes` is `false` -/// - Input notes were consumed AND `allow_unauthorized_input_notes` is `false` -/// -/// If none of these conditions are met, only the nonce is incremented without requiring a -/// signature. -/// -/// ## Use Cases -/// -/// - **Restrictive mode** (`allow_unauthorized_output_notes=false`, -/// `allow_unauthorized_input_notes=false`): All note operations require authentication, providing -/// maximum security. -/// - **Selective mode**: Allow some note operations without authentication while protecting -/// specific procedures, useful for accounts that need to process certain operations -/// automatically. -/// - **Procedure-only mode** (`allow_unauthorized_output_notes=true`, -/// `allow_unauthorized_input_notes=true`): Only specific procedures require authentication, -/// 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 -/// -/// ## Important Note on Procedure Detection -/// The procedure-based authentication relies on the `was_procedure_called` kernel function, -/// which only returns `true` if the procedure in question called into a kernel account API -/// that is restricted to the account context. Procedures that don't interact with account -/// state or kernel APIs may not be detected as "called" even if they were executed during -/// the transaction. This is an important limitation to consider when designing trigger -/// procedures for authentication. -/// -/// This component supports all account types. -pub struct AuthRpoFalcon512Acl { - public_key: PublicKey, - config: AuthRpoFalcon512AclConfig, -} - -impl AuthRpoFalcon512Acl { - /// Creates a new [`AuthRpoFalcon512Acl`] component with the given `public_key` and - /// configuration. - /// - /// # Panics - /// Panics if more than [AccountCode::MAX_NUM_PROCEDURES] procedures are specified. - pub fn new( - public_key: PublicKey, - config: AuthRpoFalcon512AclConfig, - ) -> Result { - let max_procedures = AccountCode::MAX_NUM_PROCEDURES; - if config.auth_trigger_procedures.len() > max_procedures { - return Err(AccountError::other(format!( - "Cannot track more than {max_procedures} procedures (account limit)" - ))); - } - - Ok(Self { public_key, config }) - } -} - -impl From for AccountComponent { - fn from(falcon: AuthRpoFalcon512Acl) -> Self { - let mut storage_slots = Vec::with_capacity(3); - - // Slot 0: Public key - storage_slots.push(StorageSlot::Value(falcon.public_key.into())); - - // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes, - // allow_unauthorized_input_notes, 0] - 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 - // We add the map even if there are no trigger procedures, to always maintain the same - // storage layout. - let map_entries = falcon - .config - .auth_trigger_procedures - .iter() - .enumerate() - .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())); - - AccountComponent::new(rpo_falcon_512_acl_library(), storage_slots) - .expect( - "ACL auth component should satisfy the requirements of a valid account component", - ) - .with_supports_all_types() - } -} - -/// An [`AccountComponent`] implementing a no-authentication scheme. -/// -/// This component provides **no authentication**! It only checks if the account -/// state has actually changed during transaction execution by comparing the initial -/// account commitment with the current commitment and increments the nonce if -/// they differ. This avoids unnecessary nonce increments for transactions that don't -/// modify the account state. -/// -/// It exports the procedure `auth__no_auth`, which: -/// - Checks if the account state has changed by comparing initial and final commitments -/// - Only increments the nonce if the account state has actually changed -/// - Provides no cryptographic authentication -/// -/// This component supports all account types. -pub struct NoAuth; - -impl NoAuth { - /// Creates a new [`NoAuth`] component. - pub fn new() -> Self { - Self - } -} - -impl Default for NoAuth { - fn default() -> Self { - Self::new() - } -} - -impl From for AccountComponent { - fn from(_: NoAuth) -> Self { - AccountComponent::new(no_auth_library(), vec![]) - .expect("NoAuth component should satisfy the requirements of a valid account component") - .with_supports_all_types() - } -} - -// MULTISIG AUTHENTICATION COMPONENT -// ================================================================================================ - -/// An [`AccountComponent`] implementing a multisig based on RpoFalcon512 signatures. -/// -/// This component requires a threshold number of signatures from a set of approvers. -/// -/// 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 -/// -/// This component supports all account types. -#[derive(Debug)] -pub struct AuthRpoFalcon512Multisig { - threshold: u32, - approvers: Vec, -} - -impl AuthRpoFalcon512Multisig { - /// Creates a new [`AuthRpoFalcon512Multisig`] component with the given `threshold` and - /// list of approver public keys. - /// - /// # Errors - /// Returns an error if threshold is 0 or greater than the number of approvers. - pub fn new(threshold: u32, approvers: Vec) -> Result { - if threshold == 0 { - return Err(AccountError::other("threshold must be at least 1")); - } - - if threshold > approvers.len() as u32 { - return Err(AccountError::other( - "threshold cannot be greater than number of approvers", - )); - } - - Ok(Self { threshold, approvers }) - } -} - -impl From for AccountComponent { - fn from(multisig: AuthRpoFalcon512Multisig) -> Self { - let mut storage_slots = Vec::with_capacity(3); - - // Slot 0: [threshold, num_approvers, 0, 0] - let num_approvers = multisig.approvers.len() as u32; - storage_slots.push(StorageSlot::Value(Word::from([ - multisig.threshold, - num_approvers, - 0, - 0, - ]))); - - // Slot 1: A map with approver public keys - let map_entries = multisig - .approvers - .iter() - .enumerate() - .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())); - - // Slot 2: A map which stores executed transactions - let executed_transactions = StorageMap::default(); - storage_slots.push(StorageSlot::Map(executed_transactions)); - - AccountComponent::new(multisig_library(), storage_slots) - .expect("Multisig auth component should satisfy the requirements of a valid account component") - .with_supports_all_types() - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use alloc::string::ToString; - - use miden_objects::Word; - use miden_objects::account::AccountBuilder; - - use super::*; - use crate::account::components::WellKnownComponent; - use crate::account::wallets::BasicWallet; - - /// Test configuration for parametrized ACL tests - struct AclTestConfig { - /// Whether to include auth trigger procedures - with_procedures: bool, - /// Allow unauthorized output notes flag - 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, - } - - /// Helper function to get the basic wallet procedures for testing - fn get_basic_wallet_procedures() -> Vec { - // Get the two trigger procedures from BasicWallet: `receive_asset`, `move_asset_to_note`. - let procedures: Vec = WellKnownComponent::BasicWallet.procedure_digests().collect(); - - assert_eq!(procedures.len(), 2); - procedures - } - - /// Parametrized test helper for ACL component testing - fn test_acl_component(config: AclTestConfig) { - let public_key = PublicKey::new(Word::empty()); - - // Build the configuration - let mut acl_config = AuthRpoFalcon512AclConfig::new() - .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes) - .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes); - - let auth_trigger_procedures = if config.with_procedures { - let procedures = get_basic_wallet_procedures(); - acl_config = acl_config.with_auth_trigger_procedures(procedures.clone()); - procedures - } else { - vec![] - }; - - // Create component and account - let component = - AuthRpoFalcon512Acl::new(public_key, acl_config).expect("component creation failed"); - - let (account, _) = AccountBuilder::new([0; 32]) - .with_auth_component(component) - .with_component(BasicWallet) - .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"); - assert_eq!(public_key_slot, Word::from(public_key)); - - // 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); - - // Assert procedure roots in map (slot 2) - 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])) - .expect("storage map access failed"); - assert_eq!(proc_root, *expected_proc_root); - } - } else { - // 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()) - .expect("storage map access failed"); - assert_eq!(proc_root, Word::empty()); - } - } - - /// Test ACL component with no procedures and both authorization flags set to false - #[test] - fn test_rpo_falcon_512_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] - }); - } - - /// Test ACL component with two procedures and both authorization flags set to false - #[test] - fn test_rpo_falcon_512_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]), - }); - } - - /// 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() { - 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]), - }); - } - - /// 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() { - 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]), - }); - } - - /// 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() { - 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]), - }); - } - - /// Test ACL component with two procedures and both authorization flags set to true - #[test] - fn test_rpo_falcon_512_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]), - }); - } - - #[test] - fn test_no_auth_component() { - // Create an account using the NoAuth component - let (_account, _) = AccountBuilder::new([0; 32]) - .with_auth_component(NoAuth) - .with_component(BasicWallet) - .build() - .expect("account building failed"); - } - - // MULTISIG TESTS - // ============================================================================================ - - /// Test multisig component setup with various configurations - #[test] - fn test_multisig_component_setup() { - // Create test public keys - let pub_key_1 = PublicKey::new(Word::from([1u32, 0, 0, 0])); - let pub_key_2 = PublicKey::new(Word::from([2u32, 0, 0, 0])); - let pub_key_3 = PublicKey::new(Word::from([3u32, 0, 0, 0])); - let approvers = vec![pub_key_1, pub_key_2, pub_key_3]; - let threshold = 2u32; - - // Create multisig component - let multisig_component = AuthRpoFalcon512Multisig::new(threshold, approvers.clone()) - .expect("multisig component creation failed"); - - // Build account with multisig component - let (account, _) = AccountBuilder::new([0; 32]) - .with_auth_component(multisig_component) - .with_component(BasicWallet) - .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 slot 1: Approver public keys in map - 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"); - assert_eq!(stored_pub_key, Word::from(*expected_pub_key)); - } - } - - /// Test multisig component with minimum threshold (1 of 1) - #[test] - fn test_multisig_component_minimum_threshold() { - let pub_key = PublicKey::new(Word::from([42u32, 0, 0, 0])); - let approvers = vec![pub_key]; - let threshold = 1u32; - - let multisig_component = AuthRpoFalcon512Multisig::new(threshold, approvers.clone()) - .expect("multisig component creation failed"); - - let (account, _) = AccountBuilder::new([0; 32]) - .with_auth_component(multisig_component) - .with_component(BasicWallet) - .build() - .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 stored_pub_key = account - .storage() - .get_map_item(1, Word::from([0u32, 0, 0, 0])) - .expect("storage map access failed"); - assert_eq!(stored_pub_key, Word::from(pub_key)); - } - - /// Test multisig component error cases - #[test] - fn test_multisig_component_error_cases() { - let pub_key = PublicKey::new(Word::from([1u32, 0, 0, 0])); - let approvers = vec![pub_key]; - - // Test threshold = 0 (should fail) - let result = AuthRpoFalcon512Multisig::new(0, approvers.clone()); - assert!(result.unwrap_err().to_string().contains("threshold must be at least 1")); - - // Test threshold > number of approvers (should fail) - let result = AuthRpoFalcon512Multisig::new(2, approvers); - assert!( - result - .unwrap_err() - .to_string() - .contains("threshold cannot be greater than number of approvers") - ); - } -} +mod rpo_falcon_512_multisig; +pub use rpo_falcon_512_multisig::{AuthRpoFalcon512Multisig, AuthRpoFalcon512MultisigConfig}; diff --git a/crates/miden-lib/src/account/auth/no_auth.rs b/crates/miden-lib/src/account/auth/no_auth.rs new file mode 100644 index 0000000000..21d9d2fb7e --- /dev/null +++ b/crates/miden-lib/src/account/auth/no_auth.rs @@ -0,0 +1,58 @@ +use miden_objects::account::AccountComponent; + +use crate::account::components::no_auth_library; + +/// An [`AccountComponent`] implementing a no-authentication scheme. +/// +/// This component provides **no authentication**! It only checks if the account +/// state has actually changed during transaction execution by comparing the initial +/// account commitment with the current commitment and increments the nonce if +/// they differ. This avoids unnecessary nonce increments for transactions that don't +/// modify the account state. +/// +/// It exports the procedure `auth_no_auth`, which: +/// - Checks if the account state has changed by comparing initial and final commitments +/// - Only increments the nonce if the account state has actually changed +/// - Provides no cryptographic authentication +/// +/// This component supports all account types. +pub struct NoAuth; + +impl NoAuth { + /// Creates a new [`NoAuth`] component. + pub fn new() -> Self { + Self + } +} + +impl Default for NoAuth { + fn default() -> Self { + Self::new() + } +} + +impl From for AccountComponent { + fn from(_: NoAuth) -> Self { + AccountComponent::new(no_auth_library(), vec![]) + .expect("NoAuth component should satisfy the requirements of a valid account component") + .with_supports_all_types() + } +} + +#[cfg(test)] +mod tests { + use miden_objects::account::AccountBuilder; + + use super::*; + use crate::account::wallets::BasicWallet; + + #[test] + fn test_no_auth_component() { + // Create an account using the NoAuth component + let _account = AccountBuilder::new([0; 32]) + .with_auth_component(NoAuth) + .with_component(BasicWallet) + .build() + .expect("account building failed"); + } +} diff --git a/crates/miden-lib/src/account/auth/rpo_falcon_512.rs b/crates/miden-lib/src/account/auth/rpo_falcon_512.rs new file mode 100644 index 0000000000..a8e3e2ada8 --- /dev/null +++ b/crates/miden-lib/src/account/auth/rpo_falcon_512.rs @@ -0,0 +1,39 @@ +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/account/auth/rpo_falcon_512_acl.rs b/crates/miden-lib/src/account/auth/rpo_falcon_512_acl.rs new file mode 100644 index 0000000000..f9982d5456 --- /dev/null +++ b/crates/miden-lib/src/account/auth/rpo_falcon_512_acl.rs @@ -0,0 +1,325 @@ +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. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthRpoFalcon512AclConfig { + /// 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 + /// authentication. When `true`, output notes can be created without authentication. + pub allow_unauthorized_output_notes: bool, + /// When `false`, consuming input notes (processing notes sent to this account) requires + /// authentication. When `true`, input notes can be consumed without authentication. + pub allow_unauthorized_input_notes: bool, +} + +impl AuthRpoFalcon512AclConfig { + /// Creates a new configuration with no trigger procedures and both flags set to `false` (most + /// restrictive). + pub fn new() -> Self { + Self { + auth_trigger_procedures: vec![], + allow_unauthorized_output_notes: false, + allow_unauthorized_input_notes: false, + } + } + + /// Sets the list of procedure roots that require authentication when called. + pub fn with_auth_trigger_procedures(mut self, procedures: Vec) -> Self { + self.auth_trigger_procedures = procedures; + self + } + + /// Sets whether unauthorized output notes are allowed. + pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self { + self.allow_unauthorized_output_notes = allow; + self + } + + /// Sets whether unauthorized input notes are allowed. + pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self { + self.allow_unauthorized_input_notes = allow; + self + } +} + +impl Default for AuthRpoFalcon512AclConfig { + fn default() -> Self { + Self::new() + } +} + +/// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the +/// RpoFalcon512 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 +/// procedures are called during the transaction. +/// 2. **Output note authentication**: Controls whether creating output notes requires +/// authentication. Output notes are new notes created by the account and sent to other accounts +/// (e.g., when transferring assets). When `allow_unauthorized_output_notes` is `false`, any +/// transaction that creates output notes must be authenticated, ensuring account owners control +/// when their account sends assets to other accounts. +/// 3. **Input note authentication**: Controls whether consuming input notes requires +/// authentication. Input notes are notes that were sent to this account by other accounts (e.g., +/// incoming asset transfers). When `allow_unauthorized_input_notes` is `false`, any transaction +/// that consumes input notes must be authenticated, ensuring account owners control when their +/// account processes incoming notes. +/// +/// ## Authentication Logic +/// +/// Authentication is required if ANY of the following conditions are true: +/// - Any trigger procedure from the ACL was called +/// - Output notes were created AND `allow_unauthorized_output_notes` is `false` +/// - Input notes were consumed AND `allow_unauthorized_input_notes` is `false` +/// +/// If none of these conditions are met, only the nonce is incremented without requiring a +/// signature. +/// +/// ## Use Cases +/// +/// - **Restrictive mode** (`allow_unauthorized_output_notes=false`, +/// `allow_unauthorized_input_notes=false`): All note operations require authentication, providing +/// maximum security. +/// - **Selective mode**: Allow some note operations without authentication while protecting +/// specific procedures, useful for accounts that need to process certain operations +/// automatically. +/// - **Procedure-only mode** (`allow_unauthorized_output_notes=true`, +/// `allow_unauthorized_input_notes=true`): Only specific procedures require authentication, +/// 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 +/// +/// ## Important Note on Procedure Detection +/// The procedure-based authentication relies on the `was_procedure_called` kernel function, +/// which only returns `true` if the procedure in question called into a kernel account API +/// that is restricted to the account context. Procedures that don't interact with account +/// state or kernel APIs may not be detected as "called" even if they were executed during +/// the transaction. This is an important limitation to consider when designing trigger +/// procedures for authentication. +/// +/// This component supports all account types. +pub struct AuthRpoFalcon512Acl { + pub_key: PublicKeyCommitment, + config: AuthRpoFalcon512AclConfig, +} + +impl AuthRpoFalcon512Acl { + /// Creates a new [`AuthRpoFalcon512Acl`] 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, + ) -> Result { + let max_procedures = AccountCode::MAX_NUM_PROCEDURES; + if config.auth_trigger_procedures.len() > max_procedures { + return Err(AccountError::other(format!( + "Cannot track more than {max_procedures} procedures (account limit)" + ))); + } + + Ok(Self { pub_key, config }) + } +} + +impl From for AccountComponent { + fn from(falcon: AuthRpoFalcon512Acl) -> Self { + let mut storage_slots = Vec::with_capacity(3); + + // Slot 0: Public key + storage_slots.push(StorageSlot::Value(falcon.pub_key.into())); + + // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes, + // allow_unauthorized_input_notes, 0] + 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 + // We add the map even if there are no trigger procedures, to always maintain the same + // storage layout. + let map_entries = falcon + .config + .auth_trigger_procedures + .iter() + .enumerate() + .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())); + + AccountComponent::new(rpo_falcon_512_acl_library(), storage_slots) + .expect( + "ACL auth component should satisfy the requirements of a valid account component", + ) + .with_supports_all_types() + } +} + +#[cfg(test)] +mod tests { + use miden_objects::Word; + use miden_objects::account::AccountBuilder; + + use super::*; + use crate::account::components::WellKnownComponent; + use crate::account::wallets::BasicWallet; + + /// Test configuration for parametrized ACL tests + struct AclTestConfig { + /// Whether to include auth trigger procedures + with_procedures: bool, + /// Allow unauthorized output notes flag + 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, + } + + /// Helper function to get the basic wallet procedures for testing + fn get_basic_wallet_procedures() -> Vec { + // Get the two trigger procedures from BasicWallet: `receive_asset`, `move_asset_to_note`. + let procedures: Vec = WellKnownComponent::BasicWallet.procedure_digests().collect(); + + assert_eq!(procedures.len(), 2); + procedures + } + + /// Parametrized test helper for ACL component testing + fn test_acl_component(config: AclTestConfig) { + let public_key = PublicKeyCommitment::from(Word::empty()); + + // Build the configuration + let mut acl_config = AuthRpoFalcon512AclConfig::new() + .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes) + .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes); + + let auth_trigger_procedures = if config.with_procedures { + let procedures = get_basic_wallet_procedures(); + acl_config = acl_config.with_auth_trigger_procedures(procedures.clone()); + procedures + } else { + vec![] + }; + + // Create component and account + let component = + AuthRpoFalcon512Acl::new(public_key, acl_config).expect("component creation failed"); + + let account = AccountBuilder::new([0; 32]) + .with_auth_component(component) + .with_component(BasicWallet) + .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"); + 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); + + // Assert procedure roots in map (slot 2) + 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])) + .expect("storage map access failed"); + assert_eq!(proc_root, *expected_proc_root); + } + } else { + // 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()) + .expect("storage map access failed"); + assert_eq!(proc_root, Word::empty()); + } + } + + /// Test ACL component with no procedures and both authorization flags set to false + #[test] + fn test_rpo_falcon_512_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] + }); + } + + /// Test ACL component with two procedures and both authorization flags set to false + #[test] + fn test_rpo_falcon_512_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]), + }); + } + + /// 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() { + 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]), + }); + } + + /// 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() { + 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]), + }); + } + + /// 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() { + 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]), + }); + } + + /// Test ACL component with two procedures and both authorization flags set to true + #[test] + fn test_rpo_falcon_512_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]), + }); + } +} diff --git a/crates/miden-lib/src/account/auth/rpo_falcon_512_multisig.rs b/crates/miden-lib/src/account/auth/rpo_falcon_512_multisig.rs new file mode 100644 index 0000000000..f364abc298 --- /dev/null +++ b/crates/miden-lib/src/account/auth/rpo_falcon_512_multisig.rs @@ -0,0 +1,245 @@ +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; + +// MULTISIG AUTHENTICATION COMPONENT +// ================================================================================================ + +/// Configuration for [`AuthRpoFalcon512Multisig`] component. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthRpoFalcon512MultisigConfig { + approvers: Vec, + default_threshold: u32, + proc_thresholds: Vec<(Word, u32)>, +} + +impl AuthRpoFalcon512MultisigConfig { + /// 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. + pub fn new( + approvers: Vec, + default_threshold: u32, + ) -> Result { + if default_threshold == 0 { + return Err(AccountError::other("threshold must be at least 1")); + } + if default_threshold > approvers.len() as u32 { + return Err(AccountError::other( + "threshold cannot be greater than number of approvers", + )); + } + + Ok(Self { + approvers, + default_threshold, + proc_thresholds: vec![], + }) + } + + /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and + /// at most the number of approvers. + pub fn with_proc_thresholds( + mut self, + proc_thresholds: Vec<(Word, u32)>, + ) -> Result { + for (_, threshold) in &proc_thresholds { + if *threshold == 0 { + return Err(AccountError::other("procedure threshold must be at least 1")); + } + if *threshold > self.approvers.len() as u32 { + return Err(AccountError::other( + "procedure threshold cannot be greater than number of approvers", + )); + } + } + self.proc_thresholds = proc_thresholds; + Ok(self) + } + + pub fn approvers(&self) -> &[PublicKeyCommitment] { + &self.approvers + } + + pub fn default_threshold(&self) -> u32 { + self.default_threshold + } + + pub fn proc_thresholds(&self) -> &[(Word, u32)] { + &self.proc_thresholds + } +} + +/// An [`AccountComponent`] implementing a multisig based on RpoFalcon512 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) +/// +/// This component supports all account types. +#[derive(Debug)] +pub struct AuthRpoFalcon512Multisig { + config: AuthRpoFalcon512MultisigConfig, +} + +impl AuthRpoFalcon512Multisig { + /// Creates a new [`AuthRpoFalcon512Multisig`] component from the provided configuration. + pub fn new(config: AuthRpoFalcon512MultisigConfig) -> Result { + Ok(Self { config }) + } +} + +impl From for AccountComponent { + fn from(multisig: AuthRpoFalcon512Multisig) -> Self { + let mut storage_slots = Vec::with_capacity(3); + + // Slot 0: [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 + let map_entries = multisig + .config + .approvers() + .iter() + .enumerate() + .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())); + + // Slot 2: A map which stores executed transactions + let executed_transactions = StorageMap::default(); + storage_slots.push(StorageSlot::Map(executed_transactions)); + + // Slot 3: A map which stores procedure thresholds (PROC_ROOT -> threshold) + let proc_threshold_roots = StorageMap::with_entries( + multisig + .config + .proc_thresholds() + .iter() + .map(|(proc_root, threshold)| (*proc_root, Word::from([*threshold, 0, 0, 0]))), + ) + .unwrap(); + storage_slots.push(StorageSlot::Map(proc_threshold_roots)); + + AccountComponent::new(rpo_falcon_512_multisig_library(), storage_slots) + .expect("Multisig auth component should satisfy the requirements of a valid account component") + .with_supports_all_types() + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use miden_objects::Word; + use miden_objects::account::AccountBuilder; + + use super::*; + use crate::account::wallets::BasicWallet; + + /// Test multisig component setup with various configurations + #[test] + fn test_multisig_component_setup() { + // Create test public keys + let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0])); + let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0])); + let pub_key_3 = PublicKeyCommitment::from(Word::from([3u32, 0, 0, 0])); + let approvers = vec![pub_key_1, pub_key_2, pub_key_3]; + let threshold = 2u32; + + // Create multisig component + let multisig_component = AuthRpoFalcon512Multisig::new( + AuthRpoFalcon512MultisigConfig::new(approvers.clone(), threshold) + .expect("invalid multisig config"), + ) + .expect("multisig component creation failed"); + + // Build account with multisig component + let account = AccountBuilder::new([0; 32]) + .with_auth_component(multisig_component) + .with_component(BasicWallet) + .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 slot 1: Approver public keys in map + 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"); + assert_eq!(stored_pub_key, Word::from(*expected_pub_key)); + } + } + + /// Test multisig component with minimum threshold (1 of 1) + #[test] + fn test_multisig_component_minimum_threshold() { + let pub_key = PublicKeyCommitment::from(Word::from([42u32, 0, 0, 0])); + let approvers = vec![pub_key]; + let threshold = 1u32; + + let multisig_component = AuthRpoFalcon512Multisig::new( + AuthRpoFalcon512MultisigConfig::new(approvers.clone(), threshold) + .expect("invalid multisig config"), + ) + .expect("multisig component creation failed"); + + let account = AccountBuilder::new([0; 32]) + .with_auth_component(multisig_component) + .with_component(BasicWallet) + .build() + .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 stored_pub_key = account + .storage() + .get_map_item(1, Word::from([0u32, 0, 0, 0])) + .expect("storage map access failed"); + assert_eq!(stored_pub_key, Word::from(pub_key)); + } + + /// Test multisig component error cases + #[test] + fn test_multisig_component_error_cases() { + let pub_key = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0])); + let approvers = vec![pub_key]; + + // Test threshold = 0 (should fail) + let result = AuthRpoFalcon512MultisigConfig::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); + assert!( + result + .unwrap_err() + .to_string() + .contains("threshold cannot be greater than number of approvers") + ); + } +} diff --git a/crates/miden-lib/src/account/components/mod.rs b/crates/miden-lib/src/account/components/mod.rs index de2b7700ec..96f5b6c149 100644 --- a/crates/miden-lib/src/account/components/mod.rs +++ b/crates/miden-lib/src/account/components/mod.rs @@ -6,6 +6,7 @@ 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 crate::account::interface::AccountComponentInterface; @@ -32,6 +33,15 @@ static BASIC_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { 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/network_fungible_faucet.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Network Fungible Faucet library is well-formed") +}); + // Initialize the Rpo Falcon 512 ACL library only once. static RPO_FALCON_512_ACL_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( @@ -66,6 +76,11 @@ pub fn basic_fungible_faucet_library() -> Library { BASIC_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Network Fungible Faucet Library. +pub fn network_fungible_faucet_library() -> Library { + NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() +} + /// Returns the Rpo Falcon 512 Library. pub fn rpo_falcon_512_library() -> Library { RPO_FALCON_512_LIBRARY.clone() @@ -81,8 +96,8 @@ pub fn no_auth_library() -> Library { NO_AUTH_LIBRARY.clone() } -/// Returns the Multisig Library. -pub fn multisig_library() -> Library { +/// Returns the RPO Falcon 512 Multisig Library. +pub fn rpo_falcon_512_multisig_library() -> Library { RPO_FALCON_512_MULTISIG_LIBRARY.clone() } @@ -93,6 +108,7 @@ pub fn multisig_library() -> Library { pub enum WellKnownComponent { BasicWallet, BasicFungibleFaucet, + NetworkFungibleFaucet, AuthRpoFalcon512, AuthRpoFalcon512Acl, AuthRpoFalcon512Multisig, @@ -105,6 +121,7 @@ impl WellKnownComponent { let library = match self { Self::BasicWallet => BASIC_WALLET_LIBRARY.as_ref(), Self::BasicFungibleFaucet => BASIC_FUNGIBLE_FAUCET_LIBRARY.as_ref(), + Self::NetworkFungibleFaucet => NETWORK_FUNGIBLE_FAUCET_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(), @@ -148,6 +165,8 @@ impl WellKnownComponent { }, Self::BasicFungibleFaucet => component_interface_vec .push(AccountComponentInterface::BasicFungibleFaucet(storage_offset)), + Self::NetworkFungibleFaucet => component_interface_vec + .push(AccountComponentInterface::NetworkFungibleFaucet(storage_offset)), Self::AuthRpoFalcon512 => component_interface_vec .push(AccountComponentInterface::AuthRpoFalcon512(storage_offset)), Self::AuthRpoFalcon512Acl => component_interface_vec @@ -169,6 +188,7 @@ impl WellKnownComponent { ) { 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::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); diff --git a/crates/miden-lib/src/account/faucets/basic_fungible.rs b/crates/miden-lib/src/account/faucets/basic_fungible.rs new file mode 100644 index 0000000000..dd1dadb397 --- /dev/null +++ b/crates/miden-lib/src/account/faucets/basic_fungible.rs @@ -0,0 +1,416 @@ +use miden_objects::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountStorage, + AccountStorageMode, + AccountType, + StorageSlot, +}; +use miden_objects::asset::{FungibleAsset, TokenSymbol}; +use miden_objects::{Felt, FieldElement, Word}; + +use super::FungibleFaucetError; +use crate::account::AuthScheme; +use crate::account::auth::{AuthRpoFalcon512Acl, AuthRpoFalcon512AclConfig}; +use crate::account::components::basic_fungible_faucet_library; +use crate::account::interface::{AccountComponentInterface, AccountInterface}; +use crate::procedure_digest; + +// BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT +// ================================================================================================ + +// Initialize the digest of the `distribute` procedure of the Basic Fungible Faucet only once. +procedure_digest!( + BASIC_FUNGIBLE_FAUCET_DISTRIBUTE, + BasicFungibleFaucet::DISTRIBUTE_PROC_NAME, + basic_fungible_faucet_library +); + +// Initialize the digest of the `burn` procedure of the Basic Fungible Faucet only once. +procedure_digest!( + BASIC_FUNGIBLE_FAUCET_BURN, + BasicFungibleFaucet::BURN_PROC_NAME, + basic_fungible_faucet_library +); + +/// 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: +/// - `distribute`, which mints an assets and create a note for the provided recipient. +/// - `burn`, which burns the provided asset. +/// +/// The `distribute` procedure can be called from a transaction script and requires authentication +/// via the authentication component. The `burn` procedure can only be called from a note script +/// and requires the calling note to contain the asset to be burned. +/// This component must be combined with an authentication component. +/// +/// This component supports accounts of type [`AccountType::FungibleFaucet`]. +/// +/// [kasm]: crate::transaction::TransactionKernel::assembler +pub struct BasicFungibleFaucet { + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, +} + +impl BasicFungibleFaucet { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// 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"; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata. + /// + /// # Errors: + /// 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 + /// ([`FungibleAsset::MAX_AMOUNT`]) + pub fn new( + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + ) -> Result { + // First check that the metadata is valid. + if decimals > Self::MAX_DECIMALS { + return Err(FungibleFaucetError::TooManyDecimals { + actual: decimals as u64, + max: Self::MAX_DECIMALS, + }); + } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT { + return Err(FungibleFaucetError::MaxSupplyTooLarge { + actual: max_supply.as_int(), + max: FungibleAsset::MAX_AMOUNT, + }); + } + + Ok(Self { symbol, decimals, max_supply }) + } + + /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account + /// interface and storage. + /// + /// # Errors: + /// Returns an error if: + /// - the provided [`AccountInterface`] does not contain a + /// [`AccountComponentInterface::BasicFungibleFaucet`] component. + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. + /// - the max supply value exceeds maximum possible amount for a fungible asset of + /// [`FungibleAsset::MAX_AMOUNT`]. + /// - the token symbol encoded value exceeds the maximum value of + /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + fn try_from_interface( + interface: AccountInterface, + 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 + let faucet_metadata = storage + .get_item(*offset) + .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?; + let [max_supply, decimals, token_symbol, _] = *faucet_metadata; + + // verify metadata values + let token_symbol = TokenSymbol::try_from(token_symbol) + .map_err(FungibleFaucetError::InvalidTokenSymbol)?; + let decimals = decimals.as_int().try_into().map_err(|_| { + FungibleFaucetError::TooManyDecimals { + actual: decimals.as_int(), + max: Self::MAX_DECIMALS, + } + })?; + + return BasicFungibleFaucet::new(token_symbol, decimals, max_supply); + } + } + + Err(FungibleFaucetError::NoAvailableInterface) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the symbol of the faucet. + pub fn symbol(&self) -> TokenSymbol { + self.symbol + } + + /// Returns the decimals of the faucet. + pub fn decimals(&self) -> u8 { + self.decimals + } + + /// Returns the max supply of the faucet. + pub fn max_supply(&self) -> Felt { + self.max_supply + } + + /// Returns the digest of the `distribute` account procedure. + pub fn distribute_digest() -> Word { + *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE + } + + /// Returns the digest of the `burn` account procedure. + pub fn burn_digest() -> Word { + *BASIC_FUNGIBLE_FAUCET_BURN + } +} + +impl From for AccountComponent { + fn from(faucet: BasicFungibleFaucet) -> Self { + // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as + // [a3, a2, a1, a0, ...] + let metadata = Word::new([ + faucet.max_supply, + Felt::from(faucet.decimals), + faucet.symbol.into(), + Felt::ZERO, + ]); + + AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)]) + .expect("basic fungible faucet component should satisfy the requirements of a valid account component") + .with_supported_type(AccountType::FungibleFaucet) + } +} + +impl TryFrom for BasicFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: Account) -> Result { + let account_interface = AccountInterface::from(&account); + + BasicFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +impl TryFrom<&Account> for BasicFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: &Account) -> Result { + let account_interface = AccountInterface::from(account); + + BasicFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +/// Creates a new faucet account with basic fungible faucet interface, +/// account storage type, specified authentication scheme, and provided meta data (token symbol, +/// decimals, max supply). +/// +/// The basic faucet interface exposes two procedures: +/// - `distribute`, which mints an assets and create a note for the provided recipient. +/// - `burn`, which burns the provided asset. +/// +/// The `distribute` procedure can be called from a transaction script and requires authentication +/// via the specified authentication scheme. The `burn` procedure can only be called from a note +/// script and requires the calling note to contain the asset to be burned. +/// +/// 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, +/// 0]. +/// - Slot 3: A map with tracked procedure roots. +/// - Slot 4: Token metadata of the faucet. +pub fn create_basic_fungible_faucet( + init_seed: [u8; 32], + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + account_storage_mode: AccountStorageMode, + auth_scheme: AuthScheme, +) -> Result { + let distribute_proc_root = BasicFungibleFaucet::distribute_digest(); + + let auth_component: AccountComponent = match auth_scheme { + AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new( + pub_key, + AuthRpoFalcon512AclConfig::new() + .with_auth_trigger_procedures(vec![distribute_proc_root]) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into(), + AuthScheme::NoAuth => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "basic fungible faucets cannot be created with NoAuth authentication scheme".into(), + )); + }, + AuthScheme::RpoFalcon512Multisig { threshold: _, pub_keys: _ } => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "basic fungible faucets do not support multisig authentication".into(), + )); + }, + AuthScheme::Unknown => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "basic fungible faucets cannot be created with Unknown authentication scheme" + .into(), + )); + }, + }; + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(account_storage_mode) + .with_auth_component(auth_component) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?) + .build() + .map_err(FungibleFaucetError::AccountError)?; + + Ok(account) +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use miden_objects::account::auth::PublicKeyCommitment; + use miden_objects::{FieldElement, ONE, Word}; + + use super::{ + AccountBuilder, + AccountStorageMode, + AccountType, + AuthScheme, + BasicFungibleFaucet, + Felt, + FungibleFaucetError, + TokenSymbol, + create_basic_fungible_faucet, + }; + use crate::account::auth::AuthRpoFalcon512; + 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() }; + + // we need to use an initial seed to create the wallet account + let init_seed: [u8; 32] = [ + 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, + 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, + ]; + + let max_supply = Felt::new(123); + let token_symbol_string = "POL"; + let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap(); + let decimals = 2u8; + let storage_mode = AccountStorageMode::Private; + + let faucet_account = create_basic_fungible_faucet( + init_seed, + token_symbol, + decimals, + max_supply, + storage_mode, + auth_scheme, + ) + .unwrap(); + + // The reserved faucet slot should be initialized to an empty word. + assert_eq!(faucet_account.storage().get_item(0).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); + + // 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]. + assert_eq!( + faucet_account.storage().get_item(2).unwrap(), + [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into() + ); + + // The procedure root map in slot 3 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()) + .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. + assert_eq!( + faucet_account.storage().get_item(4).unwrap(), + [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into() + ); + + assert!(faucet_account.is_faucet()); + + assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); + + // Verify the faucet can be extracted and has correct metadata + let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap(); + assert_eq!(faucet_component.symbol(), token_symbol); + assert_eq!(faucet_component.decimals(), decimals); + assert_eq!(faucet_component.max_supply(), max_supply); + } + + #[test] + fn faucet_create_from_account() { + // prepare the test data + let mock_word = Word::from([0, 1, 2, 3u32]); + let mock_public_key = PublicKeyCommitment::from(mock_word); + let mock_seed = mock_word.as_bytes(); + + // valid account + let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol"); + let faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_component( + BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100)) + .expect("failed to create a fungible faucet component"), + ) + .with_auth_component(AuthRpoFalcon512::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)); + + // 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)) + // we need to add some other component so the builder doesn't fail + .with_component(BasicWallet) + .build_existing() + .expect("failed to create wallet account"); + + let err = BasicFungibleFaucet::try_from(invalid_faucet_account) + .err() + .expect("basic fungible faucet creation should fail"); + assert_matches!(err, FungibleFaucetError::NoAvailableInterface); + } + + /// Check that the obtaining of the basic fungible faucet procedure digests does not panic. + #[test] + fn get_faucet_procedures() { + let _distribute_digest = BasicFungibleFaucet::distribute_digest(); + let _burn_digest = BasicFungibleFaucet::burn_digest(); + } +} diff --git a/crates/miden-lib/src/account/faucets/mod.rs b/crates/miden-lib/src/account/faucets/mod.rs index ede914c554..f09f361044 100644 --- a/crates/miden-lib/src/account/faucets/mod.rs +++ b/crates/miden-lib/src/account/faucets/mod.rs @@ -1,227 +1,16 @@ use alloc::string::String; -use miden_objects::account::{ - Account, - AccountBuilder, - AccountComponent, - AccountStorage, - AccountStorageMode, - AccountType, - StorageSlot, -}; -use miden_objects::assembly::{ProcedureName, QualifiedProcedureName}; -use miden_objects::asset::{FungibleAsset, TokenSymbol}; -use miden_objects::utils::sync::LazyLock; -use miden_objects::{AccountError, Felt, FieldElement, TokenSymbolError, Word}; +use miden_objects::account::{Account, AccountType}; +use miden_objects::{AccountError, Felt, TokenSymbolError}; use thiserror::Error; -use super::AuthScheme; -use super::interface::{AccountComponentInterface, AccountInterface}; -use crate::account::auth::{ - AuthRpoFalcon512Acl, - AuthRpoFalcon512AclConfig, - AuthRpoFalcon512Multisig, -}; -use crate::account::components::basic_fungible_faucet_library; use crate::transaction::memory::FAUCET_STORAGE_DATA_SLOT; -// BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT -// ================================================================================================ - -// Initialize the digest of the `distribute` procedure of the Basic Fungible Faucet only once. -static BASIC_FUNGIBLE_FAUCET_DISTRIBUTE: LazyLock = LazyLock::new(|| { - let distribute_proc_name = QualifiedProcedureName::new( - Default::default(), - ProcedureName::new(BasicFungibleFaucet::DISTRIBUTE_PROC_NAME) - .expect("failed to create name for 'distribute' procedure"), - ); - basic_fungible_faucet_library() - .get_procedure_root_by_name(distribute_proc_name) - .expect("Basic Fungible Faucet should contain 'distribute' procedure") -}); - -// Initialize the digest of the `burn` procedure of the Basic Fungible Faucet only once. -static BASIC_FUNGIBLE_FAUCET_BURN: LazyLock = LazyLock::new(|| { - let burn_proc_name = QualifiedProcedureName::new( - Default::default(), - ProcedureName::new(BasicFungibleFaucet::BURN_PROC_NAME) - .expect("failed to create name for 'burn' procedure"), - ); - basic_fungible_faucet_library() - .get_procedure_root_by_name(burn_proc_name) - .expect("Basic Fungible Faucet should contain 'burn' procedure") -}); - -/// 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: -/// - `distribute`, which mints an assets and create a note for the provided recipient. -/// - `burn`, which burns the provided asset. -/// -/// `distribute` requires 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`]. -/// -/// [kasm]: crate::transaction::TransactionKernel::assembler -pub struct BasicFungibleFaucet { - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, -} - -impl BasicFungibleFaucet { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// 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"; - - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata. - /// - /// # Errors: - /// 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 - /// ([`FungibleAsset::MAX_AMOUNT`]) - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - ) -> Result { - // First check that the metadata is valid. - if decimals > Self::MAX_DECIMALS { - return Err(FungibleFaucetError::TooManyDecimals { - actual: decimals as u64, - max: Self::MAX_DECIMALS, - }); - } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT { - return Err(FungibleFaucetError::MaxSupplyTooLarge { - actual: max_supply.as_int(), - max: FungibleAsset::MAX_AMOUNT, - }); - } - - Ok(Self { symbol, decimals, max_supply }) - } - - /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account - /// interface and storage. - /// - /// # Errors: - /// Returns an error if: - /// - the provided [`AccountInterface`] does not contain a - /// [`AccountComponentInterface::BasicFungibleFaucet`] component. - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply value exceeds maximum possible amount for a fungible asset of - /// [`FungibleAsset::MAX_AMOUNT`]. - /// - the token symbol encoded value exceeds the maximum value of - /// [`TokenSymbol::MAX_ENCODED_VALUE`]. - fn try_from_interface( - interface: AccountInterface, - 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 - let faucet_metadata = storage - .get_item(*offset) - .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?; - let [max_supply, decimals, token_symbol, _] = *faucet_metadata; - - // verify metadata values - let token_symbol = TokenSymbol::try_from(token_symbol) - .map_err(FungibleFaucetError::InvalidTokenSymbol)?; - let decimals = decimals.as_int().try_into().map_err(|_| { - FungibleFaucetError::TooManyDecimals { - actual: decimals.as_int(), - max: Self::MAX_DECIMALS, - } - })?; - - return BasicFungibleFaucet::new(token_symbol, decimals, max_supply); - } - } - - Err(FungibleFaucetError::NoAvailableInterface) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the symbol of the faucet. - pub fn symbol(&self) -> TokenSymbol { - self.symbol - } - - /// Returns the decimals of the faucet. - pub fn decimals(&self) -> u8 { - self.decimals - } - - /// Returns the max supply of the faucet. - pub fn max_supply(&self) -> Felt { - self.max_supply - } - - /// Returns the digest of the `distribute` account procedure. - pub fn distribute_digest() -> Word { - *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE - } - - /// Returns the digest of the `burn` account procedure. - pub fn burn_digest() -> Word { - *BASIC_FUNGIBLE_FAUCET_BURN - } -} - -impl From for AccountComponent { - fn from(faucet: BasicFungibleFaucet) -> Self { - // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as - // [a3, a2, a1, a0, ...] - let metadata = Word::new([ - faucet.max_supply, - Felt::from(faucet.decimals), - faucet.symbol.into(), - Felt::ZERO, - ]); - - AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)]) - .expect("basic fungible faucet component should satisfy the requirements of a valid account component") - .with_supported_type(AccountType::FungibleFaucet) - } -} - -impl TryFrom for BasicFungibleFaucet { - type Error = FungibleFaucetError; +mod basic_fungible; +mod network_fungible; - fn try_from(account: Account) -> Result { - let account_interface = AccountInterface::from(&account); - - BasicFungibleFaucet::try_from_interface(account_interface, account.storage()) - } -} - -impl TryFrom<&Account> for BasicFungibleFaucet { - type Error = FungibleFaucetError; - - fn try_from(account: &Account) -> Result { - let account_interface = AccountInterface::from(account); - - BasicFungibleFaucet::try_from_interface(account_interface, account.storage()) - } -} +pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; +pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; // FUNGIBLE FAUCET // ================================================================================================ @@ -256,72 +45,6 @@ impl FungibleFaucetExt for Account { } } -/// Creates a new faucet account with basic fungible faucet interface, -/// account storage type, specified authentication scheme, and provided meta data (token symbol, -/// decimals, max supply). -/// -/// The basic faucet interface exposes two procedures: -/// - `distribute`, which mints an assets and create a note for the provided recipient. -/// - `burn`, which burns the provided asset. -/// -/// `distribute` requires authentication. The authentication procedure is defined by the specified -/// authentication scheme. `burn` does not require authentication and can be called by anyone. -/// -/// 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, -/// 0]. -/// - Slot 3: A map with tracked procedure roots. -/// - Slot 4: Token metadata of the faucet. -pub fn create_basic_fungible_faucet( - init_seed: [u8; 32], - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - account_storage_mode: AccountStorageMode, - auth_scheme: AuthScheme, -) -> Result<(Account, Word), FungibleFaucetError> { - let distribute_proc_root = BasicFungibleFaucet::distribute_digest(); - - let auth_component: AccountComponent = match auth_scheme { - AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new( - pub_key, - AuthRpoFalcon512AclConfig::new() - .with_auth_trigger_procedures(vec![distribute_proc_root]) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into(), - AuthScheme::RpoFalcon512Multisig { threshold, pub_keys } => { - AuthRpoFalcon512Multisig::new(threshold, pub_keys) - .map_err(FungibleFaucetError::AccountError)? - .into() - }, - AuthScheme::NoAuth => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "basic fungible faucets cannot be created with NoAuth authentication scheme".into(), - )); - }, - AuthScheme::Unknown => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "basic fungible faucets cannot be created with Unknown authentication scheme" - .into(), - )); - }, - }; - - let (account, account_seed) = AccountBuilder::new(init_seed) - .account_type(AccountType::FungibleFaucet) - .storage_mode(account_storage_mode) - .with_auth_component(auth_component) - .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?) - .build() - .map_err(FungibleFaucetError::AccountError)?; - - Ok((account, account_seed)) -} - // FUNGIBLE FAUCET ERROR // ================================================================================================ @@ -347,136 +70,3 @@ pub enum FungibleFaucetError { #[error("account is not a fungible faucet account")] NotAFungibleFaucetAccount, } - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use miden_objects::crypto::dsa::rpo_falcon512::{self, PublicKey}; - use miden_objects::{FieldElement, ONE, Word}; - - use super::{ - AccountBuilder, - AccountStorageMode, - AccountType, - AuthScheme, - BasicFungibleFaucet, - Felt, - FungibleFaucetError, - TokenSymbol, - create_basic_fungible_faucet, - }; - use crate::account::auth::AuthRpoFalcon512; - use crate::account::wallets::BasicWallet; - - #[test] - fn faucet_contract_creation() { - let pub_key = rpo_falcon512::PublicKey::new(Word::new([ONE; 4])); - let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key }; - - // we need to use an initial seed to create the wallet account - let init_seed: [u8; 32] = [ - 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, - 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, - ]; - - let max_supply = Felt::new(123); - let token_symbol_string = "POL"; - let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap(); - let decimals = 2u8; - let storage_mode = AccountStorageMode::Private; - - let (faucet_account, _) = create_basic_fungible_faucet( - init_seed, - token_symbol, - decimals, - max_supply, - storage_mode, - auth_scheme, - ) - .unwrap(); - - // The reserved faucet slot should be initialized to an empty word. - assert_eq!(faucet_account.storage().get_item(0).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(), Word::from(pub_key)); - - // 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]. - assert_eq!( - faucet_account.storage().get_item(2).unwrap(), - [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into() - ); - - // The procedure root map in slot 3 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()) - .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. - assert_eq!( - faucet_account.storage().get_item(4).unwrap(), - [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into() - ); - - assert!(faucet_account.is_faucet()); - } - - #[test] - fn faucet_create_from_account() { - // prepare the test data - let mock_public_key = PublicKey::new(Word::from([0, 1, 2, 3u32])); - let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); - - // valid account - let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol"); - let faucet_account = AccountBuilder::new(mock_seed) - .account_type(AccountType::FungibleFaucet) - .with_component( - BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100)) - .expect("failed to create a fungible faucet component"), - ) - .with_auth_component(AuthRpoFalcon512::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)); - - // 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)) - // we need to add some other component so the builder doesn't fail - .with_component(BasicWallet) - .build_existing() - .expect("failed to create wallet account"); - - let err = BasicFungibleFaucet::try_from(invalid_faucet_account) - .err() - .expect("basic fungible faucet creation should fail"); - assert_matches!(err, FungibleFaucetError::NoAvailableInterface); - } - - /// Check that the obtaining of the basic fungible faucet procedure digests does not panic. - #[test] - fn get_faucet_procedures() { - let _distribute_digest = BasicFungibleFaucet::distribute_digest(); - let _burn_digest = BasicFungibleFaucet::burn_digest(); - } -} diff --git a/crates/miden-lib/src/account/faucets/network_fungible.rs b/crates/miden-lib/src/account/faucets/network_fungible.rs new file mode 100644 index 0000000000..3407fd71fc --- /dev/null +++ b/crates/miden-lib/src/account/faucets/network_fungible.rs @@ -0,0 +1,276 @@ +use miden_objects::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountId, + AccountStorage, + AccountStorageMode, + AccountType, + StorageSlot, +}; +use miden_objects::asset::TokenSymbol; +use miden_objects::{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::procedure_digest; + +// NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT +// ================================================================================================ + +// Initialize the digest of the `distribute` procedure of the Network Fungible Faucet only once. +procedure_digest!( + NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE, + NetworkFungibleFaucet::DISTRIBUTE_PROC_NAME, + network_fungible_faucet_library +); + +// Initialize the digest of the `burn` procedure of the Network Fungible Faucet only once. +procedure_digest!( + NETWORK_FUNGIBLE_FAUCET_BURN, + NetworkFungibleFaucet::BURN_PROC_NAME, + network_fungible_faucet_library +); + +/// 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: +/// - `distribute`, which mints an assets and create a note for the provided recipient. +/// - `burn`, which burns the provided asset. +/// +/// Both `distribute` and `burn` can only be called from note scripts. `distribute` requires +/// 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`]. +/// +/// 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 +/// +/// [kasm]: crate::transaction::TransactionKernel::assembler +pub struct NetworkFungibleFaucet { + faucet: BasicFungibleFaucet, + owner_account_id: AccountId, +} + +impl NetworkFungibleFaucet { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// 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"; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`NetworkFungibleFaucet`] component from the given pieces of metadata. + /// + /// # Errors: + /// 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`]) + pub fn new( + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + owner_account_id: AccountId, + ) -> Result { + // Create the basic fungible faucet (this validates the metadata) + let faucet = BasicFungibleFaucet::new(symbol, decimals, max_supply)?; + + Ok(Self { faucet, owner_account_id }) + } + + /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account + /// interface and storage. + /// + /// # Errors: + /// Returns an error if: + /// - the provided [`AccountInterface`] does not contain a + /// [`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`]. + /// - the token symbol encoded value exceeds the maximum value of + /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + fn try_from_interface( + interface: AccountInterface, + storage: &AccountStorage, + ) -> Result { + for component in interface.components().iter() { + if let AccountComponentInterface::NetworkFungibleFaucet(offset) = component { + // obtain metadata from storage using offset provided by NetworkFungibleFaucet + // interface + let faucet_metadata = storage + .get_item(*offset) + .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?; + 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))?; + + // Convert Word back to AccountId + // Storage format: [0, 0, suffix, prefix] + let prefix = owner_account_id_word[3]; + let suffix = owner_account_id_word[2]; + let owner_account_id = AccountId::new_unchecked([prefix, suffix]); + + // verify metadata values and create BasicFungibleFaucet + let token_symbol = TokenSymbol::try_from(token_symbol) + .map_err(FungibleFaucetError::InvalidTokenSymbol)?; + let decimals = decimals.as_int().try_into().map_err(|_| { + FungibleFaucetError::TooManyDecimals { + actual: decimals.as_int(), + max: Self::MAX_DECIMALS, + } + })?; + + let faucet = BasicFungibleFaucet::new(token_symbol, decimals, max_supply)?; + + return Ok(Self { faucet, owner_account_id }); + } + } + + Err(FungibleFaucetError::NoAvailableInterface) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the symbol of the faucet. + pub fn symbol(&self) -> TokenSymbol { + self.faucet.symbol() + } + + /// Returns the decimals of the faucet. + pub fn decimals(&self) -> u8 { + self.faucet.decimals() + } + + /// Returns the max supply of the faucet. + pub fn max_supply(&self) -> Felt { + self.faucet.max_supply() + } + + /// Returns the owner account ID of the faucet. + pub fn owner_account_id(&self) -> AccountId { + self.owner_account_id + } + + /// Returns the digest of the `distribute` account procedure. + pub fn distribute_digest() -> Word { + *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE + } + + /// Returns the digest of the `burn` account procedure. + pub fn burn_digest() -> Word { + *NETWORK_FUNGIBLE_FAUCET_BURN + } +} + +impl From for AccountComponent { + fn from(network_faucet: NetworkFungibleFaucet) -> Self { + // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as + // [a3, a2, a1, a0, ...] + let metadata = Word::new([ + network_faucet.faucet.max_supply(), + Felt::from(network_faucet.faucet.decimals()), + network_faucet.faucet.symbol().into(), + Felt::ZERO, + ]); + + // Convert AccountId to Word representation for storage + let owner_account_id_word: Word = [ + Felt::new(0), + Felt::new(0), + network_faucet.owner_account_id.suffix(), + network_faucet.owner_account_id.prefix().as_felt(), + ] + .into(); + + // Second storage slot stores the owner account ID + let owner_slot = StorageSlot::Value(owner_account_id_word); + + AccountComponent::new( + network_fungible_faucet_library(), + vec![StorageSlot::Value(metadata), owner_slot] + ) + .expect("network fungible faucet component should satisfy the requirements of a valid account component") + .with_supported_type(AccountType::FungibleFaucet) + } +} + +impl TryFrom for NetworkFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: Account) -> Result { + let account_interface = AccountInterface::from(&account); + + NetworkFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +impl TryFrom<&Account> for NetworkFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: &Account) -> Result { + let account_interface = AccountInterface::from(account); + + NetworkFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +/// Creates a new faucet account with network fungible faucet interface and provided metadata +/// (token symbol, decimals, max supply, owner account ID). +/// +/// The network faucet interface exposes two procedures: +/// - `distribute`, which mints an assets and create a note for the provided recipient. +/// - `burn`, which burns the provided asset. +/// +/// Both `distribute` and `burn` can only be called from note scripts. `distribute` requires +/// authentication using the NoAuth scheme. `burn` does not require authentication and can be +/// called by anyone. +/// +/// Network fungible faucets always use: +/// - [`AccountStorageMode::Network`] for storage +/// - [`NoAuth`] for authentication +/// +/// 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, +/// 0]. +/// - Slot 3: A map with tracked procedure roots. +/// - Slot 4: Token metadata of the faucet. +/// - Slot 5: Owner account ID. +pub fn create_network_fungible_faucet( + init_seed: [u8; 32], + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + owner_account_id: AccountId, +) -> Result { + let auth_component: AccountComponent = NoAuth::new().into(); + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Network) + .with_auth_component(auth_component) + .with_component(NetworkFungibleFaucet::new(symbol, decimals, max_supply, owner_account_id)?) + .build() + .map_err(FungibleFaucetError::AccountError)?; + + Ok(account) +} diff --git a/crates/miden-lib/src/account/interface/component.rs b/crates/miden-lib/src/account/interface/component.rs index 374f7a72d4..db6136db52 100644 --- a/crates/miden-lib/src/account/interface/component.rs +++ b/crates/miden-lib/src/account/interface/component.rs @@ -2,8 +2,8 @@ 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::crypto::dsa::rpo_falcon512::PublicKey; use miden_objects::note::PartialNote; use miden_objects::{Felt, FieldElement, Word}; @@ -26,6 +26,12 @@ pub enum AccountComponentInterface { /// slot has a format of `[max_supply, faucet_decimals, token_symbol, 0]`. BasicFungibleFaucet(u8), /// 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), + /// Exposes procedures from the /// [`AuthRpoFalcon512`][crate::account::auth::AuthRpoFalcon512] module. /// /// Internal value holds the storage slot index where the public key for the RpoFalcon512 @@ -65,6 +71,9 @@ impl AccountComponentInterface { AccountComponentInterface::BasicFungibleFaucet(_) => { "Basic Fungible Faucet".to_string() }, + AccountComponentInterface::NetworkFungibleFaucet(_) => { + "Network Fungible Faucet".to_string() + }, AccountComponentInterface::AuthRpoFalcon512(_) => "RPO Falcon512".to_string(), AccountComponentInterface::AuthRpoFalcon512Acl(_) => "RPO Falcon512 ACL".to_string(), AccountComponentInterface::AuthRpoFalcon512Multisig(_) => { @@ -101,7 +110,7 @@ impl AccountComponentInterface { AccountComponentInterface::AuthRpoFalcon512(storage_index) | AccountComponentInterface::AuthRpoFalcon512Acl(storage_index) => { vec![AuthScheme::RpoFalcon512 { - pub_key: PublicKey::new( + pub_key: PublicKeyCommitment::from( storage .get_item(*storage_index) .expect("invalid storage index of the public key"), @@ -174,7 +183,7 @@ impl AccountComponentInterface { /// /// ```masm /// push.{note_information} - /// call.::miden::tx::create_note + /// call.::miden::output_note::create /// /// push.{note asset} /// call.::miden::contracts::wallets::basic::move_asset_to_note dropw @@ -248,7 +257,7 @@ impl AccountComponentInterface { // stack => [] }, AccountComponentInterface::BasicWallet => { - body.push_str("call.::miden::tx::create_note\n"); + body.push_str("call.::miden::output_note::create\n"); // stack => [note_idx] for asset in partial_note.assets().iter() { @@ -289,25 +298,31 @@ fn extract_multisig_auth_scheme(storage: &AccountStorage, storage_index: u8) -> let threshold = config[0].as_int() as u32; let num_approvers = config[1].as_int() as u8; - // The public keys are stored in a map at the next slot (storage_index + 1) + // 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 for key_index in 0..num_approvers { + // 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()) { - Ok(pub_key_word) => { - pub_keys.push(PublicKey::new(pub_key_word)); + Ok(pub_key) => { + pub_keys.push(PublicKeyCommitment::from(pub_key)); }, Err(_) => { // If we can't read a public key, panic with a clear error message panic!( - "Failed to read public key {} from multisig configuration at storage index {}. \ + "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, storage_index + key_index, pub_keys_map_slot ); }, } diff --git a/crates/miden-lib/src/account/interface/mod.rs b/crates/miden-lib/src/account/interface/mod.rs index d8e0b62d8b..2586134cf2 100644 --- a/crates/miden-lib/src/account/interface/mod.rs +++ b/crates/miden-lib/src/account/interface/mod.rs @@ -8,19 +8,21 @@ use miden_objects::account::{Account, AccountCode, AccountId, AccountIdPrefix, A 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 thiserror::Error; use crate::AuthScheme; use crate::account::components::{ basic_fungible_faucet_library, basic_wallet_library, - 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::well_known_note::WellKnownNote; +use crate::note::WellKnownNote; use crate::utils::ScriptBuilder; #[cfg(test)] @@ -79,12 +81,12 @@ impl AccountInterface { self.account_id.is_regular_account() } - /// Returns `true` if the full state of the account is on chain, i.e. if the modes are + /// 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), /// `false` otherwise. - pub fn is_onchain(&self) -> bool { - self.account_id.is_onchain() + pub fn has_public_state(&self) -> bool { + self.account_id.has_public_state() } /// Returns `true` if the reference account is a private account, `false` otherwise. @@ -139,6 +141,11 @@ impl AccountInterface { 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::AuthRpoFalcon512(_) => { component_proc_digests .extend(rpo_falcon_512_library().mast_forest().procedure_digests()); @@ -148,8 +155,9 @@ impl AccountInterface { .extend(rpo_falcon_512_acl_library().mast_forest().procedure_digests()); }, AccountComponentInterface::AuthRpoFalcon512Multisig(_) => { - component_proc_digests - .extend(multisig_library().mast_forest().procedure_digests()); + component_proc_digests.extend( + rpo_falcon_512_multisig_library().mast_forest().procedure_digests(), + ); }, AccountComponentInterface::AuthNoAuth => { component_proc_digests @@ -250,6 +258,14 @@ impl AccountInterface { 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(_)) + }) + { + // Network fungible faucet doesn't support send_note_body, because minting + // is done via a MINT note. + Err(AccountInterfaceError::UnsupportedAccountInterface) } else if self.components().contains(&AccountComponentInterface::BasicWallet) { AccountComponentInterface::BasicWallet.send_note_body(*self.id(), output_notes) } else { diff --git a/crates/miden-lib/src/account/interface/test.rs b/crates/miden-lib/src/account/interface/test.rs index 969ad4a6bf..9ee92f06c4 100644 --- a/crates/miden-lib/src/account/interface/test.rs +++ b/crates/miden-lib/src/account/interface/test.rs @@ -3,11 +3,11 @@ use alloc::sync::Arc; use alloc::vec::Vec; use assert_matches::assert_matches; -use miden_objects::account::{Account, AccountBuilder, AccountComponent, AccountType, StorageSlot}; +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::dsa::rpo_falcon512::PublicKey; use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin}; use miden_objects::note::{ Note, @@ -26,7 +26,12 @@ use miden_objects::testing::account_id::{ use miden_objects::{AccountError, Felt, NoteError, Word, ZERO}; use crate::AuthScheme; -use crate::account::auth::{AuthRpoFalcon512, AuthRpoFalcon512Multisig, NoAuth}; +use crate::account::auth::{ + AuthRpoFalcon512, + AuthRpoFalcon512Multisig, + AuthRpoFalcon512MultisigConfig, + NoAuth, +}; use crate::account::faucets::BasicFungibleFaucet; use crate::account::interface::{ AccountComponentInterface, @@ -35,6 +40,7 @@ use crate::account::interface::{ }; use crate::account::wallets::BasicWallet; 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; @@ -702,8 +708,10 @@ impl AccountComponentExt for AccountComponent { } } +/// Helper function to create a mock auth component for testing fn get_mock_auth_component() -> AuthRpoFalcon512 { - let mock_public_key = PublicKey::new(Word::from([0, 1, 2, 3u32])); + let mock_word = Word::from([0, 1, 2, 3u32]); + let mock_public_key = PublicKeyCommitment::from(mock_word); AuthRpoFalcon512::new(mock_public_key) } @@ -734,7 +742,7 @@ fn test_get_auth_scheme_rpo_falcon512() { let auth_scheme = &auth_schemes[0]; match auth_scheme { AuthScheme::RpoFalcon512 { pub_key } => { - assert_eq!(*pub_key, PublicKey::new(Word::from([0, 1, 2, 3u32]))); + assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); }, _ => panic!("Expected RpoFalcon512 auth scheme"), } @@ -800,7 +808,8 @@ fn test_account_interface_from_account_uses_get_auth_scheme() { match &wallet_account_interface.auth()[0] { AuthScheme::RpoFalcon512 { pub_key } => { - assert_eq!(*pub_key, PublicKey::new(Word::from([0, 1, 2, 3u32]))); + let expected_pub_key = PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32])); + assert_eq!(*pub_key, expected_pub_key); }, _ => panic!("Expected RpoFalcon512 auth scheme"), } @@ -839,7 +848,7 @@ fn test_account_interface_get_auth_scheme() { assert_eq!(wallet_account_interface.auth().len(), 1); match &wallet_account_interface.auth()[0] { AuthScheme::RpoFalcon512 { pub_key } => { - assert_eq!(*pub_key, PublicKey::new(Word::from([0, 1, 2, 3u32]))); + assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); }, _ => panic!("Expected RpoFalcon512 auth scheme"), } @@ -864,26 +873,6 @@ fn test_account_interface_get_auth_scheme() { // accounts are required to have auth components in the current system design } -fn get_public_keys_from_account(account: &Account) -> Vec { - let mut pub_keys = vec![]; - let interface: AccountInterface = account.into(); - - for auth in interface.auth() { - match auth { - AuthScheme::NoAuth => {}, - AuthScheme::RpoFalcon512 { pub_key } => pub_keys.push(Word::from(*pub_key)), - AuthScheme::RpoFalcon512Multisig { pub_keys: multisig_keys, .. } => { - for key in multisig_keys { - pub_keys.push(Word::from(*key)); - } - }, - AuthScheme::Unknown => {}, - } - } - - pub_keys -} - #[test] fn test_public_key_extraction_regular_account() { let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); @@ -903,15 +892,17 @@ fn test_public_key_extraction_regular_account() { #[test] fn test_public_key_extraction_multisig_account() { // Create test public keys - let pub_key_1 = PublicKey::new(Word::from([1u32, 0, 0, 0])); - let pub_key_2 = PublicKey::new(Word::from([2u32, 0, 0, 0])); - let pub_key_3 = PublicKey::new(Word::from([3u32, 0, 0, 0])); + let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0])); + let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0])); + let pub_key_3 = PublicKeyCommitment::from(Word::from([3u32, 0, 0, 0])); let approvers = vec![pub_key_1, pub_key_2, pub_key_3]; let threshold = 2u32; // Create multisig component - let multisig_component = AuthRpoFalcon512Multisig::new(threshold, approvers.clone()) - .expect("multisig component creation failed"); + let multisig_component = AuthRpoFalcon512Multisig::new( + AuthRpoFalcon512MultisigConfig::new(approvers.clone(), threshold).unwrap(), + ) + .expect("multisig component creation failed"); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let multisig_account = AccountBuilder::new(mock_seed) diff --git a/crates/miden-lib/src/account/mod.rs b/crates/miden-lib/src/account/mod.rs index ec362c6587..78afc9f8dd 100644 --- a/crates/miden-lib/src/account/mod.rs +++ b/crates/miden-lib/src/account/mod.rs @@ -5,3 +5,42 @@ pub mod components; pub mod faucets; pub mod interface; pub mod wallets; + +/// Macro to simplify the creation of static procedure digest constants. +/// +/// 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. +/// +/// # Arguments +/// * `$name` - The name of the static variable to create +/// * `$proc_name` - The string name of the procedure +/// * `$library_fn` - The function that returns the library containing the procedure +/// +/// # Example +/// ```ignore +/// procedure_digest!( +/// BASIC_WALLET_RECEIVE_ASSET, +/// BasicWallet::RECEIVE_ASSET_PROC_NAME, +/// basic_wallet_library +/// ); +/// ``` +#[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(|| { + panic!("{} should contain '{}' procedure", stringify!($library_fn), $proc_name) + }) + }); + }; +} diff --git a/crates/miden-lib/src/account/wallets/mod.rs b/crates/miden-lib/src/account/wallets/mod.rs index 1cdb367208..7dd73650e8 100644 --- a/crates/miden-lib/src/account/wallets/mod.rs +++ b/crates/miden-lib/src/account/wallets/mod.rs @@ -7,41 +7,34 @@ use miden_objects::account::{ AccountStorageMode, AccountType, }; -use miden_objects::assembly::{ProcedureName, QualifiedProcedureName}; -use miden_objects::utils::sync::LazyLock; use miden_objects::{AccountError, Word}; use thiserror::Error; use super::AuthScheme; -use crate::account::auth::{AuthRpoFalcon512, AuthRpoFalcon512Multisig}; +use crate::account::auth::{ + AuthRpoFalcon512, + AuthRpoFalcon512Multisig, + AuthRpoFalcon512MultisigConfig, +}; use crate::account::components::basic_wallet_library; +use crate::procedure_digest; // BASIC WALLET // ================================================================================================ // Initialize the digest of the `receive_asset` procedure of the Basic Wallet only once. -static BASIC_WALLET_RECEIVE_ASSET: LazyLock = LazyLock::new(|| { - let receive_asset_proc_name = QualifiedProcedureName::new( - Default::default(), - ProcedureName::new(BasicWallet::RECEIVE_ASSET_PROC_NAME) - .expect("failed to create name for 'receive_asset' procedure"), - ); - basic_wallet_library() - .get_procedure_root_by_name(receive_asset_proc_name) - .expect("Basic Wallet should contain 'receive_asset' procedure") -}); +procedure_digest!( + BASIC_WALLET_RECEIVE_ASSET, + BasicWallet::RECEIVE_ASSET_PROC_NAME, + basic_wallet_library +); // Initialize the digest of the `move_asset_to_note` procedure of the Basic Wallet only once. -static BASIC_WALLET_MOVE_ASSET_TO_NOTE: LazyLock = LazyLock::new(|| { - let move_asset_to_note_proc_name = QualifiedProcedureName::new( - Default::default(), - ProcedureName::new(BasicWallet::MOVE_ASSET_TO_NOTE_PROC_NAME) - .expect("failed to create name for 'move_asset_to_note' procedure"), - ); - basic_wallet_library() - .get_procedure_root_by_name(move_asset_to_note_proc_name) - .expect("Basic Wallet should contain 'move_asset_to_note' procedure") -}); +procedure_digest!( + BASIC_WALLET_MOVE_ASSET_TO_NOTE, + BasicWallet::MOVE_ASSET_TO_NOTE_PROC_NAME, + basic_wallet_library +); /// An [`AccountComponent`] implementing a basic wallet. /// @@ -116,7 +109,7 @@ pub fn create_basic_wallet( auth_scheme: AuthScheme, account_type: AccountType, account_storage_mode: AccountStorageMode, -) -> Result<(Account, Word), BasicWalletError> { +) -> Result { if matches!(account_type, AccountType::FungibleFaucet | AccountType::NonFungibleFaucet) { return Err(BasicWalletError::AccountError(AccountError::other( "basic wallet accounts cannot have a faucet account type", @@ -126,7 +119,12 @@ pub fn create_basic_wallet( let auth_component: AccountComponent = match auth_scheme { AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512::new(pub_key).into(), AuthScheme::RpoFalcon512Multisig { threshold, pub_keys } => { - AuthRpoFalcon512Multisig::new(threshold, pub_keys) + let config = AuthRpoFalcon512MultisigConfig::new(pub_keys, threshold) + .and_then(|cfg| { + cfg.with_proc_thresholds(vec![(BasicWallet::receive_asset_digest(), 1)]) + }) + .map_err(BasicWalletError::AccountError)?; + AuthRpoFalcon512Multisig::new(config) .map_err(BasicWalletError::AccountError)? .into() }, @@ -142,7 +140,7 @@ pub fn create_basic_wallet( }, }; - let (account, account_seed) = AccountBuilder::new(init_seed) + let account = AccountBuilder::new(init_seed) .account_type(account_type) .storage_mode(account_storage_mode) .with_auth_component(auth_component) @@ -150,7 +148,7 @@ pub fn create_basic_wallet( .build() .map_err(BasicWalletError::AccountError)?; - Ok((account, account_seed)) + Ok(account) } // TESTS @@ -158,8 +156,7 @@ pub fn create_basic_wallet( #[cfg(test)] mod tests { - - use miden_objects::crypto::dsa::rpo_falcon512; + use miden_objects::account::auth::PublicKeyCommitment; use miden_objects::{ONE, Word}; use miden_processor::utils::{Deserializable, Serializable}; @@ -168,7 +165,7 @@ mod tests { #[test] fn test_create_basic_wallet() { - let pub_key = rpo_falcon512::PublicKey::new(Word::from([ONE; 4])); + let pub_key = PublicKeyCommitment::from(Word::from([ONE; 4])); let wallet = create_basic_wallet( [1; 32], AuthScheme::RpoFalcon512 { pub_key }, @@ -183,15 +180,14 @@ mod tests { #[test] fn test_serialize_basic_wallet() { - let pub_key = rpo_falcon512::PublicKey::new(Word::from([ONE; 4])); + let pub_key = PublicKeyCommitment::from(Word::from([ONE; 4])); let wallet = create_basic_wallet( [1; 32], AuthScheme::RpoFalcon512 { pub_key }, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, ) - .unwrap() - .0; + .unwrap(); let bytes = wallet.to_bytes(); let deserialized_wallet = Account::read_from_bytes(&bytes).unwrap(); diff --git a/crates/miden-lib/src/auth.rs b/crates/miden-lib/src/auth.rs index 33de58f69c..53644d8784 100644 --- a/crates/miden-lib/src/auth.rs +++ b/crates/miden-lib/src/auth.rs @@ -1,25 +1,43 @@ use alloc::vec::Vec; -use miden_objects::crypto::dsa::rpo_falcon512; +use miden_objects::account::auth::PublicKeyCommitment; /// Defines authentication schemes available to standard and faucet accounts. pub enum AuthScheme { - /// A single-key authentication scheme which relies RPO Falcon512 signatures. RPO Falcon512 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: rpo_falcon512::PublicKey }, + /// A minimal authentication scheme that provides no cryptographic authentication. + /// + /// It only increments the nonce if the account state has actually changed during transaction + /// execution, avoiding unnecessary nonce increments for transactions that don't modify the + /// account state. + NoAuth, + /// A single-key authentication scheme which relies RPO Falcon512 signatures. + /// + /// RPO Falcon512 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. + /// /// Requires a threshold number of signatures from the provided public keys. RpoFalcon512Multisig { threshold: u32, - pub_keys: Vec, + pub_keys: Vec, }, - /// A minimal authentication scheme that provides no cryptographic authentication. - /// It only increments the nonce if the account state has actually changed during - /// transaction execution, avoiding unnecessary nonce increments for transactions - /// that don't modify the account state. - NoAuth, /// A non-standard authentication scheme. Unknown, } + +impl AuthScheme { + /// Returns all public key commitments associated with this authentication scheme. + /// + /// For unknown schemes, an empty vector is returned. + pub fn get_public_key_commitments(&self) -> Vec { + match self { + AuthScheme::NoAuth => Vec::new(), + AuthScheme::RpoFalcon512 { pub_key } => vec![*pub_key], + AuthScheme::RpoFalcon512Multisig { pub_keys, .. } => pub_keys.clone(), + AuthScheme::Unknown => Vec::new(), + } + } +} diff --git a/crates/miden-lib/src/errors/mod.rs b/crates/miden-lib/src/errors/mod.rs index cd36431118..6bd4bda685 100644 --- a/crates/miden-lib/src/errors/mod.rs +++ b/crates/miden-lib/src/errors/mod.rs @@ -13,8 +13,4 @@ mod script_builder_errors; pub use script_builder_errors::ScriptBuilderError; mod transaction_errors; -pub use transaction_errors::{ - TransactionEventError, - TransactionKernelError, - TransactionTraceParsingError, -}; +pub use transaction_errors::{TransactionEventError, TransactionTraceParsingError}; diff --git a/crates/miden-lib/src/errors/note_script_errors.rs b/crates/miden-lib/src/errors/note_script_errors.rs index 953761897d..2ee15d05e9 100644 --- a/crates/miden-lib/src/errors/note_script_errors.rs +++ b/crates/miden-lib/src/errors/note_script_errors.rs @@ -13,6 +13,18 @@ use crate::errors::MasmError; /// 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: "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: "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"); /// Error Message: "P2IDE reclaim is disabled" @@ -33,3 +45,6 @@ pub const ERR_P2ID_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_st 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: "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-lib/src/errors/transaction_errors.rs b/crates/miden-lib/src/errors/transaction_errors.rs index 83e6da6e13..8c8013c8e4 100644 --- a/crates/miden-lib/src/errors/transaction_errors.rs +++ b/crates/miden-lib/src/errors/transaction_errors.rs @@ -1,90 +1,7 @@ -use alloc::boxed::Box; -use alloc::vec::Vec; -use core::error::Error; - -use miden_objects::note::NoteMetadata; -use miden_objects::transaction::TransactionSummary; -use miden_objects::{AccountDeltaError, AssetError, Felt, NoteError, Word}; +use miden_core::EventId; use thiserror::Error; -// TRANSACTION KERNEL ERROR -// ================================================================================================ - -#[derive(Debug, Error)] -pub enum TransactionKernelError { - #[error("failed to add asset to account delta")] - AccountDeltaAddAssetFailed(#[source] AccountDeltaError), - #[error("failed to remove asset to account delta")] - AccountDeltaRemoveAssetFailed(#[source] AccountDeltaError), - #[error("failed to add asset to note")] - 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" - )] - MissingAuthenticator, - #[error("failed to generate signature")] - SignatureGenerationFailed(#[source] Box), - #[error("transaction returned unauthorized event but a commitment did not match: {0}")] - TransactionSummaryCommitmentMismatch(#[source] Box), - #[error("failed to construct transaction summary")] - TransactionSummaryConstructionFailed(#[source] Box), - #[error("asset data extracted from the stack by event handler `{handler}` is not well formed")] - MalformedAssetInEventHandler { - handler: &'static str, - source: AssetError, - }, - #[error( - "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" - )] - MalformedNoteScript { - data: Vec, - // This is always a DeserializationError, but we can't import it directly here without - // adding dependencies, so we make it a trait object instead. - source: Box, - }, - #[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), - #[error( - "public note with metadata {0:?} and recipient digest {1} is missing details in the advice provider" - )] - PublicNoteMissingDetails(NoteMetadata, Word), - #[error( - "note input data in advice provider contains fewer elements ({actual}) than specified ({specified}) by its inputs length" - )] - TooFewElementsForNoteInputs { specified: u64, actual: u64 }, - #[error("account procedure with procedure root {0} is not in the advice provider")] - UnknownAccountProcedure(Word), - #[error("code commitment {0} is not in the advice provider")] - UnknownCodeCommitment(Word), - #[error("account storage slots number is missing in memory at address {0}")] - AccountStorageSlotsNumMissing(u32), - #[error("account nonce can only be incremented once")] - NonceCanOnlyIncrementOnce, - #[error("failed to convert fee asset into fungible asset")] - FailedToConvertFeeAsset(#[source] AssetError), - #[error( - "native asset amount {account_balance} in the account vault is not sufficient to cover the transaction fee of {tx_fee}" - )] - InsufficientFee { account_balance: u64, tx_fee: u64 }, - /// This variant signals that a signature over the contained commitments is required, but - /// missing. - #[error("transaction requires a signature")] - Unauthorized(Box), -} +use crate::transaction::TransactionEvent; // TRANSACTION EVENT PARSING ERROR // ================================================================================================ @@ -92,11 +9,11 @@ pub enum TransactionKernelError { #[derive(Debug, Error)] pub enum TransactionEventError { #[error("event id {0} is not a valid transaction event")] - InvalidTransactionEvent(u32), + InvalidTransactionEvent(EventId, Option<&'static str>), #[error("event id {0} is not a transaction kernel event")] - NotTransactionEvent(u32), + NotTransactionEvent(EventId, Option<&'static str>), #[error("event id {0} can only be emitted from the root context")] - NotRootContext(u32), + NotRootContext(TransactionEvent), } // TRANSACTION TRACE PARSING ERROR @@ -107,15 +24,3 @@ pub enum TransactionTraceParsingError { #[error("trace id {0} is an unknown transaction kernel trace")] UnknownTransactionTrace(u32), } - -#[cfg(test)] -mod error_assertions { - use super::*; - - /// Asserts at compile time that the passed error has Send + Sync + 'static bounds. - fn _assert_error_is_send_sync_static(_: E) {} - - fn _assert_transaction_kernel_error_bounds(err: TransactionKernelError) { - _assert_error_is_send_sync_static(err); - } -} diff --git a/crates/miden-lib/src/errors/tx_kernel_errors.rs b/crates/miden-lib/src/errors/tx_kernel_errors.rs index dcbfda2c8d..3022640235 100644 --- a/crates/miden-lib/src/errors/tx_kernel_errors.rs +++ b/crates/miden-lib/src/errors/tx_kernel_errors.rs @@ -28,14 +28,12 @@ pub const ERR_ACCOUNT_ID_UNKNOWN_STORAGE_MODE: MasmError = MasmError::from_stati 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 current account is not native" -pub const ERR_ACCOUNT_IS_NOT_NATIVE: MasmError = MasmError::from_static_str("the current account is not native"); +/// 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: "account nonce did not increase after a state changing transaction" -pub const ERR_ACCOUNT_NONCE_DID_NOT_INCREASE_AFTER_STATE_CHANGE: MasmError = MasmError::from_static_str("account nonce did not increase after a state changing transaction"); /// 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" @@ -52,10 +50,12 @@ pub const ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT: MasmError = MasmError::f pub const ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT: MasmError = MasmError::from_static_str("failed to write an account value item to a non-value storage slot"); /// Error Message: "depth of the nested FPI calls exceeded 64" pub const ERR_ACCOUNT_STACK_OVERFLOW: MasmError = MasmError::from_static_str("depth of the nested FPI calls exceeded 64"); -/// Error Message: "failed to end foreign context because the current account is the native account" -pub const ERR_ACCOUNT_STACK_UNDERFLOW: MasmError = MasmError::from_static_str("failed to end foreign context because the current account is the native account"); +/// Error Message: "failed to end foreign context because the active account is the native account" +pub const ERR_ACCOUNT_STACK_UNDERFLOW: MasmError = MasmError::from_static_str("failed to end foreign context because the active account is the native account"); /// Error Message: "computed account storage commitment does not match recorded account storage commitment" 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: "number of account procedures exceeds the maximum limit of 256" @@ -65,6 +65,8 @@ pub const ERR_ACCOUNT_TOO_MANY_STORAGE_SLOTS: MasmError = MasmError::from_static /// 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" +pub const ERR_EPILOGUE_NONCE_CANNOT_BE_0: MasmError = MasmError::from_static_str("nonce cannot be 0 after an account-creating transaction"); /// Error Message: "total number of assets in the account and all involved notes must stay the same" pub const ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME: MasmError = MasmError::from_static_str("total number of assets in the account and all involved notes must stay the same"); @@ -145,12 +147,18 @@ pub const ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO: MasmE /// 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: "attempted to access note assets from incorrect context" -pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_ASSETS_FROM_INCORRECT_CONTEXT: MasmError = MasmError::from_static_str("attempted to access note assets from incorrect context"); -/// Error Message: "attempted to access note inputs from incorrect context" -pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_FROM_INCORRECT_CONTEXT: MasmError = MasmError::from_static_str("attempted to access note inputs from incorrect context"); -/// Error Message: "attempted to access note sender from incorrect context" -pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SENDER_FROM_INCORRECT_CONTEXT: MasmError = MasmError::from_static_str("attempted to access note sender from incorrect context"); +/// 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"); +/// Error Message: "failed to access note inputs of active note because no note is currently being processed" +pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_INPUTS_WHILE_NO_NOTE_BEING_PROCESSED: MasmError = MasmError::from_static_str("failed to access note inputs of active note because no note is currently being processed"); +/// Error Message: "failed to access note metadata of active note because no note is currently being processed" +pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED: MasmError = MasmError::from_static_str("failed to access note metadata of active note because no note is currently being processed"); +/// Error Message: "failed to access note recipient of active note because no note is currently being processed" +pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_RECIPIENT_WHILE_NO_NOTE_BEING_PROCESSED: MasmError = MasmError::from_static_str("failed to access note recipient of active note because no note is currently being processed"); +/// Error Message: "failed to access note script root of active note because no note is currently being processed" +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" @@ -160,11 +168,9 @@ pub const ERR_NOTE_INVALID_INDEX: MasmError = MasmError::from_static_str("failed /// 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_NOTE_INPUTS: MasmError = MasmError::from_static_str("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: "network execution mode with a specific target can only target network accounts" -pub const ERR_NOTE_NETWORK_EXECUTION_DOES_NOT_TARGET_NETWORK_ACCOUNT: MasmError = MasmError::from_static_str("network execution mode with a specific target can only target network accounts"); /// 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" @@ -181,10 +187,8 @@ pub const ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_COMMITMENT: Mas pub const ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_NUMBER_COMMITMENT: MasmError = MasmError::from_static_str("the provided global inputs do not match the block number commitment"); /// Error Message: "note commitment computed from the input note data does not match given note commitment" pub const ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH: MasmError = MasmError::from_static_str("note commitment computed from the input note data does not match given note commitment"); -/// Error Message: "sequential hash over kernel commitments does not match tx kernel commitment from block" -pub const ERR_PROLOGUE_KERNEL_COMMITMENT_MISMATCH: MasmError = MasmError::from_static_str("sequential hash over kernel commitments does not match tx kernel commitment from block"); -/// Error Message: "sequential hash over kernel procedures does not match kernel commitment" -pub const ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH: MasmError = MasmError::from_static_str("sequential hash over kernel procedures does not match kernel commitment"); +/// Error Message: "sequential hash over kernel procedures does not match kernel commitment from block" +pub const ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH: MasmError = MasmError::from_static_str("sequential hash over kernel procedures does not match kernel commitment from block"); /// Error Message: "account IDs provided via global inputs and advice provider do not match" pub const ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER: MasmError = MasmError::from_static_str("account IDs provided via global inputs and advice provider do not match"); /// Error Message: "reference block MMR and note's authentication MMR must match" @@ -205,6 +209,8 @@ 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" diff --git a/crates/miden-lib/src/lib.rs b/crates/miden-lib/src/lib.rs index 5f377b7281..85e2797b9a 100644 --- a/crates/miden-lib/src/lib.rs +++ b/crates/miden-lib/src/lib.rs @@ -82,7 +82,7 @@ mod tests { #[test] fn test_compile() { - let path = "miden::account::get_id".parse::().unwrap(); + let path = "miden::active_account::get_id".parse::().unwrap(); let miden = MidenLib::default(); let exists = miden.0.module_infos().any(|module| { module diff --git a/crates/miden-lib/src/note/mod.rs b/crates/miden-lib/src/note/mod.rs index 31b5319288..b02d417867 100644 --- a/crates/miden-lib/src/note/mod.rs +++ b/crates/miden-lib/src/note/mod.rs @@ -17,10 +17,11 @@ use miden_objects::note::{ }; use miden_objects::{Felt, NoteError, Word}; use utils::build_swap_tag; -use well_known_note::WellKnownNote; pub mod utils; -pub mod well_known_note; + +mod well_known_note; +pub use well_known_note::{NoteConsumptionStatus, WellKnownNote}; // STANDARDIZED SCRIPTS // ================================================================================================ diff --git a/crates/miden-lib/src/note/well_known_note.rs b/crates/miden-lib/src/note/well_known_note.rs index 7224460a6f..0a0b200217 100644 --- a/crates/miden-lib/src/note/well_known_note.rs +++ b/crates/miden-lib/src/note/well_known_note.rs @@ -1,9 +1,16 @@ -use miden_objects::Word; +use alloc::boxed::Box; +use alloc::string::String; +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 crate::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; use crate::account::interface::{AccountComponentInterface, AccountInterface}; use crate::account::wallets::BasicWallet; @@ -31,6 +38,20 @@ static SWAP_SCRIPT: LazyLock = LazyLock::new(|| { NoteScript::new(program) }); +// Initialize the MINT note script only once +static MINT_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/MINT.masb")); + let program = Program::read_from_bytes(bytes).expect("Shipped MINT script is well-formed"); + NoteScript::new(program) +}); + +// Initialize the BURN note script only once +static BURN_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/BURN.masb")); + let program = Program::read_from_bytes(bytes).expect("Shipped BURN script is well-formed"); + NoteScript::new(program) +}); + /// Returns the P2ID (Pay-to-ID) note script. fn p2id() -> NoteScript { P2ID_SCRIPT.clone() @@ -61,6 +82,26 @@ fn swap_root() -> Word { SWAP_SCRIPT.root() } +/// Returns the MINT (Mint note) note script. +fn mint() -> NoteScript { + MINT_SCRIPT.clone() +} + +/// Returns the MINT (Mint note) note script root. +fn mint_root() -> Word { + MINT_SCRIPT.root() +} + +/// Returns the BURN (Burn note) note script. +fn burn() -> NoteScript { + BURN_SCRIPT.clone() +} + +/// Returns the BURN (Burn note) note script root. +fn burn_root() -> Word { + BURN_SCRIPT.root() +} + // WELL KNOWN NOTE // ================================================================================================ @@ -69,6 +110,8 @@ pub enum WellKnownNote { P2ID, P2IDE, SWAP, + MINT, + BURN, } impl WellKnownNote { @@ -84,6 +127,12 @@ impl WellKnownNote { /// Expected number of inputs of the SWAP note. const SWAP_NUM_INPUTS: usize = 10; + /// Expected number of inputs of the MINT note. + const MINT_NUM_INPUTS: usize = 9; + + /// Expected number of inputs of the BURN note. + const BURN_NUM_INPUTS: usize = 0; + // CONSTRUCTOR // -------------------------------------------------------------------------------------------- @@ -101,6 +150,12 @@ impl WellKnownNote { if note_script_root == swap_root() { return Some(Self::SWAP); } + if note_script_root == mint_root() { + return Some(Self::MINT); + } + if note_script_root == burn_root() { + return Some(Self::BURN); + } None } @@ -108,12 +163,14 @@ impl WellKnownNote { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the expected inputs number of the current note. + /// Returns the expected inputs number of the active note. pub fn num_expected_inputs(&self) -> usize { match self { 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::BURN => Self::BURN_NUM_INPUTS, } } @@ -123,6 +180,8 @@ impl WellKnownNote { Self::P2ID => p2id(), Self::P2IDE => p2ide(), Self::SWAP => swap(), + Self::MINT => mint(), + Self::BURN => burn(), } } @@ -132,6 +191,8 @@ impl WellKnownNote { Self::P2ID => p2id_root(), Self::P2IDE => p2ide_root(), Self::SWAP => swap_root(), + Self::MINT => mint_root(), + Self::BURN => burn_root(), } } @@ -155,6 +216,244 @@ impl WellKnownNote { interface_proc_digests.contains(&BasicWallet::receive_asset_digest()) && interface_proc_digests.contains(&BasicWallet::move_asset_to_note_digest()) }, + Self::MINT => { + // MINT notes work only with network fungible faucets. The network faucet uses + // note-based authentication (checking if the note sender equals the faucet owner) + // to authorize minting, while basic faucets have different mint procedures that + // are not compatible with MINT notes. + interface_proc_digests.contains(&NetworkFungibleFaucet::distribute_digest()) + }, + Self::BURN => { + // BURN notes work with both basic and network fungible faucets because both + // faucet types export the same `burn` procedure with identical MAST roots. + // This allows a single BURN note script to work with either faucet type. + interface_proc_digests.contains(&BasicFungibleFaucet::burn_digest()) + || interface_proc_digests.contains(&NetworkFungibleFaucet::burn_digest()) + }, + } + } + + /// Performs the inputs check of the provided well-known note against the target account and the + /// block number. + /// + /// This function returns: + /// - `Some` if we can definitively determine whether the note can be consumed not by the target + /// account. + /// - `None` if the consumption status of the note cannot be determined conclusively and further + /// checks are necessary. + pub fn is_consumable( + &self, + note: &Note, + target_account_id: AccountId, + block_ref: BlockNumber, + ) -> Option { + match self.is_consumable_inner(note, target_account_id, block_ref) { + Ok(status) => status, + Err(err) => { + let err: Box = Box::from(err); + Some(NoteConsumptionStatus::NeverConsumable(err)) + }, + } + } + + /// Performs the inputs check of the provided note against the target account and the block + /// number. + /// + /// It performs: + /// - for `P2ID` note: + /// - check that note inputs have correct number of values. + /// - assertion that the account ID provided by the note inputs is equal to the target + /// account ID. + /// - for `P2IDE` note: + /// - check that note inputs have correct number of values. + /// - check that the target account is either the receiver account or the sender account. + /// - check that depending on whether the target account is sender or receiver, it could be + /// either consumed, or consumed after timelock height, or consumed after reclaim height. + fn is_consumable_inner( + &self, + note: &Note, + target_account_id: AccountId, + block_ref: BlockNumber, + ) -> Result, StaticAnalysisError> { + match self { + WellKnownNote::P2ID => { + let input_account_id = parse_p2id_inputs(note.inputs().values())?; + + if input_account_id == target_account_id { + Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization)) + } else { + Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note inputs doesn't match the target account ID".into()))) + } + }, + WellKnownNote::P2IDE => { + let (receiver_account_id, reclaim_height, timelock_height) = + parse_p2ide_inputs(note.inputs().values())?; + + let current_block_height = block_ref.as_u32(); + + // block height after which sender account can consume the note + let consumable_after = reclaim_height.max(timelock_height); + + // handle the case when the target account of the transaction is sender + if target_account_id == note.metadata().sender() { + // For the sender, the current block height needs to have reached both reclaim + // and timelock height to be consumable. + if current_block_height >= consumable_after { + Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization)) + } else { + Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from( + consumable_after, + )))) + } + // handle the case when the target account of the transaction is receiver + } else if target_account_id == receiver_account_id { + // For the receiver, the current block height needs to have reached only the + // timelock height to be consumable: we can ignore the reclaim height in this + // case + if current_block_height >= timelock_height { + Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization)) + } else { + Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from( + timelock_height, + )))) + } + // if the target account is neither the sender nor the receiver (from the note's + // inputs), then this account cannot consume the note + } else { + Ok(Some(NoteConsumptionStatus::NeverConsumable( + "target account of the transaction does not match neither the receiver account specified by the P2IDE inputs, nor the sender account".into() + ))) + } + }, + + // the consumption status of any other note cannot be determined by the static analysis, + // further checks are necessary. + _ => Ok(None), + } + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Returns the receiver account ID parsed from the provided P2ID note inputs. +/// +/// # Errors +/// +/// Returns an error if: +/// - the length of the provided note inputs array is not equal to the expected inputs number of the +/// P2ID note. +/// - first two elements of the note inputs array does not form the valid account ID. +fn parse_p2id_inputs(note_inputs: &[Felt]) -> Result { + if note_inputs.len() != WellKnownNote::P2ID.num_expected_inputs() { + return Err(StaticAnalysisError::new(format!( + "P2ID note should have {} inputs, but {} was provided", + WellKnownNote::P2ID.num_expected_inputs(), + note_inputs.len() + ))); + } + + try_read_account_id_from_inputs(note_inputs) +} + +/// Returns the receiver account ID, reclaim height and timelock height parsed from the provided +/// P2IDE note inputs. +/// +/// # Errors +/// +/// Returns an error if: +/// - the length of the provided note inputs array is not equal to the expected inputs number of the +/// P2IDE note. +/// - first two elements of the note inputs array does not form the valid account ID. +/// - third note inputs array element (reclaim height) is not a valid u32 value. +/// - fourth note inputs array element (timelock height) is not a valid u32 value. +fn parse_p2ide_inputs(note_inputs: &[Felt]) -> Result<(AccountId, u32, u32), StaticAnalysisError> { + if note_inputs.len() != WellKnownNote::P2IDE.num_expected_inputs() { + return Err(StaticAnalysisError::new(format!( + "P2IDE note should have {} inputs, but {} was provided", + WellKnownNote::P2IDE.num_expected_inputs(), + note_inputs.len() + ))); + } + + let receiver_account_id = try_read_account_id_from_inputs(note_inputs)?; + + let reclaim_height = u32::try_from(note_inputs[2]) + .map_err(|_err| StaticAnalysisError::new("reclaim block height should be a u32"))?; + + let timelock_height = u32::try_from(note_inputs[3]) + .map_err(|_err| StaticAnalysisError::new("timelock block height should be a u32"))?; + + Ok((receiver_account_id, reclaim_height, timelock_height)) +} + +/// Reads the account ID from the first two note input values. +/// +/// Returns None if the note input values used to construct the account ID are invalid. +fn try_read_account_id_from_inputs(note_inputs: &[Felt]) -> Result { + if note_inputs.len() < 2 { + return Err(StaticAnalysisError::new(format!( + "P2ID and P2IDE notes should have at least 2 note inputs, but {} was provided", + note_inputs.len() + ))); + } + + AccountId::try_from([note_inputs[1], note_inputs[0]]).map_err(|source| { + StaticAnalysisError::with_source( + "failed to create an account ID from the first two note inputs", + source, + ) + }) +} + +// HELPER STRUCTURES +// ================================================================================================ + +/// Describes if a note could be consumed under a specific conditions: target account state +/// and block height. +/// +/// The status does not account for any authorization that may be required to consume the +/// note, nor does it indicate whether the account has sufficient fees to consume it. +#[derive(Debug)] +pub enum NoteConsumptionStatus { + /// The note can be consumed by the account at the specified block height. + Consumable, + /// The note can be consumed by the account after the required block height is achieved. + ConsumableAfter(BlockNumber), + /// The note can be consumed by the account if proper authorization is provided. + ConsumableWithAuthorization, + /// The note cannot be consumed by the account at the specified conditions (i.e., block + /// height and account state). + UnconsumableConditions, + /// The note cannot be consumed by the specified account under any conditions. + NeverConsumable(Box), +} + +#[derive(thiserror::Error, Debug)] +#[error("{message}")] +struct StaticAnalysisError { + /// Stack size of `Box` is smaller than String. + message: Box, + /// thiserror will return this when calling Error::source on StaticAnalysisError. + source: Option>, +} + +impl StaticAnalysisError { + /// Creates a new static analysis error from an error message. + pub fn new(message: impl Into) -> Self { + let message: String = message.into(); + Self { message: message.into(), source: None } + } + + /// Creates a new static analysis error from an error message and a source error. + pub fn with_source( + message: impl Into, + source: impl Error + Send + Sync + 'static, + ) -> Self { + let message: String = message.into(); + Self { + message: message.into(), + source: Some(Box::new(source)), } } } diff --git a/crates/miden-lib/src/testing/account_component/conditional_auth.rs b/crates/miden-lib/src/testing/account_component/conditional_auth.rs index dfb4c626e7..40e847e26f 100644 --- a/crates/miden-lib/src/testing/account_component/conditional_auth.rs +++ b/crates/miden-lib/src/testing/account_component/conditional_auth.rs @@ -11,11 +11,11 @@ pub const ERR_WRONG_ARGS_MSG: &str = "auth procedure args are incorrect"; static CONDITIONAL_AUTH_CODE: LazyLock = LazyLock::new(|| { format!( r#" - use.miden::account + use.miden::native_account const.WRONG_ARGS="{ERR_WRONG_ARGS_MSG}" - export.auth__conditional + export.auth_conditional # => [AUTH_ARGS] # If [97, 98, 99] is passed as an argument, all good. @@ -26,7 +26,7 @@ static CONDITIONAL_AUTH_CODE: LazyLock = LazyLock::new(|| { # Last element is the incr_nonce_flag. if.true - exec.account::incr_nonce drop + exec.native_account::incr_nonce drop end dropw dropw dropw dropw end @@ -42,7 +42,7 @@ static CONDITIONAL_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { /// Creates a mock authentication [`AccountComponent`] for testing purposes. /// -/// The component defines an `auth__conditional` procedure that conditionally succeeds and +/// The component defines an `auth_conditional` procedure that conditionally succeeds and /// conditionally increments the nonce based on the authentication arguments. /// /// The auth procedure expects the first three arguments as [99, 98, 97] to succeed. diff --git a/crates/miden-lib/src/testing/account_component/incr_nonce.rs b/crates/miden-lib/src/testing/account_component/incr_nonce.rs index 3cc85304fb..1af38a1529 100644 --- a/crates/miden-lib/src/testing/account_component/incr_nonce.rs +++ b/crates/miden-lib/src/testing/account_component/incr_nonce.rs @@ -5,10 +5,10 @@ use miden_objects::utils::sync::LazyLock; use crate::transaction::TransactionKernel; const INCR_NONCE_AUTH_CODE: &str = " - use.miden::account + use.miden::native_account - export.auth__incr_nonce - exec.account::incr_nonce drop + export.auth_incr_nonce + exec.native_account::incr_nonce drop end "; @@ -20,7 +20,7 @@ static INCR_NONCE_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { /// Creates a mock authentication [`AccountComponent`] for testing purposes. /// -/// The component defines an `auth__incr_nonce` procedure that always increments the nonce by 1. +/// The component defines an `auth_incr_nonce` procedure that always increments the nonce by 1. pub struct IncrNonceAuthComponent; impl From for AccountComponent { diff --git a/crates/miden-lib/src/testing/account_interface.rs b/crates/miden-lib/src/testing/account_interface.rs new file mode 100644 index 0000000000..dbac4986eb --- /dev/null +++ b/crates/miden-lib/src/testing/account_interface.rs @@ -0,0 +1,18 @@ +use alloc::vec::Vec; + +use miden_objects::Word; +use miden_objects::account::Account; + +use crate::account::interface::AccountInterface; + +/// Helper function to extract public keys from an account +pub fn get_public_keys_from_account(account: &Account) -> Vec { + let interface: AccountInterface = account.into(); + + interface + .auth() + .iter() + .flat_map(|auth| auth.get_public_key_commitments()) + .map(Word::from) + .collect() +} diff --git a/crates/miden-lib/src/testing/mock_account.rs b/crates/miden-lib/src/testing/mock_account.rs index 01bf53baa6..35fd96e137 100644 --- a/crates/miden-lib/src/testing/mock_account.rs +++ b/crates/miden-lib/src/testing/mock_account.rs @@ -31,9 +31,9 @@ pub trait MockAccountExt { .with_assets(AssetVault::mock().assets()) .build_existing() .expect("account should be valid"); - let (_id, vault, storage, code, nonce) = account.into_parts(); + let (_id, vault, storage, code, nonce, _seed) = account.into_parts(); - Account::from_parts(account_id, vault, storage, code, nonce) + Account::new_existing(account_id, vault, storage, code, nonce) } /// Creates a mock account with fungible faucet storage and the given account ID. @@ -47,12 +47,12 @@ pub trait MockAccountExt { .with_component(MockFaucetComponent) .build_existing() .expect("account should be valid"); - let (_id, vault, mut storage, code, nonce) = account.into_parts(); + 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(); - Account::from_parts(account_id, vault, storage, code, nonce) + Account::new_existing(account_id, vault, storage, code, nonce) } /// Creates a mock account with non-fungible faucet storage and the given account ID. @@ -66,15 +66,15 @@ pub trait MockAccountExt { .with_component(MockFaucetComponent) .build_existing() .expect("account should be valid"); - let (_id, vault, _storage, code, nonce) = account.into_parts(); + let (_id, vault, _storage, code, nonce, _seed) = account.into_parts(); let asset = NonFungibleAsset::mock(&constants::NON_FUNGIBLE_ASSET_DATA_2); let non_fungible_storage_map = - StorageMap::with_entries([(asset.vault_key(), asset.into())]).unwrap(); + StorageMap::with_entries([(asset.vault_key().into(), asset.into())]).unwrap(); let storage = AccountStorage::new(vec![StorageSlot::Map(non_fungible_storage_map)]).unwrap(); - Account::from_parts(account_id, vault, storage, code, nonce) + Account::new_existing(account_id, vault, storage, code, nonce) } } diff --git a/crates/miden-lib/src/testing/mock_account_code.rs b/crates/miden-lib/src/testing/mock_account_code.rs index f3a04e1ba7..50c757ba81 100644 --- a/crates/miden-lib/src/testing/mock_account_code.rs +++ b/crates/miden-lib/src/testing/mock_account_code.rs @@ -24,7 +24,8 @@ const MOCK_FAUCET_CODE: &str = " "; const MOCK_ACCOUNT_CODE: &str = " - use.miden::account + use.miden::active_account + use.miden::native_account use.miden::tx export.::miden::contracts::wallets::basic::receive_asset @@ -37,14 +38,25 @@ const MOCK_ACCOUNT_CODE: &str = " # Stack: [index, VALUE_TO_SET, pad(11)] # Output: [PREVIOUS_STORAGE_VALUE, pad(12)] export.set_item - exec.account::set_item + exec.native_account::set_item # => [V, pad(12)] end # Stack: [index, pad(15)] # Output: [VALUE, pad(12)] export.get_item - exec.account::get_item + exec.active_account::get_item + # => [VALUE, pad(15)] + + # truncate the stack + movup.8 drop movup.8 drop movup.8 drop + # => [VALUE, pad(12)] + end + + # Stack: [index, pad(15)] + # Output: [VALUE, pad(12)] + export.get_initial_item + exec.active_account::get_initial_item # => [VALUE, pad(15)] # truncate the stack @@ -55,20 +67,26 @@ const MOCK_ACCOUNT_CODE: &str = " # Stack: [index, KEY, VALUE, pad(7)] # Output: [OLD_MAP_ROOT, OLD_MAP_VALUE, pad(8)] export.set_map_item - exec.account::set_map_item + exec.native_account::set_map_item # => [R', V, pad(8)] end # Stack: [index, KEY, pad(11)] # Output: [VALUE, pad(12)] export.get_map_item - exec.account::get_map_item + exec.active_account::get_map_item + end + + # Stack: [index, KEY, pad(11)] + # Output: [VALUE, pad(12)] + export.get_initial_map_item + exec.active_account::get_initial_map_item end # Stack: [pad(16)] # Output: [CODE_COMMITMENT, pad(12)] export.get_code_commitment - exec.account::get_code_commitment + exec.active_account::get_code_commitment # => [CODE_COMMITMENT, pad(16)] # truncate the stack @@ -79,7 +97,7 @@ const MOCK_ACCOUNT_CODE: &str = " # Stack: [pad(16)] # Output: [CODE_COMMITMENT, pad(12)] export.compute_storage_commitment - exec.account::compute_storage_commitment + exec.active_account::compute_storage_commitment # => [STORAGE_COMMITMENT, pad(16)] swapw dropw @@ -89,14 +107,14 @@ const MOCK_ACCOUNT_CODE: &str = " # Stack: [ASSET, pad(12)] # Output: [ASSET', pad(12)] export.add_asset - exec.account::add_asset + exec.native_account::add_asset # => [ASSET', pad(12)] end # Stack: [ASSET, pad(12)] # Output: [ASSET, pad(12)] export.remove_asset - exec.account::remove_asset + exec.native_account::remove_asset # => [ASSET, pad(12)] end diff --git a/crates/miden-lib/src/testing/mock_util_lib.rs b/crates/miden-lib/src/testing/mock_util_lib.rs new file mode 100644 index 0000000000..5f6c99cb37 --- /dev/null +++ b/crates/miden-lib/src/testing/mock_util_lib.rs @@ -0,0 +1,50 @@ +use miden_objects::assembly::Library; +use miden_objects::assembly::diagnostics::NamedSource; +use miden_objects::utils::sync::LazyLock; + +use crate::transaction::TransactionKernel; + +const MOCK_UTIL_LIBRARY_CODE: &str = " + use.miden::output_note + + # Inputs: [] + # Outputs: [note_idx] + export.create_random_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)] + + exec.output_note::create + # => [note_idx] + end + + # Inputs: [ASSET] + # Outputs: [] + export.create_random_note_with_asset + exec.create_random_note + # => [note_idx, ASSET] + + movdn.4 + # => [ASSET, note_idx] + + exec.output_note::add_asset + # => [] + end +"; + +static MOCK_UTIL_LIBRARY: LazyLock = LazyLock::new(|| { + let source = NamedSource::new("mock::util", MOCK_UTIL_LIBRARY_CODE); + TransactionKernel::assembler() + .assemble_library([source]) + .expect("mock util library should be valid") +}); + +/// Returns the mock test [`Library`] under the `mock::util` namespace. +/// +/// This provides convenient wrappers for testing purposes. +pub fn mock_util_library() -> Library { + MOCK_UTIL_LIBRARY.clone() +} diff --git a/crates/miden-lib/src/testing/mod.rs b/crates/miden-lib/src/testing/mod.rs index f567f0e456..fa43a14205 100644 --- a/crates/miden-lib/src/testing/mod.rs +++ b/crates/miden-lib/src/testing/mod.rs @@ -1,4 +1,6 @@ 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-lib/src/testing/note.rs index 7805499b86..34c9e372b7 100644 --- a/crates/miden-lib/src/testing/note.rs +++ b/crates/miden-lib/src/testing/note.rs @@ -103,6 +103,12 @@ impl NoteBuilder { self } + /// Overwrites the generated serial number with a custom one. + pub fn serial_number(mut self, serial_number: Word) -> Self { + self.serial_num = serial_number; + self + } + pub fn aux(mut self, aux: Felt) -> Self { self.aux = aux; self diff --git a/crates/miden-lib/src/transaction/events.rs b/crates/miden-lib/src/transaction/events.rs index 2ba78b2ff7..2b3c5edd67 100644 --- a/crates/miden-lib/src/transaction/events.rs +++ b/crates/miden-lib/src/transaction/events.rs @@ -1,79 +1,42 @@ use core::fmt; +pub use miden_core::EventId; + use super::TransactionEventError; // CONSTANTS // ================================================================================================ +// Include the generated event constants +include!(concat!(env!("OUT_DIR"), "/assets/transaction_events.rs")); // TRANSACTION EVENT // ================================================================================================ -const ACCOUNT_VAULT_BEFORE_ADD_ASSET: u32 = 0x2_0000; // 131072 -const ACCOUNT_VAULT_AFTER_ADD_ASSET: u32 = 0x2_0001; // 131073 - -const ACCOUNT_VAULT_BEFORE_REMOVE_ASSET: u32 = 0x2_0002; // 131074 -const ACCOUNT_VAULT_AFTER_REMOVE_ASSET: u32 = 0x2_0003; // 131075 - -const ACCOUNT_STORAGE_BEFORE_SET_ITEM: u32 = 0x2_0004; // 131076 -const ACCOUNT_STORAGE_AFTER_SET_ITEM: u32 = 0x2_0005; // 131077 - -const ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM: u32 = 0x2_0006; // 131078 -const ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM: u32 = 0x2_0007; // 131079 - -const ACCOUNT_BEFORE_INCREMENT_NONCE: u32 = 0x2_0008; // 131080 -const ACCOUNT_AFTER_INCREMENT_NONCE: u32 = 0x2_0009; // 131081 - -const ACCOUNT_PUSH_PROCEDURE_INDEX: u32 = 0x2_000a; // 131082 - -const NOTE_BEFORE_CREATED: u32 = 0x2_000b; // 131083 -const NOTE_AFTER_CREATED: u32 = 0x2_000c; // 131084 - -const NOTE_BEFORE_ADD_ASSET: u32 = 0x2_000d; // 131085 -const NOTE_AFTER_ADD_ASSET: u32 = 0x2_000e; // 131086 - -const AUTH_REQUEST: u32 = 0x2_000f; // 131087 - -const PROLOGUE_START: u32 = 0x2_0010; // 131088 -const PROLOGUE_END: u32 = 0x2_0011; // 131089 - -const NOTES_PROCESSING_START: u32 = 0x2_0012; // 131090 -const NOTES_PROCESSING_END: u32 = 0x2_0013; // 131091 - -const NOTE_EXECUTION_START: u32 = 0x2_0014; // 131092 -const NOTE_EXECUTION_END: u32 = 0x2_0015; // 131093 - -const TX_SCRIPT_PROCESSING_START: u32 = 0x2_0016; // 131094 -const TX_SCRIPT_PROCESSING_END: u32 = 0x2_0017; // 131095 - -const EPILOGUE_START: u32 = 0x2_0018; // 131096 -const EPILOGUE_TX_CYCLES_OBTAINED: u32 = 0x2_0019; // 131097 -const EPILOGUE_TX_FEE_COMPUTED: u32 = 0x2_001a; // 131098 -const EPILOGUE_END: u32 = 0x2_001b; // 131099 - -const LINK_MAP_SET_EVENT: u32 = 0x2_001c; // 131100 -const LINK_MAP_GET_EVENT: u32 = 0x2_001d; // 131101 - -const UNAUTHORIZED_EVENT: u32 = 0x2_001e; // 131102 - /// Events which may be emitted by a transaction kernel. /// -/// The events are emitted via the `emit.` instruction. The event ID is a 32-bit -/// unsigned integer which is used to identify the event type. For events emitted by the -/// transaction kernel, the event_id is structured as follows: -/// - The upper 16 bits of the event ID are set to 2. -/// - The lower 16 bits represent a unique event ID within the transaction kernel. -#[repr(u32)] +/// The events are emitted via the `emit.` instruction. The event ID is a Felt +/// derived from the `EventId` string which is used to identify the event type. Events emitted +/// by the transaction kernel are in the `miden` namespace. +#[repr(u64)] #[derive(Debug, Clone, Eq, PartialEq)] pub enum TransactionEvent { + AccountBeforeForeignLoad = ACCOUNT_BEFORE_FOREIGN_LOAD, + AccountVaultBeforeAddAsset = ACCOUNT_VAULT_BEFORE_ADD_ASSET, AccountVaultAfterAddAsset = ACCOUNT_VAULT_AFTER_ADD_ASSET, AccountVaultBeforeRemoveAsset = ACCOUNT_VAULT_BEFORE_REMOVE_ASSET, AccountVaultAfterRemoveAsset = ACCOUNT_VAULT_AFTER_REMOVE_ASSET, + AccountVaultBeforeGetBalance = ACCOUNT_VAULT_BEFORE_GET_BALANCE, + + AccountVaultBeforeHasNonFungibleAsset = ACCOUNT_VAULT_BEFORE_HAS_NON_FUNGIBLE_ASSET, + AccountStorageBeforeSetItem = ACCOUNT_STORAGE_BEFORE_SET_ITEM, AccountStorageAfterSetItem = ACCOUNT_STORAGE_AFTER_SET_ITEM, + AccountStorageBeforeGetMapItem = ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM, + AccountStorageBeforeSetMapItem = ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM, AccountStorageAfterSetMapItem = ACCOUNT_STORAGE_AFTER_SET_MAP_ITEM, @@ -103,26 +66,32 @@ pub enum TransactionEvent { TxScriptProcessingEnd = TX_SCRIPT_PROCESSING_END, EpilogueStart = EPILOGUE_START, - EpilogueTxCyclesObtained = EPILOGUE_TX_CYCLES_OBTAINED, - EpilogueTxFeeComputed = EPILOGUE_TX_FEE_COMPUTED, EpilogueEnd = EPILOGUE_END, - LinkMapSetEvent = LINK_MAP_SET_EVENT, - LinkMapGetEvent = LINK_MAP_GET_EVENT, + EpilogueAuthProcStart = EPILOGUE_AUTH_PROC_START, + EpilogueAuthProcEnd = EPILOGUE_AUTH_PROC_END, + + EpilogueAfterTxCyclesObtained = EPILOGUE_AFTER_TX_CYCLES_OBTAINED, + EpilogueBeforeTxFeeRemovedFromAccount = EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT, + + LinkMapSet = LINK_MAP_SET, + LinkMapGet = LINK_MAP_GET, - Unauthorized = UNAUTHORIZED_EVENT, + Unauthorized = AUTH_UNAUTHORIZED, } impl TransactionEvent { - /// Value of the top 16 bits of a transaction kernel event ID. - pub const ID_PREFIX: u32 = 2; - /// 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 { let is_unprivileged = matches!(self, Self::AuthRequest | Self::Unauthorized); !is_unprivileged } + + /// Returns the [`EventId`] of the transaction event. + pub fn event_id(&self) -> EventId { + EventId::from_u64(self.clone() as u64) + } } impl fmt::Display for TransactionEvent { @@ -131,15 +100,17 @@ impl fmt::Display for TransactionEvent { } } -impl TryFrom for TransactionEvent { +impl TryFrom for TransactionEvent { type Error = TransactionEventError; - fn try_from(value: u32) -> Result { - if value >> 16 != TransactionEvent::ID_PREFIX { - return Err(TransactionEventError::NotTransactionEvent(value)); - } + fn try_from(event_id: EventId) -> Result { + let raw = event_id.as_felt().as_int(); + + let name = EVENT_NAME_LUT.get(&raw).copied(); + + match raw { + ACCOUNT_BEFORE_FOREIGN_LOAD => Ok(TransactionEvent::AccountBeforeForeignLoad), - match value { ACCOUNT_VAULT_BEFORE_ADD_ASSET => Ok(TransactionEvent::AccountVaultBeforeAddAsset), ACCOUNT_VAULT_AFTER_ADD_ASSET => Ok(TransactionEvent::AccountVaultAfterAddAsset), @@ -148,9 +119,19 @@ impl TryFrom for TransactionEvent { }, ACCOUNT_VAULT_AFTER_REMOVE_ASSET => Ok(TransactionEvent::AccountVaultAfterRemoveAsset), + ACCOUNT_VAULT_BEFORE_GET_BALANCE => Ok(TransactionEvent::AccountVaultBeforeGetBalance), + + ACCOUNT_VAULT_BEFORE_HAS_NON_FUNGIBLE_ASSET => { + Ok(TransactionEvent::AccountVaultBeforeHasNonFungibleAsset) + }, + ACCOUNT_STORAGE_BEFORE_SET_ITEM => Ok(TransactionEvent::AccountStorageBeforeSetItem), ACCOUNT_STORAGE_AFTER_SET_ITEM => Ok(TransactionEvent::AccountStorageAfterSetItem), + ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM => { + Ok(TransactionEvent::AccountStorageBeforeGetMapItem) + }, + ACCOUNT_STORAGE_BEFORE_SET_MAP_ITEM => { Ok(TransactionEvent::AccountStorageBeforeSetMapItem) }, @@ -184,16 +165,22 @@ impl TryFrom for TransactionEvent { TX_SCRIPT_PROCESSING_END => Ok(TransactionEvent::TxScriptProcessingEnd), EPILOGUE_START => Ok(TransactionEvent::EpilogueStart), - EPILOGUE_TX_CYCLES_OBTAINED => Ok(TransactionEvent::EpilogueTxCyclesObtained), - EPILOGUE_TX_FEE_COMPUTED => Ok(TransactionEvent::EpilogueTxFeeComputed), + EPILOGUE_AUTH_PROC_START => Ok(TransactionEvent::EpilogueAuthProcStart), + EPILOGUE_AUTH_PROC_END => Ok(TransactionEvent::EpilogueAuthProcEnd), + EPILOGUE_AFTER_TX_CYCLES_OBTAINED => { + Ok(TransactionEvent::EpilogueAfterTxCyclesObtained) + }, + EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT => { + Ok(TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount) + }, EPILOGUE_END => Ok(TransactionEvent::EpilogueEnd), - LINK_MAP_SET_EVENT => Ok(TransactionEvent::LinkMapSetEvent), - LINK_MAP_GET_EVENT => Ok(TransactionEvent::LinkMapGetEvent), + LINK_MAP_SET => Ok(TransactionEvent::LinkMapSet), + LINK_MAP_GET => Ok(TransactionEvent::LinkMapGet), - UNAUTHORIZED_EVENT => Ok(TransactionEvent::Unauthorized), + AUTH_UNAUTHORIZED => Ok(TransactionEvent::Unauthorized), - _ => Err(TransactionEventError::InvalidTransactionEvent(value)), + _ => Err(TransactionEventError::InvalidTransactionEvent(event_id, name)), } } } diff --git a/crates/miden-lib/src/transaction/inputs.rs b/crates/miden-lib/src/transaction/inputs.rs index b48b37110f..84140838e8 100644 --- a/crates/miden-lib/src/transaction/inputs.rs +++ b/crates/miden-lib/src/transaction/inputs.rs @@ -2,15 +2,12 @@ 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::{ - InputNote, - PartialBlockchain, - TransactionArgs, - TransactionInputs, -}; +use miden_objects::transaction::{AccountInputs, InputNote, PartialBlockchain, TransactionInputs}; use miden_objects::vm::AdviceInputs; -use miden_objects::{EMPTY_WORD, Felt, FieldElement, WORD_SIZE, Word, ZERO}; +use miden_objects::{EMPTY_WORD, Felt, FieldElement, Word, ZERO}; +use miden_processor::AdviceMutation; use thiserror::Error; use super::TransactionKernel; @@ -20,31 +17,24 @@ use super::TransactionKernel; /// Advice inputs wrapper for inputs that are meant to be used exclusively in the transaction /// kernel. -#[derive(Default, Clone, Debug)] +#[derive(Debug, Clone, Default)] pub struct TransactionAdviceInputs(AdviceInputs); impl TransactionAdviceInputs { /// Creates a [`TransactionAdviceInputs`]. /// /// The created advice inputs will be populated with the data required for executing a - /// transaction with the specified inputs and arguments. This includes the initial account, an - /// optional account seed (required for new accounts), and the input note data, including - /// core note data + authentication paths all the way to the root of one of partial - /// blockchain peaks. - pub fn new( - tx_inputs: &TransactionInputs, - tx_args: &TransactionArgs, - ) -> Result { - let mut inputs = TransactionAdviceInputs::default(); - let kernel_version = 0; // TODO: replace with user input + /// transaction with the specified transaction inputs. + pub fn new(tx_inputs: &TransactionInputs) -> Result { + let mut inputs = TransactionAdviceInputs(tx_inputs.advice_inputs().clone()); - inputs.build_stack(tx_inputs, tx_args, kernel_version); - inputs.add_kernel_commitments(kernel_version); + inputs.build_stack(tx_inputs); + inputs.add_kernel_commitment(); inputs.add_partial_blockchain(tx_inputs.blockchain()); - inputs.add_input_notes(tx_inputs, tx_args)?; + inputs.add_input_notes(tx_inputs)?; - // Add the script's MAST forest's advice inputs - if let Some(tx_script) = tx_args.tx_script() { + // Add the script's MAST forest's advice inputs. + if let Some(tx_script) = tx_inputs.tx_args().tx_script() { inputs.extend_map( tx_script .mast() @@ -54,35 +44,32 @@ impl TransactionAdviceInputs { ); } - // --- native account injection --------------------------------------- - - let native_acc = PartialAccount::from(tx_inputs.account()); - inputs.add_account(&native_acc)?; + // Inject native account. + let partial_native_acc = tx_inputs.account(); + inputs.add_account(partial_native_acc)?; - // if a seed was provided, extend the map appropriately - if let Some(seed) = tx_inputs.account_seed() { + // If a seed was provided, extend the map appropriately. + if let Some(seed) = tx_inputs.account().seed() { // ACCOUNT_ID |-> ACCOUNT_SEED - let account_id_key = build_account_id_key(native_acc.id()); + let account_id_key = Self::account_id_map_key(partial_native_acc.id()); inputs.add_map_entry(account_id_key, seed.to_vec()); } - // --- foreign account injection -------------------------------------- - - for foreign_acc in tx_args.foreign_account_inputs() { - inputs.add_account(foreign_acc.account())?; - inputs.add_account_witness(foreign_acc.witness()); - - // for foreign accounts, we need to insert the id to state mapping - // NOTE: keep this in sync with the start_foreign_context kernel procedure - let account_id_key = build_account_id_key(foreign_acc.id()); - let header = AccountHeader::from(foreign_acc.account()); - - // ACCOUNT_ID |-> [ID_AND_NONCE, VAULT_ROOT, STORAGE_COMMITMENT, CODE_COMMITMENT] - inputs.add_map_entry(account_id_key, header.as_elements()); + // if the account is new, insert the storage map entries into the advice provider. + if partial_native_acc.is_new() { + for storage_map in partial_native_acc.storage().maps() { + let map_entries = storage_map + .entries() + .flat_map(|(key, value)| { + value.as_elements().iter().chain(key.as_elements().iter()).copied() + }) + .collect(); + inputs.add_map_entry(storage_map.root(), map_entries); + } } - // any extra user-supplied advice - inputs.extend(tx_args.advice_inputs().clone()); + // Extend with extra user-supplied advice. + inputs.extend(tx_inputs.tx_args().advice_inputs().clone()); Ok(inputs) } @@ -97,6 +84,18 @@ impl TransactionAdviceInputs { self.0 } + /// Consumes self and returns an iterator of [`AdviceMutation`]s in arbitrary order. + pub fn into_advice_mutations(self) -> impl Iterator { + [ + AdviceMutation::ExtendMap { other: self.0.map }, + AdviceMutation::ExtendMerkleStore { + infos: self.0.store.inner_nodes().collect(), + }, + AdviceMutation::ExtendStack { values: self.0.stack }, + ] + .into_iter() + } + // MUTATORS // -------------------------------------------------------------------------------------------- @@ -105,6 +104,27 @@ impl TransactionAdviceInputs { self.0.extend(adv_inputs); } + /// Adds the provided account inputs into the advice inputs. + 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_witness(foreign_acc.witness()); + + // for foreign accounts, we need to insert the id to state mapping + // NOTE: keep this in sync with the account::load_from_advice procedure + let account_id_key = Self::account_id_map_key(foreign_acc.id()); + let header = AccountHeader::from(foreign_acc.account()); + + // 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. /// /// The following data is pushed to the advice stack: @@ -131,12 +151,7 @@ impl TransactionAdviceInputs { /// TX_SCRIPT_ARGS, /// AUTH_ARGS, /// ] - fn build_stack( - &mut self, - tx_inputs: &TransactionInputs, - tx_args: &TransactionArgs, - kernel_version: u8, - ) { + fn build_stack(&mut self, tx_inputs: &TransactionInputs) { let header = tx_inputs.block_header(); // --- block header data (keep in sync with kernel's process_block_data) -- @@ -162,9 +177,6 @@ impl TransactionAdviceInputs { self.extend_stack([ZERO, ZERO, ZERO, ZERO]); self.extend_stack(header.note_root()); - // --- kernel version (keep in sync with process_kernel_data) --------- - self.extend_stack([Felt::from(kernel_version)]); - // --- core account items (keep in sync with process_account_data) ---- let account = tx_inputs.account(); self.extend_stack([ @@ -179,6 +191,7 @@ impl TransactionAdviceInputs { // --- number of notes, script root and args -------------------------- self.extend_stack([Felt::from(tx_inputs.input_notes().num_notes())]); + let tx_args = tx_inputs.tx_args(); self.extend_stack(tx_args.tx_script().map_or(Word::empty(), |script| script.root())); self.extend_stack(tx_args.tx_script_args()); @@ -217,27 +230,13 @@ impl TransactionAdviceInputs { // KERNEL INJECTIONS // -------------------------------------------------------------------------------------------- - /// Inserts kernel commitments and hashes of their procedures into the advice inputs. + /// Inserts the kernel commitment and its procedure roots into the advice map. /// /// Inserts the following entries into the advice map: - /// - The accumulative hash of all kernels |-> array of each kernel commitment. - /// - The hash of the selected kernel |-> array of the kernel's procedure roots. - fn add_kernel_commitments(&mut self, kernel_version: u8) { - const NUM_KERNELS: usize = TransactionKernel::NUM_VERSIONS; - - // insert kernels root with kernel commitments into the advice map - let mut kernel_commitments: Vec = Vec::with_capacity(NUM_KERNELS * WORD_SIZE); - for version in 0..NUM_KERNELS { - let kernel_commitment = TransactionKernel::commitment(version as u8); - kernel_commitments.extend_from_slice(kernel_commitment.as_elements()); - } - self.add_map_entry(TransactionKernel::kernel_commitment(), kernel_commitments); - - // insert the selected kernel commitment with its procedure roots into the advice map - self.add_map_entry( - TransactionKernel::commitment(kernel_version), - TransactionKernel::procedures_as_elements(kernel_version), - ); + /// - The commitment of the kernel |-> array of the kernel's procedure roots. + fn add_kernel_commitment(&mut self) { + // insert the kernel commitment with its procedure roots into the advice map + self.add_map_entry(TransactionKernel.to_commitment(), TransactionKernel.to_elements()); } // ACCOUNT INJECTION @@ -332,7 +331,6 @@ impl TransactionAdviceInputs { fn add_input_notes( &mut self, tx_inputs: &TransactionInputs, - tx_args: &TransactionArgs, ) -> Result<(), TransactionAdviceMapMismatch> { if tx_inputs.input_notes().is_empty() { return Ok(()); @@ -343,13 +341,10 @@ impl TransactionAdviceInputs { let note = input_note.note(); let assets = note.assets(); let recipient = note.recipient(); - let note_arg = tx_args.get_note_args(note.id()).unwrap_or(&EMPTY_WORD); + let note_arg = tx_inputs.tx_args().get_note_args(note.id()).unwrap_or(&EMPTY_WORD); // recipient inputs / assets commitments - self.add_map_entry( - recipient.inputs().commitment(), - recipient.inputs().format_for_advice(), - ); + self.add_map_entry(recipient.inputs().commitment(), recipient.inputs().to_elements()); self.add_map_entry(assets.commitment(), assets.to_padded_assets()); // note details / metadata @@ -403,6 +398,7 @@ impl TransactionAdviceInputs { } self.add_map_entry(tx_inputs.input_notes().commitment(), note_data); + Ok(()) } @@ -428,6 +424,13 @@ impl TransactionAdviceInputs { 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 @@ -445,13 +448,6 @@ impl From for TransactionAdviceInputs { } } -// HELPER FUNCTIONS -// ================================================================================================ - -fn build_account_id_key(id: AccountId) -> Word { - Word::from([id.suffix(), id.prefix().as_felt(), ZERO, ZERO]) -} - // CONFLICT ERROR // ================================================================================================ diff --git a/crates/miden-lib/src/transaction/kernel_procedures.rs b/crates/miden-lib/src/transaction/kernel_procedures.rs new file mode 100644 index 0000000000..59f5573b31 --- /dev/null +++ b/crates/miden-lib/src/transaction/kernel_procedures.rs @@ -0,0 +1,114 @@ +// 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; 52] = [ + // account_get_initial_commitment + word!("0x1c95a0386ebf3645c6271253a4ae49ea4be8610dea7b4436c58951277a75f0c1"), + // account_compute_commitment + word!("0x1aed40e2cc4d3798448f4efdce1a14c9598611da065eebe58432f144c3bca9de"), + // account_get_id + word!("0xd76288f2e94b9e6a8f7eeee45c4ee0a23997d78496f6132e3f55681efea809c4"), + // account_get_nonce + word!("0x4a1f11db21ddb1f0ebf7c9fd244f896a95e99bb136008185da3e7d6aa85827a3"), + // account_incr_nonce + word!("0x99c6b16e86eb9eae02657256b8859e2809acd05bf25810933cf50e72d876d8bf"), + // account_get_code_commitment + word!("0xb2ebd0acc4ef40d37c403190dc07d03d7df9169fb8752e8025e3fe469b5ee192"), + // account_get_initial_storage_commitment + word!("0x91377c2852feb7a2798e54d7dbaa2d97000270ec4c0d0888b26d720a25ae0e84"), + // account_compute_storage_commitment + word!("0xa87008550383e1a88dde5d0adefc68ee3bf477aec07e4700f9101241aa1e868f"), + // account_get_item + word!("0xe1e6843fb47f24476a12ef8cd19dd5de2dd74b90433051b26720dce5ab223bf0"), + // account_get_initial_item + word!("0x5e956c876cd6eaaa15f5800a5232c6b4e3e50e0335a31ba2e4a9e5f2401aece4"), + // account_set_item + word!("0x84b5206c5a0dccf56568bc0157b8322e8a506332bc212f1ad35bab4fe9f6bfed"), + // account_get_map_item + word!("0xeb40115d6ca9bed0ac817b246e3d247bb3386e56ee17692d30a3be799f98f2f6"), + // account_get_initial_map_item + word!("0x054f48624a30f260269cc9d780e9d84b77091e29f1d44c6450ab73e0eb91cc39"), + // account_set_map_item + word!("0x33ab43462ceb87f00c5fbebd47cdf948246db0e82b8e85c98edc1b568b784ba3"), + // account_get_initial_vault_root + word!("0x46297d9ac95afd60c7ef1a065e024ad49aa4c019f6b3924191905449b244d4ec"), + // account_get_vault_root + word!("0x42a2bfb8eac4fce9bbf75ea15215b00729faeeaf7fff784692948d3f618a9bb7"), + // account_add_asset + word!("0x11890564573ff2ad0916b1a8ca12a9d2546f4c3dcf09fa4bfb6d72d701a3d7ff"), + // account_remove_asset + word!("0xcb3118521acfe552030b988439713717fdee54d9e7cb54116d4034cf89ce8a34"), + // account_get_balance + word!("0x1ed792cc7775aa1ce2f32367a3d430561ec9bceb33f5bb222691c49a6bde8112"), + // account_get_initial_balance + word!("0xdc1320d6f044c40d37e5e835a584b643d6e77e3fc1136f498815298e28c912b8"), + // account_has_non_fungible_asset + word!("0xfaad11de0c026551df15231790c2364cc598e891444bf826da01b524b1a8ca8f"), + // account_compute_delta_commitment + word!("0x88a66fd1da5df01c98d02c7d76cd567de9a17c6641c045fcd9b05881e990826a"), + // account_get_num_procedures + word!("0x53b5ec38b7841948762c258010e6e07ad93963bcaac2d83813f8edb6710dc720"), + // account_get_procedure_root + word!("0x4d7b2e6083820088cd1139ed658b631cf391989b16de8af6741d7e17de9245cd"), + // account_was_procedure_called + word!("0x37f1f8e67ec9105153721ad59c1f765f58da8f87b0b8eea303701bf4583a5b89"), + // account_has_procedure + word!("0x667d5ce1b7a54c3b8965666ce90e59085c97775b82eba25dddfe218db5fe137d"), + // faucet_mint_asset + word!("0xfc1923fc651f8122a2f32fce96711203bd8309180ce6f484ea6fd4ceb175e3f0"), + // faucet_burn_asset + word!("0x2633226e8831cfb1e9970ae2ee79b83678538f2c6656a755c707ba85cd462562"), + // faucet_get_total_fungible_asset_issuance + word!("0x7d32952d4dc0edd0311e3424b8128df2d48cf949f800c28218fbc851a8db42b5"), + // faucet_is_non_fungible_asset_issued + word!("0x0323d18fe6bd7bbada87d265d2ffb27f2a7828b1b6865d6d1a3b4a64924a9f7f"), + // input_note_get_metadata + word!("0x7ad3e94585e7a397ee27443c98b376ed8d4ba762122af6413fde9314c00a6219"), + // input_note_get_assets_info + word!("0x159439fe48dbc11e674c5d05830d0408dcfa033c26e85e01256002c6cbc07e9a"), + // input_note_get_script_root + word!("0x527036257e58c3a84cf0aa170fb3f219a4553db17d269279355ad164a2b90ac5"), + // input_note_get_inputs_info + word!("0xdd8bbf4cdb48051da346bc89760b77fdf4c948904276a99d96409922a00bd322"), + // input_note_get_serial_number + word!("0x25815e02b7976d8e5c297dde60d372cc142c81f702f424ac0920190528c547ee"), + // input_note_get_recipient + word!("0xd3c255177f9243bb1a523a87615bbe76dd5a3605fcae87eb9d3a626d4ecce33c"), + // output_note_create + word!("0x6562a9d1d8605d158415a4dcd4c8fd48fc2b6c21ec64c188db9def16b991a560"), + // output_note_get_metadata + word!("0xde4a5b57f9d53692459383e6cf6302ef3602a348896ed6ab6fdf67e07fa483ff"), + // output_note_get_assets_info + word!("0x7e5d726b5f25f6cfd533bd0294853f3fceea62c41e5f2fd68919d8d53a48b3f8"), + // output_note_get_recipient + word!("0xc824115ed79a2e1670daed8c18fba1bc15f54c5ec0ec6699de69a00b21d9df92"), + // output_note_add_asset + word!("0x9b6929d1ce24b3a97c6fb098f2fa8d0958beb15f91e268b9c787194b0a977a0d"), + // tx_get_num_input_notes + word!("0xfcc186d4b65c584f3126dda1460b01eef977efd76f9e36f972554af28e33c685"), + // tx_get_input_notes_commitment + word!("0xc3a334434daa7d4ea15e1b2cb1a8000ad757f9348560a7246336662b77b0d89a"), + // tx_get_num_output_notes + word!("0x2511fca9c078cd96e526fd488d1362cbfd597eb3db8452aedb00beffee9782b4"), + // tx_get_output_notes_commitment + word!("0xd5b22dae48ec4b20ed479f2c43573d34930720886371ef6b484310a3bea4e818"), + // tx_get_block_commitment + word!("0xe474b491a64d222397fcf83ee5db7b048061988e5e83ce99b91bae6fd75a3522"), + // tx_get_block_number + word!("0x297797dff54b8108dd2df254b95d43895d3f917ab10399efc62adaf861c905ae"), + // tx_get_block_timestamp + word!("0x7903185b847517debb6c2072364e3e757b99ee623e97c2bd0a4661316c5c5418"), + // tx_start_foreign_context + word!("0x4bfde60ab4b1e42148ceea2845ecf9aae061a577972baf348379701760d476d7"), + // tx_end_foreign_context + word!("0xaa0018aa8da890b73511879487f65553753fb7df22de380dd84c11e6f77eec6f"), + // tx_get_expiration_delta + word!("0xa60286e820a755128b2269db5057b0e2d9b79fef6f813bf3fe3337553a8fbb53"), + // tx_update_expiration_block_delta + word!("0xa16440a9a8cd2a6d0ff7f5c3bcce2958976e5d3e6e8a6935ff40ae1863c324f0"), +]; diff --git a/crates/miden-lib/src/transaction/memory.rs b/crates/miden-lib/src/transaction/memory.rs index f00fc1844f..4fbb83178b 100644 --- a/crates/miden-lib/src/transaction/memory.rs +++ b/crates/miden-lib/src/transaction/memory.rs @@ -32,21 +32,21 @@ pub type StorageSlot = u8; // // Here the "end pointer" is the last memory pointer occupied by the current data // -// | 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 root | 8 (2) | 11 (2) | | -// | Code root | 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) | | +// | 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) | | // Relative layout of the native account's delta. // @@ -73,8 +73,8 @@ pub const FAUCET_STORAGE_DATA_SLOT: StorageSlot = 0; // BOOKKEEPING // ------------------------------------------------------------------------------------------------ -/// The memory address at which a pointer to the input note being executed is stored. -pub const CURRENT_INPUT_NOTE_PTR: MemoryAddress = 0; +/// The memory address at which a pointer to the currently active input note is stored. +pub const ACTIVE_INPUT_NOTE_PTR: MemoryAddress = 0; /// The memory address at which the number of output notes is stored. pub const NUM_OUTPUT_NOTES_PTR: MemoryAddress = 4; @@ -96,7 +96,7 @@ pub const NATIVE_ACCT_STORAGE_COMMITMENT_DIRTY_FLAG_PTR: MemoryAddress = 16; pub const TX_EXPIRATION_BLOCK_NUM_PTR: MemoryAddress = 20; /// The memory address at which the pointer to the stack element containing the pointer to the -/// currently active account data is stored. +/// active account data is stored. /// /// The stack starts at the address `29`. Stack has a length of `64` elements meaning that the /// maximum depth of FPI calls is `63` — the first slot is always occupied by the native account @@ -215,7 +215,7 @@ pub const PARTIAL_BLOCKCHAIN_PEAKS_PTR: MemoryAddress = 1204; // KERNEL DATA // ------------------------------------------------------------------------------------------------ -/// The memory address at which the number of the procedures of the selected kernel is stored. +/// The memory address at which the number of the kernel procedures is stored. pub const NUM_KERNEL_PROCEDURES_PTR: MemoryAddress = 1600; /// The memory address at which the section, where the hashes of the kernel procedures are stored, @@ -415,7 +415,7 @@ pub const INPUT_NOTE_ASSETS_OFFSET: MemoryOffset = 40; // // Dirty flag is set to 0 after every recomputation of the assets commitment in the // `kernel::note::compute_output_note_assets_commitment` procedure. It is set to 1 in the -// `kernel::tx::add_asset_to_note` procedure after any change was made to the assets data . +// `kernel::output_note::add_asset` procedure after any change was made to the assets data. /// The memory address at which the output notes section begins. pub const OUTPUT_NOTE_SECTION_OFFSET: MemoryOffset = 16_777_216; @@ -449,11 +449,11 @@ pub const LINK_MAP_USED_MEMORY_SIZE: MemoryAddress = 33_554_432; pub const LINK_MAP_ENTRY_SIZE: MemoryOffset = 16; const _: () = assert!( - LINK_MAP_REGION_START_PTR % LINK_MAP_ENTRY_SIZE == 0, + LINK_MAP_REGION_START_PTR.is_multiple_of(LINK_MAP_ENTRY_SIZE), "link map region start ptr should be aligned to entry size" ); const _: () = assert!( - (LINK_MAP_REGION_END_PTR - LINK_MAP_REGION_START_PTR) % LINK_MAP_ENTRY_SIZE == 0, + (LINK_MAP_REGION_END_PTR - LINK_MAP_REGION_START_PTR).is_multiple_of(LINK_MAP_ENTRY_SIZE), "the link map memory range should cleanly contain a multiple of the entry size" ); diff --git a/crates/miden-lib/src/transaction/mod.rs b/crates/miden-lib/src/transaction/mod.rs index eeed669290..54c55744cc 100644 --- a/crates/miden-lib/src/transaction/mod.rs +++ b/crates/miden-lib/src/transaction/mod.rs @@ -9,13 +9,8 @@ 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::transaction::{ - OutputNote, - OutputNotes, - TransactionArgs, - TransactionInputs, - TransactionOutputs, -}; +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}; @@ -27,7 +22,7 @@ use super::MidenLib; pub mod memory; mod events; -pub use events::TransactionEvent; +pub use events::{EventId, TransactionEvent}; mod inputs; pub use inputs::{TransactionAdviceInputs, TransactionAdviceMapMismatch}; @@ -41,13 +36,10 @@ pub use outputs::{ parse_final_account_header, }; -pub use crate::errors::{ - TransactionEventError, - TransactionKernelError, - TransactionTraceParsingError, -}; +pub use crate::errors::{TransactionEventError, TransactionTraceParsingError}; -mod procedures; +mod kernel_procedures; +use kernel_procedures::KERNEL_PROCEDURES; // CONSTANTS // ================================================================================================ @@ -82,6 +74,12 @@ static TX_SCRIPT_MAIN: LazyLock = LazyLock::new(|| { pub struct TransactionKernel; impl TransactionKernel { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Array of kernel procedures. + pub const PROCEDURES: &'static [Word] = &KERNEL_PROCEDURES; + // KERNEL SOURCE CODE // -------------------------------------------------------------------------------------------- @@ -121,29 +119,22 @@ impl TransactionKernel { ProgramInfo::new(program_hash, kernel) } - /// Transforms the provided [TransactionInputs] and [TransactionArgs] into stack and advice + /// Transforms the provided [`TransactionInputs`] into stack and advice /// inputs needed to execute a transaction kernel for a specific transaction. - /// - /// If `init_advice_inputs` is provided, they will be included in the returned advice inputs. pub fn prepare_inputs( tx_inputs: &TransactionInputs, - tx_args: &TransactionArgs, - init_advice_inputs: Option, ) -> Result<(StackInputs, TransactionAdviceInputs), TransactionAdviceMapMismatch> { let account = tx_inputs.account(); let stack_inputs = TransactionKernel::build_input_stack( account.id(), - account.init_commitment(), + account.initial_commitment(), tx_inputs.input_notes().commitment(), tx_inputs.block_header().commitment(), tx_inputs.block_header().block_num(), ); - let mut tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs, tx_args)?; - if let Some(init_advice_inputs) = init_advice_inputs { - tx_advice_inputs.extend(init_advice_inputs); - } + let tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs)?; Ok((stack_inputs, tx_advice_inputs)) } @@ -196,7 +187,7 @@ impl TransactionKernel { /// - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`. pub fn build_input_stack( account_id: AccountId, - init_account_commitment: Word, + initial_account_commitment: Word, input_notes_commitment: Word, block_commitment: Word, block_num: BlockNumber, @@ -207,7 +198,7 @@ impl TransactionKernel { inputs.push(account_id.suffix()); inputs.push(account_id.prefix().as_felt()); inputs.extend(input_notes_commitment); - inputs.extend_from_slice(init_account_commitment.as_elements()); + inputs.extend_from_slice(initial_account_commitment.as_elements()); inputs.extend_from_slice(block_commitment.as_elements()); StackInputs::new(inputs) .map_err(|e| e.to_string()) @@ -280,18 +271,18 @@ impl TransactionKernel { /// - Indices 13..16 on the stack are not zeroes. /// - Overflow addresses are not empty. pub fn parse_output_stack( - stack: &StackOutputs, + 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(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4) + .get_stack_word_be(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4) .expect("output_notes_commitment (first word) missing"); let account_update_commitment = stack - .get_stack_word(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4) + .get_stack_word_be(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4) .expect("account_update_commitment (second word) missing"); let fee = stack - .get_stack_word(FEE_ASSET_WORD_IDX * 4) + .get_stack_word_be(FEE_ASSET_WORD_IDX * 4) .expect("fee_asset (third word) missing"); let expiration_block_num = stack @@ -308,7 +299,7 @@ impl TransactionKernel { // Make sure that indices 13, 14 and 15 are zeroes (i.e. the fourth word without the // expiration block number). - if stack.get_stack_word(12).expect("fourth word missing").as_elements()[..3] + if stack.get_stack_word_be(12).expect("fourth word missing").as_elements()[..3] != Word::empty().as_elements()[..3] { return Err(TransactionOutputError::OutputStackInvalid( @@ -432,6 +423,14 @@ impl TransactionKernel { Ok((final_account_commitment, account_delta_commitment)) } + + // UTILITY METHODS + // -------------------------------------------------------------------------------------------- + + /// Computes the sequential hash of all kernel procedures. + pub fn to_commitment(&self) -> Word { + ::to_commitment(self) + } } #[cfg(any(feature = "testing", test))] @@ -468,16 +467,20 @@ impl TransactionKernel { .with_debug_mode(true) } - /// Returns an [`Assembler`] with the mock account and faucet libraries. + /// Returns an [`Assembler`] with the `mock::{account, faucet, util}` libraries. /// /// This assembler is the same as [`TransactionKernel::with_kernel_library`] but additionally - /// includes the [`MockAccountCodeExt::mock_account_library`][account_lib] - /// and [`MockAccountCodeExt::mock_faucet_library`][faucet_lib], which are the standard - /// testing account libraries. + /// 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() { @@ -487,6 +490,19 @@ impl TransactionKernel { } assembler + .link_static_library(mock_util_library()) + .expect("failed to add mock test library"); + + assembler + } +} + +impl SequentialCommit for TransactionKernel { + type Commitment = Word; + + /// Returns kernel procedures as vector of Felts. + fn to_elements(&self) -> Vec { + Word::words_as_elements(Self::PROCEDURES).to_vec() } } diff --git a/crates/miden-lib/src/transaction/procedures/kernel_v0.rs b/crates/miden-lib/src/transaction/procedures/kernel_v0.rs deleted file mode 100644 index b2a5491c5a..0000000000 --- a/crates/miden-lib/src/transaction/procedures/kernel_v0.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! This file is generated by build.rs, do not modify - -use miden_objects::{word, Word}; - -// KERNEL V0 PROCEDURES -// ================================================================================================ - -/// Hashes of all dynamically executed procedures from the kernel 0. -pub const KERNEL0_PROCEDURES: [Word; 48] = [ - // account_get_initial_commitment - word!("0x920898348bacd6d98a399301eb308478fd32b32eab019a5a6ef7a6b44abb61f6"), - // account_compute_current_commitment - word!("0x1aed40e2cc4d3798448f4efdce1a14c9598611da065eebe58432f144c3bca9de"), - // account_get_id - word!("0xc9f7e71b294e16d7a297ba283afb2f8c864817e40e73b6ef1d64efc310937fc7"), - // account_get_nonce - word!("0x4a1f11db21ddb1f0ebf7c9fd244f896a95e99bb136008185da3e7d6aa85827a3"), - // account_incr_nonce - word!("0x72f4595fd7030542ab303c77be42962671948ef18ffeda49b0e88a374f0969f6"), - // account_get_code_commitment - word!("0x02e55aa37f40207bc2a3882383d4c0f1f6633b5f3ea5b7ef814d827632aa7ae8"), - // account_get_initial_storage_commitment - word!("0x5932cb0394cc85330e65e4c0325bb869ec1d08b295c310f7375076a98b143d57"), - // account_compute_storage_commitment - word!("0x2e508f8505188ce9b6c46be727e2c1a237806a19402486e38105665b87191526"), - // account_get_item - word!("0x011e508cf9b261c33e5f3da1afbf23caefa1f6bc7eac9de2cd123c77fa74f02a"), - // account_set_item - word!("0xd2232daa3895669f2bb34af764504d72432fb119eb0be0ce07481290c8701af8"), - // account_get_map_item - word!("0x5a2678ee09bf93cb284b4539345c8eb601614fa60d75259d6502ca19a4acc921"), - // account_set_map_item - word!("0x9fff2a2b67d045ec5baa1db7f63a1f1670d6fc0015a1dd2cfb47d040c25ca55e"), - // account_get_initial_vault_root - word!("0x4d4d91079aaacad1bc86b29a0d61d25508ccb705c29d1b1357016f7373bf299e"), - // account_get_vault_root - word!("0x42a2bfb8eac4fce9bbf75ea15215b00729faeeaf7fff784692948d3f618a9bb7"), - // account_add_asset - word!("0xf0d6be80cdd2a62cab2b4ad5606dde01af1968e3ab3357dcb63fe36aea7f8b80"), - // account_remove_asset - word!("0x7a8f7b23afe3d6264686c4895f78744fbccda957e901247c8f3585427d470039"), - // account_get_balance - word!("0xb4e92ae0196ca128a451e40dd8a5ff56c13919efa67f63dca488214fbba3ffbc"), - // account_has_non_fungible_asset - word!("0x9c0f7851d3211ff4744393b673ce4e1aeb05525dc9186a218c8ff8d6f1a04ee7"), - // account_compute_delta_commitment - word!("0x95dab713c8f9fe01a4c81d1fb57737ac6a45171659456dbc03df049654db3d78"), - // account_was_procedure_called - word!("0x84c8c518a005605619909976ce54c41d6a88505e815421ff4b5516d0285b28bf"), - // faucet_mint_asset - word!("0x68ba9ead0f07cd4012c8a4282bab32b12d74ed5443eb734541166f6fe0b063a8"), - // faucet_burn_asset - word!("0x9c44cd37081368b335b2c3f9ab4629c57bd13d12a38df75ab87a42d9d72886f9"), - // faucet_get_total_fungible_asset_issuance - word!("0x7d32952d4dc0edd0311e3424b8128df2d48cf949f800c28218fbc851a8db42b5"), - // faucet_is_non_fungible_asset_issued - word!("0xc827d2430763c70880216804d3523c14e70e5cf926d5d8a72844c6476add5ef7"), - // note_get_assets_info - word!("0x13a1496a239fa95b3b376c53d899ce2d495e956cdae0f121666408462784673a"), - // note_add_asset - word!("0x47673b932aac8c186cb0979bbc3c4c2afa00fa1b80c0afb5e5efb4924bba48d9"), - // note_get_serial_number - word!("0x6989241a99d9aa1630daae03fc55ecac269e184d6b455c7c0bc996d15ef7f9a8"), - // note_get_inputs_commitment_and_len - word!("0x6ae9f25739a4368330c40e9bd21e5beed4583656443877f3f59cad5040decee1"), - // note_get_sender - word!("0x125832eac0e511f2d86c508b5fa79b7ea6a302c4200222c7f58e1832260bde8f"), - // note_get_script_root - word!("0x317c3f724d57093c98927f7820dc00f5cf3509d2de4306dccaef4e2266fcd5b6"), - // input_note_get_assets_info - word!("0x0816c7215676487f3ce03d372bf5512afb528ab62466074921829d7d2974147c"), - // output_note_get_assets_info - word!("0x71d0e246d52c4d896e6508564207e049d4d68da187a143fe95bf5e7f5602f967"), - // input_note_get_recipient - word!("0x1e612cf8aa3cca674363d9e1fb15c666a0cf2febc80bf9d7800920941133a3f4"), - // output_note_get_recipient - word!("0xc824115ed79a2e1670daed8c18fba1bc15f54c5ec0ec6699de69a00b21d9df92"), - // input_note_get_metadata - word!("0x4b0c2a8560a007abadd55013c3b3c620b2de2189c08109500ba46a4222b37d89"), - // output_note_get_metadata - word!("0xde4a5b57f9d53692459383e6cf6302ef3602a348896ed6ab6fdf67e07fa483ff"), - // tx_create_note - word!("0x52b37f8b25e26517f22f1f600acae7fbfffa84094595ba961af2af807a484736"), - // tx_get_input_notes_commitment - word!("0xc3a334434daa7d4ea15e1b2cb1a8000ad757f9348560a7246336662b77b0d89a"), - // tx_get_num_input_notes - word!("0xfcc186d4b65c584f3126dda1460b01eef977efd76f9e36f972554af28e33c685"), - // tx_get_output_notes_commitment - word!("0xd5b22dae48ec4b20ed479f2c43573d34930720886371ef6b484310a3bea4e818"), - // tx_get_num_output_notes - word!("0x2511fca9c078cd96e526fd488d1362cbfd597eb3db8452aedb00beffee9782b4"), - // tx_get_block_commitment - word!("0xe474b491a64d222397fcf83ee5db7b048061988e5e83ce99b91bae6fd75a3522"), - // tx_get_block_number - word!("0x297797dff54b8108dd2df254b95d43895d3f917ab10399efc62adaf861c905ae"), - // tx_get_block_timestamp - word!("0x7903185b847517debb6c2072364e3e757b99ee623e97c2bd0a4661316c5c5418"), - // tx_start_foreign_context - word!("0x447889987e3a8b589b7852c32752df4e2a855e67076496195d40364c0b0730b0"), - // tx_end_foreign_context - word!("0xaa0018aa8da890b73511879487f65553753fb7df22de380dd84c11e6f77eec6f"), - // tx_get_expiration_delta - word!("0xa60286e820a755128b2269db5057b0e2d9b79fef6f813bf3fe3337553a8fbb53"), - // tx_update_expiration_block_num - word!("0xa16440a9a8cd2a6d0ff7f5c3bcce2958976e5d3e6e8a6935ff40ae1863c324f0"), -]; diff --git a/crates/miden-lib/src/transaction/procedures/mod.rs b/crates/miden-lib/src/transaction/procedures/mod.rs deleted file mode 100644 index fd657d57e7..0000000000 --- a/crates/miden-lib/src/transaction/procedures/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -use alloc::vec::Vec; - -use kernel_v0::KERNEL0_PROCEDURES; -use miden_objects::{Felt, Hasher, Word}; - -use super::TransactionKernel; - -// Include kernel v0 procedure roots generated in build.rs -#[rustfmt::skip] -mod kernel_v0; - -// TRANSACTION KERNEL -// ================================================================================================ - -impl TransactionKernel { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// Number of currently used kernel versions. - pub const NUM_VERSIONS: usize = 1; - - /// Array of all available kernels. - pub const PROCEDURES: [&'static [Word]; Self::NUM_VERSIONS] = [&KERNEL0_PROCEDURES]; - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns procedures of the kernel specified by the `kernel_version` as vector of Felts. - pub fn procedures_as_elements(kernel_version: u8) -> Vec { - Word::words_as_elements( - Self::PROCEDURES - .get(kernel_version as usize) - .expect("provided kernel index is out of bounds"), - ) - .to_vec() - } - - /// Computes the accumulative hash of all procedures of the kernel specified by the - /// `kernel_version`. - pub fn commitment(kernel_version: u8) -> Word { - Hasher::hash_elements(&Self::procedures_as_elements(kernel_version)) - } - - /// Computes a hash from all kernel commitments. - pub fn kernel_commitment() -> Word { - Hasher::hash_elements(&[Self::commitment(0).as_elements()].concat()) - } -} diff --git a/crates/miden-lib/src/utils/script_builder.rs b/crates/miden-lib/src/utils/script_builder.rs index 6ee8badf83..e2df108f5e 100644 --- a/crates/miden-lib/src/utils/script_builder.rs +++ b/crates/miden-lib/src/utils/script_builder.rs @@ -294,23 +294,27 @@ impl ScriptBuilder { // TESTING CONVENIENCE FUNCTIONS // -------------------------------------------------------------------------------------------- - /// Returns a [`ScriptBuilder`] with the mock account and faucet libraries. + /// Returns a [`ScriptBuilder`] with the `mock::{account, faucet, util}` libraries. /// - /// This script builder includes the [`MockAccountCodeExt::mock_account_library`][account_lib] - /// and [`MockAccountCodeExt::mock_faucet_library`][faucet_lib], which are the standard - /// testing account libraries. + /// This script builder 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 #[cfg(any(feature = "testing", test))] pub fn with_mock_libraries() -> Result { use miden_objects::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_dynamically_linked_library(&AccountCode::mock_faucet_library())? + .with_statically_linked_library(&mock_util_library()) } } @@ -348,20 +352,23 @@ mod tests { fn test_create_library_and_create_tx_script() -> anyhow::Result<()> { let script_code = " use.external_contract::counter_contract + begin call.counter_contract::increment end "; let account_code = " - use.miden::account + use.miden::active_account + use.miden::native_account use.std::sys + export.increment push.0 - exec.account::get_item + exec.active_account::get_item push.1 add push.0 - exec.account::set_item + exec.native_account::set_item exec.sys::truncate_stack end "; @@ -383,20 +390,23 @@ mod tests { fn test_compile_library_and_add_to_builder() -> anyhow::Result<()> { let script_code = " use.external_contract::counter_contract + begin call.counter_contract::increment end "; let account_code = " - use.miden::account + use.miden::active_account + use.miden::native_account use.std::sys + export.increment push.0 - exec.account::get_item + exec.active_account::get_item push.1 add push.0 - exec.account::set_item + exec.native_account::set_item exec.sys::truncate_stack end "; @@ -431,20 +441,23 @@ mod tests { fn test_builder_style_chaining() -> anyhow::Result<()> { let script_code = " use.external_contract::counter_contract + begin call.counter_contract::increment end "; let account_code = " - use.miden::account + use.miden::active_account + use.miden::native_account use.std::sys + export.increment push.0 - exec.account::get_item + exec.active_account::get_item push.1 add push.0 - exec.account::set_item + exec.native_account::set_item exec.sys::truncate_stack end "; @@ -489,27 +502,31 @@ mod tests { "; let account_code_1 = " - use.miden::account + use.miden::active_account + use.miden::native_account use.std::sys + export.increment_1 push.0 - exec.account::get_item + exec.active_account::get_item push.1 add push.0 - exec.account::set_item + exec.native_account::set_item exec.sys::truncate_stack end "; let account_code_2 = " - use.miden::account + use.miden::active_account + use.miden::native_account use.std::sys + export.increment_2 push.0 - exec.account::get_item + exec.active_account::get_item push.2 add push.0 - exec.account::set_item + exec.native_account::set_item exec.sys::truncate_stack end "; diff --git a/crates/miden-objects/Cargo.toml b/crates/miden-objects/Cargo.toml index 186bc029e0..445dfea655 100644 --- a/crates/miden-objects/Cargo.toml +++ b/crates/miden-objects/Cargo.toml @@ -10,7 +10,7 @@ name = "miden-objects" readme = "README.md" repository.workspace = true rust-version.workspace = true -version = "0.11.5" +version.workspace = true [[bench]] harness = false @@ -26,41 +26,47 @@ std = [ "dep:toml", "miden-assembly/std", "miden-core/std", + "miden-crypto/concurrent", "miden-crypto/std", "miden-processor/std", "miden-verifier/std", ] -testing = ["dep:rand", "dep:rand_xoshiro", "dep:winter-rand-utils"] +testing = ["dep:rand_xoshiro", "dep:winter-rand-utils", "miden-air/testing"] [dependencies] # Miden dependencies -miden-assembly = { workspace = true } -miden-core = { workspace = true } -miden-crypto = { workspace = true } -miden-processor = { workspace = true } -miden-utils-sync = { workspace = true } -miden-verifier = { workspace = true } -winter-rand-utils = { optional = true, version = "0.13" } +miden-assembly = { workspace = true } +miden-assembly-syntax = { workspace = true } +miden-core = { workspace = true } +miden-crypto = { workspace = true } +miden-mast-package = { workspace = true } +miden-processor = { 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 = { optional = true, workspace = true } +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.8" } +toml = { optional = true, version = "0.9" } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { features = ["wasm_js"], version = "0.3" } [dev-dependencies] -anyhow = { default-features = false, features = ["backtrace", "std"], version = "1.0" } +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 = "." } -pprof = { default-features = false, features = ["criterion", "flamegraph"], version = "0.14" } +pprof = { default-features = false, features = ["criterion", "flamegraph"], version = "0.15" } rstest = { workspace = true } tempfile = { version = "3.19" } winter-air = { version = "0.13" } +# for HashFunction/ExecutionProof::new_dummy +color-eyre = { version = "0.5" } +miden-air = { features = ["std", "testing"], workspace = true } diff --git a/crates/miden-objects/src/account/account_id/address_type.rs b/crates/miden-objects/src/account/account_id/address_type.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/miden-objects/src/account/account_id/id_prefix.rs b/crates/miden-objects/src/account/account_id/id_prefix.rs index ff11830bfe..97fa2552b4 100644 --- a/crates/miden-objects/src/account/account_id/id_prefix.rs +++ b/crates/miden-objects/src/account/account_id/id_prefix.rs @@ -119,10 +119,10 @@ impl AccountIdPrefix { } } - /// Returns `true` if the full state of the account is on chain, i.e. if the modes are + /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise. - pub fn is_onchain(&self) -> bool { - self.storage_mode().is_onchain() + pub fn has_public_state(&self) -> bool { + self.storage_mode().has_public_state() } /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise. diff --git a/crates/miden-objects/src/account/account_id/mod.rs b/crates/miden-objects/src/account/account_id/mod.rs index b4528c28d8..2c94bb9850 100644 --- a/crates/miden-objects/src/account/account_id/mod.rs +++ b/crates/miden-objects/src/account/account_id/mod.rs @@ -6,9 +6,6 @@ pub use id_prefix::AccountIdPrefix; mod seed; -mod network_id; -pub use network_id::NetworkId; - mod account_type; pub use account_type::AccountType; @@ -19,12 +16,14 @@ mod id_version; use alloc::string::{String, ToString}; use core::fmt; +use bech32::primitives::decode::ByteIter; pub use id_version::AccountIdVersion; use miden_core::Felt; use miden_core::utils::{ByteReader, Deserializable, Serializable}; use miden_crypto::utils::hex_to_bytes; use miden_processor::DeserializationError; +use crate::address::NetworkId; use crate::errors::AccountIdError; use crate::{AccountError, Word}; @@ -231,10 +230,10 @@ impl AccountId { } } - /// Returns `true` if the full state of the account is on chain, i.e. if the modes are + /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise. - pub fn is_onchain(&self) -> bool { - self.storage_mode().is_onchain() + pub fn has_public_state(&self) -> bool { + self.storage_mode().has_public_state() } /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise. @@ -275,6 +274,54 @@ impl AccountId { } } + /// Encodes the [`AccountId`] into a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) + /// string. + /// + /// # Encoding + /// + /// The encoding of an account ID into bech32 is done as follows: + /// - Convert the account ID into its `[u8; 15]` data format. + /// - Insert the address type `AddressType::AccountId` byte at index 0, shifting all other + /// elements to the right. + /// - Choose an HRP, defined as a [`NetworkId`], for example [`NetworkId::Mainnet`] whose string + /// representation is `mm`. + /// - Encode the resulting HRP together with the data into a bech32 string using the + /// [`bech32::Bech32m`] checksum algorithm. + /// + /// This is an example of an account ID in hex and bech32 representations: + /// + /// ```text + /// hex: 0x6d449e4034fadca075d1976fef7e38 + /// bech32: mm1apk5f8jqxnadegr46xtklmm78qhdgkwc + /// ``` + /// + /// ## Rationale + /// + /// Having the address type at the very beginning is so that it can be decoded to detect the + /// type of the address without having to decode the entire data. Moreover, choosing the + /// address type as a multiple of 8 means the first character of the bech32 string after the + /// `1` separator will be different for every address type. This makes the type of the address + /// conveniently human-readable. + pub fn to_bech32(&self, network_id: NetworkId) -> String { + match self { + AccountId::V0(account_id_v0) => account_id_v0.to_bech32(network_id), + } + } + + /// Decodes a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string into an [`AccountId`]. + /// + /// See [`AccountId::to_bech32`] for details on the format. The procedure for decoding the + /// bech32 data into the ID consists of the inverse operations of encoding. + pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> { + AccountIdV0::from_bech32(bech32_string) + .map(|(network_id, account_id)| (network_id, AccountId::V0(account_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) + } + /// Returns the [`AccountIdPrefix`] of this ID. /// /// The prefix of an account ID is guaranteed to be unique. @@ -439,8 +486,15 @@ impl Deserializable for AccountId { #[cfg(test)] mod tests { + use alloc::boxed::Box; + + use assert_matches::assert_matches; + use bech32::{Bech32, Bech32m, NoChecksum}; use super::*; + use crate::account::account_id::v0::{extract_storage_mode, extract_type, extract_version}; + use crate::address::{AddressType, CustomNetworkId}; + use crate::errors::Bech32Error; use crate::testing::account_id::{ ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, @@ -448,6 +502,7 @@ mod tests { ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + AccountIdBuilder, }; #[test] @@ -471,4 +526,129 @@ mod tests { ); } } + + #[test] + fn bech32_encode_decode_roundtrip() -> anyhow::Result<()> { + // We use this to check that encoding does not panic even when using the longest possible + // HRP. + let longest_possible_hrp = + "01234567890123456789012345678901234567890123456789012345678901234567890123456789012"; + assert_eq!(longest_possible_hrp.len(), 83); + + let random_id = AccountIdBuilder::new().build_with_rng(&mut rand::rng()); + + for network_id in [ + NetworkId::Mainnet, + NetworkId::Custom(Box::new("custom".parse::()?)), + NetworkId::Custom(Box::new(longest_possible_hrp.parse::()?)), + ] { + for account_id in [ + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_SENDER, + random_id.into(), + ] + .into_iter() + { + let account_id = AccountId::try_from(account_id).unwrap(); + + let bech32_string = account_id.to_bech32(network_id.clone()); + let (decoded_network_id, decoded_account_id) = + AccountId::from_bech32(&bech32_string).unwrap(); + + assert_eq!(network_id, decoded_network_id, "network id failed for {account_id}",); + assert_eq!(account_id, decoded_account_id, "account id failed for {account_id}"); + + let (_, data) = bech32::decode(&bech32_string).unwrap(); + + // Raw bech32 data should contain the address type as the first byte. + assert_eq!(data[0], AddressType::AccountId as u8); + + // Raw bech32 data should contain the metadata byte at index 8. + assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version()); + assert_eq!(extract_type(data[8] as u64), account_id.account_type()); + assert_eq!( + extract_storage_mode(data[8] as u64).unwrap(), + account_id.storage_mode() + ); + } + } + + Ok(()) + } + + #[test] + fn bech32_invalid_checksum() { + let network_id = NetworkId::Mainnet; + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + + let bech32_string = account_id.to_bech32(network_id); + let mut invalid_bech32_1 = bech32_string.clone(); + invalid_bech32_1.remove(0); + let mut invalid_bech32_2 = bech32_string.clone(); + invalid_bech32_2.remove(7); + + let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err(); + assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_))); + + let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err(); + assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_))); + } + + #[test] + fn bech32_invalid_address_type() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let mut id_bytes = account_id.to_bytes(); + + // Set invalid address type. + id_bytes.insert(0, 16); + + let invalid_bech32 = + bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap(); + + let error = AccountId::from_bech32(&invalid_bech32).unwrap_err(); + assert_matches!( + error, + AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16)) + ); + } + + #[test] + fn bech32_invalid_other_checksum() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let mut id_bytes = account_id.to_bytes(); + id_bytes.insert(0, AddressType::AccountId as u8); + + // Use Bech32 instead of Bech32m which is disallowed. + let invalid_bech32_regular = + bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap(); + let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err(); + assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_))); + + // Use no checksum instead of Bech32m which is disallowed. + let invalid_bech32_no_checksum = + bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap(); + let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err(); + assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_))); + } + + #[test] + fn bech32_invalid_length() { + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let mut id_bytes = account_id.to_bytes(); + id_bytes.insert(0, AddressType::AccountId as u8); + // Add one byte to make the length invalid. + id_bytes.push(5); + + let invalid_bech32 = + bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap(); + + let error = AccountId::from_bech32(&invalid_bech32).unwrap_err(); + assert_matches!( + error, + AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. }) + ); + } } diff --git a/crates/miden-objects/src/account/account_id/storage_mode.rs b/crates/miden-objects/src/account/account_id/storage_mode.rs index 107ae269b2..12670701ce 100644 --- a/crates/miden-objects/src/account/account_id/storage_mode.rs +++ b/crates/miden-objects/src/account/account_id/storage_mode.rs @@ -28,9 +28,9 @@ pub enum AccountStorageMode { } impl AccountStorageMode { - /// Returns `true` if the full state of the account is on chain, i.e. if the modes are + /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are /// [`Self::Public`] or [`Self::Network`], `false` otherwise. - pub fn is_onchain(&self) -> bool { + pub fn has_public_state(&self) -> bool { matches!(self, Self::Public | Self::Network) } diff --git a/crates/miden-objects/src/account/account_id/v0/mod.rs b/crates/miden-objects/src/account/account_id/v0/mod.rs index f08efff057..c14095819d 100644 --- a/crates/miden-objects/src/account/account_id/v0/mod.rs +++ b/crates/miden-objects/src/account/account_id/v0/mod.rs @@ -4,9 +4,12 @@ use alloc::vec::Vec; use core::fmt; use core::hash::Hash; +use bech32::Bech32m; +use bech32::primitives::decode::{ByteIter, CheckedHrpstring}; use miden_crypto::utils::hex_to_bytes; pub use prefix::AccountIdPrefixV0; +use crate::account::account_id::NetworkId; use crate::account::account_id::account_type::{ FUNGIBLE_FAUCET, NON_FUNGIBLE_FAUCET, @@ -15,7 +18,8 @@ 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::errors::AccountIdError; +use crate::address::AddressType; +use crate::errors::{AccountIdError, Bech32Error}; use crate::utils::{ByteReader, Deserializable, DeserializationError, Serializable}; use crate::{AccountError, EMPTY_WORD, Felt, Hasher, Word}; @@ -212,6 +216,83 @@ impl AccountIdV0 { hex_string } + /// See [`AccountId::to_bech32`](super::AccountId::to_bech32) for details. + pub fn to_bech32(&self, network_id: NetworkId) -> String { + let id_bytes: [u8; Self::SERIALIZED_SIZE] = (*self).into(); + + let mut data = [0; Self::SERIALIZED_SIZE + 1]; + data[0] = AddressType::AccountId as u8; + data[1..16].copy_from_slice(&id_bytes); + + // SAFETY: Encoding only panics if the total length of the hrp, data (in GF(32)), separator + // and checksum exceeds Bech32m::CODE_LENGTH, which is 1023. Since the data is 26 bytes in + // that field and the hrp is at most 83 in size we are way below the limit. + // + // The only allowed checksum algorithm is [`Bech32m`](bech32::Bech32m) due to being the + // best available checksum algorithm with no known weaknesses (unlike + // [`Bech32`](bech32::Bech32)). No checksum is also not allowed since the intended + // use of bech32 is to have error detection capabilities. + bech32::encode::(network_id.into_hrp(), &data) + .expect("code length of bech32 should not be exceeded") + } + + /// See [`AccountId::from_bech32`](super::AccountId::from_bech32) for details. + pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> { + // We use CheckedHrpString instead of bech32::decode with an explicit checksum algorithm so + // we don't allow the `Bech32` or `NoChecksum` algorithms. + let checked_string = CheckedHrpstring::new::(bech32_string).map_err(|source| { + // The CheckedHrpStringError does not implement core::error::Error, only + // std::error::Error, so for now we convert it to a String. Even if it will + // implement the trait in the future, we should include it as an opaque + // error since the crate does not have a stable release yet. + AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(source.to_string().into())) + })?; + + let hrp = checked_string.hrp(); + let network_id = NetworkId::from_hrp(hrp); + + let mut byte_iter = checked_string.byte_iter(); + + // The length must be the serialized size of the account ID plus the address byte. + if byte_iter.len() != Self::SERIALIZED_SIZE + 1 { + return Err(AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { + expected: Self::SERIALIZED_SIZE + 1, + actual: byte_iter.len(), + })); + } + + let address_byte = byte_iter.next().expect("there should be at least one byte"); + if address_byte != AddressType::AccountId as u8 { + return Err(AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType( + address_byte, + ))); + } + + Self::from_bech32_byte_iter(byte_iter).map(|account_id| (network_id, account_id)) + } + + /// Decodes the data from the bech32 byte iterator into an [`AccountId`]. + pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result { + // The _remaining_ length of the iterator must be the serialized size of the account ID. + if byte_iter.len() != Self::SERIALIZED_SIZE { + return Err(AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { + expected: Self::SERIALIZED_SIZE, + actual: byte_iter.len(), + })); + } + + // Every byte is guaranteed to be overwritten since we've checked the length of the + // iterator. + let mut id_bytes = [0_u8; Self::SERIALIZED_SIZE]; + for (i, byte) in byte_iter.enumerate() { + id_bytes[i] = byte; + } + + let account_id = Self::try_from(id_bytes)?; + + Ok(account_id) + } + /// Returns the [`AccountIdPrefixV0`] of this account ID. /// /// See also [`AccountId::prefix`](super::AccountId::prefix) for details. diff --git a/crates/miden-objects/src/account/auth.rs b/crates/miden-objects/src/account/auth.rs index 694b165c2e..65d1ba9169 100644 --- a/crates/miden-objects/src/account/auth.rs +++ b/crates/miden-objects/src/account/auth.rs @@ -1,8 +1,8 @@ -// AUTH SECRET KEY -// ================================================================================================ +use alloc::vec::Vec; -use miden_crypto::dsa::rpo_falcon512::{self, SecretKey}; +use rand::{CryptoRng, Rng}; +use crate::crypto::dsa::{ecdsa_k256_keccak, rpo_falcon512}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -10,44 +10,403 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; +use crate::{AuthSchemeError, Felt, Hasher, Word}; -/// Types of secret keys used for signing messages -#[derive(Clone, Debug)] +// AUTH SCHEME +// ================================================================================================ + +/// Identifier of signature schemes use for transaction authentication +const RPO_FALCON_512: u8 = 0; +const ECDSA_K256_KECCAK: u8 = 1; + +/// Defines standard authentication schemes (i.e., signature schemes) available in the Miden +/// protocol. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +#[repr(u8)] +pub enum AuthScheme { + /// A deterministic RPO 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, + + /// ECDSA signature scheme over secp256k1 curve using Keccak to hash the messages when signing. + EcdsaK256Keccak = ECDSA_K256_KECCAK, +} + +impl AuthScheme { + /// Returns a numerical value of this auth scheme. + pub fn as_u8(&self) -> u8 { + *self as u8 + } +} + +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::EcdsaK256Keccak => f.write_str("EcdsaK256Keccak"), + } + } +} + +impl TryFrom for AuthScheme { + type Error = AuthSchemeError; + + fn try_from(value: u8) -> Result { + match value { + RPO_FALCON_512 => Ok(Self::RpoFalcon512), + ECDSA_K256_KECCAK => Ok(Self::EcdsaK256Keccak), + value => Err(AuthSchemeError::InvalidAuthSchemeIdentifier(value)), + } + } +} + +impl Serializable for AuthScheme { + fn write_into(&self, target: &mut W) { + target.write_u8(*self as u8); + } + + fn get_size_hint(&self) -> usize { + // auth scheme is encoded as a single byte + size_of::() + } +} + +impl Deserializable for AuthScheme { + fn read_from(source: &mut R) -> Result { + match source.read_u8()? { + RPO_FALCON_512 => Ok(Self::RpoFalcon512), + ECDSA_K256_KECCAK => Ok(Self::EcdsaK256Keccak), + value => Err(DeserializationError::InvalidValue(format!( + "auth scheme identifier `{value}` is not valid" + ))), + } + } +} + +// AUTH SECRET KEY +// ================================================================================================ + +/// Secret keys of the standard [`AuthScheme`]s available in the Miden protocol. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] #[repr(u8)] pub enum AuthSecretKey { - RpoFalcon512(rpo_falcon512::SecretKey) = 0, + RpoFalcon512(rpo_falcon512::SecretKey) = RPO_FALCON_512, + EcdsaK256Keccak(ecdsa_k256_keccak::SecretKey) = ECDSA_K256_KECCAK, } impl AuthSecretKey { - /// Identifier for the type of authentication key - pub fn auth_scheme_id(&self) -> u8 { + /// Generates an RpoFalcon512 secret key from the OS-provided randomness. + #[cfg(feature = "std")] + pub fn new_rpo_falcon512() -> Self { + Self::RpoFalcon512(rpo_falcon512::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 EcdsaK256Keccak secret key from the OS-provided randomness. + #[cfg(feature = "std")] + pub fn new_ecdsa_k256_keccak() -> Self { + Self::EcdsaK256Keccak(ecdsa_k256_keccak::SecretKey::new()) + } + + /// Generates an EcdsaK256Keccak secret key using the provided random number generator. + pub fn new_ecdsa_k256_keccak_with_rng(rng: &mut R) -> Self { + Self::EcdsaK256Keccak(ecdsa_k256_keccak::SecretKey::with_rng(rng)) + } + + /// Returns the authentication scheme of this secret key. + pub fn auth_scheme(&self) -> AuthScheme { match self { - AuthSecretKey::RpoFalcon512(_) => 0u8, + AuthSecretKey::RpoFalcon512(_) => AuthScheme::RpoFalcon512, + AuthSecretKey::EcdsaK256Keccak(_) => AuthScheme::EcdsaK256Keccak, + } + } + + /// 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::EcdsaK256Keccak(key) => PublicKey::EcdsaK256Keccak(key.public_key()), + } + } + + /// 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::EcdsaK256Keccak(key) => Signature::EcdsaK256Keccak(key.sign(message)), } } } impl Serializable for AuthSecretKey { fn write_into(&self, target: &mut W) { - target.write_u8(self.auth_scheme_id()); + self.auth_scheme().write_into(target); match self { - AuthSecretKey::RpoFalcon512(secret_key) => { - secret_key.write_into(target); - }, + AuthSecretKey::RpoFalcon512(key) => key.write_into(target), + AuthSecretKey::EcdsaK256Keccak(key) => key.write_into(target), } } } impl Deserializable for AuthSecretKey { fn read_from(source: &mut R) -> Result { - let auth_key_id: u8 = source.read_u8()?; - match auth_key_id { - // RpoFalcon512 - 0u8 => { - let secret_key = SecretKey::read_from(source)?; + match source.read::()? { + AuthScheme::RpoFalcon512 => { + let secret_key = rpo_falcon512::SecretKey::read_from(source)?; Ok(AuthSecretKey::RpoFalcon512(secret_key)) }, - val => Err(DeserializationError::InvalidValue(format!("Invalid auth scheme ID {val}"))), + AuthScheme::EcdsaK256Keccak => { + let secret_key = ecdsa_k256_keccak::SecretKey::read_from(source)?; + Ok(AuthSecretKey::EcdsaK256Keccak(secret_key)) + }, + } + } +} + +// PUBLIC KEY +// ================================================================================================ + +/// Commitment to a public key. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct PublicKeyCommitment(Word); + +impl core::fmt::Display for PublicKeyCommitment { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for PublicKeyCommitment { + fn from(value: rpo_falcon512::PublicKey) -> Self { + Self(value.to_commitment()) + } +} + +impl From for Word { + fn from(value: PublicKeyCommitment) -> Self { + value.0 + } +} + +impl From for PublicKeyCommitment { + fn from(value: Word) -> Self { + Self(value) + } +} + +/// Public keys of the standard authentication schemes available in the Miden protocol. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum PublicKey { + RpoFalcon512(rpo_falcon512::PublicKey), + EcdsaK256Keccak(ecdsa_k256_keccak::PublicKey), +} + +impl PublicKey { + /// Returns the authentication scheme of this public key. + pub fn auth_scheme(&self) -> AuthScheme { + match self { + PublicKey::RpoFalcon512(_) => AuthScheme::RpoFalcon512, + PublicKey::EcdsaK256Keccak(_) => AuthScheme::EcdsaK256Keccak, + } + } + + /// Returns a commitment to this public key. + pub fn to_commitment(&self) -> PublicKeyCommitment { + match self { + PublicKey::RpoFalcon512(key) => key.to_commitment().into(), + PublicKey::EcdsaK256Keccak(key) => key.to_commitment().into(), + } + } + + /// 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)) => { + key.verify(message, &sig) + }, + (PublicKey::EcdsaK256Keccak(key), Signature::EcdsaK256Keccak(sig)) => { + key.verify(message, &sig) + }, + _ => false, + } + } +} + +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::EcdsaK256Keccak(pub_key) => pub_key.write_into(target), + } + } +} + +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::EcdsaK256Keccak => { + let pub_key = ecdsa_k256_keccak::PublicKey::read_from(source)?; + Ok(PublicKey::EcdsaK256Keccak(pub_key)) + }, + } + } +} + +// SIGNATURE +// ================================================================================================ + +/// Represents a signature object ready for native verification. +/// +/// In order to use this signature within the Miden VM, a preparation step may be necessary to +/// 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}; +/// +/// let secret_key = SecretKey::new(); +/// let message = Word::default(); +/// let signature: Signature = secret_key.sign(message).into(); +/// let prepared_signature: Vec = signature.to_prepared_signature(); +/// ``` +#[derive(Clone, Debug)] +#[repr(u8)] +pub enum Signature { + RpoFalcon512(rpo_falcon512::Signature) = RPO_FALCON_512, + EcdsaK256Keccak(ecdsa_k256_keccak::Signature) = ECDSA_K256_KECCAK, +} + +impl Signature { + /// Returns the authentication scheme of this signature. + pub fn auth_scheme(&self) -> AuthScheme { + match self { + Signature::RpoFalcon512(_) => AuthScheme::RpoFalcon512, + Signature::EcdsaK256Keccak(_) => AuthScheme::EcdsaK256Keccak, + } + } + + /// Converts this signature to a sequence of field elements in the format expected by the + /// native verification procedure in the VM. + pub fn to_prepared_signature(&self) -> Vec { + match self { + Signature::RpoFalcon512(sig) => prepare_rpo_falcon512_signature(sig), + Signature::EcdsaK256Keccak(sig) => prepare_ecdsa_k256_keccak_signature(sig), + } + } +} + +impl From for Signature { + fn from(signature: rpo_falcon512::Signature) -> Self { + Signature::RpoFalcon512(signature) + } +} + +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::EcdsaK256Keccak(signature) => signature.write_into(target), + } + } +} + +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::EcdsaK256Keccak => { + let signature = ecdsa_k256_keccak::Signature::read_from(source)?; + Ok(Signature::EcdsaK256Keccak(signature)) + }, } } } + +// SIGNATURE PREPARATION +// ================================================================================================ + +/// Converts a Falcon [rpo_falcon512::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: +/// +/// 1. The challenge point at which we evaluate the polynomials in the subsequent three bullet +/// points, i.e. `h`, `s2` and `pi`, to check the product relationship. +/// 2. The expanded public key represented as the coefficients of a polynomial `h` of degree < 512. +/// 3. The signature represented as the coefficients of a polynomial `s2` of degree < 512. +/// 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; + + // The signature is composed of a nonce and a polynomial s2 + // The nonce is represented as 8 field elements. + let nonce = sig.nonce(); + // We convert the signature to a polynomial + let s2 = sig.sig_poly(); + // We also need in the VM the expanded key corresponding to the public key that was provided + // via the operand stack + let h = sig.public_key(); + // Lastly, for the probabilistic product routine that is part of the verification procedure, + // we need to compute the product of the expanded key and the signature polynomial in + // the ring of polynomials with coefficients in the Miden field. + let pi = Polynomial::mul_modulo_p(h, s2); + + // We now push the expanded key, the signature polynomial, and the product of the + // expanded key and the signature polynomial to the advice stack. We also push + // the challenge point at which the previous polynomials will be evaluated. + // Finally, we push the nonce needed for the hash-to-point algorithm. + + let mut polynomials: Vec = + h.coefficients.iter().map(|a| Felt::from(a.value() as u32)).collect(); + polynomials.extend(s2.coefficients.iter().map(|a| Felt::from(a.value() as u32))); + polynomials.extend(pi.iter().map(|a| Felt::new(*a))); + + let digest_polynomials = Hasher::hash_elements(&polynomials); + let challenge = (digest_polynomials[0], digest_polynomials[1]); + + let mut result: Vec = vec![challenge.0, challenge.1]; + result.extend_from_slice(&polynomials); + result.extend_from_slice(&nonce.to_elements()); + + result.reverse(); + result +} + +/// Converts a ECDSA [ecdsa_k256_keccak::Signature] to a vector of values to be pushed to be +/// written to memory. The values are the ones required for a ECDSA signature precompile inside +/// the Miden VM. +fn prepare_ecdsa_k256_keccak_signature(sig: &ecdsa_k256_keccak::Signature) -> Vec { + const BYTES_PER_U32: usize = size_of::(); + + let bytes = sig.to_bytes(); + bytes + .chunks(BYTES_PER_U32) + .map(|chunk| { + // Pack up to 4 bytes into a u32 in little-endian format + let mut packed = [0u8; BYTES_PER_U32]; + packed[..chunk.len()].copy_from_slice(chunk); + Felt::from(u32::from_le_bytes(packed)) + }) + .collect() +} diff --git a/crates/miden-objects/src/account/builder/mod.rs b/crates/miden-objects/src/account/builder/mod.rs index ff173d7b80..e5d19206be 100644 --- a/crates/miden-objects/src/account/builder/mod.rs +++ b/crates/miden-objects/src/account/builder/mod.rs @@ -38,7 +38,7 @@ use crate::{AccountError, Felt, Word}; /// /// Under the `testing` feature, it is possible to: /// - Build an existing account using [`AccountBuilder::build_existing`] which will set the -/// account's nonce to `1`. +/// account's nonce to `1` by default, or to the configured value. /// - Add assets to the account's vault, however this will only succeed when using /// [`AccountBuilder::build_existing`]. /// @@ -56,6 +56,8 @@ use crate::{AccountError, Felt, Word}; pub struct AccountBuilder { #[cfg(any(feature = "testing", test))] assets: Vec, + #[cfg(any(feature = "testing", test))] + nonce: Option, components: Vec, auth_component: Option, account_type: AccountType, @@ -73,6 +75,8 @@ impl AccountBuilder { Self { #[cfg(any(feature = "testing", test))] assets: vec![], + #[cfg(any(feature = "testing", test))] + nonce: None, components: vec![], auth_component: None, init_seed, @@ -112,7 +116,7 @@ impl AccountBuilder { /// Adds a designated authentication [`AccountComponent`] to the builder. /// /// This component may contain multiple procedures, but is expected to contain exactly one - /// authentication procedure (named `auth__*`). + /// authentication procedure (named `auth_*`). /// Calling this method multiple times will override the previous auth component. /// /// Procedures from this component will be placed at the beginning of the account procedure @@ -190,7 +194,7 @@ impl AccountBuilder { /// - [`MastForest::merge`](miden_processor::MastForest::merge) fails on the given components. /// - If duplicate assets were added to the builder (only under the `testing` feature). /// - If the vault is not empty on new accounts (only under the `testing` feature). - pub fn build(mut self) -> Result<(Account, Word), AccountError> { + pub fn build(mut self) -> Result { let (vault, code, storage) = self.build_inner()?; #[cfg(any(feature = "testing", test))] @@ -219,9 +223,12 @@ impl AccountBuilder { debug_assert_eq!(account_id.account_type(), self.account_type); debug_assert_eq!(account_id.storage_mode(), self.storage_mode); - let account = Account::from_parts(account_id, vault, storage, code, Felt::ZERO); + // SAFETY: The account ID was derived from the seed and the seed is provided, so it is safe + // to bypass the checks of `Account::new`. + let account = + Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed)); - Ok((account, seed)) + Ok(account) } } @@ -236,6 +243,15 @@ impl AccountBuilder { self } + /// Sets the nonce of an existing account. + /// + /// This method is optional. It must only be used when using [`Self::build_existing`] + /// instead of [`Self::build`] since new accounts must have a nonce of `0`. + pub fn nonce(mut self, nonce: Felt) -> Self { + self.nonce = Some(nonce); + self + } + /// Builds the account as an existing account, that is, with the nonce set to [`Felt::ONE`]. /// /// The [`AccountId`] is constructed by slightly modifying `init_seed[0..8]` to be a valid ID. @@ -255,7 +271,10 @@ impl AccountBuilder { ) }; - Ok(Account::from_parts(account_id, vault, storage, code, Felt::ONE)) + // Use the nonce value set by the Self::nonce method or Felt::ONE as a default. + let nonce = self.nonce.unwrap_or(Felt::ONE); + + Ok(Account::new_existing(account_id, vault, storage, code, nonce)) } } @@ -269,6 +288,7 @@ mod tests { use assert_matches::assert_matches; use miden_assembly::{Assembler, Library}; use miden_core::FieldElement; + use miden_processor::MastNodeExt; use super::*; use crate::account::StorageSlot; @@ -336,7 +356,7 @@ mod tests { let storage_slot1 = 12; let storage_slot2 = 42; - let (account, seed) = Account::builder([5; 32]) + let account = Account::builder([5; 32]) .with_auth_component(NoopAuthComponent) .with_component(CustomComponent1 { slot0: storage_slot0 }) .with_component(CustomComponent2 { @@ -350,7 +370,7 @@ mod tests { assert_eq!(account.nonce(), Felt::ZERO); let computed_id = AccountId::new( - seed, + account.seed().unwrap(), AccountIdVersion::Version0, account.code.commitment(), account.storage.commitment(), diff --git a/crates/miden-objects/src/account/code/mod.rs b/crates/miden-objects/src/account/code/mod.rs index 944e029f6d..d72690d689 100644 --- a/crates/miden-objects/src/account/code/mod.rs +++ b/crates/miden-objects/src/account/code/mod.rs @@ -553,11 +553,11 @@ mod tests { let code_with_multiple_auth = " use.miden::account - export.auth__basic + export.auth_basic push.1 drop end - export.auth__secondary + export.auth_secondary push.0 drop end "; diff --git a/crates/miden-objects/src/account/code/procedure.rs b/crates/miden-objects/src/account/code/procedure.rs index 7804501184..8281bc532e 100644 --- a/crates/miden-objects/src/account/code/procedure.rs +++ b/crates/miden-objects/src/account/code/procedure.rs @@ -3,7 +3,7 @@ use alloc::sync::Arc; use miden_core::mast::MastForest; use miden_core::prettier::PrettyPrint; -use miden_processor::{MastNode, MastNodeId}; +use miden_processor::{MastNode, MastNodeExt, MastNodeId}; use super::Felt; use crate::utils::serde::{ diff --git a/crates/miden-objects/src/account/component/mod.rs b/crates/miden-objects/src/account/component/mod.rs index dd6fb4efcc..22874bc06e 100644 --- a/crates/miden-objects/src/account/component/mod.rs +++ b/crates/miden-objects/src/account/component/mod.rs @@ -3,6 +3,8 @@ 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; mod template; @@ -11,6 +13,41 @@ pub use template::*; use crate::account::{AccountType, StorageSlot}; use crate::{AccountError, Word}; +// IMPLEMENTATIONS +// ================================================================================================ + +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)", + ) + })?; + + Ok(AccountComponentTemplate::new(metadata, library)) + } +} + /// An [`AccountComponent`] defines a [`Library`] of code and the initial value and types of /// the [`StorageSlot`]s it accesses. /// @@ -112,6 +149,31 @@ impl AccountComponent { .with_supported_types(template.metadata().supported_types().clone())) } + /// Creates an [`AccountComponent`] from a [`Package`] using [`InitStorageData`]. + /// + /// This method provides type safety by leveraging the component's metadata to validate + /// storage initialization data. The package must contain explicit account component metadata. + /// + /// # Arguments + /// + /// * `package` - The package containing the library and account component metadata + /// * `init_storage_data` - The initialization data for storage slots + /// + /// # Errors + /// + /// Returns an error if: + /// - The package does not contain account component metadata + /// - The package cannot be converted to an [`AccountComponentTemplate`] + /// - The storage initialization fails due to invalid or missing data + /// - The component creation fails + pub fn from_package_with_init_data( + package: &Package, + init_storage_data: &InitStorageData, + ) -> Result { + let template = AccountComponentTemplate::try_from(package.clone())?; + Self::from_template(&template, init_storage_data) + } + // ACCESSORS // -------------------------------------------------------------------------------------------- @@ -147,11 +209,11 @@ impl AccountComponent { } /// Returns a vector of tuples (digest, is_auth) for all procedures in this component. - pub(crate) fn get_procedures(&self) -> Vec<(Word, bool)> { + pub fn get_procedures(&self) -> Vec<(Word, bool)> { let mut procedures = Vec::new(); for module in self.library.module_infos() { for (_, procedure_info) in module.procedures() { - let is_auth = procedure_info.name.contains("auth__"); + let is_auth = procedure_info.name.starts_with("auth_"); procedures.push((procedure_info.digest, is_auth)); } } @@ -205,3 +267,132 @@ impl From for Library { component.library } } + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeSet; + use alloc::string::ToString; + use alloc::sync::Arc; + + use miden_assembly::Assembler; + use miden_core::utils::Serializable; + use miden_mast_package::{MastArtifact, Package, PackageManifest, Section}; + use semver::Version; + + use super::*; + use crate::testing::account_code::CODE; + + #[test] + fn test_try_from_package_for_template() { + // Create a simple library for testing + let library = Assembler::default().assemble_library([CODE]).unwrap(); + + // Test with metadata + let metadata = AccountComponentMetadata::new( + "test_component".to_string(), + "A test component".to_string(), + Version::new(1, 0, 0), + BTreeSet::from_iter([AccountType::RegularAccountImmutableCode]), + vec![], + ) + .unwrap(); + + 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), + + sections: vec![Section::new( + SectionId::ACCOUNT_COMPONENT_METADATA, + metadata_bytes.clone(), + )], + version: Default::default(), + description: None, + }; + + let template = AccountComponentTemplate::try_from(package_with_metadata).unwrap(); + assert_eq!(template.metadata().name(), "test_component"); + assert!( + template + .metadata() + .supported_types() + .contains(&AccountType::RegularAccountImmutableCode) + ); + + // Test without metadata - should fail + let package_without_metadata = Package { + name: "test_package_no_metadata".to_string(), + mast: MastArtifact::Library(Arc::new(library)), + manifest: PackageManifest::new(None), + sections: vec![], // No metadata section + version: Default::default(), + description: None, + }; + + let result = AccountComponentTemplate::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() { + // Create a simple library for testing + let library = Assembler::default().assemble_library([CODE]).unwrap(); + + // Create metadata for the component + let metadata = AccountComponentMetadata::new( + "test_component".to_string(), + "A test component".to_string(), + Version::new(1, 0, 0), + BTreeSet::from_iter([ + 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, + }; + + // Test with empty init data - this tests the complete workflow: + // Package -> AccountComponentTemplate -> AccountComponent + let init_data = InitStorageData::default(); + let component = + AccountComponent::from_package_with_init_data(&package, &init_data).unwrap(); + + // Verify the component was created correctly + assert_eq!(component.storage_size(), 0); + assert!(component.supports_type(AccountType::RegularAccountImmutableCode)); + assert!(component.supports_type(AccountType::RegularAccountUpdatableCode)); + assert!(!component.supports_type(AccountType::FungibleFaucet)); + + // Test without metadata - should fail + let package_without_metadata = Package { + name: "test_package_no_metadata".to_string(), + mast: MastArtifact::Library(Arc::new(library)), + 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); + 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-objects/src/account/component/template/mod.rs b/crates/miden-objects/src/account/component/template/mod.rs index 842b83acd2..c646fa50a1 100644 --- a/crates/miden-objects/src/account/component/template/mod.rs +++ b/crates/miden-objects/src/account/component/template/mod.rs @@ -104,7 +104,7 @@ impl Deserializable for AccountComponentTemplate { /// /// ``` /// # use semver::Version; -/// # use std::collections::BTreeSet; +/// # use std::collections::{BTreeMap, BTreeSet}; /// # use miden_objects::{testing::account_code::CODE, account::{ /// # AccountComponent, AccountComponentMetadata, StorageEntry, /// # StorageValueName, @@ -126,8 +126,10 @@ impl Deserializable for AccountComponentTemplate { /// .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())]); +/// let init_storage_data = InitStorageData::new( +/// [(StorageValueName::new("test_value.foo")?, "300".to_string())], +/// BTreeMap::new(), +/// ); /// /// let component_template = AccountComponentMetadata::new( /// "test name".into(), @@ -327,7 +329,7 @@ impl Deserializable for AccountComponentMetadata { #[cfg(test)] mod tests { - use std::collections::BTreeSet; + use std::collections::{BTreeMap, BTreeSet}; use std::string::ToString; use assert_matches::assert_matches; @@ -377,7 +379,7 @@ mod tests { storage, }; - let serialized = original_config.as_toml().unwrap(); + let serialized = original_config.to_toml().unwrap(); let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap(); assert_eq!(deserialized, original_config); } @@ -476,6 +478,8 @@ mod tests { #[test] pub fn fail_duplicate_key_instance() { + let _ = color_eyre::install(); + let toml_text = r#" name = "Test Component" description = "This is a test component" @@ -498,10 +502,13 @@ mod tests { // Fail to instantiate on a duplicate key - let init_storage_data = InitStorageData::new([( - StorageValueName::new("map.duplicate_key").unwrap(), - "0x0000000000000000000000000000000000000000000000000100000000000000".to_string(), - )]); + 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, @@ -511,10 +518,10 @@ mod tests { ); // Successfully instantiate a map (keys are not duplicate) - let valid_init_storage_data = InitStorageData::new([( - StorageValueName::new("map.duplicate_key").unwrap(), - "0x30".to_string(), - )]); + 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 index 6575ad803e..54780435b3 100644 --- a/crates/miden-objects/src/account/component/template/storage/entry_content.rs +++ b/crates/miden-objects/src/account/component/template/storage/entry_content.rs @@ -466,30 +466,52 @@ impl Deserializable for FeltRepresentation { /// Supported map representations for a component's storage entries. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(::serde::Deserialize, ::serde::Serialize))] -pub struct MapRepresentation { - /// The human-readable name of the map slot. - /// An optional description for the slot, explaining its purpose. - identifier: FieldIdentifier, - /// Storage map entries, consisting of a list of keys associated with their values. - entries: Vec, +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(entries: Vec, name: impl Into) -> Self { - Self { + 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 { - MapRepresentation { - entries: self.entries, - identifier: FieldIdentifier { - name: self.identifier.name, - description: Some(description.into()), + 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()), + }, }, } } @@ -497,36 +519,60 @@ impl MapRepresentation { /// Returns an iterator over all of the storage entries' placeholder keys, alongside their /// expected type. pub fn template_requirements(&self) -> TemplateRequirementsIter<'_> { - Box::new( - self.entries - .iter() - .flat_map(move |entry| entry.template_requirements(self.identifier.name.clone())), - ) + 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] { - &self.entries + 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 { - &self.identifier.name + match self { + MapRepresentation::Template { identifier } + | MapRepresentation::Value { identifier, .. } => &identifier.name, + } } /// Returns a reference to the field's description. pub fn description(&self) -> Option<&String> { - self.identifier.description.as_ref() + match self { + MapRepresentation::Template { identifier } + | MapRepresentation::Value { identifier, .. } => identifier.description.as_ref(), + } } - /// Returns the number of key-value pairs in the map. + /// Returns the number of statically defined key-value pairs in the map. pub fn len(&self) -> usize { - self.entries.len() + match self { + MapRepresentation::Value { entries, .. } => entries.len(), + MapRepresentation::Template { .. } => 0, + } } - /// Returns `true` if there are no entries in the map. + /// Returns `true` if there are no statically defined entries in the map. pub fn is_empty(&self) -> bool { - self.entries.is_empty() + match self { + MapRepresentation::Value { entries, .. } => entries.is_empty(), + MapRepresentation::Template { .. } => true, + } } /// Attempts to convert the [MapRepresentation] into a [StorageMap]. @@ -537,66 +583,98 @@ impl MapRepresentation { &self, init_storage_data: &InitStorageData, ) -> Result { - let entries = self - .entries - .iter() - .map(|map_entry| { - let key = map_entry - .key() - .try_build_word(init_storage_data, self.identifier.name.clone())?; - let value = map_entry - .value() - .try_build_word(init_storage_data, self.identifier.name.clone())?; - Ok((key, value)) - }) - .collect::, _>>()?; - - StorageMap::with_entries(entries) - .map_err(|err| AccountComponentTemplateError::StorageMapHasDuplicateKeys(Box::new(err))) - } - - /// Validates map keys by checking for duplicates. - /// - /// Because keys can be represented in a variety of ways, the `to_string()` implementation is - /// used to check for duplicates. - pub(crate) fn validate(&self) -> Result<(), AccountComponentTemplateError> { - let mut seen_keys = BTreeSet::new(); - for entry in self.entries() { - 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"), - ))); - } - } + 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)) + }); + } - Ok(()) + Err(AccountComponentTemplateError::PlaceholderValueNotProvided( + identifier.name.clone(), + )) + }, + } } -} -impl From for Vec { - fn from(value: MapRepresentation) -> Self { - value.entries + /// 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) { - self.entries.write_into(target); - target.write(&self.identifier); + 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 entries = Vec::::read_from(source)?; - let identifier = FieldIdentifier::read_from(source)?; - Ok(Self { entries, identifier }) + 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" + ))), + } } } 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 index b8d6693125..7bf195ad07 100644 --- 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 @@ -1,7 +1,9 @@ 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 @@ -10,8 +12,12 @@ use super::StorageValueName; /// 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. - storage_placeholders: BTreeMap, + value_entries: BTreeMap, + /// A mapping of map placeholder names to their corresponding key/value entries. + map_entries: BTreeMap>, } impl InitStorageData { @@ -23,23 +29,35 @@ impl InitStorageData { /// # Parameters /// /// - `entries`: An iterable collection of key-value pairs. - pub fn new(entries: impl IntoIterator) -> Self { + /// - `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 { - storage_placeholders: entries - .into_iter() - .filter(|(entry_name, _)| !entry_name.as_str().is_empty()) - .collect(), + value_entries, + map_entries: map_entries.into_iter().collect(), } } /// Retrieves a reference to the storage placeholders. pub fn placeholders(&self) -> &BTreeMap { - &self.storage_placeholders + &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.storage_placeholders.get(key) + self.value_entries.get(key) + } + + /// Returns the map entries associated with the given placeholder name, if any. + pub(crate) 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 index af7d14a22f..e75e4c5ebc 100644 --- a/crates/miden-objects/src/account/component/template/storage/mod.rs +++ b/crates/miden-objects/src/account/component/template/storage/mod.rs @@ -372,7 +372,7 @@ impl Deserializable for MapEntry { #[cfg(test)] mod tests { - use alloc::collections::BTreeSet; + use alloc::collections::{BTreeMap, BTreeSet}; use alloc::string::ToString; use core::error::Error; use core::panic; @@ -421,7 +421,7 @@ mod tests { let test_word: Word = word!("0x000001"); let test_word = test_word.map(FeltRepresentation::from); - let map_representation = MapRepresentation::new( + let map_representation = MapRepresentation::new_value( vec![ MapEntry { key: WordRepresentation::new_template( @@ -496,7 +496,7 @@ mod tests { supported_types: BTreeSet::from([AccountType::FungibleFaucet]), storage, }; - let toml = config.as_toml().unwrap(); + let toml = config.to_toml().unwrap(); let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap(); assert_eq!(deserialized, config); @@ -570,18 +570,21 @@ mod tests { 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()), - ]); + 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!( @@ -594,18 +597,21 @@ mod tests { ); // 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()), - ]); + 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!( @@ -658,6 +664,234 @@ mod tests { assert_matches::assert_matches!(err, AccountComponentTemplateError::InvalidType(_, _)) } + #[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 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#" diff --git a/crates/miden-objects/src/account/component/template/storage/placeholder.rs b/crates/miden-objects/src/account/component/template/storage/placeholder.rs index 548a2d9395..6508417ab1 100644 --- a/crates/miden-objects/src/account/component/template/storage/placeholder.rs +++ b/crates/miden-objects/src/account/component/template/storage/placeholder.rs @@ -7,7 +7,6 @@ use core::fmt::{self, Display}; use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use miden_core::{Felt, Word}; use miden_crypto::dsa::rpo_falcon512::{self}; -use miden_crypto::word::parse_hex_string_as_word; use miden_processor::DeserializationError; use thiserror::Error; @@ -42,7 +41,7 @@ pub static TEMPLATE_REGISTRY: LazyLock = LazyLock::new(|| { /// 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. +/// 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))] @@ -56,7 +55,7 @@ impl StorageValueName { /// 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 (`.`). + /// 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 (`-`). /// @@ -237,6 +236,11 @@ impl 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 @@ -374,20 +378,35 @@ impl TemplateFelt for TokenSymbol { #[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 { - parse_hex_string_as_word(input) - .map_err(|err| { - TemplateTypeError::parse( - Self::type_name().as_str(), - Self::type_name(), - WordParseError(err.into()), - ) - }) - .map(Word::from) + 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()), + ) + }) } } @@ -396,15 +415,15 @@ impl TemplateWord for rpo_falcon512::PublicKey { TemplateType::new("auth::rpo_falcon512::pub_key").expect("type is well formed") } fn parse_word(input: &str) -> Result { - parse_hex_string_as_word(input) - .map_err(|err| { - TemplateTypeError::parse( - input.to_string(), - Self::type_name(), - WordParseError(err.into()), - ) - }) - .map(Word::from) + 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()), + ) + }) } } diff --git a/crates/miden-objects/src/account/component/template/storage/toml.rs b/crates/miden-objects/src/account/component/template/storage/toml.rs index 8153ab4fdd..f1e1b1a49c 100644 --- a/crates/miden-objects/src/account/component/template/storage/toml.rs +++ b/crates/miden-objects/src/account/component/template/storage/toml.rs @@ -3,7 +3,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt; -use miden_core::Felt; +use miden_core::{Felt, Word}; use serde::de::value::MapAccessDeserializer; use serde::de::{self, Error, MapAccess, SeqAccess, Visitor}; use serde::ser::{SerializeMap, SerializeStruct}; @@ -25,7 +25,6 @@ use crate::account::component::FieldIdentifier; use crate::account::component::template::storage::placeholder::{TEMPLATE_REGISTRY, TemplateFelt}; use crate::account::{AccountComponentMetadata, StorageValueName}; use crate::errors::AccountComponentTemplateError; -use crate::utils::parse_hex_string_as_word; // ACCOUNT COMPONENT METADATA TOML FROM/TO // ================================================================================================ @@ -48,7 +47,7 @@ impl AccountComponentMetadata { } /// Serializes the account component template into a TOML string. - pub fn as_toml(&self) -> Result { + pub fn to_toml(&self) -> Result { let toml = toml::to_string(self).map_err(AccountComponentTemplateError::TomlSerializationError)?; Ok(toml) @@ -105,13 +104,13 @@ impl<'de> Deserialize<'de> for WordRepresentation { where E: Error, { - let parsed_value = parse_hex_string_as_word(value).map_err(|_err| { + let parsed_value = Word::parse(value).map_err(|_err| { E::invalid_value( serde::de::Unexpected::Str(value), &"a valid hexadecimal string", ) })?; - Ok(parsed_value.into()) + Ok(<[Felt; _]>::from(&parsed_value).into()) } fn visit_string(self, value: String) -> Result @@ -316,14 +315,25 @@ impl From for RawStorageEntry { ..Default::default() }, }, - StorageEntry::Map { slot, map } => RawStorageEntry { - slot: Some(slot), - identifier: Some(FieldIdentifier { - name: map.name().clone(), - description: map.description().cloned(), - }), - values: Some(StorageValues::MapEntries(map.into())), - ..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 { @@ -369,11 +379,30 @@ impl<'de> Deserialize<'de> for StorageEntry { 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"))?; - let mut map = MapRepresentation::new(map_entries, name); + 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; + let mut map = 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 @@ -426,10 +455,17 @@ impl InitStorageData { /// - 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 placeholders = BTreeMap::new(); + 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 placeholders)?; - Ok(InitStorageData::new(placeholders)) + 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. @@ -439,8 +475,9 @@ impl InitStorageData { /// an error is returned. Arrays are not supported. fn flatten_parse_toml_value( prefix: StorageValueName, - value: &toml::Value, - map: &mut BTreeMap, + value: toml::Value, + value_entries: &mut BTreeMap, + map_entries: &mut BTreeMap>, ) -> Result<(), InitStorageDataError> { match value { toml::Value::Table(table) => { @@ -453,19 +490,35 @@ impl InitStorageData { 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, map)?; + 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(_) => { - return Err(InitStorageDataError::ArraysNotSupported); + 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(), - _ => value.to_string(), + _ => toml_value.to_string(), }; - map.insert(prefix, value); + value_entries.insert(prefix, value); }, } Ok(()) @@ -485,6 +538,9 @@ pub enum InitStorageDataError { #[error("invalid storage value name")] InvalidStorageValueName(#[source] StorageValueNameError), + + #[error("invalid map entry: {0}")] + InvalidMapEntry(String), } impl Serialize for FieldIdentifier { @@ -577,6 +633,34 @@ fn parse_field_identifier( }) } +/// 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 // ================================================================================================ @@ -645,6 +729,30 @@ mod tests { ); } + #[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#" diff --git a/crates/miden-objects/src/account/delta/mod.rs b/crates/miden-objects/src/account/delta/mod.rs index e33600735f..df1e245791 100644 --- a/crates/miden-objects/src/account/delta/mod.rs +++ b/crates/miden-objects/src/account/delta/mod.rs @@ -1,10 +1,18 @@ use alloc::string::ToString; use alloc::vec::Vec; -use crate::account::{Account, AccountId}; +use crate::account::{ + Account, + AccountCode, + AccountId, + AccountStorage, + StorageSlot, + StorageSlotType, +}; +use crate::asset::AssetVault; use crate::crypto::SequentialCommit; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{AccountDeltaError, Felt, Word, ZERO}; +use crate::{AccountDeltaError, AccountError, Felt, Word, ZERO}; mod storage; pub use storage::{AccountStorageDelta, StorageMapDelta}; @@ -24,12 +32,19 @@ pub use vault::{ /// one or more transaction. /// /// The differences are represented as follows: -/// - storage: an [AccountStorageDelta] that contains the changes to the account storage. -/// - vault: an [AccountVaultDelta] object that contains the changes to the account vault. +/// - storage: an [`AccountStorageDelta`] that contains the changes to the account storage. +/// - vault: an [`AccountVaultDelta`] object that contains the changes to the account vault. /// - nonce: if the nonce of the account has changed, the _delta_ of the nonce is stored, i.e. the /// value by which the nonce increased. +/// - code: an [`AccountCode`] for new accounts and `None` for others. +/// +/// The presence of the code in a delta signals if the delta is a _full state_ or _partial state_ +/// delta. A full state delta must be converted into an [`Account`] object, while a partial state +/// delta must be applied to an existing [`Account`]. /// -/// TODO: add ability to trace account code updates. +/// TODO(code_upgrades): The ability to track account code updates is an outstanding feature. For +/// that reason, the account code is not considered as part of the "nonce must be incremented if +/// state changed" check. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AccountDelta { /// The ID of the account to which this delta applies. If the delta is created during @@ -39,6 +54,8 @@ pub struct AccountDelta { storage: AccountStorageDelta, /// The delta of the account's asset vault. vault: AccountVaultDelta, + /// The code of a new account (`Some`) or `None` for existing accounts. + code: Option, /// The value by which the nonce was incremented. Must be greater than zero if storage or vault /// are non-empty. nonce_delta: Felt, @@ -47,12 +64,12 @@ pub struct AccountDelta { impl AccountDelta { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- + /// Returns new [AccountDelta] instantiated from the provided components. /// /// # Errors /// - /// - Returns an error if storage or vault were updated, but the nonce was either not updated or - /// set to 0. + /// - Returns an error if storage or vault were updated, but the nonce_delta is 0. pub fn new( account_id: AccountId, storage: AccountStorageDelta, @@ -62,7 +79,13 @@ impl AccountDelta { // nonce must be updated if either account storage or vault were updated validate_nonce(nonce_delta, &storage, &vault)?; - Ok(Self { account_id, storage, vault, nonce_delta }) + Ok(Self { + account_id, + storage, + vault, + code: None, + nonce_delta, + }) } // PUBLIC MUTATORS @@ -80,6 +103,17 @@ impl AccountDelta { }); } + // TODO(code_upgrades): This should go away once we have proper account code updates in + // deltas. Then, the two code updates can be merged. For now, code cannot be merged + // and this should never happen. + if self.is_full_state() && other.is_full_state() { + return Err(AccountDeltaError::MergingFullStateDeltas); + } + + if let Some(code) = other.code { + self.code = Some(code); + } + self.nonce_delta = new_nonce_delta; self.storage.merge(other.storage)?; @@ -91,6 +125,12 @@ impl AccountDelta { &mut self.vault } + /// Sets the [`AccountCode`] of the delta. + pub fn with_code(mut self, code: Option) -> Self { + self.code = code; + self + } + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -99,6 +139,17 @@ impl AccountDelta { self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO } + /// Returns `true` if this delta is a "full state" delta, `false` otherwise, i.e. if it is a + /// "partial state" delta. + /// + /// See the type-level docs for more on this distinction. + pub fn is_full_state(&self) -> bool { + // TODO(code_upgrades): Change this to another detection mechanism once we have code upgrade + // support, at which point the presence of code may not be enough of an indication + // that a delta can be converted to a full account. + self.code.is_some() + } + /// Returns storage updates for this account delta. pub fn storage(&self) -> &AccountStorageDelta { &self.storage @@ -119,16 +170,23 @@ impl AccountDelta { self.account_id } + /// Returns a reference to the account code of this delta, if present. + pub fn code(&self) -> Option<&AccountCode> { + self.code.as_ref() + } + /// Converts this storage delta into individual delta components. - pub fn into_parts(self) -> (AccountStorageDelta, AccountVaultDelta, Felt) { - (self.storage, self.vault, self.nonce_delta) + pub fn into_parts(self) -> (AccountStorageDelta, AccountVaultDelta, Option, Felt) { + (self.storage, self.vault, self.code, self.nonce_delta) } /// Computes the commitment to the account delta. /// + /// # Computation + /// /// The delta is a sequential hash over a vector of field elements which starts out empty and - /// is appended to in the following way. Whenever sorting is expected, it is that of a link map - /// key. The WORD layout is in memory-order. + /// is appended to in the following way. Whenever sorting is expected, it is that of a + /// [`LexicographicWord`](crate::LexicographicWord). The WORD layout is in memory-order. /// /// - Append `[[nonce_delta, 0, account_id_suffix, account_id_prefix], EMPTY_WORD]`, where /// account_id_{prefix,suffix} are the prefix and suffix felts of the native account id and @@ -169,12 +227,12 @@ impl AccountDelta { /// /// # Security /// - /// The general concern with the commitment is that two deltas must never has to the same - /// commitment. E.g. a commitment of a delta that changes a key-value pair in a storage map - /// slot should be different from a delta that adds a non-fungible asset to the vault. If - /// not, a delta can be crafted in the VM that sets a map key but a malicious actor crafts a - /// delta outside the VM that adds a non-fungible asset. To prevent that, a couple of - /// measures are taken. + /// The general concern with the commitment is that two distinct deltas must never hash to the + /// same commitment. E.g. a commitment of a delta that changes a key-value pair in a storage + /// map slot should be different from a delta that adds a non-fungible asset to the vault. + /// If not, a delta can be crafted in the VM that sets a map key but a malicious actor + /// crafts a delta outside the VM that adds a non-fungible asset. To prevent that, a couple + /// of measures are taken. /// /// - Because multiple unrelated contexts (e.g. vaults and storage slots) are hashed in the same /// hasher, domain separators are used to disambiguate. For each changed asset and each @@ -255,6 +313,57 @@ impl AccountDelta { } } +impl TryFrom<&AccountDelta> for Account { + type Error = AccountError; + + /// Converts an [`AccountDelta`] into an [`Account`]. + /// + /// Conceptually, this applies the delta onto an empty account. + /// + /// # Errors + /// + /// Returns an error if: + /// - If the delta is not a full state delta. See [`AccountDelta`] for details. + /// - If any vault delta operation removes an asset. + /// - If any vault delta operation adds an asset that would overflow the maximum representable + /// amount. + /// - If any storage delta update violates account storage constraints. + fn try_from(delta: &AccountDelta) -> Result { + if !delta.is_full_state() { + return Err(AccountError::PartialStateDeltaToAccount); + } + + let Some(code) = delta.code().cloned() else { + return Err(AccountError::PartialStateDeltaToAccount); + }; + + let mut vault = AssetVault::default(); + vault.apply_delta(delta.vault()).map_err(AccountError::AssetVaultUpdateError)?; + + // Once we support addition and removal of storage slots, we may be able to change + // 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, + }; + empty_storage_slots.push(slot); + } + let mut storage = AccountStorage::new(empty_storage_slots) + .expect("storage delta should contain a valid number of slots"); + storage.apply_delta(delta.storage())?; + + // The nonce of the account is the initial nonce of 0 plus the nonce_delta, so the + // nonce_delta itself. + let nonce = delta.nonce_delta(); + + Account::new(delta.id(), vault, storage, code, nonce, None) + } +} + impl SequentialCommit for AccountDelta { type Commitment = Word; @@ -304,21 +413,18 @@ impl SequentialCommit for AccountDelta { /// In particular, private account changes aren't tracked at all; they are represented as /// [`AccountUpdateDetails::Private`]. /// -/// New non-private accounts are included in full and changes to a non-private account are tracked -/// as an [`AccountDelta`]. +/// Non-private accounts are tracked as an [`AccountDelta`]. If the account is new, the delta can be +/// converted into an [`Account`]. If not, the delta can be applied to the existing account using +/// [`Account::apply_delta`]. /// /// Note that these details can represent the changes from one or more transactions in which case -/// the delta is either applied to the new account or deltas are merged together using -/// [`AccountDelta::merge`]. +/// the deltas of each transaction are merged together using [`AccountDelta::merge`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AccountUpdateDetails { - /// Account is private (no on-chain state change). + /// The state update details of a private account is not publicly accessible. Private, - /// The whole state is needed for new accounts. - New(Account), - - /// For existing accounts, only the delta is needed. + /// The state update details of non-private accounts. Delta(AccountDelta), } @@ -336,16 +442,6 @@ impl AccountUpdateDetails { (AccountUpdateDetails::Private, AccountUpdateDetails::Private) => { AccountUpdateDetails::Private }, - (AccountUpdateDetails::New(mut account), AccountUpdateDetails::Delta(delta)) => { - account.apply_delta(&delta).map_err(|err| { - AccountDeltaError::AccountDeltaApplicationFailed { - account_id: account.id(), - source: err, - } - })?; - - AccountUpdateDetails::New(account) - }, (AccountUpdateDetails::Delta(mut delta), AccountUpdateDetails::Delta(new_delta)) => { delta.merge(new_delta)?; AccountUpdateDetails::Delta(delta) @@ -365,7 +461,6 @@ impl AccountUpdateDetails { pub(crate) const fn as_tag_str(&self) -> &'static str { match self { AccountUpdateDetails::Private => "private", - AccountUpdateDetails::New(_) => "new", AccountUpdateDetails::Delta(_) => "delta", } } @@ -379,6 +474,7 @@ impl Serializable for AccountDelta { self.account_id.write_into(target); self.storage.write_into(target); self.vault.write_into(target); + self.code.write_into(target); self.nonce_delta.write_into(target); } @@ -386,6 +482,7 @@ impl Serializable for AccountDelta { self.account_id.get_size_hint() + self.storage.get_size_hint() + self.vault.get_size_hint() + + self.code.get_size_hint() + self.nonce_delta.get_size_hint() } } @@ -395,12 +492,19 @@ impl Deserializable for AccountDelta { let account_id = AccountId::read_from(source)?; let storage = AccountStorageDelta::read_from(source)?; let vault = AccountVaultDelta::read_from(source)?; + let code = >::read_from(source)?; let nonce_delta = Felt::read_from(source)?; validate_nonce(nonce_delta, &storage, &vault) .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; - Ok(Self { account_id, storage, vault, nonce_delta }) + Ok(Self { + account_id, + storage, + vault, + code, + nonce_delta, + }) } } @@ -410,12 +514,8 @@ impl Serializable for AccountUpdateDetails { AccountUpdateDetails::Private => { 0_u8.write_into(target); }, - AccountUpdateDetails::New(account) => { - 1_u8.write_into(target); - account.write_into(target); - }, AccountUpdateDetails::Delta(delta) => { - 2_u8.write_into(target); + 1_u8.write_into(target); delta.write_into(target); }, } @@ -427,7 +527,6 @@ impl Serializable for AccountUpdateDetails { match self { AccountUpdateDetails::Private => u8_size, - AccountUpdateDetails::New(account) => u8_size + account.get_size_hint(), AccountUpdateDetails::Delta(account_delta) => u8_size + account_delta.get_size_hint(), } } @@ -437,10 +536,9 @@ impl Deserializable for AccountUpdateDetails { fn read_from(source: &mut R) -> Result { match u8::read_from(source)? { 0 => Ok(Self::Private), - 1 => Ok(Self::New(Account::read_from(source)?)), - 2 => Ok(Self::Delta(AccountDelta::read_from(source)?)), - v => Err(DeserializationError::InvalidValue(format!( - "Unknown variant {v} for AccountDetails" + 1 => Ok(Self::Delta(AccountDelta::read_from(source)?)), + variant => Err(DeserializationError::InvalidValue(format!( + "Unknown variant {variant} for AccountDetails" ))), } } @@ -616,8 +714,13 @@ mod tests { let account_code = AccountCode::mock(); assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint()); - let account = - Account::from_parts(account_id, asset_vault, account_storage, account_code, Felt::ZERO); + let account = Account::new_existing( + account_id, + asset_vault, + account_storage, + account_code, + Felt::ONE, + ); assert_eq!(account.to_bytes().len(), account.get_size_hint()); // AccountUpdateDetails @@ -627,8 +730,5 @@ mod tests { let update_details_delta = AccountUpdateDetails::Delta(account_delta); assert_eq!(update_details_delta.to_bytes().len(), update_details_delta.get_size_hint()); - - let update_details_new = AccountUpdateDetails::New(account); - assert_eq!(update_details_new.to_bytes().len(), update_details_new.get_size_hint()); } } diff --git a/crates/miden-objects/src/account/delta/storage.rs b/crates/miden-objects/src/account/delta/storage.rs index b2fbb8200d..17779a6441 100644 --- a/crates/miden-objects/src/account/delta/storage.rs +++ b/crates/miden-objects/src/account/delta/storage.rs @@ -12,7 +12,7 @@ use super::{ Serializable, Word, }; -use crate::account::StorageMap; +use crate::account::{StorageMap, StorageSlotType}; use crate::{EMPTY_WORD, Felt, LexicographicWord, ZERO}; // ACCOUNT STORAGE DELTA @@ -59,6 +59,17 @@ impl AccountStorageDelta { 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 @@ -292,13 +303,15 @@ impl StorageMapDelta { } /// 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, key: Word, value: Word) { - self.0.insert(LexicographicWord::new(key), value); + 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. diff --git a/crates/miden-objects/src/account/file.rs b/crates/miden-objects/src/account/file.rs index 2c23b0f242..f882f261e6 100644 --- a/crates/miden-objects/src/account/file.rs +++ b/crates/miden-objects/src/account/file.rs @@ -15,7 +15,8 @@ use super::super::utils::serde::{ DeserializationError, Serializable, }; -use super::{Account, AuthSecretKey, Word}; +use super::Account; +use super::auth::AuthSecretKey; const MAGIC: &str = "acct"; @@ -33,21 +34,12 @@ const MAGIC: &str = "acct"; #[derive(Debug, Clone)] pub struct AccountFile { pub account: Account, - pub account_seed: Option, pub auth_secret_keys: Vec, } impl AccountFile { - pub fn new( - account: Account, - account_seed: Option, - auth_keys: Vec, - ) -> Self { - Self { - account, - account_seed, - auth_secret_keys: auth_keys, - } + pub fn new(account: Account, auth_keys: Vec) -> Self { + Self { account, auth_secret_keys: auth_keys } } } @@ -76,14 +68,9 @@ impl AccountFile { impl Serializable for AccountFile { fn write_into(&self, target: &mut W) { target.write_bytes(MAGIC.as_bytes()); - let AccountFile { - account, - account_seed, - auth_secret_keys: auth, - } = self; + let AccountFile { account, auth_secret_keys: auth } = self; account.write_into(target); - account_seed.write_into(target); auth.write_into(target); } } @@ -97,10 +84,9 @@ impl Deserializable for AccountFile { ))); } let account = Account::read_from(source)?; - let account_seed = >::read_from(source)?; let auth_secret_keys = >::read_from(source)?; - Ok(Self::new(account, account_seed, auth_secret_keys)) + Ok(Self::new(account, auth_secret_keys)) } fn read_from_bytes(bytes: &[u8]) -> Result { @@ -113,14 +99,14 @@ impl Deserializable for AccountFile { #[cfg(test)] mod tests { - use miden_crypto::dsa::rpo_falcon512::SecretKey; use miden_crypto::utils::{Deserializable, Serializable}; use storage::AccountStorage; #[cfg(feature = "std")] use tempfile::tempdir; use super::AccountFile; - use crate::account::{Account, AccountCode, AccountId, AuthSecretKey, Felt, Word, storage}; + use crate::account::auth::AuthSecretKey; + use crate::account::{Account, AccountCode, AccountId, Felt, storage}; use crate::asset::AssetVault; use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; @@ -131,13 +117,12 @@ mod tests { // create account and auth let vault = AssetVault::new(&[]).unwrap(); let storage = AccountStorage::new(vec![]).unwrap(); - let nonce = Felt::new(0); - let account = Account::from_parts(id, vault, storage, code, nonce); - let account_seed = Some(Word::empty()); - let auth_secret_key = AuthSecretKey::RpoFalcon512(SecretKey::new()); - let auth_secret_key_2 = AuthSecretKey::RpoFalcon512(SecretKey::new()); + 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(); - AccountFile::new(account, account_seed, vec![auth_secret_key, auth_secret_key_2]) + AccountFile::new(account, vec![auth_secret_key, auth_secret_key_2]) } #[test] @@ -146,7 +131,6 @@ mod tests { let serialized = account_file.to_bytes(); let deserialized = AccountFile::read_from_bytes(&serialized).unwrap(); assert_eq!(deserialized.account, account_file.account); - assert_eq!(deserialized.account_seed, account_file.account_seed); assert_eq!( deserialized.auth_secret_keys.to_bytes(), account_file.auth_secret_keys.to_bytes() @@ -164,7 +148,6 @@ mod tests { let deserialized = AccountFile::read(filepath.as_path()).unwrap(); assert_eq!(deserialized.account, account_file.account); - assert_eq!(deserialized.account_seed, account_file.account_seed); assert_eq!( deserialized.auth_secret_keys.to_bytes(), account_file.auth_secret_keys.to_bytes() diff --git a/crates/miden-objects/src/account/mod.rs b/crates/miden-objects/src/account/mod.rs index d3555e2586..0918e43ce8 100644 --- a/crates/miden-objects/src/account/mod.rs +++ b/crates/miden-objects/src/account/mod.rs @@ -1,4 +1,9 @@ -use crate::asset::AssetVault; +use alloc::collections::BTreeMap; +use alloc::string::ToString; + +use miden_core::LexicographicWord; + +use crate::asset::{Asset, AssetVault}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -17,13 +22,10 @@ pub use account_id::{ AccountIdVersion, AccountStorageMode, AccountType, - NetworkId, }; pub mod auth; -pub use auth::AuthSecretKey; - mod builder; pub use builder::AccountBuilder; @@ -66,7 +68,9 @@ pub use storage::{ AccountStorageHeader, PartialStorage, PartialStorageMap, + SlotName, StorageMap, + StorageMapWitness, StorageSlot, StorageSlotType, }; @@ -108,21 +112,50 @@ pub struct Account { storage: AccountStorage, code: AccountCode, nonce: Felt, + seed: Option, } impl Account { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Returns an [Account] instantiated with the provided components. - pub fn from_parts( + /// Returns an [`Account`] instantiated with the provided components. + /// + /// # Errors + /// + /// Returns an error if: + /// - an account seed is provided but the account's nonce indicates the account already exists. + /// - an account seed is not provided but the account's nonce indicates the account is new. + /// - an account seed is provided but the account ID derived from it is invalid or does not + /// match the provided account's ID. + pub fn new( id: AccountId, vault: AssetVault, storage: AccountStorage, code: AccountCode, nonce: Felt, + seed: Option, + ) -> Result { + validate_account_seed(id, code.commitment(), storage.commitment(), seed, nonce)?; + + Ok(Self::new_unchecked(id, vault, storage, code, nonce, seed)) + } + + /// Returns an [`Account`] instantiated with the provided components. + /// + /// # Warning + /// + /// This does not check that the provided seed is valid with respect to the provided components. + /// Prefer using [`Account::new`] whenever possible. + pub fn new_unchecked( + id: AccountId, + vault: AssetVault, + storage: AccountStorage, + code: AccountCode, + nonce: Felt, + seed: Option, ) -> Self { - Self { id, vault, storage, code, nonce } + Self { id, vault, storage, code, nonce, seed } } /// Creates an account's [`AccountCode`] and [`AccountStorage`] from the provided components. @@ -213,7 +246,7 @@ impl Account { /// [crate::EMPTY_WORD] to distinguish new accounts from existing accounts. The actual /// commitment of the initial account state (and the initial state itself), are provided to /// the VM via the advice provider. - pub fn init_commitment(&self) -> Word { + pub fn initial_commitment(&self) -> Word { if self.is_new() { Word::empty() } else { @@ -251,6 +284,13 @@ impl Account { self.nonce } + /// Returns the seed of the account's ID if the account is new. + /// + /// That is, if [`Account::is_new`] returns `true`, the seed will be `Some`. + pub fn seed(&self) -> Option { + self.seed + } + /// Returns true if this account can issue assets. pub fn is_faucet(&self) -> bool { self.id.is_faucet() @@ -261,10 +301,10 @@ impl Account { self.id.is_regular_account() } - /// Returns `true` if the full state of the account is on chain, i.e. if the storage modes are + /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise. - pub fn is_onchain(&self) -> bool { - self.id().is_onchain() + pub fn has_public_state(&self) -> bool { + self.id().has_public_state() } /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise. @@ -282,14 +322,19 @@ impl Account { self.id().is_network() } - /// Returns true if the account is new (i.e. it has not been initialized yet). + /// Returns `true` if the account is new, `false` otherwise. + /// + /// An account is considered new if the account's nonce is zero and it hasn't been registered on + /// chain yet. pub fn is_new(&self) -> bool { self.nonce == ZERO } /// Decomposes the account into the underlying account components. - pub fn into_parts(self) -> (AccountId, AssetVault, AccountStorage, AccountCode, Felt) { - (self.id, self.vault, self.storage, self.code, self.nonce) + pub fn into_parts( + self, + ) -> (AccountId, AssetVault, AccountStorage, AccountCode, Felt, Option) { + (self.id, self.vault, self.storage, self.code, self.nonce, self.seed) } // DATA MUTATORS @@ -299,12 +344,19 @@ impl Account { /// to the values specified by the delta. /// /// # Errors + /// /// Returns an error if: + /// - [`AccountDelta::is_full_state`] returns `true`, i.e. represents the state of an entire + /// account. Only partial state deltas can be applied to an account. /// - Applying vault sub-delta to the vault of this account fails. /// - Applying storage sub-delta to the storage of this account fails. /// - The nonce specified in the provided delta smaller than or equal to the current account /// nonce. pub fn apply_delta(&mut self, delta: &AccountDelta) -> Result<(), AccountError> { + if delta.is_full_state() { + return Err(AccountError::ApplyFullStateDeltaToAccount); + } + // update vault; we don't check vault delta validity here because `AccountDelta` can contain // only valid vault deltas self.vault @@ -339,6 +391,14 @@ impl Account { self.nonce = new_nonce; + // Maintain internal consistency of the account, i.e. the seed should not be present for + // existing accounts, where existing accounts are defined as having a nonce > 0. + // If we've incremented the nonce, then we should remove the seed (if it was present at + // all). + if !self.is_new() { + self.seed = None; + } + Ok(()) } @@ -358,18 +418,93 @@ impl Account { } } +impl TryFrom for AccountDelta { + type Error = AccountError; + + /// Converts an [`Account`] into an [`AccountDelta`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the account has a seed. Accounts with seeds have a nonce of 0. Representing such accounts + /// as deltas is not possible because deltas with a non-empty state change need a nonce_delta + /// greater than 0. + fn try_from(account: Account) -> Result { + let Account { id, vault, storage, code, nonce, seed } = account; + + if seed.is_some() { + 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 mut fungible_delta = FungibleAssetDelta::default(); + let mut non_fungible_delta = NonFungibleAssetDelta::default(); + for asset in vault.assets() { + // SAFETY: All assets in the account vault should be representable in the delta. + match asset { + Asset::Fungible(fungible_asset) => { + fungible_delta + .add(fungible_asset) + .expect("delta should allow representing valid fungible assets"); + }, + Asset::NonFungible(non_fungible_asset) => { + non_fungible_delta + .add(non_fungible_asset) + .expect("delta should allow representing valid non-fungible assets"); + }, + } + } + let vault_delta = AccountVaultDelta::new(fungible_delta, non_fungible_delta); + + // The nonce of the account is the nonce delta since adding the nonce_delta to 0 would + // result in the nonce. + let nonce_delta = nonce; + + // SAFETY: As checked earlier, the nonce delta should be greater than 0 allowing for + // non-empty state changes. + let delta = AccountDelta::new(id, storage_delta, vault_delta, nonce_delta) + .expect("nonce_delta should be greater than 0") + .with_code(Some(code)); + + Ok(delta) + } +} + // SERIALIZATION // ================================================================================================ impl Serializable for Account { fn write_into(&self, target: &mut W) { - let Account { id, vault, storage, code, nonce } = self; + let Account { id, vault, storage, code, nonce, seed } = self; id.write_into(target); vault.write_into(target); storage.write_into(target); code.write_into(target); nonce.write_into(target); + seed.write_into(target); } fn get_size_hint(&self) -> usize { @@ -378,6 +513,7 @@ impl Serializable for Account { + self.storage.get_size_hint() + self.code.get_size_hint() + self.nonce.get_size_hint() + + self.seed.get_size_hint() } } @@ -388,8 +524,10 @@ impl Deserializable for Account { let storage = AccountStorage::read_from(source)?; let code = AccountCode::read_from(source)?; let nonce = Felt::read_from(source)?; + let seed = >::read_from(source)?; - Ok(Self::from_parts(id, vault, storage, code, nonce)) + Self::new(id, vault, storage, code, nonce, seed) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -418,6 +556,40 @@ pub fn hash_account( Hasher::hash_elements(&elements) } +// HELPER FUNCTIONS +// ================================================================================================ + +/// Validates that the provided seed is valid for the provided account components. +pub(super) fn validate_account_seed( + id: AccountId, + code_commitment: Word, + storage_commitment: Word, + seed: Option, + nonce: Felt, +) -> Result<(), AccountError> { + let account_is_new = nonce == ZERO; + + match (account_is_new, seed) { + (true, Some(seed)) => { + let account_id = + AccountId::new(seed, id.version(), code_commitment, storage_commitment) + .map_err(AccountError::SeedConvertsToInvalidAccountId)?; + + if account_id != id { + return Err(AccountError::AccountIdSeedMismatch { + expected: id, + actual: account_id, + }); + } + + Ok(()) + }, + (true, None) => Err(AccountError::NewAccountMissingSeed), + (false, Some(_)) => Err(AccountError::ExistingAccountWithSeed), + (false, None) => Ok(()), + } +} + /// Validates that all `components` support the given `account_type`. fn validate_components_support_account_type( components: &[AccountComponent], @@ -444,6 +616,7 @@ mod tests { use assert_matches::assert_matches; use miden_assembly::Assembler; + use miden_core::FieldElement; use miden_crypto::utils::{Deserializable, Serializable}; use miden_crypto::{Felt, Word}; @@ -456,10 +629,14 @@ mod tests { AccountVaultDelta, }; use crate::AccountError; + use crate::account::AccountStorageMode::Network; use crate::account::{ Account, + AccountBuilder, AccountComponent, + AccountIdVersion, AccountType, + PartialAccount, StorageMap, StorageMapDelta, StorageSlot, @@ -469,6 +646,7 @@ mod tests { 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; @@ -552,7 +730,7 @@ mod tests { let updated_map = StorageMapDelta::from_iters([], [(new_map_entry.0, new_map_entry.1.into())]); - storage_map.insert(new_map_entry.0, new_map_entry.1.into()); + storage_map.insert(new_map_entry.0, new_map_entry.1.into()).unwrap(); // build account delta let final_nonce = Felt::new(2); @@ -676,7 +854,7 @@ mod tests { let storage = AccountStorage::new(slots).unwrap(); - Account::from_parts(id, vault, storage, code, nonce) + Account::new_existing(id, vault, storage, code, nonce) } /// Tests that initializing code and storage from a component which does not support the given @@ -729,4 +907,87 @@ mod tests { assert_matches!(err, AccountError::AccountComponentDuplicateProcedureRoot(_)) } + + /// Tests all cases of account ID seed validation. + #[test] + fn seed_validation() -> anyhow::Result<()> { + let account = AccountBuilder::new([5; 32]) + .with_auth_component(NoopAuthComponent) + .with_component(AddComponent) + .build()?; + let (id, vault, storage, code, _nonce, seed) = account.into_parts(); + assert!(seed.is_some()); + + let other_seed = AccountId::compute_account_seed( + [9; 32], + AccountType::FungibleFaucet, + Network, + AccountIdVersion::Version0, + code.commitment(), + storage.commitment(), + )?; + + // Set nonce to 1 so the account is considered existing and provide the seed. + let err = Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ONE, seed) + .unwrap_err(); + assert_matches!(err, AccountError::ExistingAccountWithSeed); + + // Set nonce to 0 so the account is considered new but don't provide the seed. + let err = Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ZERO, None) + .unwrap_err(); + assert_matches!(err, AccountError::NewAccountMissingSeed); + + // Set nonce to 0 so the account is considered new and provide a valid seed that results in + // a different ID than the provided one. + let err = Account::new( + id, + vault.clone(), + storage.clone(), + code.clone(), + Felt::ZERO, + Some(other_seed), + ) + .unwrap_err(); + assert_matches!(err, AccountError::AccountIdSeedMismatch { .. }); + + // Set nonce to 0 so the account is considered new and provide a seed that results in an + // invalid ID. + let err = Account::new( + id, + vault.clone(), + storage.clone(), + code.clone(), + Felt::ZERO, + Some(Word::from([1, 2, 3, 4u32])), + ) + .unwrap_err(); + assert_matches!(err, AccountError::SeedConvertsToInvalidAccountId(_)); + + // Set nonce to 1 so the account is considered existing and don't provide the seed, which + // should be valid. + Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ONE, None)?; + + // Set nonce to 0 so the account is considered new and provide the original seed, which + // should be valid. + Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ZERO, seed)?; + + Ok(()) + } + + #[test] + fn incrementing_nonce_should_remove_seed() -> anyhow::Result<()> { + let mut account = AccountBuilder::new([5; 32]) + .with_auth_component(NoopAuthComponent) + .with_component(AddComponent) + .build()?; + account.increment_nonce(Felt::ONE)?; + + assert_matches!(account.seed(), None); + + // Sanity check: We should be able to convert the account into a partial account which will + // re-check the internal seed - nonce consistency. + let _partial_account = PartialAccount::from(&account); + + Ok(()) + } } diff --git a/crates/miden-objects/src/account/partial.rs b/crates/miden-objects/src/account/partial.rs index 914fcfd891..fd1161f07e 100644 --- a/crates/miden-objects/src/account/partial.rs +++ b/crates/miden-objects/src/account/partial.rs @@ -1,48 +1,77 @@ -use miden_core::Felt; +use alloc::string::ToString; + use miden_core::utils::{Deserializable, Serializable}; +use miden_core::{Felt, ZERO}; use super::{Account, AccountCode, AccountId, PartialStorage}; +use crate::account::{hash_account, validate_account_seed}; use crate::asset::PartialVault; +use crate::utils::serde::DeserializationError; +use crate::{AccountError, Word}; /// A partial representation of an account. /// /// A partial account is used as inputs to the transaction kernel and contains only the essential /// data needed for verification and transaction processing without requiring the full account /// state. +/// +/// For new accounts, the partial storage must be the full initial account storage. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PartialAccount { /// The ID for the partial account id: AccountId, - /// The current transaction nonce of the account - nonce: Felt, - /// Account code - account_code: AccountCode, - /// Partial representation of the account's storage, containing the storage commitment and - /// proofs for specific storage slots that need to be accessed - partial_storage: PartialStorage, /// Partial representation of the account's vault, containing the vault root and necessary /// proof information for asset verification partial_vault: PartialVault, + /// Partial representation of the account's storage, containing the storage commitment and + /// proofs for specific storage slots that need to be accessed + partial_storage: PartialStorage, + /// Account code + code: AccountCode, + /// The current transaction nonce of the account + nonce: Felt, + /// The seed of the account ID, if any. + seed: Option, } impl PartialAccount { - /// Creates a new instance of a partial account with the specified components. + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`PartialAccount`] with the provided account parts and seed. + /// + /// # Errors + /// + /// Returns an error if: + /// - an account seed is provided but the account's nonce indicates the account already exists. + /// - an account seed is not provided but the account's nonce indicates the account is new. + /// - an account seed is provided but the account ID derived from it is invalid or does not + /// match the provided ID. pub fn new( id: AccountId, nonce: Felt, - account_code: AccountCode, + code: AccountCode, partial_storage: PartialStorage, partial_vault: PartialVault, - ) -> Self { - Self { + seed: Option, + ) -> Result { + validate_account_seed(id, code.commitment(), partial_storage.commitment(), seed, nonce)?; + + let account = Self { id, nonce, - account_code, + code, partial_storage, partial_vault, - } + seed, + }; + + Ok(account) } + // ACCESSORS + // -------------------------------------------------------------------------------------------- + /// Returns the account's unique identifier. pub fn id(&self) -> AccountId { self.id @@ -55,7 +84,7 @@ impl PartialAccount { /// Returns a reference to the account code. pub fn code(&self) -> &AccountCode { - &self.account_code + &self.code } /// Returns a reference to the partial storage representation of the account. @@ -67,23 +96,108 @@ impl PartialAccount { pub fn vault(&self) -> &PartialVault { &self.partial_vault } -} -impl From for PartialAccount { - fn from(account: Account) -> Self { - PartialAccount::from(&account) + /// Returns the seed of the account's ID if the account is new. + /// + /// That is, if [`PartialAccount::is_new`] returns `true`, the seed will be `Some`. + pub fn seed(&self) -> Option { + self.seed + } + + /// Returns `true` if the account is new, `false` otherwise. + /// + /// An account is considered new if the account's nonce is zero and it hasn't been registered on + /// chain yet. + pub fn is_new(&self) -> bool { + self.nonce == ZERO + } + + /// Returns the commitment of this account. + /// + /// The commitment of an account is computed as: + /// + /// ```text + /// hash(id, nonce, vault_root, storage_commitment, code_commitment). + /// ``` + pub fn commitment(&self) -> Word { + hash_account( + self.id, + self.nonce, + self.vault().root(), + self.storage().commitment(), + self.code().commitment(), + ) + } + + /// Returns the commitment of this account as used for the initial account state commitment in + /// transaction proofs. + /// + /// For existing accounts, this is exactly the same as [Account::commitment()], however, for new + /// accounts this value is set to [`Word::empty`]. This is because when a transaction is + /// executed against a new account, public input for the initial account state is set to + /// [`Word::empty`] to distinguish new accounts from existing accounts. The actual + /// commitment of the initial account state (and the initial state itself), are provided to + /// the VM via the advice provider. + pub fn initial_commitment(&self) -> Word { + if self.is_new() { + Word::empty() + } else { + self.commitment() + } + } + + /// Returns `true` if the full state of the account is public on chain, and `false` otherwise. + pub fn has_public_state(&self) -> bool { + self.id.has_public_state() + } + + /// Consumes self and returns the underlying parts of the partial account. + pub fn into_parts( + self, + ) -> (AccountId, PartialVault, PartialStorage, AccountCode, Felt, Option) { + ( + self.id, + self.partial_vault, + self.partial_storage, + self.code, + self.nonce, + self.seed, + ) } } impl From<&Account> for PartialAccount { + /// Constructs a [`PartialAccount`] from the provided account. + /// + /// The behavior is different whether the [`Account::is_new`] or not: + /// - For new accounts, the storage is tracked in full. This is because transactions that create + /// accounts need the full state. + /// - For existing accounts, the storage is tracked minimally, i.e. the minimal necessary data + /// is included. + /// + /// Because new accounts always have empty vaults, in both cases, the asset vault is a minimal + /// representation. + /// + /// For precise control over how an account is converted to a partial account, use + /// [`PartialAccount::new`]. fn from(account: &Account) -> Self { - PartialAccount::new( + let partial_storage = if account.is_new() { + // This is somewhat expensive, but it allows us to do this conversion from &Account and + // it penalizes only the rare case (new accounts). + PartialStorage::new_full(account.storage.clone()) + } else { + PartialStorage::new_minimal(account.storage()) + }; + + Self::new( account.id(), account.nonce(), account.code().clone(), - account.storage().into(), - account.vault().into(), + partial_storage, + PartialVault::new_minimal(account.vault()), + account.seed(), ) + .expect("account should ensure that seed is valid for account") } } @@ -91,9 +205,10 @@ impl Serializable for PartialAccount { fn write_into(&self, target: &mut W) { target.write(self.id); target.write(self.nonce); - target.write(&self.account_code); + target.write(&self.code); target.write(&self.partial_storage); target.write(&self.partial_vault); + target.write(self.seed); } } @@ -106,13 +221,9 @@ impl Deserializable for PartialAccount { let account_code = source.read()?; let partial_storage = source.read()?; let partial_vault = source.read()?; + let seed: Option = source.read()?; - Ok(PartialAccount { - id: account_id, - nonce, - account_code, - partial_storage, - partial_vault, - }) + PartialAccount::new(account_id, nonce, account_code, partial_storage, partial_vault, seed) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } diff --git a/crates/miden-objects/src/account/storage/map/mod.rs b/crates/miden-objects/src/account/storage/map/mod.rs index 529ae29e25..4b262f88ed 100644 --- a/crates/miden-objects/src/account/storage/map/mod.rs +++ b/crates/miden-objects/src/account/storage/map/mod.rs @@ -4,26 +4,29 @@ 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, SmtProof}; +use crate::crypto::merkle::{InnerNodeInfo, LeafIndex, SMT_DEPTH, Smt, SmtLeaf}; use crate::errors::StorageMapError; +use crate::{AccountError, Felt, Hasher}; mod partial; pub use partial::PartialStorageMap; +mod witness; +pub use witness::StorageMapWitness; + // ACCOUNT STORAGE MAP // ================================================================================================ /// Empty storage map root. -pub const EMPTY_STORAGE_MAP_ROOT: Word = *EmptySubtreeRoots::entry(StorageMap::TREE_DEPTH, 0); +pub const EMPTY_STORAGE_MAP_ROOT: Word = *EmptySubtreeRoots::entry(StorageMap::DEPTH, 0); -/// An account storage map is a sparse merkle tree of depth [`Self::TREE_DEPTH`] (64). +/// An account storage map is a sparse merkle tree of depth [`Self::DEPTH`]. /// /// It can be used to store a large amount of data in an account than would be otherwise possible /// using just the account's storage slots. This works by storing the root of the map's underlying /// SMT in one account storage slot. Each map entry is a leaf in the tree and its inclusion is -/// proven while retrieving it (e.g. via `account::get_map_item`). +/// proven while retrieving it (e.g. via `active_account::get_map_item`). /// /// As a side-effect, this also means that _not all_ entries of the map have to be present at /// transaction execution time in order to access or modify the map. It is sufficient if _just_ the @@ -39,19 +42,19 @@ pub const EMPTY_STORAGE_MAP_ROOT: Word = *EmptySubtreeRoots::entry(StorageMap::T pub struct StorageMap { /// The SMT where each key is the hashed original key. smt: Smt, - /// The entries of the map where the key is the original user-chosen one. + /// The entries of the map where the key is the raw user-chosen one. /// /// It is an invariant of this type that the map's entries are always consistent with the SMT's /// entries and vice-versa. - map: BTreeMap, + entries: BTreeMap, } impl StorageMap { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// Depth of the storage tree. - pub const TREE_DEPTH: u8 = SMT_DEPTH; + /// The depth of the SMT that represents the storage map. + pub const DEPTH: u8 = SMT_DEPTH; /// The default value of empty leaves. pub const EMPTY_VALUE: Word = Smt::EMPTY_VALUE; @@ -63,7 +66,10 @@ impl StorageMap { /// /// All leaves in the returned tree are set to [Self::EMPTY_VALUE]. pub fn new() -> Self { - StorageMap { smt: Smt::new(), map: BTreeMap::new() } + StorageMap { + smt: Smt::new(), + entries: BTreeMap::new(), + } } /// Creates a new [`StorageMap`] from the provided key-value entries. @@ -91,12 +97,12 @@ impl StorageMap { } /// Creates a new [`StorageMap`] from the given map. For internal use. - fn from_btree_map(map: BTreeMap) -> Self { - let hashed_keys_iter = map.iter().map(|(key, value)| (Self::hash_key(*key), *value)); + fn from_btree_map(entries: BTreeMap) -> Self { + let hashed_keys_iter = entries.iter().map(|(key, value)| (Self::hash_key(*key), *value)); let smt = Smt::with_entries(hashed_keys_iter) .expect("btree maps should not contain duplicate keys"); - StorageMap { smt, map } + StorageMap { smt, entries } } // PUBLIC ACCESSORS @@ -107,18 +113,39 @@ impl StorageMap { self.smt.root() } + /// Returns the number of non-empty leaves in this storage map. + /// + /// Note that this may return a different value from [Self::num_entries()] as a single leaf may + /// contain more than one key-value pair. + pub fn num_leaves(&self) -> usize { + self.smt.num_leaves() + } + + /// Returns the number of key-value pairs with non-default values in this storage map. + /// + /// Note that this may return a different value from [Self::num_leaves()] as a single leaf may + /// contain more than one key-value pair. + pub fn num_entries(&self) -> usize { + self.smt.num_entries() + } + /// Returns the value corresponding to the key or [`Self::EMPTY_VALUE`] if the key is not /// associated with a value. - pub fn get(&self, key: &Word) -> Word { - self.map.get(key).copied().unwrap_or_default() + pub fn get(&self, raw_key: &Word) -> Word { + self.entries.get(raw_key).copied().unwrap_or_default() } - /// Returns an opening of the leaf associated with `key`. + /// Returns an opening of the leaf associated with raw key. /// /// Conceptually, an opening is a Merkle path to the leaf, as well as the leaf itself. - pub fn open(&self, key: &Word) -> SmtProof { - let key = Self::hash_key(*key); - self.smt.open(&key) + pub fn open(&self, raw_key: &Word) -> StorageMapWitness { + let hashed_map_key = Self::hash_key(*raw_key); + let smt_proof = self.smt.open(&hashed_map_key); + let value = self.entries.get(raw_key).copied().unwrap_or_default(); + + // SAFETY: The key value pair is guaranteed to be present in the provided proof since we + // open its hashed version and because of the guarantees of the storage map. + StorageMapWitness::new_unchecked(smt_proof, [(*raw_key, value)]) } // ITERATORS @@ -129,9 +156,11 @@ impl StorageMap { self.smt.leaves() // Delegate to Smt's leaves method } - /// Returns an iterator over the key value pairs of the map. + /// Returns an iterator over the key-value pairs in this storage map. + /// + /// Note that the returned key is the raw map key. pub fn entries(&self) -> impl Iterator { - self.map.iter() + self.entries.iter() } /// Returns an iterator over the inner nodes of the underlying [`Smt`]. @@ -146,35 +175,44 @@ impl StorageMap { /// [`Self::EMPTY_VALUE`] if no entry was previously present. /// /// If the provided `value` is [`Self::EMPTY_VALUE`] the entry will be removed. - pub fn insert(&mut self, key: Word, value: Word) -> Word { + pub fn insert(&mut self, raw_key: Word, value: Word) -> Result { if value == EMPTY_WORD { - self.map.remove(&key); + self.entries.remove(&raw_key); } else { - self.map.insert(key, value); + self.entries.insert(raw_key, value); } - let key = Self::hash_key(key); - self.smt.insert(key, value) // Delegate to Smt's insert method + let hashed_key = Self::hash_key(raw_key); + self.smt + .insert(hashed_key, value) + .map_err(AccountError::MaxNumStorageMapLeavesExceeded) } /// Applies the provided delta to this account storage. - pub fn apply_delta(&mut self, delta: &StorageMapDelta) -> Word { + pub fn apply_delta(&mut self, delta: &StorageMapDelta) -> Result { // apply the updated and cleared leaves to the storage map for (&key, &value) in delta.entries().iter() { - self.insert(key.into_inner(), value); + self.insert(key.into_inner(), value)?; } - self.root() + Ok(self.root()) } /// Consumes the map and returns the underlying map of entries. pub fn into_entries(self) -> BTreeMap { - self.map + self.entries } /// Hashes the given key to get the key of the SMT. - pub fn hash_key(key: Word) -> Word { - Hasher::hash_elements(key.as_elements()) + 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 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] } } @@ -189,7 +227,7 @@ impl Default for StorageMap { impl Serializable for StorageMap { fn write_into(&self, target: &mut W) { - self.map.write_into(target); + self.entries.write_into(target); } fn get_size_hint(&self) -> usize { @@ -224,6 +262,8 @@ mod tests { (Word::from([105, 106, 107, 108u32]), Word::from([5, 6, 7, 8u32])), ]; let storage_map = StorageMap::with_entries(storage_map_leaves_2).unwrap(); + assert_eq!(storage_map.num_entries(), 2); + assert_eq!(storage_map.num_leaves(), 2); let bytes = storage_map.to_bytes(); let deserialized_map = StorageMap::read_from_bytes(&bytes).unwrap(); diff --git a/crates/miden-objects/src/account/storage/map/partial.rs b/crates/miden-objects/src/account/storage/map/partial.rs index 6a02438f41..92fd97e868 100644 --- a/crates/miden-objects/src/account/storage/map/partial.rs +++ b/crates/miden-objects/src/account/storage/map/partial.rs @@ -1,3 +1,5 @@ +use alloc::collections::BTreeMap; + use miden_core::utils::{Deserializable, Serializable}; use miden_crypto::Word; use miden_crypto::merkle::{ @@ -10,7 +12,8 @@ use miden_crypto::merkle::{ SmtProof, }; -use crate::account::StorageMap; +use crate::account::{StorageMap, StorageMapWitness}; +use crate::utils::serde::{ByteReader, DeserializationError}; /// A partial representation of a [`StorageMap`], containing only proofs for a subset of the /// key-value pairs. @@ -18,41 +21,111 @@ use crate::account::StorageMap; /// A partial storage map carries only the Merkle authentication data a transaction will need. /// Every included entry pairs a value with its proof, letting the transaction kernel verify reads /// (and prepare writes) without needing the complete tree. +/// +/// ## Guarantees +/// +/// This type guarantees that the raw key-value pairs it contains are all present in the +/// contained partial SMT. Note that the inverse is not necessarily true. The SMT may contain more +/// entries than the map because to prove inclusion of a given raw key A an +/// [`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(Clone, Debug, PartialEq, Eq, Default)] pub struct PartialStorageMap { partial_smt: PartialSmt, + /// The entries of the map where the key is the raw user-chosen one. + /// + /// It is an invariant of this type that the map's entries are always consistent with the + /// partial SMT's entries and vice-versa. + entries: BTreeMap, } impl PartialStorageMap { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Returns a new instance of partial storage map with the specified partial SMT. - pub fn new(partial_smt: PartialSmt) -> Self { - PartialStorageMap { partial_smt } + /// Constructs a [`PartialStorageMap`] from a [`StorageMap`] root. + /// + /// For conversion from a [`StorageMap`], prefer [`Self::new_minimal`] to be more explicit. + pub fn new(root: Word) -> Self { + PartialStorageMap { + partial_smt: PartialSmt::new(root), + entries: BTreeMap::new(), + } + } + + /// Returns a new instance of a [`PartialStorageMap`] with all provided witnesses added to it. + pub fn with_witnesses( + witnesses: impl IntoIterator, + ) -> Result { + let mut map = BTreeMap::new(); + + let partial_smt = PartialSmt::from_proofs(witnesses.into_iter().map(|witness| { + map.extend(witness.entries()); + SmtProof::from(witness) + }))?; + + Ok(PartialStorageMap { partial_smt, entries: map }) + } + + /// Converts a [`StorageMap`] into a partial storage representation. + /// + /// The resulting [`PartialStorageMap`] will contain the _full_ entries and merkle paths of the + /// original storage map. + pub fn new_full(storage_map: StorageMap) -> Self { + let partial_smt = PartialSmt::from(storage_map.smt); + let entries = storage_map.entries; + + PartialStorageMap { partial_smt, entries } + } + + /// Converts a [`StorageMap`] into a partial storage representation. + /// + /// The resulting [`PartialStorageMap`] will represent the root of the storage map, but not + /// track any key-value pairs, which means it is the most _minimal_ representation of the + /// storage map. + pub fn new_minimal(storage_map: &StorageMap) -> Self { + Self::new(storage_map.root()) } + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the underlying [`PartialSmt`]. pub fn partial_smt(&self) -> &PartialSmt { &self.partial_smt } + /// Returns the root of the underlying [`PartialSmt`]. pub fn root(&self) -> Word { self.partial_smt.root() } - /// Returns an opening of the leaf associated with `key`. + /// Looks up the provided key in this map and returns: + /// - a non-empty [`Word`] if the key is tracked by this map and exists in it, + /// - [`Word::empty`] if the key is tracked by this map and does not exist, + /// - `None` if the key is not tracked by this map. + pub fn get(&self, raw_key: &Word) -> Option { + let hashed_key = StorageMap::hash_key(*raw_key); + // This returns an error if the key is not tracked which we map to a `None`. + self.partial_smt.get_value(&hashed_key).ok() + } + + /// Returns an opening of the leaf associated with the raw key. /// /// Conceptually, an opening is a Merkle path to the leaf, as well as the leaf itself. - /// The key needs to be hashed to have a behavior in line with [`StorageMap`]. For more details - /// as to why this is needed, refer to the docs for that struct. /// /// # Errors /// /// Returns an error if: /// - the key is not tracked by this partial storage map. - pub fn open(&self, key: &Word) -> Result { - let key = StorageMap::hash_key(*key); - self.partial_smt.open(&key) + pub fn open(&self, raw_key: &Word) -> Result { + let hashed_key = StorageMap::hash_key(*raw_key); + let smt_proof = self.partial_smt.open(&hashed_key)?; + let value = self.entries.get(raw_key).copied().unwrap_or_default(); + + // SAFETY: The key value pair is guaranteed to be present in the provided proof since we + // open its hashed version and because of the guarantees of the partial storage map. + Ok(StorageMapWitness::new_unchecked(smt_proof, [(*raw_key, value)])) } // ITERATORS @@ -63,9 +136,11 @@ impl PartialStorageMap { self.partial_smt.leaves() } - /// Returns an iterator over the key value pairs of the map. - pub fn entries(&self) -> impl Iterator { - self.partial_smt.entries().copied() + /// Returns an iterator over the key-value pairs in this storage map. + /// + /// Note that the returned key is the raw map key. + pub fn entries(&self) -> impl Iterator { + self.entries.iter() } /// Returns an iterator over the inner nodes of the underlying [`PartialSmt`]. @@ -76,37 +151,39 @@ impl PartialStorageMap { // MUTATORS // -------------------------------------------------------------------------------------------- - /// Adds an [`SmtProof`] to this [`PartialStorageMap`]. - pub fn add(&mut self, proof: SmtProof) -> Result<(), MerkleError> { - self.partial_smt.add_proof(proof) - } -} - -impl From for PartialStorageMap { - fn from(value: StorageMap) -> Self { - let v = value.smt; - - PartialStorageMap { partial_smt: v.into() } - } -} - -impl From for PartialStorageMap { - fn from(partial_smt: PartialSmt) -> Self { - PartialStorageMap { partial_smt } + /// Adds a [`StorageMapWitness`] for the specific key-value pair to this [`PartialStorageMap`]. + pub fn add(&mut self, witness: StorageMapWitness) -> Result<(), MerkleError> { + self.entries.extend(witness.entries().map(|(key, value)| (*key, *value))); + self.partial_smt.add_proof(SmtProof::from(witness)) } } impl Serializable for PartialStorageMap { fn write_into(&self, target: &mut W) { target.write(&self.partial_smt); + target.write_usize(self.entries.len()); + target.write_many(self.entries.keys()); } } impl Deserializable for PartialStorageMap { - fn read_from( - source: &mut R, - ) -> Result { - let storage: PartialSmt = source.read()?; - Ok(PartialStorageMap { partial_smt: storage }) + fn read_from(source: &mut R) -> Result { + let mut map = BTreeMap::new(); + + let partial_smt: PartialSmt = source.read()?; + let num_entries: usize = source.read()?; + + for _ in 0..num_entries { + let key: Word = source.read()?; + let hashed_map_key = StorageMap::hash_key(key); + let value = partial_smt.get_value(&hashed_map_key).map_err(|err| { + DeserializationError::InvalidValue(format!( + "failed to find map key {key} in partial SMT: {err}" + )) + })?; + map.insert(key, value); + } + + Ok(PartialStorageMap { partial_smt, entries: map }) } } diff --git a/crates/miden-objects/src/account/storage/map/witness.rs b/crates/miden-objects/src/account/storage/map/witness.rs new file mode 100644 index 0000000000..401d8345a4 --- /dev/null +++ b/crates/miden-objects/src/account/storage/map/witness.rs @@ -0,0 +1,138 @@ +use alloc::collections::BTreeMap; + +use miden_crypto::merkle::{InnerNodeInfo, SmtProof}; + +use crate::Word; +use crate::account::StorageMap; +use crate::errors::StorageMapError; + +/// A witness of an asset in a [`StorageMap`](super::StorageMap). +/// +/// It proves inclusion of a certain storage item in the map. +/// +/// ## Guarantees +/// +/// 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. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StorageMapWitness { + proof: SmtProof, + /// The entries of the map where the key is the raw user-chosen one. + /// + /// It is an invariant of this type that the map's entries are always consistent with the SMT's + /// entries and vice-versa. + entries: BTreeMap, +} + +impl StorageMapWitness { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`StorageMapWitness`] from an SMT proof and a provided set of map keys. + /// + /// # Errors + /// + /// Returns an error if: + /// - Any of the map keys is not contained in the proof. + pub fn new( + proof: SmtProof, + raw_keys: impl IntoIterator, + ) -> Result { + let mut entries = BTreeMap::new(); + + for raw_key in raw_keys.into_iter() { + let hashed_map_key = StorageMap::hash_key(raw_key); + let value = + proof.get(&hashed_map_key).ok_or(StorageMapError::MissingKey { raw_key })?; + entries.insert(raw_key, value); + } + + Ok(Self { proof, entries }) + } + + /// Creates a new [`StorageMapWitness`] from an SMT proof and a set of raw key value pairs. + /// + /// # Warning + /// + /// This does not validate any of the guarantees of this type. See the type-level docs for more + /// details. + pub fn new_unchecked( + proof: SmtProof, + raw_key_values: impl IntoIterator, + ) -> Self { + Self { + proof, + entries: raw_key_values.into_iter().collect(), + } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the underlying [`SmtProof`]. + pub fn proof(&self) -> &SmtProof { + &self.proof + } + + /// Looks up the provided key in this witness and returns: + /// - a non-empty [`Word`] if the key is tracked by this witness and exists in it, + /// - [`Word::empty`] if the key is tracked by this witness and does not exist, + /// - `None` if the key is not tracked by this witness. + pub fn get(&self, raw_key: &Word) -> Option { + let hashed_key = StorageMap::hash_key(*raw_key); + self.proof.get(&hashed_key) + } + + /// Returns an iterator over the key-value pairs in this witness. + /// + /// Note that the returned key is the raw map key. + pub fn entries(&self) -> impl Iterator { + self.entries.iter() + } + + /// Returns an iterator over every inner node of this witness' merkle path. + pub fn authenticated_nodes(&self) -> impl Iterator + '_ { + self.proof + .path() + .authenticated_nodes(self.proof.leaf().index().value(), self.proof.leaf().hash()) + .expect("leaf index is u64 and should be less than 2^SMT_DEPTH") + } +} + +impl From for SmtProof { + fn from(witness: StorageMapWitness) -> Self { + witness.proof + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + use crate::account::StorageMap; + + #[test] + fn creating_witness_fails_on_missing_key() { + // Create a storage map with one key-value pair + let key1 = Word::from([1, 2, 3, 4u32]); + let value1 = Word::from([10, 20, 30, 40u32]); + let entries = [(key1, value1)]; + let storage_map = StorageMap::with_entries(entries).unwrap(); + + // Create a proof for the existing key + let proof = storage_map.open(&key1).into(); + + // Try to create a witness for a different key that's not in the proof + let missing_key = Word::from([5, 6, 7, 8u32]); + let result = StorageMapWitness::new(proof, [missing_key]); + + assert_matches!(result, Err(StorageMapError::MissingKey { raw_key }) => { + assert_eq!(raw_key, missing_key); + }); + } +} diff --git a/crates/miden-objects/src/account/storage/mod.rs b/crates/miden-objects/src/account/storage/mod.rs index 672e931266..168f12ee85 100644 --- a/crates/miden-objects/src/account/storage/mod.rs +++ b/crates/miden-objects/src/account/storage/mod.rs @@ -16,10 +16,10 @@ use super::{ use crate::account::{AccountComponent, AccountType}; mod slot; -pub use slot::{StorageSlot, StorageSlotType}; +pub use slot::{SlotName, StorageSlot, StorageSlotType}; mod map; -pub use map::{PartialStorageMap, StorageMap}; +pub use map::{PartialStorageMap, StorageMap, StorageMapWitness}; mod header; pub use header::{AccountStorageHeader, StorageSlotHeader}; @@ -39,7 +39,7 @@ pub use partial::PartialStorage; /// - [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, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AccountStorage { slots: Vec, } @@ -112,10 +112,15 @@ impl AccountStorage { } /// Returns a reference to the storage slots. - pub fn slots(&self) -> &Vec { + 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( @@ -185,7 +190,7 @@ impl AccountStorage { _ => return Err(AccountError::StorageSlotNotMap(idx)), }; - storage_map.apply_delta(map); + storage_map.apply_delta(map)?; } // update storage values @@ -260,7 +265,7 @@ impl AccountStorage { let old_root = storage_map.root(); // update the key-value pair in the map - let old_value = storage_map.insert(key, value); + let old_value = storage_map.insert(key, value)?; Ok((old_root, old_value)) } diff --git a/crates/miden-objects/src/account/storage/partial.rs b/crates/miden-objects/src/account/storage/partial.rs index c5ba46217d..53c90828a7 100644 --- a/crates/miden-objects/src/account/storage/partial.rs +++ b/crates/miden-objects/src/account/storage/partial.rs @@ -25,6 +25,9 @@ pub struct PartialStorage { } impl PartialStorage { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + /// Returns a new instance of partial storage with the specified header and storage map SMTs. /// /// The storage commitment is computed during instantiation based on the provided header. @@ -48,6 +51,47 @@ impl PartialStorage { Ok(Self { commitment, header: storage_header, maps }) } + /// Converts an [`AccountStorage`] into a partial storage representation. + /// + /// This creates a partial storage that contains the _full_ proofs for all key-value pairs + /// 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 mut maps = BTreeMap::new(); + for slot in account_storage { + if let StorageSlot::Map(storage_map) = slot { + let partial_map = PartialStorageMap::new_full(storage_map); + maps.insert(partial_map.root(), partial_map); + } + } + + PartialStorage { header, maps, commitment } + } + + /// Converts an [`AccountStorage`] into a partial storage representation. + /// + /// For every storage map, a single unspecified key-value pair is tracked so that the + /// [`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 mut maps = BTreeMap::new(); + for slot in account_storage.slots() { + if let StorageSlot::Map(storage_map) = slot { + let partial_map = PartialStorageMap::new_minimal(storage_map); + maps.insert(partial_map.root(), partial_map); + } + } + + PartialStorage { header, maps, commitment } + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + /// Returns a reference to the header of this partial storage. pub fn header(&self) -> &AccountStorageHeader { &self.header @@ -58,6 +102,13 @@ impl PartialStorage { self.commitment } + // TODO: Consider removing once no longer needed so we don't commit to the underlying BTreeMap + // type. + /// Consumes self and returns the underlying parts. + pub fn into_parts(self) -> (Word, AccountStorageHeader, BTreeMap) { + (self.commitment, self.header, self.maps) + } + // TODO: Add from account storage with (slot/[key])? // ITERATORS @@ -80,26 +131,6 @@ impl PartialStorage { } } -impl From<&AccountStorage> for PartialStorage { - /// Converts a full account storage into a partial storage representation. - /// - /// This creates a partial storage that contains proofs for all key-value pairs - /// in all map slots of the account storage. - fn from(account_storage: &AccountStorage) -> Self { - let mut map_smts = BTreeMap::new(); - for slot in account_storage.slots() { - if let StorageSlot::Map(map) = slot { - let smt: PartialStorageMap = map.clone().into(); - map_smts.insert(smt.root(), smt); - } - } - - let header: AccountStorageHeader = account_storage.to_header(); - let commitment = header.compute_commitment(); - PartialStorage { header, maps: map_smts, commitment } - } -} - impl Serializable for PartialStorage { fn write_into(&self, target: &mut W) { target.write(&self.header); @@ -124,12 +155,12 @@ impl Deserializable for PartialStorage { mod tests { use anyhow::Context; use miden_core::Word; - use miden_crypto::merkle::PartialSmt; use crate::account::{ AccountStorage, AccountStorageHeader, PartialStorage, + PartialStorageMap, StorageMap, StorageSlot, }; @@ -140,19 +171,19 @@ mod tests { let map_key_absent: Word = [9u64, 12, 18, 3].try_into()?; let mut map_1 = StorageMap::new(); - map_1.insert(map_key_absent, Word::try_from([1u64, 2, 3, 2])?); - map_1.insert(map_key_present, Word::try_from([5u64, 4, 3, 2])?); + map_1.insert(map_key_absent, Word::try_from([1u64, 2, 3, 2])?).unwrap(); + 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(); // Create partial storage with validation of one map key let storage_header = AccountStorageHeader::from(&storage); - let proof = map_1.open(&map_key_present); - let partial_smt = PartialSmt::from_proofs([proof])?; + let witness = map_1.open(&map_key_present); - let partial_storage = PartialStorage::new(storage_header, [partial_smt.into()]) - .context("creating partial storage")?; + let partial_storage = + 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(); assert!(retrieved_map.open(&map_key_absent).is_err()); diff --git a/crates/miden-objects/src/account/storage/slot/mod.rs b/crates/miden-objects/src/account/storage/slot/mod.rs index c1aef7453a..9f6389c38b 100644 --- a/crates/miden-objects/src/account/storage/slot/mod.rs +++ b/crates/miden-objects/src/account/storage/slot/mod.rs @@ -5,6 +5,9 @@ use miden_processor::DeserializationError; use super::map::EMPTY_STORAGE_MAP_ROOT; use super::{StorageMap, Word}; +mod slot_name; +pub use slot_name::SlotName; + mod r#type; pub use r#type::StorageSlotType; diff --git a/crates/miden-objects/src/account/storage/slot/slot_name.rs b/crates/miden-objects/src/account/storage/slot/slot_name.rs new file mode 100644 index 0000000000..0efbb96a97 --- /dev/null +++ b/crates/miden-objects/src/account/storage/slot/slot_name.rs @@ -0,0 +1,314 @@ +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/account/storage/slot/type.rs b/crates/miden-objects/src/account/storage/slot/type.rs index b15d09be06..bb5d9f2645 100644 --- a/crates/miden-objects/src/account/storage/slot/type.rs +++ b/crates/miden-objects/src/account/storage/slot/type.rs @@ -29,6 +29,16 @@ impl StorageSlotType { StorageSlotType::Map => Word::from([1, 0, 0, 0u32]), } } + + /// Returns `true` if the slot is a value slot, `false` otherwise. + pub fn is_value(&self) -> bool { + matches!(self, Self::Value) + } + + /// Returns `true` if the slot is a map slot, `false` otherwise. + pub fn is_map(&self) -> bool { + matches!(self, Self::Map) + } } impl TryFrom for StorageSlotType { diff --git a/crates/miden-objects/src/address/address_id.rs b/crates/miden-objects/src/address/address_id.rs new file mode 100644 index 0000000000..2974e66ac3 --- /dev/null +++ b/crates/miden-objects/src/address/address_id.rs @@ -0,0 +1,117 @@ +use alloc::string::ToString; + +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::note::NoteTag; +use crate::utils::serde::{ByteWriter, Deserializable, Serializable}; + +/// The identifier of an [`Address`](super::Address). +/// +/// See the address docs for more details. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AddressId { + AccountId(AccountId), +} + +impl AddressId { + /// Returns the [`AddressType`] of this ID. + pub fn address_type(&self) -> AddressType { + match self { + AddressId::AccountId(_) => AddressType::AccountId, + } + } + + /// 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`]). + pub fn default_note_tag_len(&self) -> u8 { + match self { + AddressId::AccountId(id) => { + if id.storage_mode() == AccountStorageMode::Network { + NoteTag::DEFAULT_NETWORK_TAG_LENGTH + } else { + NoteTag::DEFAULT_LOCAL_TAG_LENGTH + } + }, + } + } + + /// Decodes a bech32 string into an identifier. + pub(crate) fn decode(bech32_string: &str) -> Result<(NetworkId, Self), AddressError> { + // We use CheckedHrpString with an explicit checksum algorithm so we don't allow the + // `Bech32` or `NoChecksum` algorithms. + let checked_string = CheckedHrpstring::new::(bech32_string).map_err(|source| { + // The CheckedHrpStringError does not implement core::error::Error, only + // std::error::Error, so for now we convert it to a String. Even if it will + // implement the trait in the future, we should include it as an opaque + // error since the crate does not have a stable release yet. + AddressError::Bech32DecodeError(Bech32Error::DecodeError(source.to_string().into())) + })?; + + let hrp = checked_string.hrp(); + let network_id = NetworkId::from_hrp(hrp); + + let mut byte_iter = checked_string.byte_iter(); + + // We only know the expected length once we know the address type, but to get the + // address type, the length must be at least one. + let address_byte = byte_iter.next().ok_or_else(|| { + AddressError::Bech32DecodeError(Bech32Error::InvalidDataLength { + expected: 1, + actual: byte_iter.len(), + }) + })?; + + let address_type = AddressType::try_from(address_byte)?; + + let identifier = match address_type { + AddressType::AccountId => AccountId::from_bech32_byte_iter(byte_iter) + .map_err(AddressError::AccountIdDecodeError) + .map(AddressId::AccountId)?, + }; + + Ok((network_id, identifier)) + } +} + +impl From for AddressId { + fn from(id: AccountId) -> Self { + Self::AccountId(id) + } +} + +impl Serializable for AddressId { + fn write_into(&self, target: &mut W) { + target.write_u8(self.address_type() as u8); + match self { + AddressId::AccountId(id) => { + id.write_into(target); + }, + } + } +} + +impl Deserializable for AddressId { + fn read_from( + source: &mut R, + ) -> Result { + let address_type: u8 = source.read_u8()?; + let address_type = AddressType::try_from(address_type) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + + match address_type { + AddressType::AccountId => { + let id: AccountId = source.read()?; + Ok(AddressId::AccountId(id)) + }, + } + } +} diff --git a/crates/miden-objects/src/address/interface.rs b/crates/miden-objects/src/address/interface.rs index d298f39480..562feb2585 100644 --- a/crates/miden-objects/src/address/interface.rs +++ b/crates/miden-objects/src/address/interface.rs @@ -1,3 +1,5 @@ +use core::fmt::{self, Display, Formatter}; + use crate::AddressError; /// The account interface of an [`Address`](super::Address). @@ -17,16 +19,13 @@ use crate::AddressError; #[repr(u16)] #[non_exhaustive] pub enum AddressInterface { - /// Signals that the account interface is not specified. - Unspecified = Self::UNSPECIFIED, /// The basic wallet interface. BasicWallet = Self::BASIC_WALLET, } impl AddressInterface { // Constants for internal use only. - const UNSPECIFIED: u16 = 0; - const BASIC_WALLET: u16 = 1; + const BASIC_WALLET: u16 = 0; } impl TryFrom for AddressInterface { @@ -35,9 +34,16 @@ impl TryFrom for AddressInterface { /// Decodes an [`AddressInterface`] from its bytes representation. fn try_from(value: u16) -> Result { match value { - Self::UNSPECIFIED => Ok(Self::Unspecified), Self::BASIC_WALLET => Ok(Self::BasicWallet), other => Err(AddressError::UnknownAddressInterface(other)), } } } + +impl Display for AddressInterface { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::BasicWallet => write!(f, "BasicWallet"), + } + } +} diff --git a/crates/miden-objects/src/address/mod.rs b/crates/miden-objects/src/address/mod.rs index 03608f845d..142dd9e8b8 100644 --- a/crates/miden-objects/src/address/mod.rs +++ b/crates/miden-objects/src/address/mod.rs @@ -1,321 +1,235 @@ mod r#type; +use alloc::string::ToString; -mod interface; -use alloc::string::{String, ToString}; - -use bech32::Bech32m; -use bech32::primitives::decode::{ByteIter, CheckedHrpstring}; -pub use interface::AddressInterface; pub use r#type::AddressType; -use crate::AddressError; -use crate::account::{AccountId, AccountStorageMode, NetworkId}; -use crate::errors::Bech32Error; -use crate::note::NoteTag; - -/// A user-facing address in Miden. -#[non_exhaustive] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Address { - AccountId(AccountIdAddress), -} - -impl Address { - /// Returns a note tag derived from this address. - pub fn to_note_tag(&self) -> NoteTag { - match self { - Address::AccountId(addr) => addr.to_note_tag(), - } - } +mod routing_parameters; +use alloc::borrow::ToOwned; - /// Returns the [`AddressInterface`] of the account to which the address points. - pub fn interface(&self) -> AddressInterface { - match self { - Address::AccountId(account_id_address) => account_id_address.interface(), - } - } +pub use routing_parameters::RoutingParameters; - /// Encodes the [`Address`] into a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string. - /// - /// ## Encoding - /// - /// The encoding of an address into bech32 is done as follows: - /// - Encode the underlying address to bytes. - /// - Into that data, insert the [`AddressType`] byte at index 0, shifting all other elements to - /// the right. - /// - Choose an HRP, defined as a [`NetworkId`], e.g. [`NetworkId::Mainnet`] whose string - /// representation is `mm`. - /// - Encode the resulting HRP together with the data into a bech32 string using the - /// [`bech32::Bech32m`] checksum algorithm. - /// - /// This is an example of an address in bech32 representation: - /// - /// ```text - /// mm1qpkdyek2c0ywwvzupakc7zlzty8qn2qnfc - /// ``` - /// - /// ## Rationale - /// - /// The address type is at the very beginning so that it can be decoded first to detect the type - /// of the address, without having to decode the entire data. Moreover, since the address type - /// is chosen as a multiple of 8, the first character of the bech32 string after the - /// `1` separator will be different for every address type. That makes the type of the address - /// conveniently human-readable. - /// - /// The only allowed checksum algorithm is [`Bech32m`] due to being the best available checksum - /// algorithm with no known weaknesses (unlike [`Bech32`](bech32::Bech32)). No checksum is - /// also not allowed since the intended use of bech32 is to have error - /// detection capabilities. - pub fn to_bech32(&self, network_id: NetworkId) -> String { - match self { - Address::AccountId(account_id_address) => account_id_address.to_bech32(network_id), - } - } +mod interface; +mod network_id; +use alloc::string::String; - /// Decodes a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string - /// into the [`NetworkId`] and an [`Address`]. - /// - /// See [`Address::to_bech32`] for details on the format. The procedure for decoding the bech32 - /// data into the address are the inverse operations of encoding. - pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AddressError> { - // We use CheckedHrpString with an explicit checksum algorithm so we don't allow the - // `Bech32` or `NoChecksum` algorithms. - let checked_string = CheckedHrpstring::new::(bech32_string).map_err(|source| { - // The CheckedHrpStringError does not implement core::error::Error, only - // std::error::Error, so for now we convert it to a String. Even if it will - // implement the trait in the future, we should include it as an opaque - // error since the crate does not have a stable release yet. - AddressError::Bech32DecodeError(Bech32Error::DecodeError(source.to_string().into())) - })?; - - let hrp = checked_string.hrp(); - let network_id = NetworkId::from_hrp(hrp); - - let mut byte_iter = checked_string.byte_iter(); - - // We only know the expected length once we know the address type, but to get the address - // type, the length must be at least one. - let address_byte = byte_iter.next().ok_or_else(|| { - AddressError::Bech32DecodeError(Bech32Error::InvalidDataLength { - expected: 1, - actual: byte_iter.len(), - }) - })?; - - let address_type = AddressType::try_from(address_byte)?; - - let address = match address_type { - AddressType::AccountId => { - AccountIdAddress::from_bech32_byte_iter(byte_iter).map(Address::from)? - }, - }; +pub use interface::AddressInterface; +use miden_processor::DeserializationError; +pub use network_id::{CustomNetworkId, NetworkId}; - Ok((network_id, address)) - } -} +use crate::AddressError; +use crate::account::AccountStorageMode; +use crate::crypto::ies::SealingKey; +use crate::note::NoteTag; +use crate::utils::serde::{ByteWriter, Deserializable, Serializable}; -// ACCOUNT ID ADDRESS -// ================================================================================================ +mod address_id; +pub use address_id::AddressId; -/// An [`Address`] that targets a specific [`AccountId`] with an explicit tag length preference. +/// A user-facing address in Miden. +/// +/// An address consists of an [`AddressId`] and optional [`RoutingParameters`]. +/// +/// A user who wants to receive a note creates an address and sends it to the sender of the note. +/// The sender creates a note intended for the holder of this address ID (e.g., it provides +/// discoverability and potentially access-control) and the routing parameters inform the sender +/// about various aspects like: +/// - what kind of note the receiver's account can consume. +/// - how the receiver discovers the note. +/// - how to encrypt the note for the receiver. +/// +/// It can be encoded to a string using [`Self::encode`] and decoded using [`Self::decode`]. +/// If routing parameters are present, the ID and parameters are separated by +/// [`Address::SEPARATOR`]. /// -/// The tag length preference determines how many bits of the account ID are encoded into -/// [`NoteTag`]s of notes targeted to this address. This lets the owner of the account choose -/// their level of privacy. A higher tag length makes the account more uniquely identifiable and -/// reduces privacy, while a shorter length increases privacy at the cost of matching more notes -/// published onchain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct AccountIdAddress { - id: AccountId, - tag_len: u8, - interface: AddressInterface, +/// ## Example +/// +/// ```text +/// # account ID +/// mm1apt3l475qemeqqp57xjycfdwcvw0sfhq +/// # account ID + routing parameters (interface & note tag length) +/// mm1apt3l475qemeqqp57xjycfdwcvw0sfhq_qruqqypuyph +/// # account ID + routing parameters (interface, note tag length, encryption key) +/// mm1apt3l475qemeqqp57xjycfdwcvw0sfhq_qruqqqgqjmsgjsh3687mt2w0qtqunxt3th442j48qwdnezl0fv6qm3x9c8zqsv7pku +/// ``` +/// +/// The encoding of an address without routing parameters matches the encoding of the underlying +/// identifier exactly (e.g. an account ID). This provides compatibility between identifiers and +/// addresses and gives end-users a hint that an address is only an extension of the identifier +/// (e.g. their account's ID) that they are likely to recognize. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Address { + id: AddressId, + routing_params: Option, } -impl AccountIdAddress { +impl Address { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The serialized size of an [`AccountIdAddress`] in bytes. - pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + 2; + /// The separator character in an encoded address between the ID and routing parameters. + pub const SEPARATOR: char = '_'; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new account ID based address with the default tag length. + /// Returns a new address from an [`AddressId`] and routing parameters set to `None`. /// - /// The tag length defaults to [`NoteTag::DEFAULT_LOCAL_TAG_LENGTH`] for local, and - /// [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`] for network accounts. - pub fn new(id: AccountId, interface: AddressInterface) -> Self { - let tag_len = if id.storage_mode() == AccountStorageMode::Network { - NoteTag::DEFAULT_NETWORK_TAG_LENGTH - } else { - NoteTag::DEFAULT_LOCAL_TAG_LENGTH - }; - - Self { id, tag_len, interface } + /// To set routing parameters, use [`Self::with_routing_parameters`]. + pub fn new(id: impl Into) -> Self { + Self { id: id.into(), routing_params: None } } - // PUBLIC MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Sets a custom tag length for the address, determining how many bits of the account ID - /// are encoded into [`NoteTag`]s. - /// /// For local (both public and private) accounts, up to 30 bits can be encoded into the tag. /// For network accounts, the tag length must be set to 30 bits. /// /// # Errors /// /// Returns an error if: - /// - The tag length exceeds [`NoteTag::MAX_LOCAL_TAG_LENGTH`] for local accounts. - /// - The tag length is not [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`] for network accounts. - pub fn with_tag_len(mut self, tag_len: u8) -> Result { - if self.id.storage_mode() == AccountStorageMode::Network { - if tag_len != NoteTag::DEFAULT_NETWORK_TAG_LENGTH { - return Err(AddressError::CustomTagLengthNotAllowedForNetworkAccounts(tag_len)); + /// - The tag length routing parameter is not [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`] for + /// network accounts. + pub fn with_routing_parameters( + mut self, + routing_params: RoutingParameters, + ) -> Result { + if let Some(tag_len) = routing_params.note_tag_len() { + match self.id { + AddressId::AccountId(account_id) => { + if account_id.storage_mode() == AccountStorageMode::Network + && tag_len != NoteTag::DEFAULT_NETWORK_TAG_LENGTH + { + return Err(AddressError::CustomTagLengthNotAllowedForNetworkAccounts( + tag_len, + )); + } + }, } - } else if tag_len > NoteTag::MAX_LOCAL_TAG_LENGTH { - return Err(AddressError::TagLengthTooLarge(tag_len)); } - self.tag_len = tag_len; + self.routing_params = Some(routing_params); + Ok(self) } - // PUBLIC ACCESSORS + // ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the underlying account id. - pub fn id(&self) -> AccountId { + /// Returns the identifier of the address. + pub fn id(&self) -> AddressId { self.id } + /// Returns the [`AddressInterface`] of the account to which the address points. + pub fn interface(&self) -> Option { + self.routing_params.as_ref().map(RoutingParameters::interface) + } + /// 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`]). pub fn note_tag_len(&self) -> u8 { - self.tag_len - } - - /// Returns the [`AddressInterface`] of the account to which the address points. - pub fn interface(&self) -> AddressInterface { - self.interface + self.routing_params + .as_ref() + .and_then(RoutingParameters::note_tag_len) + .unwrap_or(self.id.default_note_tag_len()) } /// Returns a note tag derived from this address. pub fn to_note_tag(&self) -> NoteTag { - match self.id.storage_mode() { - AccountStorageMode::Network => NoteTag::from_network_account_id(self.id), - AccountStorageMode::Private | AccountStorageMode::Public => { - NoteTag::from_local_account_id(self.id, self.tag_len) - .expect("AccountIdAddress validated that tag len does not exceed MAX_LOCAL_TAG_LENGTH bits") + let note_tag_len = self.note_tag_len(); + + match self.id { + AddressId::AccountId(id) => { + 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") + } + } }, } } - // PRIVATE HELPERS - // ---------------------------------------------------------------------------------------- - - /// Encodes the [`AccountIdAddress`] to a bech32 string. + /// Returns the optional public encryption key from routing parameters. /// - /// See [`Address::to_bech32`] for more details. - fn to_bech32(self, network_id: NetworkId) -> String { - let id_bytes: [u8; Self::SERIALIZED_SIZE] = self.into(); - - // Create an array that fits the encoded account ID address plus the address type byte. - let mut data = [0; Self::SERIALIZED_SIZE + 1]; - // Encode the address type into index 0. - data[0] = AddressType::AccountId as u8; - // Encode the 17 account ID address bytes into 1..18. - data[1..].copy_from_slice(&id_bytes); - - // SAFETY: Encoding panics if the total length of the hrp + data (encoded in GF(32)) + the - // separator + the checksum exceeds Bech32m::CODE_LENGTH, which is 1023. - // The total 18 bytes of data we encode result in (18 bytes * 8 bits / 5 bits per base32 - // symbol) = 29 characters. The hrp is at most 83 in length, so we are guaranteed to be - // below the limit. - bech32::encode::(network_id.into_hrp(), &data) - .expect("code length of bech32 should not be exceeded") + /// This key can be used for sealed box encryption when sending notes to this address. + pub fn encryption_key(&self) -> Option<&SealingKey> { + self.routing_params.as_ref().and_then(RoutingParameters::encryption_key) } - /// Decodes the data from the bech32 byte iterator into an [`AccountIdAddress`]. + /// Encodes the [`Address`] into a string. /// - /// See [`Address::from_bech32`] for details. - fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result { - // The _remaining_ length of the iterator must be the serialized size of the account ID - // address. - if byte_iter.len() != Self::SERIALIZED_SIZE { - return Err(AddressError::Bech32DecodeError(Bech32Error::InvalidDataLength { - expected: Self::SERIALIZED_SIZE, - actual: byte_iter.len(), - })); - } + /// ## Encoding + /// + /// The encoding of an address into a string is done as follows: + /// - Encode the underlying [`AddressId`] to a bech32 string. + /// - If routing parameters are present: + /// - Append the [`Address::SEPARATOR`] to that string. + /// - Append the encoded routing parameters to that string. + pub fn encode(&self, network_id: NetworkId) -> String { + let mut encoded = match self.id { + AddressId::AccountId(id) => id.to_bech32(network_id), + }; - // Every byte is guaranteed to be overwritten since we've checked the length of the - // iterator. - let mut id_bytes = [0_u8; Self::SERIALIZED_SIZE]; - for (i, byte) in byte_iter.enumerate() { - id_bytes[i] = byte; + if let Some(routing_params) = &self.routing_params { + encoded.push(Self::SEPARATOR); + encoded.push_str(&routing_params.encode_to_string()); } - let account_id_address = Self::try_from(id_bytes)?; - - Ok(account_id_address) + encoded } -} -impl From for Address { - fn from(addr: AccountIdAddress) -> Self { - Address::AccountId(addr) - } -} + /// Decodes an address string into the [`NetworkId`] and an [`Address`]. + /// + /// See [`Address::encode`] for details on the format. The procedure for decoding the string + /// into the address are the inverse operations of encoding. + pub fn decode(address_str: &str) -> Result<(NetworkId, Self), AddressError> { + if address_str.ends_with(Self::SEPARATOR) { + return Err(AddressError::TrailingSeparator); + } -impl From for [u8; AccountIdAddress::SERIALIZED_SIZE] { - fn from(account_id_address: AccountIdAddress) -> Self { - let mut result = [0_u8; AccountIdAddress::SERIALIZED_SIZE]; + let mut split = address_str.split(Self::SEPARATOR); + let encoded_identifier = split + .next() + .ok_or_else(|| AddressError::decode_error("identifier missing in address string"))?; - // Encode the account ID into 0..15. - let encoded_account_id_address = <[u8; 15]>::from(account_id_address.id); - result[..15].copy_from_slice(&encoded_account_id_address); + let (network_id, identifier) = AddressId::decode(encoded_identifier)?; - let interface = account_id_address.interface as u16; - debug_assert_eq!( - interface >> 11, - 0, - "address interface should have its upper 5 bits unset" - ); + let mut address = Address::new(identifier); - // The interface takes up 11 bits and the tag length 5 bits, so we can merge them together. - let tag_len = (account_id_address.tag_len as u16) << 11; - let encoded = tag_len | interface; - let encoded: [u8; 2] = encoded.to_be_bytes(); + if let Some(encoded_routing_params) = split.next() { + let routing_params = RoutingParameters::decode(encoded_routing_params.to_owned())?; + address = address.with_routing_parameters(routing_params)?; + } - // Encode the interface and tag length into 15..17. - result[15] = encoded[0]; - result[16] = encoded[1]; + Ok((network_id, address)) + } +} - result +impl Serializable for Address { + fn write_into(&self, target: &mut W) { + self.id.write_into(target); + self.routing_params.write_into(target); } } -impl TryFrom<[u8; AccountIdAddress::SERIALIZED_SIZE]> for AccountIdAddress { - type Error = AddressError; +impl Deserializable for Address { + fn read_from( + source: &mut R, + ) -> Result { + let identifier: AddressId = source.read()?; + let routing_params: Option = source.read()?; - fn try_from(bytes: [u8; AccountIdAddress::SERIALIZED_SIZE]) -> Result { - let account_id_bytes: [u8; AccountId::SERIALIZED_SIZE] = bytes - [..AccountId::SERIALIZED_SIZE] - .try_into() - .expect("we should have sliced off exactly 15 bytes"); - let account_id = - AccountId::try_from(account_id_bytes).map_err(AddressError::AccountIdDecodeError)?; + let mut address = Self::new(identifier); - let interface_tag_len = u16::from_be_bytes([bytes[15], bytes[16]]); - let tag_len = (interface_tag_len >> 11) as u8; - let interface = interface_tag_len & 0b0000_0111_1111_1111; - let interface = AddressInterface::try_from(interface)?; + if let Some(routing_params) = routing_params { + address = address + .with_routing_parameters(routing_params) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + } - Self::new(account_id, interface).with_tag_len(tag_len) + Ok(address) } } @@ -324,16 +238,22 @@ impl TryFrom<[u8; AccountIdAddress::SERIALIZED_SIZE]> for AccountIdAddress { #[cfg(test)] mod tests { + use alloc::boxed::Box; + use alloc::str::FromStr; + use assert_matches::assert_matches; - use bech32::{Bech32, Hrp, NoChecksum}; + use bech32::{Bech32, Bech32m, NoChecksum}; use super::*; - use crate::account::AccountType; + use crate::AccountIdError; + use crate::account::{AccountId, AccountType}; + use crate::address::CustomNetworkId; + use crate::errors::Bech32Error; use crate::testing::account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, AccountIdBuilder}; /// Tests that an account ID address can be encoded and decoded. #[test] - fn address_bech32_encode_decode_roundtrip() { + fn address_encode_decode_roundtrip() -> anyhow::Result<()> { // We use this to check that encoding does not panic even when using the longest possible // HRP. let longest_possible_hrp = @@ -343,8 +263,8 @@ mod tests { let rng = &mut rand::rng(); for network_id in [ NetworkId::Mainnet, - NetworkId::Custom(Hrp::parse("custom").unwrap()), - NetworkId::Custom(Hrp::parse(longest_possible_hrp).unwrap()), + NetworkId::Custom(Box::new(CustomNetworkId::from_str("custom").unwrap())), + NetworkId::Custom(Box::new(CustomNetworkId::from_str(longest_possible_hrp).unwrap())), ] { for (idx, account_id) in [ AccountIdBuilder::new() @@ -363,59 +283,93 @@ mod tests { .into_iter() .enumerate() { - let account_id_address = - AccountIdAddress::new(account_id, AddressInterface::BasicWallet); - let address = Address::from(account_id_address); + // Encode/Decode without routing parameters should be valid. + let mut address = Address::new(account_id); + + let bech32_string = address.encode(network_id.clone()); + assert!( + !bech32_string.contains(Address::SEPARATOR), + "separator should not be present in address without routing params" + ); + let (decoded_network_id, decoded_address) = Address::decode(&bech32_string)?; + + assert_eq!(network_id, decoded_network_id, "network id failed in {idx}"); + assert_eq!(address, decoded_address, "address failed in {idx}"); + + let AddressId::AccountId(decoded_account_id) = address.id(); + assert_eq!(account_id, decoded_account_id); + + // 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)?, + )?; - let bech32_string = address.to_bech32(network_id); - let (decoded_network_id, decoded_address) = - Address::from_bech32(&bech32_string).unwrap(); + let bech32_string = address.encode(network_id.clone()); + assert!( + bech32_string.contains(Address::SEPARATOR), + "separator should be present in address without routing params" + ); + let (decoded_network_id, decoded_address) = Address::decode(&bech32_string)?; assert_eq!(network_id, decoded_network_id, "network id failed in {idx}"); assert_eq!(address, decoded_address, "address failed in {idx}"); - let Address::AccountId(decoded_account_id) = address; - assert_eq!(account_id, decoded_account_id.id()); - assert_eq!(account_id_address.note_tag_len(), decoded_account_id.note_tag_len()); + let AddressId::AccountId(decoded_account_id) = address.id(); + assert_eq!(account_id, decoded_account_id); } } + + Ok(()) + } + + #[test] + fn address_decoding_fails_on_trailing_separator() -> anyhow::Result<()> { + let id = AccountIdBuilder::new() + .account_type(AccountType::FungibleFaucet) + .build_with_rng(&mut rand::rng()); + + let address = Address::new(id); + let mut encoded_address = address.encode(NetworkId::Devnet); + encoded_address.push(Address::SEPARATOR); + + let err = Address::decode(&encoded_address).unwrap_err(); + assert_matches!(err, AddressError::TrailingSeparator); + + Ok(()) } /// Tests that an invalid checksum returns an error. #[test] - fn bech32_invalid_checksum() { + fn bech32_invalid_checksum() -> anyhow::Result<()> { let network_id = NetworkId::Mainnet; - let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let address = - Address::from(AccountIdAddress::new(account_id, AddressInterface::BasicWallet)); + let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; + let address = Address::new(account_id).with_routing_parameters( + RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(14)?, + )?; - let bech32_string = address.to_bech32(network_id); + let bech32_string = address.encode(network_id); let mut invalid_bech32_1 = bech32_string.clone(); invalid_bech32_1.remove(0); let mut invalid_bech32_2 = bech32_string.clone(); invalid_bech32_2.remove(7); - let error = Address::from_bech32(&invalid_bech32_1).unwrap_err(); + let error = Address::decode(&invalid_bech32_1).unwrap_err(); assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_))); - let error = Address::from_bech32(&invalid_bech32_2).unwrap_err(); + let error = Address::decode(&invalid_bech32_2).unwrap_err(); assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_))); + + Ok(()) } /// Tests that an unknown address type returns an error. #[test] fn bech32_unknown_address_type() { - let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let account_id_address = AccountIdAddress::new(account_id, AddressInterface::BasicWallet); - let mut id_address_bytes = <[u8; _]>::from(account_id_address).to_vec(); + let invalid_bech32_address = + bech32::encode::(NetworkId::Mainnet.into_hrp(), &[250]).unwrap(); - // Set invalid address type. - id_address_bytes.insert(0, 250); - - let invalid_bech32 = - bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_address_bytes).unwrap(); - - let error = Address::from_bech32(&invalid_bech32).unwrap_err(); + let error = Address::decode(&invalid_bech32_address).unwrap_err(); assert_matches!( error, AddressError::Bech32DecodeError(Bech32Error::UnknownAddressType(250)) @@ -426,20 +380,18 @@ mod tests { #[test] fn bech32_invalid_other_checksum() { let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let account_id_address = AccountIdAddress::new(account_id, AddressInterface::BasicWallet); - let mut id_address_bytes = <[u8; _]>::from(account_id_address).to_vec(); - id_address_bytes.insert(0, AddressType::AccountId as u8); + let address_id_bytes = AddressId::from(account_id).to_bytes(); // Use Bech32 instead of Bech32m which is disallowed. let invalid_bech32_regular = - bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_address_bytes).unwrap(); - let error = Address::from_bech32(&invalid_bech32_regular).unwrap_err(); + bech32::encode::(NetworkId::Mainnet.into_hrp(), &address_id_bytes).unwrap(); + let error = Address::decode(&invalid_bech32_regular).unwrap_err(); assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_))); // Use no checksum instead of Bech32m which is disallowed. let invalid_bech32_no_checksum = - bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_address_bytes).unwrap(); - let error = Address::from_bech32(&invalid_bech32_no_checksum).unwrap_err(); + bech32::encode::(NetworkId::Mainnet.into_hrp(), &address_id_bytes).unwrap(); + let error = Address::decode(&invalid_bech32_no_checksum).unwrap_err(); assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_))); } @@ -447,19 +399,126 @@ mod tests { #[test] fn bech32_invalid_length() { let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let account_id_address = AccountIdAddress::new(account_id, AddressInterface::BasicWallet); - let mut id_address_bytes = <[u8; _]>::from(account_id_address).to_vec(); - id_address_bytes.insert(0, AddressType::AccountId as u8); + let mut address_id_bytes = AddressId::from(account_id).to_bytes(); // Add one byte to make the length invalid. - id_address_bytes.push(5); + address_id_bytes.push(5); let invalid_bech32 = - bech32::encode::(NetworkId::Mainnet.into_hrp(), &id_address_bytes).unwrap(); + bech32::encode::(NetworkId::Mainnet.into_hrp(), &address_id_bytes).unwrap(); - let error = Address::from_bech32(&invalid_bech32).unwrap_err(); + let error = Address::decode(&invalid_bech32).unwrap_err(); assert_matches!( error, - AddressError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. }) + AddressError::AccountIdDecodeError(AccountIdError::Bech32DecodeError( + Bech32Error::InvalidDataLength { .. } + )) ); } + + /// Tests that an Address can be serialized and deserialized + #[test] + fn address_serialization() -> anyhow::Result<()> { + let rng = &mut rand::rng(); + + for account_type in [ + AccountType::FungibleFaucet, + AccountType::NonFungibleFaucet, + AccountType::RegularAccountImmutableCode, + AccountType::RegularAccountUpdatableCode, + ] + .into_iter() + { + 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)?, + )?; + + let serialized = address.to_bytes(); + let deserialized = Address::read_from_bytes(&serialized)?; + assert_eq!(address, deserialized); + } + + Ok(()) + } + + /// 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::ies::{SealingKey, UnsealingKey}; + + let rng = &mut rand::rng(); + let account_id = AccountIdBuilder::new() + .account_type(AccountType::FungibleFaucet) + .build_with_rng(rng); + + // Create keypair using rand::rng() + let secret_key = SecretKey::with_rng(rng); + let public_key = secret_key.public_key(); + let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key.clone()); + let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key.clone()); + + // Create address with encryption key + let address = Address::new(account_id).with_routing_parameters( + RoutingParameters::new(AddressInterface::BasicWallet) + .with_encryption_key(sealing_key.clone()), + )?; + + // Verify encryption key is present + let retrieved_key = + address.encryption_key().expect("encryption key should be present").clone(); + assert_eq!(retrieved_key, sealing_key); + + // Test seal/unseal round-trip + let plaintext = b"hello world"; + let sealed_message = + retrieved_key.seal_bytes(rng, plaintext).expect("sealing should succeed"); + let decrypted = + unsealing_key.unseal_bytes(sealed_message).expect("unsealing should succeed"); + assert_eq!(decrypted.as_slice(), plaintext); + + Ok(()) + } + + /// 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; + + let rng = &mut rand::rng(); + // Use a local account type (RegularAccountImmutableCode) instead of network + // (FungibleFaucet) + let account_id = AccountIdBuilder::new() + .account_type(AccountType::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .build_with_rng(rng); + + // Create keypair + let secret_key = SecretKey::with_rng(rng); + let public_key = secret_key.public_key(); + let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key); + + // Create address with encryption key + let address = Address::new(account_id).with_routing_parameters( + RoutingParameters::new(AddressInterface::BasicWallet) + .with_encryption_key(sealing_key.clone()), + )?; + + // Encode and decode + let encoded = address.encode(NetworkId::Mainnet); + let (decoded_network, decoded_address) = Address::decode(&encoded)?; + + assert_eq!(decoded_network, NetworkId::Mainnet); + assert_eq!(address, decoded_address); + + // Verify encryption key is preserved + let decoded_key = decoded_address + .encryption_key() + .expect("encryption key should be present") + .clone(); + assert_eq!(decoded_key, sealing_key); + + Ok(()) + } } diff --git a/crates/miden-objects/src/account/account_id/network_id.rs b/crates/miden-objects/src/address/network_id.rs similarity index 64% rename from crates/miden-objects/src/account/account_id/network_id.rs rename to crates/miden-objects/src/address/network_id.rs index 9b38a73d15..dc26b449ca 100644 --- a/crates/miden-objects/src/account/account_id/network_id.rs +++ b/crates/miden-objects/src/address/network_id.rs @@ -1,5 +1,6 @@ +use alloc::boxed::Box; +use alloc::str::FromStr; use alloc::string::ToString; -use core::str::FromStr; use bech32::Hrp; @@ -9,12 +10,13 @@ use crate::errors::NetworkIdError; // the public API since that crate does not have a stable release. /// The identifier of a Miden network. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum NetworkId { Mainnet, Testnet, Devnet, - Custom(Hrp), + // Box the [`CustomNetworkId`] to keep the stack size of network ID relatively small. + Custom(Box), } impl NetworkId { @@ -43,7 +45,7 @@ impl NetworkId { NetworkId::MAINNET => NetworkId::Mainnet, NetworkId::TESTNET => NetworkId::Testnet, NetworkId::DEVNET => NetworkId::Devnet, - _ => NetworkId::Custom(hrp), + _ => NetworkId::Custom(Box::new(CustomNetworkId::from_hrp(hrp))), } } @@ -59,7 +61,7 @@ impl NetworkId { Hrp::parse(NetworkId::TESTNET).expect("testnet hrp should be valid") }, NetworkId::Devnet => Hrp::parse(NetworkId::DEVNET).expect("devnet hrp should be valid"), - NetworkId::Custom(custom) => custom, + NetworkId::Custom(custom) => custom.as_hrp(), } } @@ -105,3 +107,50 @@ impl core::fmt::Display for NetworkId { f.write_str(self.as_str()) } } + +// CUSTOM NETWORK ID +// ================================================================================================ + +/// A wrapper around bech32 HRP(human-readable part) for custom network identifiers. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CustomNetworkId { + hrp: Hrp, +} + +impl CustomNetworkId { + /// Creates a new [`CustomNetworkId`] from a [`bech32::Hrp`]. + pub(crate) fn from_hrp(hrp: Hrp) -> Self { + CustomNetworkId { hrp } + } + + /// Converts this [`CustomNetworkId`] to a [`bech32::Hrp`]. + pub(crate) fn as_hrp(&self) -> Hrp { + self.hrp + } + + /// Returns the string representation of this custom HRP. + pub fn as_str(&self) -> &str { + self.hrp.as_str() + } +} + +impl FromStr for CustomNetworkId { + type Err = NetworkIdError; + + /// Creates a [`CustomNetworkId`] from a String. + /// # Errors + /// + /// Returns an error if the string is not a valid HRP according to bech32 rules + fn from_str(hrp_str: &str) -> Result { + Ok(CustomNetworkId { + hrp: Hrp::parse(hrp_str) + .map_err(|source| NetworkIdError::NetworkIdParseError(source.to_string().into()))?, + }) + } +} + +impl core::fmt::Display for CustomNetworkId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/crates/miden-objects/src/address/routing_parameters.rs b/crates/miden-objects/src/address/routing_parameters.rs new file mode 100644 index 0000000000..5d173f9ee6 --- /dev/null +++ b/crates/miden-objects/src/address/routing_parameters.rs @@ -0,0 +1,577 @@ +use alloc::borrow::ToOwned; +use alloc::string::{String, ToString}; +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::ies::SealingKey; +use crate::errors::Bech32Error; +use crate::note::NoteTag; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; +use crate::utils::sync::LazyLock; + +/// The HRP used for encoding routing parameters. +/// +/// This HRP is only used internally, but needs to be well-defined for other routing parameter +/// encode/decode implementations. +/// +/// `mrp` stands for Miden Routing Parameters. +static ROUTING_PARAMETERS_HRP: LazyLock = + LazyLock::new(|| Hrp::parse("mrp").expect("hrp should be valid")); + +/// The separator character used in bech32. +const BECH32_SEPARATOR: &str = "1"; + +/// The value to encode the absence of a note tag routing parameter (i.e. `None`). +/// +/// The note tag length occupies 5 bits (values 0..=31). Valid tag lengths are 0..=30, +/// so we reserve the maximum 5-bit value (31) to represent `None`. +/// +/// If the note tag length is absent from routing parameters, the note tag length for the address +/// will be set to the default default tag length of the address' ID component. +const ABSENT_NOTE_TAG_LEN: u8 = (1 << 5) - 1; // 31 + +/// The routing parameter key for the receiver profile. +const RECEIVER_PROFILE_PARAM_KEY: u8 = 0; + +/// The routing parameter key for the encryption key. +const ENCRYPTION_KEY_PARAM_KEY: u8 = 1; + +/// The expected length of Ed25519/X25519 public keys in bytes. +const X25519_PUBLIC_KEY_LENGTH: usize = 32; + +/// The expected length of K256 (secp256k1) public keys in bytes (compressed format). +const K256_PUBLIC_KEY_LENGTH: usize = 33; + +/// Discriminants for encryption key variants. +const ENCRYPTION_KEY_X25519_XCHACHA20POLY1305: u8 = 0; +const ENCRYPTION_KEY_K256_XCHACHA20POLY1305: u8 = 1; +const ENCRYPTION_KEY_X25519_AEAD_RPO: u8 = 2; +const ENCRYPTION_KEY_K256_AEAD_RPO: u8 = 3; + +/// Parameters that define how a sender should route a note to the [`AddressId`](super::AddressId) +/// in an [`Address`](super::Address). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RoutingParameters { + interface: AddressInterface, + note_tag_len: Option, + encryption_key: Option, +} + +impl RoutingParameters { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates new [`RoutingParameters`] from an [`AddressInterface`] and all other parameters + /// initialized to `None`. + pub fn new(interface: AddressInterface) -> Self { + Self { + interface, + note_tag_len: None, + encryption_key: None, + } + } + + /// Sets the note tag length routing parameter. + /// + /// 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. + /// + /// # Errors + /// + /// Returns an error if: + /// - The tag length exceeds the maximum of [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and + /// [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]. + pub fn with_note_tag_len(mut self, note_tag_len: u8) -> Result { + if note_tag_len > NoteTag::MAX_LOCAL_TAG_LENGTH { + return Err(AddressError::TagLengthTooLarge(note_tag_len)); + } + + self.note_tag_len = Some(note_tag_len); + Ok(self) + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// 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`]). + pub fn note_tag_len(&self) -> Option { + self.note_tag_len + } + + /// Returns the [`AddressInterface`] of the account to which the address points. + pub fn interface(&self) -> AddressInterface { + self.interface + } + + /// Returns the public encryption key. + pub fn encryption_key(&self) -> Option<&SealingKey> { + self.encryption_key.as_ref() + } + + /// Sets the encryption key routing parameter. + /// + /// This allows senders to encrypt note payloads using sealed box encryption + /// for the recipient of this address. + pub fn with_encryption_key(mut self, key: SealingKey) -> Self { + self.encryption_key = Some(key); + self + } + + // HELPERS + // -------------------------------------------------------------------------------------------- + + /// Encodes [`RoutingParameters`] to a byte vector. + pub(crate) fn encode_to_bytes(&self) -> Vec { + let mut encoded = Vec::new(); + + // Append the receiver profile key and the encoded value to the vector. + encoded.push(RECEIVER_PROFILE_PARAM_KEY); + encoded.extend(encode_receiver_profile(self.interface, self.note_tag_len)); + + // Append the encryption key if present. + if let Some(encryption_key) = &self.encryption_key { + encoded.push(ENCRYPTION_KEY_PARAM_KEY); + encode_encryption_key(encryption_key, &mut encoded); + } + + encoded + } + + /// Encodes [`RoutingParameters`] to a bech32 string _without_ the leading hrp and separator. + pub(crate) fn encode_to_string(&self) -> String { + let encoded = self.encode_to_bytes(); + + let bech32_str = + bech32::encode::(*ROUTING_PARAMETERS_HRP, &encoded).expect("TODO"); + let encoded_str = bech32_str + .strip_prefix(ROUTING_PARAMETERS_HRP.as_str()) + .expect("bech32 str should start with the hrp"); + let encoded_str = encoded_str + .strip_prefix(BECH32_SEPARATOR) + .expect("encoded str should start with bech32 separator `1`"); + encoded_str.to_owned() + } + + /// Decodes [`RoutingParameters`] from a bech32 string _without_ the leading hrp and separator. + pub(crate) fn decode(mut bech32_string: String) -> Result { + // ------ Decode bech32 string into bytes ------ + + // Reinsert the expected HRP into the string that is stripped during encoding. + bech32_string.insert_str(0, BECH32_SEPARATOR); + bech32_string.insert_str(0, ROUTING_PARAMETERS_HRP.as_str()); + + // We use CheckedHrpString with an explicit checksum algorithm so we don't allow the + // `Bech32` or `NoChecksum` algorithms. + let checked_string = + CheckedHrpstring::new::(&bech32_string).map_err(|source| { + // The CheckedHrpStringError does not implement core::error::Error, only + // std::error::Error, so for now we convert it to a String. Even if it will + // implement the trait in the future, we should include it as an opaque + // error since the crate does not have a stable release yet. + AddressError::decode_error_with_source( + "failed to decode routing parameters bech32 string", + Bech32Error::DecodeError(source.to_string().into()), + ) + })?; + + Self::decode_from_bytes(checked_string.byte_iter()) + } + + /// Decodes [`RoutingParameters`] from a byte iterator. + pub(crate) fn decode_from_bytes( + mut byte_iter: impl ExactSizeIterator, + ) -> Result { + let mut interface = None; + let mut note_tag_len = None; + let mut encryption_key = None; + + while let Some(key) = byte_iter.next() { + match key { + RECEIVER_PROFILE_PARAM_KEY => { + if interface.is_some() { + return Err(AddressError::decode_error( + "duplicate receiver profile routing parameter", + )); + } + let receiver_profile = decode_receiver_profile(&mut byte_iter)?; + interface = Some(receiver_profile.0); + note_tag_len = receiver_profile.1; + }, + ENCRYPTION_KEY_PARAM_KEY => { + if encryption_key.is_some() { + return Err(AddressError::decode_error( + "duplicate encryption key routing parameter", + )); + } + encryption_key = Some(decode_encryption_key(&mut byte_iter)?); + }, + other => { + return Err(AddressError::UnknownRoutingParameterKey(other)); + }, + } + } + + let interface = interface.ok_or_else(|| { + AddressError::decode_error("interface must be present in routing parameters") + })?; + + let mut routing_parameters = RoutingParameters::new(interface); + routing_parameters.note_tag_len = note_tag_len; + routing_parameters.encryption_key = encryption_key; + + Ok(routing_parameters) + } +} + +impl Serializable for RoutingParameters { + fn write_into(&self, target: &mut W) { + let bytes = self.encode_to_bytes(); + // Due to the bech32 constraint of max 633 bytes, a u16 is sufficient. + let num_bytes = bytes.len() as u16; + + target.write_u16(num_bytes); + target.write_many(bytes); + } +} + +impl Deserializable for RoutingParameters { + fn read_from(source: &mut R) -> Result { + let num_bytes = source.read_u16()?; + let bytes: Vec = source.read_many(num_bytes as usize)?; + + Self::decode_from_bytes(bytes.into_iter()) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// ENCODING / DECODING HELPERS +// ================================================================================================ + +/// Returns receiver profile bytes constructed from the provided interface and note tag length. +fn encode_receiver_profile(interface: AddressInterface, note_tag_len: Option) -> [u8; 2] { + let note_tag_len = note_tag_len.unwrap_or(ABSENT_NOTE_TAG_LEN); + + let interface = interface as u16; + debug_assert_eq!(interface >> 11, 0, "address interface should have its upper 5 bits unset"); + + // The interface takes up 11 bits and the tag length 5 bits, so we can merge them + // together. + let tag_len = (note_tag_len as u16) << 11; + let receiver_profile: u16 = tag_len | interface; + receiver_profile.to_be_bytes() +} + +/// Reads the receiver profile from the provided bytes. +fn decode_receiver_profile( + byte_iter: &mut impl ExactSizeIterator, +) -> Result<(AddressInterface, Option), AddressError> { + if byte_iter.len() < 2 { + return Err(AddressError::decode_error("expected two bytes to decode receiver profile")); + }; + + let byte0 = byte_iter.next().expect("byte0 should exist"); + let byte1 = byte_iter.next().expect("byte1 should exist"); + let receiver_profile = u16::from_be_bytes([byte0, byte1]); + + let tag_len = (receiver_profile >> 11) as u8; + let note_tag_len = if tag_len == ABSENT_NOTE_TAG_LEN { + None + } else { + Some(tag_len) + }; + + let addr_interface = receiver_profile & 0b0000_0111_1111_1111; + let addr_interface = AddressInterface::try_from(addr_interface).map_err(|err| { + AddressError::decode_error_with_source("failed to decode address interface", err) + })?; + + Ok((addr_interface, note_tag_len)) +} + +/// Append encryption key variant discriminant and key to the provided vector of bytes. +fn encode_encryption_key(key: &SealingKey, encoded: &mut Vec) { + match key { + SealingKey::X25519XChaCha20Poly1305(pk) => { + encoded.push(ENCRYPTION_KEY_X25519_XCHACHA20POLY1305); + encoded.extend(&pk.to_bytes()); + }, + SealingKey::K256XChaCha20Poly1305(pk) => { + encoded.push(ENCRYPTION_KEY_K256_XCHACHA20POLY1305); + encoded.extend(&pk.to_bytes()); + }, + SealingKey::X25519AeadRpo(pk) => { + encoded.push(ENCRYPTION_KEY_X25519_AEAD_RPO); + encoded.extend(&pk.to_bytes()); + }, + SealingKey::K256AeadRpo(pk) => { + encoded.push(ENCRYPTION_KEY_K256_AEAD_RPO); + encoded.extend(&pk.to_bytes()); + }, + } +} + +/// Reads the encryption key from the provided bytes. +fn decode_encryption_key( + byte_iter: &mut impl ExactSizeIterator, +) -> Result { + // Read variant discriminant + let Some(variant) = byte_iter.next() else { + return Err(AddressError::decode_error( + "expected at least 1 byte for encryption key variant", + )); + }; + + // Reconstruct the appropriate PublicEncryptionKey variant + let public_encryption_key = match variant { + ENCRYPTION_KEY_X25519_XCHACHA20POLY1305 => { + SealingKey::X25519XChaCha20Poly1305(read_x25519_pub_key(byte_iter)?) + }, + ENCRYPTION_KEY_K256_XCHACHA20POLY1305 => { + SealingKey::K256XChaCha20Poly1305(read_k256_pub_key(byte_iter)?) + }, + ENCRYPTION_KEY_X25519_AEAD_RPO => { + SealingKey::X25519AeadRpo(read_x25519_pub_key(byte_iter)?) + }, + ENCRYPTION_KEY_K256_AEAD_RPO => SealingKey::K256AeadRpo(read_k256_pub_key(byte_iter)?), + other => { + return Err(AddressError::decode_error(format!( + "unknown encryption key variant: {}", + other + ))); + }, + }; + + Ok(public_encryption_key) +} + +fn read_x25519_pub_key( + byte_iter: &mut impl ExactSizeIterator, +) -> Result { + if byte_iter.len() < X25519_PUBLIC_KEY_LENGTH { + return Err(AddressError::decode_error(format!( + "expected {} bytes to decode X25519 public key", + X25519_PUBLIC_KEY_LENGTH + ))); + } + let key_bytes: [u8; X25519_PUBLIC_KEY_LENGTH] = read_byte_array(byte_iter); + eddsa_25519::PublicKey::read_from_bytes(&key_bytes).map_err(|err| { + AddressError::decode_error_with_source("failed to decode X25519 public key", err) + }) +} + +fn read_k256_pub_key( + byte_iter: &mut impl ExactSizeIterator, +) -> Result { + if byte_iter.len() < K256_PUBLIC_KEY_LENGTH { + return Err(AddressError::decode_error(format!( + "expected {} bytes to decode K256 public key", + K256_PUBLIC_KEY_LENGTH + ))); + } + let key_bytes: [u8; K256_PUBLIC_KEY_LENGTH] = read_byte_array(byte_iter); + ecdsa_k256_keccak::PublicKey::read_from_bytes(&key_bytes).map_err(|err| { + AddressError::decode_error_with_source("failed to decode K256 public key", err) + }) +} + +/// Reads bytes from the provided iterator into an array of length N and returns this array. +/// +/// Assumes that there are at least N bytes in the iterator. +fn read_byte_array(byte_iter: &mut impl ExactSizeIterator) -> [u8; N] { + let mut array = [0u8; N]; + for byte in array.iter_mut() { + *byte = byte_iter.next().expect("iterator should have enough bytes"); + } + array +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use bech32::{Bech32m, Checksum, Hrp}; + + use super::*; + + /// Checks the assumptions about the total length allowed in bech32 encoding. + /// + /// The assumption is that encoding should error if the total length of the hrp + data (encoded + /// in GF(32)) + the separator + the checksum exceeds Bech32m::CODE_LENGTH. + #[test] + fn bech32_code_length_assertions() -> anyhow::Result<()> { + let hrp = Hrp::parse("mrp").unwrap(); + let separator_len = BECH32_SEPARATOR.len(); + // The fixed number of characters included in a bech32 string. + let fixed_num_bytes = hrp.as_str().len() + separator_len + Bech32m::CHECKSUM_LENGTH; + let num_allowed_chars = Bech32m::CODE_LENGTH - fixed_num_bytes; + // Multiply by the 5 bits per base32 character and divide by 8 bits per byte. + let num_allowed_bytes = num_allowed_chars * 5 / 8; + + // The number of bytes that routing parameters effectively have available. + assert_eq!(num_allowed_bytes, 633); + + // This amount of data is the max that should be okay to encode. + let data_ok = vec![5; num_allowed_bytes]; + // One more byte than the max allowed amount should result in an error. + let data_too_long = vec![5; num_allowed_bytes + 1]; + + assert!(bech32::encode::(hrp, &data_ok).is_ok()); + assert!(bech32::encode::(hrp, &data_too_long).is_err()); + + Ok(()) + } + + /// Tests bech32 encoding and decoding roundtrip with various tag lengths. + #[test] + fn routing_parameters_bech32_encode_decode_roundtrip() -> anyhow::Result<()> { + // Test case 1: No explicit tag length + let params_no_tag = RoutingParameters::new(AddressInterface::BasicWallet); + let encoded = params_no_tag.encode_to_string(); + let decoded = RoutingParameters::decode(encoded)?; + assert_eq!(params_no_tag, decoded); + assert_eq!(decoded.note_tag_len(), None); + + // Test case 2: Explicit tag length 0 + let params_tag_0 = + RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(0)?; + let encoded = params_tag_0.encode_to_string(); + let decoded = RoutingParameters::decode(encoded)?; + assert_eq!(params_tag_0, decoded); + assert_eq!(decoded.note_tag_len(), Some(0)); + + // Test case 3: Explicit tag length 6 + let params_tag_6 = + RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(6)?; + let encoded = params_tag_6.encode_to_string(); + let decoded = RoutingParameters::decode(encoded)?; + assert_eq!(params_tag_6, decoded); + assert_eq!(decoded.note_tag_len(), Some(6)); + + // 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)?; + 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)); + + Ok(()) + } + + /// Tests serialization and deserialization roundtrip with various tag lengths. + #[test] + fn routing_parameters_serialization() -> anyhow::Result<()> { + // Test case 1: No explicit tag length + let params_no_tag = RoutingParameters::new(AddressInterface::BasicWallet); + let serialized = params_no_tag.to_bytes(); + let deserialized = RoutingParameters::read_from_bytes(&serialized)?; + assert_eq!(params_no_tag, deserialized); + assert_eq!(deserialized.note_tag_len(), None); + + // Test case 2: Explicit tag length 0 + let params_tag_0 = + RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(0)?; + let serialized = params_tag_0.to_bytes(); + let deserialized = RoutingParameters::read_from_bytes(&serialized)?; + assert_eq!(params_tag_0, deserialized); + assert_eq!(deserialized.note_tag_len(), Some(0)); + + // Test case 3: Explicit tag length 6 + let params_tag_6 = + RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(6)?; + let serialized = params_tag_6.to_bytes(); + let deserialized = RoutingParameters::read_from_bytes(&serialized)?; + assert_eq!(params_tag_6, deserialized); + assert_eq!(deserialized.note_tag_len(), Some(6)); + + // 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)?; + 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)); + + Ok(()) + } + + /// Tests encoding/decoding and serialization for all encryption key variants. + #[test] + fn routing_parameters_all_encryption_key_variants() -> anyhow::Result<()> { + // Helper function to test both encoding/decoding and serialization + fn test_encryption_key_roundtrip(encryption_key: SealingKey) -> anyhow::Result<()> { + let routing_params = RoutingParameters::new(AddressInterface::BasicWallet) + .with_encryption_key(encryption_key.clone()); + + // Test bech32 encoding/decoding + let encoded = routing_params.encode_to_string(); + let decoded = RoutingParameters::decode(encoded)?; + assert_eq!(routing_params, decoded); + assert_eq!(decoded.encryption_key(), Some(&encryption_key)); + + // Test serialization/deserialization + let serialized = routing_params.to_bytes(); + let deserialized = RoutingParameters::read_from_bytes(&serialized)?; + assert_eq!(routing_params, deserialized); + assert_eq!(deserialized.encryption_key(), Some(&encryption_key)); + + Ok(()) + } + + // Test X25519XChaCha20Poly1305 + { + use crate::crypto::dsa::eddsa_25519::SecretKey; + let secret_key = SecretKey::with_rng(&mut rand::rng()); + let public_key = secret_key.public_key(); + let encryption_key = SealingKey::X25519XChaCha20Poly1305(public_key); + test_encryption_key_roundtrip(encryption_key)?; + } + + // Test K256XChaCha20Poly1305 + { + use crate::crypto::dsa::ecdsa_k256_keccak::SecretKey; + let secret_key = SecretKey::with_rng(&mut rand::rng()); + let public_key = secret_key.public_key(); + let encryption_key = SealingKey::K256XChaCha20Poly1305(public_key); + test_encryption_key_roundtrip(encryption_key)?; + } + + // Test X25519AeadRpo + { + use crate::crypto::dsa::eddsa_25519::SecretKey; + let secret_key = SecretKey::with_rng(&mut rand::rng()); + let public_key = secret_key.public_key(); + let encryption_key = SealingKey::X25519AeadRpo(public_key); + test_encryption_key_roundtrip(encryption_key)?; + } + + // Test K256AeadRpo + { + use crate::crypto::dsa::ecdsa_k256_keccak::SecretKey; + let secret_key = SecretKey::with_rng(&mut rand::rng()); + let public_key = secret_key.public_key(); + let encryption_key = SealingKey::K256AeadRpo(public_key); + test_encryption_key_roundtrip(encryption_key)?; + } + + Ok(()) + } +} diff --git a/crates/miden-objects/src/address/type.rs b/crates/miden-objects/src/address/type.rs index 2ed771a9ee..188755e28d 100644 --- a/crates/miden-objects/src/address/type.rs +++ b/crates/miden-objects/src/address/type.rs @@ -6,15 +6,23 @@ use crate::errors::Bech32Error; /// The byte values of this address type should be chosen as a multiple of 8. That way, the first /// character of the bech32 string after the `1` separator will be different for every address type. /// This makes the type of the address conveniently human-readable. +/// +/// For instance, [`AddressType::AccountId`] is chosen as 232 which is 0b1110_1000 in binary. Base32 +/// encodes one character for every 5 bits and 0b11101 (= 29) translates to `a`. So, every account +/// ID address will start with `mm1a`. +/// +/// See the table in the [bech32 spec](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32) +/// for a convenient overview. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[repr(u8)] +#[non_exhaustive] pub enum AddressType { AccountId = Self::ACCOUNT_ID, } impl AddressType { // Constants for internal use only. - const ACCOUNT_ID: u8 = 0; + const ACCOUNT_ID: u8 = 232; } impl TryFrom for AddressType { diff --git a/crates/miden-objects/src/asset/fungible.rs b/crates/miden-objects/src/asset/fungible.rs index 17f1ae112c..95e7dfdcb6 100644 --- a/crates/miden-objects/src/asset/fungible.rs +++ b/crates/miden-objects/src/asset/fungible.rs @@ -2,6 +2,7 @@ use alloc::boxed::Box; use alloc::string::ToString; use core::fmt; +use super::vault::AssetVaultKey; use super::{AccountType, Asset, AssetError, Felt, Word, ZERO, is_not_a_non_fungible_asset}; use crate::account::{AccountId, AccountIdPrefix}; use crate::utils::serde::{ @@ -83,8 +84,9 @@ impl FungibleAsset { } /// Returns the key which is used to store this asset in the account vault. - pub fn vault_key(&self) -> Word { - Self::vault_key_from_faucet(self.faucet_id) + pub fn vault_key(&self) -> AssetVaultKey { + AssetVaultKey::from_account_id(self.faucet_id) + .expect("faucet ID should be of type fungible") } // OPERATIONS @@ -161,14 +163,6 @@ impl FungibleAsset { Ok(self) } - - /// Returns the key which is used to store this asset in the account vault. - pub(super) fn vault_key_from_faucet(faucet_id: AccountId) -> Word { - let mut key = Word::empty(); - key[2] = faucet_id.suffix(); - key[3] = faucet_id.prefix().as_felt(); - key - } } impl From for Word { diff --git a/crates/miden-objects/src/asset/mod.rs b/crates/miden-objects/src/asset/mod.rs index 9078866113..3c3fc7d73c 100644 --- a/crates/miden-objects/src/asset/mod.rs +++ b/crates/miden-objects/src/asset/mod.rs @@ -15,13 +15,14 @@ use alloc::boxed::Box; pub use fungible::FungibleAsset; mod nonfungible; + pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails}; mod token_symbol; pub use token_symbol::TokenSymbol; mod vault; -pub use vault::{AssetVault, PartialVault}; +pub use vault::{AssetVault, AssetVaultKey, AssetWitness, PartialVault}; // ASSET // ================================================================================================ @@ -136,7 +137,7 @@ impl Asset { } /// Returns the key which is used to store this asset in the account vault. - pub fn vault_key(&self) -> Word { + pub fn vault_key(&self) -> AssetVaultKey { match self { Self::Fungible(asset) => asset.vault_key(), Self::NonFungible(asset) => asset.vault_key(), @@ -197,13 +198,14 @@ impl TryFrom for Asset { fn try_from(value: Word) -> Result { // Return an error if element 3 is not a valid account ID prefix, which cannot be checked by // is_not_a_non_fungible_asset. - AccountIdPrefix::try_from(value[3]) + // Keep in mind serialized assets do _not_ carry the suffix required to reconstruct the full + // account identifier. + let prefix = AccountIdPrefix::try_from(value[3]) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::from(err)))?; - - if is_not_a_non_fungible_asset(value) { - FungibleAsset::try_from(value).map(Asset::from) - } else { - NonFungibleAsset::try_from(value).map(Asset::from) + match prefix.account_type() { + AccountType::FungibleFaucet => FungibleAsset::try_from(value).map(Asset::from), + AccountType::NonFungibleFaucet => NonFungibleAsset::try_from(value).map(Asset::from), + _ => Err(AssetError::InvalidFaucetAccountIdPrefix(prefix)), } } } diff --git a/crates/miden-objects/src/asset/nonfungible.rs b/crates/miden-objects/src/asset/nonfungible.rs index aaeb8a3f7f..b0fa5f5ddd 100644 --- a/crates/miden-objects/src/asset/nonfungible.rs +++ b/crates/miden-objects/src/asset/nonfungible.rs @@ -3,12 +3,13 @@ use alloc::string::ToString; use alloc::vec::Vec; use core::fmt; +use super::vault::AssetVaultKey; use super::{AccountIdPrefix, AccountType, Asset, AssetError, Felt, Hasher, Word}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; use crate::{FieldElement, WORD_SIZE}; -/// Position of the faucet_id inside the [`NonFungibleAsset`] word. -const FAUCET_ID_POS: usize = 3; +/// Position of the faucet_id inside the [`NonFungibleAsset`] word having fields in BigEndian. +const FAUCET_ID_POS_BE: usize = 3; // NON-FUNGIBLE ASSET // ================================================================================================ @@ -74,7 +75,7 @@ impl NonFungibleAsset { return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); } - data_hash[FAUCET_ID_POS] = Felt::from(faucet_id); + data_hash[FAUCET_ID_POS_BE] = Felt::from(faucet_id); Ok(Self(data_hash)) } @@ -106,22 +107,22 @@ impl NonFungibleAsset { /// It also ensures that there is never any collision in the leaf index between a non-fungible /// asset and a fungible asset, as the former's vault key always has the fungible bit set to `0` /// and the latter's vault key always has the bit set to `1`. - pub fn vault_key(&self) -> Word { + pub fn vault_key(&self) -> AssetVaultKey { let mut vault_key = self.0; // Swap prefix of faucet ID with hash0. - vault_key.swap(0, FAUCET_ID_POS); + vault_key.swap(0, FAUCET_ID_POS_BE); // Set the fungible bit to zero. vault_key[3] = AccountIdPrefix::clear_fungible_bit(self.faucet_id_prefix().version(), vault_key[3]); - vault_key + AssetVaultKey::new_unchecked(vault_key) } /// Return ID prefix of the faucet which issued this asset. pub fn faucet_id_prefix(&self) -> AccountIdPrefix { - AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS]) + AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS_BE]) } // HELPER FUNCTIONS @@ -133,7 +134,7 @@ impl NonFungibleAsset { /// - The faucet_id is not a valid non-fungible faucet ID. /// - The most significant bit of the asset is not ZERO. fn validate(&self) -> Result<(), AssetError> { - let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS]) + let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS_BE]) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; let account_type = faucet_id.account_type(); diff --git a/crates/miden-objects/src/asset/vault/asset_witness.rs b/crates/miden-objects/src/asset/vault/asset_witness.rs new file mode 100644 index 0000000000..41154d23c8 --- /dev/null +++ b/crates/miden-objects/src/asset/vault/asset_witness.rs @@ -0,0 +1,131 @@ +use miden_crypto::merkle::{InnerNodeInfo, SmtLeaf, SmtProof}; + +use super::vault_key::AssetVaultKey; +use crate::AssetError; +use crate::asset::Asset; + +/// A witness of an asset in an [`AssetVault`](super::AssetVault). +/// +/// It proves inclusion of a certain asset in the vault. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AssetWitness(SmtProof); + +impl AssetWitness { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`AssetWitness`] from an SMT proof. + /// + /// # Errors + /// + /// Returns an error if: + /// - any of the entries in the SMT leaf is not a valid asset. + /// - any of the entries' vault keys does not match the expected vault key of the asset. + pub fn new(smt_proof: SmtProof) -> Result { + for (vault_key, asset) in smt_proof.leaf().entries() { + let asset = Asset::try_from(asset)?; + if *vault_key != asset.vault_key().into() { + return Err(AssetError::AssetVaultKeyMismatch { + actual: *vault_key, + expected: asset.vault_key().into(), + }); + } + } + + Ok(Self(smt_proof)) + } + + /// Creates a new [`AssetWitness`] from an SMT proof without checking that the proof contains + /// valid assets. + /// + /// Prefer [`AssetWitness::new`] whenever possible. + pub fn new_unchecked(smt_proof: SmtProof) -> Self { + Self(smt_proof) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// 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) + } + + /// Returns an iterator over the [`Asset`]s in this witness. + pub fn assets(&self) -> impl Iterator { + // TODO: Avoid cloning the vector by not calling SmtLeaf::entries. + // Once SmtLeaf::entries returns a slice (i.e. once + // https://github.com/0xMiden/crypto/pull/521 is available), replace this match statement. + let entries = match self.0.leaf() { + SmtLeaf::Empty(_) => &[], + SmtLeaf::Single(kv_pair) => core::slice::from_ref(kv_pair), + SmtLeaf::Multiple(kv_pairs) => kv_pairs, + }; + + entries.iter().map(|(_key, value)| { + Asset::try_from(value).expect("asset witness should track valid assets") + }) + } + + /// Returns an iterator over every inner node of this witness' merkle path. + pub fn authenticated_nodes(&self) -> impl Iterator + '_ { + self.0 + .path() + .authenticated_nodes(self.0.leaf().index().value(), self.0.leaf().hash()) + .expect("leaf index is u64 and should be less than 2^SMT_DEPTH") + } +} + +impl From for SmtProof { + fn from(witness: AssetWitness) -> Self { + witness.0 + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use miden_crypto::merkle::Smt; + + use super::*; + use crate::Word; + use crate::asset::{FungibleAsset, NonFungibleAsset}; + + /// Tests that constructing an asset witness fails if any asset in the smt proof is invalid. + #[test] + fn create_asset_witness_fails_on_invalid_asset() -> anyhow::Result<()> { + let invalid_asset = Word::from([0, 0, 0, 5u32]); + let smt = Smt::with_entries([(invalid_asset, invalid_asset)])?; + let proof = smt.open(&invalid_asset); + + let err = AssetWitness::new(proof).unwrap_err(); + + assert_matches!(err, AssetError::InvalidFaucetAccountId(_)); + + Ok(()) + } + + /// Tests that constructing an asset witness fails if the vault key is from a fungible asset and + /// the asset is a non-fungible one. + #[test] + fn create_asset_witness_fails_on_vault_key_mismatch() -> anyhow::Result<()> { + let fungible_asset = FungibleAsset::mock(500); + let non_fungible_asset = NonFungibleAsset::mock(&[1]); + + let smt = + Smt::with_entries([(fungible_asset.vault_key().into(), non_fungible_asset.into())])?; + let proof = smt.open(&fungible_asset.vault_key().into()); + + let err = AssetWitness::new(proof).unwrap_err(); + + assert_matches!(err, AssetError::AssetVaultKeyMismatch { actual, expected } => { + assert_eq!(actual, fungible_asset.vault_key().into()); + assert_eq!(expected, non_fungible_asset.vault_key().into()); + }); + + Ok(()) + } +} diff --git a/crates/miden-objects/src/asset/vault/mod.rs b/crates/miden-objects/src/asset/vault/mod.rs index d3883a6b9c..88b57e0dcf 100644 --- a/crates/miden-objects/src/asset/vault/mod.rs +++ b/crates/miden-objects/src/asset/vault/mod.rs @@ -1,5 +1,8 @@ use alloc::string::ToString; +use miden_crypto::merkle::InnerNodeInfo; +use miden_processor::SMT_DEPTH; + use super::{ AccountType, Asset, @@ -18,6 +21,12 @@ use crate::{AssetVaultError, Word}; mod partial; pub use partial::PartialVault; +mod asset_witness; +pub use asset_witness::AssetWitness; + +mod vault_key; +pub use vault_key::AssetVaultKey; + // ASSET VAULT // ================================================================================================ @@ -38,13 +47,20 @@ pub struct AssetVault { } impl AssetVault { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The depth of the SMT that represents the asset vault. + pub const DEPTH: u8 = SMT_DEPTH; + // CONSTRUCTOR // -------------------------------------------------------------------------------------------- + /// Returns a new [AssetVault] initialized with the provided assets. pub fn new(assets: &[Asset]) -> Result { Ok(Self { asset_tree: Smt::with_entries( - assets.iter().map(|asset| (asset.vault_key(), (*asset).into())), + assets.iter().map(|asset| (asset.vault_key().into(), (*asset).into())), ) .map_err(AssetVaultError::DuplicateAsset)?, }) @@ -61,7 +77,7 @@ impl AssetVault { /// Returns true if the specified non-fungible asset is stored in this vault. pub fn has_non_fungible_asset(&self, asset: NonFungibleAsset) -> Result { // check if the asset is stored in the vault - match self.asset_tree.get_value(&asset.vault_key()) { + match self.asset_tree.get_value(&asset.vault_key().into()) { asset if asset == Smt::EMPTY_VALUE => Ok(false), _ => Ok(true), } @@ -78,7 +94,11 @@ impl AssetVault { } // if the tree value is [0, 0, 0, 0], the asset is not stored in the vault - match self.asset_tree.get_value(&FungibleAsset::vault_key_from_faucet(faucet_id)) { + match self.asset_tree.get_value( + &AssetVaultKey::from_account_id(faucet_id) + .expect("faucet ID should be of type fungible") + .into(), + ) { asset if asset == Smt::EMPTY_VALUE => Ok(0), asset => Ok(FungibleAsset::new_unchecked(asset).amount()), } @@ -86,12 +106,22 @@ impl AssetVault { /// Returns an iterator over the assets stored in the vault. pub fn assets(&self) -> impl Iterator + '_ { - self.asset_tree.entries().map(|x| Asset::new_unchecked(x.1)) + // SAFETY: The asset tree tracks only valid assets. + self.asset_tree.entries().map(|(_key, value)| Asset::new_unchecked(*value)) + } + + /// Returns an iterator over the inner nodes of the underlying [`Smt`]. + pub fn inner_nodes(&self) -> impl Iterator + '_ { + self.asset_tree.inner_nodes() } - /// Returns a reference to the Sparse Merkle Tree underling this asset vault. - pub fn asset_tree(&self) -> &Smt { - &self.asset_tree + /// Returns an opening of the leaf associated with `vault_key`. + /// + /// The `vault_key` can be obtained with [`Asset::vault_key`]. + pub fn open(&self, vault_key: AssetVaultKey) -> AssetWitness { + let smt_proof = self.asset_tree.open(&vault_key.into()); + // SAFETY: The asset vault should only contain valid assets. + AssetWitness::new_unchecked(smt_proof) } /// Returns a bool indicating whether the vault is empty. @@ -99,6 +129,22 @@ impl AssetVault { self.asset_tree.is_empty() } + /// Returns the number of non-empty leaves in the underlying [`Smt`]. + /// + /// Note that this may return a different value from [Self::num_assets()] as a single leaf may + /// contain more than one asset. + pub fn num_leaves(&self) -> usize { + self.asset_tree.num_leaves() + } + + /// Returns the number of assets in this vault. + /// + /// Note that this may return a different value from [Self::num_leaves()] as a single leaf may + /// contain more than one asset. + pub fn num_assets(&self) -> usize { + self.asset_tree.num_entries() + } + // PUBLIC MODIFIERS // -------------------------------------------------------------------------------------------- @@ -111,6 +157,7 @@ impl AssetVault { /// the vault. /// - If the delta contains a non-fungible asset removal that is not stored in the vault. /// - If the delta contains a non-fungible asset addition that is already stored in the vault. + /// - The maximum number of leaves per asset is exceeded. pub fn apply_delta(&mut self, delta: &AccountVaultDelta) -> Result<(), AssetVaultError> { for (&faucet_id, &delta) in delta.fungible().iter() { let asset = FungibleAsset::new(faucet_id, delta.unsigned_abs()) @@ -138,6 +185,7 @@ impl AssetVault { /// # Errors /// - If the total value of two fungible assets is greater than or equal to 2^63. /// - If the vault already contains the same non-fungible asset. + /// - The maximum number of leaves per asset is exceeded. pub fn add_asset(&mut self, asset: Asset) -> Result { Ok(match asset { Asset::Fungible(asset) => Asset::Fungible(self.add_fungible_asset(asset)?), @@ -150,19 +198,22 @@ impl AssetVault { /// /// # Errors /// - If the total value of assets is greater than or equal to 2^63. + /// - The maximum number of leaves per asset is exceeded. fn add_fungible_asset( &mut self, asset: FungibleAsset, ) -> Result { // fetch current asset value from the tree and add the new asset to it. - let new: FungibleAsset = match self.asset_tree.get_value(&asset.vault_key()) { + let new: FungibleAsset = match self.asset_tree.get_value(&asset.vault_key().into()) { current if current == Smt::EMPTY_VALUE => asset, current => { let current = FungibleAsset::new_unchecked(current); current.add(asset).map_err(AssetVaultError::AddFungibleAssetBalanceError)? }, }; - self.asset_tree.insert(new.vault_key(), new.into()); + self.asset_tree + .insert(new.vault_key().into(), new.into()) + .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; // return the new asset Ok(new) @@ -172,12 +223,16 @@ impl AssetVault { /// /// # Errors /// - If the vault already contains the same non-fungible asset. + /// - The maximum number of leaves per asset is exceeded. fn add_non_fungible_asset( &mut self, asset: NonFungibleAsset, ) -> Result { // add non-fungible asset to the vault - let old = self.asset_tree.insert(asset.vault_key(), asset.into()); + let old = self + .asset_tree + .insert(asset.vault_key().into(), asset.into()) + .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; // if the asset already exists, return an error if old != Smt::EMPTY_VALUE { @@ -214,12 +269,13 @@ impl AssetVault { /// # Errors /// - The asset is not found in the vault. /// - The amount of the asset in the vault is less than the amount to be removed. + /// - The maximum number of leaves per asset is exceeded. fn remove_fungible_asset( &mut self, asset: FungibleAsset, ) -> Result { // fetch the asset from the vault. - let new: FungibleAsset = match self.asset_tree.get_value(&asset.vault_key()) { + let new: FungibleAsset = match self.asset_tree.get_value(&asset.vault_key().into()) { current if current == Smt::EMPTY_VALUE => { return Err(AssetVaultError::FungibleAssetNotFound(asset)); }, @@ -234,7 +290,9 @@ impl AssetVault { 0 => Smt::EMPTY_VALUE, _ => new.into(), }; - self.asset_tree.insert(new.vault_key(), value); + self.asset_tree + .insert(new.vault_key().into(), value) + .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; // return the asset that was removed. Ok(asset) @@ -245,12 +303,16 @@ impl AssetVault { /// /// # Errors /// - The non-fungible asset is not found in the vault. + /// - The maximum number of leaves per asset is exceeded. fn remove_non_fungible_asset( &mut self, asset: NonFungibleAsset, ) -> Result { // remove the asset from the vault. - let old = self.asset_tree.insert(asset.vault_key(), Smt::EMPTY_VALUE); + let old = self + .asset_tree + .insert(asset.vault_key().into(), Smt::EMPTY_VALUE) + .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; // return an error if the asset did not exist in the vault. if old == Smt::EMPTY_VALUE { diff --git a/crates/miden-objects/src/asset/vault/partial.rs b/crates/miden-objects/src/asset/vault/partial.rs index 4f3b7061d0..b303842cff 100644 --- a/crates/miden-objects/src/asset/vault/partial.rs +++ b/crates/miden-objects/src/asset/vault/partial.rs @@ -2,9 +2,9 @@ use alloc::string::ToString; use miden_crypto::merkle::{InnerNodeInfo, MerkleError, PartialSmt, SmtLeaf, SmtProof}; -use super::AssetVault; +use super::{AssetVault, AssetVaultKey}; use crate::Word; -use crate::asset::Asset; +use crate::asset::{Asset, AssetWitness}; use crate::errors::PartialAssetVaultError; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; @@ -23,18 +23,29 @@ impl PartialVault { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Returns a new instance of a partial vault from the provided partial SMT. + /// Constructs a [`PartialVault`] from an [`AssetVault`] root. /// - /// # Errors + /// For conversion from an [`AssetVault`], prefer [`Self::new_minimal`] to be more explicit. + pub fn new(root: Word) -> Self { + PartialVault { partial_smt: PartialSmt::new(root) } + } + + /// Converts an [`AssetVault`] into a partial vault representation. /// - /// Returns an error if: - /// - the provided SMT does not track only valid [`Asset`]s. - /// - the vault key at which the asset is stored does not match the vault key derived from the - /// asset. - pub fn new(partial_smt: PartialSmt) -> Result { - Self::validate_entries(partial_smt.entries())?; + /// The resulting [`PartialVault`] will contain the _full_ merkle paths of the original asset + /// vault. + pub fn new_full(vault: AssetVault) -> Self { + let partial_smt = PartialSmt::from(vault.asset_tree); - Ok(PartialVault { partial_smt }) + PartialVault { partial_smt } + } + + /// Converts an [`AssetVault`] into a partial vault representation. + /// + /// The resulting [`PartialVault`] will represent the root of the asset vault, but not track any + /// key-value pairs, which means it is the most _minimal_ representation of the asset vault. + pub fn new_minimal(vault: &AssetVault) -> Self { + PartialVault::new(vault.root()) } // ACCESSORS @@ -60,6 +71,23 @@ impl PartialVault { self.partial_smt.leaves().map(|(_, leaf)| leaf) } + /// Returns an opening of the leaf associated with `vault_key`. + /// + /// The `vault_key` can be obtained with [`Asset::vault_key`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the key is not tracked by this partial vault. + pub fn open(&self, vault_key: AssetVaultKey) -> Result { + let smt_proof = self + .partial_smt + .open(&vault_key.into()) + .map_err(PartialAssetVaultError::UntrackedAsset)?; + // SAFETY: The partial vault should only contain valid assets. + Ok(AssetWitness::new_unchecked(smt_proof)) + } + /// Returns the [`Asset`] associated with the given `vault_key`. /// /// The return value is `None` if the asset does not exist in the vault. @@ -68,8 +96,8 @@ impl PartialVault { /// /// Returns an error if: /// - the key is not tracked by this partial SMT. - pub fn get(&self, vault_key: Word) -> Result, MerkleError> { - self.partial_smt.get_value(&vault_key).map(|word| { + pub fn get(&self, vault_key: AssetVaultKey) -> Result, MerkleError> { + self.partial_smt.get_value(&vault_key.into()).map(|word| { if word.is_empty() { None } else { @@ -83,18 +111,15 @@ impl PartialVault { // MUTATORS // -------------------------------------------------------------------------------------------- - /// Adds an [`SmtProof`] to this [`PartialVault`]. + /// Adds an [`AssetWitness`] to this [`PartialVault`]. /// /// # Errors /// /// Returns an error if: - /// - the provided proof does not prove inclusion of valid [`Asset`]s. - /// - the vault key of the proven asset does not match the vault key derived from the asset. /// - the new root after the insertion of the leaf and the path does not match the existing root /// (except when the first leaf is added). - pub fn add(&mut self, proof: SmtProof) -> Result<(), PartialAssetVaultError> { - Self::validate_entries(proof.leaf().entries())?; - + pub fn add(&mut self, witness: AssetWitness) -> Result<(), PartialAssetVaultError> { + let proof = SmtProof::from(witness); self.partial_smt .add_proof(proof) .map_err(PartialAssetVaultError::FailedToAddProof) @@ -115,8 +140,8 @@ impl PartialVault { PartialAssetVaultError::InvalidAssetInSmt { entry: *asset, source } })?; - if asset.vault_key() != *vault_key { - return Err(PartialAssetVaultError::VaultKeyMismatch { + if *vault_key != asset.vault_key().into() { + return Err(PartialAssetVaultError::AssetVaultKeyMismatch { expected: asset.vault_key(), actual: *vault_key, }); @@ -127,11 +152,21 @@ impl PartialVault { } } -impl From<&AssetVault> for PartialVault { - fn from(value: &AssetVault) -> Self { - let vault_partial_smt = value.asset_tree.clone().into(); +impl TryFrom for PartialVault { + type Error = PartialAssetVaultError; + + /// Returns a new instance of a partial vault from the provided partial SMT. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided SMT does not track only valid [`Asset`]s. + /// - the vault key at which the asset is stored does not match the vault key derived from the + /// asset. + fn try_from(partial_smt: PartialSmt) -> Result { + Self::validate_entries(partial_smt.entries())?; - PartialVault { partial_smt: vault_partial_smt } + Ok(PartialVault { partial_smt }) } } @@ -143,9 +178,9 @@ impl Serializable for PartialVault { impl Deserializable for PartialVault { fn read_from(source: &mut R) -> Result { - let vault_partial_smt = source.read()?; + let partial_smt: PartialSmt = source.read()?; - PartialVault::new(vault_partial_smt) + PartialVault::try_from(partial_smt) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -168,12 +203,7 @@ mod tests { let proof = smt.open(&invalid_asset); let partial_smt = PartialSmt::from_proofs([proof.clone()])?; - let err = PartialVault::new(partial_smt).unwrap_err(); - assert_matches!(err, PartialAssetVaultError::InvalidAssetInSmt { entry, .. } => { - assert_eq!(entry, invalid_asset); - }); - - let err = PartialVault::default().add(proof).unwrap_err(); + let err = PartialVault::try_from(partial_smt).unwrap_err(); assert_matches!(err, PartialAssetVaultError::InvalidAssetInSmt { entry, .. } => { assert_eq!(entry, invalid_asset); }); @@ -189,14 +219,8 @@ mod tests { let proof = smt.open(&invalid_vault_key); let partial_smt = PartialSmt::from_proofs([proof.clone()])?; - let err = PartialVault::new(partial_smt).unwrap_err(); - assert_matches!(err, PartialAssetVaultError::VaultKeyMismatch { expected, actual } => { - assert_eq!(actual, invalid_vault_key); - assert_eq!(expected, asset.vault_key()); - }); - - let err = PartialVault::default().add(proof).unwrap_err(); - assert_matches!(err, PartialAssetVaultError::VaultKeyMismatch { expected, actual } => { + let err = PartialVault::try_from(partial_smt).unwrap_err(); + assert_matches!(err, PartialAssetVaultError::AssetVaultKeyMismatch { expected, actual } => { assert_eq!(actual, invalid_vault_key); assert_eq!(expected, asset.vault_key()); }); diff --git a/crates/miden-objects/src/asset/vault/vault_key.rs b/crates/miden-objects/src/asset/vault/vault_key.rs new file mode 100644 index 0000000000..c68cf7be10 --- /dev/null +++ b/crates/miden-objects/src/asset/vault/vault_key.rs @@ -0,0 +1,176 @@ +use core::fmt; + +use miden_crypto::merkle::LeafIndex; +use miden_processor::SMT_DEPTH; + +use crate::Word; +use crate::account::AccountType::FungibleFaucet; +use crate::account::{AccountId, AccountIdPrefix}; +use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; + +/// The key of an [`Asset`] in the asset vault. +/// +/// The layout of an asset key is: +/// - Fungible asset key: `[0, 0, faucet_id_suffix, faucet_id_prefix]`. +/// - Non-fungible asset key: `[faucet_id_prefix, hash1, hash2, hash0']`, where `hash0'` is +/// equivalent to `hash0` with the fungible bit set to `0`. See [`NonFungibleAsset::vault_key`] +/// for more details. +/// +/// For details on the layout of an asset, see the documentation of [`Asset`]. +/// +/// ## Guarantees +/// +/// This type guarantees that it contains a valid fungible or non-fungible asset key: +/// - For fungible assets +/// - The felt at index 3 has the fungible bit set to 1 and it is a valid account ID prefix. +/// - The felt at index 2 is a valid account ID suffix. +/// - For non-fungible assets +/// - The felt at index 3 has the fungible bit set to 0. +/// - The felt at index 0 is a valid account ID prefix. +/// +/// The fungible bit is the bit in the [`AccountId`] that encodes whether the ID is a faucet. +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +pub struct AssetVaultKey(Word); + +impl AssetVaultKey { + /// Creates a new [`AssetVaultKey`] from the given [`Word`] **without performing validation**. + /// + /// ## Warning + /// + /// This function **does not check** whether the provided `Word` represents a valid + /// fungible or non-fungible asset key. + pub fn new_unchecked(value: Word) -> Self { + Self(value) + } + + /// Returns an [`AccountIdPrefix`] from the asset key. + pub fn faucet_id_prefix(&self) -> AccountIdPrefix { + if self.is_fungible() { + AccountIdPrefix::new_unchecked(self.0[3]) + } else { + AccountIdPrefix::new_unchecked(self.0[0]) + } + } + + /// Returns the [`AccountId`] from the asset key if it is a fungible asset, `None` otherwise. + pub fn faucet_id(&self) -> Option { + if self.is_fungible() { + Some(AccountId::new_unchecked([self.0[3], self.0[2]])) + } else { + None + } + } + + /// Returns the leaf index of a vault key. + pub fn to_leaf_index(&self) -> LeafIndex { + LeafIndex::::from(self.0) + } + + /// Constructs a fungible asset's key from a faucet ID. + /// + /// Returns `None` if the provided ID is not of type + /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) + pub fn from_account_id(faucet_id: AccountId) -> Option { + match faucet_id.account_type() { + FungibleFaucet => { + let mut key = Word::empty(); + key[2] = faucet_id.suffix(); + key[3] = faucet_id.prefix().as_felt(); + Some(AssetVaultKey::new_unchecked(key)) + }, + _ => None, + } + } + + /// Returns `true` if the asset key is for a fungible asset, `false` otherwise. + fn is_fungible(&self) -> bool { + self.0[0].as_int() == 0 && self.0[1].as_int() == 0 + } +} + +impl fmt::Display for AssetVaultKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +// CONVERSIONS +// ================================================================================================ + +impl From for Word { + fn from(vault_key: AssetVaultKey) -> Self { + vault_key.0 + } +} + +impl From for AssetVaultKey { + fn from(asset: Asset) -> Self { + asset.vault_key() + } +} + +impl From for AssetVaultKey { + fn from(fungible_asset: FungibleAsset) -> Self { + fungible_asset.vault_key() + } +} + +impl From for AssetVaultKey { + fn from(non_fungible_asset: NonFungibleAsset) -> Self { + non_fungible_asset.vault_key() + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_core::Felt; + + use super::*; + use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; + + fn make_non_fungible_key(prefix: u64) -> AssetVaultKey { + let word = [Felt::new(prefix), Felt::new(11), Felt::new(22), Felt::new(33)].into(); + AssetVaultKey::new_unchecked(word) + } + + #[test] + fn test_faucet_id_for_fungible_asset() { + let id = AccountId::dummy( + [0xff; 15], + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let key = + AssetVaultKey::from_account_id(id).expect("Expected AssetVaultKey for FungibleFaucet"); + + // faucet_id_prefix() should match AccountId prefix + assert_eq!(key.faucet_id_prefix(), id.prefix()); + + // faucet_id() should return the same account id + assert_eq!(key.faucet_id().unwrap(), id); + } + + #[test] + fn test_faucet_id_for_non_fungible_asset() { + let id = AccountId::dummy( + [0xff; 15], + AccountIdVersion::Version0, + AccountType::NonFungibleFaucet, + AccountStorageMode::Public, + ); + + let prefix_value = id.prefix().as_u64(); + let key = make_non_fungible_key(prefix_value); + + // faucet_id_prefix() should match AccountId prefix + assert_eq!(key.faucet_id_prefix(), id.prefix()); + + // faucet_id() should return the None + assert_eq!(key.faucet_id(), None); + } +} diff --git a/crates/miden-objects/src/batch/proposed_batch.rs b/crates/miden-objects/src/batch/proposed_batch.rs index 52c816892a..d24364ef43 100644 --- a/crates/miden-objects/src/batch/proposed_batch.rs +++ b/crates/miden-objects/src/batch/proposed_batch.rs @@ -427,7 +427,6 @@ mod tests { use anyhow::Context; use miden_crypto::merkle::{Mmr, PartialMmr}; use miden_verifier::ExecutionProof; - use winter_air::proof::Proof; use winter_rand_utils::rand_value; use super::*; @@ -473,7 +472,7 @@ mod tests { let block_num = reference_block_header.block_num(); let block_ref = reference_block_header.commitment(); let expiration_block_num = reference_block_header.block_num() + 1; - let proof = ExecutionProof::new(Proof::new_dummy(), Default::default()); + let proof = ExecutionProof::new_dummy(); let tx = ProvenTransactionBuilder::new( account_id, diff --git a/crates/miden-objects/src/batch/proven_batch.rs b/crates/miden-objects/src/batch/proven_batch.rs index c6217bb78f..97075a8736 100644 --- a/crates/miden-objects/src/batch/proven_batch.rs +++ b/crates/miden-objects/src/batch/proven_batch.rs @@ -12,6 +12,8 @@ use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, use crate::{MIN_PROOF_SECURITY_LEVEL, Word}; /// A transaction batch with an execution proof. +/// Currently, there is no proof attached. Future versions will extend this structure to include +/// a proof artifact once recursive proving is implemented. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProvenBatch { id: BatchId, diff --git a/crates/miden-objects/src/block/account_tree.rs b/crates/miden-objects/src/block/account_tree.rs index b6ab7481d9..0aa6655f9c 100644 --- a/crates/miden-objects/src/block/account_tree.rs +++ b/crates/miden-objects/src/block/account_tree.rs @@ -1,17 +1,222 @@ +use alloc::boxed::Box; use alloc::string::ToString; use alloc::vec::Vec; use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; -use miden_crypto::merkle::{MerkleError, MutationSet, Smt, SmtLeaf}; +use miden_crypto::merkle::{LeafIndex, MerkleError, MutationSet, Smt, SmtLeaf, SmtProof}; use miden_processor::{DeserializationError, SMT_DEPTH}; +use crate::Word; use crate::account::{AccountId, AccountIdPrefix}; use crate::block::AccountWitness; use crate::errors::AccountTreeError; -use crate::{Felt, Word}; -// ACCOUNT TREE +// FREE HELPER FUNCTIONS // ================================================================================================ +// These module-level functions provide conversions between AccountIds and SMT keys. +// They avoid the need for awkward syntax like account_id_to_smt_key(). + +const KEY_PREFIX_IDX: usize = 3; +const KEY_SUFFIX_IDX: usize = 2; + +/// Converts an [`AccountId`] to an SMT key for use in account trees. +/// +/// The key is constructed with the account ID suffix at index 2 and prefix at index 3. +pub fn account_id_to_smt_key(account_id: AccountId) -> Word { + let mut key = Word::empty(); + key[KEY_SUFFIX_IDX] = account_id.suffix(); + key[KEY_PREFIX_IDX] = account_id.prefix().as_felt(); + key +} + +/// Recovers an [`AccountId`] from an SMT key. +/// +/// # 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. +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; +} + +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() + } +} /// The sparse merkle tree of all accounts in the blockchain. /// @@ -22,11 +227,23 @@ use crate::{Felt, Word}; /// Each account ID occupies exactly one leaf in the tree, which is identified by its /// [`AccountId::prefix`]. In other words, account ID prefixes are unique in the blockchain. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccountTree { - smt: Smt, +pub struct AccountTree { + smt: S, +} + +impl Default for AccountTree +where + S: Default, +{ + fn default() -> Self { + Self { smt: Default::default() } + } } -impl AccountTree { +impl AccountTree +where + S: AccountTreeBackend, +{ // CONSTANTS // -------------------------------------------------------------------------------------------- @@ -41,68 +258,54 @@ impl AccountTree { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new, empty account tree. - pub fn new() -> Self { - AccountTree { smt: Smt::new() } - } - - /// Returns a new [`Smt`] instantiated with the provided entries. + /// Creates a new `AccountTree` from its inner representation with validation. /// - /// If the `concurrent` feature of `miden-crypto` is enabled, this function uses a parallel - /// implementation to process the entries efficiently, otherwise it defaults to the - /// sequential implementation. + /// This constructor validates that the provided SMT upholds the guarantees of the + /// [`AccountTree`]. The constructor ensures only the uniqueness of the account ID prefix. /// /// # Errors /// /// Returns an error if: - /// - the provided entries contain multiple commitments for the same account ID. - /// - multiple account IDs share the same prefix. - pub fn with_entries( - entries: impl IntoIterator, - ) -> Result - where - I: ExactSizeIterator, - { - let entries = entries.into_iter(); - let num_accounts = entries.len(); - - let smt = Smt::with_entries( - entries.map(|(id, commitment)| (Self::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( - Felt::try_from(leaf_idx).expect("leaf index should be a valid felt"), - ), - } - })?; - - // If the number of leaves in the SMT is smaller than the number of accounts that were - // passed in, it means that at least one account ID pair ended up in the same leaf. If this - // is the case, we iterate the SMT entries to find the duplicated account ID prefix. - if smt.num_leaves() < num_accounts { - for (leaf_idx, leaf) in smt.leaves() { - if leaf.num_entries() >= 2 { - // 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. - return Err(AccountTreeError::DuplicateIdPrefix { - duplicate_prefix: AccountIdPrefix::new_unchecked( - Felt::try_from(leaf_idx.value()) - .expect("leaf index should be a valid felt"), - ), - }); - } + /// - The SMT contains duplicate account ID prefixes + pub fn new(smt: S) -> Result { + for (_leaf_idx, leaf) in smt.leaves() { + match leaf { + SmtLeaf::Empty(_) => { + // Empty leaves are fine (shouldn't be returned by leaves() but handle anyway) + continue; + }, + SmtLeaf::Single((key, _)) => { + // Single entry is good - verify it's a valid account ID + Self::smt_key_to_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); + return Err(AccountTreeError::DuplicateIdPrefix { + duplicate_prefix: account_id.prefix(), + }); + } + }, } } - Ok(AccountTree { smt }) + Ok(Self::new_unchecked(smt)) + } + + /// Creates a new `AccountTree` from its inner representation without validation. + /// + /// # Warning + /// + /// Assumes the provided SMT upholds the guarantees of the [`AccountTree`]. Specifically: + /// - Each account ID prefix must be unique (no duplicate prefixes allowed) + /// - The SMT should only contain valid account IDs and their state commitments + /// + /// See type-level documentation for more details on these invariants. Using this constructor + /// with an SMT that violates these guarantees may lead to undefined behavior. + pub fn new_unchecked(smt: S) -> Self { + AccountTree { smt } } // PUBLIC ACCESSORS @@ -112,6 +315,10 @@ impl AccountTree { /// current state commitment of the given account ID. /// /// Conceptually, an opening is a Merkle path to the leaf, as well as the leaf itself. + /// + /// # Panics + /// + /// 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 proof = self.smt.open(&key); @@ -158,7 +365,7 @@ impl AccountTree { // SAFETY: By construction, the tree only contains valid IDs. AccountId::try_from([key[Self::KEY_PREFIX_IDX], key[Self::KEY_SUFFIX_IDX]]) .expect("account tree should only contain valid IDs"), - *commitment, + commitment, ) }) } @@ -184,11 +391,14 @@ impl AccountTree { &self, account_commitments: impl IntoIterator, ) -> Result { - let mutation_set = self.smt.compute_mutations( - account_commitments - .into_iter() - .map(|(id, commitment)| (Self::id_to_smt_key(id), commitment)), - ); + let mutation_set = self + .smt + .compute_mutations(Vec::from_iter( + account_commitments + .into_iter() + .map(|(id, commitment)| (Self::id_to_smt_key(id), commitment)), + )) + .map_err(AccountTreeError::ComputeMutations)?; for id_key in mutation_set.new_pairs().keys() { // Check if the insertion would be valid. @@ -234,7 +444,10 @@ impl AccountTree { state_commitment: Word, ) -> Result { let key = Self::id_to_smt_key(account_id); - let prev_value = self.smt.insert(key, state_commitment); + // 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) + .expect("account tree should always have a single value per key, and hence cannot exceed the maximum leaf number"); // If the leaf of the account ID now has two or more entries, we've inserted a duplicate // prefix. @@ -262,6 +475,26 @@ impl AccountTree { .map_err(AccountTreeError::ApplyMutations) } + /// Applies the prospective mutations computed with [`Self::compute_mutations`] to this tree + /// and returns the reverse mutation set. + /// + /// Applying the reverse mutation sets to the updated tree will revert the changes. + /// + /// # Errors + /// + /// Returns an error if: + /// - `mutations` was computed on a tree with a different root than this one. + pub fn apply_mutations_with_reversion( + &mut self, + mutations: AccountMutationSet, + ) -> Result { + let reversion = self + .smt + .apply_mutations_with_reversion(mutations.into_mutation_set()) + .map_err(AccountTreeError::ApplyMutations)?; + Ok(AccountMutationSet::new(reversion)) + } + // HELPERS // -------------------------------------------------------------------------------------------- @@ -299,9 +532,48 @@ impl AccountTree { } } -impl Default for AccountTree { - fn default() -> Self { - Self::new() +// 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) } } @@ -317,8 +589,23 @@ impl Serializable for AccountTree { impl Deserializable for AccountTree { fn read_from(source: &mut R) -> Result { let entries = Vec::<(AccountId, Word)>::read_from(source)?; - Self::with_entries(entries) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + + // Validate uniqueness of account ID prefixes before creating the tree + let mut seen_prefixes = alloc::collections::BTreeSet::new(); + for (id, _) in &entries { + if !seen_prefixes.insert(id.prefix()) { + return Err(DeserializationError::InvalidValue(format!( + "Duplicate account ID prefix: {}", + id.prefix() + ))); + } + } + + // Create the SMT with validated entries + let smt = + Smt::with_entries(entries.into_iter().map(|(k, v)| (account_id_to_smt_key(k), v))) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + Ok(Self::new_unchecked(smt)) } } @@ -333,7 +620,7 @@ impl Deserializable for AccountTree { /// It is returned by and used in methods on the [`AccountTree`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountMutationSet { - mutation_set: MutationSet<{ AccountTree::DEPTH }, Word, Word>, + mutation_set: MutationSet, } impl AccountMutationSet { @@ -341,7 +628,7 @@ impl AccountMutationSet { // -------------------------------------------------------------------------------------------- /// Creates a new [`AccountMutationSet`] from the provided raw mutation set. - fn new(mutation_set: MutationSet<{ AccountTree::DEPTH }, Word, Word>) -> Self { + fn new(mutation_set: MutationSet) -> Self { Self { mutation_set } } @@ -349,7 +636,7 @@ impl AccountMutationSet { // -------------------------------------------------------------------------------------------- /// Returns a reference to the underlying [`MutationSet`]. - pub fn as_mutation_set(&self) -> &MutationSet<{ AccountTree::DEPTH }, Word, Word> { + pub fn as_mutation_set(&self) -> &MutationSet { &self.mutation_set } @@ -357,7 +644,7 @@ impl AccountMutationSet { // -------------------------------------------------------------------------------------------- /// Consumes self and returns the underlying [`MutationSet`]. - pub fn into_mutation_set(self) -> MutationSet<{ AccountTree::DEPTH }, Word, Word> { + pub fn into_mutation_set(self) -> MutationSet { self.mutation_set } } @@ -399,7 +686,7 @@ pub(super) mod tests { #[test] fn insert_fails_on_duplicate_prefix() { - let mut tree = AccountTree::new(); + let mut tree = AccountTree::::default(); let [(id0, commitment0), (id1, commitment1)] = setup_duplicate_prefix_ids(); tree.insert(id0, commitment0).unwrap(); @@ -412,20 +699,9 @@ pub(super) mod tests { } if duplicate_prefix == id0.prefix()); } - #[test] - fn with_entries_fails_on_duplicate_prefix() { - let entries = setup_duplicate_prefix_ids(); - - let err = AccountTree::with_entries(entries.iter().copied()).unwrap_err(); - - assert_matches!(err, AccountTreeError::DuplicateIdPrefix { - duplicate_prefix - } if duplicate_prefix == entries[0].0.prefix()); - } - #[test] fn insert_succeeds_on_multiple_updates() { - let mut tree = AccountTree::new(); + let mut tree = AccountTree::::default(); let [(id0, commitment0), (_, commitment1)] = setup_duplicate_prefix_ids(); tree.insert(id0, commitment0).unwrap(); @@ -465,7 +741,6 @@ pub(super) mod tests { let commitment2 = Word::from([0, 0, 0, 99u32]); let tree = AccountTree::with_entries([pair0, (id2, commitment2)]).unwrap(); - let err = tree.compute_mutations([pair1]).unwrap_err(); assert_matches!(err, AccountTreeError::DuplicateIdPrefix { @@ -511,8 +786,8 @@ pub(super) mod tests { assert_eq!(tree.num_accounts(), 2); for id in [id0, id1] { - let (control_path, control_leaf) = - tree.smt.open(&AccountTree::id_to_smt_key(id)).into_parts(); + let proof = tree.smt.open(&account_id_to_smt_key(id)); + let (control_path, control_leaf) = proof.into_parts(); let witness = tree.open(id); assert_eq!(witness.leaf(), control_leaf); @@ -524,7 +799,7 @@ pub(super) mod tests { fn contains_account_prefix() { // Create a tree with a single account. let [pair0, pair1] = setup_duplicate_prefix_ids(); - let tree = AccountTree::with_entries([(pair0.0, pair0.1)]).unwrap(); + let tree = AccountTree::with_entries([pair0]).unwrap(); assert_eq!(tree.num_accounts(), 1); // Validate the leaf for the inserted account exists. @@ -537,4 +812,138 @@ pub(super) mod tests { let id1 = AccountIdBuilder::new().build_with_seed([7; 32]); assert!(!tree.contains_account_id_prefix(id1.prefix())); } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_basic_operations() { + use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + + // Create test data + let id0 = AccountIdBuilder::new().build_with_seed([5; 32]); + let id1 = AccountIdBuilder::new().build_with_seed([6; 32]); + let id2 = AccountIdBuilder::new().build_with_seed([7; 32]); + + let digest0 = Word::from([0, 0, 0, 1u32]); + let digest1 = Word::from([0, 0, 0, 2u32]); + let digest2 = Word::from([0, 0, 0, 3u32]); + + // Create AccountTree with LargeSmt backend + let tree = LargeSmt::::with_entries( + MemoryStorage::default(), + [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + ) + .map(AccountTree::new_unchecked) + .unwrap(); + + // Test basic operations + assert_eq!(tree.num_accounts(), 2); + assert_eq!(tree.get(id0), digest0); + assert_eq!(tree.get(id1), digest1); + + // Test opening + let witness0 = tree.open(id0); + assert_eq!(witness0.id(), id0); + + // Test mutations + let mut tree_mut = LargeSmt::::with_entries( + MemoryStorage::default(), + [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + ) + .map(AccountTree::new_unchecked) + .unwrap(); + tree_mut.insert(id2, digest2).unwrap(); + assert_eq!(tree_mut.num_accounts(), 3); + assert_eq!(tree_mut.get(id2), digest2); + + // Verify original tree unchanged + assert_eq!(tree.num_accounts(), 2); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_duplicate_prefix_check() { + use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + + let [(id0, commitment0), (id1, commitment1)] = setup_duplicate_prefix_ids(); + + let mut tree = AccountTree::new_unchecked(LargeSmt::new(MemoryStorage::default()).unwrap()); + + tree.insert(id0, commitment0).unwrap(); + assert_eq!(tree.get(id0), commitment0); + + let err = tree.insert(id1, commitment1).unwrap_err(); + + assert_matches!( + err, + AccountTreeError::DuplicateIdPrefix { duplicate_prefix } + if duplicate_prefix == id0.prefix() + ); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_apply_mutations() { + use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + + let id0 = AccountIdBuilder::new().build_with_seed([5; 32]); + let id1 = AccountIdBuilder::new().build_with_seed([6; 32]); + let id2 = AccountIdBuilder::new().build_with_seed([7; 32]); + + let digest0 = Word::from([0, 0, 0, 1u32]); + let digest1 = Word::from([0, 0, 0, 2u32]); + let digest2 = Word::from([0, 0, 0, 3u32]); + let digest3 = Word::from([0, 0, 0, 4u32]); + + let mut tree = LargeSmt::with_entries( + MemoryStorage::default(), + [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + ) + .map(AccountTree::new_unchecked) + .unwrap(); + + let mutations = tree + .compute_mutations([(id0, digest1), (id1, digest2), (id2, digest3)]) + .unwrap(); + + tree.apply_mutations(mutations).unwrap(); + + assert_eq!(tree.num_accounts(), 3); + assert_eq!(tree.get(id0), digest1); + assert_eq!(tree.get(id1), digest2); + assert_eq!(tree.get(id2), digest3); + } + + #[cfg(feature = "std")] + #[test] + fn large_smt_backend_same_root_as_regular_smt() { + use miden_crypto::merkle::{LargeSmt, MemoryStorage}; + + let id0 = AccountIdBuilder::new().build_with_seed([5; 32]); + let id1 = AccountIdBuilder::new().build_with_seed([6; 32]); + + let digest0 = Word::from([0, 0, 0, 1u32]); + let digest1 = Word::from([0, 0, 0, 2u32]); + + // Create tree with LargeSmt backend + let large_tree = LargeSmt::with_entries( + MemoryStorage::default(), + [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + ) + .map(AccountTree::new_unchecked) + .unwrap(); + + // Create tree with regular Smt backend + let regular_tree = AccountTree::with_entries([(id0, digest0), (id1, digest1)]).unwrap(); + + // Both should have the same root + assert_eq!(large_tree.root(), regular_tree.root()); + + // Both should have the same account commitments + let large_commitments: std::collections::BTreeMap<_, _> = + large_tree.account_commitments().collect(); + let regular_commitments: std::collections::BTreeMap<_, _> = + regular_tree.account_commitments().collect(); + + assert_eq!(large_commitments, regular_commitments); + } } diff --git a/crates/miden-objects/src/block/account_witness.rs b/crates/miden-objects/src/block/account_witness.rs index 5f7d8e3c16..ca654f90cb 100644 --- a/crates/miden-objects/src/block/account_witness.rs +++ b/crates/miden-objects/src/block/account_witness.rs @@ -3,25 +3,26 @@ use alloc::string::ToString; use miden_crypto::merkle::{ InnerNodeInfo, LeafIndex, - MerklePath, SMT_DEPTH, SmtLeaf, SmtProof, SmtProofError, + SparseMerklePath, }; use crate::account::AccountId; -use crate::block::AccountTree; +use crate::block::account_tree::{account_id_to_smt_key, smt_key_to_account_id}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; use crate::{AccountTreeError, Word}; // ACCOUNT WITNESS // ================================================================================================ -/// A specialized version of an [`SmtProof`] for use in [`AccountTree`] and +/// 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 /// ID at a certain state (i.e. [`Account::commitment`](crate::account::Account::commitment)) in the -/// [`AccountTree`]. +/// [`AccountTree`](super::account_tree::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. @@ -39,7 +40,7 @@ pub struct AccountWitness { /// The state commitment of the account ID. commitment: Word, /// The merkle path of the account witness. - path: MerklePath, + path: SparseMerklePath, } impl AccountWitness { @@ -48,15 +49,15 @@ impl AccountWitness { /// # Errors /// /// Returns an error if: - /// - the merkle path's depth is not [`AccountTree::DEPTH`]. + /// - the merkle path's depth is not [`SMT_DEPTH`]. pub fn new( account_id: AccountId, commitment: Word, - path: MerklePath, + path: SparseMerklePath, ) -> Result { - if path.len() != SMT_DEPTH as usize { + if path.depth() != SMT_DEPTH { return Err(AccountTreeError::WitnessMerklePathDepthDoesNotMatchAccountTreeDepth( - path.len(), + path.depth() as usize, )); } @@ -74,7 +75,7 @@ impl AccountWitness { /// # Panics /// /// Panics if: - /// - the merkle path in the proof does not have depth equal to [`AccountTree::DEPTH`]. + /// - the merkle path in the proof does not have depth equal to [`SMT_DEPTH`]. /// - the proof contains an SmtLeaf::Multiple. pub(super) fn from_smt_proof(requested_account_id: AccountId, proof: SmtProof) -> Self { // Check which account ID this proof actually contains. We rely on the fact that the @@ -89,7 +90,7 @@ impl AccountWitness { SmtLeaf::Empty(_) => requested_account_id, SmtLeaf::Single((key_in_leaf, _)) => { // SAFETY: By construction, the tree only contains valid IDs. - AccountTree::smt_key_to_id(*key_in_leaf) + smt_key_to_account_id(*key_in_leaf) }, SmtLeaf::Multiple(_) => { unreachable!("account tree should only contain zero or one entry per ID prefix") @@ -97,12 +98,12 @@ impl AccountWitness { }; let commitment = proof - .get(&AccountTree::id_to_smt_key(witness_id)) + .get(&account_id_to_smt_key(witness_id)) .expect("we should have received a proof for the witness key"); - // SAFETY: The proof is guaranteed to have depth AccountTree::DEPTH if it comes from one of + // SAFETY: The proof is guaranteed to have depth SMT_DEPTH if it comes from one of // the account trees. - debug_assert_eq!(proof.path().depth(), AccountTree::DEPTH); + debug_assert_eq!(proof.path().depth(), SMT_DEPTH); AccountWitness::new_unchecked(witness_id, commitment, proof.into_parts().0) } @@ -112,7 +113,11 @@ impl AccountWitness { /// # Warning /// /// This does not validate any of the guarantees of this type. - pub(super) fn new_unchecked(account_id: AccountId, commitment: Word, path: MerklePath) -> Self { + pub(super) fn new_unchecked( + account_id: AccountId, + commitment: Word, + path: SparseMerklePath, + ) -> Self { Self { id: account_id, commitment, path } } @@ -126,18 +131,18 @@ impl AccountWitness { self.commitment } - /// Returns the [`MerklePath`] of the account witness. - pub fn path(&self) -> &MerklePath { + /// Returns the [`SparseMerklePath`] of the account witness. + pub fn path(&self) -> &SparseMerklePath { &self.path } /// Returns the [`SmtLeaf`] of the account witness. pub fn leaf(&self) -> SmtLeaf { if self.commitment == Word::empty() { - let leaf_idx = LeafIndex::from(AccountTree::id_to_smt_key(self.id)); + let leaf_idx = LeafIndex::from(account_id_to_smt_key(self.id)); SmtLeaf::new_empty(leaf_idx) } else { - let key = AccountTree::id_to_smt_key(self.id); + let key = account_id_to_smt_key(self.id); SmtLeaf::new_single(key, self.commitment) } } @@ -145,6 +150,7 @@ impl AccountWitness { /// Consumes self and returns the inner proof. pub fn into_proof(self) -> SmtProof { let leaf = self.leaf(); + debug_assert_eq!(self.path.depth(), SMT_DEPTH); SmtProof::new(self.path, leaf) .expect("merkle path depth should be the SMT depth by construction") } @@ -179,11 +185,11 @@ impl Deserializable for AccountWitness { fn read_from(source: &mut R) -> Result { let id = AccountId::read_from(source)?; let commitment = Word::read_from(source)?; - let path = MerklePath::read_from(source)?; + let path = SparseMerklePath::read_from(source)?; - if path.len() != SMT_DEPTH as usize { + if path.depth() != SMT_DEPTH { return Err(DeserializationError::InvalidValue( - SmtProofError::InvalidMerklePathLength(path.len()).to_string(), + SmtProofError::InvalidMerklePathLength(path.depth() as usize).to_string(), )); } diff --git a/crates/miden-objects/src/block/block_account_update.rs b/crates/miden-objects/src/block/block_account_update.rs index cac9a6a3b7..d3e2541613 100644 --- a/crates/miden-objects/src/block/block_account_update.rs +++ b/crates/miden-objects/src/block/block_account_update.rs @@ -46,10 +46,9 @@ impl BlockAccountUpdate { self.final_state_commitment } - /// Returns the description of the updates for on-chain accounts. + /// Returns the account update details for this account update. /// - /// These descriptions can be used to build the new account state from the previous account - /// state. + /// These details can be used to build the new account state from the previous account state. pub fn details(&self) -> &AccountUpdateDetails { &self.details } diff --git a/crates/miden-objects/src/block/mod.rs b/crates/miden-objects/src/block/mod.rs index f31329202d..083551363f 100644 --- a/crates/miden-objects/src/block/mod.rs +++ b/crates/miden-objects/src/block/mod.rs @@ -16,8 +16,7 @@ pub use nullifier_witness::NullifierWitness; mod partial_account_tree; pub use partial_account_tree::PartialAccountTree; -pub(super) mod account_tree; -pub use account_tree::AccountTree; +pub mod account_tree; mod nullifier_tree; pub use nullifier_tree::NullifierTree; diff --git a/crates/miden-objects/src/block/nullifier_tree.rs b/crates/miden-objects/src/block/nullifier_tree.rs index d678f8f288..ce99372208 100644 --- a/crates/miden-objects/src/block/nullifier_tree.rs +++ b/crates/miden-objects/src/block/nullifier_tree.rs @@ -123,10 +123,12 @@ impl NullifierTree { } } - let mutation_set = - self.smt.compute_mutations(nullifiers.into_iter().map(|(nullifier, block_num)| { + 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)) } @@ -145,8 +147,10 @@ impl NullifierTree { 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)); + 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)) diff --git a/crates/miden-objects/src/block/partial_account_tree.rs b/crates/miden-objects/src/block/partial_account_tree.rs index 44e09c43b5..34cfd20c01 100644 --- a/crates/miden-objects/src/block/partial_account_tree.rs +++ b/crates/miden-objects/src/block/partial_account_tree.rs @@ -2,14 +2,15 @@ use miden_crypto::merkle::SmtLeaf; use crate::Word; use crate::account::AccountId; -use crate::block::{AccountTree, AccountWitness}; +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. /// -/// This is the partial version of [`AccountTree`]. -#[derive(Debug, Clone, PartialEq, Eq)] +/// This is the partial version of [`AccountTree`](crate::block::account_tree::AccountTree). +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PartialAccountTree { smt: PartialSmt, } @@ -18,9 +19,10 @@ impl PartialAccountTree { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new, empty partial account tree. - pub fn new() -> Self { - PartialAccountTree { smt: PartialSmt::new() } + /// Creates a new partial account tree with the provided root that does not track any account + /// IDs. + pub fn new(root: Word) -> Self { + PartialAccountTree { smt: PartialSmt::new(root) } } /// Returns a new [`PartialAccountTree`] instantiated with the provided entries. @@ -33,8 +35,21 @@ impl PartialAccountTree { pub fn with_witnesses( witnesses: impl IntoIterator, ) -> Result { - let mut tree = Self::new(); + let mut witnesses = witnesses.into_iter(); + let Some(first_witness) = witnesses.next() else { + return Ok(Self::default()); + }; + + // Construct a partial account tree with the root of the first witness. + // SAFETY: This is guaranteed to _not_ result in a tree with more than one entry because + // the account witness type guarantees that it tracks zero or one entries. + let partial_smt = PartialSmt::from_proofs([first_witness.into_proof()]) + .map_err(AccountTreeError::TreeRootConflict)?; + let mut tree = PartialAccountTree { smt: partial_smt }; + + // Add all remaining witnesses to the tree, which validates the invariants of the account + // tree. for witness in witnesses { tree.track_account(witness)?; } @@ -55,7 +70,7 @@ impl PartialAccountTree { /// Returns an error if: /// - the account ID is not tracked by this account tree. pub fn open(&self, account_id: AccountId) -> Result { - let key = AccountTree::id_to_smt_key(account_id); + let key = account_id_to_smt_key(account_id); self.smt .open(&key) @@ -70,7 +85,7 @@ impl PartialAccountTree { /// Returns an error if: /// - the account ID is not tracked by this account tree. pub fn get(&self, account_id: AccountId) -> Result { - let key = AccountTree::id_to_smt_key(account_id); + let key = account_id_to_smt_key(account_id); self.smt .get_value(&key) .map_err(|source| AccountTreeError::UntrackedAccountId { id: account_id, source }) @@ -96,22 +111,23 @@ impl PartialAccountTree { /// witness. pub fn track_account(&mut self, witness: AccountWitness) -> Result<(), AccountTreeError> { let id_prefix = witness.id().prefix(); - let id_key = AccountTree::id_to_smt_key(witness.id()); - let (path, leaf) = witness.into_proof().into_parts(); + let id_key = account_id_to_smt_key(witness.id()); // If a leaf with the same prefix is already tracked by this partial tree, consider it an // error. // // We return an error even for empty leaves, because tracking the same ID prefix twice - // indicates that different IDs are attempted to be tracked. It would technically - // not violate the invariant of the tree that it only tracks zero or one entries per leaf, - // but since tracking the same ID twice should practically never happen, we return an error, - // out of an abundance of caution. + // indicates that different IDs are attempted to be tracked. It would technically not + // violate the invariant of the tree that it only tracks zero or one entries per leaf, but + // since tracking the same ID twice should practically never happen, we return an error, out + // of an abundance of caution. if self.smt.get_leaf(&id_key).is_ok() { return Err(AccountTreeError::DuplicateIdPrefix { duplicate_prefix: id_prefix }); } - self.smt.add_path(leaf, path).map_err(AccountTreeError::TreeRootConflict)?; + self.smt + .add_proof(witness.into_proof()) + .map_err(AccountTreeError::TreeRootConflict)?; Ok(()) } @@ -151,7 +167,7 @@ impl PartialAccountTree { account_id: AccountId, state_commitment: Word, ) -> Result { - let key = AccountTree::id_to_smt_key(account_id); + let key = account_id_to_smt_key(account_id); // If there exists a tracked leaf whose key is _not_ the one we're about to overwrite, then // we would insert the new commitment next to an existing account ID with the same prefix, @@ -173,31 +189,25 @@ impl PartialAccountTree { } } -impl Default for PartialAccountTree { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use assert_matches::assert_matches; use miden_crypto::merkle::Smt; use super::*; + use crate::block::account_tree::AccountTree; use crate::block::account_tree::tests::setup_duplicate_prefix_ids; #[test] - fn insert_fails_on_duplicate_prefix() { - let mut full_tree = AccountTree::new(); - let mut partial_tree = PartialAccountTree::new(); + fn insert_fails_on_duplicate_prefix() -> anyhow::Result<()> { + let mut full_tree = AccountTree::::default(); let [(id0, commitment0), (id1, commitment1)] = setup_duplicate_prefix_ids(); full_tree.insert(id0, commitment0).unwrap(); let witness = full_tree.open(id0); - partial_tree.track_account(witness).unwrap(); + let mut partial_tree = PartialAccountTree::with_witnesses([witness])?; partial_tree.insert(id0, commitment0).unwrap(); assert_eq!(partial_tree.get(id0).unwrap(), commitment0); @@ -213,17 +223,20 @@ mod tests { assert_matches!(err, AccountTreeError::DuplicateIdPrefix { duplicate_prefix } if duplicate_prefix == id0.prefix()); + + Ok(()) } #[test] fn insert_succeeds_on_multiple_updates() { - let mut full_tree = AccountTree::new(); - let mut partial_tree = PartialAccountTree::new(); + let mut full_tree = AccountTree::::default(); let [(id0, commitment0), (_, commitment1)] = setup_duplicate_prefix_ids(); full_tree.insert(id0, commitment0).unwrap(); let witness = full_tree.open(id0); + let mut partial_tree = PartialAccountTree::new(full_tree.root()); + partial_tree.track_account(witness.clone()).unwrap(); assert_eq!( partial_tree.open(id0).unwrap(), @@ -243,7 +256,7 @@ mod tests { #[test] fn upsert_state_commitments_fails_on_untracked_key() { - let mut partial_tree = PartialAccountTree::new(); + let mut partial_tree = PartialAccountTree::default(); let [update, _] = setup_duplicate_prefix_ids(); let err = partial_tree.upsert_state_commitments([update]).unwrap_err(); @@ -258,25 +271,24 @@ mod tests { // account IDs with the same prefix. let full_tree = Smt::with_entries( setup_duplicate_prefix_ids() - .map(|(id, commitment)| (AccountTree::id_to_smt_key(id), commitment)), + .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), ) .unwrap(); let [(id0, _), (id1, _)] = setup_duplicate_prefix_ids(); - let key0 = AccountTree::id_to_smt_key(id0); - let key1 = AccountTree::id_to_smt_key(id1); + let key0 = account_id_to_smt_key(id0); + let key1 = account_id_to_smt_key(id1); let proof0 = full_tree.open(&key0); let proof1 = full_tree.open(&key1); assert_eq!(proof0.leaf(), proof1.leaf()); let witness0 = - AccountWitness::new_unchecked(id0, proof0.get(&key0).unwrap(), proof0.into_parts().0); + AccountWitness::new(id0, proof0.get(&key0).unwrap(), proof0.into_parts().0).unwrap(); let witness1 = - AccountWitness::new_unchecked(id1, proof1.get(&key1).unwrap(), proof1.into_parts().0); + AccountWitness::new(id1, proof1.get(&key1).unwrap(), proof1.into_parts().0).unwrap(); - let mut partial_tree = PartialAccountTree::new(); - partial_tree.track_account(witness0).unwrap(); + let mut partial_tree = PartialAccountTree::with_witnesses([witness0]).unwrap(); let err = partial_tree.track_account(witness1).unwrap_err(); assert_matches!(err, AccountTreeError::DuplicateIdPrefix { duplicate_prefix, .. } diff --git a/crates/miden-objects/src/block/partial_nullifier_tree.rs b/crates/miden-objects/src/block/partial_nullifier_tree.rs index 37f1717299..d32d425976 100644 --- a/crates/miden-objects/src/block/partial_nullifier_tree.rs +++ b/crates/miden-objects/src/block/partial_nullifier_tree.rs @@ -12,13 +12,14 @@ use crate::note::Nullifier; /// 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)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PartialNullifierTree(PartialSmt); impl PartialNullifierTree { - /// Creates a new, empty partial nullifier tree. - pub fn new() -> Self { - PartialNullifierTree(PartialSmt::new()) + /// Creates a new partial nullifier tree with the provided root that does not track any + /// nullifiers. + pub fn new(root: Word) -> Self { + PartialNullifierTree(PartialSmt::new(root)) } /// Returns a new [`PartialNullifierTree`] instantiated with the provided entries. @@ -30,13 +31,9 @@ impl PartialNullifierTree { pub fn with_witnesses( witnesses: impl IntoIterator, ) -> Result { - let mut tree = Self::new(); - - for witness in witnesses { - tree.track_nullifier(witness)?; - } - - Ok(tree) + PartialSmt::from_proofs(witnesses.into_iter().map(NullifierWitness::into_proof)) + .map_err(NullifierTreeError::TreeRootConflict) + .map(Self) } /// Adds the given nullifier witness to the partial tree and tracks it. Once a nullifier has @@ -100,12 +97,6 @@ impl PartialNullifierTree { } } -impl Default for PartialNullifierTree { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -132,15 +123,15 @@ mod tests { let mut full = Smt::with_entries(kv_pairs).unwrap(); let stale_proof0 = full.open(&key0); // Insert a non-empty value so the nullifier tree's root changes. - full.insert(key1, value1); - full.insert(key2, value2); + full.insert(key1, value1).unwrap(); + full.insert(key2, value2).unwrap(); let proof2 = full.open(&key2); assert_ne!(stale_proof0.compute_root(), proof2.compute_root()); - let mut partial = PartialNullifierTree::new(); + let mut partial = + PartialNullifierTree::with_witnesses([NullifierWitness::new(stale_proof0)]).unwrap(); - partial.track_nullifier(NullifierWitness::new(stale_proof0)).unwrap(); let error = partial.track_nullifier(NullifierWitness::new(proof2)).unwrap_err(); assert_matches!(error, NullifierTreeError::TreeRootConflict(_)); @@ -157,8 +148,7 @@ mod tests { let witness = tree.open(&nullifier1); - let mut partial_tree = PartialNullifierTree::new(); - partial_tree.track_nullifier(witness).unwrap(); + 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(); @@ -179,7 +169,7 @@ mod tests { let mut tree = NullifierTree::with_entries([(nullifier1, block1), (nullifier2, block2)]).unwrap(); - let mut partial_tree = PartialNullifierTree::new(); + let mut partial_tree = PartialNullifierTree::new(tree.root()); for nullifier in [nullifier1, nullifier2, nullifier3] { let witness = tree.open(&nullifier); diff --git a/crates/miden-objects/src/errors.rs b/crates/miden-objects/src/errors.rs index 895d725546..3a806326a7 100644 --- a/crates/miden-objects/src/errors.rs +++ b/crates/miden-objects/src/errors.rs @@ -22,11 +22,13 @@ use crate::account::{ AccountIdPrefix, AccountStorage, AccountType, + SlotName, StorageValueName, StorageValueNameError, TemplateTypeError, }; use crate::address::AddressType; +use crate::asset::AssetVaultKey; use crate::batch::BatchId; use crate::block::BlockNumber; use crate::note::{NoteAssets, NoteExecutionHint, NoteTag, NoteType, Nullifier}; @@ -118,7 +120,7 @@ pub enum AccountError { FinalAccountHeaderIdParsingFailed(#[source] AccountIdError), #[error("account header data has length {actual} but it must be of length {expected}")] HeaderDataIncorrectLength { actual: usize, expected: usize }, - #[error("current account nonce {current} plus increment {increment} overflows a felt to {new}")] + #[error("active account nonce {current} plus increment {increment} overflows a felt to {new}")] NonceOverflow { current: Felt, increment: Felt, @@ -128,6 +130,18 @@ pub enum AccountError { "digest of the seed has {actual} trailing zeroes but must have at least {expected} trailing zeroes" )] SeedDigestTooFewTrailingZeros { expected: u32, actual: u32 }, + #[error("account ID {actual} computed from seed does not match ID {expected} on account")] + AccountIdSeedMismatch { actual: AccountId, expected: AccountId }, + #[error("account ID seed was provided for an existing account")] + ExistingAccountWithSeed, + #[error("account ID seed was not provided for a new account")] + NewAccountMissingSeed, + #[error( + "an account with a seed cannot be converted into a delta since it represents an unregistered account" + )] + DeltaFromAccountWithSeed, + #[error("seed converts to an invalid account ID")] + 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")] @@ -153,6 +167,14 @@ pub enum AccountError { account_type: AccountType, component_index: usize, }, + #[error( + "failed to apply full state delta to existing account; full state deltas can be converted to accounts directly" + )] + ApplyFullStateDeltaToAccount, + #[error("only account deltas representing a full account can be converted to a full account")] + PartialStateDeltaToAccount, + #[error("maximum number of storage map leaves exceeded")] + MaxNumStorageMapLeavesExceeded(#[source] MerkleError), /// This variant can be used by methods that are not inherent to the account but want to return /// this error type. #[error("{error_msg}")] @@ -205,6 +227,26 @@ pub enum AccountIdError { AccountIdSuffixMostSignificantBitMustBeZero, #[error("least significant byte of account ID suffix must be zero")] AccountIdSuffixLeastSignificantByteMustBeZero, + #[error("failed to decode bech32 string into account ID")] + Bech32DecodeError(#[source] Bech32Error), +} + +// SLOT NAME ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum SlotNameError { + #[error("slot names must only contain characters a..z, A..Z, 0..9 or underscore")] + InvalidCharacter, + #[error("slot names must be separated by double colons")] + UnexpectedColon, + #[error("slot name components must not start with an underscore")] + UnexpectedUnderscore, + #[error( + "slot names must contain at least {} components separated by double colons", + SlotName::MIN_NUM_COMPONENTS + )] + TooShort, } // ACCOUNT TREE ERROR @@ -226,6 +268,8 @@ pub enum AccountTreeError { TreeRootConflict(#[source] MerkleError), #[error("failed to apply mutations to account tree")] ApplyMutations(#[source] MerkleError), + #[error("failed to compute account tree mutations")] + ComputeMutations(#[source] MerkleError), #[error("smt leaf's index is not a valid account ID prefix")] InvalidAccountIdPrefix(#[source] AccountIdError), #[error("account witness merkle path depth {0} does not match AccountTree::DEPTH")] @@ -237,16 +281,51 @@ pub enum AccountTreeError { #[derive(Debug, Error)] pub enum AddressError { - #[error("tag length {0} should be {expected} bits for network accounts", expected = crate::note::NoteTag::DEFAULT_NETWORK_TAG_LENGTH)] + #[error("tag length {0} should be {expected} bits for network accounts", + expected = NoteTag::DEFAULT_NETWORK_TAG_LENGTH + )] CustomTagLengthNotAllowedForNetworkAccounts(u8), - #[error("tag length {0} is too large, must be less than or equal to {max}", max = crate::note::NoteTag::MAX_LOCAL_TAG_LENGTH)] + #[error("tag length {0} is too large, must be less than or equal to {max}", + max = NoteTag::MAX_LOCAL_TAG_LENGTH + )] TagLengthTooLarge(u8), #[error("unknown address interface `{0}`")] UnknownAddressInterface(u16), #[error("failed to decode account ID")] AccountIdDecodeError(#[source] AccountIdError), + #[error("address separator must not be included without routing parameters")] + TrailingSeparator, #[error("failed to decode bech32 string into an address")] Bech32DecodeError(#[source] Bech32Error), + #[error("{error_msg}")] + DecodeError { + error_msg: Box, + // thiserror will return this when calling Error::source on NoteError. + source: Option>, + }, + #[error("found unknown routing parameter key {0}")] + UnknownRoutingParameterKey(u8), +} + +impl AddressError { + /// Creates an [`AddressError::DecodeError`] variant from an error message. + pub fn decode_error(message: impl Into) -> Self { + let message: String = message.into(); + Self::DecodeError { error_msg: message.into(), source: None } + } + + /// Creates an [`AddressError::DecodeError`] variant from an error message and + /// a source error. + pub fn decode_error_with_source( + message: impl Into, + source: impl Error + Send + Sync + 'static, + ) -> Self { + let message: String = message.into(); + Self::DecodeError { + error_msg: message.into(), + source: Some(Box::new(source)), + } + } } // BECH32 ERROR @@ -318,6 +397,8 @@ pub enum AccountDeltaError { }, #[error("account ID {0} in fungible asset delta is not of type fungible faucet")] NotAFungibleFaucetId(AccountId), + #[error("cannot merge two full state deltas")] + MergingFullStateDeltas, } // STORAGE MAP ERROR @@ -327,6 +408,8 @@ pub enum AccountDeltaError { pub enum StorageMapError { #[error("map entries contain key {key} twice with values {value0} and {value1}")] DuplicateKey { key: Word, value0: Word, value1: Word }, + #[error("map key {raw_key} is not present in provided SMT proof")] + MissingKey { raw_key: Word }, } // BATCH ACCOUNT UPDATE ERROR @@ -373,6 +456,8 @@ pub enum AssetError { }, #[error("faucet account ID in asset is invalid")] InvalidFaucetAccountId(#[source] Box), + #[error("faucet account ID in asset has a non-faucet prefix: {}", .0)] + InvalidFaucetAccountIdPrefix(AccountIdPrefix), #[error( "faucet id {0} of type {id_type} must be of type {expected_ty} for fungible assets", id_type = .0.account_type(), @@ -385,6 +470,8 @@ pub enum AssetError { expected_ty = AccountType::NonFungibleFaucet )] NonFungibleFaucetIdTypeMismatch(AccountIdPrefix), + #[error("asset vault key {actual} does not match expected asset vault key {expected}")] + AssetVaultKeyMismatch { actual: Word, expected: Word }, } // TOKEN SYMBOL ERROR @@ -421,6 +508,8 @@ pub enum AssetVaultError { NonFungibleAssetNotFound(NonFungibleAsset), #[error("subtracting fungible asset amounts would underflow")] SubtractFungibleAssetBalanceError(#[source] AssetError), + #[error("maximum number of asset vault leaves exceeded")] + MaxLeafEntriesExceeded(#[source] MerkleError), } // PARTIAL ASSET VAULT ERROR @@ -431,9 +520,11 @@ pub enum PartialAssetVaultError { #[error("provided SMT entry {entry} is not a valid asset")] InvalidAssetInSmt { entry: Word, source: AssetError }, #[error("expected asset vault key to be {expected} but it was {actual}")] - VaultKeyMismatch { expected: Word, actual: Word }, + AssetVaultKeyMismatch { expected: AssetVaultKey, actual: Word }, #[error("failed to add asset proof")] FailedToAddProof(#[source] MerkleError), + #[error("asset is not tracked in the partial vault")] + UntrackedAsset(#[source] MerkleError), } // NOTE ERROR @@ -574,16 +665,8 @@ pub enum TransactionScriptError { #[derive(Debug, Error)] pub enum TransactionInputError { - #[error("account seed must be provided for new accounts")] - AccountSeedNotProvidedForNewAccount, - #[error("account seed must not be provided for existing accounts")] - AccountSeedProvidedForExistingAccount, #[error("transaction input note with nullifier {0} is a duplicate")] DuplicateInputNote(Nullifier), - #[error( - "ID {expected} of the new account does not match the ID {actual} computed from the provided seed" - )] - InconsistentAccountSeed { expected: AccountId, actual: AccountId }, #[error("partial blockchain has length {actual} which does not match block number {expected}")] InconsistentChainLength { expected: BlockNumber, @@ -597,8 +680,6 @@ pub enum TransactionInputError { InputNoteBlockNotInPartialBlockchain(NoteId), #[error("input note with id {0} was not created in block {1}")] InputNoteNotInBlock(NoteId, BlockNumber), - #[error("account ID computed from seed is invalid")] - InvalidAccountIdSeed(#[source] AccountIdError), #[error( "total number of input notes is {0} which exceeds the maximum of {MAX_INPUT_NOTES_PER_TX}" )] @@ -655,14 +736,14 @@ pub enum ProvenTransactionError { InputNotesError(TransactionInputError), #[error("private account {0} should not have account details")] PrivateAccountWithDetails(AccountId), - #[error("on-chain account {0} is missing its account details")] - OnChainAccountMissingDetails(AccountId), - #[error("new on-chain account {0} is missing its account details")] - NewOnChainAccountRequiresFullDetails(AccountId), + #[error("account {0} with public state is missing its account details")] + PublicStateAccountMissingDetails(AccountId), + #[error("new account {id} with public state must be accompanied by a full state delta")] + NewPublicStateAccountRequiresFullStateDelta { id: AccountId, source: AccountError }, #[error( - "existing on-chain account {0} should only provide delta updates instead of full details" + "existing account {0} with public state should only provide delta updates instead of full details" )] - ExistingOnChainAccountRequiresDeltaDetails(AccountId), + ExistingPublicStateAccountRequiresDeltaDetails(AccountId), #[error("failed to construct output notes for proven transaction")] OutputNotesError(TransactionOutputError), #[error( @@ -674,6 +755,8 @@ pub enum ProvenTransactionError { }, #[error("proven transaction neither changed the account state, nor consumed any notes")] EmptyTransaction, + #[error("failed to validate account delta in transaction account update")] + AccountDeltaCommitmentMismatch(#[source] Box), } // PROPOSED BATCH ERROR @@ -963,6 +1046,9 @@ pub enum NullifierTreeError { #[error("attempt to mark nullifier {0} as spent but it is already spent")] NullifierAlreadySpent(Nullifier), + #[error("maximum number of nullifier tree leaves exceeded")] + MaxLeafEntriesExceeded(#[source] MerkleError), + #[error("nullifier {nullifier} is not tracked by the partial nullifier tree")] UntrackedNullifier { nullifier: Nullifier, @@ -971,4 +1057,16 @@ 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")] + ComputeMutations(#[source] MerkleError), +} + +// AUTH SCHEME ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum AuthSchemeError { + #[error("auth scheme identifier `{0}` is not valid")] + InvalidAuthSchemeIdentifier(u8), } diff --git a/crates/miden-objects/src/lib.rs b/crates/miden-objects/src/lib.rs index 3602518434..34f9219efb 100644 --- a/crates/miden-objects/src/lib.rs +++ b/crates/miden-objects/src/lib.rs @@ -32,6 +32,7 @@ pub use errors::{ AddressError, AssetError, AssetVaultError, + AuthSchemeError, BatchAccountUpdateError, FeeError, NetworkIdError, @@ -42,6 +43,8 @@ pub use errors::{ ProposedBlockError, ProvenBatchError, ProvenTransactionError, + SlotNameError, + StorageMapError, TokenSymbolError, TransactionInputError, TransactionOutputError, @@ -77,13 +80,12 @@ pub mod assembly { } pub mod crypto { - pub use miden_crypto::{SequentialCommit, dsa, hash, merkle, rand, utils}; + pub use miden_crypto::{SequentialCommit, dsa, hash, ies, merkle, rand, utils}; } pub mod utils { pub use miden_core::utils::*; - pub use miden_crypto::utils::{HexParseError, bytes_to_hex_string, collections, hex_to_bytes}; - pub use miden_crypto::word::parse_hex_string_as_word; + pub use miden_crypto::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; pub use miden_utils_sync as sync; pub mod serde { @@ -98,8 +100,17 @@ 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_mast_package::{ + MastArtifact, + Package, + PackageExport, + PackageManifest, + Section, + SectionId, + }; pub use miden_processor::{AdviceInputs, FutureMaybeSend, RowIndex, StackInputs, StackOutputs}; pub use miden_verifier::ExecutionProof; } diff --git a/crates/miden-objects/src/note/assets.rs b/crates/miden-objects/src/note/assets.rs index c94c329ac2..260274eeaa 100644 --- a/crates/miden-objects/src/note/assets.rs +++ b/crates/miden-objects/src/note/assets.rs @@ -93,7 +93,7 @@ impl NoteAssets { /// because hashing the returned elements results in the note asset commitment. pub fn to_padded_assets(&self) -> Vec { // if we have an odd number of assets with pad with a single word. - let padded_len = if self.assets.len() % 2 == 0 { + let padded_len = if self.assets.len().is_multiple_of(2) { self.assets.len() * WORD_SIZE } else { (self.assets.len() + 1) * WORD_SIZE @@ -192,7 +192,7 @@ fn compute_asset_commitment(assets: &[Asset]) -> Word { // If we have an odd number of assets we pad the vector with 4 zero elements. This is to // ensure the number of elements is a multiple of 8 - the size of the hasher rate. - let word_capacity = if assets.len() % 2 == 0 { + let word_capacity = if assets.len().is_multiple_of(2) { assets.len() } else { assets.len() + 1 diff --git a/crates/miden-objects/src/note/file.rs b/crates/miden-objects/src/note/file.rs index 84b7353c21..48a77902aa 100644 --- a/crates/miden-objects/src/note/file.rs +++ b/crates/miden-objects/src/note/file.rs @@ -20,6 +20,7 @@ const MAGIC: &str = "note"; // ================================================================================================ /// A serialized representation of a note. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum NoteFile { /// The note's details aren't known. NoteId(NoteId), diff --git a/crates/miden-objects/src/note/inputs.rs b/crates/miden-objects/src/note/inputs.rs index 9a9e09203d..5d0366eeaf 100644 --- a/crates/miden-objects/src/note/inputs.rs +++ b/crates/miden-objects/src/note/inputs.rs @@ -68,14 +68,13 @@ impl NoteInputs { &self.values } - /// Returns the note's input formatted to be used with the advice map. + /// Returns the note's input as a vector of field elements. /// /// The format is `INPUTS || PADDING`, where: /// - /// Where: /// - INPUTS is the variable inputs for the note /// - PADDING is the optional padding to align the data with a 2WORD boundary - pub fn format_for_advice(&self) -> Vec { + pub fn to_elements(&self) -> Vec { pad_inputs(&self.values) } } diff --git a/crates/miden-objects/src/note/note_tag.rs b/crates/miden-objects/src/note/note_tag.rs index 4a7a5acf4f..4da8f012fb 100644 --- a/crates/miden-objects/src/note/note_tag.rs +++ b/crates/miden-objects/src/note/note_tag.rs @@ -32,7 +32,7 @@ const LOCAL_ANY: u32 = 0xc000_0000; /// /// 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 on-chain, and the account is +/// 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 diff --git a/crates/miden-objects/src/note/recipient.rs b/crates/miden-objects/src/note/recipient.rs index 50c90ca6fb..78cc247bf3 100644 --- a/crates/miden-objects/src/note/recipient.rs +++ b/crates/miden-objects/src/note/recipient.rs @@ -1,8 +1,5 @@ -use alloc::vec::Vec; use core::fmt::Debug; -use miden_crypto::Felt; - use super::{ ByteReader, ByteWriter, @@ -63,24 +60,6 @@ impl NoteRecipient { pub fn digest(&self) -> Word { self.digest } - - /// Returns the recipient formatted to be used with the advice map. - /// - /// The format is `inputs_length || INPUTS_COMMITMENT || SCRIPT_ROOT || SERIAL_NUMBER` - /// - /// Where: - /// - inputs_length is the length of the note inputs - /// - INPUTS_COMMITMENT is the commitment of the note inputs - /// - SCRIPT_ROOT is the commitment of the note script (i.e., the script's MAST root) - /// - SERIAL_NUMBER is the recipient's serial number - pub fn format_for_advice(&self) -> Vec { - let mut result = Vec::with_capacity(13); - result.push(self.inputs.num_values().into()); - result.extend(self.inputs.commitment()); - result.extend(self.script.root()); - result.extend(self.serial_num); - result - } } fn compute_recipient_digest(serial_num: Word, script: &NoteScript, inputs: &NoteInputs) -> Word { diff --git a/crates/miden-objects/src/note/script.rs b/crates/miden-objects/src/note/script.rs index 2ea6d1fbf3..457d72cfb5 100644 --- a/crates/miden-objects/src/note/script.rs +++ b/crates/miden-objects/src/note/script.rs @@ -2,6 +2,8 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::fmt::Display; +use miden_processor::MastNodeExt; + use super::Felt; use crate::assembly::mast::{MastForest, MastNodeId}; use crate::utils::serde::{ @@ -84,14 +86,14 @@ impl From<&NoteScript> for Vec { let len = bytes.len(); // Pad the data so that it can be encoded with u32 - let missing = if len % 4 > 0 { 4 - (len % 4) } else { 0 }; + let missing = if !len.is_multiple_of(4) { 4 - (len % 4) } else { 0 }; bytes.resize(bytes.len() + missing, 0); let final_size = 2 + bytes.len(); let mut result = Vec::with_capacity(final_size); // Push the length, this is used to remove the padding later - result.push(Felt::from(script.entrypoint.as_u32())); + result.push(Felt::from(u32::from(script.entrypoint))); result.push(Felt::new(len as u64)); // A Felt can not represent all u64 values, so the data is encoded using u32. @@ -162,7 +164,7 @@ impl TryFrom> for NoteScript { impl Serializable for NoteScript { fn write_into(&self, target: &mut W) { self.mast.write_into(target); - target.write_u32(self.entrypoint.as_u32()); + target.write_u32(u32::from(self.entrypoint)); } } diff --git a/crates/miden-objects/src/testing/account.rs b/crates/miden-objects/src/testing/account.rs index 7098880f57..036ce0513f 100644 --- a/crates/miden-objects/src/testing/account.rs +++ b/crates/miden-objects/src/testing/account.rs @@ -1,5 +1,6 @@ use super::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; -use crate::account::AccountId; +use crate::Felt; +use crate::account::{Account, AccountCode, AccountId, AccountStorage}; use crate::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; use crate::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, @@ -7,6 +8,28 @@ use crate::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, }; +impl Account { + /// Returns an [`Account`] instantiated with the provided components. + /// + /// This is a thin wrapper around [`Account::new`] that assumes the provided components are for + /// an existing account. See that method's docs for details on when this function panics. + /// + /// # Panics + /// + /// Panics if: + /// - the provided components are not for an existing account. + pub fn new_existing( + id: AccountId, + vault: AssetVault, + storage: AccountStorage, + code: AccountCode, + nonce: Felt, + ) -> Self { + Self::new(id, vault, storage, code, nonce, None) + .expect("account seed is invalid for provided account") + } +} + impl AssetVault { /// Creates an [AssetVault] with 4 default assets. /// diff --git a/crates/miden-objects/src/testing/account_code.rs b/crates/miden-objects/src/testing/account_code.rs index 119d378c86..e8ddbbdbb9 100644 --- a/crates/miden-objects/src/testing/account_code.rs +++ b/crates/miden-objects/src/testing/account_code.rs @@ -8,11 +8,11 @@ use crate::testing::noop_auth_component::NoopAuthComponent; pub const CODE: &str = " export.foo - push.1 push.2 mul + push.1.2 mul end export.bar - push.1 push.2 add + push.1.2 add end "; diff --git a/crates/miden-objects/src/testing/add_component.rs b/crates/miden-objects/src/testing/add_component.rs new file mode 100644 index 0000000000..eb4b79d79f --- /dev/null +++ b/crates/miden-objects/src/testing/add_component.rs @@ -0,0 +1,31 @@ +use crate::account::AccountComponent; +use crate::assembly::{Assembler, Library}; +use crate::utils::sync::LazyLock; + +// ADD COMPONENT +// ================================================================================================ + +const ADD_CODE: &str = " + export.add5 + add.5 + end +"; + +static ADD_LIBRARY: LazyLock = LazyLock::new(|| { + Assembler::default() + .assemble_library([ADD_CODE]) + .expect("add code should be valid") +}); + +/// Creates a mock authentication [`AccountComponent`] for testing purposes. +/// +/// The component defines an `add5` procedure that adds 5 to its input. +pub struct AddComponent; + +impl From for AccountComponent { + fn from(_: AddComponent) -> Self { + AccountComponent::new(ADD_LIBRARY.clone(), vec![]) + .expect("component should be valid") + .with_supports_all_types() + } +} diff --git a/crates/miden-objects/src/testing/block.rs b/crates/miden-objects/src/testing/block.rs index 92e1fd78b1..52948bece2 100644 --- a/crates/miden-objects/src/testing/block.rs +++ b/crates/miden-objects/src/testing/block.rs @@ -1,9 +1,11 @@ +use miden_crypto::merkle::Smt; #[cfg(not(target_family = "wasm"))] use winter_rand_utils::rand_value; use crate::Word; use crate::account::Account; -use crate::block::{AccountTree, BlockHeader, BlockNumber, FeeParameters}; +use crate::block::account_tree::{AccountTree, account_id_to_smt_key}; +use crate::block::{BlockHeader, BlockNumber, FeeParameters}; use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; impl BlockHeader { @@ -20,9 +22,13 @@ impl BlockHeader { accounts: &[Account], tx_kernel_commitment: Word, ) -> Self { - let acct_db = - AccountTree::with_entries(accounts.iter().map(|acct| (acct.id(), acct.commitment()))) - .expect("failed to create account db"); + let smt = Smt::with_entries( + accounts + .iter() + .map(|acct| (account_id_to_smt_key(acct.id()), acct.commitment())), + ) + .expect("failed to create account db"); + let acct_db = AccountTree::new(smt).expect("failed to create account tree"); let account_root = acct_db.root(); let fee_parameters = FeeParameters::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500) diff --git a/crates/miden-objects/src/testing/block_note_tree.rs b/crates/miden-objects/src/testing/block_note_tree.rs index 0b0b343acd..9779c06c57 100644 --- a/crates/miden-objects/src/testing/block_note_tree.rs +++ b/crates/miden-objects/src/testing/block_note_tree.rs @@ -16,8 +16,9 @@ impl BlockNoteTree { pub fn from_note_batches(notes: &[OutputNoteBatch]) -> Result { let iter = notes.iter().enumerate().flat_map(|(batch_idx, batch_notes)| { batch_notes.iter().map(move |(note_idx_in_batch, note)| { - let block_note_index = - BlockNoteIndex::new(batch_idx, *note_idx_in_batch).expect("TODO"); + // 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()) }) }); diff --git a/crates/miden-objects/src/testing/mod.rs b/crates/miden-objects/src/testing/mod.rs index 29a8192f92..7bf6c65286 100644 --- a/crates/miden-objects/src/testing/mod.rs +++ b/crates/miden-objects/src/testing/mod.rs @@ -1,6 +1,7 @@ pub mod account; pub mod account_code; pub mod account_id; +pub mod add_component; pub mod asset; pub mod block; pub mod block_note_tree; diff --git a/crates/miden-objects/src/testing/noop_auth_component.rs b/crates/miden-objects/src/testing/noop_auth_component.rs index 2139f87b66..8ebc9fb800 100644 --- a/crates/miden-objects/src/testing/noop_auth_component.rs +++ b/crates/miden-objects/src/testing/noop_auth_component.rs @@ -6,7 +6,7 @@ use crate::utils::sync::LazyLock; // ================================================================================================ const NOOP_AUTH_CODE: &str = " - export.auth__noop + export.auth_noop push.0 drop end "; @@ -19,7 +19,7 @@ static NOOP_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { /// Creates a mock authentication [`AccountComponent`] for testing purposes. /// -/// The component defines an `auth__noop` procedure that does nothing (always succeeds). +/// The component defines an `auth_noop` procedure that does nothing (always succeeds). pub struct NoopAuthComponent; impl From for AccountComponent { diff --git a/crates/miden-objects/src/testing/storage.rs b/crates/miden-objects/src/testing/storage.rs index 20e821fd08..85f8f673f5 100644 --- a/crates/miden-objects/src/testing/storage.rs +++ b/crates/miden-objects/src/testing/storage.rs @@ -139,7 +139,7 @@ impl AccountStorage { pub fn prepare_assets(note_assets: &NoteAssets) -> Vec { let mut assets = Vec::new(); for &asset in note_assets.iter() { - let asset_word: Word = asset.into(); + let asset_word = Word::from(asset); assets.push(asset_word.to_string()); } assets diff --git a/crates/miden-objects/src/transaction/executed_tx.rs b/crates/miden-objects/src/transaction/executed_tx.rs index de3eaf44ec..65084eb1fc 100644 --- a/crates/miden-objects/src/transaction/executed_tx.rs +++ b/crates/miden-objects/src/transaction/executed_tx.rs @@ -1,7 +1,6 @@ use alloc::vec::Vec; use super::{ - Account, AccountDelta, AccountHeader, AccountId, @@ -13,12 +12,12 @@ use super::{ OutputNotes, TransactionArgs, TransactionId, - TransactionInputs, TransactionOutputs, - TransactionWitness, }; +use crate::account::PartialAccount; use crate::asset::FungibleAsset; use crate::block::BlockNumber; +use crate::transaction::TransactionInputs; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -46,8 +45,6 @@ pub struct ExecutedTransaction { tx_inputs: TransactionInputs, tx_outputs: TransactionOutputs, account_delta: AccountDelta, - tx_args: TransactionArgs, - advice_witness: AdviceInputs, tx_measurements: TransactionMeasurements, } @@ -63,8 +60,6 @@ impl ExecutedTransaction { tx_inputs: TransactionInputs, tx_outputs: TransactionOutputs, account_delta: AccountDelta, - tx_args: TransactionArgs, - advice_witness: AdviceInputs, tx_measurements: TransactionMeasurements, ) -> Self { // make sure account IDs are consistent across transaction inputs and outputs @@ -73,7 +68,7 @@ impl ExecutedTransaction { // we create the id from the content, so we cannot construct the // `id` value after construction `Self {..}` without moving let id = TransactionId::new( - tx_inputs.account().init_commitment(), + tx_inputs.account().initial_commitment(), tx_outputs.account.commitment(), tx_inputs.input_notes().commitment(), tx_outputs.output_notes.commitment(), @@ -84,8 +79,6 @@ impl ExecutedTransaction { tx_inputs, tx_outputs, account_delta, - tx_args, - advice_witness, tx_measurements, } } @@ -103,12 +96,12 @@ impl ExecutedTransaction { self.initial_account().id() } - /// Returns the description of the account before the transaction was executed. - pub fn initial_account(&self) -> &Account { + /// Returns the partial state of the account before the transaction was executed. + pub fn initial_account(&self) -> &PartialAccount { self.tx_inputs.account() } - /// Returns description of the account after the transaction was executed. + /// Returns the header of the account state after the transaction was executed. pub fn final_account(&self) -> &AccountHeader { &self.tx_outputs.account } @@ -135,7 +128,7 @@ impl ExecutedTransaction { /// Returns a reference to the transaction arguments. pub fn tx_args(&self) -> &TransactionArgs { - &self.tx_args + self.tx_inputs.tx_args() } /// Returns the block header for the block against which the transaction was executed. @@ -156,7 +149,7 @@ impl ExecutedTransaction { /// Returns all the data requested by the VM from the advice provider while executing the /// transaction program. pub fn advice_witness(&self) -> &AdviceInputs { - &self.advice_witness + self.tx_inputs.advice_inputs() } /// Returns a reference to the transaction measurements which are the cycle counts for @@ -171,20 +164,14 @@ impl ExecutedTransaction { /// Returns individual components of this transaction. pub fn into_parts( self, - ) -> (AccountDelta, TransactionOutputs, TransactionWitness, TransactionMeasurements) { - let tx_witness = TransactionWitness { - tx_inputs: self.tx_inputs, - tx_args: self.tx_args, - advice_witness: self.advice_witness, - }; - (self.account_delta, self.tx_outputs, tx_witness, self.tx_measurements) + ) -> (TransactionInputs, TransactionOutputs, AccountDelta, TransactionMeasurements) { + (self.tx_inputs, self.tx_outputs, self.account_delta, self.tx_measurements) } } -impl From for TransactionWitness { +impl From for TransactionInputs { fn from(tx: ExecutedTransaction) -> Self { - let (_, _, tx_witness, _) = tx.into_parts(); - tx_witness + tx.tx_inputs } } @@ -200,8 +187,6 @@ impl Serializable for ExecutedTransaction { self.tx_inputs.write_into(target); self.tx_outputs.write_into(target); self.account_delta.write_into(target); - self.tx_args.write_into(target); - self.advice_witness.write_into(target); self.tx_measurements.write_into(target); } } @@ -211,18 +196,9 @@ impl Deserializable for ExecutedTransaction { let tx_inputs = TransactionInputs::read_from(source)?; let tx_outputs = TransactionOutputs::read_from(source)?; let account_delta = AccountDelta::read_from(source)?; - let tx_args = TransactionArgs::read_from(source)?; - let advice_witness = AdviceInputs::read_from(source)?; let tx_measurements = TransactionMeasurements::read_from(source)?; - Ok(Self::new( - tx_inputs, - tx_outputs, - account_delta, - tx_args, - advice_witness, - tx_measurements, - )) + Ok(Self::new(tx_inputs, tx_outputs, account_delta, tx_measurements)) } } @@ -238,6 +214,7 @@ pub struct TransactionMeasurements { pub note_execution: Vec<(NoteId, usize)>, pub tx_script_processing: usize, pub epilogue: usize, + pub auth_procedure: usize, /// The number of cycles the epilogue took to execute after compute_fee determined the cycle /// count. /// @@ -267,6 +244,7 @@ impl Serializable for TransactionMeasurements { self.note_execution.write_into(target); self.tx_script_processing.write_into(target); self.epilogue.write_into(target); + self.auth_procedure.write_into(target); self.after_tx_cycles_obtained.write_into(target); } } @@ -278,6 +256,7 @@ impl Deserializable for TransactionMeasurements { let note_execution = Vec::<(NoteId, usize)>::read_from(source)?; let tx_script_processing = usize::read_from(source)?; let epilogue = usize::read_from(source)?; + let auth_procedure = usize::read_from(source)?; let after_tx_cycles_obtained = usize::read_from(source)?; Ok(Self { @@ -286,6 +265,7 @@ impl Deserializable for TransactionMeasurements { note_execution, tx_script_processing, epilogue, + auth_procedure, after_tx_cycles_obtained, }) } diff --git a/crates/miden-objects/src/transaction/inputs/account.rs b/crates/miden-objects/src/transaction/inputs/account.rs index 9389ff4d66..4d7dd2faf9 100644 --- a/crates/miden-objects/src/transaction/inputs/account.rs +++ b/crates/miden-objects/src/transaction/inputs/account.rs @@ -98,10 +98,10 @@ mod tests { use miden_core::Felt; use miden_core::utils::{Deserializable, Serializable}; - use miden_crypto::merkle::MerklePath; + use miden_crypto::merkle::SparseMerklePath; use miden_processor::SMT_DEPTH; - use crate::account::{Account, AccountCode, AccountId, AccountStorage}; + use crate::account::{Account, AccountCode, AccountId, AccountStorage, PartialAccount}; use crate::asset::AssetVault; use crate::block::AccountWitness; use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; @@ -113,7 +113,7 @@ mod tests { let code = AccountCode::mock(); let vault = AssetVault::new(&[]).unwrap(); let storage = AccountStorage::new(vec![]).unwrap(); - let account = Account::from_parts(id, vault, storage, code, Felt::new(10)); + let account = Account::new_existing(id, vault, storage, code, Felt::new(10)); let commitment = account.commitment(); @@ -121,10 +121,11 @@ mod tests { for _ in 0..(SMT_DEPTH as usize) { merkle_nodes.push(commitment); } - let merkle_path = MerklePath::new(merkle_nodes); + let merkle_path = SparseMerklePath::from_sized_iter(merkle_nodes) + .expect("The nodes given are of SMT_DEPTH count"); let fpi_inputs = AccountInputs::new( - account.into(), + PartialAccount::from(&account), AccountWitness::new(id, commitment, merkle_path).unwrap(), ); diff --git a/crates/miden-objects/src/transaction/inputs/mod.rs b/crates/miden-objects/src/transaction/inputs/mod.rs index b489b6e898..59ea16e12a 100644 --- a/crates/miden-objects/src/transaction/inputs/mod.rs +++ b/crates/miden-objects/src/transaction/inputs/mod.rs @@ -1,22 +1,20 @@ +use alloc::vec::Vec; use core::fmt::Debug; +use miden_core::utils::{Deserializable, Serializable}; + use super::PartialBlockchain; -use crate::account::{Account, AccountId}; -use crate::block::BlockHeader; +use crate::TransactionInputError; +use crate::account::{AccountCode, PartialAccount}; +use crate::block::{BlockHeader, BlockNumber}; use crate::note::{Note, NoteInclusionProof}; -use crate::utils::serde::{ - ByteReader, - ByteWriter, - Deserializable, - DeserializationError, - Serializable, -}; -use crate::{TransactionInputError, Word}; +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 @@ -25,85 +23,114 @@ pub use notes::{InputNote, InputNotes, ToInputNoteCommitments}; /// Contains the data required to execute a transaction. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TransactionInputs { - account: Account, - account_seed: Option, + account: PartialAccount, block_header: BlockHeader, - block_chain: PartialBlockchain, + 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. + + /// Returns new [`TransactionInputs`] instantiated with the specified parameters. /// /// # Errors + /// /// Returns an error if: - /// - For a new account, account seed is not provided or the provided seed is invalid. - /// - For an existing account, account seed was provided. + /// - The partial blockchain does not track the block headers required to prove inclusion of any + /// authenticated input note. pub fn new( - account: Account, - account_seed: Option, + account: PartialAccount, block_header: BlockHeader, - block_chain: PartialBlockchain, + blockchain: PartialBlockchain, input_notes: InputNotes, ) -> Result { - // validate the seed - validate_account_seed(&account, account_seed)?; - - // check the block_chain and block_header are consistent - let block_num = block_header.block_num(); - if block_chain.chain_length() != block_header.block_num() { + // 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: block_chain.chain_length(), + actual: blockchain.chain_length(), }); } - - if block_chain.peaks().hash_peaks() != block_header.chain_commitment() { + if blockchain.peaks().hash_peaks() != block_header.chain_commitment() { return Err(TransactionInputError::InconsistentChainCommitment { expected: block_header.chain_commitment(), - actual: block_chain.peaks().hash_peaks(), + actual: blockchain.peaks().hash_peaks(), }); } - - // check the authentication paths of the input notes. + // 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_num { + let block_header = if note_block_num == block_header.block_num() { &block_header } else { - block_chain.get_block(note_block_num).ok_or( + blockchain.get_block(note_block_num).ok_or( TransactionInputError::InputNoteBlockNotInPartialBlockchain(note.id()), )? }; - validate_is_in_block(note, proof, block_header)?; } } Ok(Self { account, - account_seed, block_header, - block_chain, + blockchain, input_notes, + tx_args: TransactionArgs::default(), + advice_inputs: AdviceInputs::default(), + foreign_account_code: Vec::new(), }) } - // PUBLIC ACCESSORS + /// 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.tx_args = 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.advice_inputs = advice_inputs; + self + } + + // MUTATORS // -------------------------------------------------------------------------------------------- - /// Returns account against which the transaction is to be executed. - pub fn account(&self) -> &Account { - &self.account + /// 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. + pub fn set_advice_inputs(&mut self, new_advice_inputs: AdviceInputs) { + self.advice_inputs = new_advice_inputs; } - /// For newly-created accounts, returns the account seed; for existing accounts, returns None. - pub fn account_seed(&self) -> Option { - self.account_seed + /// Updates the transaction arguments of the inputs. + #[cfg(feature = "testing")] + pub fn set_tx_args(&mut self, tx_args: TransactionArgs) { + self.tx_args = 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. @@ -114,7 +141,7 @@ impl TransactionInputs { /// Returns partial blockchain containing authentication paths for all notes consumed by the /// transaction. pub fn blockchain(&self) -> &PartialBlockchain { - &self.block_chain + &self.blockchain } /// Returns the notes to be consumed in the transaction. @@ -122,78 +149,87 @@ impl TransactionInputs { &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, - ) -> (Account, Option, BlockHeader, PartialBlockchain, InputNotes) { - ( - self.account, - self.account_seed, - self.block_header, - self.block_chain, - self.input_notes, - ) + ) -> ( + PartialAccount, + BlockHeader, + PartialBlockchain, + InputNotes, + TransactionArgs, + ) { + (self.account, self.block_header, self.blockchain, self.input_notes, self.tx_args) } } impl Serializable for TransactionInputs { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { self.account.write_into(target); - self.account_seed.write_into(target); self.block_header.write_into(target); - self.block_chain.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 = Account::read_from(source)?; - let account_seed = source.read()?; + fn read_from( + source: &mut R, + ) -> Result { + let account = PartialAccount::read_from(source)?; let block_header = BlockHeader::read_from(source)?; - let block_chain = PartialBlockchain::read_from(source)?; + let blockchain = PartialBlockchain::read_from(source)?; let input_notes = InputNotes::read_from(source)?; - Self::new(account, account_seed, block_header, block_chain, input_notes) - .map_err(|err| DeserializationError::InvalidValue(format!("{err}"))) + 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 that the provided seed is valid for this account. -fn validate_account_seed( - account: &Account, - account_seed: Option, -) -> Result<(), TransactionInputError> { - match (account.is_new(), account_seed) { - (true, Some(seed)) => { - let account_id = AccountId::new( - seed, - account.id().version(), - account.code().commitment(), - account.storage().commitment(), - ) - .map_err(TransactionInputError::InvalidAccountIdSeed)?; - - if account_id != account.id() { - return Err(TransactionInputError::InconsistentAccountSeed { - expected: account.id(), - actual: account_id, - }); - } - - Ok(()) - }, - (true, None) => Err(TransactionInputError::AccountSeedNotProvidedForNewAccount), - (false, Some(_)) => Err(TransactionInputError::AccountSeedProvidedForExistingAccount), - (false, None) => Ok(()), - } -} - /// Validates whether the provided note belongs to the note tree of the specified block. fn validate_is_in_block( note: &Note, diff --git a/crates/miden-objects/src/transaction/inputs/notes.rs b/crates/miden-objects/src/transaction/inputs/notes.rs index 9655abc3cc..35663c12ca 100644 --- a/crates/miden-objects/src/transaction/inputs/notes.rs +++ b/crates/miden-objects/src/transaction/inputs/notes.rs @@ -292,6 +292,12 @@ impl InputNote { } } +impl From> for InputNotes { + fn from(notes: Vec) -> Self { + Self::new_unchecked(notes.into_iter().map(InputNote::unauthenticated).collect::>()) + } +} + impl ToInputNoteCommitments for InputNote { fn nullifier(&self) -> Nullifier { self.note().nullifier() diff --git a/crates/miden-objects/src/transaction/mod.rs b/crates/miden-objects/src/transaction/mod.rs index a63f0e4587..b11954de1a 100644 --- a/crates/miden-objects/src/transaction/mod.rs +++ b/crates/miden-objects/src/transaction/mod.rs @@ -1,4 +1,4 @@ -use super::account::{Account, AccountDelta, AccountHeader, AccountId}; +use super::account::{AccountDelta, AccountHeader, AccountId}; use super::block::BlockHeader; use super::note::{NoteId, Nullifier}; use super::vm::AdviceInputs; @@ -14,7 +14,6 @@ mod transaction_id; mod tx_args; mod tx_header; mod tx_summary; -mod tx_witness; pub use executed_tx::{ExecutedTransaction, TransactionMeasurements}; pub use inputs::{AccountInputs, InputNote, InputNotes, ToInputNoteCommitments, TransactionInputs}; @@ -31,4 +30,3 @@ pub use transaction_id::TransactionId; pub use tx_args::{TransactionArgs, TransactionScript}; pub use tx_header::TransactionHeader; pub use tx_summary::TransactionSummary; -pub use tx_witness::TransactionWitness; diff --git a/crates/miden-objects/src/transaction/outputs.rs b/crates/miden-objects/src/transaction/outputs.rs index e030ae142e..f45e313a54 100644 --- a/crates/miden-objects/src/transaction/outputs.rs +++ b/crates/miden-objects/src/transaction/outputs.rs @@ -103,7 +103,7 @@ impl OutputNotes { } } - let commitment = build_output_notes_commitment(¬es); + let commitment = Self::compute_commitment(notes.iter().map(NoteHeader::from)); Ok(Self { notes, commitment }) } @@ -140,6 +140,27 @@ impl OutputNotes { pub fn iter(&self) -> impl Iterator { self.notes.iter() } + + // HELPERS + // -------------------------------------------------------------------------------------------- + + /// 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 { + if notes.len() == 0 { + return Word::empty(); + } + + 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()); + } + + Hasher::hash_elements(&elements) + } } // SERIALIZATION @@ -307,27 +328,6 @@ impl Deserializable for OutputNote { } } -// HELPER FUNCTIONS -// ================================================================================================ - -/// Build 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. -fn build_output_notes_commitment(notes: &[OutputNote]) -> Word { - if notes.is_empty() { - return Word::empty(); - } - - let mut elements: Vec = Vec::with_capacity(notes.len() * 8); - for note in notes.iter() { - elements.extend_from_slice(note.id().as_elements()); - elements.extend_from_slice(Word::from(note.metadata()).as_elements()); - } - - Hasher::hash_elements(&elements) -} - // TESTS // ================================================================================================ diff --git a/crates/miden-objects/src/transaction/proven_tx.rs b/crates/miden-objects/src/transaction/proven_tx.rs index 454bf6287d..3b002c80c9 100644 --- a/crates/miden-objects/src/transaction/proven_tx.rs +++ b/crates/miden-objects/src/transaction/proven_tx.rs @@ -1,7 +1,9 @@ +use alloc::boxed::Box; use alloc::string::ToString; use alloc::vec::Vec; use super::{InputNote, ToInputNoteCommitments}; +use crate::account::Account; use crate::account::delta::AccountUpdateDetails; use crate::asset::FungibleAsset; use crate::block::BlockNumber; @@ -22,7 +24,7 @@ use crate::utils::serde::{ Serializable, }; use crate::vm::ExecutionProof; -use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ProvenTransactionError, Word}; +use crate::{ACCOUNT_UPDATE_MAX_SIZE, ProvenTransactionError, Word}; // PROVEN TRANSACTION // ================================================================================================ @@ -138,73 +140,46 @@ impl ProvenTransaction { /// # Errors /// /// Returns an error if: - /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`]. /// - The transaction is empty, which is the case if the account state is unchanged or the /// number of input notes is zero. - /// - The transaction was executed against a _new_ on-chain account and its account ID does not - /// match the ID in the account update. - /// - The transaction was executed against a _new_ on-chain account and its commitment does not - /// match the final state commitment of the account update. - /// - The transaction was executed against a private account and the account update is _not_ of - /// type [`AccountUpdateDetails::Private`]. - /// - The transaction was executed against an on-chain account and the update is of type - /// [`AccountUpdateDetails::Private`]. - /// - The transaction was executed against an _existing_ on-chain account and the update is of - /// type [`AccountUpdateDetails::New`]. - /// - The transaction creates a _new_ on-chain account and the update is of type - /// [`AccountUpdateDetails::Delta`]. - fn validate(self) -> Result { - // If the account is on-chain, then the account update details must be present. - if self.account_id().is_onchain() { - self.account_update.validate()?; - - // check that either the account state was changed or at least one note was consumed, - // otherwise this transaction is empty - if self.account_update.initial_state_commitment() - == self.account_update.final_state_commitment() - && self.input_notes.commitment() == EMPTY_WORD - { - return Err(ProvenTransactionError::EmptyTransaction); - } + /// - The commitment computed on the actual account delta contained in [`TxAccountUpdate`] does + /// not match its declared account delta commitment. + fn validate(mut self) -> Result { + // Check that either the account state was changed or at least one note was consumed, + // otherwise this transaction is considered empty. + if self.account_update.initial_state_commitment() + == self.account_update.final_state_commitment() + && self.input_notes.commitment().is_empty() + { + return Err(ProvenTransactionError::EmptyTransaction); + } - let is_new_account = self.account_update.initial_state_commitment() == Word::empty(); - match self.account_update.details() { - AccountUpdateDetails::Private => { - return Err(ProvenTransactionError::OnChainAccountMissingDetails( - self.account_id(), - )); - }, - AccountUpdateDetails::New(account) => { - if !is_new_account { - return Err( - ProvenTransactionError::ExistingOnChainAccountRequiresDeltaDetails( - self.account_id(), - ), - ); - } - if account.id() != self.account_id() { - return Err(ProvenTransactionError::AccountIdMismatch { - tx_account_id: self.account_id(), - details_account_id: account.id(), - }); - } - if account.commitment() != self.account_update.final_state_commitment() { - return Err(ProvenTransactionError::AccountFinalCommitmentMismatch { - tx_final_commitment: self.account_update.final_state_commitment(), - details_commitment: account.commitment(), - }); - } - }, - AccountUpdateDetails::Delta(_) => { - if is_new_account { - return Err(ProvenTransactionError::NewOnChainAccountRequiresFullDetails( - self.account_id(), - )); - } - }, - } - } else if !self.account_update.is_private() { - return Err(ProvenTransactionError::PrivateAccountWithDetails(self.account_id())); + match &mut self.account_update.details { + // The delta commitment cannot be validated for private account updates. It will be + // validated as part of transaction proof verification implicitly. + AccountUpdateDetails::Private => (), + AccountUpdateDetails::Delta(post_fee_account_delta) => { + // Add the removed fee to the post fee delta to get the pre-fee delta, against which + // the delta commitment needs to be validated. + post_fee_account_delta.vault_mut().add_asset(self.fee.into()).map_err(|err| { + ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)) + })?; + + let expected_commitment = self.account_update.account_delta_commitment; + let actual_commitment = post_fee_account_delta.to_commitment(); + if expected_commitment != actual_commitment { + return Err(ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from( + format!( + "expected account delta commitment {expected_commitment} but found {actual_commitment}" + ), + ))); + } + + // Remove the added fee again to recreate the post fee delta. + post_fee_account_delta.vault_mut().remove_asset(self.fee.into()).map_err( + |err| ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)), + )?; + }, } Ok(self) @@ -377,21 +352,21 @@ impl ProvenTransactionBuilder { /// - The total number of output notes is greater than /// [`MAX_OUTPUT_NOTES_PER_TX`](crate::constants::MAX_OUTPUT_NOTES_PER_TX). /// - The vector of output notes contains duplicates. - /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`]. /// - The transaction is empty, which is the case if the account state is unchanged or the /// number of input notes is zero. - /// - The transaction was executed against a _new_ on-chain account and its account ID does not - /// match the ID in the account update. - /// - The transaction was executed against a _new_ on-chain account and its commitment does not - /// match the final state commitment of the account update. + /// - The commitment computed on the actual account delta contained in [`TxAccountUpdate`] does + /// not match its declared account delta commitment. + /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`]. + /// - The transaction was executed against a _new_ account with public state and its account ID + /// does not match the ID in the account update. + /// - The transaction was executed against a _new_ account with public state and its commitment + /// does not match the final state commitment of the account update. + /// - The transaction creates a _new_ account with public state and the update is of type + /// [`AccountUpdateDetails::Delta`] but the account delta is not a full state delta. /// - The transaction was executed against a private account and the account update is _not_ of /// type [`AccountUpdateDetails::Private`]. - /// - The transaction was executed against an on-chain account and the update is of type - /// [`AccountUpdateDetails::Private`]. - /// - The transaction was executed against an _existing_ on-chain account and the update is of - /// type [`AccountUpdateDetails::New`]. - /// - The transaction creates a _new_ on-chain account and the update is of type - /// [`AccountUpdateDetails::Delta`]. + /// - The transaction was executed against an account with public state and the update is of + /// type [`AccountUpdateDetails::Private`]. pub fn build(self) -> Result { let input_notes = InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?; @@ -409,7 +384,7 @@ impl ProvenTransactionBuilder { self.final_account_commitment, self.account_delta_commitment, self.account_update_details, - ); + )?; let proven_transaction = ProvenTransaction { id, @@ -463,20 +438,86 @@ pub struct TxAccountUpdate { impl TxAccountUpdate { /// Returns a new [TxAccountUpdate] instantiated from the specified components. - pub const fn new( + /// + /// Returns an error if: + /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`]. + /// - The transaction was executed against a _new_ account with public state and its account ID + /// does not match the ID in the account update. + /// - The transaction was executed against a _new_ account with public state and its commitment + /// does not match the final state commitment of the account update. + /// - The transaction creates a _new_ account with public state and the update is of type + /// [`AccountUpdateDetails::Delta`] but the account delta is not a full state delta. + /// - The transaction was executed against a private account and the account update is _not_ of + /// type [`AccountUpdateDetails::Private`]. + /// - The transaction was executed against an account with public state and the update is of + /// type [`AccountUpdateDetails::Private`]. + pub fn new( account_id: AccountId, init_state_commitment: Word, final_state_commitment: Word, account_delta_commitment: Word, details: AccountUpdateDetails, - ) -> Self { - Self { + ) -> Result { + let account_update = Self { account_id, init_state_commitment, final_state_commitment, account_delta_commitment, details, + }; + + let account_update_size = account_update.details.get_size_hint(); + if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize { + return Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded { + account_id, + update_size: account_update_size, + }); } + + if account_id.is_private() { + if account_update.details.is_private() { + return Ok(account_update); + } else { + return Err(ProvenTransactionError::PrivateAccountWithDetails(account_id)); + } + } + + match account_update.details() { + AccountUpdateDetails::Private => { + return Err(ProvenTransactionError::PublicStateAccountMissingDetails( + account_update.account_id(), + )); + }, + AccountUpdateDetails::Delta(delta) => { + let is_new_account = account_update.initial_state_commitment().is_empty(); + if is_new_account { + // Validate that for new accounts, the full account state can be constructed + // from the delta. This will fail if it is not such a full state delta. + let account = Account::try_from(delta).map_err(|err| { + ProvenTransactionError::NewPublicStateAccountRequiresFullStateDelta { + id: delta.id(), + source: err, + } + })?; + + if account.id() != account_id { + return Err(ProvenTransactionError::AccountIdMismatch { + tx_account_id: account_id, + details_account_id: account.id(), + }); + } + + if account.commitment() != account_update.final_state_commitment { + return Err(ProvenTransactionError::AccountFinalCommitmentMismatch { + tx_final_commitment: account_update.final_state_commitment, + details_commitment: account.commitment(), + }); + } + } + }, + } + + Ok(account_update) } /// Returns the ID of the updated account. @@ -511,21 +552,6 @@ impl TxAccountUpdate { pub fn is_private(&self) -> bool { self.details.is_private() } - - /// Validates the following properties of the account update: - /// - /// - The size of the serialized account update does not exceed [`ACCOUNT_UPDATE_MAX_SIZE`]. - pub fn validate(&self) -> Result<(), ProvenTransactionError> { - let account_update_size = self.details().get_size_hint(); - if account_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize { - Err(ProvenTransactionError::AccountUpdateSizeLimitExceeded { - account_id: self.account_id(), - update_size: account_update_size, - }) - } else { - Ok(()) - } - } } impl Serializable for TxAccountUpdate { @@ -540,13 +566,20 @@ impl Serializable for TxAccountUpdate { impl Deserializable for TxAccountUpdate { fn read_from(source: &mut R) -> Result { - Ok(Self { - account_id: AccountId::read_from(source)?, - init_state_commitment: Word::read_from(source)?, - final_state_commitment: Word::read_from(source)?, - account_delta_commitment: Word::read_from(source)?, - details: AccountUpdateDetails::read_from(source)?, - }) + let account_id = AccountId::read_from(source)?; + let init_state_commitment = Word::read_from(source)?; + let final_state_commitment = Word::read_from(source)?; + let account_delta_commitment = Word::read_from(source)?; + let details = AccountUpdateDetails::read_from(source)?; + + Self::new( + account_id, + init_state_commitment, + final_state_commitment, + account_delta_commitment, + details, + ) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -654,12 +687,12 @@ mod tests { use anyhow::Context; use miden_core::utils::Deserializable; use miden_verifier::ExecutionProof; - use winter_air::proof::Proof; use winter_rand_utils::rand_value; use super::ProvenTransaction; use crate::account::delta::AccountUpdateDetails; use crate::account::{ + Account, AccountDelta, AccountId, AccountIdVersion, @@ -675,6 +708,8 @@ mod tests { 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::transaction::{ProvenTransactionBuilder, TxAccountUpdate}; use crate::utils::Serializable; use crate::{ @@ -704,26 +739,27 @@ mod tests { } #[test] - fn account_update_size_limit_not_exceeded() { - // A small delta does not exceed the limit. - let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap(); - let storage_delta = AccountStorageDelta::from_iters( - [1, 2, 3, 4], - [(2, Word::from([1, 1, 1, 1u32])), (3, Word::from([1, 1, 0, 1u32]))], - [], - ); - let delta = AccountDelta::new(account_id, storage_delta, AccountVaultDelta::default(), ONE) - .unwrap(); + fn account_update_size_limit_not_exceeded() -> anyhow::Result<()> { + // A small account's delta does not exceed the limit. + let account = Account::builder([9; 32]) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoopAuthComponent) + .with_component(AddComponent) + .build_existing()?; + let delta = AccountDelta::try_from(account.clone())?; + let details = AccountUpdateDetails::Delta(delta); + TxAccountUpdate::new( - AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(), - EMPTY_WORD, - EMPTY_WORD, - EMPTY_WORD, + account.id(), + account.commitment(), + account.commitment(), + Word::empty(), details, - ) - .validate() - .unwrap(); + )?; + + Ok(()) } #[test] @@ -753,7 +789,6 @@ mod tests { EMPTY_WORD, details, ) - .validate() .unwrap_err(); assert!( @@ -778,7 +813,7 @@ mod tests { let ref_block_num = BlockNumber::from(1); let ref_block_commitment = Word::empty(); let expiration_block_num = BlockNumber::from(2); - let proof = ExecutionProof::new(Proof::new_dummy(), Default::default()); + let proof = ExecutionProof::new_dummy(); let tx = ProvenTransactionBuilder::new( account_id, diff --git a/crates/miden-objects/src/transaction/tx_args.rs b/crates/miden-objects/src/transaction/tx_args.rs index be29d4edb2..0088ffdc1c 100644 --- a/crates/miden-objects/src/transaction/tx_args.rs +++ b/crates/miden-objects/src/transaction/tx_args.rs @@ -1,10 +1,12 @@ -use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; use miden_crypto::merkle::InnerNodeInfo; +use miden_processor::MastNodeExt; -use super::{AccountInputs, Felt, Word}; +use super::{Felt, Hasher, Word}; +use crate::account::auth::{PublicKeyCommitment, Signature}; use crate::note::{NoteId, NoteRecipient}; use crate::utils::serde::{ ByteReader, @@ -37,13 +39,12 @@ use crate::{EMPTY_WORD, MastForest, MastNodeId}; /// this argument is not specified, the [`EMPTY_WORD`] would be used as a default value. If the /// [AdviceInputs] are propagated with some user defined map entries, this argument could be used /// as a key to access the corresponding value. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionArgs { tx_script: Option, tx_script_args: Word, note_args: BTreeMap, advice_inputs: AdviceInputs, - foreign_account_inputs: Vec, auth_args: Word, } @@ -53,7 +54,7 @@ impl TransactionArgs { /// Returns new [TransactionArgs] instantiated with the provided transaction script, advice /// map and foreign account inputs. - pub fn new(advice_map: AdviceMap, foreign_account_inputs: Vec) -> Self { + pub fn new(advice_map: AdviceMap) -> Self { let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; Self { @@ -61,7 +62,6 @@ impl TransactionArgs { tx_script_args: EMPTY_WORD, note_args: Default::default(), advice_inputs, - foreign_account_inputs, auth_args: EMPTY_WORD, } } @@ -138,19 +138,6 @@ impl TransactionArgs { &self.advice_inputs } - /// Returns a reference to the foreign account inputs in the transaction arguments. - pub fn foreign_account_inputs(&self) -> &[AccountInputs] { - &self.foreign_account_inputs - } - - /// Collects and returns a set containing all code commitments from foreign accounts. - pub fn to_foreign_account_code_commitments(&self) -> BTreeSet { - self.foreign_account_inputs() - .iter() - .map(|acc| acc.code().commitment()) - .collect() - } - /// Returns a reference to the authentication procedure argument, or [`EMPTY_WORD`] if the /// argument was not specified. /// @@ -167,9 +154,10 @@ impl TransactionArgs { /// Populates the advice inputs with the expected recipient data for creating output notes. /// - /// The advice inputs' map is extended with the following keys: - /// - /// - recipient_digest |-> recipient details (inputs_hash, script_root, serial_num). + /// The advice inputs' map is extended with the following entries: + /// - RECIPIENT: [SERIAL_SCRIPT_HASH, INPUTS_COMMITMENT] + /// - SERIAL_SCRIPT_HASH: [SERIAL_HASH, SCRIPT_ROOT] + /// - SERIAL_HASH: [SERIAL_NUM, EMPTY_WORD] /// - inputs_commitment |-> inputs. /// - script_root |-> script. pub fn add_output_note_recipient>(&mut self, note_recipient: T) { @@ -178,15 +166,38 @@ impl TransactionArgs { let script = note_recipient.script(); let script_encoded: Vec = script.into(); - let new_elements = [ - (note_recipient.digest(), note_recipient.format_for_advice()), - (inputs.commitment(), inputs.format_for_advice()), + // Build the advice map entries + let sn_hash = Hasher::merge(&[note_recipient.serial_num(), Word::empty()]); + let sn_script_hash = Hasher::merge(&[sn_hash, script.root()]); + + let new_elements = vec![ + (sn_hash, concat_words(note_recipient.serial_num(), Word::empty())), + (sn_script_hash, concat_words(sn_hash, script.root())), + (note_recipient.digest(), concat_words(sn_script_hash, inputs.commitment())), + (inputs.commitment(), inputs.to_elements()), (script.root(), script_encoded), ]; self.advice_inputs.extend(AdviceInputs::default().with_map(new_elements)); } + /// Adds the `signature` corresponding to `pub_key` on `message` to the advice inputs' map. + /// + /// The advice inputs' map is extended with the following key: + /// + /// - hash(pub_key, message) |-> signature (prepared for VM execution). + pub fn add_signature( + &mut self, + pub_key: PublicKeyCommitment, + message: Word, + signature: Signature, + ) { + let pk_word: Word = pub_key.into(); + self.advice_inputs + .map + .insert(Hasher::merge(&[pk_word, message]), signature.to_prepared_signature()); + } + /// Populates the advice inputs with the specified note recipient details. /// /// The advice inputs' map is extended with the following keys: @@ -215,19 +226,31 @@ impl TransactionArgs { } /// Extends the advice inputs in self with the provided ones. - #[cfg(feature = "testing")] pub fn extend_advice_inputs(&mut self, advice_inputs: AdviceInputs) { self.advice_inputs.extend(advice_inputs); } } +/// Concatenates two [`Word`]s into a [`Vec`] containing 8 elements. +fn concat_words(first: Word, second: Word) -> Vec { + let mut result = Vec::with_capacity(8); + result.extend(first); + result.extend(second); + result +} + +impl Default for TransactionArgs { + fn default() -> Self { + Self::new(AdviceMap::default()) + } +} + impl Serializable for TransactionArgs { fn write_into(&self, target: &mut W) { self.tx_script.write_into(target); self.tx_script_args.write_into(target); self.note_args.write_into(target); self.advice_inputs.write_into(target); - self.foreign_account_inputs.write_into(target); self.auth_args.write_into(target); } } @@ -238,7 +261,6 @@ impl Deserializable for TransactionArgs { let tx_script_args = Word::read_from(source)?; let note_args = BTreeMap::::read_from(source)?; let advice_inputs = AdviceInputs::read_from(source)?; - let foreign_account_inputs = Vec::::read_from(source)?; let auth_args = Word::read_from(source)?; Ok(Self { @@ -246,7 +268,6 @@ impl Deserializable for TransactionArgs { tx_script_args, note_args, advice_inputs, - foreign_account_inputs, auth_args, }) } @@ -307,7 +328,7 @@ impl TransactionScript { impl Serializable for TransactionScript { fn write_into(&self, target: &mut W) { self.mast.write_into(target); - target.write_u32(self.entrypoint.as_u32()); + target.write_u32(u32::from(self.entrypoint)); } } @@ -329,7 +350,7 @@ mod tests { #[test] fn test_tx_args_serialization() { - let tx_args = TransactionArgs::new(AdviceMap::default(), std::vec::Vec::default()); + let tx_args = TransactionArgs::new(AdviceMap::default()); let bytes: std::vec::Vec = tx_args.to_bytes(); let decoded = TransactionArgs::read_from_bytes(&bytes).unwrap(); diff --git a/crates/miden-objects/src/transaction/tx_header.rs b/crates/miden-objects/src/transaction/tx_header.rs index a3ae0e4b09..bdec27be5e 100644 --- a/crates/miden-objects/src/transaction/tx_header.rs +++ b/crates/miden-objects/src/transaction/tx_header.rs @@ -3,12 +3,12 @@ use alloc::vec::Vec; use miden_processor::DeserializationError; use crate::Word; -use crate::note::NoteId; +use crate::note::NoteHeader; use crate::transaction::{ AccountId, InputNoteCommitment, - Nullifier, - OutputNote, + InputNotes, + OutputNotes, ProvenTransaction, TransactionId, }; @@ -27,8 +27,8 @@ pub struct TransactionHeader { account_id: AccountId, initial_state_commitment: Word, final_state_commitment: Word, - input_notes: Vec, - output_notes: Vec, + input_notes: InputNotes, + output_notes: Vec, } impl TransactionHeader { @@ -37,18 +37,30 @@ impl TransactionHeader { /// Constructs a new [`TransactionHeader`] from the provided parameters. /// - /// Note that the nullifiers of the input notes and note IDs of the output notes must be in the - /// same order as they appeared in the transaction. This is ensured when constructing this type - /// from a proven transaction, but cannot be validated during deserialization, hence additional - /// validation is necessary. - pub(crate) fn new( - id: TransactionId, + /// The [`TransactionId`] is computed from the provided parameters. + /// + /// 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. + pub fn new( account_id: AccountId, initial_state_commitment: Word, final_state_commitment: Word, - input_notes: Vec, - output_notes: Vec, + input_notes: InputNotes, + output_notes: Vec, ) -> Self { + let input_notes_commitment = input_notes.commitment(); + let output_notes_commitment = OutputNotes::compute_commitment(output_notes.iter().copied()); + + let id = TransactionId::new( + initial_state_commitment, + final_state_commitment, + input_notes_commitment, + output_notes_commitment, + ); + Self { id, account_id, @@ -59,24 +71,28 @@ impl TransactionHeader { } } - /// Constructs a new [`TransactionHeader`] from the provided parameters for testing purposes. - #[cfg(any(feature = "testing", test))] + /// Constructs a new [`TransactionHeader`] from the provided parameters. + /// + /// # Warning + /// + /// This does not validate the internal consistency of the data. Prefer [`Self::new`] whenever + /// possible. pub fn new_unchecked( id: TransactionId, account_id: AccountId, initial_state_commitment: Word, final_state_commitment: Word, - input_notes: Vec, - output_notes: Vec, + input_notes: InputNotes, + output_notes: Vec, ) -> Self { - Self::new( + Self { id, account_id, initial_state_commitment, final_state_commitment, input_notes, output_notes, - ) + } } // PUBLIC ACCESSORS @@ -104,32 +120,41 @@ impl TransactionHeader { self.final_state_commitment } - /// Returns a reference to the nullifiers of the consumed notes. + /// Returns a reference to the consumed notes of the transaction. + /// + /// The returned input note commitments have the same order as the transaction to which the + /// header belongs. /// /// Note that the note may have been erased at the batch or block level, so it may not be /// present there. - pub fn input_notes(&self) -> &[Nullifier] { + pub fn input_notes(&self) -> &InputNotes { &self.input_notes } - /// Returns a reference to the notes created by the transaction. + /// Returns a reference to the ID and metadata of the output notes created by the transaction. + /// + /// The returned output note data has the same order as the transaction to which the header + /// belongs. /// /// Note that the note may have been erased at the batch or block level, so it may not be /// present there. - pub fn output_notes(&self) -> &[NoteId] { + pub fn output_notes(&self) -> &[NoteHeader] { &self.output_notes } } impl From<&ProvenTransaction> for TransactionHeader { + /// Constructs a [`TransactionHeader`] from a [`ProvenTransaction`]. fn from(tx: &ProvenTransaction) -> Self { - TransactionHeader::new( + // SAFETY: The data in a proven transaction is guaranteed to be internally consistent and so + // we can skip the consistency checks by the `new` constructor. + TransactionHeader::new_unchecked( tx.id(), tx.account_id(), tx.account_update().initial_state_commitment(), tx.account_update().final_state_commitment(), - tx.input_notes().iter().map(InputNoteCommitment::nullifier).collect(), - tx.output_notes().iter().map(OutputNote::id).collect(), + tx.input_notes().clone(), + tx.output_notes().iter().map(NoteHeader::from).collect(), ) } } @@ -139,7 +164,6 @@ impl From<&ProvenTransaction> for TransactionHeader { impl Serializable for TransactionHeader { fn write_into(&self, target: &mut W) { - self.id.write_into(target); self.account_id.write_into(target); self.initial_state_commitment.write_into(target); self.final_state_commitment.write_into(target); @@ -150,20 +174,20 @@ impl Serializable for TransactionHeader { impl Deserializable for TransactionHeader { fn read_from(source: &mut R) -> Result { - let id = ::read_from(source)?; let account_id = ::read_from(source)?; let initial_state_commitment = ::read_from(source)?; let final_state_commitment = ::read_from(source)?; - let input_notes = >::read_from(source)?; - let output_notes = >::read_from(source)?; + let input_notes = >::read_from(source)?; + let output_notes = >::read_from(source)?; - Ok(Self::new( - id, + let tx_header = Self::new( account_id, initial_state_commitment, final_state_commitment, input_notes, output_notes, - )) + ); + + Ok(tx_header) } } diff --git a/crates/miden-objects/src/transaction/tx_witness.rs b/crates/miden-objects/src/transaction/tx_witness.rs deleted file mode 100644 index df2ce3dc3a..0000000000 --- a/crates/miden-objects/src/transaction/tx_witness.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::{AdviceInputs, TransactionArgs, TransactionInputs}; -use crate::utils::serde::{ByteReader, Deserializable, DeserializationError, Serializable}; - -// TRANSACTION WITNESS -// ================================================================================================ - -/// Transaction witness contains all the data required to execute and prove a Miden blockchain -/// transaction. -/// -/// The main purpose of the transaction witness is to enable stateless re-execution and proving -/// of transactions. -/// -/// A transaction witness consists of: -/// - Transaction inputs which contain information about the initial state of the account, input -/// notes, block header etc. -/// - Optional transaction arguments which may contain a transaction script, note arguments, -/// transaction script arguments and any additional advice data to initialize the advice provider -/// with prior to transaction execution. -/// - Advice witness which contains all data requested by the VM from the advice provider while -/// executing the transaction program. -/// -/// TODO: currently, the advice witness contains redundant and irrelevant data (e.g., tx inputs -/// and tx outputs; account codes and a subset of that data in advice inputs). -/// We should optimize it to contain only the minimum data required for executing/proving the -/// transaction. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TransactionWitness { - pub tx_inputs: TransactionInputs, - pub tx_args: TransactionArgs, - pub advice_witness: AdviceInputs, -} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for TransactionWitness { - fn write_into(&self, target: &mut W) { - self.tx_inputs.write_into(target); - self.tx_args.write_into(target); - self.advice_witness.write_into(target); - } -} - -impl Deserializable for TransactionWitness { - fn read_from(source: &mut R) -> Result { - let tx_inputs = TransactionInputs::read_from(source)?; - let tx_args = TransactionArgs::read_from(source)?; - let advice_witness = AdviceInputs::read_from(source)?; - Ok(Self { tx_inputs, tx_args, advice_witness }) - } -} diff --git a/crates/miden-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index 6a43026c56..01e4fe2fc2 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -10,34 +10,34 @@ name = "miden-testing" readme = "README.md" repository.workspace = true rust-version.workspace = true -version = "0.11.5" +version.workspace = true [features] std = ["miden-lib/std"] [dependencies] # Workspace dependencies -miden-block-prover = { features = ["testing"], workspace = true } -miden-lib = { features = ["testing"], workspace = true } -miden-objects = { features = ["testing"], workspace = true } -miden-tx = { features = ["testing"], workspace = true } +miden-block-prover = { features = ["testing"], workspace = true } +miden-lib = { features = ["testing"], workspace = true } +miden-objects = { features = ["testing"], workspace = true } +miden-tx = { features = ["testing"], workspace = true } +miden-tx-batch-prover = { features = ["testing"], workspace = true } # Miden dependencies miden-processor = { workspace = true } # External dependencies -anyhow = { default-features = false, version = "1.0" } +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 } -# TODO: We should be able to remove this once we remove `TransactionContext::execute_blocking` -tokio = { features = ["macros", "rt"], workspace = true } -winterfell = { version = "0.13" } +winterfell = { version = "0.13" } [dev-dependencies] -anyhow = { features = ["backtrace", "std"], version = "1.0" } +anyhow = { features = ["backtrace", "std"], workspace = true } assert_matches = { workspace = true } miden-objects = { features = ["std"], workspace = true } rstest = { workspace = true } +tokio = { features = ["macros", "rt"], workspace = true } winter-rand-utils = { version = "0.13" } diff --git a/crates/miden-testing/README.md b/crates/miden-testing/README.md index a7ac101147..57bd495f50 100644 --- a/crates/miden-testing/README.md +++ b/crates/miden-testing/README.md @@ -4,4 +4,4 @@ This crate contains tool for testing Miden transactions, batches and blocks. ## License -This project is [MIT licensed](../LICENSE). +This project is [MIT licensed](../../LICENSE). diff --git a/crates/miden-testing/src/executor.rs b/crates/miden-testing/src/executor.rs index d3a2eebec3..a8ae18cc2e 100644 --- a/crates/miden-testing/src/executor.rs +++ b/crates/miden-testing/src/executor.rs @@ -1,30 +1,19 @@ -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_processor::{ - AdviceInputs, - DefaultHost, - ExecutionError, - Process, - Program, - StackInputs, - SyncHost, -}; - -// MOCK CODE EXECUTOR +#[cfg(test)] +use miden_processor::DefaultHost; +use miden_processor::fast::{ExecutionOutput, FastProcessor}; +use miden_processor::{AdviceInputs, AsyncHost, ExecutionError, Program, StackInputs}; + +// CODE EXECUTOR // ================================================================================================ /// Helper for executing arbitrary code within arbitrary hosts. -pub struct CodeExecutor { +pub(crate) struct CodeExecutor { host: H, stack_inputs: Option, advice_inputs: AdviceInputs, } -impl CodeExecutor { +impl CodeExecutor { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- pub(crate) fn new(host: H) -> Self { @@ -49,7 +38,15 @@ impl CodeExecutor { /// /// To improve the error message quality, convert the returned [`ExecutionError`] into a /// [`Report`](miden_objects::assembly::diagnostics::Report). - pub fn run(self, code: &str) -> Result { + #[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}; + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); let assembler = TransactionKernel::with_kernel_library(source_manager.clone()); @@ -58,27 +55,39 @@ impl CodeExecutor { source_manager.load(SourceLanguage::Masm, Uri::new("_user_code"), code.to_owned()); let program = assembler.assemble_program(virtual_source_file).unwrap(); - self.execute_program(program) + self.execute_program(program).await } /// 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). - pub fn execute_program(mut self, program: Program) -> Result { - let mut process = Process::new_debug( - program.kernel().clone(), - self.stack_inputs.unwrap_or_default(), - self.advice_inputs, - ); - process.execute(&program, &mut self.host)?; - - Ok(process) + pub async fn execute_program( + mut self, + program: Program, + ) -> Result { + // This reverses the stack inputs (even though it doesn't look like it does) because the + // fast processor expects the reverse order. + // + // Once we use the FastProcessor for execution and proving, we can change the way these + // inputs are constructed in TransactionKernel::prepare_inputs. + let stack_inputs = + StackInputs::new(self.stack_inputs.unwrap_or_default().iter().copied().collect()) + .unwrap(); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), self.advice_inputs); + + let execution_output = processor.execute(&program, &mut self.host).await?; + + Ok(execution_output) } } +#[cfg(test)] impl CodeExecutor { pub fn with_default_host() -> Self { + use miden_lib::transaction::TransactionKernel; + let mut host = DefaultHost::default(); let test_lib = TransactionKernel::library(); 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 a140e0efd8..058d6a7f80 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs @@ -17,6 +17,7 @@ use rand::rngs::SmallRng; use rand::{Rng, SeedableRng}; use super::proven_tx_builder::MockProvenTxBuilder; +use crate::utils::create_p2any_note; use crate::{AccountState, Auth, MockChain, MockChainBuilder}; fn mock_account_id(num: u8) -> AccountId { @@ -36,16 +37,20 @@ struct TestSetup { chain: MockChain, account1: Account, account2: Account, + note1: Note, } fn setup_chain() -> TestSetup { let mut builder = MockChain::builder(); let account1 = generate_account(&mut builder); let account2 = generate_account(&mut builder); + let note1 = builder + .add_p2id_note(account1.id(), account2.id(), &[], NoteType::Public) + .expect("adding p2id note1 should work"); let mut chain = builder.build().expect("genesis should be valid"); chain.prove_next_block().expect("valid setup"); - TestSetup { chain, account1, account2 } + TestSetup { chain, account1, account2, note1 } } fn generate_account(chain: &mut MockChainBuilder) -> Account { @@ -77,7 +82,7 @@ fn empty_transaction_batch() -> anyhow::Result<()> { /// output note commitments. #[test] fn note_created_and_consumed_in_same_batch() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, account2 } = setup_chain(); + let TestSetup { mut chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); let block2 = chain.prove_next_block()?; @@ -110,7 +115,7 @@ fn note_created_and_consumed_in_same_batch() -> anyhow::Result<()> { /// times in different transactions. #[test] fn duplicate_unauthenticated_input_notes() -> anyhow::Result<()> { - let TestSetup { chain, account1, account2 } = setup_chain(); + let TestSetup { chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); let note = mock_note(50); @@ -149,20 +154,19 @@ fn duplicate_unauthenticated_input_notes() -> anyhow::Result<()> { /// times in different transactions. #[test] fn duplicate_authenticated_input_notes() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, account2 } = setup_chain(); - let note = chain.add_pending_p2id_note(account1.id(), account2.id(), &[], NoteType::Public)?; + let TestSetup { mut chain, account1, account2, note1 } = setup_chain(); let block1 = chain.block_header(1); let block2 = chain.prove_next_block()?; let tx1 = MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.commitment()) .ref_block_commitment(block1.commitment()) - .authenticated_notes(vec![note.clone()]) + .authenticated_notes(vec![note1.clone()]) .build()?; let tx2 = MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.commitment()) .ref_block_commitment(block1.commitment()) - .authenticated_notes(vec![note.clone()]) + .authenticated_notes(vec![note1.clone()]) .build()?; let error = ProposedBatch::new( @@ -177,7 +181,7 @@ fn duplicate_authenticated_input_notes() -> anyhow::Result<()> { note_nullifier, first_transaction_id, second_transaction_id - } if note_nullifier == note.nullifier() && + } if note_nullifier == note1.nullifier() && first_transaction_id == tx1.id() && second_transaction_id == tx2.id() ); @@ -189,20 +193,19 @@ fn duplicate_authenticated_input_notes() -> anyhow::Result<()> { /// transactions as an unauthenticated or authenticated note. #[test] fn duplicate_mixed_input_notes() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, account2 } = setup_chain(); - let note = chain.add_pending_p2id_note(account1.id(), account2.id(), &[], NoteType::Public)?; + let TestSetup { mut chain, account1, account2, note1 } = setup_chain(); let block1 = chain.block_header(1); let block2 = chain.prove_next_block()?; let tx1 = MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.commitment()) .ref_block_commitment(block1.commitment()) - .unauthenticated_notes(vec![note.clone()]) + .unauthenticated_notes(vec![note1.clone()]) .build()?; let tx2 = MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.commitment()) .ref_block_commitment(block1.commitment()) - .authenticated_notes(vec![note.clone()]) + .authenticated_notes(vec![note1.clone()]) .build()?; let error = ProposedBatch::new( @@ -217,7 +220,7 @@ fn duplicate_mixed_input_notes() -> anyhow::Result<()> { note_nullifier, first_transaction_id, second_transaction_id - } if note_nullifier == note.nullifier() && + } if note_nullifier == note1.nullifier() && first_transaction_id == tx1.id() && second_transaction_id == tx2.id() ); @@ -229,7 +232,7 @@ fn duplicate_mixed_input_notes() -> anyhow::Result<()> { /// transactions. #[test] fn duplicate_output_notes() -> anyhow::Result<()> { - let TestSetup { chain, account1, account2 } = setup_chain(); + let TestSetup { chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); let note0 = mock_output_note(50); @@ -265,29 +268,54 @@ fn duplicate_output_notes() -> anyhow::Result<()> { /// Test that an unauthenticated input note for which a proof exists is converted into an /// authenticated one and becomes part of the batch's input note commitment. -#[test] -fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, account2 } = setup_chain(); - let note0 = chain.add_pending_p2id_note(account2.id(), account1.id(), &[], NoteType::Public)?; - let note1 = chain.add_pending_p2id_note(account1.id(), account2.id(), &[], NoteType::Public)?; - // The just created note will be provable against block2. +#[tokio::test] +async fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account1 = generate_account(&mut builder); + let note1 = create_p2any_note(account1.id(), NoteType::Public, [], builder.rng_mut()); + let note2 = create_p2any_note(account1.id(), NoteType::Public, [], builder.rng_mut()); + let spawn_note = builder.add_spawn_note([¬e1, ¬e2])?; + let mut chain = builder.build()?; + + let tx = chain + .build_tx_context(account1.clone(), &[spawn_note.id()], &[])? + .extend_expected_output_notes(vec![ + OutputNote::Full(note1.clone()), + OutputNote::Full(note2.clone()), + ]) + .build()? + .execute() + .await?; + chain.add_pending_executed_transaction(&tx)?; + + // Note1 and note2 are included and therefore provable against block1. + let block1 = chain.prove_next_block()?; let block2 = chain.prove_next_block()?; let block3 = chain.prove_next_block()?; - let block4 = chain.prove_next_block()?; + + assert_eq!(block1.output_notes().count(), 2, "block 1 should contain note1 and note2"); + assert!( + block1.output_notes().any(|(_, note)| note.commitment() == note1.commitment()), + "block 1 should contain note1" + ); + assert!( + block1.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(block3.commitment()) - .unauthenticated_notes(vec![note1.clone()]) + .ref_block_commitment(block2.commitment()) + .unauthenticated_notes(vec![note2.clone()]) .build()?; - let input_note0 = chain.get_public_note(¬e0.id()).expect("note not found"); - let note_inclusion_proof0 = input_note0.proof().expect("note should be of type authenticated"); - let input_note1 = chain.get_public_note(¬e1.id()).expect("note not found"); let note_inclusion_proof1 = input_note1.proof().expect("note should be of type authenticated"); + let input_note2 = chain.get_public_note(¬e2.id()).expect("note not found"); + let note_inclusion_proof2 = input_note2.proof().expect("note should be of type authenticated"); + // The partial blockchain will contain all blocks in the mock chain, in particular block2 which // both note inclusion proofs need for verification. let partial_blockchain = chain.latest_partial_blockchain(); @@ -297,9 +325,9 @@ fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { let error = ProposedBatch::new( [tx1.clone()].into_iter().map(Arc::new).collect(), - block4.header().clone(), + block3.header().clone(), partial_blockchain.clone(), - BTreeMap::from_iter([(input_note1.id(), note_inclusion_proof0.clone())]), + BTreeMap::from_iter([(input_note2.id(), note_inclusion_proof1.clone())]), ) .unwrap_err(); @@ -307,27 +335,29 @@ fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { note_id, block_num, source: MerkleError::ConflictingRoots { .. }, - } if note_id == note1.id() && - block_num == block2.header().block_num() + } => { + assert_eq!(note_id, note2.id()); + assert_eq!(block_num, block1.header().block_num()); + } ); // Case 2: Error: The block referenced by the (valid) note inclusion proof is missing. // -------------------------------------------------------------------------------------------- - // Make a clone of the partial blockchain where block2 is missing. + // Make a clone of the partial blockchain where block1 is missing. let mut mmr = partial_blockchain.mmr().clone(); - mmr.untrack(block2.header().block_num().as_usize()); + mmr.untrack(block1.header().block_num().as_usize()); let blocks = partial_blockchain .block_headers() - .filter(|header| header.block_num() != block2.header().block_num()) + .filter(|header| header.block_num() != block1.header().block_num()) .cloned(); let error = ProposedBatch::new( [tx1.clone()].into_iter().map(Arc::new).collect(), - block4.header().clone(), + block3.header().clone(), PartialBlockchain::new(mmr, blocks) .context("failed to build partial blockchain with missing block")?, - BTreeMap::from_iter([(input_note1.id(), note_inclusion_proof1.clone())]), + BTreeMap::from_iter([(input_note2.id(), note_inclusion_proof2.clone())]), ) .unwrap_err(); @@ -336,8 +366,10 @@ fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { ProposedBatchError::UnauthenticatedInputNoteBlockNotInPartialBlockchain { block_number, note_id - } if block_number == note_inclusion_proof1.location().block_num() && - note_id == input_note1.id() + } => { + assert_eq!(block_number, note_inclusion_proof2.location().block_num()); + assert_eq!(note_id, input_note2.id()); + } ); // Case 3: Success: The correct proof is passed. @@ -345,9 +377,9 @@ fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { let batch = ProposedBatch::new( [tx1].into_iter().map(Arc::new).collect(), - block4.header().clone(), + block3.header().clone(), partial_blockchain, - BTreeMap::from_iter([(input_note1.id(), note_inclusion_proof1.clone())]), + BTreeMap::from_iter([(input_note2.id(), note_inclusion_proof2.clone())]), )?; // We expect the unauthenticated input note to have become an authenticated one, @@ -357,7 +389,7 @@ fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { batch .input_notes() .iter() - .any(|commitment| commitment == &InputNoteCommitment::from(&input_note1)) + .any(|commitment| commitment == &InputNoteCommitment::from(&input_note2)) ); assert_eq!(batch.output_notes().len(), 0); @@ -376,8 +408,7 @@ fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> { /// attack vector. #[test] fn authenticated_note_created_in_same_batch() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, account2 } = setup_chain(); - let note = chain.add_pending_p2id_note(account1.id(), account2.id(), &[], NoteType::Public)?; + let TestSetup { mut chain, account1, account2, note1 } = setup_chain(); let block1 = chain.block_header(1); let block2 = chain.prove_next_block()?; @@ -390,7 +421,7 @@ fn authenticated_note_created_in_same_batch() -> anyhow::Result<()> { let tx2 = MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.commitment()) .ref_block_commitment(block1.commitment()) - .authenticated_notes(vec![note.clone()]) + .authenticated_notes(vec![note1.clone()]) .build()?; let batch = ProposedBatch::new( @@ -481,7 +512,7 @@ fn multiple_transactions_against_same_account() -> anyhow::Result<()> { /// - The output note commitment is sorted by [`NoteId`]. #[test] fn input_and_output_notes_commitment() -> anyhow::Result<()> { - let TestSetup { chain, account1, account2 } = setup_chain(); + let TestSetup { chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); let note0 = mock_output_note(50); @@ -536,7 +567,7 @@ fn input_and_output_notes_commitment() -> anyhow::Result<()> { /// Tests that the expiration block number of a batch is the minimum of all contained transactions. #[test] fn batch_expiration() -> anyhow::Result<()> { - let TestSetup { chain, account1, account2 } = setup_chain(); + let TestSetup { chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); let tx1 = @@ -594,7 +625,7 @@ fn duplicate_transaction() -> anyhow::Result<()> { /// TX 2: Inputs [Y] -> Outputs [X] #[test] fn circular_note_dependency() -> anyhow::Result<()> { - let TestSetup { chain, account1, account2 } = setup_chain(); + let TestSetup { chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); let note_x = mock_note(20); @@ -629,7 +660,7 @@ fn circular_note_dependency() -> anyhow::Result<()> { /// Tests that expired transactions cannot be included in a batch. #[test] fn expired_transaction() -> anyhow::Result<()> { - let TestSetup { chain, account1, account2 } = setup_chain(); + let TestSetup { chain, account1, account2, .. } = setup_chain(); let block1 = chain.block_header(1); // This transaction expired at the batch's reference block. @@ -670,19 +701,21 @@ fn expired_transaction() -> anyhow::Result<()> { /// _before_ a state-updating transaction with state commitments X -> Y against account A. #[test] fn noop_tx_before_state_updating_tx_against_same_account() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, .. } = setup_chain(); + let TestSetup { mut chain, account1, note1, .. } = setup_chain(); let block1 = chain.block_header(1); let block2 = chain.prove_next_block()?; let random_final_state_commitment = Word::from([1, 2, 3, 4u32]); let note = mock_note(40); + // consume a random note to make the transaction non-empty let noop_tx1 = MockProvenTxBuilder::with_account( account1.id(), account1.commitment(), account1.commitment(), ) .ref_block_commitment(block1.commitment()) + .authenticated_notes(vec![note1]) .output_notes(vec![OutputNote::Full(note.clone())]) .build()?; @@ -719,7 +752,7 @@ fn noop_tx_before_state_updating_tx_against_same_account() -> anyhow::Result<()> /// _after_ a state-updating transaction with state commitments X -> Y against account A. #[test] fn noop_tx_after_state_updating_tx_against_same_account() -> anyhow::Result<()> { - let TestSetup { mut chain, account1, .. } = setup_chain(); + let TestSetup { mut chain, account1, note1, .. } = setup_chain(); let block1 = chain.block_header(1); let block2 = chain.prove_next_block()?; @@ -736,12 +769,14 @@ fn noop_tx_after_state_updating_tx_against_same_account() -> anyhow::Result<()> .unauthenticated_notes(vec![note.clone()]) .build()?; + // consume a random note to make the transaction non-empty let noop_tx2 = MockProvenTxBuilder::with_account( account1.id(), random_final_state_commitment, random_final_state_commitment, ) .ref_block_commitment(block1.commitment()) + .authenticated_notes(vec![note1]) .output_notes(vec![OutputNote::Full(note.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 52b8c7f4ca..3c583dd06a 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 @@ -14,7 +14,6 @@ use miden_objects::transaction::{ ProvenTransactionBuilder, }; use miden_objects::vm::ExecutionProof; -use winterfell::Proof; /// A builder to build mocked [`ProvenTransaction`]s. pub struct MockProvenTxBuilder { @@ -112,7 +111,7 @@ impl MockProvenTxBuilder { self.ref_block_commitment.unwrap_or_default(), self.fee, self.expiration_block_num, - ExecutionProof::new(Proof::new_dummy(), Default::default()), + ExecutionProof::new_dummy(), ) .add_input_notes(self.input_notes.unwrap_or_default()) .add_input_notes(self.nullifiers.unwrap_or_default()) 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 cf7739bf1f..cc7aabf71f 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,42 +3,52 @@ use std::collections::BTreeMap; use std::vec::Vec; use assert_matches::assert_matches; -use miden_objects::account::AccountId; +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; -use miden_objects::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; -use miden_objects::transaction::{OutputNote, ProvenTransaction}; -use miden_objects::{MAX_BATCHES_PER_BLOCK, ProposedBlockError}; +use miden_objects::note::{NoteInclusionProof, NoteType}; +use miden_objects::{MAX_BATCHES_PER_BLOCK, ProposedBlockError, ZERO}; use miden_processor::crypto::MerklePath; +use miden_tx::LocalTransactionProver; -use super::utils::{ - TestSetup, - generate_batch, - generate_executed_tx_with_authenticated_notes, - generate_fungible_asset, - generate_output_note, - generate_tracked_note, - generate_tracked_note_with_asset, - generate_tx_with_authenticated_notes, - generate_tx_with_expiration, - generate_tx_with_unauthenticated_notes, - generate_untracked_note, - setup_chain, -}; -use crate::ProvenTransactionExt; -use crate::utils::create_spawn_note; +use crate::kernel_tests::block::utils::MockChainBlockExt; +use crate::utils::create_p2any_note; +use crate::{Auth, MockChain}; /// Tests that too many batches produce an error. -#[test] -fn proposed_block_fails_on_too_many_batches() -> anyhow::Result<()> { +#[tokio::test] +async fn proposed_block_fails_on_too_many_batches() -> anyhow::Result<()> { let count = MAX_BATCHES_PER_BLOCK + 1; - let TestSetup { mut chain, mut txs, .. } = setup_chain(count); - let mut batches = Vec::with_capacity(count); - for i in 0..count { - batches.push(generate_batch(&mut chain, vec![txs.remove(&i).unwrap()])); - } + let (chain, batches) = { + let mut builder = MockChain::builder(); + let mut accounts = Vec::new(); + let mut notes = Vec::new(); + for _ in 0..count { + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note = builder.add_p2any_note( + account.id(), + NoteType::Public, + [FungibleAsset::mock(42)], + )?; + + accounts.push(account); + notes.push(note); + } + + let chain = builder.build()?; + + let mut batches = Vec::with_capacity(count); + for i in 0..count { + let proven_tx = chain + .create_authenticated_notes_proven_tx(accounts[i].id(), [notes[i].id()]) + .await?; + batches.push(chain.create_batch(vec![proven_tx])?); + } + + (chain, batches) + }; let block_inputs = BlockInputs::new( chain.latest_block_header(), @@ -56,11 +66,18 @@ fn proposed_block_fails_on_too_many_batches() -> anyhow::Result<()> { } /// Tests that duplicate batches produce an error. -#[test] -fn proposed_block_fails_on_duplicate_batches() -> anyhow::Result<()> { - let TestSetup { mut chain, mut txs, .. } = setup_chain(1); - let proven_tx0 = txs.remove(&0).unwrap(); - let batch0 = generate_batch(&mut chain, vec![proven_tx0]); +#[tokio::test] +async fn proposed_block_fails_on_duplicate_batches() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let sender_account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note = + builder.add_p2any_note(sender_account.id(), NoteType::Public, [FungibleAsset::mock(42)])?; + let chain = builder.build()?; + + let proven_tx0 = chain + .create_authenticated_notes_proven_tx(sender_account.id(), [note.id()]) + .await?; + let batch0 = chain.create_batch(vec![proven_tx0])?; let batches = vec![batch0.clone(), batch0.clone()]; @@ -80,18 +97,21 @@ fn proposed_block_fails_on_duplicate_batches() -> anyhow::Result<()> { } /// Tests that an expired batch produces an error. -#[test] -fn proposed_block_fails_on_expired_batches() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); +#[tokio::test] +async fn proposed_block_fails_on_expired_batches() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let mut chain = builder.build()?; + + chain.prove_next_block()?; let block1_num = chain.block_header(1).block_num(); - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let tx0 = generate_tx_with_expiration(&mut chain, account0.id(), block1_num + 5); - let tx1 = generate_tx_with_expiration(&mut chain, account1.id(), block1_num + 1); + let tx0 = chain.create_expiring_proven_tx(account0.id(), block1_num + 5).await?; + let tx1 = chain.create_expiring_proven_tx(account1.id(), block1_num + 1).await?; - let batch0 = generate_batch(&mut chain, vec![tx0]); - let batch1 = generate_batch(&mut chain, vec![tx1]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx1])?; let _block2 = chain.prove_next_block()?; @@ -117,11 +137,14 @@ fn proposed_block_fails_on_expired_batches() -> anyhow::Result<()> { } /// Tests that a timestamp at or before the previous block header produces an error. -#[test] -fn proposed_block_fails_on_timestamp_not_increasing_monotonically() -> anyhow::Result<()> { - let TestSetup { mut chain, mut txs, .. } = setup_chain(1); - let proven_tx0 = txs.remove(&0).unwrap(); - let batch0 = generate_batch(&mut chain, vec![proven_tx0]); +#[tokio::test] +async fn proposed_block_fails_on_timestamp_not_increasing_monotonically() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let chain = builder.build()?; + let proven_tx0 = chain.create_authenticated_notes_proven_tx(account, []).await?; + + let batch0 = chain.create_batch(vec![proven_tx0])?; let batches = vec![batch0]; // Mock BlockInputs. let block_inputs = BlockInputs::new( @@ -147,11 +170,15 @@ fn proposed_block_fails_on_timestamp_not_increasing_monotonically() -> anyhow::R /// Tests that a partial blockchain that is not at the state of the previous block header produces /// an error. -#[test] -fn proposed_block_fails_on_partial_blockchain_and_prev_block_inconsistency() -> anyhow::Result<()> { - let TestSetup { mut chain, mut txs, .. } = setup_chain(1); - let proven_tx0 = txs.remove(&0).unwrap(); - let batch0 = generate_batch(&mut chain, vec![proven_tx0]); +#[tokio::test] +async fn proposed_block_fails_on_partial_blockchain_and_prev_block_inconsistency() +-> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let chain = builder.build()?; + let proven_tx0 = chain.create_authenticated_notes_proven_tx(account, []).await?; + + let batch0 = chain.create_batch(vec![proven_tx0])?; let batches = vec![batch0]; // Select the partial blockchain which is valid for the current block but pass the next block in @@ -200,19 +227,23 @@ fn proposed_block_fails_on_partial_blockchain_and_prev_block_inconsistency() -> /// Tests that a partial blockchain that does not contain all reference blocks of the batches /// produces an error. -#[test] -fn proposed_block_fails_on_missing_batch_reference_block() -> anyhow::Result<()> { - let TestSetup { mut chain, mut txs, .. } = setup_chain(1); - let proven_tx0 = txs.remove(&0).unwrap(); +#[tokio::test] +async fn proposed_block_fails_on_missing_batch_reference_block() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let mut chain = builder.build()?; + chain.prove_next_block()?; + + let proven_tx0 = chain.create_authenticated_notes_proven_tx(account, []).await?; // This batch will reference the latest block with number 1. - let batch0 = generate_batch(&mut chain, vec![proven_tx0.clone()]); + let batch0 = chain.create_batch(vec![proven_tx0.clone()])?; let batches = vec![batch0.clone()]; let block2 = chain.prove_next_block()?; let (_, partial_blockchain) = - chain.latest_selective_partial_blockchain([BlockNumber::from(0)]).unwrap(); + chain.latest_selective_partial_blockchain([BlockNumber::GENESIS])?; // The proposed block references block 2 but the partial blockchain only contains block 0 but // not block 1 which is referenced by the batch. @@ -238,15 +269,15 @@ fn proposed_block_fails_on_missing_batch_reference_block() -> anyhow::Result<()> } /// Tests that duplicate input notes across batches produce an error. -#[test] -fn proposed_block_fails_on_duplicate_input_note() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); +#[tokio::test] +async fn proposed_block_fails_on_duplicate_input_note() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = builder.add_p2any_note(account0.id(), NoteType::Public, [])?; + let note1 = builder.add_p2any_note(account0.id(), NoteType::Public, [])?; + let mut chain = builder.build()?; - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - let note0 = generate_tracked_note(&mut chain, account0.id(), account1.id()); - let note1 = generate_tracked_note(&mut chain, account0.id(), account1.id()); // These notes should have different IDs. assert_ne!(note0.id(), note1.id()); @@ -254,12 +285,13 @@ fn proposed_block_fails_on_duplicate_input_note() -> anyhow::Result<()> { chain.prove_next_block()?; // Create two different transactions against the same account consuming the same note. - let tx0 = - generate_tx_with_authenticated_notes(&mut chain, account1.id(), &[note0.id(), note1.id()]); - let tx1 = generate_tx_with_authenticated_notes(&mut chain, account1.id(), &[note0.id()]); + let tx0 = chain + .create_authenticated_notes_proven_tx(account1.id(), [note0.id(), note1.id()]) + .await?; + let tx1 = chain.create_authenticated_notes_proven_tx(account1.id(), [note0.id()]).await?; - let batch0 = generate_batch(&mut chain, vec![tx0]); - let batch1 = generate_batch(&mut chain, vec![tx1]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx1])?; let batches = vec![batch0.clone(), batch1.clone()]; @@ -272,32 +304,29 @@ fn proposed_block_fails_on_duplicate_input_note() -> anyhow::Result<()> { } /// Tests that duplicate output notes across batches produce an error. -#[test] -fn proposed_block_fails_on_duplicate_output_note() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(1); - let account = accounts.remove(&0).unwrap(); - - let output_note = generate_output_note(account.id(), [10; 32]); +#[tokio::test] +async fn proposed_block_fails_on_duplicate_output_note() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let output_note = create_p2any_note(account.id(), NoteType::Private, [], builder.rng_mut()); // Create two different notes that will create the same output note. Their IDs will be different // due to having a different serial number generated from contained RNG. - let note0 = create_spawn_note(account.id(), vec![&output_note])?; - let note1 = create_spawn_note(account.id(), vec![&output_note])?; - - chain.add_pending_note(OutputNote::Full(note0.clone())); - chain.add_pending_note(OutputNote::Full(note1.clone())); + let note0 = builder.add_spawn_note([&output_note])?; + let note1 = builder.add_spawn_note([&output_note])?; + let mut chain = builder.build()?; chain.prove_next_block()?; // Create two different transactions against the same account creating the same note. // We use the same account because the sender of the created output note is set to the account // of the transaction, so it is essential we use the same account to produce a duplicate output // note. - let tx0 = generate_tx_with_authenticated_notes(&mut chain, account.id(), &[note0.id()]); - let tx1 = generate_tx_with_authenticated_notes(&mut chain, account.id(), &[note1.id()]); + let tx0 = chain.create_authenticated_notes_proven_tx(account.id(), [note0.id()]).await?; + let tx1 = chain.create_authenticated_notes_proven_tx(account.id(), [note1.id()]).await?; - let batch0 = generate_batch(&mut chain, vec![tx0]); - let batch1 = generate_batch(&mut chain, vec![tx1]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx1])?; let batches = vec![batch0.clone(), batch1.clone()]; @@ -312,25 +341,41 @@ fn proposed_block_fails_on_duplicate_output_note() -> anyhow::Result<()> { /// Tests that a missing note inclusion proof produces an error. /// Also tests that an error is produced if the block that the note inclusion proof references is /// not in the partial blockchain. -#[test] -fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_reference_block() +#[tokio::test] +async fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_reference_block() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - let note0 = generate_untracked_note(account0.id(), account1.id()); + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let p2id_note = create_p2id_note( + account0.id(), + account1.id(), + vec![], + NoteType::Private, + ZERO, + builder.rng_mut(), + )?; + let spawn_note = builder.add_spawn_note([&p2id_note])?; + let mut chain = builder.build()?; // This tx will use block1 as the reference block. - let tx0 = - generate_tx_with_unauthenticated_notes(&mut chain, account1.id(), slice::from_ref(¬e0)); + let tx0 = chain + .create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(&p2id_note)) + .await?; // This batch will use block1 as the reference block. - let batch0 = generate_batch(&mut chain, vec![tx0]); - - // Add the note to the chain so we can retrieve an inclusion proof for it. - chain.add_pending_note(OutputNote::Full(note0.clone())); + // With this setup, the block inputs need to contain a reference to block2 in order to prove + // inclusion of the unauthenticated note. + let batch0 = chain.create_batch(vec![tx0])?; + + // Add the P2ID note to the chain by consuming the SPAWN note. The note will hence be created as + // part of block 2 and the note inclusion proof references that block. + let tx = chain + .build_tx_context(account0.id(), &[spawn_note.id()], &[])? + .build()? + .execute() + .await?; + chain.add_pending_executed_transaction(&tx)?; let block2 = chain.prove_next_block()?; // Seal another block so that the next block will use this one as the reference block and block2 @@ -357,14 +402,19 @@ fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_reference_blo .expect("block2 should have been fetched"); let error = ProposedBlock::new(invalid_block_inputs, batches.clone()).unwrap_err(); - assert_matches!(error, ProposedBlockError::UnauthenticatedInputNoteBlockNotInPartialBlockchain { block_number, note_id } if block_number == block2.header().block_num() && note_id == note0.id()); + assert_matches!(error, ProposedBlockError::UnauthenticatedInputNoteBlockNotInPartialBlockchain { + block_number, note_id + } => { + assert_eq!(block_number, block2.header().block_num()); + assert_eq!(note_id, p2id_note.id()); + }); // Error: Invalid note inclusion proof. // -------------------------------------------------------------------------------------------- let original_note_proof = original_block_inputs .unauthenticated_note_proofs() - .get(¬e0.id()) + .get(&p2id_note.id()) .expect("note proof should have been fetched") .clone(); let mut original_merkle_path = MerklePath::from(original_note_proof.note_path().clone()); @@ -380,28 +430,29 @@ fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_reference_blo let mut invalid_block_inputs = original_block_inputs.clone(); invalid_block_inputs .unauthenticated_note_proofs_mut() - .insert(note0.id(), invalid_note_proof); + .insert(p2id_note.id(), invalid_note_proof); let error = ProposedBlock::new(invalid_block_inputs, batches.clone()).unwrap_err(); - assert_matches!(error, ProposedBlockError::UnauthenticatedNoteAuthenticationFailed { block_num, note_id, .. } if block_num == block2.header().block_num() && note_id == note0.id()); + assert_matches!(error, ProposedBlockError::UnauthenticatedNoteAuthenticationFailed { block_num, note_id, .. } if block_num == block2.header().block_num() && note_id == p2id_note.id()); Ok(()) } /// Tests that a missing note inclusion proof produces an error. -#[test] -fn proposed_block_fails_on_missing_note_inclusion_proof() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - let note0 = generate_tracked_note(&mut chain, account0.id(), account1.id()); +#[tokio::test] +async fn proposed_block_fails_on_missing_note_inclusion_proof() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + // Note that this note is not added to the chain state. + let note0 = create_p2any_note(account0.id(), NoteType::Private, [], builder.rng_mut()); + let chain = builder.build()?; - let tx0 = - generate_tx_with_unauthenticated_notes(&mut chain, account1.id(), slice::from_ref(¬e0)); + let tx0 = chain + .create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(¬e0)) + .await?; - let batch0 = generate_batch(&mut chain, vec![tx0]); + let batch0 = chain.create_batch(vec![tx0])?; let batches = vec![batch0.clone()]; @@ -416,25 +467,22 @@ fn proposed_block_fails_on_missing_note_inclusion_proof() -> anyhow::Result<()> } /// Tests that a missing nullifier witness produces an error. -#[test] -fn proposed_block_fails_on_missing_nullifier_witness() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - let note0 = generate_untracked_note(account0.id(), account1.id()); +#[tokio::test] +async fn proposed_block_fails_on_missing_nullifier_witness() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let p2id_note = + builder.add_p2any_note(account.id(), NoteType::Public, [FungibleAsset::mock(50)])?; + let mut chain = builder.build()?; + chain.prove_next_block()?; // This tx will use block1 as the reference block. - let tx0 = - generate_tx_with_unauthenticated_notes(&mut chain, account1.id(), slice::from_ref(¬e0)); + let tx0 = chain + .create_unauthenticated_notes_proven_tx(account.id(), slice::from_ref(&p2id_note)) + .await?; // This batch will use block1 as the reference block. - let batch0 = generate_batch(&mut chain, vec![tx0]); - - // Add the note to the chain so we can retrieve an inclusion proof for it. - chain.add_pending_note(OutputNote::Full(note0.clone())); - let _block2 = chain.prove_next_block()?; + let batch0 = chain.create_batch(vec![tx0])?; let batches = vec![batch0.clone()]; @@ -446,99 +494,89 @@ fn proposed_block_fails_on_missing_nullifier_witness() -> anyhow::Result<()> { let mut invalid_block_inputs = block_inputs.clone(); invalid_block_inputs .nullifier_witnesses_mut() - .remove(¬e0.nullifier()) + .remove(&p2id_note.nullifier()) .expect("nullifier should have been fetched"); let error = ProposedBlock::new(invalid_block_inputs, batches.clone()).unwrap_err(); - assert_matches!(error, ProposedBlockError::NullifierProofMissing(nullifier) if nullifier == note0.nullifier()); + assert_matches!(error, ProposedBlockError::NullifierProofMissing(nullifier) => { + assert_eq!(nullifier, p2id_note.nullifier()); + }); Ok(()) } /// Tests that a nullifier witness pointing to a spent nullifier produces an error. -#[test] -fn proposed_block_fails_on_spent_nullifier_witness() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - let note0 = generate_untracked_note(account0.id(), account1.id()); - - // This tx will use block1 as the reference block. - let tx0 = - generate_tx_with_unauthenticated_notes(&mut chain, account1.id(), slice::from_ref(¬e0)); - - // This batch will use block1 as the reference block. - let batch0 = generate_batch(&mut chain, vec![tx0]); - - // Add the note to the chain so we can consume it in the next step. - chain.add_pending_note(OutputNote::Full(note0.clone())); - let _block2 = chain.prove_next_block()?; +#[tokio::test] +async fn proposed_block_fails_on_spent_nullifier_witness() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let p2any_note = + builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(50)])?; + let mut chain = builder.build()?; + chain.prove_next_block()?; - // Create an alternative chain where we consume the note so it is marked as spent in the - // nullifier tree. - let mut alternative_chain = chain.clone(); - let transaction = generate_executed_tx_with_authenticated_notes( - &alternative_chain, - account1.id(), - &[note0.id()], - ); - alternative_chain.add_pending_executed_transaction(&transaction)?; - alternative_chain.prove_next_block()?; - let spent_proof = alternative_chain.nullifier_tree().open(¬e0.nullifier()); + // Consume the note with account 0 and add the transaction to a block. + let tx0 = chain + .create_authenticated_notes_proven_tx(account0.id(), [p2any_note.id()]) + .await?; + chain.add_pending_proven_transaction(tx0); + chain.prove_next_block()?; - let batches = vec![batch0.clone()]; - let mut block_inputs = chain.get_block_inputs(&batches)?; + // Consume the (already consumed) note with account 1 and build a batch from it. + let tx1 = chain + .create_authenticated_notes_proven_tx(account1.id(), [p2any_note.id()]) + .await?; + let batch1 = chain.create_batch(vec![tx1])?; + let batches = vec![batch1]; + let block_inputs = chain.get_block_inputs(&batches)?; - // Insert the spent nullifier proof from the alternative chain into the block inputs from the - // actual chain. - block_inputs.nullifier_witnesses_mut().insert(note0.nullifier(), spent_proof); + // The block inputs should contain a nullifier witness for the P2ANY note. + assert!(block_inputs.nullifier_witnesses().contains_key(&p2any_note.nullifier())); let error = ProposedBlock::new(block_inputs, batches).unwrap_err(); - assert_matches!(error, ProposedBlockError::NullifierSpent(nullifier) if nullifier == note0.nullifier()); + assert_matches!(error, ProposedBlockError::NullifierSpent(nullifier) => { + assert_eq!(nullifier, p2any_note.nullifier()) + }); Ok(()) } /// Tests that multiple transactions against the same account that start from the same initial state /// commitment but produce different final state commitments produce an error. -/// We test this simply by putting the same transaction in different batches and ensuring that the -/// batch IDs will be unique to avoid triggering the duplicate batches check. -#[test] -fn proposed_block_fails_on_conflicting_transactions_updating_same_account() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, mut txs, .. } = setup_chain(2); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let random_tx = txs.remove(&0).unwrap(); - - let note0 = generate_tracked_note(&mut chain, account0.id(), account1.id()); - let note1 = generate_tracked_note(&mut chain, account0.id(), account1.id()); +#[tokio::test] +async fn proposed_block_fails_on_conflicting_transactions_updating_same_account() +-> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = + builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(100)])?; + let note1 = + builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(200)])?; + let chain = builder.build()?; + // These notes should have different IDs. assert_ne!(note0.id(), note1.id()); - // Add notes to the chain. - chain.prove_next_block()?; - - // Create two different transactions against the same account consuming the same note. - let tx0 = generate_tx_with_authenticated_notes(&mut chain, account1.id(), &[]); + // Create two different transactions against the same account consuming a different note so they + // result in a different final state commitment for the account. + let tx0 = chain.create_authenticated_notes_proven_tx(account1.id(), [note0.id()]).await?; + let tx1 = chain.create_authenticated_notes_proven_tx(account1.id(), [note1.id()]).await?; - // Add a random tx to batch0 to make it unique. - let batch0 = generate_batch(&mut chain, vec![tx0.clone(), random_tx]); - let batch1 = generate_batch(&mut chain, vec![tx0]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx1])?; let batches = vec![batch0.clone(), batch1.clone()]; - let block_inputs = chain.get_block_inputs(&batches).expect("failed to get block inputs"); - let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err(); + let error = ProposedBlock::new(block_inputs.clone(), batches).unwrap_err(); assert_matches!(error, ProposedBlockError::ConflictingBatchesUpdateSameAccount { account_id, initial_state_commitment, first_batch_id, second_batch_id } if account_id == account1.id() && - initial_state_commitment == account1.init_commitment() && + initial_state_commitment == account1.initial_commitment() && first_batch_id == batch0.id() && second_batch_id == batch1.id() ); @@ -547,13 +585,14 @@ fn proposed_block_fails_on_conflicting_transactions_updating_same_account() -> a } /// Tests that a missing account witness produces an error. -#[test] -fn proposed_block_fails_on_missing_account_witness() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, mut txs, .. } = setup_chain(2); - let account0 = accounts.remove(&0).unwrap(); - let tx0 = txs.remove(&0).unwrap(); +#[tokio::test] +async fn proposed_block_fails_on_missing_account_witness() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let chain = builder.build()?; + let tx0 = chain.create_authenticated_notes_proven_tx(account.id(), []).await?; - let batch0 = generate_batch(&mut chain, vec![tx0]); + let batch0 = chain.create_batch(vec![tx0])?; let batches = vec![batch0.clone()]; @@ -562,54 +601,46 @@ fn proposed_block_fails_on_missing_account_witness() -> anyhow::Result<()> { let mut block_inputs = chain.get_block_inputs(&batches)?; block_inputs .account_witnesses_mut() - .remove(&account0.id()) + .remove(&account.id()) .expect("account witness should have been fetched"); let error = ProposedBlock::new(block_inputs, batches.clone()).unwrap_err(); - assert_matches!(error, ProposedBlockError::MissingAccountWitness(account_id) if account_id == account0.id()); + assert_matches!(error, ProposedBlockError::MissingAccountWitness(account_id) if account_id == account.id()); Ok(()) } /// Tests that, given three transactions 0 -> 1 -> 2 which are executed against the same account and /// build on top of each other produce an error when tx 1 is missing from the block. -#[test] -fn proposed_block_fails_on_inconsistent_account_state_transition() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - let asset = generate_fungible_asset( - 100, - AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(), - ); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - let note0 = generate_tracked_note_with_asset(&mut chain, account0.id(), account1.id(), asset); - let note1 = generate_tracked_note_with_asset(&mut chain, account0.id(), account1.id(), asset); - let note2 = generate_tracked_note_with_asset(&mut chain, account0.id(), account1.id(), asset); +#[tokio::test] +async fn proposed_block_fails_on_inconsistent_account_state_transition() -> anyhow::Result<()> { + let asset = FungibleAsset::mock(200); - // Add notes to the chain. - chain.prove_next_block()?; + let mut builder = MockChain::builder(); + let mut account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = builder.add_p2any_note(account.id(), NoteType::Public, [asset])?; + let note1 = builder.add_p2any_note(account.id(), NoteType::Public, [asset])?; + let note2 = builder.add_p2any_note(account.id(), NoteType::Public, [asset])?; + let chain = builder.build()?; // Create three transactions on the same account that build on top of each other. - let executed_tx0 = - generate_executed_tx_with_authenticated_notes(&chain, account1.clone(), &[note0.id()]); + let executed_tx0 = chain.create_authenticated_notes_tx(account.clone(), [note0.id()]).await?; + account.apply_delta(executed_tx0.account_delta())?; // Builds a tx on top of the account state from tx0. - let executed_tx1 = - generate_executed_tx_with_authenticated_notes(&chain, executed_tx0.clone(), &[note1.id()]); + let executed_tx1 = chain.create_authenticated_notes_tx(account.clone(), [note1.id()]).await?; + account.apply_delta(executed_tx1.account_delta())?; // Builds a tx on top of the account state from tx1. - let executed_tx2 = - generate_executed_tx_with_authenticated_notes(&chain, executed_tx1.clone(), &[note2.id()]); + let executed_tx2 = chain.create_authenticated_notes_tx(account.clone(), [note2.id()]).await?; // We will only include tx0 and tx2 and leave out tx1, which will trigger the error condition // that there is no transition from tx0 -> tx2. - let tx0 = ProvenTransaction::from_executed_transaction_mocked(executed_tx0.clone()); - let tx2 = ProvenTransaction::from_executed_transaction_mocked(executed_tx2.clone()); + let tx0 = LocalTransactionProver::default().prove_dummy(executed_tx0.clone())?; + let tx2 = LocalTransactionProver::default().prove_dummy(executed_tx2.clone())?; - let batch0 = generate_batch(&mut chain, vec![tx0]); - let batch1 = generate_batch(&mut chain, vec![tx2]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx2])?; let batches = vec![batch0.clone(), batch1.clone()]; let block_inputs = chain.get_block_inputs(&batches)?; @@ -619,7 +650,7 @@ fn proposed_block_fails_on_inconsistent_account_state_transition() -> anyhow::Re account_id, state_commitment, remaining_state_commitments - } if account_id == account1.id() && + } if account_id == account.id() && state_commitment == executed_tx0.final_account().commitment() && remaining_state_commitments == [executed_tx2.initial_account().commitment()] ); 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 69defbc89c..e9ab238fe9 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 @@ -5,31 +5,26 @@ 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::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; -use miden_objects::transaction::{OutputNote, ProvenTransaction, TransactionHeader}; +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_tx::LocalTransactionProver; use rand::Rng; -use super::utils::{ - TestSetup, - generate_batch, - generate_executed_tx_with_authenticated_notes, - generate_fungible_asset, - generate_tracked_note_with_asset, - generate_tx_with_expiration, - generate_tx_with_unauthenticated_notes, - generate_untracked_note, - setup_chain, -}; -use crate::kernel_tests::block::utils::generate_conditional_tx; -use crate::{AccountState, Auth, MockChain, ProvenTransactionExt}; +use super::utils::MockChainBlockExt; +use crate::{AccountState, Auth, MockChain, TxContextInput}; /// Tests that we can build empty blocks. -#[test] -fn proposed_block_succeeds_with_empty_batches() -> anyhow::Result<()> { - let TestSetup { chain, .. } = setup_chain(2); +#[tokio::test] +async fn proposed_block_succeeds_with_empty_batches() -> anyhow::Result<()> { + let mut chain = MockChain::builder().build()?; + chain.prove_next_block()?; let block_inputs = BlockInputs::new( chain.latest_block_header(), @@ -50,16 +45,24 @@ fn proposed_block_succeeds_with_empty_batches() -> anyhow::Result<()> { /// Tests that a proposed block from two batches with one transaction each can be successfully /// built. -#[test] -fn proposed_block_basic_success() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, mut txs, .. } = setup_chain(2); - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let proven_tx0 = txs.remove(&0).unwrap(); - let proven_tx1 = txs.remove(&1).unwrap(); - - let batch0 = generate_batch(&mut chain, vec![proven_tx0.clone()]); - let batch1 = generate_batch(&mut chain, vec![proven_tx1.clone()]); +#[tokio::test] +async fn proposed_block_basic_success() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = + builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(42)])?; + let note1 = + builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(42)])?; + let chain = builder.build()?; + + let proven_tx0 = + chain.create_authenticated_notes_proven_tx(account0.id(), [note0.id()]).await?; + let proven_tx1 = + chain.create_authenticated_notes_proven_tx(account1.id(), [note1.id()]).await?; + + let batch0 = chain.create_batch(vec![proven_tx0.clone()])?; + let batch1 = chain.create_batch(vec![proven_tx1.clone()])?; let batches = [batch0, batch1]; let block_inputs = chain.get_block_inputs(&batches)?; @@ -109,44 +112,39 @@ fn proposed_block_basic_success() -> anyhow::Result<()> { } /// Tests that account updates are correctly aggregated into a block-level account update. -#[test] -fn proposed_block_aggregates_account_state_transition() -> anyhow::Result<()> { - // We need authentication because we're modifying accounts with the input notes. - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - let asset = generate_fungible_asset( - 100, - AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(), - ); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); +#[tokio::test] +async fn proposed_block_aggregates_account_state_transition() -> anyhow::Result<()> { + let asset = FungibleAsset::mock(100); + let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER)?; - let note0 = generate_tracked_note_with_asset(&mut chain, account0.id(), account1.id(), asset); - let note1 = generate_tracked_note_with_asset(&mut chain, account0.id(), account1.id(), asset); - let note2 = generate_tracked_note_with_asset(&mut chain, account0.id(), account1.id(), asset); + let mut builder = MockChain::builder(); + let mut account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = builder.add_p2id_note(sender_id, account1.id(), &[asset], NoteType::Private)?; + let note1 = builder.add_p2id_note(sender_id, account1.id(), &[asset], NoteType::Public)?; + let note2 = builder.add_p2id_note(sender_id, account1.id(), &[asset], NoteType::Public)?; + let mut chain = builder.build()?; // Add notes to the chain. chain.prove_next_block()?; // Create three transactions on the same account that build on top of each other. - let executed_tx0 = - generate_executed_tx_with_authenticated_notes(&chain, account1.id(), &[note0.id()]); + let executed_tx0 = chain.create_authenticated_notes_tx(account1.id(), [note0.id()]).await?; - let executed_tx1 = - generate_executed_tx_with_authenticated_notes(&chain, executed_tx0.clone(), &[note1.id()]); + account1.apply_delta(executed_tx0.account_delta())?; + let executed_tx1 = chain.create_authenticated_notes_tx(account1.clone(), [note1.id()]).await?; - let executed_tx2 = - generate_executed_tx_with_authenticated_notes(&chain, executed_tx1.clone(), &[note2.id()]); + account1.apply_delta(executed_tx1.account_delta())?; + let executed_tx2 = chain.create_authenticated_notes_tx(account1.clone(), [note2.id()]).await?; let [tx0, tx1, tx2] = [executed_tx0, executed_tx1, executed_tx2] .into_iter() - .map(ProvenTransaction::from_executed_transaction_mocked) + .map(|tx| LocalTransactionProver::default().prove_dummy(tx).unwrap()) .collect::>() .try_into() .expect("we should have provided three executed txs"); - let batch0 = generate_batch(&mut chain, vec![tx2.clone()]); - let batch1 = generate_batch(&mut chain, vec![tx0.clone(), tx1.clone()]); + let batch0 = chain.create_batch(vec![tx2.clone()])?; + let batch1 = chain.create_batch(vec![tx0.clone(), tx1.clone()])?; let batches = vec![batch0.clone(), batch1.clone()]; let block_inputs = chain.get_block_inputs(&batches).unwrap(); @@ -180,29 +178,28 @@ fn proposed_block_aggregates_account_state_transition() -> anyhow::Result<()> { } /// Tests that unauthenticated notes can be authenticated when inclusion proofs are provided. -#[test] -fn proposed_block_authenticating_unauthenticated_notes() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(3); - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let account2 = accounts.remove(&2).unwrap(); +#[tokio::test] +async fn proposed_block_authenticating_unauthenticated_notes() -> anyhow::Result<()> { + let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER)?; - let note0 = generate_untracked_note(account0.id(), account1.id()); - let note1 = generate_untracked_note(account0.id(), account2.id()); + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = builder.add_p2id_note(sender_id, account0.id(), &[], NoteType::Private)?; + let note1 = builder.add_p2id_note(sender_id, account1.id(), &[], NoteType::Public)?; + let chain = builder.build()?; // These txs will use block1 as the reference block. - let tx0 = - generate_tx_with_unauthenticated_notes(&mut chain, account1.id(), slice::from_ref(¬e0)); - let tx1 = - generate_tx_with_unauthenticated_notes(&mut chain, account2.id(), slice::from_ref(¬e1)); + let tx0 = chain + .create_unauthenticated_notes_proven_tx(account0.id(), slice::from_ref(¬e0)) + .await?; + let tx1 = chain + .create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(¬e1)) + .await?; // These batches will use block1 as the reference block. - let batch0 = generate_batch(&mut chain, vec![tx0.clone()]); - let batch1 = generate_batch(&mut chain, vec![tx1.clone()]); - - chain.add_pending_note(OutputNote::Full(note0.clone())); - chain.add_pending_note(OutputNote::Full(note1.clone())); - chain.prove_next_block()?; + let batch0 = chain.create_batch(vec![tx0.clone()])?; + let batch1 = chain.create_batch(vec![tx1.clone()])?; let batches = [batch0, batch1]; // This block will use block2 as the reference block. @@ -231,18 +228,21 @@ fn proposed_block_authenticating_unauthenticated_notes() -> anyhow::Result<()> { } /// Tests that a batch that expires at the block being proposed is still accepted. -#[test] -fn proposed_block_with_batch_at_expiration_limit() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); +#[tokio::test] +async fn proposed_block_with_batch_at_expiration_limit() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let mut chain = builder.build()?; + + chain.prove_next_block()?; let block1_num = chain.block_header(1).block_num(); - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let tx0 = generate_tx_with_expiration(&mut chain, account0.id(), block1_num + 5); - let tx1 = generate_tx_with_expiration(&mut chain, account1.id(), block1_num + 2); + let tx0 = chain.create_expiring_proven_tx(account0.id(), block1_num + 5).await?; + let tx1 = chain.create_expiring_proven_tx(account1.id(), block1_num + 2).await?; - let batch0 = generate_batch(&mut chain, vec![tx0]); - let batch1 = generate_batch(&mut chain, vec![tx1]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx1])?; // sanity check: batch 1 should expire at block 3. assert_eq!(batch1.batch_expiration_block_num().as_u32(), 3); @@ -262,24 +262,31 @@ fn proposed_block_with_batch_at_expiration_limit() -> anyhow::Result<()> { /// Tests that a NOOP transaction with state commitments X -> X against account A can appear /// in one batch while another batch contains a state-updating transaction with state commitments X /// -> Y against the same account A. Both batches are in the same block. -#[test] -fn noop_tx_and_state_updating_tx_against_same_account_in_same_block() -> anyhow::Result<()> { +#[tokio::test] +async fn noop_tx_and_state_updating_tx_against_same_account_in_same_block() -> anyhow::Result<()> { let account_builder = Account::builder(rand::rng().random()) .storage_mode(AccountStorageMode::Public) .with_component(MockAccountComponent::with_empty_slots()); let mut builder = MockChain::builder(); - - let account0 = builder.add_account_from_builder( + let mut account0 = builder.add_account_from_builder( Auth::Conditional, account_builder, AccountState::Exists, )?; + let noop_note0 = + NoteBuilder::new(ACCOUNT_ID_SENDER.try_into().unwrap(), &mut rand::rng()).build()?; + let noop_note1 = + NoteBuilder::new(ACCOUNT_ID_SENDER.try_into().unwrap(), &mut rand::rng()).build()?; + builder.add_output_note(OutputNote::Full(noop_note0.clone())); + builder.add_output_note(OutputNote::Full(noop_note1.clone())); let mut chain = builder.build()?; - let noop_tx = generate_conditional_tx(&mut chain, account0.id(), false); - let state_updating_tx = generate_conditional_tx(&mut chain, noop_tx.clone(), true); + let noop_tx = generate_conditional_tx(&mut chain, account0.id(), noop_note0, false).await; + account0.apply_delta(noop_tx.account_delta())?; + let state_updating_tx = + generate_conditional_tx(&mut chain, account0.clone(), noop_note1, true).await; // sanity check: NOOP transaction's init and final commitment should be the same. assert_eq!(noop_tx.initial_account().commitment(), noop_tx.final_account().commitment()); @@ -290,11 +297,11 @@ fn noop_tx_and_state_updating_tx_against_same_account_in_same_block() -> anyhow: state_updating_tx.final_account().commitment() ); - let tx0 = ProvenTransaction::from_executed_transaction_mocked(noop_tx); - let tx1 = ProvenTransaction::from_executed_transaction_mocked(state_updating_tx); + let tx0 = LocalTransactionProver::default().prove_dummy(noop_tx)?; + let tx1 = LocalTransactionProver::default().prove_dummy(state_updating_tx)?; - let batch0 = generate_batch(&mut chain, vec![tx0]); - let batch1 = generate_batch(&mut chain, vec![tx1.clone()]); + let batch0 = chain.create_batch(vec![tx0])?; + let batch1 = chain.create_batch(vec![tx1.clone()])?; let batches = vec![batch0.clone(), batch1.clone()]; @@ -307,3 +314,34 @@ fn noop_tx_and_state_updating_tx_against_same_account_in_same_block() -> anyhow: Ok(()) } + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Generates a transaction, which depending on the `modify_storage` flag, does the following: +/// - if `modify_storage` is true, it increments the storage item of the account. +/// - if `modify_storage` is false, it does nothing (NOOP). +/// +/// To make this transaction (always) non-empty, it consumes one "noop note", which does nothing. +async fn generate_conditional_tx( + chain: &mut MockChain, + input: impl Into, + noop_note: Note, + modify_storage: bool, +) -> ExecutedTransaction { + let auth_args = [ + // increment nonce if modify_storage is true + if modify_storage { Felt::ONE } else { Felt::ZERO }, + Felt::new(99), + Felt::new(98), + Felt::new(97), + ]; + + let tx_context = chain + .build_tx_context(input.into(), &[noop_note.id()], &[]) + .unwrap() + .auth_args(auth_args.into()) + .build() + .unwrap(); + tx_context.execute().await.unwrap() +} diff --git a/crates/miden-testing/src/kernel_tests/block/proven_block_error.rs b/crates/miden-testing/src/kernel_tests/block/proven_block_error.rs index 6525384022..f50937fdbb 100644 --- a/crates/miden-testing/src/kernel_tests/block/proven_block_error.rs +++ b/crates/miden-testing/src/kernel_tests/block/proven_block_error.rs @@ -10,19 +10,14 @@ use miden_objects::account::{Account, AccountBuilder, AccountComponent, AccountI use miden_objects::asset::FungibleAsset; use miden_objects::batch::ProvenBatch; use miden_objects::block::{BlockInputs, BlockNumber, ProposedBlock}; -use miden_objects::transaction::{ProvenTransaction, ProvenTransactionBuilder}; +use miden_objects::note::NoteType; +use miden_objects::transaction::ProvenTransactionBuilder; use miden_objects::vm::ExecutionProof; use miden_objects::{AccountTreeError, NullifierTreeError, Word}; -use winterfell::Proof; +use miden_tx::LocalTransactionProver; -use super::utils::{ - TestSetup, - generate_batch, - generate_executed_tx_with_authenticated_notes, - generate_tracked_note, - setup_chain, -}; -use crate::{Auth, MockChain, ProvenTransactionExt, TransactionContextBuilder}; +use crate::kernel_tests::block::utils::MockChainBlockExt; +use crate::{Auth, MockChain, TransactionContextBuilder}; struct WitnessTestSetup { stale_block_inputs: BlockInputs, @@ -33,21 +28,27 @@ struct WitnessTestSetup { /// Setup for a test which returns two inputs for the same block. The valid inputs match the /// commitments of the latest block and the stale inputs match the commitments of the latest block /// minus 1. -fn witness_test_setup() -> WitnessTestSetup { - let TestSetup { mut chain, mut accounts, mut txs, .. } = setup_chain(4); +async fn witness_test_setup() -> anyhow::Result { + let mut builder = MockChain::builder(); - let account0 = accounts.remove(&0).context("failed to remove account 0").unwrap(); - let account1 = accounts.remove(&1).context("failed to remove account 1").unwrap(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account2 = builder.add_existing_mock_account(Auth::IncrNonce)?; - let note = generate_tracked_note(&mut chain, account1.id(), account0.id()); - // Add note to chain. - chain.prove_next_block().unwrap(); + let note0 = + builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(100)])?; + let note1 = + builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(100)])?; + let note2 = + builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(100)])?; + + let mut chain = builder.build()?; - let tx0 = generate_executed_tx_with_authenticated_notes(&chain, account0.id(), &[note.id()]); - let tx1 = txs.remove(&1).context("failed to remove tx 1").unwrap(); - let tx2 = txs.remove(&2).context("failed to remove tx 2").unwrap(); + let tx0 = chain.create_authenticated_notes_proven_tx(account0.id(), [note0.id()]).await?; + let tx1 = chain.create_authenticated_notes_proven_tx(account1.id(), [note1.id()]).await?; + let tx2 = chain.create_authenticated_notes_proven_tx(account2.id(), [note2.id()]).await?; - let batch1 = generate_batch(&mut chain, vec![tx1, tx2]); + let batch1 = chain.create_batch(vec![tx1, tx2])?; let batches = vec![batch1]; let stale_block_inputs = chain.get_block_inputs(&batches).unwrap(); @@ -55,7 +56,7 @@ fn witness_test_setup() -> WitnessTestSetup { let nullifier_root0 = chain.nullifier_tree().root(); // Apply the executed tx and seal a block. This invalidates the block inputs we've just fetched. - chain.add_pending_executed_transaction(&tx0).unwrap(); + chain.add_pending_proven_transaction(tx0); chain.prove_next_block().unwrap(); let valid_block_inputs = chain.get_block_inputs(&batches).unwrap(); @@ -65,17 +66,17 @@ fn witness_test_setup() -> WitnessTestSetup { assert_ne!(chain.account_tree().root(), account_root0); assert_ne!(chain.nullifier_tree().root(), nullifier_root0); - WitnessTestSetup { + Ok(WitnessTestSetup { stale_block_inputs, valid_block_inputs, batches, - } + }) } /// Tests that a proven 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). -#[test] -fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -83,7 +84,7 @@ fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { stale_block_inputs, valid_block_inputs, batches, - } = witness_test_setup(); + } = witness_test_setup().await?; // Account tree root mismatch. // -------------------------------------------------------------------------------------------- @@ -96,9 +97,7 @@ 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_without_batch_verification(proposed_block0) - .unwrap_err(); + let error = LocalBlockProver::new(0).prove_dummy(proposed_block0).unwrap_err(); assert_matches!( error, @@ -113,8 +112,8 @@ fn proven_block_fails_on_stale_account_witnesses() -> anyhow::Result<()> { /// Tests that a proven 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). -#[test] -fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -122,7 +121,7 @@ fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> { stale_block_inputs, valid_block_inputs, batches, - } = witness_test_setup(); + } = witness_test_setup().await?; // Nullifier tree root mismatch. // -------------------------------------------------------------------------------------------- @@ -135,9 +134,7 @@ 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_without_batch_verification(proposed_block2) - .unwrap_err(); + let error = LocalBlockProver::new(0).prove_dummy(proposed_block2).unwrap_err(); assert_matches!( error, @@ -152,8 +149,8 @@ fn proven_block_fails_on_stale_nullifier_witnesses() -> anyhow::Result<()> { /// Tests that a proven 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. -#[test] -fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -161,7 +158,7 @@ fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> { mut stale_block_inputs, valid_block_inputs, batches, - } = witness_test_setup(); + } = witness_test_setup().await?; // Stale and current account witnesses used together. // -------------------------------------------------------------------------------------------- @@ -183,9 +180,7 @@ 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_without_batch_verification(proposed_block1) - .unwrap_err(); + let error = LocalBlockProver::new(0).prove_dummy(proposed_block1).unwrap_err(); assert_matches!( error, @@ -200,8 +195,8 @@ fn proven_block_fails_on_account_tree_root_mismatch() -> anyhow::Result<()> { /// Tests that a proven 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. -#[test] -fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> { // Setup test with stale and valid block inputs. // -------------------------------------------------------------------------------------------- @@ -209,7 +204,7 @@ fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> { mut stale_block_inputs, valid_block_inputs, batches, - } = witness_test_setup(); + } = witness_test_setup().await?; // Stale and current nullifier witnesses used together. // -------------------------------------------------------------------------------------------- @@ -233,9 +228,7 @@ 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_without_batch_verification(proposed_block3) - .unwrap_err(); + let error = LocalBlockProver::new(0).prove_dummy(proposed_block3).unwrap_err(); assert_matches!( error, @@ -247,8 +240,9 @@ 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. -#[test] -fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() +-> anyhow::Result<()> { // Construct a new account. // -------------------------------------------------------------------------------------------- @@ -256,7 +250,7 @@ fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() -> a let auth_component: AccountComponent = IncrNonceAuthComponent.into(); - let (account, seed) = AccountBuilder::new([5; 32]) + let account = AccountBuilder::new([5; 32]) .with_auth_component(auth_component.clone()) .with_component(MockAccountComponent::with_slots(vec![StorageSlot::Value(Word::from( [5u32; 4], @@ -286,24 +280,21 @@ fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() -> a existing_id.suffix(), "test should work if suffixes are different, so we want to ensure it" ); - assert_eq!(account.init_commitment(), Word::empty()); + assert_eq!(account.initial_commitment(), Word::empty()); let existing_account = Account::mock(existing_id.into(), auth_component); builder.add_account(existing_account.clone())?; - let mut mock_chain = builder.build()?; + let mock_chain = builder.build()?; // Execute the account-creating transaction. // -------------------------------------------------------------------------------------------- - let tx_inputs = mock_chain.get_transaction_inputs(account.clone(), Some(seed), &[], &[])?; - let tx_context = TransactionContextBuilder::new(account) - .account_seed(Some(seed)) - .tx_inputs(tx_inputs) - .build()?; - let tx = tx_context.execute_blocking().context("failed to execute account creating tx")?; - let tx = ProvenTransaction::from_executed_transaction_mocked(tx); + let tx_inputs = mock_chain.get_transaction_inputs(&account, &[], &[])?; + let tx_context = TransactionContextBuilder::new(account).tx_inputs(tx_inputs).build()?; + let tx = tx_context.execute().await.context("failed to execute account creating tx")?; + let tx = LocalTransactionProver::default().prove_dummy(tx)?; - let batch = generate_batch(&mut mock_chain, vec![tx]); + let batch = mock_chain.create_batch(vec![tx])?; let batches = [batch]; let block_inputs = mock_chain.get_block_inputs(batches.iter())?; @@ -330,7 +321,7 @@ fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() -> a let block = mock_chain.propose_block(batches).context("failed to propose block")?; - let err = LocalBlockProver::new(0).prove_without_batch_verification(block).unwrap_err(); + let err = LocalBlockProver::new(0).prove_dummy(block).unwrap_err(); // This should fail when we try to _insert_ the same two prefixes into the partial tree. assert_matches!( @@ -344,12 +335,13 @@ fn proven_block_fails_on_creating_account_with_existing_account_id_prefix() -> a } /// Tests that creating two accounts in the same block whose ID prefixes match, results in an error. -#[test] -fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix() +-> anyhow::Result<()> { // Construct a new account. // -------------------------------------------------------------------------------------------- - let mut mock_chain = MockChain::new(); - let (account, _) = AccountBuilder::new([5; 32]) + 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], @@ -391,7 +383,7 @@ fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix() -> genesis_block.commitment(), FungibleAsset::mock(500).unwrap_fungible(), BlockNumber::from(u32::MAX), - ExecutionProof::new(Proof::new_dummy(), Default::default()), + ExecutionProof::new_dummy(), ) .account_update_details(AccountUpdateDetails::Private) .build() @@ -402,7 +394,7 @@ fn proven_block_fails_on_creating_account_with_duplicate_account_id_prefix() -> // Build a batch from these transactions and attempt to prove a block. // -------------------------------------------------------------------------------------------- - let batch = generate_batch(&mut mock_chain, vec![tx0, tx1]); + let batch = mock_chain.create_batch(vec![tx0, tx1])?; let batches = [batch]; // Sanity check: The block inputs should contain two account witnesses that point to the same @@ -425,7 +417,7 @@ 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_without_batch_verification(block).unwrap_err(); + let err = LocalBlockProver::new(0).prove_dummy(block).unwrap_err(); // This should fail when we try to _track_ the same two prefixes in the partial tree. assert_matches!( 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 7d638abef8..ab324b942c 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 @@ -4,70 +4,92 @@ use std::vec::Vec; use anyhow::Context; use miden_block_prover::LocalBlockProver; -use miden_objects::MIN_PROOF_SECURITY_LEVEL; +use miden_lib::note::create_p2id_note; +use miden_objects::asset::FungibleAsset; use miden_objects::batch::BatchNoteTree; -use miden_objects::block::{ - AccountTree, - BlockInputs, - BlockNoteIndex, - BlockNoteTree, - ProposedBlock, -}; +use miden_objects::block::account_tree::AccountTree; +use miden_objects::block::{BlockInputs, BlockNoteIndex, BlockNoteTree, ProposedBlock}; use miden_objects::crypto::merkle::Smt; -use miden_objects::transaction::{InputNoteCommitment, OutputNote}; -use rand::Rng; - -use super::utils::{ - TestSetup, - generate_batch, - generate_executed_tx_with_authenticated_notes, - generate_output_note, - generate_tracked_note, - generate_tx_with_authenticated_notes, - generate_tx_with_unauthenticated_notes, - setup_chain, -}; -use crate::utils::create_spawn_note; +use miden_objects::note::NoteType; +use miden_objects::transaction::InputNoteCommitment; +use miden_objects::{MIN_PROOF_SECURITY_LEVEL, ZERO}; + +use crate::kernel_tests::block::utils::MockChainBlockExt; +use crate::utils::create_p2any_note; +use crate::{Auth, MockChain}; /// Tests the outputs of a proven block with transactions that consume notes, create output notes /// and modify the account's state. -#[test] -fn proven_block_success() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_success() -> anyhow::Result<()> { // Setup test with notes that produce output notes, in order to test the block note tree root // computation. // -------------------------------------------------------------------------------------------- - let TestSetup { mut chain, mut accounts, .. } = setup_chain(4); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let account2 = accounts.remove(&2).unwrap(); - let account3 = accounts.remove(&3).unwrap(); - - let output_note0 = generate_output_note(account0.id(), [0; 32]); - let output_note1 = generate_output_note(account1.id(), [1; 32]); - let output_note2 = generate_output_note(account2.id(), [2; 32]); - let output_note3 = generate_output_note(account3.id(), [3; 32]); - - let input_note0 = create_spawn_note(account0.id(), vec![&output_note0])?; - let input_note1 = create_spawn_note(account1.id(), vec![&output_note1])?; - let input_note2 = create_spawn_note(account2.id(), vec![&output_note2])?; - let input_note3 = create_spawn_note(account3.id(), vec![&output_note3])?; - - // Add input notes to chain so we can consume them. - chain.add_pending_note(OutputNote::Full(input_note0.clone())); - chain.add_pending_note(OutputNote::Full(input_note1.clone())); - chain.add_pending_note(OutputNote::Full(input_note2.clone())); - chain.add_pending_note(OutputNote::Full(input_note3.clone())); + let asset = FungibleAsset::mock(100); + let mut builder = MockChain::builder(); + + let account0 = builder.add_existing_mock_account_with_assets(Auth::IncrNonce, [asset])?; + let account1 = builder.add_existing_mock_account_with_assets(Auth::IncrNonce, [asset])?; + let account2 = builder.add_existing_mock_account_with_assets(Auth::IncrNonce, [asset])?; + let account3 = builder.add_existing_mock_account_with_assets(Auth::IncrNonce, [asset])?; + + let output_note0 = create_p2id_note( + account0.id(), + account0.id(), + vec![asset], + NoteType::Private, + ZERO, + builder.rng_mut(), + )?; + let output_note1 = create_p2id_note( + account1.id(), + account1.id(), + vec![asset], + NoteType::Private, + ZERO, + builder.rng_mut(), + )?; + let output_note2 = create_p2id_note( + account2.id(), + account2.id(), + vec![asset], + NoteType::Private, + ZERO, + builder.rng_mut(), + )?; + let output_note3 = create_p2id_note( + account3.id(), + account3.id(), + vec![asset], + NoteType::Private, + ZERO, + builder.rng_mut(), + )?; + + let input_note0 = builder.add_spawn_note([&output_note0])?; + let input_note1 = builder.add_spawn_note([&output_note1])?; + let input_note2 = builder.add_spawn_note([&output_note2])?; + let input_note3 = builder.add_spawn_note([&output_note3])?; + + let mut chain = builder.build()?; chain.prove_next_block()?; - let tx0 = generate_tx_with_authenticated_notes(&mut chain, account0.id(), &[input_note0.id()]); - let tx1 = generate_tx_with_authenticated_notes(&mut chain, account1.id(), &[input_note1.id()]); - let tx2 = generate_tx_with_authenticated_notes(&mut chain, account2.id(), &[input_note2.id()]); - let tx3 = generate_tx_with_authenticated_notes(&mut chain, account3.id(), &[input_note3.id()]); - - let batch0 = generate_batch(&mut chain, [tx0.clone(), tx1.clone()].to_vec()); - let batch1 = generate_batch(&mut chain, [tx2.clone(), tx3.clone()].to_vec()); + let tx0 = chain + .create_authenticated_notes_proven_tx(account0.id(), [input_note0.id()]) + .await?; + let tx1 = chain + .create_authenticated_notes_proven_tx(account1.id(), [input_note1.id()]) + .await?; + let tx2 = chain + .create_authenticated_notes_proven_tx(account2.id(), [input_note2.id()]) + .await?; + let tx3 = chain + .create_authenticated_notes_proven_tx(account3.id(), [input_note3.id()]) + .await?; + + let batch0 = chain.create_batch(vec![tx0.clone(), tx1.clone()])?; + let batch1 = chain.create_batch(vec![tx2.clone(), tx3.clone()])?; // Sanity check: Batches should have two output notes each. assert_eq!(batch0.output_notes().len(), 2); @@ -126,7 +148,7 @@ fn proven_block_success() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- let proven_block = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL) - .prove_without_batch_verification(proposed_block) + .prove_dummy(proposed_block) .context("failed to prove proposed block")?; // Check tree/chain commitments against expected values. @@ -203,41 +225,37 @@ fn proven_block_success() -> anyhow::Result<()> { /// /// We also test that the batch note tree containing the output generating transactions is a subtree /// of the subtree of the overall block note tree computed from the block's output notes. -#[test] -fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { - let TestSetup { mut chain, mut accounts, .. } = setup_chain(4); - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - let account2 = accounts.remove(&2).unwrap(); - let account3 = accounts.remove(&3).unwrap(); - - // Use an Rng to randomize the note IDs and therefore their position in the output note batches. - // This is useful to test that the block note tree is correctly computed no matter at what index - // the erased note ends up in. - let mut rng = rand::rng(); - let output_note0 = generate_output_note(account0.id(), rng.random()); - let output_note2 = generate_output_note(account2.id(), rng.random()); - let output_note3 = generate_output_note(account3.id(), rng.random()); +#[tokio::test] +async fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account2 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account3 = builder.add_existing_mock_account(Auth::IncrNonce)?; + + // The builder will use an rng which randomizes the note IDs and therefore their position in the + // output note batches. This is useful to test that the block note tree is correctly + // computed no matter at what index the erased note ends up in. + let output_note0 = create_p2any_note(account0.id(), NoteType::Private, [], builder.rng_mut()); + let output_note2 = create_p2any_note(account2.id(), NoteType::Private, [], builder.rng_mut()); + let output_note3 = create_p2any_note(account3.id(), NoteType::Private, [], builder.rng_mut()); + + // Sanity check that these notes have different IDs. + assert_ne!(output_note0.id(), output_note2.id()); + assert_ne!(output_note2.id(), output_note3.id()); // Create notes that, when consumed, will create the above corresponding output notes. - let note0 = create_spawn_note(account0.id(), vec![&output_note0])?; - let note2 = create_spawn_note(account2.id(), vec![&output_note2])?; - let note3 = create_spawn_note(account3.id(), vec![&output_note3])?; - - // Add note{0,2,3} to the chain so we can consume them. - chain.add_pending_note(OutputNote::Full(note0.clone())); - chain.add_pending_note(OutputNote::Full(note2.clone())); - chain.add_pending_note(OutputNote::Full(note3.clone())); - chain.prove_next_block()?; - - let tx0 = generate_tx_with_authenticated_notes(&mut chain, account0.id(), &[note0.id()]); - let tx1 = generate_tx_with_unauthenticated_notes( - &mut chain, - account1.id(), - slice::from_ref(&output_note0), - ); - let tx2 = generate_tx_with_authenticated_notes(&mut chain, account2.id(), &[note2.id()]); - let tx3 = generate_tx_with_authenticated_notes(&mut chain, account3.id(), &[note3.id()]); + let note0 = builder.add_spawn_note([&output_note0])?; + let note2 = builder.add_spawn_note([&output_note2])?; + let note3 = builder.add_spawn_note([&output_note3])?; + let chain = builder.build()?; + + let tx0 = chain.create_authenticated_notes_proven_tx(account0.id(), [note0.id()]).await?; + let tx1 = chain + .create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(&output_note0)) + .await?; + let tx2 = chain.create_authenticated_notes_proven_tx(account2.id(), [note2.id()]).await?; + let tx3 = chain.create_authenticated_notes_proven_tx(account3.id(), [note3.id()]).await?; assert_eq!(tx0.input_notes().num_notes(), 1); assert_eq!(tx0.output_notes().num_notes(), 1); @@ -251,8 +269,8 @@ fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { tx1.input_notes().get_note(0).header().unwrap().id() ); - let batch0 = generate_batch(&mut chain, vec![tx2.clone(), tx0.clone(), tx3.clone()]); - let batch1 = generate_batch(&mut chain, vec![tx1.clone()]); + let batch0 = chain.create_batch(vec![tx2.clone(), tx0.clone(), tx3.clone()])?; + let batch1 = chain.create_batch(vec![tx1.clone()])?; // Sanity check: The batches and contained transactions should have the same input notes (sorted // by nullifier). @@ -316,7 +334,7 @@ fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { .find_map(|(idx, note)| (note.id() == output_note0.id()).then_some(idx)) .copied() .unwrap(); - expected_output_notes_batch0.remove(erased_note_idx as usize); + expected_output_notes_batch0.remove(erased_note_idx); let output_notes_batch0 = &proposed_block.output_note_batches()[0]; // The first batch creates three notes, one of which is erased, so we expect 2 notes in the @@ -325,7 +343,7 @@ fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { assert_eq!(output_notes_batch0, &expected_output_notes_batch0); let proven_block = LocalBlockProver::new(0) - .prove_without_batch_verification(proposed_block) + .prove_dummy(proposed_block) .context("failed to prove block")?; let actual_block_note_tree = proven_block.build_output_note_tree(); @@ -345,26 +363,25 @@ fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> { } /// Tests that we can build empty blocks. -#[test] -fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> { +#[tokio::test] +async fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> { // Setup a chain with a non-empty nullifier tree by consuming some notes. // -------------------------------------------------------------------------------------------- - let TestSetup { mut chain, mut accounts, .. } = setup_chain(2); - - let account0 = accounts.remove(&0).unwrap(); - let account1 = accounts.remove(&1).unwrap(); - - // Add notes to the chain we can consume. - let note0 = generate_tracked_note(&mut chain, account1.id(), account0.id()); - let note1 = generate_tracked_note(&mut chain, account0.id(), account1.id()); - chain.prove_next_block()?; + let mut builder = MockChain::builder(); + let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?; + let note0 = + builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(100)])?; + let note1 = + builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(100)])?; + let mut chain = builder.build()?; - let tx0 = generate_executed_tx_with_authenticated_notes(&chain, account0.id(), &[note0.id()]); - let tx1 = generate_executed_tx_with_authenticated_notes(&chain, account1.id(), &[note1.id()]); + let tx0 = chain.create_authenticated_notes_proven_tx(account0.id(), [note0.id()]).await?; + let tx1 = chain.create_authenticated_notes_proven_tx(account1.id(), [note1.id()]).await?; - chain.add_pending_executed_transaction(&tx0)?; - chain.add_pending_executed_transaction(&tx1)?; + chain.add_pending_proven_transaction(tx0); + chain.add_pending_proven_transaction(tx1); let blockx = chain.prove_next_block()?; // Build a block with empty inputs whose account tree and nullifier tree root are not the empty @@ -376,7 +393,7 @@ fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> { assert_eq!(latest_block_header.commitment(), blockx.commitment()); // Sanity check: The account and nullifier tree roots should not be the empty tree roots. - assert_ne!(latest_block_header.account_root(), AccountTree::new().root()); + assert_ne!(latest_block_header.account_root(), AccountTree::::default().root()); assert_ne!(latest_block_header.nullifier_root(), Smt::new().root()); let (_, empty_partial_blockchain) = chain.latest_selective_partial_blockchain([])?; @@ -394,7 +411,7 @@ fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> { ProposedBlock::new(block_inputs, Vec::new()).context("failed to propose block")?; let proven_block = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL) - .prove_without_batch_verification(proposed_block) + .prove_dummy(proposed_block) .context("failed to prove proposed block")?; // Nothing should be created or updated. diff --git a/crates/miden-testing/src/kernel_tests/block/utils.rs b/crates/miden-testing/src/kernel_tests/block/utils.rs index 115f353160..20e407616b 100644 --- a/crates/miden-testing/src/kernel_tests/block/utils.rs +++ b/crates/miden-testing/src/kernel_tests/block/utils.rs @@ -1,179 +1,102 @@ -use std::collections::BTreeMap; -use std::vec; use std::vec::Vec; -use miden_lib::note::create_p2id_note; -use miden_lib::testing::note::NoteBuilder; use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{Account, AccountId}; -use miden_objects::asset::{Asset, FungibleAsset}; +use miden_objects::account::AccountId; use miden_objects::batch::ProvenBatch; use miden_objects::block::BlockNumber; -use miden_objects::crypto::rand::RpoRandomCoin; -use miden_objects::note::{Note, NoteId, NoteTag, NoteType}; -use miden_objects::testing::account_id::ACCOUNT_ID_SENDER; -use miden_objects::transaction::{ - ExecutedTransaction, - OutputNote, - ProvenTransaction, - TransactionScript, -}; -use miden_objects::{Felt, ONE, Word, ZERO}; -use rand::rngs::SmallRng; -use rand::{Rng, SeedableRng}; - -use crate::mock_chain::ProvenTransactionExt; -use crate::{Auth, MockChain, TxContextInput}; - -pub struct TestSetup { - pub chain: MockChain, - pub accounts: BTreeMap, - pub txs: BTreeMap, +use miden_objects::note::{Note, NoteId}; +use miden_objects::transaction::{ExecutedTransaction, ProvenTransaction, TransactionScript}; +use miden_tx::LocalTransactionProver; + +use crate::{MockChain, TxContextInput}; + +// MOCK CHAIN BUILDER EXTENSION +// ================================================================================================ + +/// Provides convenience methods for testing. +pub trait MockChainBlockExt { + async fn create_authenticated_notes_tx( + &self, + input: impl Into + Send, + notes: impl IntoIterator + Send, + ) -> anyhow::Result; + + async fn create_authenticated_notes_proven_tx( + &self, + input: impl Into + Send, + notes: impl IntoIterator + Send, + ) -> anyhow::Result; + + async fn create_unauthenticated_notes_proven_tx( + &self, + account_id: AccountId, + notes: &[Note], + ) -> anyhow::Result; + + async fn create_expiring_proven_tx( + &self, + input: impl Into + Send, + expiration_block: BlockNumber, + ) -> anyhow::Result; + + fn create_batch(&self, txs: Vec) -> anyhow::Result; } -pub fn generate_tracked_note( - chain: &mut MockChain, - sender: AccountId, - receiver: AccountId, -) -> Note { - let note = generate_untracked_note_internal(sender, receiver, vec![]); - chain.add_pending_note(OutputNote::Full(note.clone())); - note -} - -pub fn generate_tracked_note_with_asset( - chain: &mut MockChain, - sender: AccountId, - receiver: AccountId, - asset: Asset, -) -> Note { - let note = generate_untracked_note_internal(sender, receiver, vec![asset]); - chain.add_pending_note(OutputNote::Full(note.clone())); - note -} - -pub fn generate_untracked_note(sender: AccountId, receiver: AccountId) -> Note { - generate_untracked_note_internal(sender, receiver, vec![]) -} - -/// Creates a NOP output note sent by the given sender. -pub fn generate_output_note(sender: AccountId, seed: [u8; 32]) -> Note { - let mut rng = SmallRng::from_seed(seed); - NoteBuilder::new(sender, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::for_local_use_case(0, 0).unwrap().into()) - .build() - .unwrap() -} - -fn generate_untracked_note_internal( - sender: AccountId, - receiver: AccountId, - asset: Vec, -) -> Note { - // Use OS-randomness so that notes with the same sender and target have different note IDs. - let mut rng = RpoRandomCoin::new(Word::new([ - Felt::new(rand::rng().random()), - Felt::new(rand::rng().random()), - Felt::new(rand::rng().random()), - Felt::new(rand::rng().random()), - ])); - create_p2id_note(sender, receiver, asset, NoteType::Public, Default::default(), &mut rng) - .unwrap() -} - -pub fn generate_fungible_asset(amount: u64, faucet_id: AccountId) -> Asset { - FungibleAsset::new(faucet_id, amount).unwrap().into() -} +impl MockChainBlockExt for MockChain { + async fn create_authenticated_notes_tx( + &self, + input: impl Into + Send, + notes: impl IntoIterator + Send, + ) -> anyhow::Result { + let notes = notes.into_iter().collect::>(); + let tx_context = self.build_tx_context(input, ¬es, &[])?.build()?; + tx_context.execute().await.map_err(From::from) + } -pub fn generate_executed_tx_with_authenticated_notes( - chain: &MockChain, - input: impl Into, - notes: &[NoteId], -) -> ExecutedTransaction { - let tx_context = chain - .build_tx_context(input, notes, &[]) - .expect("failed to build tx context") - .build() - .unwrap(); - tx_context.execute_blocking().unwrap() -} + async fn create_authenticated_notes_proven_tx( + &self, + input: impl Into + Send, + notes: impl IntoIterator + Send, + ) -> anyhow::Result { + let executed_tx = self.create_authenticated_notes_tx(input, notes).await?; + LocalTransactionProver::default().prove_dummy(executed_tx).map_err(From::from) + } -pub fn generate_tx_with_authenticated_notes( - chain: &mut MockChain, - account_id: AccountId, - notes: &[NoteId], -) -> ProvenTransaction { - let executed_tx = generate_executed_tx_with_authenticated_notes(chain, account_id, notes); - ProvenTransaction::from_executed_transaction_mocked(executed_tx) -} + async fn create_unauthenticated_notes_proven_tx( + &self, + account_id: AccountId, + notes: &[Note], + ) -> anyhow::Result { + let tx_context = self.build_tx_context(account_id, &[], notes)?.build()?; + let executed_tx = tx_context.execute().await?; + LocalTransactionProver::default().prove_dummy(executed_tx).map_err(From::from) + } -/// Generates a transaction, which depending on the `modify_storage` flag, does the following: -/// - if `modify_storage` is true, it increments the storage item of the account. -/// - if `modify_storage` is false, it does nothing (NOOP). -/// -/// To make this transaction (always) non-empty, it consumes one "noop note", which does nothing. -pub fn generate_conditional_tx( - chain: &mut MockChain, - input: impl Into, - modify_storage: bool, -) -> ExecutedTransaction { - let noop_note = NoteBuilder::new(ACCOUNT_ID_SENDER.try_into().unwrap(), &mut rand::rng()) - .build() - .expect("failed to create the noop note"); - chain.add_pending_note(OutputNote::Full(noop_note.clone())); - chain.prove_next_block().unwrap(); - - let auth_args = [ - if modify_storage { ONE } else { ZERO }, // increment nonce if modify_storage is true - Felt::new(99), - Felt::new(98), - Felt::new(97), - ]; - - let tx_context = chain - .build_tx_context(input.into(), &[noop_note.id()], &[]) - .unwrap() - .extend_input_notes(vec![noop_note]) - .auth_args(auth_args.into()) - .build() - .unwrap(); - tx_context.execute_blocking().unwrap() -} + async fn create_expiring_proven_tx( + &self, + input: impl Into + Send, + expiration_block: BlockNumber, + ) -> anyhow::Result { + let expiration_delta = expiration_block + .checked_sub(self.latest_block_header().block_num().as_u32()) + .unwrap(); + + let tx_context = self + .build_tx_context(input, &[], &[])? + .tx_script(update_expiration_tx_script(expiration_delta.as_u32() as u16)) + .build()?; + let executed_tx = tx_context.execute().await?; + LocalTransactionProver::default().prove_dummy(executed_tx).map_err(From::from) + } -/// Generates a transaction that expires at the given block number. -pub fn generate_tx_with_expiration( - chain: &mut MockChain, - input: impl Into, - expiration_block: BlockNumber, -) -> ProvenTransaction { - let expiration_delta = expiration_block - .checked_sub(chain.latest_block_header().block_num().as_u32()) - .unwrap(); - - let tx_context = chain - .build_tx_context(input, &[], &[]) - .expect("failed to build tx context") - .tx_script(update_expiration_tx_script(expiration_delta.as_u32() as u16)) - .build() - .unwrap(); - let executed_tx = tx_context.execute_blocking().unwrap(); - ProvenTransaction::from_executed_transaction_mocked(executed_tx) + fn create_batch(&self, txs: Vec) -> anyhow::Result { + self.propose_transaction_batch(txs) + .map(|batch| self.prove_transaction_batch(batch).unwrap()) + } } -pub fn generate_tx_with_unauthenticated_notes( - chain: &mut MockChain, - account_id: AccountId, - notes: &[Note], -) -> ProvenTransaction { - let tx_context = chain - .build_tx_context(account_id, &[], notes) - .expect("failed to build tx context") - .build() - .unwrap(); - let executed_tx = tx_context.execute_blocking().unwrap(); - ProvenTransaction::from_executed_transaction_mocked(executed_tx) -} +// HELPER FUNCTIONS +// ================================================================================================ fn update_expiration_tx_script(expiration_delta: u16) -> TransactionScript { let code = format!( @@ -189,46 +112,3 @@ fn update_expiration_tx_script(expiration_delta: u16) -> TransactionScript { ScriptBuilder::default().compile_tx_script(code).unwrap() } - -pub fn generate_batch(chain: &mut MockChain, txs: Vec) -> ProvenBatch { - chain - .propose_transaction_batch(txs) - .map(|batch| chain.prove_transaction_batch(batch).unwrap()) - .unwrap() -} - -/// Setup a test mock chain with the number of accounts, notes and transactions. -/// -/// This is merely generating some valid data for testing purposes. -pub fn setup_chain(num_accounts: usize) -> TestSetup { - let mut builder = MockChain::builder(); - let sender_account = builder - .add_existing_mock_account(Auth::IncrNonce) - .expect("adding account should be valid"); - let mut accounts = BTreeMap::new(); - let mut notes = BTreeMap::new(); - let mut txs = BTreeMap::new(); - - for i in 0..num_accounts { - let account = builder - .add_existing_mock_account(Auth::IncrNonce) - .expect("adding account should be valid"); - let note = builder - .add_p2id_note(sender_account.id(), account.id(), &[], NoteType::Public) - .expect("adding p2id note should be valid"); - accounts.insert(i, account); - notes.insert(i, note); - } - - let mut chain = builder.build().expect("building chain should be valid"); - - chain.prove_next_block().expect("failed to prove block"); - - for i in 0..num_accounts { - let tx = - generate_tx_with_authenticated_notes(&mut chain, accounts[&i].id(), &[notes[&i].id()]); - txs.insert(i, tx); - } - - TestSetup { chain, accounts, txs } -} diff --git a/crates/miden-testing/src/kernel_tests/tx/mod.rs b/crates/miden-testing/src/kernel_tests/tx/mod.rs index 04c2817db9..781bae46dc 100644 --- a/crates/miden-testing/src/kernel_tests/tx/mod.rs +++ b/crates/miden-testing/src/kernel_tests/tx/mod.rs @@ -2,6 +2,8 @@ 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, @@ -21,14 +23,16 @@ use miden_objects::testing::account_id::{ }; use miden_objects::testing::storage::prepare_assets; use miden_objects::vm::StackInputs; -use miden_objects::{Felt, Hasher, ONE, Word, ZERO}; -use miden_processor::{ContextId, Process}; +use miden_objects::{Felt, Word, ZERO}; +use miden_processor::ContextId; +use miden_processor::fast::ExecutionOutput; use crate::MockChain; mod test_account; mod test_account_delta; mod test_account_interface; +mod test_active_note; mod test_asset; mod test_asset_vault; mod test_auth; @@ -37,6 +41,7 @@ mod test_faucet; mod test_fee; mod test_fpi; mod test_input_note; +mod test_lazy_loading; mod test_link_map; mod test_note; mod test_output_note; @@ -46,37 +51,68 @@ mod test_tx; // HELPER FUNCTIONS // ================================================================================================ -/// Extension trait for a [`Process`] to conveniently read kernel memory. -pub trait ProcessMemoryExt { - /// Reads a word from transaction kernel memory. - /// - /// # Panics - /// - /// Panics if: - /// - the address is not word-aligned. - /// - the memory location is not initialized. +/// Extension trait for an [`ExecutionOutput`] to conveniently read the stack and kernel memory. +pub trait ExecutionOutputExt { + /// Reads a word from transaction kernel memory or returns [`Word::empty`] if that location is + /// not initialized. fn get_kernel_mem_word(&self, addr: u32) -> Word; - /// Reads a word from transaction kernel memory. - /// - /// # Panics - /// - /// Panics if: - /// - the address is not word-aligned. - fn try_get_kernel_mem_word(&self, addr: u32) -> Option; + /// 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] + } + + /// Reads an element from the stack. + fn get_stack_element(&self, idx: usize) -> Felt; + + /// Reads a [`Word`] from the stack in big-endian (reversed) order. + fn get_stack_word_be(&self, index: usize) -> Word; + + /// Reads a [`Word`] from the stack in little-endian (memory) order. + #[allow(dead_code)] + fn get_stack_word_le(&self, index: usize) -> Word; + + /// Reads the [`Word`] of the input note's memory identified by the index at the provided + /// `offset`. + fn get_note_mem_word(&self, note_idx: u32, offset: MemoryOffset) -> Word { + self.get_kernel_mem_word(input_note_data_ptr(note_idx) + offset) + } } -impl ProcessMemoryExt for Process { +impl ExecutionOutputExt for ExecutionOutput { fn get_kernel_mem_word(&self, addr: u32) -> Word { - self.try_get_kernel_mem_word(addr).expect("expected address to be initialized") - } + let tx_kernel_context = ContextId::root(); + let clk = 0u32; + let err_ctx = (); - fn try_get_kernel_mem_word(&self, addr: u32) -> Option { - self.chiplets - .memory - .get_word(ContextId::root(), addr) + self.memory + .read_word(tx_kernel_context, Felt::from(addr), clk.into(), &err_ctx) .expect("expected address to be word-aligned") } + + fn get_stack_element(&self, index: usize) -> Felt { + *self.stack.get(index).expect("index must be in bounds") + } + + fn get_stack_word_be(&self, index: usize) -> Word { + self.stack.get_stack_word_be(index).expect("index must be in bounds") + } + + 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 } /// Returns MASM code that defines a procedure called `create_mock_notes` which creates the notes @@ -106,19 +142,19 @@ pub fn create_mock_notes_procedure(notes: &[Note]) -> String { " # populate note {idx} push.{metadata} - push.{OUTPUT_NOTE_SECTION_OFFSET}.{note_offset}.{OUTPUT_NOTE_METADATA_OFFSET} add add mem_storew dropw + 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}.{note_offset}.{OUTPUT_NOTE_RECIPIENT_OFFSET} add add mem_storew dropw + 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}.{note_offset}.{OUTPUT_NOTE_NUM_ASSETS_OFFSET} add add mem_store + 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}.{note_offset}.{OUTPUT_NOTE_DIRTY_FLAG_OFFSET} add add mem_store + 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}.{note_offset}.{OUTPUT_NOTE_ASSETS_OFFSET} add add mem_storew dropw + push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_ASSETS_OFFSET} add add mem_storew_be dropw ", idx = idx, metadata = metadata, @@ -130,7 +166,7 @@ pub fn create_mock_notes_procedure(notes: &[Note]) -> String { } script.push_str(&format!( "# set num output notes - push.{count}.{NUM_OUTPUT_NOTES_PTR} mem_store + push.{count} push.{NUM_OUTPUT_NOTES_PTR} mem_store end ", count = notes.len(), 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 56ed82bb06..a4f5a4398a 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -1,4 +1,5 @@ use alloc::sync::Arc; +use alloc::vec::Vec; use std::collections::BTreeMap; use anyhow::Context; @@ -16,7 +17,7 @@ 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::StarkField; +use miden_objects::account::delta::AccountUpdateDetails; use miden_objects::account::{ Account, AccountBuilder, @@ -28,30 +29,39 @@ use miden_objects::account::{ AccountStorage, AccountStorageMode, AccountType, + StorageMap, StorageSlot, }; 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::{ ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_SENDER, }; use miden_objects::testing::storage::STORAGE_LEAVES_2; -use miden_objects::transaction::{ExecutedTransaction, TransactionScript}; -use miden_processor::{EMPTY_WORD, ExecutionError, Word}; -use miden_tx::TransactionExecutorError; +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 rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; +use winter_rand_utils::rand_value; use super::{Felt, StackInputs, ZERO}; use crate::executor::CodeExecutor; +use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::create_public_p2any_note; use crate::{ Auth, MockChain, TransactionContextBuilder, + TxContextInput, assert_execution_error, assert_transaction_executor_error, }; @@ -59,8 +69,8 @@ use crate::{ // ACCOUNT COMMITMENT TESTS // ================================================================================================ -#[test] -pub fn compute_current_commitment() -> miette::Result<()> { +#[tokio::test] +pub async fn compute_commitment() -> miette::Result<()> { let account = Account::mock(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); // Precompute a commitment to a changed account so we can assert it during tx script execution. @@ -72,15 +82,17 @@ pub fn compute_current_commitment() -> miette::Result<()> { let tx_script = format!( r#" + use.std::word + use.miden::prologue - use.miden::account + use.miden::active_account use.mock::account->mock_account begin - exec.account::get_initial_commitment + exec.active_account::get_initial_commitment # => [INITIAL_COMMITMENT] - exec.account::compute_current_commitment + exec.active_account::compute_commitment # => [CURRENT_COMMITMENT, INITIAL_COMMITMENT] assert_eqw.err="initial and current commitment should be equal when no changes have been made" @@ -102,7 +114,7 @@ pub fn compute_current_commitment() -> miette::Result<()> { # => [STORAGE_COMMITMENT0] # compute the commitment which will recompute the storage commitment - exec.account::compute_current_commitment + exec.active_account::compute_commitment # => [CURRENT_COMMITMENT, STORAGE_COMMITMENT0] push.{expected_commitment} @@ -115,10 +127,10 @@ pub fn compute_current_commitment() -> miette::Result<()> { swapdw dropw dropw swapw dropw # => [STORAGE_COMMITMENT1, STORAGE_COMMITMENT0] - eqw not assert.err="storage commitment should have been updated by compute_current_commitment" - # => [STORAGE_COMMITMENT1, STORAGE_COMMITMENT0] - - dropw dropw dropw dropw + # assert that the commitment has changed + exec.word::eq + assertz.err="storage commitment should have been updated by compute_commitment" + # => [] end "#, key = &key, @@ -137,9 +149,10 @@ pub fn compute_current_commitment() -> miette::Result<()> { .map_err(|err| miette::miette!("{err}"))?; tx_context - .execute_blocking() + .execute() + .await .into_diagnostic() - .wrap_err("failed to execute code")?; + .wrap_err("failed to execute transaction")?; Ok(()) } @@ -147,8 +160,8 @@ pub fn compute_current_commitment() -> miette::Result<()> { // ACCOUNT ID TESTS // ================================================================================================ -#[test] -pub fn test_account_type() -> miette::Result<()> { +#[tokio::test] +async fn test_account_type() -> miette::Result<()> { let procedures = vec![ ("is_fungible_faucet", AccountType::FungibleFaucet), ("is_non_fungible_faucet", AccountType::NonFungibleFaucet), @@ -179,18 +192,19 @@ pub fn test_account_type() -> miette::Result<()> { " ); - let process = CodeExecutor::with_default_host() + let exec_output = CodeExecutor::with_default_host() .stack_inputs( StackInputs::new(vec![account_id.prefix().as_felt()]).into_diagnostic()?, ) - .run(&code)?; + .run(&code) + .await?; let type_matches = account_id.account_type() == expected_type; let expected_result = Felt::from(type_matches); has_type |= type_matches; assert_eq!( - process.stack.get(0), + exec_output.get_stack_element(0), expected_result, "Rust and Masm check on account type diverge. proc: {} account_id: {} account_type: {:?} expected_type: {:?}", procedure, @@ -206,8 +220,8 @@ pub fn test_account_type() -> miette::Result<()> { Ok(()) } -#[test] -pub fn test_account_validate_id() -> miette::Result<()> { +#[tokio::test] +async fn test_account_validate_id() -> miette::Result<()> { let test_cases = [ (ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, None), (ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, None), @@ -251,7 +265,8 @@ pub fn test_account_validate_id() -> miette::Result<()> { let result = CodeExecutor::with_default_host() .stack_inputs(StackInputs::new(vec![suffix, prefix]).unwrap()) - .run(code); + .run(code) + .await; match (result, expected_error) { (Ok(_), None) => (), @@ -280,8 +295,8 @@ pub fn test_account_validate_id() -> miette::Result<()> { Ok(()) } -#[test] -fn test_is_faucet_procedure() -> miette::Result<()> { +#[tokio::test] +async fn test_is_faucet_procedure() -> miette::Result<()> { let test_cases = [ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, @@ -308,13 +323,14 @@ fn test_is_faucet_procedure() -> miette::Result<()> { prefix = account_id.prefix().as_felt(), ); - let process = CodeExecutor::with_default_host() + let exec_output = CodeExecutor::with_default_host() .run(&code) + .await .wrap_err("failed to execute is_faucet procedure")?; let is_faucet = account_id.is_faucet(); assert_eq!( - process.stack.get(0), + exec_output.get_stack_element(0), Felt::new(is_faucet as u64), "Rust and MASM is_faucet diverged for account_id {account_id}" ); @@ -327,8 +343,8 @@ fn test_is_faucet_procedure() -> miette::Result<()> { // ================================================================================================ // TODO: update this test once the ability to change the account code will be implemented -#[test] -pub fn test_compute_code_commitment() -> miette::Result<()> { +#[tokio::test] +pub async fn test_compute_code_commitment() -> miette::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); let account = tx_context.account(); @@ -348,7 +364,7 @@ pub fn test_compute_code_commitment() -> miette::Result<()> { expected_code_commitment = account.code().commitment() ); - tx_context.execute_code(&code)?; + tx_context.execute_code(&code).await?; Ok(()) } @@ -356,8 +372,8 @@ pub fn test_compute_code_commitment() -> miette::Result<()> { // ACCOUNT STORAGE TESTS // ================================================================================================ -#[test] -fn test_get_item() -> miette::Result<()> { +#[tokio::test] +async fn test_get_item() -> miette::Result<()> { for storage_item in [AccountStorage::mock_item_0(), AccountStorage::mock_item_1()] { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); @@ -382,14 +398,14 @@ fn test_get_item() -> miette::Result<()> { item_value = &storage_item.slot.value(), ); - tx_context.execute_code(&code).unwrap(); + tx_context.execute_code(&code).await.unwrap(); } Ok(()) } -#[test] -fn test_get_map_item() -> miette::Result<()> { +#[tokio::test] +async fn test_get_map_item() -> miette::Result<()> { 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])) @@ -419,26 +435,25 @@ fn test_get_map_item() -> miette::Result<()> { map_key = &key, ); - let process = &tx_context.execute_code(&code)?; - + let exec_output = &mut tx_context.execute_code(&code).await?; assert_eq!( + exec_output.get_stack_word_be(0), value, - process.stack.get_word(0), "get_map_item result doesn't match the expected value", ); assert_eq!( + exec_output.get_stack_word_be(4), Word::empty(), - process.stack.get_word(1), "The rest of the stack must be cleared", ); assert_eq!( + exec_output.get_stack_word_be(8), Word::empty(), - process.stack.get_word(2), "The rest of the stack must be cleared", ); assert_eq!( + exec_output.get_stack_word_be(12), Word::empty(), - process.stack.get_word(3), "The rest of the stack must be cleared", ); } @@ -446,8 +461,8 @@ fn test_get_map_item() -> miette::Result<()> { Ok(()) } -#[test] -fn test_get_storage_slot_type() -> 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(), @@ -476,24 +491,36 @@ fn test_get_storage_slot_type() -> miette::Result<()> { item_index = storage_item.index, ); - let process = &tx_context.execute_code(&code).unwrap(); + let exec_output = &tx_context.execute_code(&code).await.unwrap(); let storage_slot_type = storage_item.slot.slot_type(); - assert_eq!(storage_slot_type, process.stack.get(0).try_into().unwrap()); - assert_eq!(process.stack.get(1), ZERO, "the rest of the stack is empty"); - assert_eq!(process.stack.get(2), ZERO, "the rest of the stack is empty"); - assert_eq!(process.stack.get(3), ZERO, "the rest of the stack is empty"); - assert_eq!(Word::empty(), process.stack.get_word(1), "the rest of the stack is empty"); - assert_eq!(Word::empty(), process.stack.get_word(2), "the rest of the stack is empty"); - assert_eq!(Word::empty(), process.stack.get_word(3), "the rest of the stack is empty"); + assert_eq!(storage_slot_type, exec_output.get_stack_element(0).try_into().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"); + assert_eq!( + exec_output.get_stack_word_be(4), + Word::empty(), + "the rest of the stack is empty" + ); + assert_eq!( + exec_output.get_stack_word_be(8), + Word::empty(), + "the rest of the stack is empty" + ); + assert_eq!( + exec_output.get_stack_word_be(12), + Word::empty(), + "the rest of the stack is empty" + ); } Ok(()) } -#[test] -fn test_set_item() -> miette::Result<()> { +#[tokio::test] +async fn test_set_item() -> miette::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); let new_storage_item = Word::from([91, 92, 93, 94u32]); @@ -525,13 +552,13 @@ fn test_set_item() -> miette::Result<()> { new_storage_item_index = 0, ); - tx_context.execute_code(&code).unwrap(); + tx_context.execute_code(&code).await.unwrap(); Ok(()) } -#[test] -fn test_set_map_item() -> miette::Result<()> { +#[tokio::test] +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])); @@ -548,7 +575,6 @@ fn test_set_map_item() -> miette::Result<()> { " use.std::sys - use.mock::account use.$kernel::prologue use.mock::account->mock_account @@ -574,27 +600,27 @@ fn test_set_map_item() -> miette::Result<()> { new_value = &new_value, ); - let process = &tx_context.execute_code(&code).unwrap(); + let exec_output = &tx_context.execute_code(&code).await.unwrap(); let mut new_storage_map = AccountStorage::mock_map(); - new_storage_map.insert(new_key, new_value); + new_storage_map.insert(new_key, new_value).unwrap(); assert_eq!( new_storage_map.root(), - process.stack.get_word(0), + exec_output.get_stack_word_be(0), "get_item must return the new updated value", ); assert_eq!( storage_item.slot.value(), - process.stack.get_word(1), + exec_output.get_stack_word_be(4), "The original value stored in the map doesn't match the expected value", ); Ok(()) } -#[test] -fn test_account_component_storage_offset() -> miette::Result<()> { +#[tokio::test] +async fn test_account_component_storage_offset() -> miette::Result<()> { // setup assembler let assembler = TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())); @@ -609,40 +635,44 @@ fn test_account_component_storage_offset() -> miette::Result<()> { // 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.miden::account + use.std::word + use.miden::active_account + use.miden::native_account export.foo_write push.1.2.3.4.0 - exec.account::set_item + exec.native_account::set_item dropw end export.foo_read push.0 - exec.account::get_item - push.1.2.3.4 eqw assert + exec.active_account::get_item + push.1.2.3.4 - dropw dropw + exec.word::eq assert end "; let source_code_component2 = " - use.miden::account + use.std::word + use.miden::active_account + use.miden::native_account export.bar_write push.5.6.7.8.0 - exec.account::set_item + exec.native_account::set_item dropw end export.bar_read push.0 - exec.account::get_item - push.5.6.7.8 eqw assert + exec.active_account::get_item + push.5.6.7.8 - dropw dropw + exec.word::eq assert end "; @@ -731,7 +761,7 @@ fn test_account_component_storage_offset() -> miette::Result<()> { .unwrap(); // execute code in context - let tx = tx_context.execute_blocking().into_diagnostic()?; + 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 @@ -743,26 +773,23 @@ fn test_account_component_storage_offset() -> miette::Result<()> { } /// Tests that we can successfully create regular and faucet accounts with empty storage. -#[test] -fn create_account_with_empty_storage_slots() -> anyhow::Result<()> { +#[tokio::test] +async fn create_account_with_empty_storage_slots() -> anyhow::Result<()> { for account_type in [AccountType::FungibleFaucet, AccountType::RegularAccountUpdatableCode] { - let (account, seed) = AccountBuilder::new([5; 32]) + let account = AccountBuilder::new([5; 32]) .account_type(account_type) .with_auth_component(Auth::IncrNonce) .with_component(MockAccountComponent::with_empty_slots()) .build() .context("failed to build account")?; - TransactionContextBuilder::new(account) - .account_seed(Some(seed)) - .build()? - .execute_blocking()?; + TransactionContextBuilder::new(account).build()?.execute().await?; } Ok(()) } -fn create_procedure_metadata_test_account( +async fn create_procedure_metadata_test_account( account_type: AccountType, storage_offset: u8, storage_size: u8, @@ -797,15 +824,13 @@ fn create_procedure_metadata_test_account( let id = AccountId::new(seed, version, code.commitment(), storage.commitment()) .context("failed to compute ID")?; - let account = Account::from_parts(id, AssetVault::default(), storage, code, Felt::from(0u32)); + let account = + Account::new(id, AssetVault::default(), storage, code, Felt::from(0u32), Some(seed))?; - let tx_inputs = mock_chain.get_transaction_inputs(account.clone(), Some(seed), &[], &[])?; - let tx_context = TransactionContextBuilder::new(account) - .account_seed(Some(seed)) - .tx_inputs(tx_inputs) - .build()?; + 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_blocking().map_err(|err| { + let result = tx_context.execute().await.map_err(|err| { let TransactionExecutorError::TransactionProgramExecutionFailed(exec_err) = err else { panic!("should have received an execution error"); }; @@ -817,10 +842,12 @@ fn create_procedure_metadata_test_account( } /// Tests that creating an account whose procedure accesses the reserved faucet storage slot fails. -#[test] -fn creating_faucet_account_with_procedure_accessing_reserved_slot_fails() -> anyhow::Result<()> { +#[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); @@ -829,17 +856,20 @@ fn creating_faucet_account_with_procedure_accessing_reserved_slot_fails() -> any } /// Tests that creating a faucet whose procedure offset+size is out of bounds fails. -#[test] -fn creating_faucet_with_procedure_offset_plus_size_out_of_bounds_fails() -> anyhow::Result<()> { +#[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); @@ -848,11 +878,13 @@ fn creating_faucet_with_procedure_offset_plus_size_out_of_bounds_fails() -> anyh } /// Tests that creating an account whose procedure offset+size is out of bounds fails. -#[test] -fn creating_account_with_procedure_offset_plus_size_out_of_bounds_fails() -> anyhow::Result<()> { +#[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); @@ -860,6 +892,7 @@ fn creating_account_with_procedure_offset_plus_size_out_of_bounds_fails() -> any // 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); @@ -867,27 +900,27 @@ fn creating_account_with_procedure_offset_plus_size_out_of_bounds_fails() -> any Ok(()) } -#[test] -fn test_get_initial_storage_commitment() -> anyhow::Result<()> { +#[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::account + use.miden::active_account use.$kernel::prologue begin exec.prologue::prepare_transaction # get the initial storage commitment - exec.account::get_initial_storage_commitment + exec.active_account::get_initial_storage_commitment push.{expected_storage_commitment} assert_eqw.err="actual storage commitment is not equal to the expected one" end "#, expected_storage_commitment = &tx_context.account().storage().commitment(), ); - tx_context.execute_code(&code)?; + tx_context.execute_code(&code).await?; Ok(()) } @@ -901,8 +934,8 @@ fn test_get_initial_storage_commitment() -> anyhow::Result<()> { /// - Right after the previous call to make sure it returns the same commitment from the cached /// data. /// - After updating the 2nd storage slot (map slot). -#[test] -fn test_compute_storage_commitment() -> anyhow::Result<()> { +#[tokio::test] +async fn test_compute_storage_commitment() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); let mut account_clone = tx_context.account().clone(); let account_storage = account_clone.storage_mut(); @@ -943,7 +976,7 @@ fn test_compute_storage_commitment() -> anyhow::Result<()> { push.{storage_commitment_0} assert_eqw.err="storage commitment after the 0th 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 + # 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} @@ -961,7 +994,65 @@ fn test_compute_storage_commitment() -> anyhow::Result<()> { end "#, ); - tx_context.execute_code(&code)?; + tx_context.execute_code(&code).await?; + + Ok(()) +} + +/// Tests that an account with a non-empty map can be created. +/// +/// In particular, this tests the account delta logic for (non-empty) storage slots for _new_ +/// 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 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 account = AccountBuilder::new([6; 32]) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(Auth::IncrNonce) + .with_component(MockAccountComponent::with_slots(vec![ + slot0.clone(), + slot1.clone(), + map_slot, + ])) + .build()?; + + let tx = TransactionContextBuilder::new(account) + .build()? + .execute() + .await + .context("failed to execute account-creating transaction")?; + + 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( + map_entries + .into_iter() + .map(|(key, value)| { (LexicographicWord::new(key), value) }) + ) + ); + + assert!(tx.account_delta().vault().is_empty()); + assert_eq!(tx.final_account().nonce(), Felt::new(1)); + + let proven_tx = LocalTransactionProver::default().prove(tx.clone())?; + + // The delta should be present on the proven tx. + let AccountUpdateDetails::Delta(delta) = proven_tx.account_update().details() else { + panic!("expected delta"); + }; + assert_eq!(delta, tx.account_delta()); Ok(()) } @@ -969,8 +1060,8 @@ fn test_compute_storage_commitment() -> anyhow::Result<()> { // ACCOUNT VAULT TESTS // ================================================================================================ -#[test] -fn test_get_vault_root() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_vault_root() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let mut account = tx_context.account().clone(); @@ -986,28 +1077,28 @@ fn test_get_vault_root() -> anyhow::Result<()> { // get the initial vault root let code = format!( " - use.miden::account + use.miden::active_account use.$kernel::prologue begin exec.prologue::prepare_transaction # get the initial vault root - exec.account::get_initial_vault_root + exec.active_account::get_initial_vault_root push.{expected_vault_root} assert_eqw end ", expected_vault_root = &account.vault().root(), ); - tx_context.execute_code(&code)?; + tx_context.execute_code(&code).await?; // get the current vault root account.vault_mut().add_asset(fungible_asset)?; let code = format!( r#" - use.miden::account + use.miden::active_account use.$kernel::prologue use.mock::account->mock_account @@ -1020,7 +1111,7 @@ fn test_get_vault_root() -> anyhow::Result<()> { # => [] # get the current vault root - exec.account::get_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" end @@ -1028,7 +1119,267 @@ fn test_get_vault_root() -> anyhow::Result<()> { fungible_asset = Word::from(&fungible_asset), expected_vault_root = &account.vault().root(), ); - tx_context.execute_code(&code)?; + tx_context.execute_code(&code).await?; + + Ok(()) +} + +/// This test checks the correctness of the `miden::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. +#[tokio::test] +async fn test_get_init_balance_addition() -> anyhow::Result<()> { + // prepare the testing data + // ------------------------------------------ + let mut builder = MockChain::builder(); + + let faucet_existing_asset = + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).context("id should be valid")?; + let faucet_new_asset = + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).context("id should be valid")?; + + let fungible_asset_for_account = Asset::Fungible( + FungibleAsset::new(faucet_existing_asset, 10).context("fungible_asset_0 is invalid")?, + ); + let account = builder + .add_existing_wallet_with_assets(crate::Auth::BasicAuth, [fungible_asset_for_account])?; + + let fungible_asset_for_note_existing = Asset::Fungible( + FungibleAsset::new(faucet_existing_asset, 7).context("fungible_asset_0 is invalid")?, + ); + + let fungible_asset_for_note_new = Asset::Fungible( + FungibleAsset::new(faucet_new_asset, 20).context("fungible_asset_1 is invalid")?, + ); + + let p2id_note_existing_asset = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[fungible_asset_for_note_existing], + NoteType::Public, + )?; + let p2id_note_new_asset = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[fungible_asset_for_note_new], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // case 1: existing asset was added to the account + // ------------------------------------------ + + let initial_balance = account + .vault() + .get_balance(faucet_existing_asset) + .expect("faucet_id should be a fungible faucet ID"); + + let add_existing_source = format!( + r#" + use.miden::active_account + + begin + # push faucet ID prefix and suffix + push.{suffix}.{prefix} + # => [faucet_id_prefix, faucet_id_suffix] + + # get the current asset balance + dup.1 dup.1 exec.active_account::get_balance + # => [final_balance, faucet_id_prefix, faucet_id_suffix] + + # assert final balance is correct + push.{final_balance} + assert_eq.err="final balance is incorrect" + # => [faucet_id_prefix, faucet_id_suffix] + + # get the initial asset balance + exec.active_account::get_initial_balance + # => [init_balance] + + # assert initial balance is correct + push.{initial_balance} + assert_eq.err="initial balance is incorrect" + end + "#, + suffix = faucet_existing_asset.suffix(), + prefix = faucet_existing_asset.prefix().as_felt(), + final_balance = + initial_balance + fungible_asset_for_note_existing.unwrap_fungible().amount(), + ); + + let tx_script = ScriptBuilder::default().compile_tx_script(add_existing_source)?; + + let tx_context = mock_chain + .build_tx_context( + TxContextInput::AccountId(account.id()), + &[], + &[p2id_note_existing_asset], + )? + .tx_script(tx_script) + .build()?; + + tx_context.execute().await?; + + // case 2: new asset was added to the account + // ------------------------------------------ + + let initial_balance = account + .vault() + .get_balance(faucet_new_asset) + .expect("faucet_id should be a fungible faucet ID"); + + let add_new_source = format!( + r#" + use.miden::active_account + + begin + # push faucet ID prefix and suffix + push.{suffix}.{prefix} + # => [faucet_id_prefix, faucet_id_suffix] + + # get the current asset balance + dup.1 dup.1 exec.active_account::get_balance + # => [final_balance, faucet_id_prefix, faucet_id_suffix] + + # assert final balance is correct + push.{final_balance} + assert_eq.err="final balance is incorrect" + # => [faucet_id_prefix, faucet_id_suffix] + + # get the initial asset balance + exec.active_account::get_initial_balance + # => [init_balance] + + # assert initial balance is correct + push.{initial_balance} + assert_eq.err="initial balance is incorrect" + end + "#, + suffix = faucet_new_asset.suffix(), + prefix = faucet_new_asset.prefix().as_felt(), + 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_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_new_asset])? + .tx_script(tx_script) + .build()?; + + tx_context.execute().await?; + + 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. +/// +/// As part of the test pipeline it also checks the correctness of the +/// `miden::active_account::get_balance` procedure. +#[tokio::test] +async fn test_get_init_balance_subtraction() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let faucet_existing_asset = + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).context("id should be valid")?; + + let fungible_asset_for_account = Asset::Fungible( + FungibleAsset::new(faucet_existing_asset, 10).context("fungible_asset_0 is invalid")?, + ); + let account = builder + .add_existing_wallet_with_assets(crate::Auth::BasicAuth, [fungible_asset_for_account])?; + + let fungible_asset_for_note_existing = Asset::Fungible( + FungibleAsset::new(faucet_existing_asset, 7).context("fungible_asset_0 is invalid")?, + ); + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let initial_balance = account + .vault() + .get_balance(faucet_existing_asset) + .expect("faucet_id should be a fungible faucet ID"); + + let expected_output_note = + create_public_p2any_note(ACCOUNT_ID_SENDER.try_into()?, [fungible_asset_for_note_existing]); + + let remove_existing_source = format!( + r#" + use.miden::active_account + use.miden::contracts::wallets::basic->wallet + use.mock::util + + # Inputs: [ASSET, note_idx] + # Outputs: [ASSET, 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)] + + # remove excess PADs from the stack + swapdw dropw dropw swapw movdn.7 drop drop drop + # => [ASSET, note_idx] + end + + begin + # create random note and move the asset into it + exec.util::create_random_note + # => [note_idx] + + push.{REMOVED_ASSET} + exec.move_asset_to_note dropw drop + # => [] + + # push faucet ID prefix and suffix + push.{suffix}.{prefix} + # => [faucet_id_prefix, faucet_id_suffix] + + # get the current asset balance + dup.1 dup.1 exec.active_account::get_balance + # => [final_balance, faucet_id_prefix, faucet_id_suffix] + + # assert final balance is correct + push.{final_balance} + assert_eq.err="final balance is incorrect" + # => [faucet_id_prefix, faucet_id_suffix] + + # get the initial asset balance + exec.active_account::get_initial_balance + # => [init_balance] + + # assert initial balance is correct + push.{initial_balance} + assert_eq.err="initial balance is incorrect" + end + "#, + REMOVED_ASSET = Word::from(fungible_asset_for_note_existing), + suffix = faucet_existing_asset.suffix(), + prefix = faucet_existing_asset.prefix().as_felt(), + final_balance = + 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_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[])? + .tx_script(tx_script) + .extend_expected_output_notes(vec![OutputNote::Full(expected_output_note)]) + .build()?; + + tx_context.execute().await?; Ok(()) } @@ -1036,8 +1387,8 @@ fn test_get_vault_root() -> anyhow::Result<()> { // PROCEDURE AUTHENTICATION TESTS // ================================================================================================ -#[test] -fn test_authenticate_and_track_procedure() -> miette::Result<()> { +#[tokio::test] +async fn test_authenticate_and_track_procedure() -> miette::Result<()> { let mock_component = MockAccountComponent::with_empty_slots(); let account_code = AccountCode::from_components( @@ -1077,11 +1428,15 @@ fn test_authenticate_and_track_procedure() -> miette::Result<()> { // Execution of this code will return an EventError(UnknownAccountProcedure) for procs // that are not in the advice provider. - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; match valid { - true => assert!(process.is_ok(), "A valid procedure must successfully authenticate"), - false => assert!(process.is_err(), "An invalid procedure should fail to authenticate"), + true => { + assert!(exec_output.is_ok(), "A valid procedure must successfully authenticate") + }, + false => { + assert!(exec_output.is_err(), "An invalid procedure should fail to authenticate") + }, } } @@ -1091,8 +1446,8 @@ fn test_authenticate_and_track_procedure() -> miette::Result<()> { // PROCEDURE INTROSPECTION TESTS // ================================================================================================ -#[test] -fn test_was_procedure_called() -> miette::Result<()> { +#[tokio::test] +async fn test_was_procedure_called() -> miette::Result<()> { // Create a standard account using the mock component let mock_component = MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()); let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) @@ -1109,12 +1464,12 @@ fn test_was_procedure_called() -> miette::Result<()> { // 5. Checks that `was_procedure_called` returns `true` let tx_script_code = r#" use.mock::account->mock_account - use.miden::account + use.miden::native_account begin # First check that get_item procedure hasn't been called yet procref.mock_account::get_item - exec.account::was_procedure_called + exec.native_account::was_procedure_called assertz.err="procedure should not have been called" # Call the procedure first time @@ -1123,7 +1478,7 @@ fn test_was_procedure_called() -> miette::Result<()> { # => [] procref.mock_account::get_item - exec.account::was_procedure_called + exec.native_account::was_procedure_called assert.err="procedure should have been called" # Call the procedure second time @@ -1131,7 +1486,7 @@ fn test_was_procedure_called() -> miette::Result<()> { call.mock_account::get_item dropw procref.mock_account::get_item - exec.account::was_procedure_called + exec.native_account::was_procedure_called assert.err="2nd call should not change the was_called flag" end "#; @@ -1146,7 +1501,8 @@ fn test_was_procedure_called() -> miette::Result<()> { let tx_context = TransactionContextBuilder::new(account).tx_script(tx_script).build().unwrap(); tx_context - .execute_blocking() + .execute() + .await .into_diagnostic() .wrap_err("Failed to execute transaction")?; @@ -1158,15 +1514,15 @@ fn test_was_procedure_called() -> miette::Result<()> { /// /// The call chain and dependency graph in this test is: /// `tx script -> account code -> external library` -#[test] -fn transaction_executor_account_code_using_custom_library() -> miette::Result<()> { +#[tokio::test] +async fn transaction_executor_account_code_using_custom_library() -> miette::Result<()> { const EXTERNAL_LIBRARY_CODE: &str = r#" - use.miden::account + use.miden::native_account export.external_setter push.2.3.4.5 push.0 - exec.account::set_item + exec.native_account::set_item dropw dropw end"#; @@ -1221,7 +1577,7 @@ fn transaction_executor_account_code_using_custom_library() -> miette::Result<() .build() .unwrap(); - let executed_tx = tx_context.execute_blocking().into_diagnostic()?; + let executed_tx = tx_context.execute().await.into_diagnostic()?; // Account's initial nonce of 1 should have been incremented by 1. assert_eq!(executed_tx.account_delta().nonce_delta(), Felt::new(1)); @@ -1235,39 +1591,210 @@ fn transaction_executor_account_code_using_custom_library() -> miette::Result<() } /// Tests that incrementing the account nonce twice fails. -#[test] -fn incrementing_nonce_twice_fails() -> anyhow::Result<()> { +#[tokio::test] +async fn incrementing_nonce_twice_fails() -> anyhow::Result<()> { let source_code = " - use.miden::account + use.miden::native_account - export.auth__incr_nonce_twice - exec.account::incr_nonce drop - exec.account::incr_nonce drop + export.auth_incr_nonce_twice + exec.native_account::incr_nonce drop + exec.native_account::incr_nonce drop end "; let faulty_auth_component = AccountComponent::compile(source_code, TransactionKernel::assembler(), vec![])? .with_supports_all_types(); - let (account, seed) = AccountBuilder::new([5; 32]) + let account = AccountBuilder::new([5; 32]) .with_auth_component(faulty_auth_component) .with_component(MockAccountComponent::with_empty_slots()) .build() .context("failed to build account")?; - let result = TransactionContextBuilder::new(account) - .account_seed(Some(seed)) - .build()? - .execute_blocking(); + let result = TransactionContextBuilder::new(account).build()?.execute().await; assert_transaction_executor_error!(result, ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE); Ok(()) } +#[tokio::test] +async fn test_has_procedure() -> miette::Result<()> { + // Create a standard account using the mock component + let mock_component = MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()); + let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(mock_component) + .build_existing() + .unwrap(); + + let tx_script_code = r#" + use.mock::account->mock_account + use.miden::active_account + + begin + # check that get_item procedure is available on the mock account + procref.mock_account::get_item + # => [GET_ITEM_ROOT] + + exec.active_account::has_procedure + # => [is_procedure_available] + + # assert that the get_item is exposed + assert.err="get_item procedure should be exposed by the mock account" + + # get some random word and assert that it is not exposed + push.5.3.15.686 + + exec.active_account::has_procedure + # => [is_procedure_available] + + # assert that the procedure with some random root is not exposed + assertz.err="procedure with some random root should not be exposed by the mock account" + end + "#; + + // Compile the transaction script using the testing assembler with mock account + let tx_script = ScriptBuilder::with_mock_libraries() + .into_diagnostic()? + .compile_tx_script(tx_script_code) + .into_diagnostic()?; + + // Create transaction context and execute + let tx_context = TransactionContextBuilder::new(account).tx_script(tx_script).build().unwrap(); + + tx_context + .execute() + .await + .into_diagnostic() + .wrap_err("Failed to execute transaction")?; + + Ok(()) +} + +// ACCOUNT INITIAL STORAGE TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_get_initial_item() -> miette::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); + + // 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 + + begin + exec.prologue::prepare_transaction + + # get initial value of storage slot 0 + push.0 + exec.account::get_initial_item + + push.{expected_initial_value} + 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 + + # get_item should return the new value + push.0 + exec.account::get_item + push.9.10.11.12 + assert_eqw.err=\"current value should be updated\" + + # get_initial_item should still return the initial value + push.0 + exec.account::get_initial_item + push.{expected_initial_value} + assert_eqw.err=\"initial value should remain unchanged\" + end + ", + expected_initial_value = &AccountStorage::mock_item_0().slot.value(), + ); + + tx_context.execute_code(&code).await.unwrap(); + + Ok(()) +} + +#[tokio::test] +async fn test_get_initial_map_item() -> miette::Result<()> { + 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])) + .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 new_key = Word::from([201, 202, 203, 204u32]); + let new_value = Word::from([301, 302, 303, 304u32]); + + let code = format!( + " + use.$kernel::prologue + use.mock::account->mock_account + + begin + exec.prologue::prepare_transaction + + # get initial value from map + push.{initial_key} + push.0 + call.mock_account::get_initial_map_item + push.{initial_value} + 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 + call.mock_account::set_map_item dropw dropw + + # get_map_item should return the new value + push.{new_key} + push.0 + call.mock_account::get_map_item + push.{new_value} + 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 + call.mock_account::get_initial_map_item + push.{initial_value} + 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 + call.mock_account::get_initial_map_item + padw + assert_eqw.err=\"new key should have empty initial value\" + + dropw dropw + end + ", + initial_key = &initial_key, + initial_value = &initial_value, + new_key = &new_key, + new_value = &new_value, + ); + + tx_context.execute_code(&code).await.unwrap(); + + Ok(()) +} + /// Tests that incrementing the account nonce fails if it would overflow the field. -#[test] -fn incrementing_nonce_overflow_fails() -> anyhow::Result<()> { +#[tokio::test] +async fn incrementing_nonce_overflow_fails() -> anyhow::Result<()> { let mut account = AccountBuilder::new([42; 32]) .with_auth_component(Auth::IncrNonce) .with_component(MockAccountComponent::with_empty_slots()) @@ -1277,7 +1804,7 @@ fn incrementing_nonce_overflow_fails() -> anyhow::Result<()> { // modulus - 2. account.increment_nonce(Felt::new(Felt::MODULUS - 2))?; - let result = TransactionContextBuilder::new(account).build()?.execute_blocking(); + let result = TransactionContextBuilder::new(account).build()?.execute().await; assert_transaction_executor_error!(result, ERR_ACCOUNT_NONCE_AT_MAX); 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 27ea2878ea..b405fab2bb 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 @@ -5,10 +5,14 @@ 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::{ + Account, AccountBuilder, + AccountDelta, AccountId, AccountStorage, + AccountStorageMode, AccountType, StorageMap, StorageSlot, @@ -34,11 +38,12 @@ use miden_objects::testing::constants::{ use miden_objects::testing::storage::{STORAGE_INDEX_0, STORAGE_INDEX_2}; use miden_objects::transaction::TransactionScript; use miden_objects::{EMPTY_WORD, Felt, LexicographicWord, Word, ZERO}; +use miden_tx::LocalTransactionProver; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use winter_rand_utils::rand_value; -use crate::utils::create_p2any_note; +use crate::utils::create_public_p2any_note; use crate::{Auth, MockChain, TransactionContextBuilder}; // ACCOUNT DELTA TESTS @@ -52,19 +57,20 @@ use crate::{Auth, MockChain, TransactionContextBuilder}; /// /// In order to make the account delta empty but the transaction still legal, we consume a note /// without assets. -#[test] -fn empty_account_delta_commitment_is_empty_word() -> anyhow::Result<()> { +#[tokio::test] +async fn empty_account_delta_commitment_is_empty_word() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let account = builder.add_existing_mock_account(Auth::Noop)?; let p2any_note = - builder.add_p2any_note(AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(), &[])?; + builder.add_p2any_note(AccountId::try_from(ACCOUNT_ID_SENDER)?, NoteType::Public, [])?; let mock_chain = builder.build()?; let executed_tx = mock_chain .build_tx_context(account.id(), &[p2any_note.id()], &[]) .expect("failed to build tx context") .build()? - .execute_blocking() + .execute() + .await .context("failed to execute transaction")?; assert_eq!(executed_tx.account_delta().nonce_delta(), ZERO); @@ -75,15 +81,16 @@ fn empty_account_delta_commitment_is_empty_word() -> anyhow::Result<()> { } /// Tests that a noop transaction with [`Auth::IncrNonce`] results in a nonce delta of 1. -#[test] -fn delta_nonce() -> anyhow::Result<()> { - let TestSetup { mock_chain, account_id } = setup_test([], [])?; +#[tokio::test] +async fn delta_nonce() -> anyhow::Result<()> { + let TestSetup { mock_chain, account_id, .. } = setup_test([], [], [])?; let executed_tx = mock_chain .build_tx_context(account_id, &[], &[]) .expect("failed to build tx context") .build()? - .execute_blocking() + .execute() + .await .context("failed to execute transaction")?; assert_eq!(executed_tx.account_delta().nonce_delta(), Felt::new(1)); @@ -97,8 +104,8 @@ fn delta_nonce() -> anyhow::Result<()> { /// - Slot 1: EMPTY_WORD -> [3,4,5,6] -> Delta: [3,4,5,6] /// - Slot 2: [1,3,5,7] -> [1,3,5,7] -> Delta: None /// - Slot 3: [1,3,5,7] -> [2,3,4,5] -> [1,3,5,7] -> Delta: None -#[test] -fn storage_delta_for_value_slots() -> anyhow::Result<()> { +#[tokio::test] +async fn storage_delta_for_value_slots() -> anyhow::Result<()> { 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; @@ -113,7 +120,7 @@ fn storage_delta_for_value_slots() -> anyhow::Result<()> { 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( + let TestSetup { mock_chain, account_id, .. } = setup_test( vec![ StorageSlot::Value(slot_0_init_value), StorageSlot::Value(slot_1_init_value), @@ -121,55 +128,49 @@ fn storage_delta_for_value_slots() -> anyhow::Result<()> { StorageSlot::Value(slot_3_init_value), ], [], + [], )?; let tx_script = compile_tx_script(format!( " begin - push.{tmp_slot_0_value} + push.{slot_0_tmp_value} push.0 # => [index, VALUE] exec.set_item # => [] - push.{final_slot_0_value} + push.{slot_0_final_value} push.0 # => [index, VALUE] exec.set_item # => [] - push.{final_slot_1_value} + push.{slot_1_final_value} push.1 # => [index, VALUE] exec.set_item # => [] - push.{final_slot_2_value} + push.{slot_2_final_value} push.2 # => [index, VALUE] exec.set_item # => [] - push.{tmp_slot_3_value} + push.{slot_3_tmp_value} push.3 # => [index, VALUE] exec.set_item # => [] - push.{final_slot_3_value} + push.{slot_3_final_value} push.3 # => [index, VALUE] exec.set_item # => [] end - ", - // Set slot 0 to some other value initially. - tmp_slot_0_value = slot_0_tmp_value, - final_slot_0_value = slot_0_final_value, - final_slot_1_value = slot_1_final_value, - final_slot_2_value = slot_2_final_value, - tmp_slot_3_value = slot_3_tmp_value, - final_slot_3_value = slot_3_final_value, + " ))?; let executed_tx = mock_chain @@ -177,7 +178,8 @@ fn storage_delta_for_value_slots() -> anyhow::Result<()> { .expect("failed to build tx context") .tx_script(tx_script) .build()? - .execute_blocking() + .execute() + .await .context("failed to execute transaction")?; let storage_values_delta = executed_tx @@ -204,8 +206,8 @@ fn storage_delta_for_value_slots() -> anyhow::Result<()> { /// - Slot 2: key5: [1,2,3,4] -> [2,3,4,5] -> [1,2,3,4] -> Delta: None /// - key5 and key4 are the same scenario, but in different slots. In particular, slot 2's delta /// map will be empty after normalization and so it shouldn't be present in the delta at all. -#[test] -fn storage_delta_for_map_slots() -> anyhow::Result<()> { +#[tokio::test] +async fn storage_delta_for_map_slots() -> anyhow::Result<()> { // Test with random keys to make sure the ordering in the MASM and Rust implementations // matches. let key0 = rand_value::(); @@ -233,18 +235,18 @@ fn storage_delta_for_map_slots() -> anyhow::Result<()> { let key5_final_value = Word::from([1, 2, 3, 4u32]); let mut map0 = StorageMap::new(); - map0.insert(key0, key0_init_value); - map0.insert(key1, key1_init_value); + map0.insert(key0, key0_init_value).unwrap(); + map0.insert(key1, key1_init_value).unwrap(); let mut map1 = StorageMap::new(); - map1.insert(key2, key2_init_value); - map1.insert(key3, key3_init_value); - map1.insert(key4, key4_init_value); + map1.insert(key2, key2_init_value).unwrap(); + map1.insert(key3, key3_init_value).unwrap(); + map1.insert(key4, key4_init_value).unwrap(); let mut map2 = StorageMap::new(); - map2.insert(key5, key5_init_value); + map2.insert(key5, key5_init_value).unwrap(); - let TestSetup { mock_chain, account_id } = setup_test( + let TestSetup { mock_chain, account_id, .. } = setup_test( vec![ StorageSlot::Map(map0), StorageSlot::Map(map1), @@ -255,79 +257,66 @@ fn storage_delta_for_map_slots() -> anyhow::Result<()> { StorageSlot::Map(StorageMap::new()), ], [], + [], )?; let tx_script = compile_tx_script(format!( " begin - push.{key0_value}.{key0}.0 + push.{key0_final_value} push.{key0} push.0 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key1_tmp_value}.{key1}.0 + push.{key1_tmp_value} push.{key1} push.0 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key1_value}.{key1}.0 + push.{key1_final_value} push.{key1} push.0 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key2_value}.{key2}.1 + push.{key2_final_value} push.{key2} push.1 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key3_value}.{key3}.1 + push.{key3_final_value} push.{key3} push.1 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key4_tmp_value}.{key4}.1 + push.{key4_tmp_value} push.{key4} push.1 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key4_value}.{key4}.1 + push.{key4_final_value} push.{key4} push.1 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key5_tmp_value}.{key5}.2 + push.{key5_tmp_value} push.{key5} push.2 # => [index, KEY, VALUE] exec.set_map_item # => [] - push.{key5_value}.{key5}.2 + push.{key5_final_value} push.{key5} push.2 # => [index, KEY, VALUE] exec.set_map_item # => [] end - ", - key0 = key0, - key1 = key1, - key2 = key2, - key3 = key3, - key4 = key4, - key5 = key5, - key0_value = key0_final_value, - key1_tmp_value = key1_tmp_value, - key1_value = key1_final_value, - key2_value = key2_final_value, - key3_value = key3_final_value, - key4_tmp_value = key4_tmp_value, - key4_value = key4_final_value, - key5_tmp_value = key5_tmp_value, - key5_value = key5_final_value, + " ))?; let executed_tx = mock_chain .build_tx_context(account_id, &[], &[])? .tx_script(tx_script) .build()? - .execute_blocking() + .execute() + .await .context("failed to execute transaction")?; let maps_delta = executed_tx.account_delta().storage().maps(); @@ -357,8 +346,8 @@ fn storage_delta_for_map_slots() -> anyhow::Result<()> { /// - Asset2 is increased by 200 and decreased by 100 -> Delta: 100. /// - Asset3 is decreased by [`FungibleAsset::MAX_AMOUNT`] -> Delta: -MAX_AMOUNT. /// - Asset4 is increased by [`FungibleAsset::MAX_AMOUNT`] -> Delta: MAX_AMOUNT. -#[test] -fn fungible_asset_delta() -> anyhow::Result<()> { +#[tokio::test] +async fn fungible_asset_delta() -> anyhow::Result<()> { // Test with random IDs to make sure the ordering in the MASM and Rust implementations // matches. let faucet0: AccountId = AccountIdBuilder::new() @@ -392,25 +381,12 @@ fn fungible_asset_delta() -> anyhow::Result<()> { let removed_asset2 = FungibleAsset::new(faucet2, 100)?; let removed_asset3 = FungibleAsset::new(faucet3, FungibleAsset::MAX_AMOUNT)?; - let TestSetup { mut mock_chain, account_id } = setup_test( + let TestSetup { mock_chain, account_id, notes } = setup_test( [], [original_asset0, original_asset1, original_asset2, original_asset3].map(Asset::from), + [added_asset0, added_asset1, added_asset2, added_asset4].map(Asset::from), )?; - let mut added_notes = vec![]; - for added_asset in [added_asset0, added_asset1, added_asset2, added_asset4] { - let added_note = mock_chain - .add_pending_p2id_note( - account_id, - account_id, - &[Asset::from(added_asset)], - NoteType::Public, - ) - .context("failed to add note with asset")?; - added_notes.push(added_note); - } - mock_chain.prove_next_block()?; - let tx_script = compile_tx_script(format!( " begin @@ -431,10 +407,11 @@ fn fungible_asset_delta() -> anyhow::Result<()> { ))?; let executed_tx = mock_chain - .build_tx_context(account_id, &added_notes.iter().map(Note::id).collect::>(), &[])? + .build_tx_context(account_id, ¬es.iter().map(Note::id).collect::>(), &[])? .tx_script(tx_script) .build()? - .execute_blocking() + .execute() + .await .context("failed to execute transaction")?; let mut added_assets = executed_tx @@ -476,8 +453,8 @@ fn fungible_asset_delta() -> anyhow::Result<()> { /// - Asset1 is removed from the vault -> Delta: Remove. /// - Asset2 is added and removed -> Delta: No Change. /// - Asset3 is removed and added -> Delta: No Change. -#[test] -fn non_fungible_asset_delta() -> anyhow::Result<()> { +#[tokio::test] +async fn non_fungible_asset_delta() -> anyhow::Result<()> { let mut rng = rand::rng(); // Test with random IDs to make sure the ordering in the MASM and Rust implementations // matches. @@ -499,22 +476,8 @@ fn non_fungible_asset_delta() -> anyhow::Result<()> { let asset2 = NonFungibleAssetBuilder::new(faucet2.prefix(), &mut rng)?.build()?; let asset3 = NonFungibleAssetBuilder::new(faucet3.prefix(), &mut rng)?.build()?; - let TestSetup { mut mock_chain, account_id } = - setup_test([], [asset1, asset3].map(Asset::from))?; - - let mut added_notes = vec![]; - for added_asset in [asset0, asset2] { - let added_note = mock_chain - .add_pending_p2id_note( - account_id, - account_id, - &[Asset::from(added_asset)], - NoteType::Public, - ) - .context("failed to add note with asset")?; - added_notes.push(added_note); - } - mock_chain.prove_next_block()?; + 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!( " @@ -538,10 +501,11 @@ fn non_fungible_asset_delta() -> anyhow::Result<()> { ))?; let executed_tx = mock_chain - .build_tx_context(account_id, &added_notes.iter().map(Note::id).collect::>(), &[])? + .build_tx_context(account_id, ¬es.iter().map(Note::id).collect::>(), &[])? .tx_script(tx_script) .build()? - .execute_blocking() + .execute() + .await .context("failed to execute transaction")?; let mut added_assets = executed_tx @@ -568,8 +532,8 @@ fn non_fungible_asset_delta() -> anyhow::Result<()> { /// Tests that adding and removing assets and updating value and map storage slots results in the /// correct delta. -#[test] -fn asset_and_storage_delta() -> anyhow::Result<()> { +#[tokio::test] +async fn asset_and_storage_delta() -> anyhow::Result<()> { let account_assets = AssetVault::mock().assets().collect::>(); let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) @@ -637,7 +601,7 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] # create the note - call.tx::create_note + call.output_note::create # => [note_idx, pad(15)] # move an asset to the created note to partially deplete fungible asset balance @@ -659,7 +623,7 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { let tx_script_src = format!( "\ use.mock::account - use.miden::tx + use.miden::output_note ## TRANSACTION SCRIPT ## ======================================================================================== @@ -667,7 +631,7 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { ## Update account storage item ## ------------------------------------------------------------------------------------ # push a new value for the storage slot onto the stack - push.{UPDATED_SLOT_VALUE} + push.{updated_slot_value} # => [13, 11, 9, 7] # get the index of account storage slot @@ -680,11 +644,11 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { ## Update account storage map ## ------------------------------------------------------------------------------------ # push a new VALUE for the storage map onto the stack - push.{UPDATED_MAP_VALUE} + push.{updated_map_value} # => [18, 19, 20, 21] # push a new KEY for the storage map onto the stack - push.{UPDATED_MAP_KEY} + push.{updated_map_key} # => [14, 15, 16, 17, 18, 19, 20, 21] # get the index of account storage slot @@ -701,10 +665,7 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { dropw dropw dropw dropw end - ", - UPDATED_SLOT_VALUE = updated_slot_value, - UPDATED_MAP_VALUE = updated_map_value, - UPDATED_MAP_KEY = updated_map_key, + " ); let tx_script = ScriptBuilder::with_mock_libraries()?.compile_tx_script(tx_script_src)?; @@ -720,7 +681,10 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { FungibleAsset::new(faucet_id_3, CONSUMED_ASSET_3_AMOUNT)?.into(); let nonfungible_asset_1: Asset = NonFungibleAsset::mock(&NON_FUNGIBLE_ASSET_DATA_2); - create_p2any_note(account.id(), &[fungible_asset_1, fungible_asset_3, nonfungible_asset_1]) + create_public_p2any_note( + account.id(), + [fungible_asset_1, fungible_asset_3, nonfungible_asset_1], + ) }; let tx_context = TransactionContextBuilder::new(account) @@ -734,7 +698,7 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { // expected delta // -------------------------------------------------------------------------------------------- // execute the transaction and get the witness - let executed_transaction = tx_context.execute_blocking()?; + let executed_transaction = tx_context.execute().await?; // nonce delta // -------------------------------------------------------------------------------------------- @@ -793,10 +757,122 @@ fn asset_and_storage_delta() -> anyhow::Result<()> { Ok(()) } +/// Tests that the storage map updates for a _new public_ account in an executed and proven +/// transaction match up. +/// +/// This is an interesting test case because: +/// - for new accounts in general, the storage map entries must be available in the advice provider +/// and the resulting delta must be convertible to a full account. +/// - it creates an account with two identical storage maps. +/// - The prover mutates the delta to account for fee logic. +#[tokio::test] +async fn proven_tx_storage_maps_matches_executed_tx_for_new_account() -> anyhow::Result<()> { + // Use two identical maps to test that they are properly handled + // (see also https://github.com/0xMiden/miden-base/issues/2037). + let map0 = StorageMap::with_entries([(rand_value(), rand_value())])?; + let map1 = map0.clone(); + let mut map2 = StorageMap::with_entries([ + (rand_value(), rand_value()), + (rand_value(), rand_value()), + (rand_value(), rand_value()), + (rand_value(), rand_value()), + ])?; + + // 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()), + ])) + .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 + + begin + # Update an existing key. + push.{value0} + push.{existing_key} + push.{map2_index} + # => [index, KEY, VALUE] + call.account::set_map_item + + exec.::std::sys::truncate_stack + end + " + ); + + let builder = ScriptBuilder::with_mock_libraries()?; + let source_manager = builder.source_manager(); + let tx_script = builder.compile_tx_script(code)?; + + let tx = TransactionContextBuilder::new(account.clone()) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; + + 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(); + assert_eq!( + map_delta + .entries() + .iter() + .map(|(key, value)| (*key.inner(), *value)) + .collect::>(), + expected_map.into_entries() + ); + } + + let proven_tx = LocalTransactionProver::default().prove_dummy(tx.clone())?; + + let AccountUpdateDetails::Delta(proven_tx_delta) = proven_tx.account_update().details() else { + panic!("expected delta"); + }; + + let proven_tx_account = Account::try_from(proven_tx_delta)?; + let exec_tx_account = Account::try_from(tx.account_delta())?; + + assert_eq!(proven_tx_account.storage(), exec_tx_account.storage()); + + // Check the conversion back into a full-state delta works correctly. + let proven_tx_delta_converted = AccountDelta::try_from(proven_tx_account)?; + let exec_tx_delta_converted = AccountDelta::try_from(exec_tx_account)?; + + // Check that the deltas from proven and executed tx, which were converted from accounts are + // identical. This is essentially a roundtrip test. + assert_eq!(&proven_tx_delta_converted, proven_tx_delta); + assert_eq!(&exec_tx_delta_converted, tx.account_delta()); + assert_eq!(&proven_tx_delta_converted, tx.account_delta()); + + // The commitments should match as well. + assert_eq!(proven_tx_delta_converted.to_commitment(), proven_tx_delta.to_commitment()); + assert_eq!(exec_tx_delta_converted.to_commitment(), tx.account_delta().to_commitment()); + assert_eq!(proven_tx_delta_converted.to_commitment(), tx.account_delta().to_commitment()); + + Ok(()) +} + /// Tests that adding a fungible asset with amount zero to the account vault works and does not /// result in an account delta entry. -#[test] -fn adding_amount_zero_fungible_asset_to_account_vault_works() -> anyhow::Result<()> { +#[tokio::test] +async fn adding_amount_zero_fungible_asset_to_account_vault_works() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let account = builder.add_existing_mock_account(Auth::IncrNonce)?; let input_note = builder.add_p2id_note( @@ -810,7 +886,8 @@ fn adding_amount_zero_fungible_asset_to_account_vault_works() -> anyhow::Result< let tx = chain .build_tx_context(account, &[input_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; assert!(tx.account_delta().vault().is_empty()); @@ -823,22 +900,36 @@ fn adding_amount_zero_fungible_asset_to_account_vault_works() -> anyhow::Result< struct TestSetup { mock_chain: MockChain, account_id: AccountId, + notes: Vec, } fn setup_test( storage_slots: impl IntoIterator, - assets: impl IntoIterator, + vault_assets: impl IntoIterator, + note_assets: impl IntoIterator, ) -> anyhow::Result { let mut builder = MockChain::builder(); let account = builder.add_existing_mock_account_with_storage_and_assets( Auth::IncrNonce, storage_slots, - assets, + vault_assets, )?; + let mut notes = vec![]; + for note_asset in note_assets { + let added_note = builder + .add_p2id_note(account.id(), account.id(), &[note_asset], NoteType::Public) + .context("failed to add note with asset")?; + notes.push(added_note); + } + let mock_chain = builder.build()?; - Ok(TestSetup { mock_chain, account_id: account.id() }) + Ok(TestSetup { + mock_chain, + account_id: account.id(), + notes, + }) } fn compile_tx_script(code: impl AsRef) -> anyhow::Result { @@ -857,7 +948,7 @@ fn compile_tx_script(code: impl AsRef) -> anyhow::Result const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " use.mock::account - use.miden::tx + use.miden::output_note #! Inputs: [index, VALUE] #! Outputs: [] @@ -910,7 +1001,7 @@ const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = " repeat.8 push.0 movdn.8 end # => [tag, aux, note_type, execution_hint, RECIPIENT, pad(8)] - call.tx::create_note + call.output_note::create # => [note_idx, pad(15)] repeat.15 swap drop end 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 8d4deaeee8..133f437676 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 @@ -1,20 +1,32 @@ +use alloc::string::{String, ToString}; use alloc::vec::Vec; use assert_matches::assert_matches; -use miden_lib::note::{create_p2id_note, create_p2ide_note}; +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::Word; use miden_objects::account::{Account, AccountId}; -use miden_objects::asset::FungibleAsset; -use miden_objects::note::{Note, NoteType}; +use miden_objects::asset::{Asset, FungibleAsset}; +use miden_objects::crypto::rand::FeltRng; +use miden_objects::note::{ + Note, + NoteAssets, + NoteExecutionHint, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteTag, + NoteType, +}; use miden_objects::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; +use miden_objects::transaction::{InputNote, OutputNote}; +use miden_objects::{Felt, StarkField, Word, ZERO}; use miden_processor::ExecutionError; use miden_processor::crypto::RpoRandomCoin; use miden_tx::auth::UnreachableAuth; @@ -22,19 +34,17 @@ use miden_tx::{ FailedNote, NoteConsumptionChecker, NoteConsumptionInfo, - NoteConsumptionStatus, TransactionExecutor, TransactionExecutorError, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use crate::utils::create_p2any_note; +use crate::utils::create_public_p2any_note; use crate::{Auth, MockChain, TransactionContextBuilder, TxContextInput}; #[tokio::test] async fn check_note_consumability_well_known_notes_success() -> anyhow::Result<()> { - let (_, authenticator) = Auth::BasicAuth.build_component(); let p2id_note = create_p2id_note( ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), @@ -55,31 +65,30 @@ async fn check_note_consumability_well_known_notes_success() -> anyhow::Result<( &mut RpoRandomCoin::new(Word::from([2u32; 4])), )?; - let notes = vec![p2ide_note, p2id_note]; + let notes = vec![p2id_note, p2ide_note]; let tx_context = TransactionContextBuilder::with_existing_mock_account() .extend_input_notes(notes.clone()) - .authenticator(authenticator) .build()?; - let input_notes = tx_context.input_notes().clone(); let target_account_id = tx_context.account().id(); let block_ref = tx_context.tx_inputs().block_header().block_num(); let tx_args = tx_context.tx_args().clone(); - let executor = TransactionExecutor::new(&tx_context) - .with_authenticator(tx_context.authenticator().unwrap()) - .with_tracing(); + let executor = + TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); let notes_checker = NoteConsumptionChecker::new(&executor); - let execution_check_result = notes_checker - .check_notes_consumability(target_account_id, block_ref, input_notes, tx_args) + let consumption_info = notes_checker + .check_notes_consumability(target_account_id, block_ref, notes.clone(), tx_args) .await?; - assert_matches!(execution_check_result, NoteConsumptionInfo { successful, failed, .. } => { + assert_matches!(consumption_info, NoteConsumptionInfo { successful, failed, .. } => { assert_eq!(successful.len(), notes.len()); - successful.iter().zip(notes.iter()).for_each(|(success, note)| { - assert_eq!(success, note); - }); + + // we asserted that `successful` and `notes` vectors have the same length, so it's safe to + // check their equality that way + successful.iter().for_each(|successful_note| assert!(notes.contains(successful_note))); + assert!(failed.is_empty()); }); @@ -87,8 +96,7 @@ async fn check_note_consumability_well_known_notes_success() -> anyhow::Result<( } #[rstest::rstest] -#[case::empty(vec![])] -#[case::one(vec![create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)])])] +#[case::one(vec![create_public_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), [FungibleAsset::mock(100)])])] #[tokio::test] async fn check_note_consumability_custom_notes_success( #[case] notes: Vec, @@ -103,7 +111,6 @@ async fn check_note_consumability_custom_notes_success( .build()? }; - let input_notes = tx_context.input_notes().clone(); let account_id = tx_context.account().id(); let block_ref = tx_context.tx_inputs().block_header().block_num(); let tx_args = tx_context.tx_args().clone(); @@ -113,11 +120,11 @@ async fn check_note_consumability_custom_notes_success( .with_tracing(); let notes_checker = NoteConsumptionChecker::new(&executor); - let execution_check_result = notes_checker - .check_notes_consumability(account_id, block_ref, input_notes, tx_args) + let consumption_info = notes_checker + .check_notes_consumability(account_id, block_ref, notes.clone(), tx_args) .await?; - assert_matches!(execution_check_result, NoteConsumptionInfo { successful, failed, .. }=> { + assert_matches!(consumption_info, NoteConsumptionInfo { successful, failed, .. }=> { if notes.is_empty() { assert!(successful.is_empty()); assert!(failed.is_empty()); @@ -131,7 +138,7 @@ async fn check_note_consumability_custom_notes_success( #[tokio::test] async fn check_note_consumability_partial_success() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let account = builder.add_existing_wallet(Auth::IncrNonce)?; let sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); @@ -173,46 +180,45 @@ async fn check_note_consumability_partial_success() -> anyhow::Result<()> { )?; let mock_chain = builder.build()?; + let notes = vec![ + successful_note_2.clone(), + successful_note_1.clone(), + failing_note_2.clone(), + failing_note_1.clone(), + successful_note_3.clone(), + ]; let tx_context = mock_chain - .build_tx_context( - TxContextInput::Account(account), - &[], - &[ - successful_note_2.clone(), - successful_note_1.clone(), - failing_note_2.clone(), - failing_note_1.clone(), - successful_note_3.clone(), - ], - )? + .build_tx_context(TxContextInput::Account(account), &[], ¬es)? .build()?; - let input_notes = tx_context.input_notes().clone(); let account_id = tx_context.account().id(); let block_ref = tx_context.tx_inputs().block_header().block_num(); let tx_args = tx_context.tx_args().clone(); - let executor = TransactionExecutor::new(&tx_context) - .with_authenticator(tx_context.authenticator().unwrap()); + let executor = + TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); let notes_checker = NoteConsumptionChecker::new(&executor); - let execution_check_result = notes_checker - .check_notes_consumability(account_id, block_ref, input_notes, tx_args) + let consumption_info = notes_checker + .check_notes_consumability(account_id, block_ref, notes, tx_args) .await?; assert_matches!( - execution_check_result, + consumption_info, NoteConsumptionInfo { successful, failed } => { + assert_eq!(failed.len(), 2); + assert_eq!(successful.len(), 3); + // First failing note. assert_matches!( failed.first().expect("first failed notes should exist"), FailedNote { note, - error: Some(TransactionExecutorError::TransactionProgramExecutionFailed( - ExecutionError::DivideByZero { .. })) + error: TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::DivideByZero { .. }) } => { assert_eq!( note.id(), @@ -225,8 +231,8 @@ async fn check_note_consumability_partial_success() -> anyhow::Result<()> { failed.get(1).expect("second failed note should exist"), FailedNote { note, - error: Some(TransactionExecutorError::TransactionProgramExecutionFailed( - ExecutionError::DivideByZero { .. })) + error: TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::DivideByZero { .. }) } => { assert_eq!( note.id(), @@ -247,6 +253,8 @@ async fn check_note_consumability_partial_success() -> anyhow::Result<()> { #[tokio::test] async fn check_note_consumability_epilogue_failure() -> anyhow::Result<()> { let mut builder = MockChain::builder(); + + // Use basic auth which will cause epilogue failure when paired up with unreachable auth. let account = builder.add_existing_wallet(Auth::BasicAuth)?; let successful_note = builder.add_p2id_note( @@ -257,26 +265,26 @@ async fn check_note_consumability_epilogue_failure() -> anyhow::Result<()> { )?; let mock_chain = builder.build()?; + let notes = vec![successful_note.clone()]; let tx_context = mock_chain - .build_tx_context(TxContextInput::Account(account), &[], &[successful_note])? + .build_tx_context(TxContextInput::Account(account), &[], ¬es)? .build()?; - let input_notes = tx_context.input_notes().clone(); let account_id = tx_context.account().id(); let block_ref = tx_context.tx_inputs().block_header().block_num(); let tx_args = tx_context.tx_args().clone(); - // Use an auth that fails in order to force an epilogue failure. + // Use an auth that fails in order to force an epilogue failure when paired up with basic auth. let executor = TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); let notes_checker = NoteConsumptionChecker::new(&executor); - let execution_check_result = notes_checker - .check_notes_consumability(account_id, block_ref, input_notes, tx_args) + let consumption_info = notes_checker + .check_notes_consumability(account_id, block_ref, notes, tx_args) .await?; assert_matches!( - execution_check_result, + consumption_info, NoteConsumptionInfo { successful, failed @@ -288,6 +296,122 @@ async fn check_note_consumability_epilogue_failure() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn check_note_consumability_epilogue_failure_with_new_combination() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_wallet(Auth::IncrNonce)?; + + // Prepare set of notes expected to succeed despite the fact that they will be grouped with + // notes that cause epilogue failure and transaction execution failure. The epilogue failure + // in particular will cause the note checker to execute + // `find_largest_executable_combination()` which this test is mainly concerned about. + let successful_note_1 = builder.add_p2id_note( + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(10)], + NoteType::Public, + )?; + let successful_note_2 = builder.add_p2id_note( + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(145)], + NoteType::Public, + )?; + let sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); + let successful_note_3 = NoteBuilder::new( + sender, + ChaCha20Rng::from_seed(ChaCha20Rng::from_seed([0_u8; 32]).random()), + ) + .code("begin push.1 drop push.1 div end") + .dynamically_linked_libraries([TransactionKernel::library()]) + .build()?; + let failing_note_1 = NoteBuilder::new( + sender, + ChaCha20Rng::from_seed(ChaCha20Rng::from_seed([0_u8; 32]).random()), + ) + .code("begin push.1 drop push.0 div end") + .dynamically_linked_libraries([TransactionKernel::library()]) + .build()?; + + // Create a note that causes epilogue failure. Adds assets to the transaction without moving + // them anywhere which causes an "asset imbalance" that violates the asset preservation rules. + let note_asset = FungibleAsset::mock(700).unwrap_fungible(); + let fail_epilogue_note = NoteBuilder::new(account.id(), &mut rand::rng()) + .add_assets([Asset::from(note_asset)]) + .build()?; + builder.add_output_note(OutputNote::Full(fail_epilogue_note.clone())); + + let mock_chain = builder.build()?; + let notes = vec![ + successful_note_1.clone(), + fail_epilogue_note.clone(), + successful_note_2.clone(), + failing_note_1.clone(), + successful_note_3.clone(), + ]; + let tx_context = mock_chain + .build_tx_context(TxContextInput::Account(account), &[], ¬es)? + .build()?; + + let account_id = tx_context.account().id(); + let block_ref = tx_context.tx_inputs().block_header().block_num(); + let tx_args = tx_context.tx_args().clone(); + + let executor = + TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); + let notes_checker = NoteConsumptionChecker::new(&executor); + + let consumption_info = notes_checker + .check_notes_consumability(account_id, block_ref, notes, tx_args) + .await?; + + assert_matches!( + consumption_info, + NoteConsumptionInfo { + successful, + failed + } => { + assert_eq!(failed.len(), 2); + assert_eq!(successful.len(), 3); + + // First failing note should be the note that does not cause epilogue failure. + assert_matches!( + failed.first().expect("first failed notes should exist"), + FailedNote { + note, + error: TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::DivideByZero { .. }) + } => { + assert_eq!( + note.id(), + failing_note_1.id(), + ); + } + ); + // Second failing note should be the note that causes epilogue failure. + assert_matches!( + failed.get(1).expect("second failed note should exist"), + FailedNote { + note, + error: TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::FailedAssertion { .. }) + } => { + assert_eq!( + note.id(), + fail_epilogue_note.id(), + ); + } + ); + // Successful notes. + assert_eq!( + [successful[0].id(), successful[1].id(), successful[2].id()], + [successful_note_1.id(), successful_note_2.id(), successful_note_3.id()], + ); + } + ); + Ok(()) +} + #[tokio::test] async fn test_check_note_consumability_without_signatures() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -326,7 +450,349 @@ async fn test_check_note_consumability_without_signatures() -> anyhow::Result<() ) .await?; - assert_eq!(consumability_info, NoteConsumptionStatus::UnconsumableWithoutAuthorization); + assert_matches!(consumability_info, NoteConsumptionStatus::ConsumableWithAuthorization); Ok(()) } + +#[tokio::test] +async fn test_check_note_consumability_static_analysis_invalid_inputs() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let account = builder.add_existing_wallet(Auth::Noop)?; + let target_account_id = account.id(); + let sender_account_id = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(); + let wrong_target_id: AccountId = + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into().unwrap(); + + // create notes for testing + // -------------------------------------------------------------------------------------------- + let p2ide_wrong_inputs_number = create_p2ide_note_with_inputs([1, 2, 3], sender_account_id); + + let p2ide_invalid_target_id = create_p2ide_note_with_inputs([1, 2, 3, 4], sender_account_id); + + let p2ide_wrong_target = create_p2ide_note_with_inputs( + [wrong_target_id.suffix().as_int(), wrong_target_id.prefix().as_u64(), 3, 4], + sender_account_id, + ); + + let p2ide_invalid_reclaim = create_p2ide_note_with_inputs( + [ + target_account_id.suffix().as_int(), + target_account_id.prefix().as_u64(), + Felt::MODULUS - 1, + 4, + ], + sender_account_id, + ); + + let p2ide_invalid_timelock = create_p2ide_note_with_inputs( + [ + target_account_id.suffix().as_int(), + target_account_id.prefix().as_u64(), + 3, + Felt::MODULUS - 1, + ], + sender_account_id, + ); + + // finalize mock chain and create notes checker + // -------------------------------------------------------------------------------------------- + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx_context = mock_chain + .build_tx_context( + TxContextInput::Account(account), + &[], + &vec![ + p2ide_wrong_inputs_number.clone(), + p2ide_invalid_target_id.clone(), + p2ide_invalid_reclaim.clone(), + p2ide_invalid_timelock.clone(), + ], + )? + .build()?; + + let block_ref = tx_context.tx_inputs().block_header().block_num(); + let tx_args = tx_context.tx_args(); + let executor = + TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); + let notes_checker = NoteConsumptionChecker::new(&executor); + + // check the note with invalid number of inputs + // -------------------------------------------------------------------------------------------- + let consumability_info: NoteConsumptionStatus = notes_checker + .can_consume( + target_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide_wrong_inputs_number.clone() }, + tx_args.clone(), + ) + .await?; + assert_matches!(consumability_info, NoteConsumptionStatus::NeverConsumable(reason) => { + assert_eq!(reason.to_string(), format!( + "P2IDE note should have {} inputs, but {} was provided", + WellKnownNote::P2IDE.num_expected_inputs(), + p2ide_wrong_inputs_number.recipient().inputs().num_values() + )); + }); + + // check the note with invalid target account ID + // -------------------------------------------------------------------------------------------- + let consumability_info: NoteConsumptionStatus = notes_checker + .can_consume( + target_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide_invalid_target_id.clone() }, + tx_args.clone(), + ) + .await?; + assert_matches!(consumability_info, NoteConsumptionStatus::NeverConsumable(reason) => { + assert_eq!(reason.to_string(), "failed to create an account ID from the first two note inputs"); + }); + + // check the note with a wrong target account ID (target is neither the sender nor the receiver) + // -------------------------------------------------------------------------------------------- + let consumability_info: NoteConsumptionStatus = notes_checker + .can_consume( + target_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide_wrong_target.clone() }, + tx_args.clone(), + ) + .await?; + assert_matches!(consumability_info, NoteConsumptionStatus::NeverConsumable(reason) => { + assert_eq!(reason.to_string(), "target account of the transaction does not match neither the receiver account specified by the P2IDE inputs, nor the sender account"); + }); + + // check the note with an invalid reclaim height + // -------------------------------------------------------------------------------------------- + let consumability_info: NoteConsumptionStatus = notes_checker + .can_consume( + target_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide_invalid_reclaim.clone() }, + tx_args.clone(), + ) + .await?; + assert_matches!(consumability_info, NoteConsumptionStatus::NeverConsumable(reason) => { + assert_eq!(reason.to_string(), "reclaim block height should be a u32"); + }); + + // check the note with an invalid timelock height + // -------------------------------------------------------------------------------------------- + let consumability_info: NoteConsumptionStatus = notes_checker + .can_consume( + target_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide_invalid_timelock.clone() }, + tx_args.clone(), + ) + .await?; + assert_matches!(consumability_info, NoteConsumptionStatus::NeverConsumable(reason) => { + assert_eq!(reason.to_string(), "timelock block height should be a u32"); + }); + + Ok(()) +} + +/// Tests the correctness of the [`NoteConsumptionChecker::can_consume()`]. +/// +/// In this test the target account is the receiver. +/// +/// It is expected that the current block height is 3. +#[rstest::rstest] +// rc == tl == curr +#[case(3, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// rc < tl < curr +#[case(1, 2, String::from("Ok(ConsumableWithAuthorization)"))] +// rc < tl = curr +#[case(1, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// rc = tl < curr +#[case(1, 1, String::from("Ok(ConsumableWithAuthorization)"))] +// tl < rc < curr +#[case(2, 1, String::from("Ok(ConsumableWithAuthorization)"))] +// tl < rc = curr +#[case(3, 1, String::from("Ok(ConsumableWithAuthorization)"))] +// curr < rc < tl +#[case(4, 5, String::from("Ok(ConsumableAfter(BlockNumber(5)))"))] +// curr < rc = tl +#[case(4, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// curr = rc < tl +#[case(3, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// rc < curr < tl +#[case(2, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// rc < curr = tl +#[case(2, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// curr < tl < rc +#[case(5, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// curr = tl < rc +#[case(4, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// tl < curr < rc +#[case(4, 2, String::from("Ok(ConsumableWithAuthorization)"))] +// tl < curr = rc +#[case(3, 2, String::from("Ok(ConsumableWithAuthorization)"))] +#[tokio::test] +async fn test_check_note_consumability_static_analysis_receiver( + #[case] reclaim_height: u64, + #[case] timelock_height: u64, + #[case] expected: String, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let account = builder.add_existing_wallet(Auth::Noop)?; + let target_account_id = account.id(); + let sender_account_id = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(); + + let p2ide = create_p2ide_note_with_inputs( + [ + target_account_id.suffix().as_int(), + target_account_id.prefix().as_u64(), + reclaim_height, + timelock_height, + ], + sender_account_id, + ); + builder.add_output_note(OutputNote::Full(p2ide.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_until_block(3)?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::Account(account), &[p2ide.id()], &[])? + .build()?; + + let block_ref = tx_context.tx_inputs().block_header().block_num(); + let tx_args = tx_context.tx_args(); + + let executor = + TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); + let notes_checker = NoteConsumptionChecker::new(&executor); + + // check the note with invalid number of inputs + // -------------------------------------------------------------------------------------------- + let consumption_check_result = notes_checker + .can_consume( + target_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide }, + tx_args.clone(), + ) + .await; + + assert_eq!(format!("{:?}", consumption_check_result), expected); + + Ok(()) +} + +/// Tests the correctness of the [`NoteConsumptionChecker::can_consume()`] procedure. +/// +/// In this test the target account is the sender. +/// +/// It is expected that the current block height is 3. +#[rstest::rstest] +// rc == tl == curr +#[case(3, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// rc < tl < curr +#[case(1, 2, String::from("Ok(ConsumableWithAuthorization)"))] +// rc < tl = curr +#[case(1, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// rc = tl < curr +#[case(1, 1, String::from("Ok(ConsumableWithAuthorization)"))] +// tl < rc < curr +#[case(2, 1, String::from("Ok(ConsumableWithAuthorization)"))] +// tl < rc = curr +#[case(3, 1, String::from("Ok(ConsumableWithAuthorization)"))] +// curr < rc < tl +#[case(4, 5, String::from("Ok(ConsumableAfter(BlockNumber(5)))"))] +// curr < rc = tl +#[case(4, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// curr = rc < tl +#[case(3, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// rc < curr < tl +#[case(2, 4, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// rc < curr = tl +#[case(2, 3, String::from("Ok(ConsumableWithAuthorization)"))] +// curr < tl < rc +#[case(5, 4, String::from("Ok(ConsumableAfter(BlockNumber(5)))"))] +// curr = tl < rc +#[case(4, 3, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// tl < curr < rc +#[case(4, 2, String::from("Ok(ConsumableAfter(BlockNumber(4)))"))] +// tl < curr = rc +#[case(3, 2, String::from("Ok(ConsumableWithAuthorization)"))] +#[tokio::test] +async fn test_check_note_consumability_static_analysis_sender( + #[case] reclaim_height: u64, + #[case] timelock_height: u64, + #[case] expected: String, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let account = builder.add_existing_wallet(Auth::Noop)?; + let sender_account_id = account.id(); + let target_account_id: AccountId = + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(); + + let p2ide = create_p2ide_note_with_inputs( + [ + target_account_id.suffix().as_int(), + target_account_id.prefix().as_u64(), + reclaim_height, + timelock_height, + ], + sender_account_id, + ); + builder.add_output_note(OutputNote::Full(p2ide.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_until_block(3)?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::Account(account), &[p2ide.id()], &[])? + .build()?; + + let block_ref = tx_context.tx_inputs().block_header().block_num(); + let tx_args = tx_context.tx_args(); + + let executor = + TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context).with_tracing(); + let notes_checker = NoteConsumptionChecker::new(&executor); + + // check the note with invalid number of inputs + // -------------------------------------------------------------------------------------------- + let consumption_check_result = notes_checker + .can_consume( + sender_account_id, + block_ref, + InputNote::Unauthenticated { note: p2ide }, + tx_args.clone(), + ) + .await; + + assert_eq!(format!("{:?}", consumption_check_result), expected); + + Ok(()) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Creates a mock P2IDE note with the specified note inputs. +fn create_p2ide_note_with_inputs(inputs: impl IntoIterator, sender: AccountId) -> Note { + let serial_num = RpoRandomCoin::new(Default::default()).draw_word(); + let note_script = WellKnownNote::P2IDE.script(); + let recipient = NoteRecipient::new( + serial_num, + note_script, + 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(); + + 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 new file mode 100644 index 0000000000..ae4122d326 --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs @@ -0,0 +1,543 @@ +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::{ + Note, + NoteAssets, + NoteExecutionHint, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteTag, + NoteType, +}; +use miden_objects::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 crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::create_public_p2any_note; +use crate::{ + Auth, + MockChain, + TransactionContextBuilder, + TxContextInput, + assert_transaction_executor_error, +}; + +#[tokio::test] +async fn test_active_note_get_sender_fails_from_tx_script() -> anyhow::Result<()> { + // Creates a mockchain with an account and a note + let mut builder = MockChain::builder(); + let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let p2id_note = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(150)], + NoteType::Public, + )?; + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let code = " + use.miden::active_note + + begin + # try to get the sender from transaction script + exec.active_note::get_sender + end + "; + let tx_script = ScriptBuilder::default() + .compile_tx_script(code) + .context("failed to compile tx script")?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[p2id_note.id()], &[])? + .tx_script(tx_script) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!( + result, + ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_METADATA_WHILE_NO_NOTE_BEING_PROCESSED + ); + + Ok(()) +} + +#[tokio::test] +async fn test_active_note_get_metadata() -> anyhow::Result<()> { + let tx_context = { + let account = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); + let input_note = create_public_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + [FungibleAsset::mock(100)], + ); + TransactionContextBuilder::new(account) + .extend_input_notes(vec![input_note]) + .build()? + }; + + let code = format!( + r#" + use.$kernel::prologue + use.$kernel::note->note_internal + use.miden::active_note + + begin + exec.prologue::prepare_transaction + exec.note_internal::prepare_note + dropw dropw dropw dropw + + # get the metadata of the active note + exec.active_note::get_metadata + # => [METADATA] + + # assert this metadata + push.{METADATA} + 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()) + ); + + tx_context.execute_code(&code).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_active_note_get_sender() -> anyhow::Result<()> { + let tx_context = { + let account = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); + let input_note = create_public_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + [FungibleAsset::mock(100)], + ); + TransactionContextBuilder::new(account) + .extend_input_notes(vec![input_note]) + .build()? + }; + + // calling get_sender should return sender of the active note + let code = " + use.$kernel::prologue + use.$kernel::note->note_internal + use.miden::active_note + + begin + exec.prologue::prepare_transaction + exec.note_internal::prepare_note + dropw dropw dropw dropw + exec.active_note::get_sender + + # truncate the stack + swapw dropw + end + "; + + let exec_output = tx_context.execute_code(code).await?; + + let sender = tx_context.input_notes().get_note(0).note().metadata().sender(); + assert_eq!(exec_output.stack[0], sender.prefix().as_felt()); + assert_eq!(exec_output.stack[1], sender.suffix()); + + Ok(()) +} + +#[tokio::test] +async fn test_active_note_get_assets() -> anyhow::Result<()> { + // Creates a mockchain with an account and a note that it can consume + let tx_context = { + let mut builder = MockChain::builder(); + let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let p2id_note_1 = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(150)], + NoteType::Public, + )?; + let p2id_note_2 = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(300)], + NoteType::Public, + )?; + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + mock_chain + .build_tx_context( + TxContextInput::AccountId(account.id()), + &[], + &[p2id_note_1, p2id_note_2], + )? + .build()? + }; + + let notes = tx_context.input_notes(); + + const DEST_POINTER_NOTE_0: u32 = 100000000; + const DEST_POINTER_NOTE_1: u32 = 200000000; + + fn construct_asset_assertions(note: &Note) -> String { + let mut code = String::new(); + for asset in note.assets().iter() { + code += &format!( + " + # assert the asset is correct + dup padw movup.4 mem_loadw_be push.{asset} assert_eqw push.4 add + ", + asset = Word::from(asset) + ); + } + code + } + + // calling get_assets should return assets at the specified address + let code = format!( + " + use.std::sys + + use.$kernel::prologue + use.$kernel::note->note_internal + use.miden::active_note + + proc.process_note_0 + # drop the note inputs + dropw dropw dropw dropw + + # set the destination pointer for note 0 assets + push.{DEST_POINTER_NOTE_0} + + # get the assets + exec.active_note::get_assets + + # assert the number of assets is correct + eq.{note_0_num_assets} assert + + # assert the pointer is returned + dup eq.{DEST_POINTER_NOTE_0} assert + + # asset memory assertions + {NOTE_0_ASSET_ASSERTIONS} + + # clean pointer + drop + end + + proc.process_note_1 + # drop the note inputs + dropw dropw dropw dropw + + # set the destination pointer for note 1 assets + push.{DEST_POINTER_NOTE_1} + + # get the assets + exec.active_note::get_assets + + # assert the number of assets is correct + eq.{note_1_num_assets} assert + + # assert the pointer is returned + dup eq.{DEST_POINTER_NOTE_1} assert + + # asset memory assertions + {NOTE_1_ASSET_ASSERTIONS} + + # clean pointer + drop + end + + begin + # prepare tx + exec.prologue::prepare_transaction + + # prepare note 0 + exec.note_internal::prepare_note + + # process note 0 + call.process_note_0 + + # increment active input note pointer + exec.note_internal::increment_active_input_note_ptr + + # prepare note 1 + exec.note_internal::prepare_note + + # process note 1 + call.process_note_1 + + # 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()), + NOTE_1_ASSET_ASSERTIONS = construct_asset_assertions(notes.get_note(1).note()), + ); + + tx_context.execute_code(&code).await?; + Ok(()) +} + +#[tokio::test] +async fn test_active_note_get_inputs() -> anyhow::Result<()> { + // Creates a mockchain with an account and a note that it can consume + let tx_context = { + let mut builder = MockChain::builder(); + let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let p2id_note = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(100)], + NoteType::Public, + )?; + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note])? + .build()? + }; + + fn construct_inputs_assertions(note: &Note) -> String { + let mut code = String::new(); + for inputs_chunk in note.inputs().values().chunks(WORD_SIZE) { + let mut inputs_word = EMPTY_WORD; + inputs_word.as_mut_slice()[..inputs_chunk.len()].copy_from_slice(inputs_chunk); + + code += &format!( + r#" + # assert the inputs are correct + # => [dest_ptr] + dup padw movup.4 mem_loadw_be push.{inputs_word} assert_eqw.err="inputs are incorrect" + # => [dest_ptr] + + push.4 add + # => [dest_ptr+4] + "# + ); + } + code + } + + 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 + + begin + # => [BH, acct_id, IAH, NC] + exec.prologue::prepare_transaction + # => [] + + exec.note_internal::prepare_note + # => [note_script_root_ptr, NOTE_ARGS, pad(11)] + + # clean the stack + dropw dropw dropw dropw + # => [] + + push.{NOTE_0_PTR} exec.active_note::get_inputs + # => [num_inputs, dest_ptr] + + eq.{num_inputs} assert + # => [dest_ptr] + + dup eq.{NOTE_0_PTR} assert + # => [dest_ptr] + + # apply note 1 inputs assertions + {inputs_assertions} + # => [dest_ptr] + + # clear the stack + drop + # => [] + end + ", + num_inputs = note0.inputs().num_values(), + inputs_assertions = construct_inputs_assertions(note0), + NOTE_0_PTR = 100000000, + ); + + tx_context.execute_code(&code).await?; + Ok(()) +} + +/// 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. +/// +/// 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) +/// for more details. +#[tokio::test] +async fn test_active_note_get_exactly_8_inputs() -> anyhow::Result<()> { + let sender_id = ACCOUNT_ID_SENDER + .try_into() + .context("failed to convert ACCOUNT_ID_SENDER to account ID")?; + let target_id = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into().context( + "failed to convert ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE to account ID", + )?; + + // 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 vault = NoteAssets::new(vec![]).context("failed to create input note assets")?; + let note_script = ScriptBuilder::default() + .compile_note_script("begin nop end") + .context("failed to compile note script")?; + + // create a recipient with note inputs, which number divides by 8. For simplicity create 8 input + // values + let recipient = NoteRecipient::new( + serial_num, + note_script, + NoteInputs::new(vec![ + ONE, + Felt::new(2), + Felt::new(3), + Felt::new(4), + Felt::new(5), + Felt::new(6), + Felt::new(7), + Felt::new(8), + ]) + .context("failed to create note inputs")?, + ); + let input_note = Note::new(vault.clone(), metadata, recipient); + + // provide this input note to the transaction context + let tx_context = TransactionContextBuilder::with_existing_mock_account() + .extend_input_notes(vec![input_note]) + .build()?; + + let tx_code = " + use.$kernel::prologue + use.miden::active_note + + begin + exec.prologue::prepare_transaction + + # execute the `get_inputs` procedure to trigger note inputs length assertion + push.0 exec.active_note::get_inputs + # => [num_inputs, 0] + + # assert that the inputs length is 8 + push.8 assert_eq.err=\"number of inputs values should be equal to 8\" + + # clean the stack + drop + end + "; + + tx_context.execute_code(tx_code).await.context("transaction execution failed")?; + + Ok(()) +} + +#[tokio::test] +async fn test_active_note_get_serial_number() -> anyhow::Result<()> { + let tx_context = { + let mut builder = MockChain::builder(); + let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let p2id_note_1 = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(150)], + NoteType::Public, + )?; + let mock_chain = builder.build()?; + + mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1])? + .build()? + }; + + // calling get_serial_number should return the serial number of the active note + let code = " + use.$kernel::prologue + use.miden::active_note + + begin + exec.prologue::prepare_transaction + exec.active_note::get_serial_number + + # truncate the stack + swapw dropw + end + "; + + let exec_output = tx_context.execute_code(code).await?; + + let serial_number = tx_context.input_notes().get_note(0).note().serial_num(); + assert_eq!(exec_output.get_stack_word_be(0), serial_number); + Ok(()) +} + +#[tokio::test] +async fn test_active_note_get_script_root() -> anyhow::Result<()> { + let tx_context = { + let mut builder = MockChain::builder(); + let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let p2id_note_1 = builder.add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account.id(), + &[FungibleAsset::mock(150)], + NoteType::Public, + )?; + let mock_chain = builder.build()?; + + mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1])? + .build()? + }; + + // calling get_script_root should return script root of the active note + let code = " + use.$kernel::prologue + use.miden::active_note + + begin + exec.prologue::prepare_transaction + exec.active_note::get_script_root + + # truncate the stack + swapw dropw + end + "; + + let exec_output = tx_context.execute_code(code).await?; + + let script_root = tx_context.input_notes().get_note(0).note().script().root(); + assert_eq!(exec_output.get_stack_word_be(0), script_root); + Ok(()) +} 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 e6954c2d42..092e2608ac 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -6,12 +6,13 @@ use miden_objects::testing::constants::{ FUNGIBLE_FAUCET_INITIAL_BALANCE, NON_FUNGIBLE_ASSET_DATA, }; +use miden_objects::{Felt, Hasher, Word}; -use super::{Felt, Hasher, Word}; use crate::TransactionContextBuilder; +use crate::kernel_tests::tx::ExecutionOutputExt; -#[test] -fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_fungible_faucet( ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), @@ -21,14 +22,14 @@ fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { let code = format!( " use.$kernel::prologue - use.miden::asset + use.miden::faucet begin exec.prologue::prepare_transaction # create fungible asset push.{FUNGIBLE_ASSET_AMOUNT} - exec.asset::create_fungible_asset + exec.faucet::create_fungible_asset # truncate the stack swapw dropw @@ -36,11 +37,11 @@ fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { " ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); assert_eq!( - process.stack.get_word(0), + exec_output.get_stack_word_be(0), Word::from([ Felt::new(FUNGIBLE_ASSET_AMOUNT), Felt::new(0), @@ -51,8 +52,8 @@ fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { Ok(()) } -#[test] -fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; @@ -62,14 +63,14 @@ fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { let code = format!( " use.$kernel::prologue - use.miden::asset + use.miden::faucet begin exec.prologue::prepare_transaction # push non-fungible asset data hash onto the stack push.{non_fungible_asset_data_hash} - exec.asset::create_non_fungible_asset + exec.faucet::create_non_fungible_asset # truncate the stack swapw dropw @@ -78,14 +79,14 @@ fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { non_fungible_asset_data_hash = Hasher::hash(&NON_FUNGIBLE_ASSET_DATA), ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; + assert_eq!(exec_output.get_stack_word_be(0), Word::from(non_fungible_asset)); - assert_eq!(process.stack.get_word(0), Word::from(non_fungible_asset)); Ok(()) } -#[test] -fn test_validate_non_fungible_asset() -> anyhow::Result<()> { +#[tokio::test] +async fn test_validate_non_fungible_asset() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; @@ -97,18 +98,17 @@ fn test_validate_non_fungible_asset() -> anyhow::Result<()> { use.$kernel::asset begin - push.{asset} + push.{non_fungible_asset} exec.asset::validate_non_fungible_asset # truncate the stack swapw dropw end - ", - asset = non_fungible_asset + " ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; - assert_eq!(process.stack.get_word(0), non_fungible_asset); + assert_eq!(exec_output.get_stack_word_be(0), non_fungible_asset); Ok(()) } 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 5e559e638b..37b492d8cc 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 @@ -7,7 +7,6 @@ use miden_lib::errors::tx_kernel_errors::{ ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND, }; use miden_lib::transaction::memory; -use miden_objects::AssetVaultError; use miden_objects::account::AccountId; use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; use miden_objects::testing::account_id::{ @@ -16,27 +15,27 @@ use miden_objects::testing::account_id::{ 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 super::{Felt, ONE, Word, ZERO}; -use crate::kernel_tests::tx::ProcessMemoryExt; +use crate::kernel_tests::tx::ExecutionOutputExt; use crate::{TransactionContextBuilder, assert_execution_error}; /// Tests that account::get_balance returns the correct amount. -#[test] -fn get_balance_returns_correct_amount() -> anyhow::Result<()> { +#[tokio::test] +async fn get_balance_returns_correct_amount() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let code = format!( r#" use.$kernel::prologue - use.miden::account + use.miden::active_account begin exec.prologue::prepare_transaction - push.{suffix}.{prefix} - exec.account::get_balance + push.{suffix} push.{prefix} + exec.active_account::get_balance # => [balance] # truncate the stack @@ -47,10 +46,10 @@ fn get_balance_returns_correct_amount() -> anyhow::Result<()> { suffix = faucet_id.suffix(), ); - let process = tx_context.execute_code(&code)?; + let exec_output = tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get(0).as_int(), + exec_output.get_stack_element(0).as_int(), tx_context.account().vault().get_balance(faucet_id).unwrap() ); @@ -58,8 +57,8 @@ fn get_balance_returns_correct_amount() -> anyhow::Result<()> { } /// Tests that asset_vault::peek_balance returns the correct amount. -#[test] -fn peek_balance_returns_correct_amount() -> anyhow::Result<()> { +#[tokio::test] +async fn peek_balance_returns_correct_amount() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); @@ -73,8 +72,13 @@ fn peek_balance_returns_correct_amount() -> anyhow::Result<()> { begin exec.prologue::prepare_transaction - exec.memory::get_acct_vault_root_ptr - push.{suffix}.{prefix} + exec.memory::get_account_vault_root_ptr + push.{suffix} push.{prefix} + # => [prefix, suffix, account_vault_root_ptr, balance] + + # emit an event to fetch the merkle path for the asset since peek_balance does not do + # that + emit.event("miden::account::vault_before_get_balance") # => [prefix, suffix, account_vault_root_ptr, balance] exec.asset_vault::peek_balance @@ -88,45 +92,52 @@ fn peek_balance_returns_correct_amount() -> anyhow::Result<()> { suffix = faucet_id.suffix(), ); - let process = tx_context.execute_code(&code)?; + let exec_output = tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get(0).as_int(), + exec_output.get_stack_element(0).as_int(), tx_context.account().vault().get_balance(faucet_id).unwrap() ); Ok(()) } -#[test] -fn test_get_balance_non_fungible_fails() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; +#[tokio::test] +async fn test_get_balance_non_fungible_fails() -> anyhow::Result<()> { + // Disable lazy loading otherwise the handler will return an error before the transaction kernel + // can abort, which is what we want to test. + let tx_context = TransactionContextBuilder::with_existing_mock_account() + .disable_lazy_loading() + .build()?; let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(); let code = format!( " use.$kernel::prologue - use.miden::account + use.miden::active_account begin exec.prologue::prepare_transaction - push.{suffix}.{prefix} - exec.account::get_balance + push.{suffix} push.{prefix} + exec.active_account::get_balance end ", prefix = faucet_id.prefix().as_felt(), suffix = faucet_id.suffix(), ); - let process = tx_context.execute_code(&code); + let exec_result = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET); + assert_execution_error!( + exec_result, + ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET + ); Ok(()) } -#[test] -fn test_has_non_fungible_asset() -> anyhow::Result<()> { +#[tokio::test] +async fn test_has_non_fungible_asset() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let non_fungible_asset = tx_context.account().vault().assets().find(Asset::is_non_fungible).unwrap(); @@ -134,12 +145,12 @@ fn test_has_non_fungible_asset() -> anyhow::Result<()> { let code = format!( " use.$kernel::prologue - use.miden::account + use.miden::active_account begin exec.prologue::prepare_transaction push.{non_fungible_asset_key} - exec.account::has_non_fungible_asset + exec.active_account::has_non_fungible_asset # truncate the stack swap drop @@ -148,15 +159,15 @@ fn test_has_non_fungible_asset() -> anyhow::Result<()> { non_fungible_asset_key = Word::from(non_fungible_asset) ); - let process = tx_context.execute_code(&code)?; + let exec_output = tx_context.execute_code(&code).await?; - assert_eq!(process.stack.get(0), ONE); + assert_eq!(exec_output.get_stack_element(0), ONE); Ok(()) } -#[test] -fn test_add_fungible_asset_success() -> anyhow::Result<()> { +#[tokio::test] +async fn test_add_fungible_asset_success() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let mut account_vault = tx_context.account().vault().clone(); let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); @@ -186,23 +197,23 @@ fn test_add_fungible_asset_success() -> anyhow::Result<()> { FUNGIBLE_ASSET = Word::from(add_fungible_asset) ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get_word(0), - Into::::into(account_vault.add_asset(add_fungible_asset).unwrap()) + exec_output.get_stack_word_be(0), + Word::from(account_vault.add_asset(add_fungible_asset).unwrap()) ); assert_eq!( - process.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), account_vault.root() ); Ok(()) } -#[test] -fn test_add_non_fungible_asset_fail_overflow() -> anyhow::Result<()> { +#[tokio::test] +async fn test_add_non_fungible_asset_fail_overflow() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let mut account_vault = tx_context.account().vault().clone(); @@ -230,16 +241,16 @@ fn test_add_non_fungible_asset_fail_overflow() -> anyhow::Result<()> { FUNGIBLE_ASSET = Word::from(add_fungible_asset) ); - let process = tx_context.execute_code(&code); + let exec_result = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED); + assert_execution_error!(exec_result, ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED); assert!(account_vault.add_asset(add_fungible_asset).is_err()); Ok(()) } -#[test] -fn test_add_non_fungible_asset_success() -> anyhow::Result<()> { +#[tokio::test] +async fn test_add_non_fungible_asset_success() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?; let mut account_vault = tx_context.account().vault().clone(); @@ -264,23 +275,23 @@ fn test_add_non_fungible_asset_success() -> anyhow::Result<()> { FUNGIBLE_ASSET = Word::from(add_non_fungible_asset) ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get_word(0), - Into::::into(account_vault.add_asset(add_non_fungible_asset)?) + exec_output.get_stack_word_be(0), + Word::from(account_vault.add_asset(add_non_fungible_asset)?) ); assert_eq!( - process.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), account_vault.root() ); Ok(()) } -#[test] -fn test_add_non_fungible_asset_fail_duplicate() -> anyhow::Result<()> { +#[tokio::test] +async fn test_add_non_fungible_asset_fail_duplicate() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); @@ -303,16 +314,16 @@ fn test_add_non_fungible_asset_fail_duplicate() -> anyhow::Result<()> { NON_FUNGIBLE_ASSET = Word::from(non_fungible_asset) ); - let process = tx_context.execute_code(&code); + let exec_result = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS); + assert_execution_error!(exec_result, ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS); assert!(account_vault.add_asset(non_fungible_asset).is_err()); Ok(()) } -#[test] -fn test_remove_fungible_asset_success_no_balance_remaining() -> anyhow::Result<()> { +#[tokio::test] +async fn test_remove_fungible_asset_success_no_balance_remaining() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let mut account_vault = tx_context.account().vault().clone(); @@ -343,23 +354,23 @@ fn test_remove_fungible_asset_success_no_balance_remaining() -> anyhow::Result<( FUNGIBLE_ASSET = Word::from(remove_fungible_asset) ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get_word(0), - Into::::into(account_vault.remove_asset(remove_fungible_asset).unwrap()) + exec_output.get_stack_word_be(0), + Word::from(account_vault.remove_asset(remove_fungible_asset).unwrap()) ); assert_eq!( - process.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), account_vault.root() ); Ok(()) } -#[test] -fn test_remove_fungible_asset_fail_remove_too_much() -> anyhow::Result<()> { +#[tokio::test] +async fn test_remove_fungible_asset_fail_remove_too_much() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT + 1; @@ -385,15 +396,18 @@ fn test_remove_fungible_asset_fail_remove_too_much() -> anyhow::Result<()> { FUNGIBLE_ASSET = Word::from(remove_fungible_asset) ); - let process = tx_context.execute_code(&code); + let exec_result = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW); + assert_execution_error!( + exec_result, + ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW + ); Ok(()) } -#[test] -fn test_remove_fungible_asset_success_balance_remaining() -> anyhow::Result<()> { +#[tokio::test] +async fn test_remove_fungible_asset_success_balance_remaining() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let mut account_vault = tx_context.account().vault().clone(); @@ -424,23 +438,23 @@ fn test_remove_fungible_asset_success_balance_remaining() -> anyhow::Result<()> FUNGIBLE_ASSET = Word::from(remove_fungible_asset) ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get_word(0), - Into::::into(account_vault.remove_asset(remove_fungible_asset).unwrap()) + exec_output.get_stack_word_be(0), + Word::from(account_vault.remove_asset(remove_fungible_asset).unwrap()) ); assert_eq!( - process.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), account_vault.root() ); Ok(()) } -#[test] -fn test_remove_inexisting_non_fungible_asset_fails() -> anyhow::Result<()> { +#[tokio::test] +async fn test_remove_inexisting_non_fungible_asset_fails() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); @@ -470,9 +484,9 @@ fn test_remove_inexisting_non_fungible_asset_fails() -> anyhow::Result<()> { FUNGIBLE_ASSET = Word::from(non_existent_non_fungible_asset) ); - let process = tx_context.execute_code(&code); + let exec_result = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND); + assert_execution_error!(exec_result, ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND); assert_matches!( account_vault.remove_asset(non_existent_non_fungible_asset).unwrap_err(), AssetVaultError::NonFungibleAssetNotFound(err_asset) if err_asset == nonfungible, @@ -482,8 +496,8 @@ fn test_remove_inexisting_non_fungible_asset_fails() -> anyhow::Result<()> { Ok(()) } -#[test] -fn test_remove_non_fungible_asset_success() -> anyhow::Result<()> { +#[tokio::test] +async fn test_remove_non_fungible_asset_success() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); @@ -509,15 +523,15 @@ fn test_remove_non_fungible_asset_success() -> anyhow::Result<()> { FUNGIBLE_ASSET = Word::from(non_fungible_asset) ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get_word(0), - Into::::into(account_vault.remove_asset(non_fungible_asset).unwrap()) + exec_output.get_stack_word_be(0), + Word::from(account_vault.remove_asset(non_fungible_asset).unwrap()) ); assert_eq!( - process.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(memory::NATIVE_ACCT_VAULT_ROOT_PTR), account_vault.root() ); 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 bdd7772506..676bd9f4e9 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_auth.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_auth.rs @@ -7,8 +7,8 @@ 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 super::{Felt, ONE}; use crate::{Auth, TransactionContextBuilder, assert_transaction_executor_error}; pub const ERR_WRONG_ARGS: MasmError = MasmError::from_static_str(ERR_WRONG_ARGS_MSG); @@ -18,8 +18,8 @@ pub const ERR_WRONG_ARGS: MasmError = MasmError::from_static_str(ERR_WRONG_ARGS_ /// This test creates an account with a conditional auth component that expects specific /// auth arguments [97, 98, 99] to not error out. When the correct arguments are provided, /// the nonce is incremented (because of `incr_nonce_flag`). -#[test] -fn test_auth_procedure_args() -> anyhow::Result<()> { +#[tokio::test] +async fn test_auth_procedure_args() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ConditionalAuthComponent); @@ -32,7 +32,7 @@ fn test_auth_procedure_args() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::new(account).auth_args(auth_args.into()).build()?; - tx_context.execute_blocking().context("failed to execute transaction")?; + tx_context.execute().await.context("failed to execute transaction")?; Ok(()) } @@ -42,8 +42,8 @@ fn test_auth_procedure_args() -> anyhow::Result<()> { /// This test creates an account with a conditional auth component that expects specific /// auth arguments [97, 98, 99, incr_nonce_flag]. When incorrect arguments are provided /// (in this case [101, 102, 103]), the transaction should fail with an appropriate error message. -#[test] -fn test_auth_procedure_args_wrong_inputs() -> anyhow::Result<()> { +#[tokio::test] +async fn test_auth_procedure_args_wrong_inputs() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ConditionalAuthComponent); @@ -57,7 +57,7 @@ fn test_auth_procedure_args_wrong_inputs() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::new(account).auth_args(auth_args.into()).build()?; - let execution_result = tx_context.execute_blocking(); + let execution_result = tx_context.execute().await; assert_transaction_executor_error!(execution_result, ERR_WRONG_ARGS); @@ -65,8 +65,8 @@ fn test_auth_procedure_args_wrong_inputs() -> anyhow::Result<()> { } /// Tests that attempting to call the auth procedure manually from user code fails. -#[test] -fn test_auth_procedure_called_from_wrong_context() -> anyhow::Result<()> { +#[tokio::test] +async fn test_auth_procedure_called_from_wrong_context() -> anyhow::Result<()> { let (auth_component, _) = Auth::IncrNonce.build_component(); let account = AccountBuilder::new([42; 32]) @@ -77,7 +77,7 @@ 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.::auth_incr_nonce end "; @@ -87,7 +87,7 @@ fn test_auth_procedure_called_from_wrong_context() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::new(account).tx_script(tx_script).build()?; - let execution_result = tx_context.execute_blocking(); + let execution_result = tx_context.execute().await; assert_transaction_executor_error!( execution_result, 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 6138095ecf..9b778fd556 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs @@ -4,42 +4,35 @@ use alloc::vec::Vec; use miden_lib::errors::tx_kernel_errors::{ 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::transaction::{EXPIRATION_BLOCK_ELEMENT_IDX, TransactionKernel}; use miden_lib::utils::ScriptBuilder; use miden_objects::Word; use miden_objects::account::{Account, AccountDelta, AccountStorageDelta, AccountVaultDelta}; -use miden_objects::asset::{Asset, AssetVault, FungibleAsset}; +use miden_objects::asset::{Asset, FungibleAsset}; use miden_objects::note::{NoteTag, NoteType}; use miden_objects::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::testing::constants::{ - CONSUMED_ASSET_1_AMOUNT, - CONSUMED_ASSET_2_AMOUNT, - CONSUMED_ASSET_3_AMOUNT, -}; use miden_objects::transaction::{OutputNote, OutputNotes}; use miden_processor::{Felt, ONE}; -use rand::rng; use super::{ZERO, create_mock_notes_procedure}; -use crate::kernel_tests::tx::ProcessMemoryExt; -use crate::utils::{create_p2any_note, create_spawn_note}; +use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::{create_public_p2any_note, create_spawn_note}; use crate::{ Auth, MockChain, @@ -49,17 +42,21 @@ use crate::{ assert_transaction_executor_error, }; -#[test] -fn test_epilogue() -> anyhow::Result<()> { +#[tokio::test] +async fn test_epilogue() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); let tx_context = { - let output_note_1 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); + 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_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); - let input_note_2 = create_spawn_note(ACCOUNT_ID_SENDER.try_into()?, vec![&output_note_1])?; + 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)]) @@ -90,7 +87,7 @@ fn test_epilogue() -> anyhow::Result<()> { " ); - let process = tx_context.execute_code(&code)?; + let exec_output = tx_context.execute_code(&code).await?; // The final account is the initial account with the nonce incremented by one. let mut final_account = account.clone(); @@ -134,31 +131,31 @@ fn test_epilogue() -> anyhow::Result<()> { expected_stack.extend((13..16).map(|_| ZERO)); assert_eq!( - *process.stack.build_stack_outputs()?, + exec_output.stack.as_slice(), expected_stack.as_slice(), "Stack state after finalize_transaction does not contain the expected values" ); assert_eq!( - process.stack.depth(), + exec_output.stack.len(), 16, "The stack must be truncated to 16 elements after finalize_transaction" ); Ok(()) } -#[test] -fn test_compute_output_note_id() -> anyhow::Result<()> { +#[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_p2any_note(ACCOUNT_ID_SENDER.try_into()?, &[FungibleAsset::mock(100)]); + 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_p2any_note(ACCOUNT_ID_SENDER.try_into()?, &[FungibleAsset::mock(100)]); - let input_note_2 = create_spawn_note(ACCOUNT_ID_SENDER.try_into()?, vec![&output_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)]) @@ -187,11 +184,11 @@ fn test_compute_output_note_id() -> anyhow::Result<()> { " ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; assert_eq!( note.assets().commitment(), - process.get_kernel_mem_word( + exec_output.get_kernel_mem_word( OUTPUT_NOTE_SECTION_OFFSET + i * NOTE_MEM_SIZE + OUTPUT_NOTE_ASSET_COMMITMENT_OFFSET @@ -201,157 +198,121 @@ fn test_compute_output_note_id() -> anyhow::Result<()> { assert_eq!( Word::from(note.id()), - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + i * NOTE_MEM_SIZE), + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + i * NOTE_MEM_SIZE), "NOTE_ID didn't match expected value", ); } Ok(()) } -#[test] -fn test_epilogue_asset_preservation_violation_too_few_input() -> anyhow::Result<()> { +/// Tests that a transaction fails due to the asset preservation rules when the input note has an +/// asset with amount 100 and the output note has the same asset with amount 200. +#[tokio::test] +async fn epilogue_fails_when_num_output_assets_exceed_num_input_assets() -> anyhow::Result<()> { + // Create an input asset with amount 100 and an output asset with amount 200. + let input_asset = FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into()?, 100)?; + let output_asset = input_asset.add(input_asset)?; + let mut builder = MockChain::builder(); - let account = builder - .add_existing_mock_account_with_assets(Auth::IncrNonce, AssetVault::mock().assets())?; + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + // Add an input note that (automatically) adds its assets to the transaction's input vault, but + // _does not_ add the asset to the account. This is just to keep the test conceptually simple - + // there is no account involved. + let input_note = NoteBuilder::new(account.id(), *builder.rng_mut()) + .add_assets([Asset::from(input_asset)]) + .build()?; + builder.add_output_note(OutputNote::Full(input_note.clone())); let mock_chain = builder.build()?; - let fungible_asset_1: Asset = FungibleAsset::new( - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into()?, - CONSUMED_ASSET_1_AMOUNT, - )? - .into(); - let fungible_asset_2: Asset = FungibleAsset::new( - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2.try_into()?, - CONSUMED_ASSET_2_AMOUNT, - )? - .into(); - - let output_note_1 = NoteBuilder::new(account.id(), rng()) - .add_assets([fungible_asset_1]) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) - .build()?; - let output_note_2 = NoteBuilder::new(account.id(), rng()) - .add_assets([fungible_asset_2]) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) - .build()?; + let code = format!( + " + use.mock::account + use.mock::util + + begin + # create a note with the output asset + push.{OUTPUT_ASSET} + exec.util::create_random_note_with_asset + # => [] + end + ", + OUTPUT_ASSET = Word::from(output_asset), + ); - let input_note = create_spawn_note(account.id(), vec![&output_note_1, &output_note_2])?; + let builder = ScriptBuilder::with_mock_libraries()?; + let source_manager = builder.source_manager(); + let tx_script = builder.compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[input_note])? - .extend_expected_output_notes(vec![ - OutputNote::Full(output_note_1), - OutputNote::Full(output_note_2), - ]) + .tx_script(tx_script) + .with_source_manager(source_manager) .build()?; - let output_notes_data_procedure = - create_mock_notes_procedure(tx_context.expected_output_notes()); - - let code = format!( - " - use.$kernel::prologue - use.mock::account - use.$kernel::epilogue - - {output_notes_data_procedure} - - begin - exec.prologue::prepare_transaction - exec.create_mock_notes - exec.epilogue::finalize_transaction - - # truncate the stack - movupw.3 dropw movupw.3 dropw movup.9 drop - end - " + let exec_output = tx_context.execute().await; + assert_transaction_executor_error!( + exec_output, + ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME ); - let process = tx_context.execute_code(&code); - - assert_execution_error!(process, ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME); Ok(()) } -#[test] -fn test_epilogue_asset_preservation_violation_too_many_fungible_input() -> anyhow::Result<()> { +/// Tests that a transaction fails due to the asset preservation rules when the input note has an +/// asset with amount 200 and the output note has the same asset with amount 100. +#[tokio::test] +async fn epilogue_fails_when_num_input_assets_exceed_num_output_assets() -> anyhow::Result<()> { + // Create an input asset with amount 200 and an output asset with amount 100. + let output_asset = FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into()?, 100)?; + let input_asset = output_asset.add(output_asset)?; + let mut builder = MockChain::builder(); - let account = builder - .add_existing_mock_account_with_assets(Auth::IncrNonce, AssetVault::mock().assets())?; + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + // Add an input note that (automatically) adds its assets to the transaction's input vault, but + // _does not_ add the asset to the account. This is just to keep the test conceptually simple - + // there is no account involved. + let input_note = NoteBuilder::new(account.id(), *builder.rng_mut()) + .add_assets([Asset::from(output_asset)]) + .build()?; + builder.add_output_note(OutputNote::Full(input_note.clone())); let mock_chain = builder.build()?; - let fungible_asset_1: Asset = FungibleAsset::new( - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into()?, - CONSUMED_ASSET_1_AMOUNT, - )? - .into(); - let fungible_asset_2: Asset = FungibleAsset::new( - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2.try_into()?, - CONSUMED_ASSET_2_AMOUNT, - )? - .into(); - let fungible_asset_3: Asset = FungibleAsset::new( - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3.try_into()?, - CONSUMED_ASSET_3_AMOUNT, - )? - .into(); - - let output_note_1 = NoteBuilder::new(account.id(), rng()) - .add_assets([fungible_asset_1]) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) - .build()?; - let output_note_2 = NoteBuilder::new(account.id(), rng()) - .add_assets([fungible_asset_2]) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) - .build()?; - let output_note_3 = NoteBuilder::new(account.id(), rng()) - .add_assets([fungible_asset_3]) - .dynamically_linked_libraries(TransactionKernel::mock_libraries()) - .build()?; + let code = format!( + " + use.mock::account + use.mock::util + + begin + # create a note with the output asset + push.{OUTPUT_ASSET} + exec.util::create_random_note_with_asset + # => [] + end + ", + OUTPUT_ASSET = Word::from(input_asset), + ); - let input_note = create_spawn_note( - ACCOUNT_ID_SENDER.try_into()?, - vec![&output_note_1, &output_note_2, &output_note_3], - )?; + let builder = ScriptBuilder::with_mock_libraries()?; + let source_manager = builder.source_manager(); + let tx_script = builder.compile_tx_script(code)?; let tx_context = mock_chain .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[input_note])? - .extend_expected_output_notes(vec![ - OutputNote::Full(output_note_1), - OutputNote::Full(output_note_2), - ]) + .tx_script(tx_script) + .with_source_manager(source_manager) .build()?; - let output_notes_data_procedure = - create_mock_notes_procedure(tx_context.expected_output_notes()); - - let code = format!( - " - use.$kernel::prologue - use.mock::account - use.$kernel::epilogue - - {output_notes_data_procedure} - - begin - exec.prologue::prepare_transaction - exec.create_mock_notes - exec.epilogue::finalize_transaction - - # truncate the stack - movupw.3 dropw movupw.3 dropw movup.9 drop - end - " + let exec_output = tx_context.execute().await; + assert_transaction_executor_error!( + exec_output, + ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME ); - let process = tx_context.execute_code(&code); - - assert_execution_error!(process, ERR_EPILOGUE_TOTAL_NUMBER_OF_ASSETS_MUST_STAY_THE_SAME); Ok(()) } -#[test] -fn test_block_expiration_height_monotonically_decreases() -> anyhow::Result<()> { +#[tokio::test] +async fn test_block_expiration_height_monotonically_decreases() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let test_pairs: [(u64, u64); 3] = [(9, 12), (18, 3), (20, 20)]; @@ -364,9 +325,9 @@ fn test_block_expiration_height_monotonically_decreases() -> anyhow::Result<()> begin exec.prologue::prepare_transaction push.{value_1} - exec.tx::update_expiration_block_num + exec.tx::update_expiration_block_delta push.{value_2} - exec.tx::update_expiration_block_num + exec.tx::update_expiration_block_delta push.{min_value} exec.tx::get_expiration_delta assert_eq @@ -383,20 +344,23 @@ fn test_block_expiration_height_monotonically_decreases() -> anyhow::Result<()> .replace("{value_2}", &v2.to_string()) .replace("{min_value}", &v2.min(v1).to_string()); - let process = &tx_context.execute_code(code)?; + let exec_output = &tx_context.execute_code(code).await?; // Expiry block should be set to transaction's block + the stored expiration delta // (which can only decrease, not increase) let expected_expiry = v1.min(v2) + tx_context.tx_inputs().block_header().block_num().as_u64(); - assert_eq!(process.stack.get(EXPIRATION_BLOCK_ELEMENT_IDX).as_int(), expected_expiry); + assert_eq!( + exec_output.get_stack_element(EXPIRATION_BLOCK_ELEMENT_IDX).as_int(), + expected_expiry + ); } Ok(()) } -#[test] -fn test_invalid_expiration_deltas() -> anyhow::Result<()> { +#[tokio::test] +async fn test_invalid_expiration_deltas() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let test_values = [0u64, u16::MAX as u64 + 1, u32::MAX as u64]; @@ -405,22 +369,22 @@ fn test_invalid_expiration_deltas() -> anyhow::Result<()> { begin push.{value_1} - exec.tx::update_expiration_block_num + exec.tx::update_expiration_block_delta end "; for value in test_values { let code = &code_template.replace("{value_1}", &value.to_string()); - let process = tx_context.execute_code(code); + let exec_output = tx_context.execute_code(code).await; - assert_execution_error!(process, ERR_TX_INVALID_EXPIRATION_DELTA); + assert_execution_error!(exec_output, ERR_TX_INVALID_EXPIRATION_DELTA); } Ok(()) } -#[test] -fn test_no_expiration_delta_set() -> anyhow::Result<()> { +#[tokio::test] +async fn test_no_expiration_delta_set() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code_template = " @@ -441,16 +405,19 @@ fn test_no_expiration_delta_set() -> anyhow::Result<()> { end "; - let process = &tx_context.execute_code(code_template)?; + let exec_output = &tx_context.execute_code(code_template).await?; - // Default value should be equal to u32::max, set in the prologue - assert_eq!(process.stack.get(EXPIRATION_BLOCK_ELEMENT_IDX).as_int() as u32, u32::MAX); + // 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, + u32::MAX + ); Ok(()) } -#[test] -fn test_epilogue_increment_nonce_success() -> anyhow::Result<()> { +#[tokio::test] +async fn test_epilogue_increment_nonce_success() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let expected_nonce = ONE + ONE; @@ -475,19 +442,19 @@ fn test_epilogue_increment_nonce_success() -> anyhow::Result<()> { # clean the stack dropw dropw dropw dropw - exec.memory::get_acct_nonce + exec.memory::get_account_nonce push.{expected_nonce} assert_eq end " ); - tx_context.execute_code(code.as_str())?; + tx_context.execute_code(code.as_str()).await?; Ok(()) } /// Tests that changing the account state without incrementing the nonce results in an error. -#[test] -fn epilogue_fails_on_account_state_change_without_nonce_increment() -> anyhow::Result<()> { +#[tokio::test] +async fn epilogue_fails_on_account_state_change_without_nonce_increment() -> anyhow::Result<()> { let code = " use.mock::account @@ -507,7 +474,8 @@ fn epilogue_fails_on_account_state_change_without_nonce_increment() -> anyhow::R let result = TransactionContextBuilder::with_noop_auth_account() .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!( result, @@ -517,19 +485,38 @@ fn epilogue_fails_on_account_state_change_without_nonce_increment() -> anyhow::R Ok(()) } -#[test] -fn test_epilogue_execute_empty_transaction() -> anyhow::Result<()> { +#[tokio::test] +async fn epilogue_fails_when_nonce_not_incremented() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.create_new_mock_account(Auth::Noop)?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let result = mock_chain + .build_tx_context(TxContextInput::Account(account), &[], &[])? + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_EPILOGUE_NONCE_CANNOT_BE_0); + + Ok(()) +} + +#[tokio::test] +async fn test_epilogue_execute_empty_transaction() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_noop_auth_account().build()?; - let result = tx_context.execute_blocking(); + let result = tx_context.execute().await; assert_transaction_executor_error!(result, ERR_EPILOGUE_EXECUTED_TRANSACTION_IS_EMPTY); Ok(()) } -#[test] -fn test_epilogue_empty_transaction_with_empty_output_note() -> 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); @@ -538,7 +525,8 @@ fn test_epilogue_empty_transaction_with_empty_output_note() -> anyhow::Result<() // create an empty output note in the transaction script let tx_script_source = format!( r#" - use.miden::tx + use.std::word + use.miden::output_note use.$kernel::prologue use.$kernel::epilogue use.$kernel::note @@ -559,16 +547,18 @@ fn test_epilogue_empty_transaction_with_empty_output_note() -> anyhow::Result<() # => [tag, aux, execution_hint, note_type, RECIPIENT, pad(8)] # create the note - call.tx::create_note + call.output_note::create # => [note_idx, GARBAGE(15)] # make sure that output note was created: compare the output note hash with an empty # word exec.note::compute_output_notes_commitment - padw eqw assertz.err="output note was created, but the output notes hash remains to be zeros" + exec.word::eqz assertz.err="output note was created, but the output notes hash remains to be zeros" + # => [note_idx, GARBAGE(15)] # clean the stack dropw dropw dropw dropw + # => [] exec.epilogue::finalize_transaction end @@ -578,7 +568,7 @@ fn test_epilogue_empty_transaction_with_empty_output_note() -> anyhow::Result<() let tx_context = TransactionContextBuilder::with_noop_auth_account().build()?; - let result = tx_context.execute_code(&tx_script_source).map(|_| ()); + let result = tx_context.execute_code(&tx_script_source).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 f14e4643e7..7eab71ff10 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -39,14 +39,15 @@ use miden_objects::testing::noop_auth_component::NoopAuthComponent; use miden_objects::testing::storage::FAUCET_STORAGE_DATA_SLOT; use miden_objects::{Felt, Word}; -use crate::utils::create_p2any_note; +use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::create_public_p2any_note; use crate::{TransactionContextBuilder, assert_execution_error, assert_transaction_executor_error}; // FUNGIBLE FAUCET MINT TESTS // ================================================================================================ -#[test] -fn test_mint_fungible_asset_succeeds() -> anyhow::Result<()> { +#[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(), @@ -64,16 +65,16 @@ fn test_mint_fungible_asset_succeeds() -> anyhow::Result<()> { begin # mint asset exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.{suffix}.{prefix} + push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} call.faucet::mint # assert the correct asset is returned - push.{FUNGIBLE_ASSET_AMOUNT}.0.{suffix}.{prefix} + push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} assert_eqw.err="minted asset does not match expected asset" # assert the input vault has been updated exec.memory::get_input_vault_root_ptr - push.{suffix}.{prefix} + push.{suffix} push.{prefix} exec.asset_vault::get_balance push.{FUNGIBLE_ASSET_AMOUNT} assert_eq.err="input vault should contain minted asset" end @@ -82,27 +83,23 @@ fn test_mint_fungible_asset_succeeds() -> anyhow::Result<()> { suffix = faucet_id.suffix(), ); - let process = &tx_context.execute_code(&code).unwrap(); + 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 = process - .chiplets - .memory - .get_value(process.system.ctx(), faucet_storage_amount_location) - .unwrap() - .as_int(); + let faucet_storage_amount = + exec_output.get_kernel_mem_element(faucet_storage_amount_location).as_int(); assert_eq!(faucet_storage_amount, expected_final_storage_amount); Ok(()) } /// Tests that minting a fungible asset on a non-faucet account fails. -#[test] -fn mint_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { +#[tokio::test] +async fn mint_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let account = setup_non_faucet_account()?; let code = format!( @@ -121,14 +118,15 @@ fn mint_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let result = TransactionContextBuilder::new(account) .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(result, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } -#[test] -fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { +#[tokio::test] +async fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_fungible_faucet( ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, 10u32.into(), @@ -149,14 +147,14 @@ fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { asset = Word::from(FungibleAsset::mock(5)) ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); + assert_execution_error!(exec_output, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } -#[test] -fn test_mint_fungible_asset_fails_saturate_max_amount() -> anyhow::Result<()> { +#[tokio::test] +async fn test_mint_fungible_asset_fails_saturate_max_amount() -> anyhow::Result<()> { let code = format!( " use.mock::faucet @@ -176,7 +174,8 @@ fn test_mint_fungible_asset_fails_saturate_max_amount() -> anyhow::Result<()> { ) .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!( result, @@ -188,8 +187,8 @@ fn test_mint_fungible_asset_fails_saturate_max_amount() -> anyhow::Result<()> { // NON-FUNGIBLE FAUCET MINT TESTS // ================================================================================================ -#[test] -fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; @@ -234,16 +233,16 @@ fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { end "#, non_fungible_asset = Word::from(non_fungible_asset), - asset_vault_key = StorageMap::hash_key(asset_vault_key), + asset_vault_key = StorageMap::hash_key(asset_vault_key.into()), ); - tx_context.execute_code(&code)?; + tx_context.execute_code(&code).await?; Ok(()) } -#[test] -fn test_mint_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow::Result<()> { +#[tokio::test] +async fn test_mint_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet( ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, ) @@ -264,15 +263,15 @@ fn test_mint_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow::Result non_fungible_asset = Word::from(non_fungible_asset) ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); + assert_execution_error!(exec_output, ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } /// Tests that minting a non-fungible asset on a non-faucet account fails. -#[test] -fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { +#[tokio::test] +async fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let account = setup_non_faucet_account()?; let code = format!( @@ -291,14 +290,15 @@ fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let result = TransactionContextBuilder::new(account) .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(result, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } -#[test] -fn test_mint_non_fungible_asset_fails_asset_already_exists() -> anyhow::Result<()> { +#[tokio::test] +async fn test_mint_non_fungible_asset_fails_asset_already_exists() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; @@ -319,9 +319,9 @@ fn test_mint_non_fungible_asset_fails_asset_already_exists() -> anyhow::Result<( non_fungible_asset = Word::from(non_fungible_asset) ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_FAUCET_NON_FUNGIBLE_ASSET_ALREADY_ISSUED); + assert_execution_error!(exec_output, ERR_FAUCET_NON_FUNGIBLE_ASSET_ALREADY_ISSUED); Ok(()) } @@ -329,16 +329,16 @@ fn test_mint_non_fungible_asset_fails_asset_already_exists() -> anyhow::Result<( // FUNGIBLE FAUCET BURN TESTS // ================================================================================================ -#[test] -fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { let tx_context = { let account = Account::mock_fungible_faucet( ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), ); - let note = create_p2any_note( + let note = create_public_p2any_note( ACCOUNT_ID_SENDER.try_into().unwrap(), - &[FungibleAsset::new(account.id(), 100u64).unwrap().into()], + [FungibleAsset::new(account.id(), 100u64).unwrap().into()], ); TransactionContextBuilder::new(account).extend_input_notes(vec![note]).build()? }; @@ -355,17 +355,17 @@ fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { begin # burn asset exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.{suffix}.{prefix} + push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} call.faucet::burn # assert the correct asset is returned - push.{FUNGIBLE_ASSET_AMOUNT}.0.{suffix}.{prefix} + push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} assert_eqw.err="burnt asset does not match expected asset" # assert the input vault has been updated exec.memory::get_input_vault_root_ptr - push.{suffix}.{prefix} + 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" @@ -376,27 +376,23 @@ fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { final_input_vault_asset_amount = CONSUMED_ASSET_1_AMOUNT - FUNGIBLE_ASSET_AMOUNT, ); - let process = &tx_context.execute_code(&code).unwrap(); + 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 = process - .chiplets - .memory - .get_value(process.system.ctx(), faucet_storage_amount_location) - .unwrap() - .as_int(); + let faucet_storage_amount = + exec_output.get_kernel_mem_element(faucet_storage_amount_location).as_int(); assert_eq!(faucet_storage_amount, expected_final_storage_amount); Ok(()) } /// Tests that burning a fungible asset on a non-faucet account fails. -#[test] -fn burn_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { +#[tokio::test] +async fn burn_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let account = setup_non_faucet_account()?; let code = format!( @@ -415,14 +411,15 @@ fn burn_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let result = TransactionContextBuilder::new(account) .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(result, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } -#[test] -fn test_burn_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { +#[tokio::test] +async fn test_burn_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_fungible_faucet( ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, Felt::try_from(FUNGIBLE_FAUCET_INITIAL_BALANCE).unwrap(), @@ -438,7 +435,7 @@ fn test_burn_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { begin exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.{suffix}.{prefix} + push.{FUNGIBLE_ASSET_AMOUNT} push.0 push.{suffix} push.{prefix} call.faucet::burn end ", @@ -446,14 +443,14 @@ fn test_burn_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> { suffix = faucet_id.suffix(), ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); + assert_execution_error!(exec_output, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } -#[test] -fn test_burn_fungible_asset_insufficient_input_amount() -> anyhow::Result<()> { +#[tokio::test] +async fn test_burn_fungible_asset_insufficient_input_amount() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_fungible_faucet( ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), @@ -469,7 +466,7 @@ fn test_burn_fungible_asset_insufficient_input_amount() -> anyhow::Result<()> { begin exec.prologue::prepare_transaction - push.{saturating_amount}.0.{suffix}.{prefix} + push.{saturating_amount} push.0 push.{suffix} push.{prefix} call.faucet::burn end ", @@ -478,17 +475,20 @@ fn test_burn_fungible_asset_insufficient_input_amount() -> anyhow::Result<()> { saturating_amount = CONSUMED_ASSET_1_AMOUNT + 1 ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW); + assert_execution_error!( + exec_output, + ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW + ); Ok(()) } // NON-FUNGIBLE FAUCET BURN TESTS // ================================================================================================ -#[test] -fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; @@ -552,12 +552,12 @@ fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { burnt_asset_vault_key = burnt_asset_vault_key, ); - tx_context.execute_code(&code).unwrap(); + tx_context.execute_code(&code).await.unwrap(); Ok(()) } -#[test] -fn test_burn_non_fungible_asset_fails_does_not_exist() -> anyhow::Result<()> { +#[tokio::test] +async fn test_burn_non_fungible_asset_fails_does_not_exist() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; @@ -579,15 +579,15 @@ fn test_burn_non_fungible_asset_fails_does_not_exist() -> anyhow::Result<()> { non_fungible_asset = Word::from(non_fungible_asset_burnt) ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND); + assert_execution_error!(exec_output, ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND); Ok(()) } /// Tests that burning a non-fungible asset on a non-faucet account fails. -#[test] -fn burn_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { +#[tokio::test] +async fn burn_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let account = setup_non_faucet_account()?; let code = format!( @@ -606,14 +606,15 @@ fn burn_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> { let result = TransactionContextBuilder::new(account) .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(result, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN); Ok(()) } -#[test] -fn test_burn_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow::Result<()> { +#[tokio::test] +async fn test_burn_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow::Result<()> { let non_fungible_asset_burnt = NonFungibleAsset::mock(&[1, 2, 3]); // Run code from a different non-fungible asset issuer @@ -637,17 +638,17 @@ fn test_burn_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow::Result non_fungible_asset = Word::from(non_fungible_asset_burnt) ); - let process = tx_context.execute_code(&code); + let exec_output = tx_context.execute_code(&code).await; - assert_execution_error!(process, ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND); + assert_execution_error!(exec_output, ERR_FAUCET_NON_FUNGIBLE_ASSET_TO_BURN_NOT_FOUND); Ok(()) } // IS NON FUNGIBLE ASSET ISSUED TESTS // ================================================================================================ -#[test] -fn test_is_non_fungible_asset_issued_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_is_non_fungible_asset_issued_succeeds() -> anyhow::Result<()> { // NON_FUNGIBLE_ASSET_DATA_2 is "issued" during the mock faucet creation, so it is already in // the map of issued assets. let tx_context = @@ -684,15 +685,15 @@ fn test_is_non_fungible_asset_issued_succeeds() -> anyhow::Result<()> { non_fungible_asset_2 = Word::from(non_fungible_asset_2), ); - tx_context.execute_code(&code).unwrap(); + tx_context.execute_code(&code).await.unwrap(); Ok(()) } // GET TOTAL ISSUANCE TESTS // ================================================================================================ -#[test] -fn test_get_total_issuance_succeeds() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_total_issuance_succeeds() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_fungible_faucet( ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), @@ -718,7 +719,7 @@ fn test_get_total_issuance_succeeds() -> anyhow::Result<()> { "#, ); - tx_context.execute_code(&code).unwrap(); + tx_context.execute_code(&code).await.unwrap(); Ok(()) } 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 888f7bdcd2..0cc326fe60 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs @@ -1,6 +1,5 @@ use anyhow::Context; use assert_matches::assert_matches; -use miden_lib::testing::note::NoteBuilder; use miden_objects::account::{AccountId, StorageMap, StorageSlot}; use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset}; use miden_objects::note::NoteType; @@ -10,15 +9,15 @@ use miden_objects::{self, Felt, Word}; use miden_tx::TransactionExecutorError; use winter_rand_utils::rand_value; -use crate::utils::create_p2any_note; +use crate::utils::create_public_p2any_note; use crate::{Auth, MockChain}; // FEE TESTS // ================================================================================================ /// Tests that a simple wallet account can be created with non-zero fees. -#[test] -fn create_account_with_fees() -> anyhow::Result<()> { +#[tokio::test] +async fn create_account_with_fees() -> anyhow::Result<()> { let note_amount = 10_000; let mut builder = MockChain::builder().verification_base_fee(50); @@ -29,7 +28,8 @@ fn create_account_with_fees() -> anyhow::Result<()> { let tx = chain .build_tx_context(account, &[fee_note.id()], &[])? .build()? - .execute_blocking() + .execute() + .await .context("failed to execute account-creating transaction")?; let expected_fee = tx.compute_fee(); @@ -53,8 +53,8 @@ fn create_account_with_fees() -> anyhow::Result<()> { /// Tests that the transaction executor host aborts the transaction if the balance of the native /// asset in the account does not cover the computed fee. -#[test] -fn tx_host_aborts_if_account_balance_does_not_cover_fee() -> anyhow::Result<()> { +#[tokio::test] +async fn tx_host_aborts_if_account_balance_does_not_cover_fee() -> anyhow::Result<()> { let account_amount = 100; let note_amount = 100; let native_asset_id = AccountId::try_from(ACCOUNT_ID_NATIVE_ASSET_FAUCET)?; @@ -70,7 +70,8 @@ fn tx_host_aborts_if_account_balance_does_not_cover_fee() -> anyhow::Result<()> let err = chain .build_tx_context(account, &[fee_note.id()], &[])? .build()? - .execute_blocking() + .execute() + .await .unwrap_err(); assert_matches!( @@ -88,11 +89,11 @@ fn tx_host_aborts_if_account_balance_does_not_cover_fee() -> anyhow::Result<()> /// /// TODO: Once smt::set supports multiple leaves, this case should be tested explicitly here. #[rstest::rstest] -#[case::create_account_no_storage(create_account_no_storage_no_fees()?)] -#[case::mutate_account_with_storage(mutate_account_with_storage()?)] -#[case::create_output_notes(create_output_notes()?)] -#[test] -fn num_tx_cycles_after_compute_fee_are_less_than_estimated( +#[case::create_account_no_storage(create_account_no_storage_no_fees().await?)] +#[case::mutate_account_with_storage(mutate_account_with_storage().await?)] +#[case::create_output_notes(create_output_notes().await?)] +#[tokio::test] +async fn num_tx_cycles_after_compute_fee_are_less_than_estimated( #[case] tx: ExecutedTransaction, ) -> anyhow::Result<()> { // These constants should always be updated together with the equivalent constants in @@ -110,19 +111,20 @@ fn num_tx_cycles_after_compute_fee_are_less_than_estimated( } /// Returns a transaction that creates an account without storage and 0 fees. -fn create_account_no_storage_no_fees() -> anyhow::Result { +async fn create_account_no_storage_no_fees() -> anyhow::Result { let mut builder = MockChain::builder(); let account = builder.create_new_wallet(Auth::IncrNonce)?; builder .build()? .build_tx_context(account, &[], &[])? .build()? - .execute_blocking() + .execute() + .await .map_err(From::from) } /// Returns a transaction that mutates an account with storage and consumes a note. -fn mutate_account_with_storage() -> anyhow::Result { +async fn mutate_account_with_storage() -> anyhow::Result { let native_asset_id = AccountId::try_from(ACCOUNT_ID_NATIVE_ASSET_FAUCET)?; let native_asset = FungibleAsset::new(native_asset_id, 10_000)?; let mut builder = @@ -145,12 +147,13 @@ fn mutate_account_with_storage() -> anyhow::Result { .build()? .build_tx_context(account, &[p2id_note.id()], &[])? .build()? - .execute_blocking() + .execute() + .await .map_err(From::from) } /// Returns a transaction that consumes two notes and creates two notes. -fn create_output_notes() -> anyhow::Result { +async fn create_output_notes() -> anyhow::Result { let native_asset_id = AccountId::try_from(ACCOUNT_ID_NATIVE_ASSET_FAUCET)?; let native_asset = FungibleAsset::new(native_asset_id, 10_000)?; let mut builder = @@ -166,18 +169,15 @@ fn create_output_notes() -> anyhow::Result { let note_asset0 = FungibleAsset::mock(200).unwrap_fungible(); let note_asset1 = FungibleAsset::mock(500).unwrap_fungible(); - // This creates a note that adds the given assets to the transaction vault without moving them - // to the account. This is needed to preserve the overall transaction asset rules, since the - // SPAWN note does not remove the assets from the account vault. - let asset_note = NoteBuilder::new(account.id(), &mut rand::rng()) - .add_assets([Asset::from(note_asset0.add(note_asset1)?)]) - .build()?; - builder.add_note(OutputNote::Full(asset_note.clone())); + // This creates a note that adds the given assets to the account vault. + let asset_note = + create_public_p2any_note(account.id(), [Asset::from(note_asset0.add(note_asset1)?)]); + builder.add_output_note(OutputNote::Full(asset_note.clone())); - let output_note0 = create_p2any_note(account.id(), &[note_asset0.into()]); - let output_note1 = create_p2any_note(account.id(), &[note_asset1.into()]); + let output_note0 = create_public_p2any_note(account.id(), [note_asset0.into()]); + let output_note1 = create_public_p2any_note(account.id(), [note_asset1.into()]); - let spawn_note = builder.add_spawn_note(account.id(), [&output_note0, &output_note1])?; + let spawn_note = builder.add_spawn_note([&output_note0, &output_note1])?; builder .build()? .build_tx_context(account, &[asset_note.id(), spawn_note.id()], &[])? @@ -186,6 +186,7 @@ fn create_output_notes() -> anyhow::Result { OutputNote::Full(output_note1), ]) .build()? - .execute_blocking() + .execute() + .await .map_err(From::from) } 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 b6dce8aad4..915c1dd01c 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs @@ -22,27 +22,32 @@ use miden_lib::transaction::memory::{ NUM_ACCT_STORAGE_SLOTS_OFFSET, }; use miden_lib::utils::ScriptBuilder; -use miden_objects::FieldElement; use miden_objects::account::{ Account, AccountBuilder, AccountComponent, + AccountId, AccountProcedureInfo, AccountStorage, AccountStorageMode, - PartialAccount, 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::transaction::AccountInputs; +use miden_objects::{FieldElement, Word, ZERO}; +use miden_processor::fast::ExecutionOutput; use miden_processor::{AdviceInputs, Felt}; use miden_tx::LocalTransactionProver; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use super::{Process, Word, ZERO}; -use crate::kernel_tests::tx::ProcessMemoryExt; +use crate::kernel_tests::tx::ExecutionOutputExt; use crate::{Auth, MockChainBuilder, assert_execution_error, assert_transaction_executor_error}; // SIMPLE FPI TESTS @@ -51,19 +56,19 @@ use crate::{Auth, MockChainBuilder, assert_execution_error, assert_transaction_e // FOREIGN PROCEDURE INVOCATION TESTS // ================================================================================================ -#[test] -fn test_fpi_memory() -> anyhow::Result<()> { +#[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 foreign_account_code_source = " - use.miden::account + use.miden::active_account export.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.account::get_item + exec.active_account::get_item # truncate the stack movup.6 movup.6 movup.6 drop drop drop @@ -73,13 +78,14 @@ fn test_fpi_memory() -> anyhow::Result<()> { # 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.account::get_map_item + exec.active_account::get_map_item end "; + let source_manager = Arc::new(DefaultSourceManager::default()); let foreign_account_component = AccountComponent::compile( foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + TransactionKernel::with_kernel_library(source_manager.clone()), storage_slots.clone(), )? .with_supports_all_types(); @@ -99,6 +105,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { MockChainBuilder::with_accounts([native_account.clone(), foreign_account.clone()])? .build()?; mock_chain.prove_next_block()?; + let fpi_inputs = mock_chain .get_foreign_account_inputs(foreign_account.id()) .expect("failed to get foreign account inputs"); @@ -107,6 +114,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { .build_tx_context(native_account.id(), &[], &[]) .expect("failed to build tx context") .foreign_accounts(vec![fpi_inputs]) + .with_source_manager(source_manager) .build()?; // GET ITEM @@ -135,7 +143,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { push.{get_item_foreign_hash} # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(11)] exec.tx::execute_foreign_procedure @@ -150,15 +158,15 @@ fn test_fpi_memory() -> anyhow::Result<()> { get_item_foreign_hash = foreign_account.code().procedures()[1].mast_root(), ); - let process = tx_context.execute_code(&code)?; + let exec_output = tx_context.execute_code(&code).await?; assert_eq!( - process.stack.get_word(0), + 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]", ); - foreign_account_data_memory_assertions(&foreign_account, &process); + foreign_account_data_memory_assertions(&foreign_account, &exec_output); // GET MAP ITEM // -------------------------------------------------------------------------------------------- @@ -188,7 +196,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { push.{get_map_item_foreign_hash} # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + 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)] exec.tx::execute_foreign_procedure @@ -204,15 +212,15 @@ fn test_fpi_memory() -> anyhow::Result<()> { get_map_item_foreign_hash = foreign_account.code().procedures()[2].mast_root(), ); - let process = tx_context.execute_code(&code).unwrap(); + let exec_output = tx_context.execute_code(&code).await.unwrap(); assert_eq!( - process.stack.get_word(0), + exec_output.get_stack_word_be(0), STORAGE_LEAVES_2[0].1, "Value at the top of the stack should be equal [1, 2, 3, 4]", ); - foreign_account_data_memory_assertions(&foreign_account, &process); + foreign_account_data_memory_assertions(&foreign_account, &exec_output); // GET ITEM TWICE // -------------------------------------------------------------------------------------------- @@ -242,7 +250,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { push.{get_item_foreign_hash} # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure dropw @@ -260,7 +268,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { push.{get_item_foreign_hash} # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -274,7 +282,7 @@ fn test_fpi_memory() -> anyhow::Result<()> { get_item_foreign_hash = foreign_account.code().procedures()[1].mast_root(), ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; // Check that the second invocation of the foreign procedure from the same account does not load // the account data again: already loaded data should be reused. @@ -283,40 +291,40 @@ fn test_fpi_memory() -> anyhow::Result<()> { // Foreign account: [16384; 24575] <- initialized during first FPI // Next account slot: [24576; 32767] <- should not be initialized assert_eq!( - process.try_get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32 * 2), - None, + exec_output.get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32 * 2), + Word::empty(), "Memory starting from 24576 should stay uninitialized" ); Ok(()) } -#[test] -fn test_fpi_memory_two_accounts() -> 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 foreign_account_code_source_1 = " - use.miden::account + use.miden::active_account export.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.account::get_item + exec.active_account::get_item # truncate the stack movup.6 movup.6 movup.6 drop drop drop end "; let foreign_account_code_source_2 = " - use.miden::account + use.miden::active_account export.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.account::get_item + exec.active_account::get_item # truncate the stack movup.6 movup.6 movup.6 drop drop drop @@ -369,8 +377,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { .expect("failed to get foreign account inputs"); let tx_context = mock_chain - .build_tx_context(native_account.id(), &[], &[]) - .expect("failed to build tx context") + .build_tx_context(native_account.id(), &[], &[])? .foreign_accounts(vec![foreign_account_inputs_1, foreign_account_inputs_2]) .build()?; @@ -402,7 +409,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { push.{get_item_foreign_1_hash} # push the foreign account ID - push.{foreign_1_suffix}.{foreign_1_prefix} + 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)] exec.tx::execute_foreign_procedure dropw @@ -420,7 +427,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { push.{get_item_foreign_2_hash} # push the foreign account ID - push.{foreign_2_suffix}.{foreign_2_prefix} + 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)] exec.tx::execute_foreign_procedure dropw @@ -438,7 +445,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { push.{get_item_foreign_1_hash} # push the foreign account ID - push.{foreign_1_suffix}.{foreign_1_prefix} + 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)] exec.tx::execute_foreign_procedure @@ -457,7 +464,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { foreign_2_suffix = foreign_account_2.id().suffix(), ); - let process = &tx_context.execute_code(&code)?; + let exec_output = &tx_context.execute_code(&code).await?; // Check the correctness of the memory layout after multiple foreign procedure invocations from // different foreign accounts @@ -469,7 +476,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { // check that the first word of the first foreign account slot is correct assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32), + exec_output.get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32), Word::new([ foreign_account_1.id().suffix(), foreign_account_1.id().prefix().as_felt(), @@ -480,7 +487,7 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { // check that the first word of the second foreign account slot is correct assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32 * 2), + exec_output.get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32 * 2), Word::new([ foreign_account_2.id().suffix(), foreign_account_2.id().prefix().as_felt(), @@ -491,8 +498,8 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { // check that the first word of the third foreign account slot was not initialized assert_eq!( - process.try_get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32 * 3), - None, + exec_output.get_kernel_mem_word(NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32 * 3), + Word::empty(), "Memory starting from 32768 should stay uninitialized" ); @@ -504,19 +511,19 @@ fn test_fpi_memory_two_accounts() -> anyhow::Result<()> { /// It checks the foreign account code loading, providing the mast forest to the executor, /// construction of the account procedure maps and execution the foreign procedure in order to /// obtain the data from the foreign account's storage slot. -#[test] -fn test_fpi_execute_foreign_procedure() -> 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 foreign_account_code_source = " - use.miden::account + use.miden::active_account export.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.account::get_item + exec.active_account::get_item # truncate the stack movup.6 movup.6 movup.6 drop drop drop @@ -526,20 +533,21 @@ fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { # 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.account::get_map_item + exec.active_account::get_map_item end "; + let source_manager = Arc::new(DefaultSourceManager::default()); let foreign_account_component = AccountComponent::compile( - foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + NamedSource::new("foreign_account", foreign_account_code_source), + TransactionKernel::with_kernel_library(source_manager.clone()), storage_slots, )? .with_supports_all_types(); let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) - .with_component(foreign_account_component) + .with_component(foreign_account_component.clone()) .build_existing()?; let native_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) @@ -558,7 +566,6 @@ fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { use.std::sys use.miden::tx - use.miden::account begin # get the storage item at index 0 @@ -569,11 +576,11 @@ fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { # push the index of desired storage item push.0 - # get the hash of the `get_item` account procedure - push.{get_item_foreign_hash} + # get the hash of the `get_item_foreign` account procedure + procref.::foreign_account::get_item_foreign # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -595,10 +602,10 @@ fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { push.1 # get the hash of the `get_map_item_foreign` account procedure - push.{get_map_item_foreign_hash} + procref.::foreign_account::get_map_item_foreign # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + 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)] exec.tx::execute_foreign_procedure @@ -614,24 +621,251 @@ fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { ", 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(), - get_map_item_foreign_hash = foreign_account.code().procedures()[2].mast_root(), map_key = STORAGE_LEAVES_2[0].0, ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(foreign_account_component.library())? + .compile_tx_script(code)?; let foreign_account_inputs = mock_chain .get_foreign_account_inputs(foreign_account.id()) .expect("failed to get foreign account inputs"); - let tx_context = mock_chain + + mock_chain .build_tx_context(native_account.id(), &[], &[]) .expect("failed to build tx context") - .foreign_accounts(vec![foreign_account_inputs]) + .foreign_accounts([foreign_account_inputs]) .tx_script(tx_script) - .build()?; + .with_source_manager(source_manager) + .build()? + .execute() + .await?; - let _executed_transaction = tx_context.execute_blocking()?; + Ok(()) +} + +/// Test that a foreign account can get the balance of a fungible asset and check the presence of a +/// non-fungible asset. +#[tokio::test] +async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Result<()> { + let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1)?; + let non_fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?; + + // Create two different assets. + let fungible_asset = Asset::Fungible(FungibleAsset::new(fungible_faucet_id, 1)?); + let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new( + &NonFungibleAssetDetails::new(non_fungible_faucet_id.prefix(), vec![1, 2, 3])?, + )?); + + let foreign_account_code_source = format!( + " + use.miden::active_account + + export.get_asset_balance + # get balance of first asset + push.{fungible_faucet_id_suffix} push.{fungible_faucet_id_prefix} + exec.active_account::get_balance + # => [balance] + + # check presence of non fungible asset + push.{non_fungible_asset_word} + exec.active_account::has_non_fungible_asset + # => [has_asset, balance] + + # add the balance and the bool + add + # => [has_asset_balance] + + # keep only the result on stack + swap drop + # => [has_asset_balance] + end + ", + fungible_faucet_id_prefix = fungible_faucet_id.prefix().as_felt(), + fungible_faucet_id_suffix = fungible_faucet_id.suffix(), + non_fungible_asset_word = Word::from(non_fungible_asset), + ); + + 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()), + vec![], + )? + .with_supports_all_types(); + + let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(foreign_account_component.clone()) + .with_assets(vec![fungible_asset, non_fungible_asset]) + .build_existing()?; + + 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(), foreign_account.clone()])? + .build()?; + mock_chain.prove_next_block()?; + + let code = format!( + " + use.std::sys + + use.miden::tx + + begin + # Get the added balance of two assets from foreign account + # pad the stack for the `execute_foreign_procedure` execution + padw padw padw push.0.0.0 + # => [pad(15)] + + # get the hash of the `get_asset_balance` procedure + procref.::foreign_account_code::get_asset_balance + + # push the 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 + # => [has_asset_balance] + + # assert that the non fungible asset exists and the fungible asset has balance 1 + push.2 assert_eq.err=\"Total balance should be 2\" + # => [] + + # truncate the stack + exec.sys::truncate_stack + end + ", + foreign_prefix = foreign_account.id().prefix().as_felt(), + foreign_suffix = foreign_account.id().suffix(), + ); + + let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(foreign_account_component.library())? + .compile_tx_script(code)?; + + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(foreign_account.id())?; + + mock_chain + .build_tx_context(native_account.id(), &[], &[])? + .foreign_accounts([foreign_account_inputs]) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; + + Ok(()) +} + +/// Test that the `miden::get_initial_balance` procedure works correctly being called from a foreign +/// account. +#[tokio::test] +async fn foreign_account_get_initial_balance() -> anyhow::Result<()> { + let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1)?; + let fungible_asset = Asset::Fungible(FungibleAsset::new(fungible_faucet_id, 10)?); + + let foreign_account_code_source = format!( + " + use.miden::active_account + + export.get_initial_balance + # push the faucet ID on the stack + push.{fungible_faucet_id_suffix} push.{fungible_faucet_id_prefix} + + # get the initial balance of the asset associated with the provided faucet ID + exec.active_account::get_balance + # => [initial_balance] + + # truncate the stack + swap drop + # => [initial_balance] + end + ", + fungible_faucet_id_prefix = fungible_faucet_id.prefix().as_felt(), + fungible_faucet_id_suffix = fungible_faucet_id.suffix(), + ); + + 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()), + vec![], + )? + .with_supports_all_types(); + + let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(foreign_account_component.clone()) + .with_assets(vec![fungible_asset]) + .build_existing()?; + + 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(), foreign_account.clone()])? + .build()?; + mock_chain.prove_next_block()?; + + let code = format!( + " + use.std::sys + + use.miden::tx + + begin + # Get the initial balance of the fungible asset from the foreign account + + # pad the stack for the `execute_foreign_procedure` execution + padw padw padw push.0.0.0 + # => [pad(15)] + + # get the hash of the `get_initial_balance` procedure + procref.::foreign_account_code::get_initial_balance + + # push the 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 + # => [init_foreign_balance] + + # assert that the initial balance of the asset in the foreign account equals 10 + push.10 assert_eq.err=\"Initial balance should be 10\" + # => [] + + # truncate the stack + exec.sys::truncate_stack + end + ", + foreign_prefix = foreign_account.id().prefix().as_felt(), + foreign_suffix = foreign_account.id().suffix(), + ); + + let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(foreign_account_component.library())? + .compile_tx_script(code)?; + + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(foreign_account.id())?; + + mock_chain + .build_tx_context(native_account.id(), &[], &[])? + .foreign_accounts([foreign_account_inputs]) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; Ok(()) } @@ -646,13 +880,13 @@ fn test_fpi_execute_foreign_procedure() -> anyhow::Result<()> { /// /// The call chain in this test looks like so: /// `Native -> First FA -> Second FA -> First FA` -#[test] -fn test_nested_fpi_cyclic_invocation() -> 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::account + use.miden::active_account use.std::sys @@ -680,7 +914,7 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { # get the first element of the 0'th storage slot (it should be 1) and add it to the # obtained foreign value. - push.0 exec.account::get_item drop drop drop + push.0 exec.active_account::get_item drop drop drop add # assert that the resulting value equals 6 @@ -690,9 +924,10 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { end "#; + let source_manager = Arc::new(DefaultSourceManager::default()); let second_foreign_account_component = AccountComponent::compile( second_foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + TransactionKernel::with_kernel_library(source_manager.clone()), storage_slots, )? .with_supports_all_types(); @@ -707,7 +942,7 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { vec![AccountStorage::mock_item_0().slot, AccountStorage::mock_item_1().slot]; let first_foreign_account_code_source = r#" use.miden::tx - use.miden::account + use.miden::active_account use.std::sys @@ -728,7 +963,7 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { # get the second element of the 0'th storage slot (it should be 2) and add it to the # obtained foreign value. - push.0 exec.account::get_item drop drop swap drop + push.0 exec.active_account::get_item drop drop swap drop add # assert that the resulting value equals 8 @@ -741,7 +976,7 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { # 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.account::get_item + exec.active_account::get_item # return the first element of the resulting word drop drop drop @@ -749,15 +984,15 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { "#; let first_foreign_account_component = AccountComponent::compile( - first_foreign_account_code_source, - TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + NamedSource::new("first_foreign_account", first_foreign_account_code_source), + TransactionKernel::with_kernel_library(source_manager.clone()), storage_slots, )? .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) + .with_component(first_foreign_account_component.clone()) .build_existing()?; // ------ NATIVE ACCOUNT --------------------------------------------------------------- @@ -814,11 +1049,11 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { padw padw padw push.0.0.0 # => [pad(15)] - # get the hash of the `get_item` account procedure - push.{first_account_foreign_proc_hash} + # get the hash of the `first_account_foreign_proc` procedure + procref.::first_foreign_account::first_account_foreign_proc # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -836,23 +1071,25 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { "#, foreign_prefix = first_foreign_account.id().prefix().as_felt(), foreign_suffix = first_foreign_account.id().suffix(), - first_account_foreign_proc_hash = first_foreign_account.code().procedures()[1].mast_root(), ); - let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + let tx_script = ScriptBuilder::with_source_manager(source_manager.clone()) + .with_dynamically_linked_library(first_foreign_account_component.library())? + .compile_tx_script(code)?; - let tx_context = mock_chain + let executed_transaction = mock_chain .build_tx_context(native_account.id(), &[], &[]) .expect("failed to build tx context") .foreign_accounts(foreign_account_inputs) .extend_advice_inputs(advice_inputs) .tx_script(tx_script) - .build()?; - - let executed_transaction = tx_context.execute_blocking()?; + .with_source_manager(source_manager) + .build()? + .execute() + .await?; // TODO: Remove later and add a integration test using FPI. - LocalTransactionProver::default().prove(executed_transaction.into())?; + LocalTransactionProver::default().prove(executed_transaction)?; Ok(()) } @@ -861,16 +1098,12 @@ fn test_nested_fpi_cyclic_invocation() -> anyhow::Result<()> { /// /// Attempt to create a 64th foreign account first triggers the assert during the account data /// loading, but we have an additional assert during the account stack push just in case. -#[test] -fn test_nested_fpi_stack_overflow() { - // use a custom thread to increase its stack capacity - std::thread::Builder::new() - .stack_size(8 * 1_048_576) - .spawn(|| { - let mut foreign_accounts = Vec::new(); +#[tokio::test] +async fn test_nested_fpi_stack_overflow() -> anyhow::Result<()> { + let mut foreign_accounts = Vec::new(); - let last_foreign_account_code_source = " - use.miden::account + let last_foreign_account_code_source = " + use.miden::active_account export.get_item_foreign # make this foreign procedure unique to make sure that we invoke the procedure @@ -880,7 +1113,7 @@ fn test_nested_fpi_stack_overflow() { # push the index of desired storage item push.0 - exec.account::get_item + exec.active_account::get_item # return the first element of the resulting word drop drop drop @@ -890,27 +1123,27 @@ fn test_nested_fpi_stack_overflow() { end "; - 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 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 = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) - .with_auth_component(Auth::IncrNonce) - .with_component(last_foreign_account_component) - .build_existing() - .unwrap(); + let last_foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(last_foreign_account_component) + .build_existing() + .unwrap(); - foreign_accounts.push(last_foreign_account); + foreign_accounts.push(last_foreign_account); - for foreign_account_index in 0..63 { - let next_account = foreign_accounts.last().unwrap(); + for foreign_account_index in 0..63 { + let next_account = foreign_accounts.last().unwrap(); - let foreign_account_code_source = format!( + let foreign_account_code_source = format!( " use.miden::tx use.std::sys @@ -924,7 +1157,7 @@ fn test_nested_fpi_stack_overflow() { push.{next_account_proc_hash} # push the foreign account ID - push.{next_foreign_suffix}.{next_foreign_prefix} + push.{next_foreign_suffix} push.{next_foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -938,46 +1171,50 @@ fn test_nested_fpi_stack_overflow() { 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 = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) - .with_auth_component(Auth::IncrNonce) - .with_component(foreign_account_component) - .build_existing() - .unwrap(); - - foreign_accounts.push(foreign_account) - } - - // ------ 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() - .unwrap(); - - let mut mock_chain = MockChainBuilder::with_accounts( - [vec![native_account.clone()], foreign_accounts.clone()].concat(), - ).unwrap().build().unwrap(); - - mock_chain.prove_next_block().unwrap(); - - let foreign_accounts: Vec = foreign_accounts - .iter() - .map(|acc| mock_chain.get_foreign_account_inputs(acc.id()) - .expect("failed to get foreign account inputs")) - .collect(); - - let code = format!( + 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 = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(foreign_account_component) + .build_existing() + .unwrap(); + + foreign_accounts.push(foreign_account) + } + + // ------ 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() + .unwrap(); + + let mut mock_chain = MockChainBuilder::with_accounts( + [vec![native_account.clone()], foreign_accounts.clone()].concat(), + ) + .unwrap() + .build() + .unwrap(); + + mock_chain.prove_next_block().unwrap(); + + let foreign_accounts: Vec<_> = foreign_accounts + .iter() + .map(|acc| { + mock_chain + .get_foreign_account_inputs(acc.id()) + .expect("failed to get foreign account inputs") + }) + .collect(); + + let code = format!( " use.std::sys @@ -992,7 +1229,7 @@ fn test_nested_fpi_stack_overflow() { push.{foreign_account_proc_hash} # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -1002,34 +1239,28 @@ fn test_nested_fpi_stack_overflow() { end ", foreign_account_proc_hash = - foreign_accounts.last().unwrap().code().procedures()[1].mast_root(), - foreign_prefix = foreign_accounts.last().unwrap().id().prefix().as_felt(), - foreign_suffix = foreign_accounts.last().unwrap().id().suffix(), + foreign_accounts.last().unwrap().0.code().procedures()[1].mast_root(), + foreign_prefix = foreign_accounts.last().unwrap().0.id().prefix().as_felt(), + foreign_suffix = foreign_accounts.last().unwrap().0.id().suffix(), ); + let tx_script = ScriptBuilder::default().compile_tx_script(code).unwrap(); + let tx_context = mock_chain + .build_tx_context(native_account.id(), &[], &[])? + .foreign_accounts(foreign_accounts) + .tx_script(tx_script) + .build()?; - let tx_script = ScriptBuilder::default().compile_tx_script(code).unwrap(); - - let tx_context = mock_chain - .build_tx_context(native_account.id(), &[], &[]) - .expect("failed to build tx context") - .foreign_accounts(foreign_accounts) - .tx_script(tx_script) - .build().unwrap(); - - let result = tx_context.execute_blocking(); + let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED); - }) - .expect("thread panic external") - .join() - .expect("thread panic internal"); + assert_transaction_executor_error!(result, ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED); + Ok(()) } /// Test that code will panic in attempt to call a procedure from the native account. -#[test] -fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { +#[tokio::test] +async fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { // ------ FIRST FOREIGN ACCOUNT --------------------------------------------------------------- let foreign_account_code_source = " use.miden::tx @@ -1056,7 +1287,7 @@ fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { "; let foreign_account_component = AccountComponent::compile( - foreign_account_code_source, + NamedSource::new("foreign_account", foreign_account_code_source), TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), vec![], )? @@ -1064,7 +1295,7 @@ fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .with_auth_component(Auth::IncrNonce) - .with_component(foreign_account_component) + .with_component(foreign_account_component.clone()) .build_existing()?; // ------ NATIVE ACCOUNT --------------------------------------------------------------- @@ -1078,17 +1309,6 @@ fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { MockChainBuilder::with_accounts([native_account.clone(), foreign_account.clone()])? .build()?; mock_chain.prove_next_block().unwrap(); - let foreign_account_inputs = mock_chain - .get_foreign_account_inputs(foreign_account.id()) - .expect("failed to get foreign account inputs"); - - // push the hash of the native procedure and native account IDs to the advice stack to be able - // to call them dynamically. - let mut advice_inputs = AdviceInputs::default(); - advice_inputs.stack.extend(*native_account.code().procedures()[3].mast_root()); - advice_inputs - .stack - .extend([native_account.id().suffix(), native_account.id().prefix().as_felt()]); let code = format!( " @@ -1105,7 +1325,7 @@ fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { push.{first_account_foreign_proc_hash} # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -1119,17 +1339,31 @@ 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().compile_tx_script(code)?; + let tx_script = ScriptBuilder::default() + .with_dynamically_linked_library(foreign_account_component.library())? + .compile_tx_script(code)?; - let tx_context = mock_chain + let foreign_account_inputs = mock_chain + .get_foreign_account_inputs(foreign_account.id()) + .expect("failed to get foreign account inputs"); + + // push the hash of the native procedure and native account IDs to the advice stack to be able + // to call them dynamically. + let mut advice_inputs = AdviceInputs::default(); + advice_inputs.stack.extend(*native_account.code().procedures()[3].mast_root()); + advice_inputs + .stack + .extend([native_account.id().suffix(), native_account.id().prefix().as_felt()]); + + let result = mock_chain .build_tx_context(native_account.id(), &[], &[]) .expect("failed to build tx context") .foreign_accounts(vec![foreign_account_inputs]) .extend_advice_inputs(advice_inputs) .tx_script(tx_script) - .build()?; - - let result = tx_context.execute_blocking(); + .build()? + .execute() + .await; assert_transaction_executor_error!(result, ERR_FOREIGN_ACCOUNT_CONTEXT_AGAINST_NATIVE_ACCOUNT); Ok(()) @@ -1137,16 +1371,16 @@ fn test_nested_fpi_native_account_invocation() -> anyhow::Result<()> { /// Test that providing an account whose commitment does not match the one in the account tree /// results in an error. -#[test] -fn test_fpi_stale_account() -> anyhow::Result<()> { +#[tokio::test] +async fn test_fpi_stale_account() -> anyhow::Result<()> { // Prepare the test data let foreign_account_code_source = " - use.miden::account + use.miden::native_account # code is not used in this test export.set_some_item_foreign push.34.1 - exec.account::set_item + exec.native_account::set_item end "; @@ -1181,32 +1415,19 @@ fn test_fpi_stale_account() -> anyhow::Result<()> { .storage_mut() .set_item(0, Word::from([Felt::ONE, Felt::ONE, Felt::ONE, Felt::ONE]))?; - // Place the modified account in the advice provider, which will cause the commitment mismatch. - let foreign_account_inputs = mock_chain + // 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 + // proves inclusion for. + let (_foreign_account, foreign_account_witness) = mock_chain .get_foreign_account_inputs(foreign_account.id()) .expect("failed to get foreign account inputs"); - // We want to create a mixed ForeignAccountInputs because we want to have a valid account - // witness against the ref block, but have newer account data (ie, a new state). Otherwise, - // any non-validity of the account witness is caught in - // TransactionExecutor::execute_transaction() (see `test_fpi_anchoring_validations()` for - // context on this check) - let overridden_partial_accounts = PartialAccount::new( - foreign_account.id(), - foreign_account.nonce(), - foreign_account.code().clone(), - foreign_account.storage().into(), - foreign_account.vault().into(), - ); - let overridden_foreign_account_inputs = - AccountInputs::new(overridden_partial_accounts, foreign_account_inputs.witness().clone()); - // The account tree from which the transaction inputs are fetched here has the state from the // original unmodified foreign account. This should result in the foreign account's proof to be // invalid for this account tree root. let tx_context = mock_chain .build_tx_context(native_account, &[], &[])? - .foreign_accounts(vec![overridden_foreign_account_inputs]) + .foreign_accounts(vec![(foreign_account.clone(), foreign_account_witness)]) .build()?; // Attempt to run FPI. @@ -1227,11 +1448,11 @@ fn test_fpi_stale_account() -> anyhow::Result<()> { # => [pad(16)] # push some hash onto the stack - for this test it does not matter - push.0.0.0.0 + push.[1,2,3,4] # => [FOREIGN_PROC_ROOT, pad(16)] # push the foreign account ID - push.{foreign_suffix}.{foreign_prefix} + push.{foreign_suffix} push.{foreign_prefix} # => [foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, pad(16)] exec.tx::execute_foreign_procedure @@ -1241,19 +1462,136 @@ fn test_fpi_stale_account() -> anyhow::Result<()> { foreign_suffix = foreign_account.id().suffix(), ); - let result = tx_context.execute_code(&code).map(|_| ()); + let result = tx_context.execute_code(&code).await.map(|_| ()); assert_execution_error!(result, ERR_FOREIGN_ACCOUNT_INVALID_COMMITMENT); + + Ok(()) +} + +/// This test checks that our `miden::get_id` and `miden::get_native_id` procedures return IDs of +/// the current and native account respectively while being called from the foreign account. +#[tokio::test] +async fn test_fpi_get_account_id() -> anyhow::Result<()> { + let foreign_account_code_source = " + use.miden::active_account + use.miden::native_account + + export.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)] + + # get the ID of the native account + exec.native_account::get_id + # => [native_acct_id_prefix, native_acct_id_suffix, acct_id_prefix, acct_id_suffix, pad(16)] + + # truncate the stack + swapw dropw + # => [native_acct_id_prefix, native_acct_id_suffix, acct_id_prefix, acct_id_suffix, pad(12)] + end + "; + + let foreign_account_component = AccountComponent::compile( + NamedSource::new("foreign_account", foreign_account_code_source), + TransactionKernel::with_kernel_library(Arc::new(DefaultSourceManager::default())), + Vec::new(), + )? + .with_supports_all_types(); + + let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(foreign_account_component.clone()) + .build_existing()?; + + 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(), foreign_account.clone()])? + .build()?; + mock_chain.prove_next_block()?; + + let code = format!( + r#" + use.std::sys + + use.miden::tx + use.miden::account_id + + begin + # get the IDs of the foreign and native accounts + # pad the stack for the `execute_foreign_procedure` execution + padw padw padw push.0.0.0 + # => [pad(15)] + + # get the hash of the `get_current_and_native_ids` foreign account procedure + procref.::foreign_account::get_current_and_native_ids + + # push the 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 + # => [native_acct_id_prefix, native_acct_id_suffix, acct_id_prefix, acct_id_suffix] + + # push the expected native account ID and check that it is equal to the one returned + # from the FPI + push.{expected_native_suffix} push.{expected_native_prefix} + exec.account_id::is_equal + assert.err="native account ID returned from the FPI is not equal to the expected one" + # => [acct_id_prefix, acct_id_suffix] + + # push the expected foreign account ID and check that it is equal to the one returned + # from the FPI + push.{foreign_suffix} push.{foreign_prefix} + exec.account_id::is_equal + assert.err="foreign account ID returned from the FPI is not equal to the expected one" + # => [] + + # truncate the stack + exec.sys::truncate_stack + end + "#, + foreign_suffix = foreign_account.id().suffix(), + foreign_prefix = foreign_account.id().prefix().as_felt(), + expected_native_suffix = native_account.id().suffix(), + expected_native_prefix = native_account.id().prefix().as_felt(), + ); + + let tx_script = ScriptBuilder::default() + .with_dynamically_linked_library(foreign_account_component.library())? + .compile_tx_script(code)?; + + let foreign_account_inputs = mock_chain + .get_foreign_account_inputs(foreign_account.id()) + .expect("failed to get foreign account inputs"); + + mock_chain + .build_tx_context(native_account.id(), &[], &[]) + .expect("failed to build tx context") + .foreign_accounts(vec![foreign_account_inputs]) + .tx_script(tx_script) + .build()? + .execute() + .await?; + Ok(()) } // HELPER FUNCTIONS // ================================================================================================ -fn foreign_account_data_memory_assertions(foreign_account: &Account, process: &Process) { +fn foreign_account_data_memory_assertions( + foreign_account: &Account, + exec_output: &ExecutionOutput, +) { let foreign_account_data_ptr = NATIVE_ACCOUNT_DATA_PTR + ACCOUNT_DATA_LENGTH as u32; assert_eq!( - process.get_kernel_mem_word(foreign_account_data_ptr + ACCT_ID_AND_NONCE_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_ID_AND_NONCE_OFFSET), Word::new([ foreign_account.id().suffix(), foreign_account.id().prefix().as_felt(), @@ -1263,22 +1601,22 @@ fn foreign_account_data_memory_assertions(foreign_account: &Account, process: &P ); assert_eq!( - process.get_kernel_mem_word(foreign_account_data_ptr + ACCT_VAULT_ROOT_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_VAULT_ROOT_OFFSET), foreign_account.vault().root(), ); assert_eq!( - process.get_kernel_mem_word(foreign_account_data_ptr + ACCT_STORAGE_COMMITMENT_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_STORAGE_COMMITMENT_OFFSET), foreign_account.storage().commitment(), ); assert_eq!( - process.get_kernel_mem_word(foreign_account_data_ptr + ACCT_CODE_COMMITMENT_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + ACCT_CODE_COMMITMENT_OFFSET), foreign_account.code().commitment(), ); assert_eq!( - process.get_kernel_mem_word(foreign_account_data_ptr + NUM_ACCT_STORAGE_SLOTS_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + NUM_ACCT_STORAGE_SLOTS_OFFSET), Word::from([u16::try_from(foreign_account.storage().slots().len()).unwrap(), 0, 0, 0]), ); @@ -1289,7 +1627,7 @@ fn foreign_account_data_memory_assertions(foreign_account: &Account, process: &P .enumerate() { assert_eq!( - process.get_kernel_mem_word( + exec_output.get_kernel_mem_word( foreign_account_data_ptr + ACCT_STORAGE_SLOTS_SECTION_OFFSET + (i as u32) * 4 ), Word::try_from(elements).unwrap(), @@ -1297,7 +1635,7 @@ fn foreign_account_data_memory_assertions(foreign_account: &Account, process: &P } assert_eq!( - process.get_kernel_mem_word(foreign_account_data_ptr + NUM_ACCT_PROCEDURES_OFFSET), + exec_output.get_kernel_mem_word(foreign_account_data_ptr + NUM_ACCT_PROCEDURES_OFFSET), Word::from([u16::try_from(foreign_account.code().num_procedures()).unwrap(), 0, 0, 0]), ); @@ -1308,10 +1646,111 @@ fn foreign_account_data_memory_assertions(foreign_account: &Account, process: &P .enumerate() { assert_eq!( - process.get_kernel_mem_word( + exec_output.get_kernel_mem_word( foreign_account_data_ptr + ACCT_PROCEDURES_SECTION_OFFSET + (i as u32) * 4 ), Word::try_from(elements).unwrap(), ); } } + +/// Test that get_initial_item and get_initial_map_item work correctly with foreign accounts. +#[tokio::test] +async fn test_get_initial_item_and_get_initial_map_item_with_foreign_account() -> anyhow::Result<()> +{ + // Create a 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 (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 + + export.test_get_initial_item + push.0 + exec.active_account::get_initial_item + exec.sys::truncate_stack + end + + export.test_get_initial_map_item + exec.active_account::get_initial_map_item + exec.sys::truncate_stack + end + "; + + 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], + )? + .with_supports_all_types(); + + let foreign_account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(foreign_account_component.clone()) + .build_existing()?; + + // Create the mock chain with both accounts + let mut mock_chain = + MockChainBuilder::with_accounts([native_account.clone(), foreign_account.clone()])? + .build()?; + mock_chain.prove_next_block()?; + + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(foreign_account.id())?; + + let code = format!( + " + use.std::sys + use.miden::tx + + begin + + # Test get_initial_item on foreign account + padw padw padw push.0.0.0 + # => [ pad(4), pad(4), pad(4), 0, 0, 0 ] + 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\" + + # Test get_initial_map_item on foreign account + padw padw push.0.0 + push.{map_key} + push.1 + 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\" + + exec.sys::truncate_stack + end + ", + 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(), + map_key = &map_key, + map_value = &map_value, + ); + + let tx_script = ScriptBuilder::with_mock_libraries()? + .with_dynamically_linked_library(foreign_account_component.library())? + .compile_tx_script(code)?; + + mock_chain + .build_tx_context(native_account.id(), &[], &[])? + .foreign_accounts(vec![foreign_account_inputs]) + .tx_script(tx_script) + .build()? + .execute() + .await?; + + Ok(()) +} 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 cf23d83387..b903b3d6b0 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 @@ -10,8 +10,8 @@ use crate::TxContextInput; /// Check that the assets number and assets commitment obtained from the /// `input_note::get_assets_info` procedure is correct for each note with zero, one and two /// different assets. -#[test] -fn test_get_asset_info() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_asset_info() -> anyhow::Result<()> { let TestSetup { mock_chain, account, @@ -33,7 +33,7 @@ fn test_get_asset_info() -> anyhow::Result<()> { # => [ASSETS_COMMITMENT, num_assets] # assert the correctness of the assets hash - push.{COMPUTED_ASSETS_COMMITMENT} + push.{assets_commitment} assert_eqw.err="note {note_index} has incorrect assets hash" # => [num_assets] @@ -41,10 +41,7 @@ fn test_get_asset_info() -> anyhow::Result<()> { push.{assets_number} assert_eq.err="note {note_index} has incorrect assets number" # => [] - "#, - note_index = note_index, - COMPUTED_ASSETS_COMMITMENT = assets_commitment, - assets_number = assets_number, + "# ) } @@ -88,15 +85,15 @@ fn test_get_asset_info() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; Ok(()) } /// Check that recipient and metadata of a note with one asset obtained from the -/// `input_note::get_recipient` procedure is correct. -#[test] -fn test_get_recipient_and_metadata() -> anyhow::Result<()> { +/// `input_note::get_recipient` and `input_note::get_metadata` procedures are correct. +#[tokio::test] +async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { let TestSetup { mock_chain, account, @@ -142,15 +139,64 @@ fn test_get_recipient_and_metadata() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; + + Ok(()) +} + +/// Check that a sender of a note with one asset obtained from the `input_note::get_sender` +/// procedure is correct. +#[tokio::test] +async fn test_get_sender() -> anyhow::Result<()> { + let TestSetup { + mock_chain, + account, + p2id_note_0_assets: _, + p2id_note_1_asset, + p2id_note_2_assets: _, + } = setup_test()?; + + let code = format!( + r#" + use.miden::input_note + + begin + # get the sender from the input note + push.0 + exec.input_note::get_sender + # => [sender_id_prefix, sender_id_suffix] + + # assert the correctness of the prefix + push.{sender_prefix} + assert_eq.err="sender id prefix of the note 0 is incorrect" + # => [sender_id_suffix] + + # assert the correctness of the suffix + push.{sender_suffix} + assert_eq.err="sender id suffix of the note 0 is incorrect" + # => [] + end + "#, + sender_prefix = p2id_note_1_asset.metadata().sender().prefix().as_felt(), + sender_suffix = p2id_note_1_asset.metadata().sender().suffix(), + ); + + let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? + .tx_script(tx_script) + .build()?; + + tx_context.execute().await?; Ok(()) } /// Check that the assets number and assets data obtained from the `input_note::get_assets` /// procedure is correct for each note with zero, one and two different assets. -#[test] -fn test_get_assets() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_assets() -> anyhow::Result<()> { let TestSetup { mock_chain, account, @@ -163,7 +209,7 @@ fn test_get_assets() -> anyhow::Result<()> { let mut check_assets_code = format!( r#" # push the note index and memory destination pointer - push.{note_idx}.{dest_ptr} + push.{note_idx} push.{dest_ptr} # => [dest_ptr, note_index] # write the assets to the memory @@ -185,7 +231,7 @@ fn test_get_assets() -> anyhow::Result<()> { check_assets_code.push_str(&format!( r#" # load the asset stored in memory - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be # => [STORED_ASSET, dest_ptr, note_index] # assert the asset @@ -237,7 +283,143 @@ fn test_get_assets() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; + + Ok(()) +} + +/// Check that the number of the inputs and their commitment of a note with one asset +/// obtained from the `input_note::get_inputs_info` procedure is correct. +#[tokio::test] +async fn test_get_inputs_info() -> anyhow::Result<()> { + let TestSetup { + mock_chain, + account, + p2id_note_0_assets: _, + p2id_note_1_asset, + p2id_note_2_assets: _, + } = setup_test()?; + + let code = format!( + r#" + use.miden::input_note + + begin + # get the inputs commitment and length from the input note with index 0 (the only one + # we have) + push.0 + exec.input_note::get_inputs_info + # => [NOTE_INPUTS_COMMITMENT, inputs_num] + + # assert the correctness of the inputs commitment + push.{INPUTS_COMMITMENT} + assert_eqw.err="note 0 has incorrect inputs commitment" + # => [inputs_num] + + # assert the inputs have correct length + push.{inputs_num} + assert_eq.err="note 0 has incorrect inputs length" + # => [] + end + "#, + INPUTS_COMMITMENT = p2id_note_1_asset.inputs().commitment(), + inputs_num = p2id_note_1_asset.inputs().num_values(), + ); + + let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? + .tx_script(tx_script) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Check that the script root of a note with one asset obtained from the +/// `input_note::get_script_root` procedure is correct. +#[tokio::test] +async fn test_get_script_root() -> anyhow::Result<()> { + let TestSetup { + mock_chain, + account, + p2id_note_0_assets: _, + p2id_note_1_asset, + p2id_note_2_assets: _, + } = setup_test()?; + + let code = format!( + r#" + use.miden::input_note + + begin + # get the script root from the input note with index 0 (the only one we have) + push.0 + exec.input_note::get_script_root + # => [SCRIPT_ROOT] + + # assert the correctness of the script root + push.{SCRIPT_ROOT} + assert_eqw.err="note 0 has incorrect script root" + # => [] + end + "#, + SCRIPT_ROOT = p2id_note_1_asset.script().root(), + ); + + let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? + .tx_script(tx_script) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Check that the serial number of a note with one asset obtained from the +/// `input_note::get_serial_number` procedure is correct. +#[tokio::test] +async fn test_get_serial_number() -> anyhow::Result<()> { + let TestSetup { + mock_chain, + account, + p2id_note_0_assets: _, + p2id_note_1_asset, + p2id_note_2_assets: _, + } = setup_test()?; + + let code = format!( + r#" + use.miden::input_note + + begin + # get the serial number from the input note with index 0 (the only one we have) + push.0 + exec.input_note::get_serial_number + # => [SERIAL_NUMBER] + + # assert the correctness of the serial number + push.{SERIAL_NUMBER} + assert_eqw.err="note 0 has incorrect serial number" + # => [] + end + "#, + SERIAL_NUMBER = p2id_note_1_asset.serial_num(), + ); + + let tx_script = ScriptBuilder::default().compile_tx_script(code)?; + + let tx_context = mock_chain + .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1_asset])? + .tx_script(tx_script) + .build()?; + + tx_context.execute().await?; Ok(()) } 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 new file mode 100644 index 0000000000..83829fcd5e --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs @@ -0,0 +1,283 @@ +//! This module tests lazy loading. +//! +//! 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::{ + 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 super::Word; +use crate::{Auth, MockChain, TransactionContextBuilder}; + +// ASSET LAZY LOADING +// ================================================================================================ + +/// Tests that adding two different assets to the account vault succeeds when lazy loading is +/// enabled. +#[tokio::test] +async fn adding_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result<()> { + let faucet_id1: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); + let faucet_id2: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2.try_into().unwrap(); + + let fungible_asset1 = + FungibleAsset::new(faucet_id1, FungibleAsset::MAX_AMOUNT - FUNGIBLE_ASSET_AMOUNT)?; + let fungible_asset2 = FungibleAsset::new(faucet_id2, FUNGIBLE_ASSET_AMOUNT)?; + + // Build a note that adds the assets to the input vault of the transaction. This is necessary + // to adhere to asset preservation rules. + let asset_note = NoteBuilder::new(faucet_id1, rand::rng()) + .add_assets([fungible_asset1, fungible_asset2].map(Asset::from)) + .build()?; + + let code = format!( + " + use.mock::account + + begin + push.{FUNGIBLE_ASSET1} + call.account::add_asset dropw + + push.{FUNGIBLE_ASSET2} + call.account::add_asset dropw + end + ", + FUNGIBLE_ASSET1 = Word::from(fungible_asset1), + FUNGIBLE_ASSET2 = Word::from(fungible_asset2) + ); + + let builder = ScriptBuilder::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() + .tx_script(tx_script) + .extend_input_notes(vec![asset_note]) + .with_source_manager(source_manager) + .build()?; + let account = tx_context.account().clone(); + let tx = tx_context.execute().await?; + + let mut account_vault = account.vault().clone(); + account_vault.add_asset(fungible_asset1.into())?; + account_vault.add_asset(fungible_asset2.into())?; + + assert_eq!(tx.final_account().vault_root(), account_vault.root()); + + Ok(()) +} + +/// Tests that removing two different assets from the account vault succeeds when lazy loading is +/// enabled. +#[tokio::test] +async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result<()> { + let faucet_id1: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); + let faucet_id2: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2.try_into().unwrap(); + + let fungible_asset1 = + FungibleAsset::new(faucet_id1, FungibleAsset::MAX_AMOUNT - FUNGIBLE_ASSET_AMOUNT)?; + let fungible_asset2 = FungibleAsset::new(faucet_id2, FUNGIBLE_ASSET_AMOUNT)?; + + let code = format!( + " + use.mock::account + use.mock::util + + begin + push.{FUNGIBLE_ASSET1} + call.account::remove_asset + # => [] + + # move asset to note to adhere to asset preservation rules + exec.util::create_random_note_with_asset + # => [] + + push.{FUNGIBLE_ASSET2} + call.account::remove_asset + # => [ASSET] + + # move asset to note to adhere to asset preservation rules + exec.util::create_random_note_with_asset + # => [] + end + ", + FUNGIBLE_ASSET1 = Word::from(fungible_asset1), + FUNGIBLE_ASSET2 = Word::from(fungible_asset2) + ); + + let builder = ScriptBuilder::with_mock_libraries()?; + let source_manager = builder.source_manager(); + let tx_script = builder.compile_tx_script(code)?; + + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account_with_assets( + crate::Auth::IncrNonce, + [fungible_asset1, fungible_asset2].map(Asset::from), + )?; + let tx_context = builder + .build()? + .build_tx_context(account, &[], &[])? + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + let account = tx_context.account().clone(); + let tx = tx_context.execute().await?; + + let mut account_vault = account.vault().clone(); + account_vault.remove_asset(fungible_asset1.into())?; + account_vault.remove_asset(fungible_asset2.into())?; + + assert_eq!(tx.final_account().vault_root(), account_vault.root()); + + Ok(()) +} + +/// Tests that a transaction against an account with a non-empty vault successfully loads the fee +/// asset during the epilogue. +/// +/// The non-empty vault is important for the test because the advice provider's merkle store has all +/// merkle paths for an empty vault by default, and so there would be nothing to load. +#[tokio::test] +async fn loading_fee_asset_succeeds() -> anyhow::Result<()> { + let mut builder = + MockChain::builder().native_asset_id(ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into()?); + let account = builder.add_existing_mock_account_with_assets( + Auth::IncrNonce, + [ + FungibleAsset::mock(23), + FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2.try_into()?, 50)?.into(), + ], + )?; + builder.build()?.build_tx_context(account, &[], &[])?.build()?.execute().await?; + + Ok(()) +} + +// STORAGE LAZY LOADING +// ================================================================================================ + +/// Tests that updating or inserting a map item into a storage map succeeds when lazy loading is +/// enabled. +#[tokio::test] +async fn setting_map_item_with_lazy_loading_succeeds() -> anyhow::Result<()> { + // Fetch a random existing key from the map. + let mock_map = AccountStorage::mock_map(); + let existing_key = *mock_map.entries().next().unwrap().0; + + let non_existent_key = Word::from([5, 5, 5, 5u32]); + assert!( + mock_map.open(&non_existent_key).get(&non_existent_key).unwrap() == Word::empty(), + "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; + + let value0 = Word::from([3, 4, 5, 6u32]); + let value1 = Word::from([9, 8, 7, 6u32]); + + let code = format!( + " + use.mock::account + + begin + # Update an existing key. + push.{value0} + push.{existing_key} + push.{map_index} + # => [index, KEY, VALUE] + call.account::set_map_item + + # Insert a non-existent key. + push.{value1} + push.{non_existent_key} + push.{map_index} + # => [index, KEY, VALUE] + call.account::set_map_item + + exec.::std::sys::truncate_stack + end + " + ); + + let builder = ScriptBuilder::with_mock_libraries()?; + let source_manager = builder.source_manager(); + let tx_script = builder.compile_tx_script(code)?; + + let tx = TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; + + let map_delta = tx.account_delta().storage().maps().get(&map_index).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(), + &value1 + ); + + Ok(()) +} + +/// Tests that getting a map item from a storage map succeeds when lazy loading is enabled. +#[tokio::test] +async fn getting_map_item_with_lazy_loading_succeeds() -> anyhow::Result<()> { + // Fetch a random existing key from the map. + let mock_map = AccountStorage::mock_map(); + let (existing_key, existing_value) = mock_map.entries().next().unwrap(); + + let non_existent_key = Word::from([5, 5, 5, 5u32]); + assert!( + mock_map.open(&non_existent_key).get(&non_existent_key).unwrap() == Word::empty(), + "test setup requires that the non existent key does not exist" + ); + + let code = format!( + r#" + use.std::word + use.mock::account + + begin + # Fetch value from existing key. + push.{existing_key} + push.2 + # => [index, KEY] + call.account::get_map_item + + push.{existing_value} + assert_eqw.err="existing value does not match expected value" + + # Fetch a non-existent key. + push.{non_existent_key} + push.2 + # => [index, KEY] + call.account::get_map_item + + padw assert_eqw.err="non-existent value should be the empty word" + + exec.::std::sys::truncate_stack + end + "# + ); + + let builder = ScriptBuilder::with_mock_libraries()?; + let source_manager = builder.source_manager(); + let tx_script = builder.compile_tx_script(code)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; + + Ok(()) +} 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 48ebe76580..69979d73d3 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 @@ -1,25 +1,23 @@ use alloc::vec::Vec; -use core::cmp::Ordering; use std::collections::BTreeMap; use std::string::String; use anyhow::Context; use miden_objects::{EMPTY_WORD, LexicographicWord, Word}; -use miden_processor::{ONE, ProcessState, ZERO}; -use miden_tx::LinkMap; +use miden_processor::{ONE, ZERO}; +use miden_tx::{LinkMap, MemoryViewer}; use rand::seq::IteratorRandom; use winter_rand_utils::rand_value; use crate::TransactionContextBuilder; -use crate::executor::CodeExecutor; /// Tests the following properties: /// - Insertion into an empty map. /// - Insertion after an existing entry. /// - Insertion in between two existing entries. /// - Insertion before an existing head. -#[test] -fn insertion() -> anyhow::Result<()> { +#[tokio::test] +async fn insertion() -> anyhow::Result<()> { let map_ptr = 8u32; // check that using an empty word as key is fine let entry0_key = Word::from([0, 0, 0, 0u32]); @@ -171,22 +169,14 @@ fn insertion() -> anyhow::Result<()> { assert_eqw.err="retrieved value1 for key {entry3_key} should be an empty word" # => [] end - "#, - entry0_key = entry0_key, - entry0_value = entry0_value, - entry1_key = entry1_key, - entry1_value = entry1_value, - entry2_key = entry2_key, - entry2_value = entry2_value, - entry3_key = entry3_key, - entry3_value = entry3_value, + "# ); let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - let mut process = tx_context.execute_code(&code).context("failed to execute code")?; - let state = ProcessState::from(&mut process); + let exec_output = tx_context.execute_code(&code).await.context("failed to execute code")?; + let mem_viewer = MemoryViewer::ExecutionOutputs(&exec_output); - let map = LinkMap::new(map_ptr.into(), &state); + let map = LinkMap::new(map_ptr.into(), &mem_viewer); let mut map_iter = map.iter(); let entry0 = map_iter.next().expect("map should have four entries"); @@ -226,8 +216,8 @@ fn insertion() -> anyhow::Result<()> { Ok(()) } -#[test] -fn insert_and_update() -> anyhow::Result<()> { +#[tokio::test] +async fn insert_and_update() -> anyhow::Result<()> { const MAP_PTR: u32 = 8; let value0 = Word::from([1, 2, 3, 4u32]); @@ -244,11 +234,11 @@ fn insert_and_update() -> anyhow::Result<()> { TestOperation::set(MAP_PTR, link_map_key([3, 0, 0, 0]), (value1, value2)), ]; - execute_link_map_test(operations) + execute_link_map_test(operations).await } -#[test] -fn insert_at_head() -> anyhow::Result<()> { +#[tokio::test] +async fn insert_at_head() -> anyhow::Result<()> { const MAP_PTR: u32 = 8; let key3 = link_map_key([3, 0, 0, 0]); @@ -268,12 +258,12 @@ fn insert_at_head() -> anyhow::Result<()> { TestOperation::get(MAP_PTR, key3), ]; - execute_link_map_test(operations) + execute_link_map_test(operations).await } /// Tests that a get before a set results in the expected returned values and behavior. -#[test] -fn get_before_set() -> anyhow::Result<()> { +#[tokio::test] +async fn get_before_set() -> anyhow::Result<()> { const MAP_PTR: u32 = 8; let key0 = link_map_key([3, 0, 0, 0]); @@ -286,11 +276,11 @@ fn get_before_set() -> anyhow::Result<()> { TestOperation::get(MAP_PTR, key0), ]; - execute_link_map_test(operations) + execute_link_map_test(operations).await } -#[test] -fn multiple_link_maps() -> anyhow::Result<()> { +#[tokio::test] +async fn multiple_link_maps() -> anyhow::Result<()> { const MAP_PTR0: u32 = 8; const MAP_PTR1: u32 = 12; @@ -315,11 +305,11 @@ fn multiple_link_maps() -> anyhow::Result<()> { TestOperation::get(MAP_PTR1, key3), ]; - execute_link_map_test(operations) + execute_link_map_test(operations).await } -#[test] -fn iteration() -> anyhow::Result<()> { +#[tokio::test] +async fn iteration() -> anyhow::Result<()> { const MAP_PTR: u32 = 12; let entries = generate_entries(100); @@ -334,11 +324,11 @@ fn iteration() -> anyhow::Result<()> { // Iterate the map. test_operations.push(TestOperation::iter(MAP_PTR)); - execute_link_map_test(test_operations) + execute_link_map_test(test_operations).await } -#[test] -fn set_update_get_random_entries() -> anyhow::Result<()> { +#[tokio::test] +async fn set_update_get_random_entries() -> anyhow::Result<()> { const MAP_PTR: u32 = 12; let entries = generate_entries(1000); @@ -366,70 +356,7 @@ fn set_update_get_random_entries() -> anyhow::Result<()> { test_operations.extend(get_ops2); test_operations.extend(get_ops3); - execute_link_map_test(test_operations) -} - -// COMPARISON OPERATIONS TESTS -// ================================================================================================ - -#[test] -fn is_key_greater() -> anyhow::Result<()> { - execute_comparison_test(Ordering::Greater) -} - -#[test] -fn is_key_less() -> anyhow::Result<()> { - execute_comparison_test(Ordering::Less) -} - -fn execute_comparison_test(operation: Ordering) -> anyhow::Result<()> { - let procedure_name = match operation { - Ordering::Less => "is_key_less", - Ordering::Equal => anyhow::bail!("unsupported ordering operation for testing"), - Ordering::Greater => "is_key_greater", - }; - - let mut test_code = String::new(); - - for _ in 0..1000 { - let key0 = rand_value::(); - let key1 = rand_value::(); - - let cmp = LexicographicWord::from(key0).cmp(&LexicographicWord::from(key1)); - let expected = cmp == operation; - - let code = format!( - r#" - push.{KEY_1} - push.{KEY_0} - exec.link_map::{proc_name} - push.{expected_value} - assert_eq.err="failed for procedure {proc_name} with keys {key0:?}, {key1:?}" - "#, - KEY_0 = key0, - KEY_1 = key1, - proc_name = procedure_name, - expected_value = expected as u8 - ); - - test_code.push_str(&code); - } - - let code = format!( - r#" - use.$kernel::link_map - - begin - {test_code} - end - "#, - ); - - CodeExecutor::with_default_host() - .run(&code) - .with_context(|| format!("comparison test for {procedure_name} failed"))?; - - Ok(()) + execute_link_map_test(test_operations).await } // TEST HELPERS @@ -472,7 +399,7 @@ impl TestOperation { } } -fn execute_link_map_test(operations: Vec) -> anyhow::Result<()> { +async fn execute_link_map_test(operations: Vec) -> anyhow::Result<()> { let mut test_code = String::new(); let mut control_maps: BTreeMap> = BTreeMap::new(); @@ -485,7 +412,7 @@ fn execute_link_map_test(operations: Vec) -> anyhow::Result<()> { let set_code = format!( r#" - push.{value1}.{value0}.{key}.{map_ptr} + push.{value1} push.{value0} push.{key} push.{map_ptr} # => [map_ptr, KEY, VALUE] exec.link_map::set # => [is_new_key] @@ -512,7 +439,7 @@ fn execute_link_map_test(operations: Vec) -> anyhow::Result<()> { let get_code = format!( r#" - push.{key}.{map_ptr} + push.{key} push.{map_ptr} # => [map_ptr, KEY] exec.link_map::get # => [contains_key, VALUE0, VALUE1] @@ -615,11 +542,11 @@ fn execute_link_map_test(operations: Vec) -> anyhow::Result<()> { ); let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - let mut process = tx_context.execute_code(&code).context("failed to execute code")?; - let state = ProcessState::from(&mut process); + let exec_output = tx_context.execute_code(&code).await.context("failed to execute code")?; + let mem_viewer = MemoryViewer::ExecutionOutputs(&exec_output); for (map_ptr, control_map) in control_maps { - let map = LinkMap::new(map_ptr.into(), &state); + let map = LinkMap::new(map_ptr.into(), &mem_viewer); let actual_map_len = map.iter().count(); assert_eq!( actual_map_len, 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 4f940f34ed..1c37c1591e 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -1,18 +1,15 @@ use alloc::collections::BTreeMap; -use alloc::string::String; use alloc::sync::Arc; -use alloc::vec::Vec; use anyhow::Context; use miden_lib::account::wallets::BasicWallet; use miden_lib::errors::MasmError; -use miden_lib::errors::tx_kernel_errors::ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SENDER_FROM_INCORRECT_CONTEXT; -use miden_lib::testing::mock_account::MockAccountExt; use miden_lib::testing::note::NoteBuilder; use miden_lib::transaction::TransactionKernel; -use miden_lib::transaction::memory::CURRENT_INPUT_NOTE_PTR; +use miden_lib::transaction::memory::ACTIVE_INPUT_NOTE_PTR; use miden_lib::utils::ScriptBuilder; -use miden_objects::account::{Account, AccountBuilder, AccountId}; +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; @@ -31,465 +28,26 @@ use miden_objects::note::{ }; use miden_objects::testing::account_id::{ ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; -use miden_objects::transaction::{AccountInputs, OutputNote, TransactionArgs}; -use miden_objects::{EMPTY_WORD, ONE, WORD_SIZE, Word}; +use miden_objects::transaction::{OutputNote, TransactionArgs}; +use miden_objects::{Felt, Word, ZERO}; +use miden_processor::fast::ExecutionOutput; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; -use super::{Felt, Process, ZERO}; -use crate::kernel_tests::tx::ProcessMemoryExt; -use crate::utils::{create_p2any_note, input_note_data_ptr}; +use crate::kernel_tests::tx::{ExecutionOutputExt, input_note_data_ptr}; use crate::{ Auth, MockChain, TransactionContext, TransactionContextBuilder, TxContextInput, - assert_execution_error, assert_transaction_executor_error, }; -#[test] -fn test_get_sender_no_sender() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - // calling get_sender should return sender - let code = " - use.$kernel::memory - use.$kernel::prologue - use.miden::note - - begin - exec.prologue::prepare_transaction - - # force the current input note pointer to 0 - push.0 exec.memory::set_current_input_note_ptr - - # get the sender - exec.note::get_sender - end - "; - - let process = tx_context.execute_code(code); - - assert_execution_error!(process, ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SENDER_FROM_INCORRECT_CONTEXT); - Ok(()) -} - -#[test] -fn test_get_sender() -> anyhow::Result<()> { - let tx_context = { - let account = - Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - let input_note = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); - TransactionContextBuilder::new(account) - .extend_input_notes(vec![input_note]) - .build()? - }; - - // calling get_sender should return sender - let code = " - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::note - - begin - exec.prologue::prepare_transaction - exec.note_internal::prepare_note - dropw dropw dropw dropw - exec.note::get_sender - - # truncate the stack - swapw dropw - end - "; - - let process = tx_context.execute_code(code)?; - - let sender = tx_context.input_notes().get_note(0).note().metadata().sender(); - assert_eq!(process.stack.get(0), sender.prefix().as_felt()); - assert_eq!(process.stack.get(1), sender.suffix()); - Ok(()) -} - -#[test] -fn test_get_vault_data() -> anyhow::Result<()> { - let tx_context = { - let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(crate::Auth::BasicAuth)?; - let p2id_note_1 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(150)], - NoteType::Public, - )?; - let p2id_note_2 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(300)], - NoteType::Public, - )?; - let mut mock_chain = builder.build()?; - mock_chain.prove_next_block()?; - - mock_chain - .build_tx_context( - TxContextInput::AccountId(account.id()), - &[], - &[p2id_note_1, p2id_note_2], - )? - .build()? - }; - - let notes = tx_context.input_notes(); - - // calling get_assets_info should return assets info - let code = format!( - " - use.std::sys - - use.$kernel::prologue - use.$kernel::note - - begin - exec.prologue::prepare_transaction - - # get the assets info about note 0 - exec.note::get_assets_info - - # assert the assets data is correct - push.{note_0_asset_commitment} assert_eqw - push.{note_0_num_assets} assert_eq - - # increment current input note pointer - exec.note::increment_current_input_note_ptr - - # get the assets info about note 1 - exec.note::get_assets_info - - # assert the assets data is correct - push.{note_1_asset_commitment} assert_eqw - push.{note_1_num_assets} assert_eq - - # truncate the stack - exec.sys::truncate_stack - end - ", - note_0_asset_commitment = notes.get_note(0).note().assets().commitment(), - note_0_num_assets = notes.get_note(0).note().assets().num_assets(), - note_1_asset_commitment = notes.get_note(1).note().assets().commitment(), - note_1_num_assets = notes.get_note(1).note().assets().num_assets(), - ); - - tx_context.execute_code(&code)?; - Ok(()) -} - -#[test] -fn test_get_assets() -> anyhow::Result<()> { - // Creates a mockchain with an account and a note that it can consume - let tx_context = { - let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; - let p2id_note_1 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(150)], - NoteType::Public, - )?; - let p2id_note_2 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(300)], - NoteType::Public, - )?; - let mut mock_chain = builder.build()?; - mock_chain.prove_next_block()?; - - mock_chain - .build_tx_context( - TxContextInput::AccountId(account.id()), - &[], - &[p2id_note_1, p2id_note_2], - )? - .build()? - }; - - let notes = tx_context.input_notes(); - - const DEST_POINTER_NOTE_0: u32 = 100000000; - const DEST_POINTER_NOTE_1: u32 = 200000000; - - fn construct_asset_assertions(note: &Note) -> String { - let mut code = String::new(); - for asset in note.assets().iter() { - code += &format!( - " - # assert the asset is correct - dup padw movup.4 mem_loadw push.{asset} assert_eqw push.4 add - ", - asset = Word::from(asset) - ); - } - code - } - - // calling get_assets should return assets at the specified address - let code = format!( - " - use.std::sys - - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::note - - proc.process_note_0 - # drop the note inputs - dropw dropw dropw dropw - - # set the destination pointer for note 0 assets - push.{DEST_POINTER_NOTE_0} - - # get the assets - exec.note::get_assets - - # assert the number of assets is correct - eq.{note_0_num_assets} assert - - # assert the pointer is returned - dup eq.{DEST_POINTER_NOTE_0} assert - - # asset memory assertions - {NOTE_0_ASSET_ASSERTIONS} - - # clean pointer - drop - end - - proc.process_note_1 - # drop the note inputs - dropw dropw dropw dropw - - # set the destination pointer for note 1 assets - push.{DEST_POINTER_NOTE_1} - - # get the assets - exec.note::get_assets - - # assert the number of assets is correct - eq.{note_1_num_assets} assert - - # assert the pointer is returned - dup eq.{DEST_POINTER_NOTE_1} assert - - # asset memory assertions - {NOTE_1_ASSET_ASSERTIONS} - - # clean pointer - drop - end - - begin - # prepare tx - exec.prologue::prepare_transaction - - # prepare note 0 - exec.note_internal::prepare_note - - # process note 0 - call.process_note_0 - - # increment current input note pointer - exec.note_internal::increment_current_input_note_ptr - - # prepare note 1 - exec.note_internal::prepare_note - - # process note 1 - call.process_note_1 - - # 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()), - NOTE_1_ASSET_ASSERTIONS = construct_asset_assertions(notes.get_note(1).note()), - ); - - tx_context.execute_code(&code)?; - Ok(()) -} - -#[test] -fn test_get_inputs() -> anyhow::Result<()> { - // Creates a mockchain with an account and a note that it can consume - let tx_context = { - let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; - let p2id_note = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(100)], - NoteType::Public, - )?; - let mut mock_chain = builder.build()?; - mock_chain.prove_next_block()?; - - mock_chain - .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note])? - .build()? - }; - - fn construct_input_assertions(note: &Note) -> String { - let mut code = String::new(); - for input_chunk in note.inputs().values().chunks(WORD_SIZE) { - let mut input_word = EMPTY_WORD; - input_word.as_mut_slice()[..input_chunk.len()].copy_from_slice(input_chunk); - - code += &format!( - " - # assert the input is correct - dup padw movup.4 mem_loadw push.{input_word} assert_eqw push.4 add - ", - input_word = input_word - ); - } - code - } - - let note0 = tx_context.input_notes().get_note(0).note(); - - let code = format!( - " - use.$kernel::prologue - use.$kernel::note->note_internal - use.miden::note - - begin - # => [BH, acct_id, IAH, NC] - exec.prologue::prepare_transaction - # => [] - - exec.note_internal::prepare_note - # => [note_script_root_ptr, NOTE_ARGS, pad(11)] - - # drop the note inputs - dropw dropw dropw dropw - # => [] - - push.{NOTE_0_PTR} exec.note::get_inputs - # => [num_inputs, dest_ptr] - - eq.{num_inputs} assert - # => [dest_ptr] - - dup eq.{NOTE_0_PTR} assert - # => [dest_ptr] - - # apply note 1 input assertions - {input_assertions} - # => [dest_ptr] - - # clean the pointer - drop - # => [] - end - ", - num_inputs = note0.inputs().num_values(), - input_assertions = construct_input_assertions(note0), - NOTE_0_PTR = 100000000, - ); - - tx_context.execute_code(&code)?; - Ok(()) -} - -/// This test checks the scenario when an input note has exactly 8 input values, and the transaction -/// script attempts to load the inputs to memory using the `miden::note::get_inputs` procedure. -/// -/// Previously this setup was leading to the incorrect number of note input values computed during -/// the `get_inputs` procedure, see the -/// [issue #1363](https://github.com/0xMiden/miden-base/issues/1363) for more details. -#[test] -fn test_get_exactly_8_inputs() -> anyhow::Result<()> { - let sender_id = ACCOUNT_ID_SENDER - .try_into() - .context("failed to convert ACCOUNT_ID_SENDER to account ID")?; - let target_id = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into().context( - "failed to convert ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE to account ID", - )?; - - // 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 vault = NoteAssets::new(vec![]).context("failed to create input note assets")?; - let note_script = ScriptBuilder::default() - .compile_note_script("begin nop end") - .context("failed to compile note script")?; - - // create a recipient with note inputs, which number divides by 8. For simplicity create 8 input - // values - let recipient = NoteRecipient::new( - serial_num, - note_script, - NoteInputs::new(vec![ - ONE, - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - ]) - .context("failed to create note inputs")?, - ); - let input_note = Note::new(vault.clone(), metadata, recipient); - - // provide this input note to the transaction context - let tx_context = TransactionContextBuilder::with_existing_mock_account() - .extend_input_notes(vec![input_note]) - .build()?; - - let tx_code = " - use.$kernel::prologue - use.miden::note - - begin - exec.prologue::prepare_transaction - - # execute the `get_inputs` procedure to trigger note inputs number assertion - push.0 exec.note::get_inputs - # => [num_inputs, 0] - - # assert that the number if inputs is 8 - push.8 assert_eq.err=\"number of inputs should be equal to 8\" - - # clean the stack - drop - end - "; - - tx_context.execute_code(tx_code).context("transaction execution failed")?; - - Ok(()) -} - -#[test] -fn test_note_setup() -> anyhow::Result<()> { +#[tokio::test] +async fn test_note_setup() -> anyhow::Result<()> { let tx_context = { let mut builder = MockChain::builder(); let account = builder.add_existing_wallet(Auth::BasicAuth)?; @@ -515,7 +73,7 @@ fn test_note_setup() -> anyhow::Result<()> { exec.prologue::prepare_transaction exec.note::prepare_note # => [note_script_root_ptr, NOTE_ARGS, pad(11), pad(16)] - padw movup.4 mem_loadw + padw movup.4 mem_loadw_be # => [SCRIPT_ROOT, NOTE_ARGS, pad(11), pad(16)] # truncate the stack @@ -523,15 +81,15 @@ fn test_note_setup() -> anyhow::Result<()> { end "; - let process = tx_context.execute_code(code)?; + let exec_output = tx_context.execute_code(code).await?; - note_setup_stack_assertions(&process, &tx_context); - note_setup_memory_assertions(&process); + note_setup_stack_assertions(&exec_output, &tx_context); + note_setup_memory_assertions(&exec_output); Ok(()) } -#[test] -fn test_note_script_and_note_args() -> miette::Result<()> { +#[tokio::test] +async fn test_note_script_and_note_args() -> miette::Result<()> { let mut tx_context = { let mut builder = MockChain::builder(); let account = builder.add_existing_wallet(Auth::BasicAuth).map_err(|err| miette!(err))?; @@ -578,7 +136,7 @@ fn test_note_script_and_note_args() -> miette::Result<()> { repeat.11 movup.4 drop end # => [NOTE_ARGS0, pad(16)] - exec.note::increment_current_input_note_ptr drop + exec.note::increment_active_input_note_ptr drop # => [NOTE_ARGS0, pad(16)] exec.note::prepare_note drop @@ -597,22 +155,19 @@ fn test_note_script_and_note_args() -> miette::Result<()> { (tx_context.input_notes().get_note(1).note().id(), note_args[0]), ]); - let tx_args = TransactionArgs::new( - tx_context.tx_args().advice_inputs().clone().map, - Vec::::new(), - ) - .with_note_args(note_args_map); + let tx_args = TransactionArgs::new(tx_context.tx_args().advice_inputs().clone().map) + .with_note_args(note_args_map); tx_context.set_tx_args(tx_args); - let process = tx_context.execute_code(code).unwrap(); + let exec_output = tx_context.execute_code(code).await.unwrap(); - assert_eq!(process.stack.get_word(0), note_args[0]); - assert_eq!(process.stack.get_word(1), note_args[1]); + assert_eq!(exec_output.get_stack_word_be(0), note_args[0]); + assert_eq!(exec_output.get_stack_word_be(4), note_args[1]); Ok(()) } -fn note_setup_stack_assertions(process: &Process, inputs: &TransactionContext) { +fn note_setup_stack_assertions(exec_output: &ExecutionOutput, inputs: &TransactionContext) { let mut expected_stack = [ZERO; 16]; // replace the top four elements with the tx script root @@ -621,71 +176,133 @@ fn note_setup_stack_assertions(process: &Process, inputs: &TransactionContext) { expected_stack[..4].copy_from_slice(¬e_script_root); // assert that the stack contains the note inputs at the end of execution - assert_eq!(process.stack.trace_state(), expected_stack) + assert_eq!(exec_output.stack.as_slice(), expected_stack.as_slice()) } -fn note_setup_memory_assertions(process: &Process) { +fn note_setup_memory_assertions(exec_output: &ExecutionOutput) { // assert that the correct pointer is stored in bookkeeping memory assert_eq!( - process.get_kernel_mem_word(CURRENT_INPUT_NOTE_PTR)[0], + exec_output.get_kernel_mem_word(ACTIVE_INPUT_NOTE_PTR)[0], Felt::from(input_note_data_ptr(0)) ); } -#[test] -fn test_get_note_serial_number() -> anyhow::Result<()> { - let tx_context = { - let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; - let p2id_note_1 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(150)], - NoteType::Public, - )?; - let mock_chain = builder.build()?; +#[tokio::test] +async fn test_build_recipient() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - mock_chain - .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1])? - .build()? - }; + // Create test script and serial number + let note_script = ScriptBuilder::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 - // calling get_serial_number should return the serial number of the note - let code = " - use.$kernel::prologue use.miden::note begin - exec.prologue::prepare_transaction - exec.note::get_serial_number + # 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 + 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 + 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 + push.{script_root} # SCRIPT_ROOT + push.{serial_num} # SERIAL_NUM + push.13.4000 # num_inputs, inputs_ptr + exec.note::build_recipient + # => [RECIPIENT_13, RECIPIENT_5, RECIPIENT_4] # truncate the stack - swapw dropw + exec.sys::truncate_stack end - "; + ", + 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 process = tx_context.execute_code(code)?; + let exec_output = &tx_context.execute_code(&code).await?; - let serial_number = tx_context.input_notes().get_note(0).note().serial_num(); - assert_eq!(process.stack.get_word(0), serial_number); + // 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); + + 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 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); + + 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.reverse(); + + assert_eq!(exec_output.stack[0..12], expected_stack); Ok(()) } -#[test] -fn test_get_inputs_hash() -> anyhow::Result<()> { +#[tokio::test] +async fn test_compute_inputs_commitment() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - let code = " + // 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 begin # put the values that will be hashed into the memory - push.1.2.3.4.4000 mem_storew dropw - push.5.6.7.8.4004 mem_storew dropw - push.9.10.11.12.4008 mem_storew dropw - push.13.14.15.16.4012 mem_storew dropw + 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 # push the number of values and pointer to the inputs on the stack push.5.4000 @@ -712,49 +329,32 @@ fn test_get_inputs_hash() -> anyhow::Result<()> { # truncate the stack exec.sys::truncate_stack end - "; - - let process = &tx_context.execute_code(code)?; - - let note_inputs_5_hash = NoteInputs::new(vec![ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - ])? - .commitment(); - - let note_inputs_8_hash = NoteInputs::new(vec![ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - ])? - .commitment(); - - let note_inputs_15_hash = NoteInputs::new(vec![ - Felt::new(1), - Felt::new(2), - Felt::new(3), - Felt::new(4), - Felt::new(5), - Felt::new(6), - Felt::new(7), - Felt::new(8), - Felt::new(9), - Felt::new(10), - Felt::new(11), - Felt::new(12), - Felt::new(13), - Felt::new(14), - Felt::new(15), - ])? - .commitment(); + ", + 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, + ); + + let exec_output = &tx_context.execute_code(&code).await?; + + let mut inputs_5 = word_1.to_vec(); + inputs_5.push(word_2[0]); + let note_inputs_5_hash = NoteInputs::new(inputs_5)?.commitment(); + + let mut inputs_8 = word_1.to_vec(); + inputs_8.extend_from_slice(&word_2.to_vec()); + let note_inputs_8_hash = NoteInputs::new(inputs_8)?.commitment(); + + let mut inputs_15 = word_1.to_vec(); + inputs_15.extend_from_slice(&word_2.to_vec()); + inputs_15.extend_from_slice(&word_3.to_vec()); + inputs_15.extend_from_slice(&word_4[0..3]); + let note_inputs_15_hash = NoteInputs::new(inputs_15)?.commitment(); let mut expected_stack = alloc::vec::Vec::new(); @@ -764,51 +364,12 @@ fn test_get_inputs_hash() -> anyhow::Result<()> { expected_stack.extend_from_slice(Word::empty().as_elements()); expected_stack.reverse(); - assert_eq!(process.stack.get_state_at(process.system.clk())[0..16], expected_stack); - Ok(()) -} - -#[test] -fn test_get_current_script_root() -> anyhow::Result<()> { - let tx_context = { - let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; - let p2id_note_1 = builder.add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(150)], - NoteType::Public, - )?; - let mock_chain = builder.build()?; - - mock_chain - .build_tx_context(TxContextInput::AccountId(account.id()), &[], &[p2id_note_1])? - .build()? - }; - - // calling get_script_root should return script root - let code = " - use.$kernel::prologue - use.miden::note - - begin - exec.prologue::prepare_transaction - exec.note::get_script_root - - # truncate the stack - swapw dropw - end - "; - - let process = tx_context.execute_code(code)?; - - let script_root = tx_context.input_notes().get_note(0).note().script().root(); - assert_eq!(process.stack.get_word(0), script_root); + assert_eq!(exec_output.stack[0..16], expected_stack); Ok(()) } -#[test] -fn test_build_note_metadata() -> miette::Result<()> { +#[tokio::test] +async fn test_build_metadata() -> miette::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); let sender = tx_context.account().id(); @@ -839,12 +400,12 @@ fn test_build_note_metadata() -> miette::Result<()> { let code = format!( " use.$kernel::prologue - use.$kernel::tx + use.$kernel::output_note begin exec.prologue::prepare_transaction - push.{execution_hint}.{note_type}.{aux}.{tag} - exec.tx::build_note_metadata + push.{execution_hint} push.{note_type} push.{aux} push.{tag} + exec.output_note::build_metadata # truncate the stack swapw dropw @@ -856,14 +417,9 @@ fn test_build_note_metadata() -> miette::Result<()> { tag = test_metadata.tag(), ); - let process = tx_context.execute_code(&code).unwrap(); + let exec_output = tx_context.execute_code(&code).await.unwrap(); - let metadata_word = Word::new([ - process.stack.get(3), - process.stack.get(2), - process.stack.get(1), - process.stack.get(0), - ]); + let metadata_word = exec_output.get_stack_word_be(0); assert_eq!(Word::from(test_metadata), metadata_word, "failed in iteration {iteration}"); } @@ -872,18 +428,18 @@ fn test_build_note_metadata() -> miette::Result<()> { } /// This serves as a test that setting a custom timestamp on mock chain blocks works. -#[test] -pub fn test_timelock() -> anyhow::Result<()> { +#[tokio::test] +pub async fn test_timelock() -> anyhow::Result<()> { const TIMESTAMP_ERROR: MasmError = MasmError::from_static_str("123"); let code = format!( r#" - use.miden::note + use.miden::active_note use.miden::tx begin # store the note inputs to memory starting at address 0 - push.0 exec.note::get_inputs + push.0 exec.active_note::get_inputs # => [num_inputs, inputs_ptr] # make sure the number of inputs is 1 @@ -916,7 +472,7 @@ pub fn test_timelock() -> anyhow::Result<()> { .dynamically_linked_libraries(TransactionKernel::mock_libraries()) .build()?; - builder.add_note(OutputNote::Full(timelock_note.clone())); + builder.add_output_note(OutputNote::Full(timelock_note.clone())); let mut mock_chain = builder.build()?; mock_chain @@ -925,13 +481,12 @@ pub fn test_timelock() -> anyhow::Result<()> { // Attempt to consume note too early. // ---------------------------------------------------------------------------------------- - let tx_inputs = - mock_chain.get_transaction_inputs(account.clone(), None, &[timelock_note.id()], &[])?; + let tx_inputs = mock_chain.get_transaction_inputs(&account, &[timelock_note.id()], &[])?; let tx_context = TransactionContextBuilder::new(account.clone()) .with_source_manager(source_manager.clone()) .tx_inputs(tx_inputs.clone()) .build()?; - let result = tx_context.execute_blocking(); + let result = tx_context.execute().await; assert_transaction_executor_error!(result, TIMESTAMP_ERROR); // Consume note where lock timestamp matches the block timestamp. @@ -940,10 +495,9 @@ pub fn test_timelock() -> anyhow::Result<()> { .prove_next_block_at(lock_timestamp) .context("failed to prove next block at lock timestamp")?; - let tx_inputs = - mock_chain.get_transaction_inputs(account.clone(), None, &[timelock_note.id()], &[])?; + let tx_inputs = mock_chain.get_transaction_inputs(&account, &[timelock_note.id()], &[])?; let tx_context = TransactionContextBuilder::new(account).tx_inputs(tx_inputs).build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; Ok(()) } @@ -953,14 +507,14 @@ pub fn test_timelock() -> anyhow::Result<()> { /// /// Previously this setup was leading to the values collision in the advice map, see the /// [issue #1267](https://github.com/0xMiden/miden-base/issues/1267) for more details. -#[test] -fn test_public_key_as_note_input() -> anyhow::Result<()> { +#[tokio::test] +async fn test_public_key_as_note_input() -> anyhow::Result<()> { let mut rng = ChaCha20Rng::from_seed(Default::default()); let sec_key = SecretKey::with_rng(&mut rng); // this value will be used both as public key in the RPO component of the target account and as // well as the input of the input note - let public_key = sec_key.public_key(); - let public_key_value: Word = public_key.into(); + let public_key = PublicKeyCommitment::from(sec_key.public_key()); + let public_key_value = Word::from(public_key); let (rpo_component, authenticator) = Auth::BasicAuth.build_component(); @@ -997,6 +551,6 @@ fn test_public_key_as_note_input() -> anyhow::Result<()> { .authenticator(authenticator) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; 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 aeccac7117..b004a03ff7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs @@ -1,21 +1,703 @@ use alloc::string::String; +use alloc::vec::Vec; +use anyhow::Context; +use miden_lib::errors::tx_kernel_errors::{ + 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::Word; -use miden_objects::account::AccountId; -use miden_objects::asset::{Asset, FungibleAsset}; +use miden_objects::account::{Account, AccountId}; +use miden_objects::asset::{Asset, FungibleAsset, NonFungibleAsset}; use miden_objects::crypto::rand::RpoRandomCoin; -use miden_objects::note::{Note, NoteType}; +use miden_objects::note::{ + Note, + NoteAssets, + NoteExecutionHint, + NoteExecutionMode, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteTag, + NoteType, +}; use miden_objects::testing::account_id::{ + ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_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_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, + ACCOUNT_ID_SENDER, }; -use miden_objects::transaction::OutputNote; +use miden_objects::testing::constants::NON_FUNGIBLE_ASSET_DATA_2; +use miden_objects::transaction::{OutputNote, OutputNotes}; +use miden_objects::{Felt, Word, ZERO}; + +use super::{TestSetup, setup_test}; +use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::create_public_p2any_note; +use crate::{Auth, MockChain, TransactionContextBuilder, assert_execution_error}; + +#[tokio::test] +async fn test_create_note() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + 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 code = format!( + " + use.miden::output_note + + 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 + + # truncate the stack + swapdw dropw dropw + end + ", + recipient = recipient, + PUBLIC_NOTE = NoteType::Public as u8, + note_execution_hint = Felt::from(NoteExecutionHint::after_block(23.into()).unwrap()), + tag = tag, + ); + + let exec_output = &tx_context.execute_code(&code).await?; + + assert_eq!( + exec_output.get_kernel_mem_word(NUM_OUTPUT_NOTES_PTR), + Word::from([1, 0, 0, 0u32]), + "number of output notes must increment by 1", + ); + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_RECIPIENT_OFFSET), + recipient, + "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(); + + 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", + ); + + assert_eq!( + exec_output.get_stack_element(0), + ZERO, + "top item on the stack is the index of the output note" + ); + Ok(()) +} + +#[tokio::test] +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(); + + // Test invalid tag + assert!(tx_context.execute_code(¬e_creation_script(invalid_tag)).await.is_err()); + + // Test valid tag + assert!(tx_context.execute_code(¬e_creation_script(valid_tag)).await.is_ok()); + + Ok(()) +} + +fn note_creation_script(tag: Felt) -> String { + format!( + " + use.miden::output_note + use.$kernel::prologue -use super::{Felt, TestSetup, setup_test}; -use crate::{Auth, MockChain}; + begin + exec.prologue::prepare_transaction + + push.{recipient} + push.{execution_hint_always} + push.{PUBLIC_NOTE} + push.{aux} + push.{tag} + + call.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, + ) +} + +#[tokio::test] +async fn test_create_note_too_many_notes() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + let code = format!( + " + use.miden::output_note + use.$kernel::constants + use.$kernel::memory + use.$kernel::prologue + + begin + exec.constants::get_max_num_output_notes + 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 + end + ", + tag = NoteTag::for_local_use_case(1234, 5678).unwrap(), + 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; + + assert_execution_error!(exec_output, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT); + Ok(()) +} + +#[tokio::test] +async fn test_get_output_notes_commitment() -> 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)]); + + let input_note_1 = create_public_p2any_note( + ACCOUNT_ID_PRIVATE_SENDER.try_into()?, + [FungibleAsset::mock(100)], + ); + + let input_note_2 = create_public_p2any_note( + ACCOUNT_ID_PRIVATE_SENDER.try_into()?, + [FungibleAsset::mock(200)], + ); + + TransactionContextBuilder::new(account) + .extend_input_notes(vec![input_note_1, input_note_2]) + .extend_expected_output_notes(vec![OutputNote::Full(output_note_1)]) + .build()? + }; + + // extract input note data + let input_note_1 = tx_context.tx_inputs().input_notes().get_note(0).note(); + let input_asset_1 = **input_note_1 + .assets() + .iter() + .take(1) + .collect::>() + .first() + .context("getting first expected input asset")?; + let input_note_2 = tx_context.tx_inputs().input_notes().get_note(1).note(); + let input_asset_2 = **input_note_2 + .assets() + .iter() + .take(1) + .collect::>() + .first() + .context("getting second expected input asset")?; + + // Choose random accounts as the target for the note tag. + let network_account = AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET)?; + let local_account = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET)?; + + // create output note 1 + let output_serial_no_1 = Word::from([8u32; 4]); + let output_tag_1 = NoteTag::from_account_id(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 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 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 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); + + // compute expected output notes commitment + let expected_output_notes_commitment = OutputNotes::new(vec![ + OutputNote::Full(output_note_1.clone()), + OutputNote::Full(output_note_2.clone()), + ])? + .commitment(); + + let code = format!( + " + use.std::sys + + use.miden::tx + use.miden::output_note + + 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 + # => [note_idx] + + push.{asset_1} + call.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 + # => [note_idx] + + push.{asset_2} + call.output_note::add_asset + # => [] + + # compute the output notes commitment + exec.tx::get_output_notes_commitment + # => [OUTPUT_NOTES_COMMITMENT] + + # truncate the stack + exec.sys::truncate_stack + # => [OUTPUT_NOTES_COMMITMENT] + 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() + ), + ); + + let exec_output = &tx_context.execute_code(&code).await?; + + assert_eq!( + exec_output.get_kernel_mem_word(NUM_OUTPUT_NOTES_PTR), + Word::from([2u32, 0, 0, 0]), + "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", + ); + 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", + ); + + assert_eq!(exec_output.get_stack_word_be(0), expected_output_notes_commitment); + Ok(()) +} + +#[tokio::test] +async fn test_create_note_and_add_asset() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + 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 asset = Word::from(FungibleAsset::new(faucet_id, 10)?); + + let code = format!( + " + use.miden::output_note + + 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 + # => [note_idx] + + # assert that the index of the created note equals zero + dup assertz.err=\"index of the created note should be zero\" + # => [note_idx] + + push.{asset} + # => [ASSET, note_idx] + + call.output_note::add_asset + # => [] + + # truncate the stack + dropw dropw dropw + end + ", + recipient = recipient, + PUBLIC_NOTE = NoteType::Public as u8, + NOTE_EXECUTION_HINT = Felt::from(NoteExecutionHint::always()), + tag = tag, + asset = asset, + ); + + let exec_output = &tx_context.execute_code(&code).await?; + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET), + asset, + "asset must be stored at the correct memory location", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; + 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 asset = Word::from(FungibleAsset::new(faucet, 10)?); + let asset_2 = Word::from(FungibleAsset::new(faucet_2, 20)?); + let asset_3 = Word::from(FungibleAsset::new(faucet_2, 30)?); + let asset_2_and_3 = Word::from(FungibleAsset::new(faucet_2, 50)?); + + let non_fungible_asset = NonFungibleAsset::mock(&NON_FUNGIBLE_ASSET_DATA_2); + let non_fungible_asset_encoded = Word::from(non_fungible_asset); + + let code = format!( + " + use.miden::output_note + use.$kernel::prologue + + begin + exec.prologue::prepare_transaction + + push.{recipient} + push.{PUBLIC_NOTE} + push.{aux} + push.{tag} + + call.output_note::create + # => [note_idx] + + # assert that the index of the created note equals zero + dup assertz.err=\"index of the created note should be zero\" + # => [note_idx] + + dup push.{asset} + call.output_note::add_asset + # => [note_idx] + + dup push.{asset_2} + call.output_note::add_asset + # => [note_idx] + + dup push.{asset_3} + call.output_note::add_asset + # => [note_idx] + + push.{nft} + call.output_note::add_asset + # => [] + + # truncate the stack + repeat.7 dropw end + end + ", + recipient = recipient, + PUBLIC_NOTE = NoteType::Public as u8, + tag = tag, + asset = asset, + asset_2 = asset_2, + asset_3 = asset_3, + nft = non_fungible_asset_encoded, + ); + + let exec_output = &tx_context.execute_code(&code).await?; + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET), + asset, + "asset must be stored at the correct memory location", + ); + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + 4), + asset_2_and_3, + "asset_2 and asset_3 must be stored at the same correct memory location", + ); + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + 8), + non_fungible_asset_encoded, + "non_fungible_asset must be stored at the correct memory location", + ); + + Ok(()) +} + +#[tokio::test] +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 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 + + begin + exec.prologue::prepare_transaction + # => [] + + padw padw + push.{recipient} + push.{execution_hint_always} + push.{PUBLIC_NOTE} + push.{aux} + push.{tag} + + call.output_note::create + # => [note_idx] + + dup push.{nft} + # => [NFT, note_idx, note_idx] + + call.output_note::add_asset + # => [note_idx] + + push.{nft} + call.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, + ); + + let exec_output = tx_context.execute_code(&code).await; + + assert_execution_error!(exec_output, ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS); + Ok(()) +} + +/// Tests that creating a note with a fungible asset with amount zero works. +#[tokio::test] +async fn creating_note_with_fungible_asset_amount_zero_works() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let output_note = builder.add_p2id_note( + account.id(), + account.id(), + &[FungibleAsset::mock(0)], + NoteType::Private, + )?; + let input_note = builder.add_spawn_note([&output_note])?; + let chain = builder.build()?; + + chain + .build_tx_context(account, &[input_note.id()], &[])? + .build()? + .execute() + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_build_recipient_hash() -> anyhow::Result<()> { + let tx_context = { + let account = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); + + let input_note_1 = create_public_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + [FungibleAsset::mock(100)], + ); + TransactionContextBuilder::new(account) + .extend_input_notes(vec![input_note_1]) + .build()? + }; + let input_note_1 = tx_context.tx_inputs().input_notes().get_note(0).note(); + + // 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 single_input = 2; + let inputs = NoteInputs::new(vec![Felt::new(single_input)]).unwrap(); + let input_commitment = inputs.commitment(); + + 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 + + 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)] + + 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)] + + call.output_note::create + # => [note_idx, pad(19)] + + # clean the stack + dropw dropw dropw dropw dropw + 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?; + + assert_eq!( + exec_output.get_kernel_mem_word(NUM_OUTPUT_NOTES_PTR), + Word::from([1, 0, 0, 0u32]), + "number of output notes must increment by 1", + ); + + let recipient_digest = recipient.clone().digest(); + + assert_eq!( + exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_RECIPIENT_OFFSET), + recipient_digest, + "recipient hash not correct", + ); + Ok(()) +} /// This test creates an output note and then adds some assets into it checking the assets info on /// each stage. @@ -25,8 +707,8 @@ use crate::{Auth, MockChain}; /// - Right after the previous check to make sure it returns the same commitment from the cached /// data. /// - After adding the second `asset_1` to the note. -#[test] -fn test_get_asset_info() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_asset_info() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let fungible_asset_0 = Asset::Fungible( @@ -72,7 +754,6 @@ fn test_get_asset_info() -> anyhow::Result<()> { let tx_script_src = &format!( r#" - use.miden::tx use.miden::output_note use.std::sys @@ -83,7 +764,7 @@ fn test_get_asset_info() -> anyhow::Result<()> { push.{note_type} push.0 # aux push.{tag} - call.tx::create_note + call.output_note::create # => [note_idx] # move the asset 0 to the note @@ -162,15 +843,15 @@ fn test_get_asset_info() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; Ok(()) } /// Check that recipient and metadata of a note with one asset obtained from the /// `output_note::get_recipient` procedure is correct. -#[test] -fn test_get_recipient_and_metadata() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_recipient_and_metadata() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let account = @@ -189,7 +870,6 @@ fn test_get_recipient_and_metadata() -> anyhow::Result<()> { let tx_script_src = &format!( r#" - use.miden::tx use.miden::output_note use.std::sys @@ -235,15 +915,15 @@ fn test_get_recipient_and_metadata() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; Ok(()) } /// Check that the assets number and assets data obtained from the `output_note::get_assets` /// procedure is correct for each note with zero, one and two different assets. -#[test] -fn test_get_assets() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_assets() -> anyhow::Result<()> { let TestSetup { mock_chain, account, @@ -256,7 +936,7 @@ fn test_get_assets() -> anyhow::Result<()> { let mut check_assets_code = format!( r#" # push the note index and memory destination pointer - push.{note_idx}.{dest_ptr} + push.{note_idx} push.{dest_ptr} # => [dest_ptr, note_index] # write the assets to the memory @@ -278,7 +958,7 @@ fn test_get_assets() -> anyhow::Result<()> { check_assets_code.push_str(&format!( r#" # load the asset stored in memory - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be # => [STORED_ASSET, dest_ptr, note_index] # assert the asset @@ -304,7 +984,6 @@ fn test_get_assets() -> anyhow::Result<()> { let tx_script_src = &format!( " - use.miden::tx use.miden::output_note use.std::sys @@ -342,7 +1021,7 @@ fn test_get_assets() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; Ok(()) } @@ -362,7 +1041,7 @@ fn create_output_note(note: &Note) -> String { push.{note_type} push.0 # aux push.{tag} - call.tx::create_note + call.output_note::create # => [note_idx] ", RECIPIENT = note.recipient().digest(), 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 531db003c3..b3d65aa2f3 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -37,7 +37,6 @@ use miden_lib::transaction::memory::{ INPUT_NOTE_SERIAL_NUM_OFFSET, INPUT_NOTES_COMMITMENT_PTR, KERNEL_PROCEDURES_PTR, - MemoryOffset, NATIVE_ACCT_CODE_COMMITMENT_PTR, NATIVE_ACCT_ID_AND_NONCE_PTR, NATIVE_ACCT_ID_PTR, @@ -81,21 +80,17 @@ use miden_objects::testing::account_id::{ ACCOUNT_ID_SENDER, }; use miden_objects::testing::noop_auth_component::NoopAuthComponent; -use miden_objects::transaction::{ - AccountInputs, - ExecutedTransaction, - TransactionArgs, - TransactionScript, -}; -use miden_objects::{EMPTY_WORD, WORD_SIZE}; -use miden_processor::{AdviceInputs, Process, Word}; +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_tx::TransactionExecutorError; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use super::{Felt, ZERO}; -use crate::kernel_tests::tx::ProcessMemoryExt; -use crate::utils::{create_p2any_note, input_note_data_ptr}; +use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::create_public_p2any_note; use crate::{ Auth, MockChain, @@ -105,17 +100,23 @@ use crate::{ assert_transaction_executor_error, }; -#[test] -fn test_transaction_prologue() -> anyhow::Result<()> { +#[tokio::test] +async fn test_transaction_prologue() -> anyhow::Result<()> { let mut tx_context = { let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - let input_note_1 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); - let input_note_2 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); - let input_note_3 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(111)]); + let input_note_1 = create_public_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + [FungibleAsset::mock(100)], + ); + let input_note_2 = create_public_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + [FungibleAsset::mock(100)], + ); + let input_note_3 = create_public_p2any_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + [FungibleAsset::mock(111)], + ); TransactionContextBuilder::new(account) .extend_input_notes(vec![input_note_1, input_note_2, input_note_3]) .build()? @@ -142,163 +143,160 @@ fn test_transaction_prologue() -> anyhow::Result<()> { let tx_script = TransactionScript::new(mock_tx_script_program); - let note_args = [Word::from([91, 91, 91, 91u32]), Word::from([92, 92, 92, 92u32])]; + let note_args = [Word::from([91u32; 4]), Word::from([92u32; 4])]; let note_args_map = BTreeMap::from([ (tx_context.input_notes().get_note(0).note().id(), note_args[0]), (tx_context.input_notes().get_note(1).note().id(), note_args[1]), ]); - let tx_args = TransactionArgs::new( - tx_context.tx_args().advice_inputs().clone().map, - Vec::::new(), - ) - .with_tx_script(tx_script) - .with_note_args(note_args_map); + let tx_args = TransactionArgs::new(tx_context.tx_args().advice_inputs().clone().map) + .with_tx_script(tx_script) + .with_note_args(note_args_map); tx_context.set_tx_args(tx_args); - let process = &tx_context.execute_code(code)?; + let exec_output = &tx_context.execute_code(code).await?; - global_input_memory_assertions(process, &tx_context); - block_data_memory_assertions(process, &tx_context); - partial_blockchain_memory_assertions(process, &tx_context); - kernel_data_memory_assertions(process); - account_data_memory_assertions(process, &tx_context); - input_notes_memory_assertions(process, &tx_context, ¬e_args); + global_input_memory_assertions(exec_output, &tx_context); + block_data_memory_assertions(exec_output, &tx_context); + partial_blockchain_memory_assertions(exec_output, &tx_context); + kernel_data_memory_assertions(exec_output); + account_data_memory_assertions(exec_output, &tx_context); + input_notes_memory_assertions(exec_output, &tx_context, ¬e_args); Ok(()) } -fn global_input_memory_assertions(process: &Process, inputs: &TransactionContext) { +fn global_input_memory_assertions(exec_output: &ExecutionOutput, inputs: &TransactionContext) { assert_eq!( - process.get_kernel_mem_word(BLOCK_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(BLOCK_COMMITMENT_PTR), inputs.tx_inputs().block_header().commitment(), "The block commitment should be stored at the BLOCK_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCT_ID_PTR)[0], + exec_output.get_kernel_mem_word(NATIVE_ACCT_ID_PTR)[0], inputs.account().id().suffix(), "The account ID prefix should be stored at the ACCT_ID_PTR[0]" ); assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCT_ID_PTR)[1], + exec_output.get_kernel_mem_word(NATIVE_ACCT_ID_PTR)[1], inputs.account().id().prefix().as_felt(), "The account ID suffix should be stored at the ACCT_ID_PTR[1]" ); assert_eq!( - process.get_kernel_mem_word(INIT_ACCT_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(INIT_ACCT_COMMITMENT_PTR), inputs.account().commitment(), "The account commitment should be stored at the INIT_ACCT_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(INIT_NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(INIT_NATIVE_ACCT_VAULT_ROOT_PTR), inputs.account().vault().root(), "The initial native account vault root should be stored at the INIT_ACCT_VAULT_ROOT_PTR" ); assert_eq!( - process.get_kernel_mem_word(INIT_NATIVE_ACCT_STORAGE_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(INIT_NATIVE_ACCT_STORAGE_COMMITMENT_PTR), inputs.account().storage().commitment(), "The initial native account storage commitment should be stored at the INIT_ACCT_STORAGE_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(INPUT_NOTES_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(INPUT_NOTES_COMMITMENT_PTR), inputs.input_notes().commitment(), "The nullifier commitment should be stored at the INPUT_NOTES_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(INIT_NONCE_PTR)[0], + exec_output.get_kernel_mem_word(INIT_NONCE_PTR)[0], inputs.account().nonce(), "The initial nonce should be stored at the INIT_NONCE_PTR" ); assert_eq!( - process.get_kernel_mem_word(TX_SCRIPT_ROOT_PTR), + exec_output.get_kernel_mem_word(TX_SCRIPT_ROOT_PTR), inputs.tx_args().tx_script().as_ref().unwrap().root(), "The transaction script root should be stored at the TX_SCRIPT_ROOT_PTR" ); } -fn block_data_memory_assertions(process: &Process, inputs: &TransactionContext) { +fn block_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &TransactionContext) { assert_eq!( - process.get_kernel_mem_word(BLOCK_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(BLOCK_COMMITMENT_PTR), inputs.tx_inputs().block_header().commitment(), "The block commitment should be stored at the BLOCK_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(PREV_BLOCK_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(PREV_BLOCK_COMMITMENT_PTR), inputs.tx_inputs().block_header().prev_block_commitment(), "The previous block commitment should be stored at the PARENT_BLOCK_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(CHAIN_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(CHAIN_COMMITMENT_PTR), inputs.tx_inputs().block_header().chain_commitment(), "The chain commitment should be stored at the CHAIN_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(ACCT_DB_ROOT_PTR), + exec_output.get_kernel_mem_word(ACCT_DB_ROOT_PTR), inputs.tx_inputs().block_header().account_root(), "The account db root should be stored at the ACCT_DB_ROOT_PRT" ); assert_eq!( - process.get_kernel_mem_word(NULLIFIER_DB_ROOT_PTR), + exec_output.get_kernel_mem_word(NULLIFIER_DB_ROOT_PTR), inputs.tx_inputs().block_header().nullifier_root(), "The nullifier db root should be stored at the NULLIFIER_DB_ROOT_PTR" ); assert_eq!( - process.get_kernel_mem_word(TX_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(TX_COMMITMENT_PTR), inputs.tx_inputs().block_header().tx_commitment(), "The TX commitment should be stored at the TX_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(TX_KERNEL_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(TX_KERNEL_COMMITMENT_PTR), inputs.tx_inputs().block_header().tx_kernel_commitment(), "The kernel commitment should be stored at the TX_KERNEL_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(PROOF_COMMITMENT_PTR), + 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" ); assert_eq!( - process.get_kernel_mem_word(BLOCK_METADATA_PTR)[BLOCK_NUMBER_IDX], + exec_output.get_kernel_mem_word(BLOCK_METADATA_PTR)[BLOCK_NUMBER_IDX], inputs.tx_inputs().block_header().block_num().into(), "The block number should be stored at BLOCK_METADATA_PTR[BLOCK_NUMBER_IDX]" ); assert_eq!( - process.get_kernel_mem_word(BLOCK_METADATA_PTR)[PROTOCOL_VERSION_IDX], + exec_output.get_kernel_mem_word(BLOCK_METADATA_PTR)[PROTOCOL_VERSION_IDX], inputs.tx_inputs().block_header().version().into(), "The protocol version should be stored at BLOCK_METADATA_PTR[PROTOCOL_VERSION_IDX]" ); assert_eq!( - process.get_kernel_mem_word(BLOCK_METADATA_PTR)[TIMESTAMP_IDX], + exec_output.get_kernel_mem_word(BLOCK_METADATA_PTR)[TIMESTAMP_IDX], inputs.tx_inputs().block_header().timestamp().into(), "The timestamp should be stored at BLOCK_METADATA_PTR[TIMESTAMP_IDX]" ); assert_eq!( - process.get_kernel_mem_word(FEE_PARAMETERS_PTR)[NATIVE_ASSET_ID_SUFFIX_IDX], + exec_output.get_kernel_mem_word(FEE_PARAMETERS_PTR)[NATIVE_ASSET_ID_SUFFIX_IDX], inputs.tx_inputs().block_header().fee_parameters().native_asset_id().suffix(), "The native asset ID suffix should be stored at FEE_PARAMETERS_PTR[NATIVE_ASSET_ID_SUFFIX_IDX]" ); assert_eq!( - process.get_kernel_mem_word(FEE_PARAMETERS_PTR)[NATIVE_ASSET_ID_PREFIX_IDX], + exec_output.get_kernel_mem_word(FEE_PARAMETERS_PTR)[NATIVE_ASSET_ID_PREFIX_IDX], inputs .tx_inputs() .block_header() @@ -310,7 +308,7 @@ fn block_data_memory_assertions(process: &Process, inputs: &TransactionContext) ); assert_eq!( - process.get_kernel_mem_word(FEE_PARAMETERS_PTR)[VERIFICATION_BASE_FEE_IDX], + exec_output.get_kernel_mem_word(FEE_PARAMETERS_PTR)[VERIFICATION_BASE_FEE_IDX], inputs .tx_inputs() .block_header() @@ -321,20 +319,23 @@ fn block_data_memory_assertions(process: &Process, inputs: &TransactionContext) ); assert_eq!( - process.get_kernel_mem_word(NOTE_ROOT_PTR), + exec_output.get_kernel_mem_word(NOTE_ROOT_PTR), inputs.tx_inputs().block_header().note_root(), "The note root should be stored at the NOTE_ROOT_PTR" ); } -fn partial_blockchain_memory_assertions(process: &Process, prepared_tx: &TransactionContext) { +fn partial_blockchain_memory_assertions( + exec_output: &ExecutionOutput, + prepared_tx: &TransactionContext, +) { // 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); assert_eq!( - process.get_kernel_mem_word(PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR)[0], + exec_output.get_kernel_mem_word(PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR)[0], Felt::new(partial_blockchain.chain_length().as_u64()), "The number of leaves should be stored at the PARTIAL_BLOCKCHAIN_NUM_LEAVES_PTR" ); @@ -346,41 +347,35 @@ fn partial_blockchain_memory_assertions(process: &Process, prepared_tx: &Transac ); let word_aligned_peak_idx = peak_idx * WORD_SIZE as u32; assert_eq!( - process.get_kernel_mem_word(PARTIAL_BLOCKCHAIN_PEAKS_PTR + word_aligned_peak_idx), + exec_output.get_kernel_mem_word(PARTIAL_BLOCKCHAIN_PEAKS_PTR + word_aligned_peak_idx), *peak ); } } -fn kernel_data_memory_assertions(process: &Process) { - let latest_version_procedures = TransactionKernel::PROCEDURES - .last() - .expect("kernel should have at least one version"); - +fn kernel_data_memory_assertions(exec_output: &ExecutionOutput) { // check that the number of kernel procedures stored in the memory is equal to the number of - // kernel procedures in the `TransactionKernel` array. - // - // By default we check procedures of the latest kernel version + // procedures in the `TransactionKernel::PROCEDURES` array assert_eq!( - process.get_kernel_mem_word(NUM_KERNEL_PROCEDURES_PTR)[0].as_int(), - latest_version_procedures.len() as u64, + exec_output.get_kernel_mem_word(NUM_KERNEL_PROCEDURES_PTR)[0].as_int(), + TransactionKernel::PROCEDURES.len() as u64, "Number of the kernel procedures should be stored at the NUM_KERNEL_PROCEDURES_PTR" ); // check that the hashes of the kernel procedures stored in the memory is equal to the hashes in - // `TransactionKernel`'s procedures array - for (i, &proc_hash) in latest_version_procedures.iter().enumerate() { + // `TransactionKernel::PROCEDURES` array + for (i, &proc_hash) in TransactionKernel::PROCEDURES.iter().enumerate() { assert_eq!( - process.get_kernel_mem_word(KERNEL_PROCEDURES_PTR + (i * WORD_SIZE) as u32), + exec_output.get_kernel_mem_word(KERNEL_PROCEDURES_PTR + (i * WORD_SIZE) as u32), proc_hash, "hash of kernel procedure at index `{i}` does not match the hash stored in memory" ); } } -fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext) { +fn account_data_memory_assertions(exec_output: &ExecutionOutput, inputs: &TransactionContext) { assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCT_ID_AND_NONCE_PTR), + exec_output.get_kernel_mem_word(NATIVE_ACCT_ID_AND_NONCE_PTR), Word::new([ inputs.account().id().suffix(), inputs.account().id().prefix().as_felt(), @@ -391,25 +386,25 @@ fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext ); assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCT_VAULT_ROOT_PTR), + exec_output.get_kernel_mem_word(NATIVE_ACCT_VAULT_ROOT_PTR), inputs.account().vault().root(), "The account vault root should be stored at NATIVE_ACCT_VAULT_ROOT_PTR" ); assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCT_STORAGE_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(NATIVE_ACCT_STORAGE_COMMITMENT_PTR), inputs.account().storage().commitment(), "The account storage commitment should be stored at NATIVE_ACCT_STORAGE_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(NATIVE_ACCT_CODE_COMMITMENT_PTR), + exec_output.get_kernel_mem_word(NATIVE_ACCT_CODE_COMMITMENT_PTR), inputs.account().code().commitment(), "account code commitment should be stored at NATIVE_ACCT_CODE_COMMITMENT_PTR" ); assert_eq!( - process.get_kernel_mem_word(NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR), + exec_output.get_kernel_mem_word(NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR), Word::from([u16::try_from(inputs.account().storage().slots().len()).unwrap(), 0, 0, 0]), "The number of initialised storage slots should be stored at NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR" ); @@ -422,7 +417,7 @@ fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext .enumerate() { assert_eq!( - process.get_kernel_mem_word( + exec_output.get_kernel_mem_word( NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR + (i * WORD_SIZE) as u32 ), Word::try_from(elements).unwrap(), @@ -431,7 +426,7 @@ fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext } assert_eq!( - process.get_kernel_mem_word(NATIVE_NUM_ACCT_PROCEDURES_PTR), + exec_output.get_kernel_mem_word(NATIVE_NUM_ACCT_PROCEDURES_PTR), Word::from([u16::try_from(inputs.account().code().procedures().len()).unwrap(), 0, 0, 0]), "The number of procedures should be stored at NATIVE_NUM_ACCT_PROCEDURES_PTR" ); @@ -444,7 +439,7 @@ fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext .enumerate() { assert_eq!( - process + 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" @@ -453,12 +448,12 @@ fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext } fn input_notes_memory_assertions( - process: &Process, + exec_output: &ExecutionOutput, inputs: &TransactionContext, note_args: &[Word], ) { assert_eq!( - process.get_kernel_mem_word(INPUT_NOTE_SECTION_PTR), + exec_output.get_kernel_mem_word(INPUT_NOTE_SECTION_PTR), Word::from([inputs.input_notes().num_notes(), 0, 0, 0]), "number of input notes should be stored at the INPUT_NOTES_OFFSET" ); @@ -467,7 +462,7 @@ fn input_notes_memory_assertions( let note = input_note.note(); assert_eq!( - process.get_kernel_mem_word( + exec_output.get_kernel_mem_word( INPUT_NOTE_NULLIFIER_SECTION_PTR + note_idx * WORD_SIZE as u32 ), note.nullifier().as_word(), @@ -475,55 +470,55 @@ fn input_notes_memory_assertions( ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_ID_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_ID_OFFSET), note.id().as_word(), "ID hash should be computed and stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_SERIAL_NUM_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_SERIAL_NUM_OFFSET), note.serial_num(), "note serial num should be stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_SCRIPT_ROOT_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_SCRIPT_ROOT_OFFSET), note.script().root(), "note script root should be stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_INPUTS_COMMITMENT_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_INPUTS_COMMITMENT_OFFSET), note.inputs().commitment(), "note input commitment should be stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_RECIPIENT_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_RECIPIENT_OFFSET), note.recipient().digest(), "note recipient should be stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_ASSETS_COMMITMENT_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_ASSETS_COMMITMENT_OFFSET), note.assets().commitment(), "note asset commitment should be stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_METADATA_OFFSET), + 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" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_ARGS_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_ARGS_OFFSET), note_args[note_idx as usize], "note args should be stored at the correct offset" ); assert_eq!( - read_note_element(process, note_idx, INPUT_NOTE_NUM_ASSETS_OFFSET), + exec_output.get_note_mem_word(note_idx, INPUT_NOTE_NUM_ASSETS_OFFSET), Word::from([::try_from(note.assets().num_assets()).unwrap(), 0, 0, 0]), "number of assets should be stored at the correct offset" ); @@ -531,8 +526,7 @@ fn input_notes_memory_assertions( for (asset, asset_idx) in note.assets().iter().cloned().zip(0_u32..) { let word: Word = asset.into(); assert_eq!( - read_note_element( - process, + exec_output.get_note_mem_word( note_idx, INPUT_NOTE_ASSETS_OFFSET + asset_idx * WORD_SIZE as u32 ), @@ -548,18 +542,18 @@ fn input_notes_memory_assertions( /// Tests that a simple account can be created in a complete transaction execution (not using /// [`TransactionContext::execute_code`]). -#[test] -fn create_simple_account() -> anyhow::Result<()> { - let (account, seed) = AccountBuilder::new([6; 32]) +#[tokio::test] +async fn create_simple_account() -> anyhow::Result<()> { + let account = AccountBuilder::new([6; 32]) .storage_mode(AccountStorageMode::Public) .with_auth_component(Auth::IncrNonce) .with_component(MockAccountComponent::with_empty_slots()) .build()?; let tx = TransactionContextBuilder::new(account) - .account_seed(Some(seed)) .build()? - .execute_blocking() + .execute() + .await .context("failed to execute account-creating transaction")?; assert_eq!(tx.account_delta().nonce_delta(), Felt::new(1)); @@ -575,18 +569,13 @@ fn create_simple_account() -> anyhow::Result<()> { /// Test helper which executes the prologue to check if the creation of the given `account` with its /// `seed` is valid in the context of the given `mock_chain`. -pub fn create_account_test( +pub async fn create_account_test( account: Account, - seed: Word, ) -> Result { - TransactionContextBuilder::new(account) - .account_seed(Some(seed)) - .build() - .unwrap() - .execute_blocking() + TransactionContextBuilder::new(account).build().unwrap().execute().await } -pub fn create_multiple_accounts_test(storage_mode: AccountStorageMode) -> anyhow::Result<()> { +pub async fn create_multiple_accounts_test(storage_mode: AccountStorageMode) -> anyhow::Result<()> { let mut accounts = Vec::new(); for account_type in [ @@ -595,7 +584,7 @@ pub fn create_multiple_accounts_test(storage_mode: AccountStorageMode) -> anyhow AccountType::FungibleFaucet, AccountType::NonFungibleFaucet, ] { - let (account, seed) = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .account_type(account_type) .storage_mode(storage_mode) .with_auth_component(Auth::IncrNonce) @@ -605,12 +594,12 @@ pub fn create_multiple_accounts_test(storage_mode: AccountStorageMode) -> anyhow .build() .context("account build failed")?; - accounts.push((account, seed)); + accounts.push(account); } - for (account, seed) in accounts { + for account in accounts { let account_type = account.account_type(); - create_account_test(account, seed).context(format!( + create_account_test(account).await.context(format!( "create_multiple_accounts_test test failed for account type {account_type}" ))?; } @@ -619,18 +608,18 @@ pub fn create_multiple_accounts_test(storage_mode: AccountStorageMode) -> anyhow } /// Tests that a valid account of each storage mode can be created successfully. -#[test] -pub fn create_accounts_with_all_storage_modes() -> anyhow::Result<()> { - create_multiple_accounts_test(AccountStorageMode::Private)?; +#[tokio::test] +pub async fn create_accounts_with_all_storage_modes() -> anyhow::Result<()> { + create_multiple_accounts_test(AccountStorageMode::Private).await?; - create_multiple_accounts_test(AccountStorageMode::Public)?; + create_multiple_accounts_test(AccountStorageMode::Public).await?; - create_multiple_accounts_test(AccountStorageMode::Network) + create_multiple_accounts_test(AccountStorageMode::Network).await } /// Takes an account with a placeholder ID and returns the same account but with its ID replaced /// with a newly generated one. -fn compute_valid_account_id(account: Account) -> (Account, Word) { +fn compute_valid_account_id(account: Account) -> Account { let init_seed: [u8; 32] = [5; 32]; let seed = AccountId::compute_account_seed( init_seed, @@ -651,34 +640,34 @@ fn compute_valid_account_id(account: Account) -> (Account, Word) { .unwrap(); // Overwrite old ID with generated ID. - let (_, vault, storage, code, nonce) = account.into_parts(); - let account = Account::from_parts(account_id, vault, storage, code, nonce); - - (account, seed) + let (_, vault, storage, code, _nonce, _seed) = account.into_parts(); + // Set nonce to zero so this is considered a new account. + Account::new(account_id, vault, storage, code, ZERO, Some(seed)).unwrap() } /// Tests that creating a fungible faucet account with a non-empty initial balance in its reserved /// slot fails. -#[test] -pub fn create_account_fungible_faucet_invalid_initial_balance() -> anyhow::Result<()> { +#[tokio::test] +pub async fn create_account_fungible_faucet_invalid_initial_balance() -> anyhow::Result<()> { let account = AccountBuilder::new([1; 32]) .account_type(AccountType::FungibleFaucet) .with_auth_component(NoopAuthComponent) .with_component(MockAccountComponent::with_empty_slots()) .build_existing() .expect("account should be valid"); - let (id, vault, mut storage, code, _nonce) = account.into_parts(); + let (id, vault, mut storage, code, _nonce, _seed) = account.into_parts(); // 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(); - // Set the nonce to zero so this is considered a new account. - let account = Account::from_parts(id, vault, storage, code, ZERO); - let (account, account_seed) = compute_valid_account_id(account); + // The compute account ID function will set the nonce to zero so this is considered a new + // account. + let account = Account::new(id, vault, storage, code, ONE, None)?; + let account = compute_valid_account_id(account); - let result = create_account_test(account, account_seed); + let result = create_account_test(account).await; assert_transaction_executor_error!( result, @@ -690,27 +679,29 @@ pub fn create_account_fungible_faucet_invalid_initial_balance() -> anyhow::Resul /// Tests that creating a non fungible faucet account with a non-empty storage map in its reserved /// slot fails. -#[test] -pub fn create_account_non_fungible_faucet_invalid_initial_reserved_slot() -> anyhow::Result<()> { +#[tokio::test] +pub async fn create_account_non_fungible_faucet_invalid_initial_reserved_slot() -> anyhow::Result<()> +{ // Create a storage map with a mock asset to make it non-empty. let asset = NonFungibleAsset::mock(&[1, 2, 3, 4]); let non_fungible_storage_map = - StorageMap::with_entries([(asset.vault_key(), asset.into())]).unwrap(); + StorageMap::with_entries([(asset.vault_key().into(), asset.into())]).unwrap(); let storage = AccountStorage::new(vec![StorageSlot::Map(non_fungible_storage_map)]).unwrap(); - let (account, _seed) = AccountBuilder::new([1; 32]) + let account = AccountBuilder::new([1; 32]) .account_type(AccountType::NonFungibleFaucet) .with_auth_component(NoopAuthComponent) .with_component(MockAccountComponent::with_empty_slots()) .build() .expect("account should be valid"); - let (id, vault, _storage, code, nonce) = account.into_parts(); + let (id, vault, _storage, code, _nonce, _seed) = account.into_parts(); - // Set the nonce to zero so this is considered a new account. - let account = Account::from_parts(id, vault, storage, code, nonce); - let (account, account_seed) = compute_valid_account_id(account); + // The compute account ID function will set the nonce to zero so this is considered a new + // account. + let account = Account::new(id, vault, storage, code, ONE, None)?; + let account = compute_valid_account_id(account); - let result = create_account_test(account, account_seed); + let result = create_account_test(account).await; assert_transaction_executor_error!( result, @@ -721,19 +712,19 @@ pub fn create_account_non_fungible_faucet_invalid_initial_reserved_slot() -> any } /// Tests that supplying an invalid seed causes account creation to fail. -#[test] -pub fn create_account_invalid_seed() -> anyhow::Result<()> { +#[tokio::test] +pub async fn create_account_invalid_seed() -> anyhow::Result<()> { let mut mock_chain = MockChain::new(); mock_chain.prove_next_block()?; - let (account, seed) = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) .account_type(AccountType::RegularAccountUpdatableCode) .with_auth_component(Auth::IncrNonce) .with_component(BasicWallet) .build()?; let tx_inputs = mock_chain - .get_transaction_inputs(account.clone(), Some(seed), &[], &[]) + .get_transaction_inputs(&account, &[], &[]) .expect("failed to get transaction inputs from mock chain"); // override the seed with an invalid seed to ensure the kernel fails @@ -742,7 +733,6 @@ pub fn create_account_invalid_seed() -> anyhow::Result<()> { AdviceInputs::default().with_map([(Word::from(account_seed_key), vec![ZERO; WORD_SIZE])]); let tx_context = TransactionContextBuilder::new(account) - .account_seed(Some(seed)) .tx_inputs(tx_inputs) .extend_advice_inputs(adv_inputs) .build()?; @@ -755,15 +745,15 @@ pub fn create_account_invalid_seed() -> anyhow::Result<()> { end "; - let result = tx_context.execute_code(code); + let result = tx_context.execute_code(code).await; assert_execution_error!(result, ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH); Ok(()) } -#[test] -fn test_get_blk_version() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_blk_version() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = " use.$kernel::memory @@ -778,15 +768,18 @@ fn test_get_blk_version() -> anyhow::Result<()> { end "; - let process = tx_context.execute_code(code)?; + let exec_output = tx_context.execute_code(code).await?; - assert_eq!(process.stack.get(0), tx_context.tx_inputs().block_header().version().into()); + assert_eq!( + exec_output.get_stack_element(0), + tx_context.tx_inputs().block_header().version().into() + ); Ok(()) } -#[test] -fn test_get_blk_timestamp() -> anyhow::Result<()> { +#[tokio::test] +async fn test_get_blk_timestamp() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = " use.$kernel::memory @@ -801,16 +794,12 @@ fn test_get_blk_timestamp() -> anyhow::Result<()> { end "; - let process = tx_context.execute_code(code)?; + let exec_output = tx_context.execute_code(code).await?; - assert_eq!(process.stack.get(0), tx_context.tx_inputs().block_header().timestamp().into()); + assert_eq!( + exec_output.get_stack_element(0), + tx_context.tx_inputs().block_header().timestamp().into() + ); Ok(()) } - -// HELPER FUNCTIONS -// ================================================================================================ - -fn read_note_element(process: &Process, note_idx: u32, offset: MemoryOffset) -> Word { - process.get_kernel_mem_word(input_note_data_ptr(note_idx) + offset) -} 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 6451366021..909492c73c 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -1,42 +1,27 @@ -use alloc::string::String; use alloc::sync::Arc; -use alloc::vec::Vec; 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::errors::tx_kernel_errors::{ - 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::account_component::IncrNonceAuthComponent; 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::transaction::{TransactionEvent, TransactionKernel}; +use miden_lib::transaction::TransactionKernel; use miden_lib::utils::ScriptBuilder; use miden_objects::account::{ Account, AccountBuilder, AccountCode, AccountComponent, - AccountId, AccountStorage, AccountStorageMode, AccountType, StorageSlot, }; use miden_objects::assembly::DefaultSourceManager; -use miden_objects::assembly::diagnostics::{IntoDiagnostic, NamedSource, miette}; +use miden_objects::assembly::diagnostics::NamedSource; use miden_objects::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; use miden_objects::block::BlockNumber; use miden_objects::note::{ @@ -53,20 +38,13 @@ use miden_objects::note::{ NoteType, }; use miden_objects::testing::account_id::{ - ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, - ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_SENDER, -}; -use miden_objects::testing::constants::{ - FUNGIBLE_ASSET_AMOUNT, - NON_FUNGIBLE_ASSET_DATA, - NON_FUNGIBLE_ASSET_DATA_2, }; +use miden_objects::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; use miden_objects::testing::note::DEFAULT_NOTE_CODE; use miden_objects::transaction::{ InputNotes, @@ -75,63 +53,14 @@ use miden_objects::transaction::{ TransactionArgs, TransactionSummary, }; -use miden_objects::{FieldElement, Hasher, Word}; +use miden_objects::{Felt, FieldElement, Hasher, ONE, Word}; use miden_processor::crypto::RpoRandomCoin; -use miden_processor::fast::FastProcessor; -use miden_processor::{AdviceInputs, StackInputs}; use miden_tx::auth::UnreachableAuth; -use miden_tx::{ - AccountProcedureIndexMap, - ScriptMastForestStore, - TransactionExecutor, - TransactionExecutorError, - TransactionExecutorHost, - TransactionMastStore, -}; - -use super::{Felt, ONE, ZERO}; -use crate::kernel_tests::tx::ProcessMemoryExt; -use crate::utils::{create_p2any_note, create_spawn_note}; -use crate::{Auth, MockChain, TransactionContextBuilder, assert_execution_error}; - -/// Tests that executing a transaction with a foreign account whose inputs are stale fails. -#[test] -fn transaction_with_stale_foreign_account_inputs_fails() -> anyhow::Result<()> { - // Create a chain with an account - let mut builder = MockChain::builder(); - let native_account = builder.add_existing_wallet(Auth::IncrNonce)?; - let foreign_account = builder.add_existing_wallet(Auth::IncrNonce)?; - let new_account = builder.create_new_wallet(Auth::IncrNonce)?; - - let mut mock_chain = builder.build()?; +use miden_tx::{TransactionExecutor, TransactionExecutorError}; - // Retrieve inputs which will become stale - let inputs = mock_chain - .get_foreign_account_inputs(foreign_account.id()) - .expect("failed to get foreign account inputs"); - - // Create a new unrelated account to modify the account tree. - let tx = mock_chain - .build_tx_context(new_account, &[], &[])? - .build()? - .execute_blocking()?; - mock_chain.add_pending_executed_transaction(&tx)?; - mock_chain.prove_next_block()?; - - // Attempt to execute with older foreign account inputs. The AccountWitness in the foreign - // account's inputs have become stale and so this should fail. - let transaction = mock_chain - .build_tx_context(native_account.id(), &[], &[])? - .foreign_accounts(vec![inputs]) - .build()? - .execute_blocking(); - - assert_matches::assert_matches!( - transaction, - Err(TransactionExecutorError::ForeignAccountNotAnchoredInReference(_)) - ); - Ok(()) -} +use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::utils::{create_public_p2any_note, create_spawn_note}; +use crate::{Auth, MockChain, TransactionContextBuilder}; /// Tests that consuming a note created in a block that is newer than the reference block of the /// transaction fails. @@ -139,25 +68,31 @@ fn transaction_with_stale_foreign_account_inputs_fails() -> anyhow::Result<()> { async fn consuming_note_created_in_future_block_fails() -> anyhow::Result<()> { // Create a chain with an account let mut builder = MockChain::builder(); - let account = builder.add_existing_wallet(Auth::BasicAuth)?; + let asset = FungibleAsset::mock(400); + let account1 = builder.add_existing_wallet_with_assets(Auth::BasicAuth, [asset])?; + let account2 = builder.add_existing_wallet_with_assets(Auth::BasicAuth, [asset])?; + let output_note = create_public_p2any_note(account1.id(), [asset]); + let spawn_note = builder.add_spawn_note([&output_note])?; let mut mock_chain = builder.build()?; mock_chain.prove_until_block(10u32)?; - // Create a note that will be contained in block 11. - let note = mock_chain - .add_pending_p2id_note( - account.id(), - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), - &[], - NoteType::Private, - ) - .unwrap(); + // Consume the spawn note which creates a note for account 2 to consume. It will be contained in + // block 11. We use account 1 for this, so that account 2 remains unchanged and is still valid + // against reference block 1 which we'll use for the later transaction. + let tx = mock_chain + .build_tx_context(account1.id(), &[spawn_note.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .build()? + .execute() + .await?; + + // Add the transaction to the mock chain's mempool so it will be included in the next block. + mock_chain.add_pending_executed_transaction(&tx)?; // Create block 11. mock_chain.prove_next_block()?; - // Get as input note, and assert that the note was created after block 1 (which we'll - // use as reference) - let input_note = mock_chain.get_public_note(¬e.id()).expect("note not found"); + // Get the input note and assert that the note was created after block 11. + let input_note = mock_chain.get_public_note(&output_note.id()).expect("note not found"); assert_eq!(input_note.location().unwrap().block_num().as_u32(), 11); mock_chain.prove_next_block()?; @@ -165,7 +100,7 @@ async fn consuming_note_created_in_future_block_fails() -> anyhow::Result<()> { // Attempt to execute a transaction against reference block 1 with the note created in block 11 // - which should fail. - let tx_context = mock_chain.build_tx_context(account.id(), &[], &[])?.build()?; + let tx_context = mock_chain.build_tx_context(account2.id(), &[], &[])?.build()?; let tx_executor = TransactionExecutor::<'_, '_, _, UnreachableAuth>::new(&tx_context) .with_source_manager(tx_context.source_manager()); @@ -173,7 +108,7 @@ async fn consuming_note_created_in_future_block_fails() -> anyhow::Result<()> { // Try to execute with block_ref==1 let error = tx_executor .execute_transaction( - account.id(), + account2.id(), BlockNumber::from(1), InputNotes::new(vec![input_note]).unwrap(), TransactionArgs::default(), @@ -188,668 +123,11 @@ async fn consuming_note_created_in_future_block_fails() -> anyhow::Result<()> { Ok(()) } -#[test] -fn test_create_note() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - 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 code = format!( - " - use.miden::tx - - use.$kernel::prologue - - begin - exec.prologue::prepare_transaction - - push.{recipient} - push.{note_execution_hint} - push.{PUBLIC_NOTE} - push.{aux} - push.{tag} - - call.tx::create_note - - # truncate the stack - swapdw dropw dropw - end - ", - recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, - note_execution_hint = Felt::from(NoteExecutionHint::after_block(23.into()).unwrap()), - tag = tag, - ); - - let process = &tx_context.execute_code(&code)?; - - assert_eq!( - process.get_kernel_mem_word(NUM_OUTPUT_NOTES_PTR), - Word::from([1, 0, 0, 0u32]), - "number of output notes must increment by 1", - ); - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_RECIPIENT_OFFSET), - recipient, - "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(); - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_OFFSET), - expected_note_metadata, - "metadata must be stored at the correct memory location", - ); - - assert_eq!( - process.stack.get(0), - ZERO, - "top item on the stack is the index of the output note" - ); - Ok(()) -} - -#[test] -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(); - - // Test invalid tag - assert!(tx_context.execute_code(¬e_creation_script(invalid_tag)).is_err()); - - // Test valid tag - assert!(tx_context.execute_code(¬e_creation_script(valid_tag)).is_ok()); - - Ok(()) -} - -fn note_creation_script(tag: Felt) -> String { - format!( - " - use.miden::tx - use.$kernel::prologue - - begin - exec.prologue::prepare_transaction - - push.{recipient} - push.{execution_hint_always} - push.{PUBLIC_NOTE} - push.{aux} - push.{tag} - - call.tx::create_note - - # 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 = Felt::ZERO, - ) -} - -#[test] -fn test_create_note_too_many_notes() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - - let code = format!( - " - use.miden::tx - use.$kernel::constants - use.$kernel::memory - use.$kernel::prologue - - begin - exec.constants::get_max_num_output_notes - exec.memory::set_num_output_notes - exec.prologue::prepare_transaction - - push.{recipient} - push.{execution_hint_always} - push.{PUBLIC_NOTE} - push.{aux} - push.{tag} - - call.tx::create_note - end - ", - tag = NoteTag::for_local_use_case(1234, 5678).unwrap(), - recipient = Word::from([0, 1, 2, 3u32]), - execution_hint_always = Felt::from(NoteExecutionHint::always()), - PUBLIC_NOTE = NoteType::Public as u8, - aux = Felt::ZERO, - ); - - let process = tx_context.execute_code(&code); - - assert_execution_error!(process, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT); - Ok(()) -} - -#[test] -fn test_get_output_notes_commitment() -> anyhow::Result<()> { - let tx_context = { - let account = - Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - - let output_note_1 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into()?, &[FungibleAsset::mock(100)]); - - let input_note_1 = - create_p2any_note(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, &[FungibleAsset::mock(100)]); - - let input_note_2 = - create_p2any_note(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, &[FungibleAsset::mock(200)]); - - TransactionContextBuilder::new(account) - .extend_input_notes(vec![input_note_1, input_note_2]) - .extend_expected_output_notes(vec![OutputNote::Full(output_note_1)]) - .build()? - }; - - // extract input note data - let input_note_1 = tx_context.tx_inputs().input_notes().get_note(0).note(); - let input_asset_1 = **input_note_1 - .assets() - .iter() - .take(1) - .collect::>() - .first() - .context("getting first expected input asset")?; - let input_note_2 = tx_context.tx_inputs().input_notes().get_note(1).note(); - let input_asset_2 = **input_note_2 - .assets() - .iter() - .take(1) - .collect::>() - .first() - .context("getting second expected input asset")?; - - // Choose random accounts as the target for the note tag. - let network_account = AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET)?; - let local_account = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET)?; - - // create output note 1 - let output_serial_no_1 = Word::from([8u32; 4]); - let output_tag_1 = NoteTag::from_account_id(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 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 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 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); - - // compute expected output notes commitment - let expected_output_notes_commitment = OutputNotes::new(vec![ - OutputNote::Full(output_note_1.clone()), - OutputNote::Full(output_note_2.clone()), - ])? - .commitment(); - - let code = format!( - " - use.std::sys - - use.miden::tx - - use.$kernel::prologue - use.mock::account - - 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.tx::create_note - # => [note_idx] - - push.{asset_1} - call.tx::add_asset_to_note - # => [ASSET, note_idx] - - dropw drop - # => [] - - # create output note 2 - push.{recipient_2} - push.{NOTE_EXECUTION_HINT_2} - push.{PUBLIC_NOTE} - push.{aux_2} - push.{tag_2} - call.tx::create_note - # => [note_idx] - - push.{asset_2} - call.tx::add_asset_to_note - # => [ASSET, note_idx] - - dropw drop - # => [] - - # compute the output notes commitment - exec.tx::get_output_notes_commitment - # => [OUTPUT_NOTES_COMMITMENT] - - # truncate the stack - exec.sys::truncate_stack - # => [OUTPUT_NOTES_COMMITMENT] - 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() - ), - ); - - let process = &tx_context.execute_code(&code)?; - - assert_eq!( - process.get_kernel_mem_word(NUM_OUTPUT_NOTES_PTR), - Word::from([2u32, 0, 0, 0]), - "The test creates two notes", - ); - assert_eq!( - NoteMetadata::try_from( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_OFFSET) - ) - .unwrap(), - *output_note_1.metadata(), - "Validate the output note 1 metadata", - ); - assert_eq!( - NoteMetadata::try_from(process.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", - ); - - assert_eq!(process.stack.get_word(0), expected_output_notes_commitment); - Ok(()) -} - -#[test] -fn test_create_note_and_add_asset() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - - 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 asset = Word::from(FungibleAsset::new(faucet_id, 10)?); - - let code = format!( - " - use.miden::tx - - use.$kernel::prologue - use.mock::account - - begin - exec.prologue::prepare_transaction - - push.{recipient} - push.{NOTE_EXECUTION_HINT} - push.{PUBLIC_NOTE} - push.{aux} - push.{tag} - - call.tx::create_note - # => [note_idx] - - push.{asset} - call.tx::add_asset_to_note - # => [ASSET, note_idx] - - dropw - # => [note_idx] - - # truncate the stack - swapdw dropw dropw - end - ", - recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, - NOTE_EXECUTION_HINT = Felt::from(NoteExecutionHint::always()), - tag = tag, - asset = asset, - ); - - let process = &tx_context.execute_code(&code)?; - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET), - asset, - "asset must be stored at the correct memory location", - ); - - assert_eq!( - process.stack.get(0), - ZERO, - "top item on the stack is the index to the output note" - ); - Ok(()) -} - -#[test] -fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { - let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; - - let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; - 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 asset = Word::from(FungibleAsset::new(faucet, 10)?); - let asset_2 = Word::from(FungibleAsset::new(faucet_2, 20)?); - let asset_3 = Word::from(FungibleAsset::new(faucet_2, 30)?); - let asset_2_and_3 = Word::from(FungibleAsset::new(faucet_2, 50)?); - - let non_fungible_asset = NonFungibleAsset::mock(&NON_FUNGIBLE_ASSET_DATA_2); - let non_fungible_asset_encoded = Word::from(non_fungible_asset); - - let code = format!( - " - use.miden::tx - - use.$kernel::prologue - use.mock::account - - begin - exec.prologue::prepare_transaction - - push.{recipient} - push.{PUBLIC_NOTE} - push.{aux} - push.{tag} - - call.tx::create_note - # => [note_idx] - - push.{asset} - call.tx::add_asset_to_note dropw - # => [note_idx] - - push.{asset_2} - call.tx::add_asset_to_note dropw - # => [note_idx] - - push.{asset_3} - call.tx::add_asset_to_note dropw - # => [note_idx] - - push.{nft} - call.tx::add_asset_to_note dropw - # => [note_idx] - - # truncate the stack - swapdw dropw drop drop drop - end - ", - recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, - tag = tag, - asset = asset, - asset_2 = asset_2, - asset_3 = asset_3, - nft = non_fungible_asset_encoded, - ); - - let process = &tx_context.execute_code(&code)?; - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET), - asset, - "asset must be stored at the correct memory location", - ); - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + 4), - asset_2_and_3, - "asset_2 and asset_3 must be stored at the same correct memory location", - ); - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + 8), - non_fungible_asset_encoded, - "non_fungible_asset must be stored at the correct memory location", - ); - - assert_eq!( - process.stack.get(0), - ZERO, - "top item on the stack is the index to the output note" - ); - Ok(()) -} - -#[test] -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 non_fungible_asset = NonFungibleAsset::mock(&[1, 2, 3]); - let encoded = Word::from(non_fungible_asset); - - let code = format!( - " - use.$kernel::prologue - use.mock::account - use.miden::tx - - begin - exec.prologue::prepare_transaction - # => [] - - padw padw - push.{recipient} - push.{execution_hint_always} - push.{PUBLIC_NOTE} - push.{aux} - push.{tag} - - call.tx::create_note - # => [note_idx, pad(15)] - - push.{nft} - call.tx::add_asset_to_note - # => [NFT, note_idx, pad(15)] - dropw - - push.{nft} - call.tx::add_asset_to_note - # => [NFT, note_idx, pad(15)] - - repeat.5 dropw end - end - ", - recipient = recipient, - PUBLIC_NOTE = NoteType::Public as u8, - execution_hint_always = Felt::from(NoteExecutionHint::always()), - aux = Felt::new(0), - tag = tag, - nft = encoded, - ); - - let process = tx_context.execute_code(&code); - - assert_execution_error!(process, ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS); - Ok(()) -} - -/// Tests that creating a note with a fungible asset with amount zero works. -#[test] -fn creating_note_with_fungible_asset_amount_zero_works() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - let account = builder.add_existing_mock_account(Auth::IncrNonce)?; - let output_note = builder.add_p2id_note( - account.id(), - account.id(), - &[FungibleAsset::mock(0)], - NoteType::Private, - )?; - let input_note = builder.add_spawn_note(account.id(), [&output_note])?; - let chain = builder.build()?; - - chain - .build_tx_context(account, &[input_note.id()], &[])? - .build()? - .execute_blocking()?; - - Ok(()) -} - -#[test] -fn test_build_recipient_hash() -> anyhow::Result<()> { - let tx_context = { - let account = - Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - - let input_note_1 = - create_p2any_note(ACCOUNT_ID_SENDER.try_into().unwrap(), &[FungibleAsset::mock(100)]); - TransactionContextBuilder::new(account) - .extend_input_notes(vec![input_note_1]) - .build()? - }; - let input_note_1 = tx_context.tx_inputs().input_notes().get_note(0).note(); - - // 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 single_input = 2; - let inputs = NoteInputs::new(vec![Felt::new(single_input)]).unwrap(); - let input_commitment = inputs.commitment(); - - let recipient = NoteRecipient::new(output_serial_no, input_note_1.script().clone(), inputs); - let code = format!( - " - use.miden::tx - use.$kernel::prologue - - proc.build_recipient_hash - exec.tx::build_recipient_hash - end - - 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)] - - call.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)] - - call.tx::create_note - # => [note_idx, pad(19)] - - # clean the stack - dropw dropw dropw dropw dropw - 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 process = &tx_context.execute_code(&code)?; - - assert_eq!( - process.get_kernel_mem_word(NUM_OUTPUT_NOTES_PTR), - Word::from([1, 0, 0, 0u32]), - "number of output notes must increment by 1", - ); - - let recipient_digest = recipient.clone().digest(); - - assert_eq!( - process.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_RECIPIENT_OFFSET), - recipient_digest, - "recipient hash not correct", - ); - Ok(()) -} - // BLOCK TESTS // ================================================================================================ -#[test] -fn test_block_procedures() -> anyhow::Result<()> { +#[tokio::test] +async fn test_block_procedures() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let code = " @@ -870,128 +148,30 @@ fn test_block_procedures() -> anyhow::Result<()> { end "; - let process = &tx_context.execute_code(code)?; + let exec_output = &tx_context.execute_code(code).await?; assert_eq!( - process.stack.get_word(0), + exec_output.get_stack_word_be(0), tx_context.tx_inputs().block_header().commitment(), "top word on the stack should be equal to the block header commitment" ); assert_eq!( - process.stack.get(4).as_int(), + exec_output.get_stack_element(4).as_int(), tx_context.tx_inputs().block_header().timestamp() as u64, "fifth element on the stack should be equal to the timestamp of the last block creation" ); assert_eq!( - process.stack.get(5).as_int(), + exec_output.get_stack_element(5).as_int(), tx_context.tx_inputs().block_header().block_num().as_u64(), "sixth element on the stack should be equal to the block number" ); Ok(()) } -/// Tests that the transaction witness retrieved from an executed transaction contains all necessary -/// advice input to execute the transaction again. #[tokio::test] -async fn advice_inputs_from_transaction_witness_are_sufficient_to_reexecute_transaction() --> miette::Result<()> { - // Creates a mockchain with an account and a note that it can consume - let tx_context = { - let mut builder = MockChain::builder(); - let account = builder - .add_existing_wallet(Auth::BasicAuth) - .map_err(|err| miette::miette!(err))?; - let p2id_note = builder - .add_p2id_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - account.id(), - &[FungibleAsset::mock(100)], - NoteType::Public, - ) - .map_err(|err| miette::miette!(err))?; - let mock_chain = builder.build().map_err(|err| miette::miette!(err))?; - - mock_chain - .build_tx_context(account.id(), &[], &[p2id_note]) - .unwrap() - .build() - .unwrap() - }; - - let executed_transaction = tx_context.execute().await.into_diagnostic()?; - - let tx_inputs = executed_transaction.tx_inputs(); - let tx_args = executed_transaction.tx_args(); - - let scripts_mast_store = ScriptMastForestStore::new( - tx_args.tx_script(), - tx_inputs.input_notes().iter().map(|n| n.note().script()), - ); - - // use the witness to execute the transaction again - let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs( - tx_inputs, - tx_args, - Some(executed_transaction.advice_witness().clone()), - ) - .into_diagnostic()?; - - // load account/note/tx_script MAST to the mast_store - let mast_store = Arc::new(TransactionMastStore::new()); - mast_store.load_account_code(tx_inputs.account().code()); - - let mut host = { - let acct_procedure_index_map = - AccountProcedureIndexMap::from_transaction_params(tx_inputs, tx_args, &advice_inputs) - .unwrap(); - - TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new( - &tx_inputs.account().into(), - tx_inputs.input_notes().clone(), - mast_store.as_ref(), - scripts_mast_store, - acct_procedure_index_map, - None, - tx_inputs.block_header().fee_parameters(), - Arc::new(DefaultSourceManager::default()), - ) - }; - let advice_inputs = advice_inputs.into_advice_inputs(); - // This reverses the stack inputs (even though it doesn't look like it does) because the - // fast processor expects the reverse order. - let stack_inputs = StackInputs::new(stack_inputs.iter().copied().collect()).unwrap(); - - let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); - let (stack_outputs, advice_provider) = processor - .execute(&TransactionKernel::main(), &mut host) - .await - .map_err(TransactionExecutorError::TransactionProgramExecutionFailed) - .into_diagnostic()?; - - // Extract advice map from advice provider. - let advice_inputs = AdviceInputs { - map: advice_provider.into_parts().1, - ..Default::default() - }; - - let (_, output_notes, _signatures, _tx_progress) = host.into_parts(); - let tx_outputs = - TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes) - .unwrap(); - - assert_eq!( - executed_transaction.final_account().commitment(), - tx_outputs.account.commitment() - ); - assert_eq!(executed_transaction.output_notes(), &tx_outputs.output_notes); - - Ok(()) -} - -#[test] -fn executed_transaction_output_notes() -> anyhow::Result<()> { +async fn executed_transaction_output_notes() -> anyhow::Result<()> { let executor_account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, IncrNonceAuthComponent); let account_id = executor_account.id(); @@ -1064,8 +244,7 @@ fn executed_transaction_output_notes() -> anyhow::Result<()> { let tx_script_src = format!( "\ use.miden::contracts::wallets::basic->wallet - use.miden::tx - use.mock::account + use.miden::output_note # Inputs: [tag, aux, note_type, execution_hint, RECIPIENT] # Outputs: [note_idx] @@ -1075,7 +254,7 @@ fn executed_transaction_output_notes() -> anyhow::Result<()> { padw padw swapdw # => [tag, aux, execution_hint, note_type, RECIPIENT, pad(8)] - call.tx::create_note + call.output_note::create # => [note_idx, pad(15)] # remove excess PADs from the stack @@ -1177,7 +356,7 @@ fn executed_transaction_output_notes() -> anyhow::Result<()> { ]) .build()?; - let executed_transaction = tx_context.execute_blocking()?; + let executed_transaction = tx_context.execute().await?; // output notes // -------------------------------------------------------------------------------------------- @@ -1230,20 +409,20 @@ fn executed_transaction_output_notes() -> anyhow::Result<()> { /// Tests that a transaction consuming and creating one note can emit an abort event in its auth /// component to result in a [`TransactionExecutorError::Unauthorized`] error. -#[test] -fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { - let source_code = format!( - " +#[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") #! Inputs: [AUTH_ARGS, pad(12)] #! Outputs: [pad(16)] - export.auth__abort_tx + export.auth_abort_tx dropw # => [pad(16)] push.0.0 exec.tx::get_block_number - exec.::miden::account::incr_nonce + exec.::miden::native_account::incr_nonce # => [[final_nonce, block_num, 0, 0], pad(16)] # => [SALT, pad(16)] @@ -1255,11 +434,9 @@ fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { exec.auth::hash_tx_summary # => [MESSAGE, pad(16)] - emit.{abort_event} + emit.AUTH_UNAUTHORIZED_EVENT end - ", - abort_event = TransactionEvent::Unauthorized as u32 - ); + "#; let auth_component = AccountComponent::compile(source_code, TransactionKernel::assembler(), vec![]) @@ -1283,12 +460,11 @@ fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { Felt::ZERO, &mut rng, )?; - let input_note = create_spawn_note(account.id(), vec![&output_note])?; + let input_note = create_spawn_note(vec![&output_note])?; - let mut mock_chain = MockChain::new(); - - mock_chain.add_pending_note(OutputNote::Full(input_note.clone())); - mock_chain.prove_next_block()?; + let mut builder = MockChain::builder(); + builder.add_output_note(OutputNote::Full(input_note.clone())); + let mock_chain = builder.build()?; let tx_context = mock_chain.build_tx_context(account, &[input_note.id()], &[])?.build()?; let ref_block_num = tx_context.tx_inputs().block_header().block_num().as_u32(); @@ -1296,7 +472,7 @@ fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { let input_notes = tx_context.input_notes().clone(); let output_notes = OutputNotes::new(vec![OutputNote::Partial(output_note.into())])?; - let error = tx_context.execute_blocking().unwrap_err(); + let error = tx_context.execute().await.unwrap_err(); assert_matches!(error, TransactionExecutorError::Unauthorized(tx_summary) => { assert!(tx_summary.account_delta().vault().is_empty()); @@ -1314,8 +490,8 @@ fn user_code_can_abort_transaction_with_summary() -> anyhow::Result<()> { /// Tests that a transaction consuming and creating one note with basic authentication correctly /// signs the transaction summary. -#[test] -fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> { +#[tokio::test] +async fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let account = builder.add_existing_mock_account(Auth::BasicAuth)?; let mut rng = RpoRandomCoin::new(Word::empty()); @@ -1327,13 +503,14 @@ fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> { Felt::ZERO, &mut rng, )?; - let spawn_note = builder.add_spawn_note(account.id(), [&p2id_note])?; + let spawn_note = builder.add_spawn_note([&p2id_note])?; let chain = builder.build()?; let tx = chain .build_tx_context(account.id(), &[spawn_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; let summary = TransactionSummary::new( tx.account_delta().clone(), @@ -1412,7 +589,7 @@ async fn execute_tx_view_script() -> anyhow::Result<()> { .with_source_manager(source_manager); let stack_outputs = executor - .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs, Vec::default()) + .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs) .await?; assert_eq!(stack_outputs[..3], [Felt::new(7), Felt::new(2), ONE]); @@ -1424,8 +601,8 @@ async fn execute_tx_view_script() -> anyhow::Result<()> { // ================================================================================================ /// Tests transaction script inputs. -#[test] -fn test_tx_script_inputs() -> anyhow::Result<()> { +#[tokio::test] +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!( @@ -1434,17 +611,15 @@ fn test_tx_script_inputs() -> anyhow::Result<()> { begin # push the tx script input key onto the stack - push.{key} + push.{tx_script_input_key} # load the tx script input value from the map and read it onto the stack adv.push_mapval adv_loadw # assert that the value is correct - push.{value} assert_eqw + push.{tx_script_input_value} assert_eqw end - ", - key = tx_script_input_key, - value = tx_script_input_value + " ); let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_src)?; @@ -1454,14 +629,14 @@ fn test_tx_script_inputs() -> anyhow::Result<()> { .extend_advice_map([(tx_script_input_key, tx_script_input_value.to_vec())]) .build()?; - tx_context.execute_blocking().context("failed to execute transaction")?; + tx_context.execute().await.context("failed to execute transaction")?; Ok(()) } /// Tests transaction script arguments. -#[test] -fn test_tx_script_args() -> anyhow::Result<()> { +#[tokio::test] +async fn test_tx_script_args() -> anyhow::Result<()> { let tx_script_args = Word::from([1, 2, 3, 4u32]); let tx_script_src = r#" @@ -1500,15 +675,15 @@ fn test_tx_script_args() -> anyhow::Result<()> { .tx_script_args(tx_script_args) .build()?; - tx_context.execute_blocking()?; + tx_context.execute().await?; Ok(()) } // Tests that advice map from the account code and transaction script gets correctly passed as // part of the transaction advice inputs -#[test] -fn inputs_created_correctly() -> anyhow::Result<()> { +#[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] @@ -1564,7 +739,7 @@ fn inputs_created_correctly() -> anyhow::Result<()> { .is_some() ); - let account = Account::from_parts( + let account = Account::new_existing( ACCOUNT_ID_PRIVATE_SENDER.try_into()?, AssetVault::mock(), AccountStorage::mock(), @@ -1572,7 +747,7 @@ fn inputs_created_correctly() -> anyhow::Result<()> { Felt::new(1u64), ); let tx_context = crate::TransactionContextBuilder::new(account).tx_script(tx_script).build()?; - _ = tx_context.execute_blocking()?; + _ = tx_context.execute().await?; Ok(()) } diff --git a/crates/miden-testing/src/lib.rs b/crates/miden-testing/src/lib.rs index 914e374235..fbbd4402b8 100644 --- a/crates/miden-testing/src/lib.rs +++ b/crates/miden-testing/src/lib.rs @@ -13,7 +13,6 @@ pub use mock_chain::{ MockChain, MockChainBuilder, MockChainNote, - ProvenTransactionExt, TxContextInput, }; @@ -22,7 +21,6 @@ pub use tx_context::{TransactionContext, TransactionContextBuilder}; pub mod executor; -pub use mock_host::MockHost; mod mock_host; pub mod utils; diff --git a/crates/miden-testing/src/mock_chain/auth.rs b/crates/miden-testing/src/mock_chain/auth.rs index 5077b730a3..29d8270b3c 100644 --- a/crates/miden-testing/src/mock_chain/auth.rs +++ b/crates/miden-testing/src/mock_chain/auth.rs @@ -7,11 +7,12 @@ use miden_lib::account::auth::{ AuthRpoFalcon512Acl, AuthRpoFalcon512AclConfig, AuthRpoFalcon512Multisig, + AuthRpoFalcon512MultisigConfig, }; use miden_lib::testing::account_component::{ConditionalAuthComponent, IncrNonceAuthComponent}; use miden_objects::Word; -use miden_objects::account::{AccountComponent, AuthSecretKey}; -use miden_objects::crypto::dsa::rpo_falcon512::{PublicKey, SecretKey}; +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; @@ -20,14 +21,18 @@ use rand_chacha::ChaCha20Rng; /// Specifies which authentication mechanism is desired for accounts #[derive(Debug, Clone)] pub enum Auth { - /// Creates a [SecretKey] for the account and creates a [BasicAuthenticator] used to + /// Creates a secret key for the account and creates a [BasicAuthenticator] used to /// authenticate the account with [AuthRpoFalcon512]. BasicAuth, /// Multisig - Multisig { threshold: u32, approvers: Vec }, + Multisig { + threshold: u32, + approvers: Vec, + proc_threshold_map: Vec<(Word, u32)>, + }, - /// Creates a [SecretKey] for the account, and creates a [BasicAuthenticator] used to + /// Creates a secret key for the account, and creates a [BasicAuthenticator] used to /// authenticate the account with [AuthRpoFalcon512Acl]. Authentication will only be /// triggered if any of the procedures specified in the list are called during execution. Acl { @@ -54,25 +59,26 @@ impl Auth { /// Converts `self` into its corresponding authentication [`AccountComponent`] and an optional /// [`BasicAuthenticator`]. The component is always returned, but the authenticator is only /// `Some` when [`Auth::BasicAuth`] is passed." - pub fn build_component(&self) -> (AccountComponent, Option>) { + pub fn build_component(&self) -> (AccountComponent, Option) { match self { Auth::BasicAuth => { let mut rng = ChaCha20Rng::from_seed(Default::default()); - let sec_key = SecretKey::with_rng(&mut rng); - let pub_key = sec_key.public_key(); + let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let pub_key = sec_key.public_key().to_commitment(); let component = AuthRpoFalcon512::new(pub_key).into(); - let authenticator = BasicAuthenticator::::new_with_rng( - &[(pub_key.into(), AuthSecretKey::RpoFalcon512(sec_key))], - rng, - ); + let authenticator = BasicAuthenticator::new(&[sec_key]); (component, Some(authenticator)) }, - Auth::Multisig { threshold, approvers } => { - let pub_keys: Vec<_> = approvers.iter().map(|word| PublicKey::new(*word)).collect(); + Auth::Multisig { threshold, approvers, proc_threshold_map } => { + let pub_keys: Vec<_> = + approvers.iter().map(|word| PublicKeyCommitment::from(*word)).collect(); - let component = AuthRpoFalcon512Multisig::new(*threshold, pub_keys) + let config = AuthRpoFalcon512MultisigConfig::new(pub_keys, *threshold) + .and_then(|cfg| cfg.with_proc_thresholds(proc_threshold_map.clone())) + .expect("invalid multisig config"); + let component = AuthRpoFalcon512Multisig::new(config) .expect("multisig component creation failed") .into(); @@ -84,8 +90,8 @@ impl Auth { allow_unauthorized_input_notes, } => { let mut rng = ChaCha20Rng::from_seed(Default::default()); - let sec_key = SecretKey::with_rng(&mut rng); - let pub_key = sec_key.public_key(); + let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let pub_key = sec_key.public_key().to_commitment(); let component = AuthRpoFalcon512Acl::new( pub_key, @@ -96,10 +102,7 @@ impl Auth { ) .expect("component creation failed") .into(); - let authenticator = BasicAuthenticator::::new_with_rng( - &[(pub_key.into(), AuthSecretKey::RpoFalcon512(sec_key))], - rng, - ); + let authenticator = BasicAuthenticator::new(&[sec_key]); (component, Some(authenticator)) }, diff --git a/crates/miden-testing/src/mock_chain/chain.rs b/crates/miden-testing/src/mock_chain/chain.rs index 5a801eaf4c..39b5a7d688 100644 --- a/crates/miden-testing/src/mock_chain/chain.rs +++ b/crates/miden-testing/src/mock_chain/chain.rs @@ -1,17 +1,14 @@ -use alloc::boxed::Box; use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::string::ToString; use alloc::vec::Vec; use anyhow::Context; use miden_block_prover::{LocalBlockProver, ProvenBlockError}; -use miden_lib::note::{create_p2id_note, create_p2ide_note}; +use miden_objects::account::auth::AuthSecretKey; use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::account::{Account, AccountId, AuthSecretKey, StorageSlot}; -use miden_objects::asset::Asset; +use miden_objects::account::{Account, AccountId, PartialAccount}; use miden_objects::batch::{ProposedBatch, ProvenBatch}; +use miden_objects::block::account_tree::AccountTree; use miden_objects::block::{ - AccountTree, AccountWitness, BlockHeader, BlockInputs, @@ -22,92 +19,47 @@ use miden_objects::block::{ ProposedBlock, ProvenBlock, }; -use miden_objects::crypto::merkle::SmtProof; -use miden_objects::note::{Note, NoteHeader, NoteId, NoteInclusionProof, NoteType, Nullifier}; +use miden_objects::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier}; use miden_objects::transaction::{ - AccountInputs, ExecutedTransaction, InputNote, InputNotes, - OrderedTransactionHeaders, OutputNote, PartialBlockchain, ProvenTransaction, - TransactionHeader, TransactionInputs, }; -use miden_objects::{MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, NoteError}; -use miden_processor::crypto::RpoRandomCoin; -use miden_processor::{DeserializationError, Word}; +use miden_processor::DeserializationError; +use miden_tx::LocalTransactionProver; use miden_tx::auth::BasicAuthenticator; use miden_tx::utils::{ByteReader, Deserializable, Serializable}; -use rand::SeedableRng; -use rand_chacha::ChaCha20Rng; +use miden_tx_batch_prover::LocalBatchProver; use winterfell::ByteWriter; use super::note::MockChainNote; -use crate::{MockChainBuilder, ProvenTransactionExt, TransactionContextBuilder}; +use crate::{MockChainBuilder, TransactionContextBuilder}; // MOCK CHAIN // ================================================================================================ /// The [`MockChain`] simulates a simplified blockchain environment for testing purposes. -/// It allows creating and managing accounts, minting assets, executing transactions, and applying -/// state updates. /// -/// This struct is designed to mock transaction workflows, asset transfers, and -/// note creation in a test setting. Once entities are set up, [`TransactionContextBuilder`] objects -/// can be obtained in order to execute transactions accordingly. -/// -/// On a high-level, there are two ways to interact with the mock chain: -/// - Generating transactions yourself and adding them to the mock chain "mempool" using -/// [`MockChain::add_pending_executed_transaction`] or -/// [`MockChain::add_pending_proven_transaction`]. Once some transactions have been added, they -/// can be proven into a block using [`MockChain::prove_next_block`], which commits them to the -/// chain state. -/// - Using any of the other pending APIs to _magically_ add new notes, accounts or nullifiers in -/// the next block. For example, [`MockChain::add_pending_p2id_note`] will create a new P2ID note -/// in the next proven block, without actually containing a transaction that creates that note. -/// -/// Both approaches can be mixed in the same block, within limits. In particular, avoid modification -/// of the _same_ entities using both regular transactions and the magic pending APIs. +/// The typical usage of a mock chain is: +/// - Creating it using a [`MockChainBuilder`], which allows adding accounts and notes to the +/// genesis state. +/// - Creating transactions against the chain state and executing them. +/// - Adding executed or proven transactions to the set of pending transactions (the "mempool"), +/// e.g. using [`MockChain::add_pending_executed_transaction`]. +/// - Proving a block, which adds all pending transactions to the chain state, e.g. using +/// [`MockChain::prove_next_block`]. /// /// The mock chain uses the batch and block provers underneath to process pending transactions, so /// the generated blocks are realistic and indistinguishable from a real node. The only caveat is /// that no real ZK proofs are generated or validated as part of transaction, batch or block -/// building. If realistic data is important for your use case, avoid using any pending APIs except -/// for [`MockChain::add_pending_executed_transaction`] and -/// [`MockChain::add_pending_proven_transaction`]. +/// building. /// /// # Examples /// -/// ## Create mock objects and build a transaction context -/// ``` -/// # use anyhow::Result; -/// # use miden_objects::{Felt, asset::{Asset, FungibleAsset}, note::NoteType}; -/// # use miden_testing::{Auth, MockChain, TransactionContextBuilder}; -/// -/// # fn main() -> Result<()> { -/// let mut builder = MockChain::builder(); -/// -/// let faucet = builder.create_new_faucet(Auth::BasicAuth, "USDT", 100_000)?; -/// let asset = Asset::from(FungibleAsset::new(faucet.id(), 10)?); -/// -/// let sender = builder.create_new_wallet(Auth::BasicAuth)?; -/// let target = builder.create_new_wallet(Auth::BasicAuth)?; -/// -/// let note = builder.add_p2id_note(faucet.id(), target.id(), &[asset], NoteType::Public)?; -/// -/// let mock_chain = builder.build()?; -/// -/// // The target account is a new account so we move it into the build_tx_context, since the -/// // chain's committed accounts do not yet contain it. -/// let tx_context = mock_chain.build_tx_context(target, &[note.id()], &[])?.build()?; -/// let executed_transaction = tx_context.execute_blocking()?; -/// # Ok(()) -/// # } -/// ``` -/// /// ## Executing a simple transaction /// ``` /// # use anyhow::Result; @@ -116,8 +68,12 @@ use crate::{MockChainBuilder, ProvenTransactionExt, TransactionContextBuilder}; /// # note::NoteType, /// # }; /// # use miden_testing::{Auth, MockChain}; +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() -> Result<()> { +/// // Build a genesis state for a mock chain using a MockChainBuilder. +/// // -------------------------------------------------------------------------------------------- /// -/// # fn main() -> Result<()> { /// let mut builder = MockChain::builder(); /// /// // Add a recipient wallet. @@ -125,9 +81,9 @@ use crate::{MockChainBuilder, ProvenTransactionExt, TransactionContextBuilder}; /// /// // Add a wallet with assets. /// let sender = builder.add_existing_wallet(Auth::IncrNonce)?; -/// let fungible_asset = FungibleAsset::mock(10).unwrap_fungible(); /// -/// // Add a pending P2ID note to the chain. +/// let fungible_asset = FungibleAsset::mock(10).unwrap_fungible(); +/// // Add a P2ID note with a fungible asset to the chain. /// let note = builder.add_p2id_note( /// sender.id(), /// receiver.id(), @@ -135,19 +91,27 @@ use crate::{MockChainBuilder, ProvenTransactionExt, TransactionContextBuilder}; /// NoteType::Public, /// )?; /// -/// let mut mock_chain = builder.build()?; +/// let mut mock_chain: MockChain = builder.build()?; +/// +/// // Create a transaction against the receiver account consuming the note. +/// // -------------------------------------------------------------------------------------------- /// /// let transaction = mock_chain /// .build_tx_context(receiver.id(), &[note.id()], &[])? /// .build()? -/// .execute_blocking()?; +/// .execute() +/// .await?; +/// +/// // Add the transaction to the chain state. +/// // -------------------------------------------------------------------------------------------- /// /// // Add the transaction to the mock chain's "mempool" of pending transactions. -/// mock_chain.add_pending_executed_transaction(&transaction); +/// mock_chain.add_pending_executed_transaction(&transaction)?; /// /// // Prove the next block to include the transaction in the chain state. /// mock_chain.prove_next_block()?; /// +/// // The receiver account should now have the asset in its account vault. /// assert_eq!( /// mock_chain /// .committed_account(receiver.id())? @@ -158,6 +122,35 @@ use crate::{MockChainBuilder, ProvenTransactionExt, TransactionContextBuilder}; /// # Ok(()) /// # } /// ``` +/// +/// ## Create mock objects and build a transaction context +/// +/// ``` +/// # use anyhow::Result; +/// # use miden_objects::{Felt, asset::{Asset, FungibleAsset}, note::NoteType}; +/// # use miden_testing::{Auth, MockChain, TransactionContextBuilder}; +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() -> Result<()> { +/// let mut builder = MockChain::builder(); +/// +/// let faucet = builder.create_new_faucet(Auth::BasicAuth, "USDT", 100_000)?; +/// let asset = Asset::from(FungibleAsset::new(faucet.id(), 10)?); +/// +/// let sender = builder.create_new_wallet(Auth::BasicAuth)?; +/// let target = builder.create_new_wallet(Auth::BasicAuth)?; +/// +/// let note = builder.add_p2id_note(faucet.id(), target.id(), &[asset], NoteType::Public)?; +/// +/// let mock_chain = builder.build()?; +/// +/// // The target account is a new account so we move it into the build_tx_context, since the +/// // chain's committed accounts do not yet contain it. +/// let tx_context = mock_chain.build_tx_context(target, &[note.id()], &[])?.build()?; +/// let executed_transaction = tx_context.execute().await?; +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, Clone)] pub struct MockChain { /// An append-only structure used to represent the history of blocks produced for this chain. @@ -172,11 +165,6 @@ pub struct MockChain { /// Tree containing the state commitments of all accounts. account_tree: AccountTree, - /// Note batches created in transactions in the block. - /// - /// These will become available once the block is proven. - pending_output_notes: Vec, - /// Transactions that have been submitted to the chain but have not yet been included in a /// block. pending_transactions: Vec, @@ -192,12 +180,9 @@ pub struct MockChain { /// remain at the last observed state. committed_accounts: BTreeMap, - /// AccountId |-> AccountCredentials mapping to store the seed and authenticator for accounts - /// to simplify transaction creation. - account_credentials: BTreeMap, - - // The RNG used to generate note serial numbers, account seeds or cryptographic keys. - rng: ChaCha20Rng, + /// AccountId |-> AccountAuthenticator mapping to store the authenticator for accounts to + /// simplify transaction creation. + account_authenticators: BTreeMap, } impl MockChain { @@ -228,20 +213,17 @@ impl MockChain { pub(super) fn from_genesis_block( genesis_block: ProvenBlock, account_tree: AccountTree, - account_credentials: BTreeMap, + account_authenticators: BTreeMap, ) -> anyhow::Result { let mut chain = MockChain { chain: Blockchain::default(), blocks: vec![], nullifier_tree: NullifierTree::default(), account_tree, - pending_output_notes: Vec::new(), pending_transactions: Vec::new(), committed_notes: BTreeMap::new(), committed_accounts: BTreeMap::new(), - account_credentials, - // Initialize RNG with default seed. - rng: ChaCha20Rng::from_seed(Default::default()), + account_authenticators, }; // We do not have to apply the tree changes, because the account tree is already initialized @@ -482,37 +464,8 @@ impl MockChain { &self, proposed_batch: ProposedBatch, ) -> anyhow::Result { - let ( - transactions, - block_header, - _partial_blockchain, - _unauthenticated_note_proofs, - id, - account_updates, - input_notes, - output_notes, - batch_expiration_block_num, - ) = proposed_batch.into_parts(); - - // SAFETY: This satisfies the requirements of the ordered tx headers. - let tx_headers = OrderedTransactionHeaders::new_unchecked( - transactions - .iter() - .map(AsRef::as_ref) - .map(TransactionHeader::from) - .collect::>(), - ); - - Ok(ProvenBatch::new( - id, - block_header.commitment(), - block_header.block_num(), - account_updates, - input_notes, - output_notes, - batch_expiration_block_num, - tx_headers, - )?) + let batch_prover = LocalBatchProver::new(0); + Ok(batch_prover.prove_dummy(proposed_batch)?) } // BLOCK APIS @@ -565,7 +518,7 @@ impl MockChain { &self, proposed_block: ProposedBlock, ) -> Result { - LocalBlockProver::new(0).prove_without_batch_verification(proposed_block) + LocalBlockProver::new(0).prove_dummy(proposed_block) } // TRANSACTION APIS @@ -578,17 +531,13 @@ impl MockChain { /// from the chain for the public account identified by the ID. /// - [`TxContextInput::Account`]: Initialize the builder with [`TransactionInputs`] where the /// account is passed as-is to the inputs. - /// - [`TxContextInput::ExecutedTransaction`]: Initialize the builder with [`TransactionInputs`] - /// where the account passed to the inputs is the final account of the executed transaction. - /// This is the initial account of the transaction with the account delta applied. /// - /// In all cases, if the chain contains a seed or authenticator for the account, they are added - /// to the builder. + /// In all cases, if the chain contains authenticator for the account, they are added to the + /// builder. /// - /// [`TxContextInput::Account`] and [`TxContextInput::ExecutedTransaction`] can be used to build - /// a chain of transactions against the same account that build on top of each other. For - /// example, transaction A modifies an account from state 0 to 1, and transaction B modifies - /// it from state 1 to 2. + /// [`TxContextInput::Account`] can be used to build a chain of transactions against the same + /// account that build on top of each other. For example, transaction A modifies an account + /// from state 0 to 1, and transaction B modifies it from state 1 to 2. pub fn build_tx_context_at( &self, reference_block: impl Into, @@ -599,10 +548,9 @@ impl MockChain { let input = input.into(); let reference_block = reference_block.into(); - let credentials = self.account_credentials.get(&input.id()); + let authenticator = self.account_authenticators.get(&input.id()); let authenticator = - credentials.and_then(|credentials| credentials.authenticator().cloned()); - let seed = credentials.and_then(|credentials| credentials.seed()); + authenticator.and_then(|authenticator| authenticator.authenticator().cloned()); anyhow::ensure!( reference_block.as_usize() < self.blocks.len(), @@ -626,29 +574,14 @@ impl MockChain { .clone() }, TxContextInput::Account(account) => account, - TxContextInput::ExecutedTransaction(executed_transaction) => { - let mut initial_account = executed_transaction.initial_account().clone(); - initial_account - .apply_delta(executed_transaction.account_delta()) - .context("could not apply delta from previous transaction")?; - - initial_account - }, }; let tx_inputs = self - .get_transaction_inputs_at( - reference_block, - account.clone(), - seed, - note_ids, - unauthenticated_notes, - ) + .get_transaction_inputs_at(reference_block, &account, note_ids, unauthenticated_notes) .context("failed to gather transaction inputs")?; let tx_context_builder = TransactionContextBuilder::new(account) .authenticator(authenticator) - .account_seed(seed) .tx_inputs(tx_inputs); Ok(tx_context_builder) @@ -671,13 +604,12 @@ impl MockChain { // INPUTS APIS // ---------------------------------------------------------------------------------------- - /// Returns a valid [`TransactionInputs`] for the specified entities, executing against a - /// specific block number. + /// Returns a valid [`TransactionInputs`] for the specified entities, executing against + /// a specific block number. pub fn get_transaction_inputs_at( &self, reference_block: BlockNumber, - account: Account, - account_seed: Option, + account: impl Into, notes: &[NoteId], unauthenticated_notes: &[Note], ) -> anyhow::Result { @@ -734,8 +666,7 @@ impl MockChain { let input_notes = InputNotes::new(input_notes)?; Ok(TransactionInputs::new( - account, - account_seed, + account.into(), ref_block.clone(), partial_blockchain, input_notes, @@ -745,19 +676,12 @@ impl MockChain { /// Returns a valid [`TransactionInputs`] for the specified entities. pub fn get_transaction_inputs( &self, - account: Account, - account_seed: Option, + account: impl Into, notes: &[NoteId], unauthenticated_notes: &[Note], ) -> anyhow::Result { let latest_block_num = self.latest_block_header().block_num(); - self.get_transaction_inputs_at( - latest_block_num, - account, - account_seed, - notes, - unauthenticated_notes, - ) + self.get_transaction_inputs_at(latest_block_num, account, notes, unauthenticated_notes) } /// Returns inputs for a transaction batch for all the reference blocks of the provided @@ -786,25 +710,19 @@ impl MockChain { } /// Gets foreign account inputs to execute FPI transactions. - pub fn get_foreign_account_inputs( + /// + /// Only used internally and so does not need to be public. + #[cfg(test)] + pub(crate) fn get_foreign_account_inputs( &self, account_id: AccountId, - ) -> anyhow::Result { - let account = self.committed_account(account_id)?; + ) -> anyhow::Result<(Account, AccountWitness)> { + let account = self.committed_account(account_id)?.clone(); let account_witness = self.account_tree().open(account_id); assert_eq!(account_witness.state_commitment(), account.commitment()); - let mut storage_map_proofs = vec![]; - for slot in account.storage().slots() { - // if there are storage maps, we populate the merkle store and advice map - if let StorageSlot::Map(map) = slot { - let proofs: Vec = map.entries().map(|(key, _)| map.open(key)).collect(); - storage_map_proofs.extend(proofs); - } - } - - Ok(AccountInputs::new(account.into(), account_witness)) + Ok((account, account_witness)) } /// Gets the inputs for a block for the provided batches. @@ -847,16 +765,18 @@ impl MockChain { // PUBLIC MUTATORS // ---------------------------------------------------------------------------------------- - /// Creates the next block in the mock chain. + /// Proves the next block in the mock chain. /// - /// This will make all the objects currently pending available for use. + /// This will commit all the currently pending transactions into the chain state. pub fn prove_next_block(&mut self) -> anyhow::Result { - self.prove_block_inner(None) + self.prove_and_apply_block(None) } /// Proves the next block in the mock chain at the given timestamp. + /// + /// This will commit all the currently pending transactions into the chain state. pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result { - self.prove_block_inner(Some(timestamp)) + self.prove_and_apply_block(Some(timestamp)) } /// Proves new blocks until the block with the given target block number has been created. @@ -887,11 +807,6 @@ impl MockChain { Ok(last_block.expect("at least one block should have been created")) } - /// Sets the seed for the internal RNG. - pub fn set_rng_seed(&mut self, seed: [u8; 32]) { - self.rng = ChaCha20Rng::from_seed(seed); - } - // PUBLIC MUTATORS (PENDING APIS) // ---------------------------------------------------------------------------------------- @@ -899,21 +814,18 @@ impl MockChain { /// /// A block has to be created to apply the transaction effects to the chain state, e.g. using /// [`MockChain::prove_next_block`]. - /// - /// Returns the resulting state of the executing account after executing the transaction. pub fn add_pending_executed_transaction( &mut self, transaction: &ExecutedTransaction, - ) -> anyhow::Result { - let mut account = transaction.initial_account().clone(); - account.apply_delta(transaction.account_delta())?; - - // This essentially transforms an executed tx into a proven tx with a dummy proof. - let proven_tx = ProvenTransaction::from_executed_transaction_mocked(transaction.clone()); + ) -> anyhow::Result<()> { + // Transform the executed tx into a proven tx with a dummy proof. + let proven_tx = LocalTransactionProver::default() + .prove_dummy(transaction.clone()) + .context("failed to dummy-prove executed transaction into proven transaction")?; self.pending_transactions.push(proven_tx); - Ok(account) + Ok(()) } /// Adds the given [`ProvenTransaction`] to the list of pending transactions. @@ -924,71 +836,6 @@ impl MockChain { self.pending_transactions.push(transaction); } - /// Adds the given [`OutputNote`] to the list of pending notes. - /// - /// A block has to be created to add the note to that block and make it available in the chain - /// state, e.g. using [`MockChain::prove_next_block`]. - pub fn add_pending_note(&mut self, note: OutputNote) { - self.pending_output_notes.push(note); - } - - /// Adds a plain P2ID [`OutputNote`] to the list of pending notes. - /// - /// The note is immediately spendable by `target_account_id` and carries no - /// additional reclaim or timelock conditions. - pub fn add_pending_p2id_note( - &mut self, - sender_account_id: AccountId, - target_account_id: AccountId, - asset: &[Asset], - note_type: NoteType, - ) -> Result { - let mut rng = RpoRandomCoin::new(Word::empty()); - - let note = create_p2id_note( - sender_account_id, - target_account_id, - asset.to_vec(), - note_type, - Default::default(), - &mut rng, - )?; - - self.add_pending_note(OutputNote::Full(note.clone())); - Ok(note) - } - - /// Adds a P2IDE [`OutputNote`] (pay‑to‑ID‑extended) to the list of pending notes. - /// - /// A P2IDE note can include an optional `timelock_height` and/or an optional - /// `reclaim_height` after which the `sender_account_id` may reclaim the - /// funds. - pub fn add_pending_p2ide_note( - &mut self, - sender_account_id: AccountId, - target_account_id: AccountId, - asset: &[Asset], - note_type: NoteType, - reclaim_height: Option, - timelock_height: Option, - ) -> Result { - let mut rng = RpoRandomCoin::new(Word::empty()); - - let note = create_p2ide_note( - sender_account_id, - target_account_id, - asset.to_vec(), - reclaim_height, - timelock_height, - note_type, - Default::default(), - &mut rng, - )?; - - self.add_pending_note(OutputNote::Full(note.clone())); - Ok(note) - } - // PRIVATE HELPERS // ---------------------------------------------------------------------------------------- @@ -1018,17 +865,22 @@ impl MockChain { for account_update in proven_block.updated_accounts() { match account_update.details() { - AccountUpdateDetails::New(account) => { - self.committed_accounts.insert(account.id(), account.clone()); - }, AccountUpdateDetails::Delta(account_delta) => { - let committed_account = - self.committed_accounts.get_mut(&account_update.account_id()).ok_or_else( - || anyhow::anyhow!("account delta in block for non-existent account"), - )?; - committed_account - .apply_delta(account_delta) - .context("failed to apply account delta")?; + if account_delta.is_full_state() { + let account = Account::try_from(account_delta) + .context("failed to convert full state delta into full account")?; + self.committed_accounts.insert(account.id(), account.clone()); + } else { + let committed_account = self + .committed_accounts + .get_mut(&account_update.account_id()) + .ok_or_else(|| { + anyhow::anyhow!("account delta in block for non-existent account") + })?; + committed_account + .apply_delta(account_delta) + .context("failed to apply account delta")?; + } }, // No state to keep for private accounts other than the commitment on the account // tree @@ -1044,7 +896,7 @@ impl MockChain { block_note_index.leaf_index_value(), note_path, ) - .context("failed to construct note inclusion proof")?; + .context("failed to create inclusion proof for output note")?; if let OutputNote::Full(note) = created_note { self.committed_notes @@ -1095,83 +947,16 @@ impl MockChain { Ok(vec![proven_batch]) } - fn apply_pending_notes_to_block( - &mut self, - proven_block: &mut ProvenBlock, - ) -> anyhow::Result<()> { - // Add pending output notes to block. - let output_notes_block: BTreeSet = - proven_block.output_notes().map(|(_, output_note)| output_note.id()).collect(); - - // We could distribute notes over multiple batches (if space is available), but most likely - // one is sufficient. - if self.pending_output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH { - return Err(anyhow::anyhow!( - "too many pending output notes: {}, max allowed: {MAX_OUTPUT_NOTES_PER_BATCH}", - self.pending_output_notes.len(), - )); - } - - let mut pending_note_batch = Vec::with_capacity(self.pending_output_notes.len()); - let pending_output_notes = core::mem::take(&mut self.pending_output_notes); - for (note_idx, output_note) in pending_output_notes.into_iter().enumerate() { - if output_notes_block.contains(&output_note.id()) { - return Err(anyhow::anyhow!( - "output note {} is already created in block through transactions", - output_note.id() - )); - } - - pending_note_batch.push((note_idx, output_note)); - } - - if (proven_block.output_note_batches().len() + 1) > MAX_BATCHES_PER_BLOCK { - return Err(anyhow::anyhow!( - "too many batches in block: cannot add more pending notes".to_string(), - )); - } - - proven_block.output_note_batches_mut().push(pending_note_batch); - - let updated_block_note_tree = proven_block.build_output_note_tree().root(); - - // Update note tree root in the block header. - let block_header = proven_block.header(); - let updated_header = BlockHeader::new( - block_header.version(), - block_header.prev_block_commitment(), - block_header.block_num(), - block_header.chain_commitment(), - block_header.account_root(), - block_header.nullifier_root(), - updated_block_note_tree, - block_header.tx_commitment(), - block_header.tx_kernel_commitment(), - block_header.proof_commitment(), - block_header.fee_parameters().clone(), - block_header.timestamp(), - ); - proven_block.set_block_header(updated_header); - - Ok(()) - } - /// Creates a new block in the mock chain. /// - /// This will make all the objects currently pending available for use. - /// - /// If a `timestamp` is provided, it will be set on the block. - /// - /// Block building is divided into a few steps: + /// Block building is divided into two steps: /// /// 1. Build batches from pending transactions and a block from those batches. This results in a /// block. - /// 2. Take the pending notes and insert them directly into the proven block. This means we have - /// to update the header of the block with the updated block note tree root. - /// 3. Finally, the block contains both the updates from the regular transactions/batches as - /// well as the pending notes. Now insert all the accounts, nullifier and notes into the - /// chain state. - fn prove_block_inner(&mut self, timestamp: Option) -> anyhow::Result { + /// 2. Insert all the account updates, nullifiers and notes from the block into the chain state. + /// + /// If a `timestamp` is provided, it will be set on the block. + fn prove_and_apply_block(&mut self, timestamp: Option) -> anyhow::Result { // Create batches from pending transactions. // ---------------------------------------------------------------------------------------- @@ -1186,11 +971,10 @@ impl MockChain { let proposed_block = self .propose_block_at(batches, block_timestamp) .context("failed to create proposed block")?; - let mut proven_block = self.prove_block(proposed_block).context("failed to prove block")?; + let proven_block = self.prove_block(proposed_block).context("failed to prove block")?; - if !self.pending_output_notes.is_empty() { - self.apply_pending_notes_to_block(&mut proven_block)?; - } + // Apply block. + // ---------------------------------------------------------------------------------------- self.apply_block(proven_block.clone()).context("failed to apply block")?; @@ -1213,11 +997,10 @@ impl Serializable for MockChain { self.blocks.write_into(target); self.nullifier_tree.write_into(target); self.account_tree.write_into(target); - self.pending_output_notes.write_into(target); self.pending_transactions.write_into(target); self.committed_accounts.write_into(target); self.committed_notes.write_into(target); - self.account_credentials.write_into(target); + self.account_authenticators.write_into(target); } } @@ -1227,23 +1010,21 @@ impl Deserializable for MockChain { let blocks = Vec::::read_from(source)?; let nullifier_tree = NullifierTree::read_from(source)?; let account_tree = AccountTree::read_from(source)?; - let pending_output_notes = Vec::::read_from(source)?; let pending_transactions = Vec::::read_from(source)?; let committed_accounts = BTreeMap::::read_from(source)?; let committed_notes = BTreeMap::::read_from(source)?; - let account_credentials = BTreeMap::::read_from(source)?; + let account_authenticators = + BTreeMap::::read_from(source)?; Ok(Self { chain, blocks, nullifier_tree, account_tree, - pending_output_notes, pending_transactions, committed_notes, committed_accounts, - account_credentials, - rng: ChaCha20Rng::from_os_rng(), + account_authenticators, }) } } @@ -1258,65 +1039,56 @@ pub enum AccountState { Exists, } -// ACCOUNT CREDENTIALS +// ACCOUNT AUTHENTICATOR // ================================================================================================ -/// A wrapper around the seed and authenticator of an account. +/// A wrapper around the authenticator of an account. #[derive(Debug, Clone)] -pub(super) struct AccountCredentials { - seed: Option, - authenticator: Option>, +pub(super) struct AccountAuthenticator { + authenticator: Option, } -impl AccountCredentials { - pub fn new(seed: Option, authenticator: Option>) -> Self { - Self { seed, authenticator } +impl AccountAuthenticator { + pub fn new(authenticator: Option) -> Self { + Self { authenticator } } - pub fn authenticator(&self) -> Option<&BasicAuthenticator> { + pub fn authenticator(&self) -> Option<&BasicAuthenticator> { self.authenticator.as_ref() } - - pub fn seed(&self) -> Option { - self.seed - } } -impl PartialEq for AccountCredentials { +impl PartialEq for AccountAuthenticator { fn eq(&self, other: &Self) -> bool { - let authenticator_eq = match (&self.authenticator, &other.authenticator) { + match (&self.authenticator, &other.authenticator) { (Some(a), Some(b)) => { a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key) }, (None, None) => true, _ => false, - }; - authenticator_eq && self.seed == other.seed + } } } // SERIALIZATION // ================================================================================================ -impl Serializable for AccountCredentials { +impl Serializable for AccountAuthenticator { fn write_into(&self, target: &mut W) { - self.seed.write_into(target); self.authenticator .as_ref() - .map(|auth| auth.keys().iter().collect::>()) + .map(|auth| auth.keys().values().collect::>()) .write_into(target); } } -impl Deserializable for AccountCredentials { +impl Deserializable for AccountAuthenticator { fn read_from(source: &mut R) -> Result { - let seed = Option::::read_from(source)?; - let authenticator = Option::>::read_from(source)?; + let authenticator = Option::>::read_from(source)?; - let authenticator = authenticator - .map(|keys| BasicAuthenticator::new_with_rng(&keys, ChaCha20Rng::from_os_rng())); + let authenticator = authenticator.map(|keys| BasicAuthenticator::new(&keys)); - Ok(Self { seed, authenticator }) + Ok(Self { authenticator }) } } @@ -1325,11 +1097,11 @@ impl Deserializable for AccountCredentials { /// Helper type to abstract over the inputs to [`MockChain::build_tx_context`]. See that method's /// docs for details. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum TxContextInput { AccountId(AccountId), Account(Account), - ExecutedTransaction(Box), } impl TxContextInput { @@ -1338,9 +1110,6 @@ impl TxContextInput { match self { TxContextInput::AccountId(account_id) => *account_id, TxContextInput::Account(account) => account.id(), - TxContextInput::ExecutedTransaction(executed_transaction) => { - executed_transaction.account_id() - }, } } } @@ -1357,12 +1126,6 @@ impl From for TxContextInput { } } -impl From for TxContextInput { - fn from(tx: ExecutedTransaction) -> Self { - Self::ExecutedTransaction(Box::new(tx)) - } -} - // TESTS // ================================================================================================ @@ -1370,7 +1133,8 @@ impl From for TxContextInput { mod tests { use miden_lib::account::wallets::BasicWallet; use miden_objects::account::{AccountBuilder, AccountStorageMode}; - use miden_objects::asset::FungibleAsset; + use miden_objects::asset::{Asset, FungibleAsset}; + use miden_objects::note::NoteType; use miden_objects::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, @@ -1390,8 +1154,8 @@ mod tests { Ok(()) } - #[test] - fn private_account_state_update() -> anyhow::Result<()> { + #[tokio::test] + async fn private_account_state_update() -> anyhow::Result<()> { let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?; let account_builder = AccountBuilder::new([4; 32]) .storage_mode(AccountStorageMode::Private) @@ -1420,7 +1184,8 @@ mod tests { let tx = mock_chain .build_tx_context(TxContextInput::Account(account), &[], &[note_1])? .build()? - .execute_blocking()?; + .execute() + .await?; mock_chain.add_pending_executed_transaction(&tx)?; mock_chain.prove_next_block()?; @@ -1434,8 +1199,8 @@ mod tests { Ok(()) } - #[test] - fn mock_chain_serialization() { + #[tokio::test] + async fn mock_chain_serialization() { let mut builder = MockChain::builder(); let mut notes = vec![]; @@ -1471,7 +1236,8 @@ mod tests { .unwrap() .build() .unwrap() - .execute_blocking() + .execute() + .await .unwrap(); chain.add_pending_executed_transaction(&tx).unwrap(); chain.prove_next_block().unwrap(); @@ -1485,10 +1251,9 @@ mod tests { assert_eq!(chain.blocks, deserialized.blocks); assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree); assert_eq!(chain.account_tree, deserialized.account_tree); - assert_eq!(chain.pending_output_notes, deserialized.pending_output_notes); assert_eq!(chain.pending_transactions, deserialized.pending_transactions); assert_eq!(chain.committed_accounts, deserialized.committed_accounts); assert_eq!(chain.committed_notes, deserialized.committed_notes); - assert_eq!(chain.account_credentials, deserialized.account_credentials); + assert_eq!(chain.account_authenticators, deserialized.account_authenticators); } } diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 8bca03847d..93aeafa53a 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -2,8 +2,18 @@ use alloc::collections::BTreeMap; use alloc::vec::Vec; use anyhow::Context; + +// CONSTANTS +// ================================================================================================ + +/// Default number of decimals for faucets created in tests. +const DEFAULT_FAUCET_DECIMALS: u8 = 10; + +// IMPORTS +// ================================================================================================ + use itertools::Itertools; -use miden_lib::account::faucets::BasicFungibleFaucet; +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; @@ -12,14 +22,15 @@ use miden_objects::account::delta::AccountUpdateDetails; use miden_objects::account::{ Account, AccountBuilder, + AccountDelta, AccountId, AccountStorageMode, AccountType, StorageSlot, }; use miden_objects::asset::{Asset, FungibleAsset, TokenSymbol}; +use miden_objects::block::account_tree::AccountTree; use miden_objects::block::{ - AccountTree, BlockAccountUpdate, BlockHeader, BlockNoteTree, @@ -37,15 +48,57 @@ use miden_objects::{Felt, FieldElement, MAX_OUTPUT_NOTES_PER_BATCH, NoteError, W use miden_processor::crypto::RpoRandomCoin; use rand::Rng; -use crate::mock_chain::chain::AccountCredentials; +use crate::mock_chain::chain::AccountAuthenticator; use crate::utils::{create_p2any_note, create_spawn_note}; use crate::{AccountState, Auth, MockChain}; -/// A builder for a [`MockChain`]. +/// A builder for a [`MockChain`]'s genesis block. +/// +/// ## Example +/// +/// ``` +/// # use anyhow::Result; +/// # use miden_objects::{ +/// # asset::{Asset, FungibleAsset}, +/// # note::NoteType, +/// # }; +/// # use miden_testing::{Auth, MockChain}; +/// # +/// # fn main() -> Result<()> { +/// let mut builder = MockChain::builder(); +/// let existing_wallet = +/// builder.add_existing_wallet_with_assets(Auth::IncrNonce, [FungibleAsset::mock(500)])?; +/// let new_wallet = builder.create_new_wallet(Auth::IncrNonce)?; +/// +/// let existing_note = builder.add_p2id_note( +/// existing_wallet.id(), +/// new_wallet.id(), +/// &[FungibleAsset::mock(100)], +/// NoteType::Private, +/// )?; +/// let chain = builder.build()?; +/// +/// // The existing wallet and note should be part of the chain state. +/// assert!(chain.committed_account(existing_wallet.id()).is_ok()); +/// assert!(chain.committed_notes().get(&existing_note.id()).is_some()); +/// +/// // The new wallet should *not* be part of the chain state - it must be created in +/// // a transaction first. +/// assert!(chain.committed_account(new_wallet.id()).is_err()); +/// +/// # Ok(()) +/// # } +/// ``` +/// +/// Note the distinction between `add_` and `create_` APIs. Any `add_` APIs will add something to +/// the genesis chain state while `create_` APIs do not mutate the genesis state. The latter are +/// simply convenient for creating accounts or notes that will be created by transactions. +/// +/// See also the [`MockChain`] docs for examples on using the mock chain. #[derive(Debug, Clone)] pub struct MockChainBuilder { accounts: BTreeMap, - account_credentials: BTreeMap, + account_authenticators: BTreeMap, notes: Vec, rng: RpoRandomCoin, // Fee parameters. @@ -69,7 +122,7 @@ impl MockChainBuilder { Self { accounts: BTreeMap::new(), - account_credentials: BTreeMap::new(), + account_authenticators: BTreeMap::new(), notes: Vec::new(), rng: RpoRandomCoin::new(Default::default()), native_asset_id, @@ -79,9 +132,9 @@ impl MockChainBuilder { /// Initializes a new mock chain builder with the provided accounts. /// - /// This method only adds the accounts and cannot not register any seed or authenticator for it. + /// This method only adds the accounts and cannot not register any authenticators for them. /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if the - /// account is new or if they need an authenticator. + /// account needs an authenticator. /// /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g. /// [`MockChainBuilder::add_account_from_builder`]. @@ -122,11 +175,13 @@ impl MockChainBuilder { .accounts .into_values() .map(|account| { - BlockAccountUpdate::new( - account.id(), - account.commitment(), - AccountUpdateDetails::New(account), - ) + let account_id = account.id(); + let account_commitment = account.commitment(); + let account_delta = AccountDelta::try_from(account) + .expect("chain builder should only store existing accounts without seeds"); + let update_details = AccountUpdateDetails::Delta(account_delta); + + BlockAccountUpdate::new(account_id, account_commitment, update_details) }) .collect(); @@ -157,7 +212,7 @@ impl MockChainBuilder { let nullifier_root = NullifierTree::new().root(); let note_root = note_tree.root(); let tx_commitment = transactions.commitment(); - let tx_kernel_commitment = TransactionKernel::kernel_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) @@ -186,17 +241,17 @@ impl MockChainBuilder { transactions, ); - MockChain::from_genesis_block(genesis_block, account_tree, self.account_credentials) + MockChain::from_genesis_block(genesis_block, account_tree, self.account_authenticators) } // ACCOUNT METHODS // ---------------------------------------------------------------------------------------- - /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) and - /// seed. + /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) for + /// it. /// /// This does not add the account to the chain state, but it can still be used to call - /// [`MockChain::build_tx_context`] to automatically handle the authenticator and seed. + /// [`MockChain::build_tx_context`] to automatically add the authenticator. pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result { let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) @@ -227,10 +282,10 @@ impl MockChainBuilder { } /// Creates a new public [`BasicFungibleFaucet`] account and registers the authenticator (if - /// any) and seed. + /// any) for it. /// /// This does not add the account to the chain state, but it can still be used to call - /// [`MockChain::build_tx_context`] to automatically handle the authenticator and seed. + /// [`MockChain::build_tx_context`] to automatically add the authenticator. pub fn create_new_faucet( &mut self, auth_method: Auth, @@ -242,8 +297,9 @@ impl MockChainBuilder { let max_supply_felt = max_supply.try_into().map_err(|_| { anyhow::anyhow!("max supply value cannot be converted to Felt: {max_supply}") })?; - let basic_faucet = BasicFungibleFaucet::new(token_symbol, 10, max_supply_felt) - .context("failed to create BasicFungibleFaucet")?; + let basic_faucet = + BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt) + .context("failed to create BasicFungibleFaucet")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) @@ -253,9 +309,11 @@ impl MockChainBuilder { self.add_account_from_builder(auth_method, account_builder, AccountState::New) } - /// Adds an existing public [`BasicFungibleFaucet`] account to the initial chain state and - /// registers the authenticator (if the given [`Auth`] results in the creation of one). - pub fn add_existing_faucet( + /// Adds an existing [`BasicFungibleFaucet`] account to the initial chain state and + /// registers the authenticator. + /// + /// Basic fungible faucets always use `AccountStorageMode::Public` and require authentication. + pub fn add_existing_basic_faucet( &mut self, auth_method: Auth, token_symbol: &str, @@ -263,8 +321,9 @@ impl MockChainBuilder { total_issuance: Option, ) -> anyhow::Result { let token_symbol = TokenSymbol::new(token_symbol).context("invalid argument")?; - let basic_faucet = BasicFungibleFaucet::new(token_symbol, 10u8, Felt::new(max_supply)) - .context("invalid argument")?; + let basic_faucet = + BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, Felt::new(max_supply)) + .context("invalid argument")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) @@ -290,6 +349,50 @@ impl MockChainBuilder { Ok(account) } + /// Adds an existing [`NetworkFungibleFaucet`] account to the initial chain state. + /// + /// Network fungible faucets always use `AccountStorageMode::Network` and `Auth::NoAuth`. + pub fn add_existing_network_faucet( + &mut self, + token_symbol: &str, + max_supply: u64, + owner_account_id: AccountId, + total_issuance: Option, + ) -> anyhow::Result { + let token_symbol = TokenSymbol::new(token_symbol).context("invalid argument")?; + let network_faucet = NetworkFungibleFaucet::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + Felt::new(max_supply), + owner_account_id, + ) + .context("invalid argument")?; + + let account_builder = AccountBuilder::new(self.rng.random()) + .storage_mode(AccountStorageMode::Network) + .with_component(network_faucet) + .account_type(AccountType::FungibleFaucet); + + // Network faucets always use Noop auth (no authentication) + 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. + // 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, + Word::from([ZERO, ZERO, ZERO, Felt::new(issuance)]), + ) + .context("failed to set faucet storage")?; + self.accounts.insert(account.id(), account.clone()); + } + + Ok(account) + } + /// Creates a new public account with an [`MockAccountComponent`] and registers the /// authenticator (if any). pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result { @@ -349,9 +452,9 @@ impl MockChainBuilder { /// to the initial chain state. It can then be used in a transaction without having to /// validate its seed. /// - If [`AccountState::New`] is given the account is built as a new account and is **not** - /// added to the chain. Its seed and authenticator are registered (if any). Its first - /// transaction will be its creation transaction. [`MockChain::build_tx_context`] can be - /// called with the account to automatically handle the authenticator and seed. + /// added to the chain. Its authenticator is registered (if present). Its first transaction + /// will be its creation transaction. [`MockChain::build_tx_context`] can be called with the + /// account to automatically add the authenticator. pub fn add_account_from_builder( &mut self, auth_method: Auth, @@ -361,19 +464,16 @@ impl MockChainBuilder { let (auth_component, authenticator) = auth_method.build_component(); account_builder = account_builder.with_auth_component(auth_component); - let (account, seed) = if let AccountState::New = account_state { - let (account, seed) = - account_builder.build().context("failed to build account from builder")?; - (account, Some(seed)) + let account = if let AccountState::New = account_state { + account_builder.build().context("failed to build account from builder")? } else { - let account = account_builder + account_builder .build_existing() - .context("failed to build account from builder")?; - (account, None) + .context("failed to build account from builder")? }; - self.account_credentials - .insert(account.id(), AccountCredentials::new(seed, authenticator)); + self.account_authenticators + .insert(account.id(), AccountAuthenticator::new(authenticator)); if let AccountState::Exists = account_state { self.accounts.insert(account.id(), account.clone()); @@ -384,9 +484,9 @@ impl MockChainBuilder { /// Adds the provided account to the list of genesis accounts. /// - /// This method only adds the account and does not store its account credentials (seed and - /// authenticator) for it. Calling [`MockChain::build_tx_context`] on accounts added in this - /// way will not work if the account is new or if they need an authenticator. + /// This method only adds the account and does not store its account authenticator for it. + /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if + /// the account needs an authenticator. /// /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g. /// [`MockChainBuilder::add_account_from_builder`]. @@ -398,27 +498,26 @@ impl MockChainBuilder { Ok(()) } - // NOTE METHODS + // NOTE ADD METHODS // ---------------------------------------------------------------------------------------- /// Adds the provided note to the initial chain state. - pub fn add_note(&mut self, note: impl Into) { + pub fn add_output_note(&mut self, note: impl Into) { self.notes.push(note.into()); } - /// Creates a new P2ANY note from the provided parameters and adds it to the list of genesis - /// notes. This note is similar to a P2ID note but can be consumed by any account. + /// Creates a new P2ANY note from the provided parameters and adds it to the list of + /// genesis notes. /// - /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id` - /// and carries no additional reclaim or timelock conditions. + /// This note is similar to a P2ID note but can be consumed by any account. pub fn add_p2any_note( &mut self, sender_account_id: AccountId, - asset: &[Asset], + note_type: NoteType, + assets: impl IntoIterator, ) -> anyhow::Result { - let note = create_p2any_note(sender_account_id, asset); - - self.add_note(OutputNote::Full(note.clone())); + let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng); + self.add_output_note(OutputNote::Full(note.clone())); Ok(note) } @@ -440,11 +539,10 @@ impl MockChainBuilder { target_account_id, asset.to_vec(), note_type, - Default::default(), + Felt::ZERO, &mut self.rng, )?; - - self.add_note(OutputNote::Full(note.clone())); + self.add_output_note(OutputNote::Full(note.clone())); Ok(note) } @@ -474,7 +572,7 @@ impl MockChainBuilder { &mut self.rng, )?; - self.add_note(OutputNote::Full(note.clone())); + self.add_output_note(OutputNote::Full(note.clone())); Ok(note) } @@ -498,7 +596,7 @@ impl MockChainBuilder { &mut self.rng, )?; - self.add_note(OutputNote::Full(swap_note.clone())); + self.add_output_note(OutputNote::Full(swap_note.clone())); Ok((swap_note, payback_note)) } @@ -507,15 +605,21 @@ impl MockChainBuilder { /// /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a /// parameter. - pub fn add_spawn_note<'note>( + /// + /// # Errors + /// + /// Returns an error if: + /// - the sender account ID of the provided output notes is not consistent or does not match the + /// transaction's sender. + pub fn add_spawn_note<'note, I>( &mut self, - sender_id: AccountId, - output_notes: impl IntoIterator, - ) -> anyhow::Result { - let output_notes = output_notes.into_iter().collect(); - let note = create_spawn_note(sender_id, output_notes)?; - - self.add_note(OutputNote::Full(note.clone())); + output_notes: impl IntoIterator, + ) -> anyhow::Result + where + I: ExactSizeIterator, + { + let note = create_spawn_note(output_notes)?; + self.add_output_note(OutputNote::Full(note.clone())); Ok(note) } @@ -545,6 +649,13 @@ impl MockChainBuilder { // HELPER FUNCTIONS // ---------------------------------------------------------------------------------------- + /// Returns a mutable reference to the builder's RNG. + /// + /// This can be used when creating accounts or notes and randomness is required. + pub fn rng_mut(&mut self) -> &mut RpoRandomCoin { + &mut self.rng + } + /// Constructs a fungible asset based on the native asset ID and the provided amount. fn native_fee_asset(&self, amount: u64) -> anyhow::Result { FungibleAsset::new(self.native_asset_id, amount).context("failed to create fee asset") diff --git a/crates/miden-testing/src/mock_chain/mod.rs b/crates/miden-testing/src/mock_chain/mod.rs index 335ccf1eb8..263c572975 100644 --- a/crates/miden-testing/src/mock_chain/mod.rs +++ b/crates/miden-testing/src/mock_chain/mod.rs @@ -2,10 +2,8 @@ mod auth; mod chain; mod chain_builder; mod note; -mod proven_tx_ext; pub use auth::Auth; pub use chain::{AccountState, MockChain, TxContextInput}; pub use chain_builder::MockChainBuilder; pub use note::MockChainNote; -pub use proven_tx_ext::ProvenTransactionExt; diff --git a/crates/miden-testing/src/mock_chain/proven_tx_ext.rs b/crates/miden-testing/src/mock_chain/proven_tx_ext.rs deleted file mode 100644 index fbc28e7e66..0000000000 --- a/crates/miden-testing/src/mock_chain/proven_tx_ext.rs +++ /dev/null @@ -1,53 +0,0 @@ -use miden_objects::account::delta::AccountUpdateDetails; -use miden_objects::transaction::{ - ExecutedTransaction, - ProvenTransaction, - ProvenTransactionBuilder, -}; -use miden_objects::vm::ExecutionProof; -use winterfell::Proof; - -/// Extension trait to convert an [`ExecutedTransaction`] into a [`ProvenTransaction`] with a dummy -/// proof for testing purposes. -pub trait ProvenTransactionExt { - /// Converts the transaction into a proven transaction with a dummy proof. - fn from_executed_transaction_mocked(executed_tx: ExecutedTransaction) -> ProvenTransaction; -} - -impl ProvenTransactionExt for ProvenTransaction { - fn from_executed_transaction_mocked(executed_tx: ExecutedTransaction) -> ProvenTransaction { - let block_reference = executed_tx.block_header(); - let account_delta = executed_tx.account_delta().clone(); - let initial_account = executed_tx.initial_account().clone(); - - let account_update_details = if initial_account.is_onchain() { - if initial_account.is_new() { - let mut account = initial_account; - account.apply_delta(&account_delta).expect("account delta should be applicable"); - - AccountUpdateDetails::New(account) - } else { - AccountUpdateDetails::Delta(account_delta) - } - } else { - AccountUpdateDetails::Private - }; - - ProvenTransactionBuilder::new( - executed_tx.account_id(), - executed_tx.initial_account().init_commitment(), - executed_tx.final_account().commitment(), - executed_tx.account_delta().to_commitment(), - block_reference.block_num(), - block_reference.commitment(), - executed_tx.fee(), - executed_tx.expiration_block_num(), - ExecutionProof::new(Proof::new_dummy(), Default::default()), - ) - .add_input_notes(executed_tx.input_notes()) - .add_output_notes(executed_tx.output_notes().iter().cloned()) - .account_update_details(account_update_details) - .build() - .unwrap() - } -} diff --git a/crates/miden-testing/src/mock_host.rs b/crates/miden-testing/src/mock_host.rs index 7c74b2a2ba..ded9a6e663 100644 --- a/crates/miden-testing/src/mock_host.rs +++ b/crates/miden-testing/src/mock_host.rs @@ -1,88 +1,96 @@ -use alloc::boxed::Box; use alloc::collections::BTreeSet; -use alloc::rc::Rc; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::{TransactionEvent, TransactionEventError}; -use miden_objects::account::{AccountHeader, AccountVaultDelta}; -use miden_objects::assembly::debuginfo::SourceManagerSync; -use miden_objects::assembly::{DefaultSourceManager, SourceManager}; -use miden_objects::{Felt, Word}; +use miden_lib::StdLibrary; +use miden_lib::transaction::{EventId, TransactionEvent}; +use miden_objects::Word; use miden_processor::{ - AdviceInputs, AdviceMutation, + AsyncHost, BaseHost, - ContextId, EventError, + FutureMaybeSend, MastForest, - MastForestStore, ProcessState, - SyncHost, }; -use miden_tx::{AccountProcedureIndexMap, LinkMap, TransactionMastStore}; +use miden_tx::TransactionExecutorHost; +use miden_tx::auth::UnreachableAuth; + +use crate::TransactionContext; // MOCK HOST // ================================================================================================ -/// This is very similar to the TransactionHost in miden-tx. The differences include: -/// - We do not track account delta here. -/// - There is special handling of EMPTY_DIGEST in account procedure index map. -/// - This host uses `MemAdviceProvider` which is instantiated from the passed in advice inputs. -pub struct MockHost { - acct_procedure_index_map: AccountProcedureIndexMap, - mast_store: Rc, - source_manager: Arc, +/// The [`MockHost`] wraps a [`TransactionExecutorHost`] and forwards event handling requests to it, +/// with the difference that it only handles a subset of the events that the executor host handles. +/// +/// Why don't we always forward requests to the executor host? In some tests, when using +/// [`TransactionContext::execute_code`], we want to test that the transaction kernel fails +/// with a certain error when given invalid inputs, but the event handler in the executor host would +/// prematurely abort the transaction due to the invalid inputs. To avoid this situation, the event +/// handler can be disabled and we can test that the transaction kernel has the expected behavior +/// (e.g. even if the transaction host was malicious). +/// +/// Some event handlers, such as delta or output note tracking, will similarly interfere with +/// testing a procedure in isolation and these are also turned off in this host. +pub(crate) struct MockHost<'store> { + /// The underlying [`TransactionExecutorHost`] that the mock host will forward requests to. + exec_host: TransactionExecutorHost<'store, 'static, TransactionContext, UnreachableAuth>, + + /// The set of event IDs that the mock host will forward to the [`TransactionExecutorHost`]. + /// + /// Event IDs that are not in this set are not handled. This can be useful in certain test + /// scenarios. + handled_events: BTreeSet, } -impl MockHost { - /// Returns a new [`MockHost`] instance with the provided [`AdviceInputs`]. +impl<'store> MockHost<'store> { + /// Returns a new [`MockHost`] instance with the provided inputs. pub fn new( - account: AccountHeader, - advice_inputs: &AdviceInputs, - mast_store: Rc, - mut foreign_code_commitments: BTreeSet, + exec_host: TransactionExecutorHost<'store, 'static, TransactionContext, UnreachableAuth>, ) -> Self { - foreign_code_commitments.insert(account.code_commitment()); - let account_procedure_index_map = - AccountProcedureIndexMap::new(foreign_code_commitments, advice_inputs) - .expect("account procedure index map should be valid"); + // StdLibrary events are always handled. + let stdlib_handlers = StdLibrary::default() + .handlers() + .into_iter() + .map(|(handler_event_name, _)| handler_event_name.to_event_id()); + let mut handled_events = BTreeSet::from_iter(stdlib_handlers); - Self { - acct_procedure_index_map: account_procedure_index_map, - mast_store, - source_manager: Arc::new(DefaultSourceManager::default()), - } - } + // The default set of transaction events that are always handled. + handled_events.extend( + [ + &TransactionEvent::AccountPushProcedureIndex, + &TransactionEvent::LinkMapSet, + &TransactionEvent::LinkMapGet, + // TODO: It should be possible to remove this after implementing + // https://github.com/0xMiden/miden-base/issues/1852. + &TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount, + ] + .map(TransactionEvent::event_id), + ); - /// Sets the provided [`SourceManagerSync`] on the host. - pub fn with_source_manager(mut self, source_manager: Arc) -> Self { - self.source_manager = source_manager; - self + Self { exec_host, handled_events } } - /// Consumes `self` and returns the advice provider and account vault delta. - pub fn into_parts(self) -> AccountVaultDelta { - AccountVaultDelta::default() - } - - // EVENT HANDLERS - // -------------------------------------------------------------------------------------------- - - fn on_push_account_procedure_index( - &mut self, - process: &ProcessState, - ) -> Result, EventError> { - let proc_idx = self.acct_procedure_index_map.get_proc_index(process).map_err(Box::new)?; - Ok(vec![AdviceMutation::extend_stack([Felt::from(proc_idx)])]) + // Adds the transaction events needed for Lazy loading to the set of handled events. + 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, + ] + .map(TransactionEvent::event_id), + ); } } -impl BaseHost for MockHost { - fn get_mast_forest(&self, node_digest: &Word) -> Option> { - self.mast_store.get(node_digest) - } - +impl<'store> BaseHost for MockHost<'store> { fn get_label_and_source_file( &self, location: &miden_objects::assembly::debuginfo::Location, @@ -90,33 +98,28 @@ impl BaseHost for MockHost { miden_objects::assembly::debuginfo::SourceSpan, Option>, ) { - let maybe_file = self.source_manager.get_by_uri(location.uri()); - let span = self.source_manager.location_to_span(location.clone()).unwrap_or_default(); - (span, maybe_file) + self.exec_host.get_label_and_source_file(location) } } -impl SyncHost for MockHost { +impl<'store> AsyncHost for MockHost<'store> { + fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend>> { + self.exec_host.get_mast_forest(node_digest) + } + fn on_event( &mut self, process: &ProcessState, - event_id: u32, - ) -> Result, EventError> { - let event = TransactionEvent::try_from(event_id).map_err(Box::new)?; + ) -> impl FutureMaybeSend, EventError>> { + let event_id = EventId::from_felt(process.get_stack_item(0)); - if process.ctx() != ContextId::root() { - return Err(Box::new(TransactionEventError::NotRootContext(event_id))); + async move { + // If the host should handle the event, delegate to the tx executor host. + if self.handled_events.contains(&event_id) { + self.exec_host.on_event(process).await + } else { + Ok(Vec::new()) + } } - - let advice_mutations = match event { - TransactionEvent::AccountPushProcedureIndex => { - self.on_push_account_procedure_index(process) - }, - TransactionEvent::LinkMapSetEvent => LinkMap::handle_set_event(process), - TransactionEvent::LinkMapGetEvent => LinkMap::handle_get_event(process), - _ => Ok(Vec::new()), - }?; - - Ok(advice_mutations) } } diff --git a/crates/miden-testing/src/tx_context/builder.rs b/crates/miden-testing/src/tx_context/builder.rs index 3200395a4d..8e524171f9 100644 --- a/crates/miden-testing/src/tx_context/builder.rs +++ b/crates/miden-testing/src/tx_context/builder.rs @@ -9,30 +9,27 @@ 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::Account; +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::note::{Note, NoteId}; +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::{ - AccountInputs, OutputNote, TransactionArgs, TransactionInputs, TransactionScript, }; -use miden_objects::vm::AdviceMap; use miden_processor::{AdviceInputs, Felt, Word}; use miden_tx::TransactionMastStore; use miden_tx::auth::BasicAuthenticator; -use rand_chacha::ChaCha20Rng; use super::TransactionContext; use crate::{MockChain, MockChainNote}; -pub type MockAuthenticator = BasicAuthenticator; - // TRANSACTION CONTEXT BUILDER // ================================================================================================ @@ -44,10 +41,14 @@ pub type MockAuthenticator = BasicAuthenticator; /// /// Create a new account and execute code: /// ``` +/// # use anyhow::Result; /// # use miden_testing::TransactionContextBuilder; /// # use miden_objects::{account::AccountBuilder,Felt, FieldElement}; /// # use miden_lib::transaction::TransactionKernel; -/// let tx_context = TransactionContextBuilder::with_existing_mock_account().build().unwrap(); +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() -> Result<()> { +/// let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; /// /// let code = " /// use.$kernel::prologue @@ -60,23 +61,27 @@ pub type MockAuthenticator = BasicAuthenticator; /// end /// "; /// -/// let process = tx_context.execute_code(code).unwrap(); -/// assert_eq!(process.stack.get(0), Felt::new(5),); +/// let exec_output = tx_context.execute_code(code).await?; +/// assert_eq!(exec_output.stack.get(0).unwrap(), &Felt::new(5)); +/// # Ok(()) +/// # } /// ``` pub struct TransactionContextBuilder { source_manager: Arc, account: Account, - account_seed: Option, advice_inputs: AdviceInputs, - authenticator: Option, + authenticator: Option, expected_output_notes: Vec, - foreign_account_inputs: Vec, + foreign_account_inputs: BTreeMap, input_notes: Vec, tx_script: Option, tx_script_args: Word, note_args: BTreeMap, - transaction_inputs: Option, + tx_inputs: Option, auth_args: Word, + signatures: Vec<(PublicKeyCommitment, Word, Signature)>, + is_lazy_loading_enabled: bool, + note_scripts: BTreeMap, } impl TransactionContextBuilder { @@ -84,17 +89,19 @@ impl TransactionContextBuilder { Self { source_manager: Arc::new(DefaultSourceManager::default()), account, - account_seed: None, input_notes: Vec::new(), expected_output_notes: Vec::new(), tx_script: None, tx_script_args: EMPTY_WORD, authenticator: None, advice_inputs: Default::default(), - transaction_inputs: None, + tx_inputs: None, note_args: BTreeMap::new(), - foreign_account_inputs: vec![], + foreign_account_inputs: BTreeMap::new(), auth_args: EMPTY_WORD, + signatures: Vec::new(), + is_lazy_loading_enabled: true, + note_scripts: BTreeMap::new(), } } @@ -134,12 +141,6 @@ impl TransactionContextBuilder { Self::new(account) } - /// Override and set the account seed manually - pub fn account_seed(mut self, account_seed: Option) -> Self { - self.account_seed = account_seed; - self - } - /// Extend the advice inputs with the provided [AdviceInputs] instance. pub fn extend_advice_inputs(mut self, advice_inputs: AdviceInputs) -> Self { self.advice_inputs.extend(advice_inputs); @@ -156,14 +157,19 @@ impl TransactionContextBuilder { } /// Set the authenticator for the transaction (if needed) - pub fn authenticator(mut self, authenticator: Option) -> Self { + pub fn authenticator(mut self, authenticator: Option) -> Self { self.authenticator = authenticator; self } /// Set foreign account codes that are used by the transaction - pub fn foreign_accounts(mut self, inputs: Vec) -> Self { - self.foreign_account_inputs = inputs; + pub fn foreign_accounts( + mut self, + inputs: impl IntoIterator, + ) -> Self { + self.foreign_account_inputs.extend( + inputs.into_iter().map(|(account, witness)| (account.id(), (account, witness))), + ); self } @@ -193,7 +199,21 @@ impl TransactionContextBuilder { /// Set the desired transaction inputs pub fn tx_inputs(mut self, tx_inputs: TransactionInputs) -> Self { - self.transaction_inputs = Some(tx_inputs); + assert_eq!( + AccountHeader::from(&self.account), + tx_inputs.account().into(), + "account in context and account provided via tx inputs are not the same account" + ); + self.tx_inputs = Some(tx_inputs); + self + } + + /// Disables lazy loading. + /// + /// Only affects [`TransactionContext::execute_code`] and causes the host to _not_ handle lazy + /// loading events. + pub fn disable_lazy_loading(mut self) -> Self { + self.is_lazy_loading_enabled = false; self } @@ -224,21 +244,39 @@ impl TransactionContextBuilder { self } + /// Add a new signature for the message and the public key. + pub fn add_signature( + mut self, + pub_key: PublicKeyCommitment, + message: Word, + signature: Signature, + ) -> Self { + self.signatures.push((pub_key, message, signature)); + self + } + + /// Add a note script to the context for testing. + pub fn add_note_script(mut self, script: NoteScript) -> Self { + self.note_scripts.insert(script.root(), script); + self + } + /// Builds the [TransactionContext]. /// /// If no transaction inputs were provided manually, an ad-hoc MockChain is created in order /// to generate valid block data for the required notes. pub fn build(self) -> anyhow::Result { - let tx_inputs = match self.transaction_inputs { + let mut tx_inputs = match self.tx_inputs { Some(tx_inputs) => tx_inputs, None => { // If no specific transaction inputs was provided, initialize an ad-hoc mockchain // to generate valid block header/MMR data - let mut mock_chain = MockChain::default(); + let mut builder = MockChain::builder(); for i in self.input_notes { - mock_chain.add_pending_note(OutputNote::Full(i)); + builder.add_output_note(OutputNote::Full(i)); } + let mut mock_chain = builder.build()?; mock_chain.prove_next_block().context("failed to prove first block")?; mock_chain.prove_next_block().context("failed to prove second block")?; @@ -247,49 +285,49 @@ impl TransactionContextBuilder { mock_chain.committed_notes().values().map(MockChainNote::id).collect(); mock_chain - .get_transaction_inputs( - self.account.clone(), - self.account_seed, - &input_note_ids, - &[], - ) + .get_transaction_inputs(&self.account, &input_note_ids, &[]) .context("failed to get transaction inputs from mock chain")? }, }; - let tx_args = TransactionArgs::new(AdviceMap::default(), self.foreign_account_inputs) - .with_note_args(self.note_args); + let mut tx_args = TransactionArgs::default().with_note_args(self.note_args); - let mut tx_args = if let Some(tx_script) = self.tx_script { + tx_args = if let Some(tx_script) = self.tx_script { tx_args.with_tx_script_and_args(tx_script, self.tx_script_args) } else { tx_args }; - tx_args = tx_args.with_auth_args(self.auth_args); - tx_args.extend_advice_inputs(self.advice_inputs.clone()); tx_args.extend_output_note_recipients(self.expected_output_notes.clone()); + for (public_key_commitment, message, signature) in self.signatures { + tx_args.add_signature(public_key_commitment, message, signature); + } + + tx_inputs.set_tx_args(tx_args); + let mast_store = { let mast_forest_store = TransactionMastStore::new(); mast_forest_store.load_account_code(tx_inputs.account().code()); - for acc_inputs in tx_args.foreign_account_inputs() { - mast_forest_store.insert(acc_inputs.code().mast()); + for (account, _) in self.foreign_account_inputs.values() { + mast_forest_store.insert(account.code().mast()); } mast_forest_store }; Ok(TransactionContext { + account: self.account, expected_output_notes: self.expected_output_notes, - tx_args, + foreign_account_inputs: self.foreign_account_inputs, tx_inputs, mast_store, authenticator: self.authenticator, - advice_inputs: self.advice_inputs, source_manager: self.source_manager, + is_lazy_loading_enabled: self.is_lazy_loading_enabled, + note_scripts: self.note_scripts, }) } } diff --git a/crates/miden-testing/src/tx_context/context.rs b/crates/miden-testing/src/tx_context/context.rs index a40fd183bd..d09ffc069c 100644 --- a/crates/miden-testing/src/tx_context/context.rs +++ b/crates/miden-testing/src/tx_context/context.rs @@ -1,16 +1,17 @@ use alloc::borrow::ToOwned; -use alloc::collections::BTreeSet; -use alloc::rc::Rc; +use alloc::collections::{BTreeMap, BTreeSet}; use alloc::sync::Arc; use alloc::vec::Vec; use miden_lib::transaction::TransactionKernel; -use miden_objects::account::{Account, AccountId}; +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::block::{BlockHeader, BlockNumber}; -use miden_objects::note::Note; +use miden_objects::asset::{AssetVaultKey, AssetWitness}; +use miden_objects::block::{AccountWitness, BlockHeader, BlockNumber}; +use miden_objects::note::{Note, NoteScript}; use miden_objects::transaction::{ + AccountInputs, ExecutedTransaction, InputNote, InputNotes, @@ -18,28 +19,22 @@ use miden_objects::transaction::{ TransactionArgs, TransactionInputs, }; -use miden_processor::{ - AdviceInputs, - ExecutionError, - FutureMaybeSend, - MastForest, - MastForestStore, - Process, - Word, -}; -use miden_tx::auth::BasicAuthenticator; +use miden_processor::fast::ExecutionOutput; +use miden_processor::{ExecutionError, FutureMaybeSend, MastForest, MastForestStore, Word}; +use miden_tx::auth::{BasicAuthenticator, UnreachableAuth}; use miden_tx::{ + AccountProcedureIndexMap, DataStore, DataStoreError, + ScriptMastForestStore, TransactionExecutor, TransactionExecutorError, + TransactionExecutorHost, TransactionMastStore, }; -use rand_chacha::ChaCha20Rng; -use crate::MockHost; use crate::executor::CodeExecutor; -use crate::tx_context::builder::MockAuthenticator; +use crate::mock_host::MockHost; // TRANSACTION CONTEXT // ================================================================================================ @@ -49,27 +44,30 @@ use crate::tx_context::builder::MockAuthenticator; /// It implements [`DataStore`], so transactions may be executed with /// [TransactionExecutor](miden_tx::TransactionExecutor) pub struct TransactionContext { + pub(super) account: Account, pub(super) expected_output_notes: Vec, - pub(super) tx_args: TransactionArgs, + pub(super) foreign_account_inputs: BTreeMap, pub(super) tx_inputs: TransactionInputs, pub(super) mast_store: TransactionMastStore, - pub(super) advice_inputs: AdviceInputs, - pub(super) authenticator: Option, + pub(super) authenticator: Option, pub(super) source_manager: Arc, + pub(super) is_lazy_loading_enabled: bool, + pub(super) note_scripts: BTreeMap, } impl TransactionContext { /// Executes arbitrary code within the context of a mocked transaction environment and returns - /// the resulting [Process]. + /// 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 [`MockHost`] which is loaded with - /// the procedures exposed by the transaction kernel, and also individual kernel functions (not - /// normally exposed). + /// 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). + /// [`Report`](miden_objects::assembly::diagnostics::Report) or use `?` with + /// [`miden_objects::assembly::diagnostics::Result`]. /// /// # Errors /// @@ -78,13 +76,9 @@ impl TransactionContext { /// # Panics /// /// - If the provided `code` is not a valid program. - pub fn execute_code(&self, code: &str) -> Result { - let (stack_inputs, advice_inputs) = TransactionKernel::prepare_inputs( - &self.tx_inputs, - &self.tx_args, - Some(self.advice_inputs.clone()), - ) - .expect("error initializing transaction inputs"); + 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"); // Virtual file name should be unique. let virtual_source_file = self.source_manager.load( @@ -100,28 +94,45 @@ impl TransactionContext { .assemble_program(virtual_source_file) .expect("code was not well formed"); - let mast_store = Rc::new(TransactionMastStore::new()); + // Load transaction kernel and the program into the mast forest in self. + // Note that native and foreign account's code are already loaded by the + // TransactionContextBuilder. + self.mast_store.insert(TransactionKernel::library().mast_forest().clone()); + self.mast_store.insert(program.mast_forest().clone()); - mast_store.insert(program.mast_forest().clone()); - mast_store.insert(TransactionKernel::library().mast_forest().clone()); - mast_store.load_account_code(self.account().code()); - for acc_inputs in self.tx_args.foreign_account_inputs() { - mast_store.load_account_code(acc_inputs.code()); - } + let account_procedure_idx_map = AccountProcedureIndexMap::new( + [self.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 exec_host = TransactionExecutorHost::<'_, '_, _, UnreachableAuth>::new( + &PartialAccount::from(self.account()), + self.tx_inputs().input_notes().clone(), + self, + ScriptMastForestStore::default(), + account_procedure_idx_map, + None, + ref_block, + self.source_manager(), + ); let advice_inputs = advice_inputs.into_advice_inputs(); - CodeExecutor::new( - MockHost::new( - self.tx_inputs.account().into(), - &advice_inputs, - mast_store, - self.tx_args.to_foreign_account_code_commitments(), - ) - .with_source_manager(self.source_manager()), - ) - .stack_inputs(stack_inputs) - .extend_advice_inputs(advice_inputs) - .execute_program(program) + + let mut mock_host = MockHost::new(exec_host); + if self.is_lazy_loading_enabled { + mock_host.enable_lazy_loading() + } + + CodeExecutor::new(mock_host) + .stack_inputs(stack_inputs) + .extend_advice_inputs(advice_inputs) + .execute_program(program) + .await } /// Executes the transaction through a [TransactionExecutor] @@ -141,20 +152,8 @@ impl TransactionContext { tx_executor.execute_transaction(account_id, block_num, notes, tx_args).await } - /// Executes the transaction through a [TransactionExecutor] - /// - /// TODO: This is a temporary workaround to avoid having to update each test to use tokio::test. - /// Eventually we should get rid of this method and use tokio::test + execute, but for the POC - /// stage this is easier. - pub fn execute_blocking(self) -> Result { - tokio::runtime::Builder::new_current_thread() - .build() - .unwrap() - .block_on(self.execute()) - } - pub fn account(&self) -> &Account { - self.tx_inputs.account() + &self.account } pub fn expected_output_notes(&self) -> &[Note] { @@ -162,7 +161,7 @@ impl TransactionContext { } pub fn tx_args(&self) -> &TransactionArgs { - &self.tx_args + self.tx_inputs.tx_args() } pub fn input_notes(&self) -> &InputNotes { @@ -170,14 +169,14 @@ impl TransactionContext { } pub fn set_tx_args(&mut self, tx_args: TransactionArgs) { - self.tx_args = tx_args; + self.tx_inputs.set_tx_args(tx_args); } pub fn tx_inputs(&self) -> &TransactionInputs { &self.tx_inputs } - pub fn authenticator(&self) -> Option<&BasicAuthenticator> { + pub fn authenticator(&self) -> Option<&BasicAuthenticator> { self.authenticator.as_ref() } @@ -192,12 +191,153 @@ impl DataStore for TransactionContext { &self, account_id: AccountId, _ref_blocks: BTreeSet, - ) -> impl FutureMaybeSend< - Result<(Account, Option, BlockHeader, PartialBlockchain), DataStoreError>, - > { + ) -> impl FutureMaybeSend> + { assert_eq!(account_id, self.account().id()); - let (account, seed, header, mmr, _) = self.tx_inputs.clone().into_parts(); - async move { Ok((account, seed, header, mmr)) } + assert_eq!(account_id, self.tx_inputs.account().id()); + + 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)) } + } + + fn get_foreign_account_inputs( + &self, + foreign_account_id: AccountId, + _ref_block: BlockNumber, + ) -> impl FutureMaybeSend> { + // Note that we cannot validate that the foreign account inputs are valid for the + // transaction's reference block. + async move { + let (foreign_account, account_witness) = + self.foreign_account_inputs.get(&foreign_account_id).ok_or_else(|| { + DataStoreError::other(format!( + "failed to find foreign account {foreign_account_id}" + )) + })?; + + Ok(AccountInputs::new( + PartialAccount::from(foreign_account), + account_witness.clone(), + )) + } + } + + fn get_vault_asset_witness( + &self, + account_id: AccountId, + vault_root: Word, + asset_key: AssetVaultKey, + ) -> impl FutureMaybeSend> { + async move { + 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)) + } else { + let (foreign_account, _witness) = self + .foreign_account_inputs + .iter() + .find_map( + |(id, account_inputs)| { + if account_id == *id { Some(account_inputs) } else { None } + }, + ) + .ok_or_else(|| { + DataStoreError::other(format!( + "failed to find foreign account {account_id} in foreign account inputs" + )) + })?; + + if foreign_account.vault().root() != vault_root { + return Err(DataStoreError::other(format!( + "foreign account {account_id} has vault root {} but {vault_root} was requested", + foreign_account.vault().root() + ))); + } + + Ok(foreign_account.vault().open(asset_key)) + } + } + } + + fn get_storage_map_witness( + &self, + account_id: AccountId, + map_root: Word, + map_key: Word, + ) -> impl FutureMaybeSend> { + async move { + if account_id == self.account().id() { + // Iterate the account storage to find the map with the requested root. + let storage_map = self + .account() + .storage() + .slots() + .iter() + .find_map(|slot| match slot { + StorageSlot::Map(storage_map) if storage_map.root() == map_root => { + Some(storage_map) + }, + _ => None, + }) + .ok_or_else(|| { + DataStoreError::other(format!( + "failed to find storage map with root {map_root} in account storage" + )) + })?; + + Ok(storage_map.open(&map_key)) + } else { + let (foreign_account, _witness) = self + .foreign_account_inputs + .iter() + .find_map( + |(id, account_inputs)| { + if account_id == *id { Some(account_inputs) } else { None } + }, + ) + .ok_or_else(|| { + DataStoreError::other(format!( + "failed to find foreign account {account_id} in foreign account inputs" + )) + })?; + + let map = foreign_account + .storage() + .slots() + .iter() + .find_map(|slot| match slot { + StorageSlot::Map(storage_map) if storage_map.root() == map_root => {Some(storage_map)}, + _ => None, + }) + .ok_or_else(|| { + DataStoreError::other(format!( + "failed to find storage map with root {map_root} in foreign account {account_id}" + )) + })?; + + Ok(map.open(&map_key)) + } + } + } + + 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)) + } } } @@ -206,3 +346,62 @@ impl MastForestStore for TransactionContext { self.mast_store.get(procedure_hash) } } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_objects::Felt; + use miden_objects::assembly::Assembler; + use miden_objects::note::NoteScript; + + use super::*; + use crate::TransactionContextBuilder; + + #[tokio::test] + async fn test_get_note_scripts() { + // Create two note scripts + let assembler1 = Assembler::default(); + let script1_code = "begin push.1 end"; + let program1 = assembler1 + .assemble_program(script1_code) + .expect("Failed to assemble note script 1"); + let note_script1 = NoteScript::new(program1); + let script_root1 = note_script1.root(); + + let assembler2 = Assembler::default(); + let script2_code = "begin push.2 push.3 add end"; + let program2 = assembler2 + .assemble_program(script2_code) + .expect("Failed to assemble note script 2"); + let note_script2 = NoteScript::new(program2); + let script_root2 = note_script2.root(); + + // Build a transaction context with both note scripts + let tx_context = TransactionContextBuilder::with_existing_mock_account() + .add_note_script(note_script1.clone()) + .add_note_script(note_script2.clone()) + .build() + .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"); + assert_eq!(retrieved_script1, note_script1); + + let retrieved_script2 = tx_context + .get_note_script(script_root2) + .await + .expect("Failed to get note script 2"); + assert_eq!(retrieved_script2, note_script2); + + // Fetching a non-existent one fails + 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(_)))); + } +} diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index 914d1d3cac..f6b5b1c7d3 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -2,12 +2,14 @@ use alloc::string::String; use alloc::vec::Vec; use miden_lib::testing::note::NoteBuilder; -use miden_lib::transaction::{TransactionKernel, memory}; +use miden_lib::transaction::TransactionKernel; use miden_objects::account::AccountId; use miden_objects::asset::Asset; -use miden_objects::note::Note; +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 rand::SeedableRng; use rand::rngs::SmallRng; @@ -63,23 +65,37 @@ macro_rules! assert_transaction_executor_error { }; } -// TEST UTILITIES +// HELPER NOTES // ================================================================================================ -pub fn input_note_data_ptr(note_idx: u32) -> memory::MemoryAddress { - memory::INPUT_NOTE_DATA_SECTION_OFFSET + note_idx * memory::NOTE_MEM_SIZE +/// Creates a public `P2ANY` note. +/// +/// A `P2ANY` note carries `assets` and a script that moves the assets into the executing account's +/// vault. +/// +/// The created note does not require authentication and can be consumed by any account. +pub fn create_public_p2any_note( + sender: AccountId, + assets: impl IntoIterator, +) -> Note { + let mut rng = RpoRandomCoin::new(Default::default()); + create_p2any_note(sender, NoteType::Public, assets, &mut rng) } -// HELPER NOTES -// ================================================================================================ - /// Creates a `P2ANY` note. /// /// A `P2ANY` note carries `assets` and a script that moves the assets into the executing account's /// vault. /// /// The created note does not require authentication and can be consumed by any account. -pub fn create_p2any_note(sender: AccountId, assets: &[Asset]) -> Note { +pub fn create_p2any_note( + sender: AccountId, + note_type: NoteType, + assets: impl IntoIterator, + rng: &mut RpoRandomCoin, +) -> Note { + let serial_number = rng.draw_word(); + let assets: Vec<_> = assets.into_iter().collect(); let mut code_body = String::new(); for i in 0..assets.len() { if i == 0 { @@ -88,7 +104,7 @@ pub fn create_p2any_note(sender: AccountId, assets: &[Asset]) -> Note { " # add first asset - padw dup.4 mem_loadw + padw dup.4 mem_loadw_be padw swapw padw padw swapdw call.wallet::receive_asset dropw movup.12 @@ -101,7 +117,7 @@ pub fn create_p2any_note(sender: AccountId, assets: &[Asset]) -> Note { # add next asset add.4 dup movdn.13 - padw movup.4 mem_loadw + padw movup.4 mem_loadw_be call.wallet::receive_asset dropw movup.12 # => [dest_ptr, pad(12)]", @@ -113,12 +129,12 @@ pub fn create_p2any_note(sender: AccountId, assets: &[Asset]) -> Note { let code = format!( " use.mock::account - use.miden::note + use.miden::active_note use.miden::contracts::wallets::basic->wallet begin # fetch pointer & number of assets - push.0 exec.note::get_assets # [num_assets, dest_ptr] + 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] @@ -132,6 +148,8 @@ pub fn create_p2any_note(sender: AccountId, assets: &[Asset]) -> Note { NoteBuilder::new(sender, SmallRng::from_seed([0; 32])) .add_assets(assets.iter().copied()) + .note_type(note_type) + .serial_number(serial_number) .code(code) .dynamically_linked_libraries(TransactionKernel::mock_libraries()) .build() @@ -142,8 +160,30 @@ pub fn create_p2any_note(sender: AccountId, assets: &[Asset]) -> Note { /// /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a /// parameter. -pub fn create_spawn_note(sender_id: AccountId, output_notes: Vec<&Note>) -> anyhow::Result { - let note_code = note_script_that_creates_notes(output_notes); +/// +/// # Errors +/// +/// Returns an error if: +/// - the sender account ID of the provided output notes is not consistent or does not match the +/// transaction's sender. +pub fn create_spawn_note<'note, I>( + output_notes: impl IntoIterator, +) -> anyhow::Result +where + I: ExactSizeIterator, +{ + let mut output_notes = output_notes.into_iter().peekable(); + if output_notes.len() == 0 { + anyhow::bail!("at least one output note is needed to create a SPAWN note"); + } + + let sender_id = output_notes + .peek() + .expect("at least one output note should be present") + .metadata() + .sender(); + + let note_code = note_script_that_creates_notes(sender_id, output_notes)?; let note = NoteBuilder::new(sender_id, SmallRng::from_os_rng()) .code(note_code) @@ -154,23 +194,44 @@ pub fn create_spawn_note(sender_id: AccountId, output_notes: Vec<&Note>) -> anyh } /// Returns the code for a note that creates all notes in `output_notes` -fn note_script_that_creates_notes(output_notes: Vec<&Note>) -> String { - let mut out = String::from("use.miden::tx\nuse.mock::account\n\nbegin\n"); +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"); + + for (idx, note) in output_notes.into_iter().enumerate() { + anyhow::ensure!( + note.metadata().sender() == sender_id, + "sender IDs of output notes passed to SPAWN note are inconsistent" + ); + + // Make sure that the transaction's native account matches the note sender. + out.push_str(&format!( + r#"exec.::miden::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] + push.{sender_suffix} assert_eq.err="sender ID suffix does not match native account ID's suffix" + # => [] + "#, + sender_prefix = sender_id.prefix().as_felt(), + sender_suffix = sender_id.suffix() + )); - for (idx, note) in output_notes.iter().enumerate() { if idx == 0 { out.push_str("padw padw\n"); } else { out.push_str("dropw dropw dropw\n"); } - let assets_str = prepare_assets(note.assets()); out.push_str(&format!( - " push.{recipient} - push.{hint} - push.{note_type} - push.{aux} - push.{tag} - call.tx::create_note\n", + " + push.{recipient} + push.{hint} + push.{note_type} + push.{aux} + push.{tag} + call.output_note::create\n", recipient = note.recipient().digest(), hint = Felt::from(note.metadata().execution_hint()), note_type = note.metadata().note_type() as u8, @@ -178,14 +239,16 @@ fn note_script_that_creates_notes(output_notes: Vec<&Note>) -> String { tag = note.metadata().tag(), )); + let assets_str = prepare_assets(note.assets()); for asset in assets_str { out.push_str(&format!( " push.{asset} - call.tx::add_asset_to_note\n", + call.::miden::contracts::wallets::basic::move_asset_to_note\n", )); } } out.push_str("repeat.5 dropw end\nend"); - out + + Ok(out) } diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index deea2e9281..4d20657bc5 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -1,16 +1,13 @@ -use assert_matches::assert_matches; +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_objects::account::{ - Account, - AccountBuilder, - AccountId, - AccountStorageMode, - AccountType, - AuthSecretKey, -}; +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::crypto::dsa::rpo_falcon512::{PublicKey, SecretKey}; use miden_objects::note::NoteType; use miden_objects::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, @@ -19,14 +16,12 @@ use miden_objects::testing::account_id::{ 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_testing::utils::create_spawn_note; use miden_testing::{Auth, MockChainBuilder, assert_transaction_executor_error}; +use miden_tx::TransactionExecutorError; use miden_tx::auth::{BasicAuthenticator, SigningInputs, TransactionAuthenticator}; -use miden_tx::{ - NoteConsumptionChecker, - NoteConsumptionStatus, - TransactionExecutor, - TransactionExecutorError, -}; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -34,33 +29,31 @@ use rand_chacha::ChaCha20Rng; // HELPER FUNCTIONS // ================================================================================================ -type MultisigTestSetup = (Vec, Vec, Vec>); +type MultisigTestSetup = (Vec, Vec, Vec); /// Sets up secret keys, public keys, and authenticators for multisig testing fn setup_keys_and_authenticators( num_approvers: usize, threshold: usize, ) -> anyhow::Result { - let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let seed: [u8; 32] = rand::random(); + let mut rng = ChaCha20Rng::from_seed(seed); let mut secret_keys = Vec::new(); let mut public_keys = Vec::new(); let mut authenticators = Vec::new(); for _ in 0..num_approvers { - let sec_key = SecretKey::with_rng(&mut rng); + let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); let pub_key = sec_key.public_key(); secret_keys.push(sec_key); public_keys.push(pub_key); } - // Create authenticators only for the signers we'll actually use - for i in 0..threshold { - let authenticator = BasicAuthenticator::::new_with_rng( - &[(public_keys[i].into(), AuthSecretKey::RpoFalcon512(secret_keys[i].clone()))], - rng.clone(), - ); + // Create authenticators for required signers + for secret_key in secret_keys.iter().take(threshold) { + let authenticator = BasicAuthenticator::new(core::slice::from_ref(secret_key)); authenticators.push(authenticator); } @@ -72,11 +65,12 @@ fn create_multisig_account( threshold: u32, public_keys: &[PublicKey], asset_amount: u64, + proc_threshold_map: Vec<(Word, u32)>, ) -> anyhow::Result { - let approvers: Vec<_> = public_keys.iter().map(|pk| (*pk).into()).collect(); + let approvers: Vec<_> = public_keys.iter().map(|pk| pk.to_commitment().into()).collect(); let multisig_account = AccountBuilder::new([0; 32]) - .with_auth_component(Auth::Multisig { threshold, approvers }) + .with_auth_component(Auth::Multisig { threshold, approvers, proc_threshold_map }) .with_component(BasicWallet) .account_type(AccountType::RegularAccountUpdatableCode) .storage_mode(AccountStorageMode::Public) @@ -106,14 +100,15 @@ async fn test_multisig_2_of_2_with_note_creation() -> anyhow::Result<()> { // Create multisig account let multisig_starting_balance = 10u64; - let mut multisig_account = create_multisig_account(2, &public_keys, multisig_starting_balance)?; + let mut multisig_account = + create_multisig_account(2, &public_keys, multisig_starting_balance, vec![])?; let output_note_asset = FungibleAsset::mock(0); let mut mock_chain_builder = MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); - // Create output note using add_p2id_note for spawn note + // Create output note for spawn note let output_note = mock_chain_builder.add_p2id_note( multisig_account.id(), ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), @@ -121,8 +116,8 @@ async fn test_multisig_2_of_2_with_note_creation() -> anyhow::Result<()> { NoteType::Public, )?; - // Create spawn note that will create the output note - let input_note = mock_chain_builder.add_spawn_note(multisig_account.id(), [&output_note])?; + // Create spawn note to generate the output note + let input_note = mock_chain_builder.add_spawn_note([&output_note])?; let mut mock_chain = mock_chain_builder.build().unwrap(); @@ -144,19 +139,19 @@ async fn test_multisig_2_of_2_with_note_creation() -> anyhow::Result<()> { let msg = tx_summary.as_ref().to_commitment(); let tx_summary = SigningInputs::TransactionSummary(tx_summary); - let sig_1 = authenticators[0].get_signature(public_keys[0].into(), &tx_summary).await?; - let sig_2 = authenticators[1].get_signature(public_keys[1].into(), &tx_summary).await?; - - // Populate advice map with signatures - let mut advice_map = AdviceMap::default(); - advice_map.insert(Hasher::merge(&[public_keys[0].into(), msg]), sig_1); - advice_map.insert(Hasher::merge(&[public_keys[1].into(), msg]), sig_2); + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary) + .await?; // Execute transaction with signatures - should succeed let tx_context_execute = mock_chain .build_tx_context(multisig_account.id(), &[input_note.id()], &[])? .extend_expected_output_notes(vec![OutputNote::Full(output_note)]) - .extend_advice_map(advice_map.iter().map(|(k, v)| (*k, v.to_vec()))) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) .auth_args(salt) .build()? .execute() @@ -191,7 +186,7 @@ async fn test_multisig_2_of_4_all_signer_combinations() -> anyhow::Result<()> { let (_secret_keys, public_keys, authenticators) = setup_keys_and_authenticators(4, 4)?; // Create multisig account with 4 approvers but threshold of 2 - let multisig_account = create_multisig_account(2, &public_keys, 10)?; + let multisig_account = create_multisig_account(2, &public_keys, 10, vec![])?; let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) .unwrap() @@ -227,22 +222,18 @@ async fn test_multisig_2_of_4_all_signer_combinations() -> anyhow::Result<()> { let tx_summary = SigningInputs::TransactionSummary(tx_summary); let sig_1 = authenticators[*signer1_idx] - .get_signature(public_keys[*signer1_idx].into(), &tx_summary) + .get_signature(public_keys[*signer1_idx].to_commitment(), &tx_summary) .await?; let sig_2 = authenticators[*signer2_idx] - .get_signature(public_keys[*signer2_idx].into(), &tx_summary) + .get_signature(public_keys[*signer2_idx].to_commitment(), &tx_summary) .await?; - // Populate advice map with signatures from the chosen signers - let mut advice_map = AdviceMap::default(); - advice_map.insert(Hasher::merge(&[public_keys[*signer1_idx].into(), msg]), sig_1); - advice_map.insert(Hasher::merge(&[public_keys[*signer2_idx].into(), msg]), sig_2); - // Execute transaction with signatures - should succeed for any combination let tx_context_execute = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .extend_advice_map(advice_map.iter().map(|(k, v)| (*k, v.to_vec()))) .auth_args(salt) + .add_signature(public_keys[*signer1_idx].to_commitment(), msg, sig_1) + .add_signature(public_keys[*signer2_idx].to_commitment(), msg, sig_2) .build()?; let executed_tx = tx_context_execute.execute().await.unwrap_or_else(|_| { @@ -273,7 +264,7 @@ async fn test_multisig_replay_protection() -> anyhow::Result<()> { let (_secret_keys, public_keys, authenticators) = setup_keys_and_authenticators(3, 2)?; // Create 2/3 multisig account - let multisig_account = create_multisig_account(2, &public_keys, 20)?; + let multisig_account = create_multisig_account(2, &public_keys, 20, vec![])?; let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) .unwrap() @@ -297,18 +288,18 @@ async fn test_multisig_replay_protection() -> anyhow::Result<()> { let msg = tx_summary.as_ref().to_commitment(); let tx_summary = SigningInputs::TransactionSummary(tx_summary); - let sig_1 = authenticators[0].get_signature(public_keys[0].into(), &tx_summary).await?; - let sig_2 = authenticators[1].get_signature(public_keys[1].into(), &tx_summary).await?; - - // Populate advice map with signatures - let mut advice_map = AdviceMap::default(); - advice_map.insert(Hasher::merge(&[public_keys[0].into(), msg]), sig_1); - advice_map.insert(Hasher::merge(&[public_keys[1].into(), msg]), sig_2); + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary) + .await?; // Execute transaction with signatures - should succeed (first execution) let tx_context_execute = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .extend_advice_map(advice_map.iter().map(|(k, v)| (*k, v.to_vec()))) + .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) + .add_signature(public_keys[1].to_commitment(), msg, sig_2.clone()) .auth_args(salt) .build()?; @@ -318,115 +309,745 @@ async fn test_multisig_replay_protection() -> anyhow::Result<()> { mock_chain.add_pending_executed_transaction(&executed_tx)?; mock_chain.prove_next_block()?; - // Now attempt to execute the same transaction again - should fail due to replay protection + // Attempt to execute the same transaction again - should fail due to replay protection let tx_context_replay = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? - .extend_advice_map(advice_map.iter().map(|(k, v)| (*k, v.to_vec()))) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) .auth_args(salt) .build()?; - // This should fail - due to replay protection + // This should fail due to replay protection let result = tx_context_replay.execute().await; assert_transaction_executor_error!(result, ERR_TX_ALREADY_EXECUTED); Ok(()) } +/// Tests multisig signer update functionality. +/// +/// This test verifies that a multisig account can: +/// 1. Execute a transaction script to update signers and threshold +/// 2. Create a second transaction signed by the new owners +/// 3. Properly handle multisig authentication with the updated signers +/// +/// **Roles:** +/// - 2 Original Approvers (multisig signers) +/// - 4 New Approvers (updated multisig signers) +/// - 1 Multisig Contract +/// - 1 Transaction Script calling multisig procedures #[tokio::test] -async fn test_check_note_consumability_multisig() -> anyhow::Result<()> { - // Setup keys and authenticators +async fn test_multisig_update_signers() -> anyhow::Result<()> { let (_secret_keys, public_keys, authenticators) = setup_keys_and_authenticators(2, 2)?; - // Create multisig account - let multisig_account = create_multisig_account(2, &public_keys, 10)?; + let multisig_account = create_multisig_account(2, &public_keys, 10, vec![])?; + + // SECTION 1: Execute a transaction script to update signers and threshold + // ================================================================================ let mut mock_chain_builder = MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); - let p2id_note = mock_chain_builder.add_p2id_note( - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), + let output_note_asset = FungibleAsset::mock(0); + + // Create output note for spawn note + let output_note = mock_chain_builder.add_p2id_note( multisig_account.id(), - &[FungibleAsset::mock(1)], + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), + &[output_note_asset], NoteType::Public, )?; - let mock_chain = mock_chain_builder.build().unwrap(); + let mut mock_chain = mock_chain_builder.clone().build().unwrap(); - let salt = Word::from([Felt::new(1); 4]); + let salt = Word::from([Felt::new(3); 4]); + + // Setup new signers + let mut advice_map = AdviceMap::default(); + let (_new_secret_keys, new_public_keys, _new_authenticators) = + setup_keys_and_authenticators(4, 4)?; + + let threshold = 3u64; + let num_of_approvers = 4u64; + + // Create vector with threshold config and public keys (4 field elements each) + let mut config_and_pubkeys_vector = Vec::new(); + config_and_pubkeys_vector.extend_from_slice(&[ + Felt::new(threshold), + Felt::new(num_of_approvers), + Felt::new(0), + Felt::new(0), + ]); + + // Add each public key to the vector + for public_key in new_public_keys.iter().rev() { + let key_word: Word = public_key.to_commitment().into(); + config_and_pubkeys_vector.extend_from_slice(key_word.as_elements()); + } + + // Hash the vector to create config hash + let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector); + + // Insert config and public keys into advice map + advice_map.insert(multisig_config_hash, config_and_pubkeys_vector); + + // Create a transaction script that calls the update_signers procedure + let tx_script_code = " + begin + call.::update_signers_and_threshold + end + "; + + let tx_script = ScriptBuilder::new(true) + .with_dynamically_linked_library(&rpo_falcon_512_multisig_library())? + .compile_tx_script(tx_script_code)?; + + let advice_inputs = AdviceInputs { + map: advice_map.clone(), + ..Default::default() + }; + + // Pass the MULTISIG_CONFIG_HASH as the tx_script_args + let tx_script_args: Word = multisig_config_hash; - // get the transaction context without signatures - let tx_context_without_signatures = mock_chain - .build_tx_context(multisig_account.id(), &[p2id_note.id()], &[])? + // Execute transaction without signatures first to get tx summary + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script.clone()) + .tx_script_args(tx_script_args) + .extend_advice_inputs(advice_inputs.clone()) .auth_args(salt) .build()?; - let block_ref = tx_context_without_signatures.tx_inputs().block_header().block_num(); - let tx_args = tx_context_without_signatures.tx_args(); - let tx_executor = TransactionExecutor::<'_, '_, _, BasicAuthenticator>::new( - &tx_context_without_signatures, - ); + 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:?}"), + }; + + // Get signatures from both approvers + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary = SigningInputs::TransactionSummary(tx_summary); - let notes_checker = NoteConsumptionChecker::new(&tx_executor); - - // this check should return `UnconsumableWithoutAuthorization` variant: the note is consumable, - // but authentication is failing - let unconsumable_without_authorization = notes_checker - .can_consume( - multisig_account.id(), - block_ref, - miden_objects::transaction::InputNote::Unauthenticated { note: p2id_note.clone() }, - tx_args.clone(), - ) + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary) .await?; - assert_matches!( - unconsumable_without_authorization, - NoteConsumptionStatus::UnconsumableWithoutAuthorization + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary) + .await?; + + // Execute transaction with signatures - should succeed + let update_approvers_tx = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script) + .tx_script_args(multisig_config_hash) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) + .auth_args(salt) + .extend_advice_inputs(advice_inputs) + .build()? + .execute() + .await + .unwrap(); + + // Verify the transaction executed successfully + assert_eq!(update_approvers_tx.account_delta().nonce_delta(), Felt::new(1)); + + mock_chain.add_pending_executed_transaction(&update_approvers_tx)?; + mock_chain.prove_next_block()?; + + // Apply the delta to get the updated account with new signers + let mut updated_multisig_account = multisig_account.clone(); + updated_multisig_account.apply_delta(update_approvers_tx.account_delta())?; + + // 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 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(); + + assert_eq!( + threshold_config_storage[0], + Felt::new(threshold), + "Threshold was not updated correctly" + ); + assert_eq!( + threshold_config_storage[1], + Felt::new(num_of_approvers), + "Num approvers was not updated correctly" ); - // execute the transaction to get the summary - let tx_summary = match tx_context_without_signatures.execute().await.unwrap_err() { + // Extract public keys using the interface function + let extracted_pub_keys = get_public_keys_from_account(&updated_multisig_account); + + // Verify that we have the expected number of public keys (4 new ones) + assert_eq!( + extracted_pub_keys.len(), + 4, + "get_public_keys_from_account should return 4 public keys after update" + ); + + // Verify that the extracted public keys match the new ones we set + for (i, expected_key) in new_public_keys.iter().enumerate() { + let expected_word: Word = expected_key.to_commitment().into(); + + // Find the matching key in extracted keys (order might be different) + let found_key = extracted_pub_keys.iter().find(|&key| *key == expected_word); + + assert!( + found_key.is_some(), + "Public key {} not found in extracted keys: expected {:?}, got {:?}", + i, + expected_word, + extracted_pub_keys + ); + } + + // SECTION 2: Create a second transaction signed by the new owners + // ================================================================================ + + // Now test creating a note with the new signers + // Setup authenticators for the new signers (we need 3 out of 4 for threshold 3) + let mut new_authenticators = Vec::new(); + for secret_key in _new_secret_keys.iter().take(3) { + let authenticator = BasicAuthenticator::new(core::slice::from_ref(secret_key)); + new_authenticators.push(authenticator); + } + + // Create a new output note for the second transaction with new signers + let output_note_new = create_p2id_note( + updated_multisig_account.id(), + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), + vec![output_note_asset], + NoteType::Public, + Default::default(), + &mut RpoRandomCoin::new(Word::empty()), + )?; + + // Create a new spawn note for the second transaction + let input_note_new = create_spawn_note([&output_note_new])?; + + let salt_new = Word::from([Felt::new(4); 4]); + + // Build the new mock chain with the updated account and notes + let mut new_mock_chain_builder = + MockChainBuilder::with_accounts([updated_multisig_account.clone()]).unwrap(); + new_mock_chain_builder.add_output_note(OutputNote::Full(input_note_new.clone())); + let new_mock_chain = new_mock_chain_builder.build().unwrap(); + + // Execute transaction without signatures first to get tx summary + let tx_context_init_new = new_mock_chain + .build_tx_context(updated_multisig_account.id(), &[input_note_new.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .auth_args(salt_new) + .build()?; + + let tx_summary_new = match tx_context_init_new.execute().await.unwrap_err() { TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, error => panic!("expected abort with tx effects: {error:?}"), }; - // Get signatures from both approvers + // Get signatures from 3 of the 4 new approvers (threshold is 3) + let msg_new = tx_summary_new.as_ref().to_commitment(); + let tx_summary_new = SigningInputs::TransactionSummary(tx_summary_new); + + let sig_1_new = new_authenticators[0] + .get_signature(new_public_keys[0].to_commitment(), &tx_summary_new) + .await?; + let sig_2_new = new_authenticators[1] + .get_signature(new_public_keys[1].to_commitment(), &tx_summary_new) + .await?; + let sig_3_new = new_authenticators[2] + .get_signature(new_public_keys[2].to_commitment(), &tx_summary_new) + .await?; + + // SECTION 3: Properly handle multisig authentication with the updated signers + // ================================================================================ + + // Execute transaction with new signatures - should succeed + let tx_context_execute_new = new_mock_chain + .build_tx_context(updated_multisig_account.id(), &[input_note_new.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note_new)]) + .add_signature(new_public_keys[0].to_commitment(), msg_new, sig_1_new) + .add_signature(new_public_keys[1].to_commitment(), msg_new, sig_2_new) + .add_signature(new_public_keys[2].to_commitment(), msg_new, sig_3_new) + .auth_args(salt_new) + .build()? + .execute() + .await?; + + // Verify the transaction executed successfully with new signers + assert_eq!(tx_context_execute_new.account_delta().nonce_delta(), Felt::new(1)); + + Ok(()) +} + +/// Tests multisig signer update functionality with owner removal. +/// +/// This test verifies that a multisig account can: +/// 1. Start with 5 owners and threshold 4 +/// 2. Execute a transaction to remove 3 owners (updating to 2 owners) +/// 3. Verify that all removed owners' storage slots are properly cleared +/// +/// **Roles:** +/// - 5 Original Approvers (multisig signers, threshold 4) +/// - 2 Updated Approvers (after removing 3 owners) +/// - 1 Multisig Contract +/// - 1 Transaction Script calling multisig procedures +#[tokio::test] +async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { + // Setup 5 original owners with threshold 4 + let (_secret_keys, public_keys, authenticators) = setup_keys_and_authenticators(5, 5)?; + let multisig_account = create_multisig_account(4, &public_keys, 10, vec![])?; + + // Build mock chain + let mock_chain_builder = MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); + let mut mock_chain = mock_chain_builder.build().unwrap(); + + // Setup new signers (remove the last 3 owners, keeping first 2) + let new_public_keys = &public_keys[0..2]; + let threshold = 1u64; + let num_of_approvers = 2u64; + + // Create multisig config vector + let mut config_and_pubkeys_vector = + vec![Felt::new(threshold), Felt::new(num_of_approvers), Felt::new(0), Felt::new(0)]; + + // Add public keys in reverse order + for public_key in new_public_keys.iter().rev() { + let key_word: Word = public_key.to_commitment().into(); + config_and_pubkeys_vector.extend_from_slice(key_word.as_elements()); + } + + // Create config hash and advice map + let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector); + let mut advice_map = AdviceMap::default(); + 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 advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; + + let salt = Word::from([Felt::new(3); 4]); + + // Execute without signatures to get tx summary + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script.clone()) + .tx_script_args(multisig_config_hash) + .extend_advice_inputs(advice_inputs.clone()) + .auth_args(salt) + .build()?; + + 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:?}"), + }; + + // Get signatures from 4 of the 5 original approvers (threshold is 4) let msg = tx_summary.as_ref().to_commitment(); let tx_summary = SigningInputs::TransactionSummary(tx_summary); - let sig_1 = authenticators[0].get_signature(public_keys[0].into(), &tx_summary).await?; - let sig_2 = authenticators[1].get_signature(public_keys[1].into(), &tx_summary).await?; + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary) + .await?; + let sig_3 = authenticators[2] + .get_signature(public_keys[2].to_commitment(), &tx_summary) + .await?; + let sig_4 = authenticators[3] + .get_signature(public_keys[3].to_commitment(), &tx_summary) + .await?; - // Populate advice map with signatures + // Execute with signatures + let update_approvers_tx = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script) + .tx_script_args(multisig_config_hash) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) + .add_signature(public_keys[2].to_commitment(), msg, sig_3) + .add_signature(public_keys[3].to_commitment(), msg, sig_4) + .auth_args(salt) + .extend_advice_inputs(advice_inputs) + .build()? + .execute() + .await + .unwrap(); + + // Verify transaction success + assert_eq!(update_approvers_tx.account_delta().nonce_delta(), Felt::new(1)); + + mock_chain.add_pending_executed_transaction(&update_approvers_tx)?; + mock_chain.prove_next_block()?; + + // Apply delta to get updated account + let mut updated_multisig_account = multisig_account.clone(); + updated_multisig_account.apply_delta(update_approvers_tx.account_delta())?; + + // 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 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(); + 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"); + + // Verify extracted public keys + let extracted_pub_keys = get_public_keys_from_account(&updated_multisig_account); + assert_eq!(extracted_pub_keys.len(), 2, "Should have 2 public keys after update"); + + for expected_key in new_public_keys.iter() { + let expected_word: Word = expected_key.to_commitment().into(); + assert!( + extracted_pub_keys.contains(&expected_word), + "Public key not found in extracted keys" + ); + } + + // Verify removed owners' slots are empty (indices 2, 3, and 4 should be cleared) + 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(); + assert_eq!( + removed_owner_slot, + Word::empty(), + "Removed owner's slot at index {} should be empty", + removed_idx + ); + } + + // Verify only 2 non-empty keys remain (at indices 0 and 1) + 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(); + + if storage_item != Word::empty() { + non_empty_count += 1; + assert!(i < 2, "Found non-empty key at index {} which should be removed", i); + + let expected_word: Word = new_public_keys.get(i).unwrap().to_commitment().into(); + assert_eq!(storage_item, expected_word, "Key at index {} doesn't match", i); + } + } + assert_eq!( + non_empty_count, 2, + "Should have exactly 2 non-empty keys after removing 3 owners" + ); + + Ok(()) +} + +/// Tests that newly added approvers cannot sign transactions before the signer update is executed. +/// +/// This is a regression test to ensure that unauthorized parties cannot add their own public keys +/// to the multisig configuration and immediately use them to sign transactions before +/// the current approvers have validated and executed the signer update. +/// +/// **Test Flow:** +/// 1. Create a multisig account with 2 original approvers +/// 2. Prepare a signer update transaction with new approvers +/// 3. Try to sign the transaction with the NEW approvers (should fail) +/// 4. Verify that only the CURRENT approvers can sign the update transaction +#[tokio::test] +async fn test_multisig_new_approvers_cannot_sign_before_update() -> anyhow::Result<()> { + // SECTION 1: Create a multisig account with 2 original approvers + // ================================================================================ + + let (_secret_keys, public_keys, _authenticators) = setup_keys_and_authenticators(2, 2)?; + + let multisig_account = create_multisig_account(2, &public_keys, 10, vec![])?; + + let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) + .unwrap() + .build() + .unwrap(); + + let salt = Word::from([Felt::new(5); 4]); + + // SECTION 2: Prepare a signer update transaction with new approvers + // ================================================================================ + + // Get the multisig library + + // Setup new signers (these should NOT be able to sign the update transaction) let mut advice_map = AdviceMap::default(); - advice_map.insert(Hasher::merge(&[public_keys[0].into(), msg]), sig_1); - advice_map.insert(Hasher::merge(&[public_keys[1].into(), msg]), sig_2); - - // get the transaction context with signatures - let tx_context_with_signatures = mock_chain - .build_tx_context(multisig_account.id(), &[p2id_note.id()], &[])? - .extend_expected_output_notes(vec![OutputNote::Full(p2id_note)]) - .extend_advice_map(advice_map.iter().map(|(k, v)| (*k, v.to_vec()))) + let (_new_secret_keys, new_public_keys, new_authenticators) = + setup_keys_and_authenticators(4, 4)?; + + let threshold = 3u64; + let num_of_approvers = 4u64; + + // Create vector with threshold config and public keys (4 field elements each) + let mut config_and_pubkeys_vector = Vec::new(); + config_and_pubkeys_vector.extend_from_slice(&[ + Felt::new(threshold), + Felt::new(num_of_approvers), + Felt::new(0), + Felt::new(0), + ]); + + // Add each public key to the vector + for public_key in new_public_keys.iter().rev() { + let key_word: Word = public_key.to_commitment().into(); + config_and_pubkeys_vector.extend_from_slice(key_word.as_elements()); + } + + // Hash the vector to create config hash + let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector); + + // Insert config and public keys into advice map + advice_map.insert(multisig_config_hash, config_and_pubkeys_vector); + + // Create a transaction script that calls the update_signers procedure + let tx_script_code = " + begin + call.::update_signers_and_threshold + end + "; + + let tx_script = ScriptBuilder::new(true) + .with_dynamically_linked_library(&rpo_falcon_512_multisig_library())? + .compile_tx_script(tx_script_code)?; + + let advice_inputs = AdviceInputs { + map: advice_map.clone(), + ..Default::default() + }; + + // Pass the MULTISIG_CONFIG_HASH as the tx_script_args + let tx_script_args: Word = multisig_config_hash; + + // Execute transaction without signatures first to get tx summary + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script.clone()) + .tx_script_args(tx_script_args) + .extend_advice_inputs(advice_inputs.clone()) .auth_args(salt) .build()?; - let block_num = tx_context_with_signatures.tx_inputs().block_header().block_num(); - let notes = tx_context_with_signatures.tx_inputs().input_notes().clone(); - let tx_args = tx_context_with_signatures.tx_args().clone(); + 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:?}"), + }; - let mut tx_executor = TransactionExecutor::new(&tx_context_with_signatures) - .with_source_manager(tx_context_with_signatures.source_manager()); - if let Some(authenticator) = tx_context_with_signatures.authenticator() { - tx_executor = tx_executor.with_authenticator(authenticator); - } + // SECTION 3: Try to sign the transaction with the NEW approvers (should fail) + // ================================================================================ + + // Get signatures from the NEW approvers (these should NOT work) + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary.clone()); + + let new_sig_1 = new_authenticators[0] + .get_signature(new_public_keys[0].to_commitment(), &tx_summary_signing) + .await?; + let new_sig_2 = new_authenticators[1] + .get_signature(new_public_keys[1].to_commitment(), &tx_summary_signing) + .await?; + + // Try to execute transaction with NEW signatures - should FAIL + let tx_context_with_new_sigs = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script.clone()) + .tx_script_args(multisig_config_hash) + .add_signature(new_public_keys[0].to_commitment(), msg, new_sig_1) + .add_signature(new_public_keys[1].to_commitment(), msg, new_sig_2) + .auth_args(salt) + .extend_advice_inputs(advice_inputs.clone()) + .build()?; + + // SECTION 4: Verify that only the CURRENT approvers can sign the update transaction + // ================================================================================ + + // Should fail - new approvers not yet authorized + let result = tx_context_with_new_sigs.execute().await; + + // Assert that the transaction fails as expected + assert!( + result.is_err(), + "Transaction should fail when signed by unauthorized new approvers" + ); + + Ok(()) +} + +/// Tests that 1-of-2 approvers can consume a note but 2-of-2 are required to send a note. +/// +/// This test verifies that a multisig account with 2 approvers and threshold 2, but a procedure +/// threshold of 1 for note consumption, can: +/// 1. Consume a note when only one approver signs the transaction +/// 2. Send a note only when both approvers sign the transaction (default threshold) +#[tokio::test] +async fn test_multisig_proc_threshold_overrides() -> anyhow::Result<()> { + // Setup keys and authenticators + let (_secret_keys, public_keys, authenticators) = setup_keys_and_authenticators(2, 2)?; + + let proc_threshold_map = vec![(BasicWallet::receive_asset_digest(), 1)]; + + // Create multisig account + let multisig_starting_balance = 10u64; + let mut multisig_account = + create_multisig_account(2, &public_keys, multisig_starting_balance, proc_threshold_map)?; + + // SECTION 1: Test note consumption with 1 signature + // ================================================================================ + + // 1. create a mock note from some random account + let mut mock_chain_builder = + MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); + + let note = mock_chain_builder.add_p2id_note( + multisig_account.id(), + multisig_account.id(), + &[FungibleAsset::mock(1)], + NoteType::Public, + )?; - let notes_checker = NoteConsumptionChecker::new(&tx_executor); + let mut mock_chain = mock_chain_builder.build()?; - // this check should return `Consumable` variant: we provided the signatures, so the transaction - // should execute successfully. - let consumable_with_authorization = notes_checker - .can_consume(multisig_account.id(), block_num, notes.get_note(0).clone(), tx_args) + // 2. consume without signatures + let salt = Word::from([Felt::new(1); 4]); + let tx_context = mock_chain + .build_tx_context(multisig_account.id(), &[note.id()], &[])? + .auth_args(salt) + .build()?; + + let tx_summary = match tx_context.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_summary) => tx_summary, + error => panic!("expected abort with tx summary: {error:?}"), + }; + + // 3. get signature from one approver + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary.clone()); + let sig = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary_signing) .await?; - assert_matches!(consumable_with_authorization, NoteConsumptionStatus::Consumable); + + // 4. execute with signature + let tx_result = mock_chain + .build_tx_context(multisig_account.id(), &[note.id()], &[])? + .add_signature(public_keys[0].to_commitment(), msg, sig) + .auth_args(salt) + .build()? + .execute() + .await; + + assert!(tx_result.is_ok(), "Note consumption with 1 signature should succeed"); + + // Apply the transaction to the account + multisig_account.apply_delta(tx_result.as_ref().unwrap().account_delta())?; + mock_chain.add_pending_executed_transaction(&tx_result.unwrap())?; + mock_chain.prove_next_block()?; + + // SECTION 2: Test note sending requires 2 signatures + // ================================================================================ + + let salt2 = Word::from([Felt::new(2); 4]); + + // Create output note to send 5 units from the account + let output_note = create_p2id_note( + multisig_account.id(), + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), + vec![FungibleAsset::mock(5)], + NoteType::Public, + 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, + )?; + + // Execute transaction without signatures to get tx summary + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .tx_script(send_note_transaction_script.clone()) + .auth_args(salt2) + .build()?; + + let tx_summary2 = match tx_context_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => panic!("expected abort with tx effects: {error:?}"), + }; + // Get signature from only ONE approver + let msg2 = tx_summary2.as_ref().to_commitment(); + let tx_summary2_signing = SigningInputs::TransactionSummary(tx_summary2.clone()); + + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary2_signing) + .await?; + + // Try to execute with only 1 signature - should FAIL + let tx_context_one_sig = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .add_signature(public_keys[0].to_commitment(), msg2, sig_1) + .tx_script(send_note_transaction_script.clone()) + .auth_args(salt2) + .build()?; + + let result = tx_context_one_sig.execute().await; + match result { + Err(TransactionExecutorError::Unauthorized(_)) => { + // Expected: transaction should fail with insufficient signatures + }, + _ => panic!( + "Transaction should fail with Unauthorized error when only 1 signature provided for note sending" + ), + } + + // Now get signatures from BOTH approvers + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary2_signing) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary2_signing) + .await?; + + // Execute with 2 signatures - should SUCCEED + let result = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note)]) + .add_signature(public_keys[0].to_commitment(), msg2, sig_1) + .add_signature(public_keys[1].to_commitment(), msg2, sig_2) + .auth_args(salt2) + .tx_script(send_note_transaction_script) + .build()? + .execute() + .await; + + assert!(result.is_ok(), "Transaction should succeed with 2 signatures for note sending"); + + // Apply the transaction to the account + multisig_account.apply_delta(result.as_ref().unwrap().account_delta())?; + mock_chain.add_pending_executed_transaction(&result.unwrap())?; + mock_chain.prove_next_block()?; + + assert_eq!(multisig_account.vault().get_balance(FungibleAsset::mock_issuer())?, 6); Ok(()) } diff --git a/crates/miden-testing/tests/auth/rpo_falcon_acl.rs b/crates/miden-testing/tests/auth/rpo_falcon_acl.rs index 9cf12bffdd..2b8d51fe19 100644 --- a/crates/miden-testing/tests/auth/rpo_falcon_acl.rs +++ b/crates/miden-testing/tests/auth/rpo_falcon_acl.rs @@ -5,14 +5,14 @@ use miden_lib::testing::account_component::MockAccountComponent; use miden_lib::testing::note::NoteBuilder; use miden_lib::utils::ScriptBuilder; use miden_objects::account::{ + Account, AccountBuilder, AccountComponent, - AccountId, AccountStorage, AccountStorageMode, AccountType, }; -use miden_objects::testing::account_id::ACCOUNT_ID_SENDER; +use miden_objects::note::Note; use miden_objects::transaction::OutputNote; use miden_objects::{Felt, FieldElement, Word}; use miden_testing::{Auth, MockChain}; @@ -37,7 +37,7 @@ const TX_SCRIPT_NO_TRIGGER: &str = r#" fn setup_rpo_falcon_acl_test( allow_unauthorized_output_notes: bool, allow_unauthorized_input_notes: bool, -) -> anyhow::Result<(miden_objects::account::Account, MockChain, miden_objects::note::Note)> { +) -> anyhow::Result<(Account, MockChain, Note)> { let component: AccountComponent = MockAccountComponent::with_slots(AccountStorage::mock_storage_slots()).into(); @@ -65,20 +65,19 @@ fn setup_rpo_falcon_acl_test( let mut builder = MockChain::builder(); builder.add_account(account.clone())?; - let mock_chain = builder.build()?; - // Create a mock note to consume (needed to make the transaction non-empty) - let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER)?; - let note = NoteBuilder::new(sender_id, &mut rand::rng()) + let note = NoteBuilder::new(account.id(), &mut rand::rng()) .build() .expect("failed to create mock note"); + builder.add_output_note(OutputNote::Full(note.clone())); + let mock_chain = builder.build()?; Ok((account, mock_chain, note)) } -#[test] -fn test_rpo_falcon_acl() -> anyhow::Result<()> { - let (account, mut mock_chain, note) = setup_rpo_falcon_acl_test(false, true)?; +#[tokio::test] +async fn test_rpo_falcon_acl() -> anyhow::Result<()> { + let (account, mock_chain, note) = setup_rpo_falcon_acl_test(false, true)?; // We need to get the authenticator separately for this test let component: AccountComponent = @@ -99,9 +98,6 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> { } .build_component(); - mock_chain.add_pending_note(OutputNote::Full(note.clone())); - mock_chain.prove_next_block()?; - let tx_script_with_trigger_1 = r#" use.mock::account @@ -139,7 +135,8 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> { .build()?; tx_context_with_auth_1 - .execute_blocking() + .execute() + .await .expect("trigger 1 with auth should succeed"); // Test 2: Transaction WITH authenticator calling trigger procedure 2 (should succeed) @@ -150,7 +147,8 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> { .build()?; tx_context_with_auth_2 - .execute_blocking() + .execute() + .await .expect("trigger 2 with auth should succeed"); // Test 3: Transaction WITHOUT authenticator calling trigger procedure (should fail) @@ -160,7 +158,7 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> { .tx_script(tx_script_trigger_1) .build()?; - let executed_tx_no_auth = tx_context_no_auth.execute_blocking(); + let executed_tx_no_auth = tx_context_no_auth.execute().await; assert_matches!(executed_tx_no_auth, Err(TransactionExecutorError::MissingAuthenticator)); @@ -172,7 +170,8 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> { .build()?; let executed = tx_context_no_trigger - .execute_blocking() + .execute() + .await .expect("no trigger, no auth should succeed"); assert_eq!( executed.account_delta().nonce_delta(), @@ -183,8 +182,8 @@ fn test_rpo_falcon_acl() -> anyhow::Result<()> { Ok(()) } -#[test] -fn test_rpo_falcon_acl_with_allow_unauthorized_output_notes() -> anyhow::Result<()> { +#[tokio::test] +async fn test_rpo_falcon_acl_with_allow_unauthorized_output_notes() -> anyhow::Result<()> { let (account, mock_chain, note) = setup_rpo_falcon_acl_test(true, true)?; // Verify the storage layout includes both authorization flags @@ -208,7 +207,8 @@ fn test_rpo_falcon_acl_with_allow_unauthorized_output_notes() -> anyhow::Result< .build()?; let executed = tx_context_no_trigger - .execute_blocking() + .execute() + .await .expect("no trigger, no auth should succeed"); assert_eq!( executed.account_delta().nonce_delta(), @@ -219,8 +219,8 @@ fn test_rpo_falcon_acl_with_allow_unauthorized_output_notes() -> anyhow::Result< Ok(()) } -#[test] -fn test_rpo_falcon_acl_with_disallow_unauthorized_input_notes() -> anyhow::Result<()> { +#[tokio::test] +async fn test_rpo_falcon_acl_with_disallow_unauthorized_input_notes() -> anyhow::Result<()> { let (account, mock_chain, note) = setup_rpo_falcon_acl_test(true, false)?; // Verify the storage layout includes both flags @@ -243,7 +243,7 @@ fn test_rpo_falcon_acl_with_disallow_unauthorized_input_notes() -> anyhow::Resul .tx_script(tx_script_no_trigger) .build()?; - let executed_tx_no_auth = tx_context_no_auth.execute_blocking(); + let executed_tx_no_auth = tx_context_no_auth.execute().await; // This should fail with MissingAuthenticator error because input notes are being consumed // and allow_unauthorized_input_notes is false diff --git a/crates/miden-testing/tests/lib.rs b/crates/miden-testing/tests/lib.rs index f2b568159f..c5992e03d5 100644 --- a/crates/miden-testing/tests/lib.rs +++ b/crates/miden-testing/tests/lib.rs @@ -32,7 +32,7 @@ pub fn prove_and_verify_transaction( let proof_options = ProvingOptions::default(); let prover = LocalTransactionProver::new(proof_options); - let proven_transaction = prover.prove(executed_transaction.into()).unwrap(); + let proven_transaction = prover.prove(executed_transaction).unwrap(); assert_eq!(proven_transaction.id(), executed_transaction_id); diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 78c29c3d31..6a12481a59 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -1,37 +1,59 @@ extern crate alloc; -use miden_lib::account::faucets::FungibleFaucetExt; +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::WellKnownNote; +use miden_lib::testing::note::NoteBuilder; use miden_lib::utils::ScriptBuilder; +use miden_objects::account::{ + Account, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, +}; +use miden_objects::assembly::DefaultSourceManager; use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::note::{NoteAssets, NoteExecutionHint, NoteId, NoteMetadata, NoteTag, NoteType}; -use miden_objects::transaction::OutputNote; +use miden_objects::note::{ + Note, + NoteAssets, + NoteExecutionHint, + NoteExecutionMode, + NoteId, + NoteInputs, + NoteMetadata, + NoteRecipient, + 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_testing::{Auth, MockChain, assert_transaction_executor_error}; +use crate::scripts::swap::create_p2id_note_exact; use crate::{get_note_with_fungible_asset_and_script, prove_and_verify_transaction}; -// TESTS MINT FUNGIBLE ASSET +// Shared test utilities for faucet tests // ================================================================================================ -#[test] -fn prove_faucet_contract_mint_fungible_asset_succeeds() -> anyhow::Result<()> { - // CONSTRUCT AND EXECUTE TX (Success) - // -------------------------------------------------------------------------------------------- - let mut builder = MockChain::builder(); - let faucet = builder.add_existing_faucet(Auth::BasicAuth, "TST", 200, None)?; - let mock_chain = builder.build()?; - - let recipient = Word::from([0, 1, 2, 3u32]); - let tag = NoteTag::for_local_use_case(0, 0).unwrap(); - let aux = Felt::new(27); - let note_execution_hint = NoteExecutionHint::on_block_slot(5, 6, 7); - let note_type = NoteType::Private; - let amount = Felt::new(100); - - tag.validate(note_type).expect("note tag should support private notes"); +/// Common test parameters for faucet tests +pub struct FaucetTestParams { + pub recipient: Word, + pub tag: NoteTag, + pub aux: Felt, + pub note_execution_hint: NoteExecutionHint, + pub note_type: NoteType, + pub amount: Felt, +} - let tx_script_code = format!( +/// Creates minting script code for fungible asset distribution +pub fn create_mint_script_code(params: &FaucetTestParams) -> String { + format!( " begin # pad the stack before call @@ -52,45 +74,98 @@ fn prove_faucet_contract_mint_fungible_asset_succeeds() -> anyhow::Result<()> { dropw dropw dropw dropw end ", - note_type = note_type as u8, - recipient = recipient, - aux = aux, - tag = u32::from(tag), - note_execution_hint = Felt::from(note_execution_hint) - ); + 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, + ) +} - let tx_script = ScriptBuilder::default().compile_tx_script(tx_script_code)?; +/// Executes a minting transaction with the given faucet and parameters +pub async fn execute_mint_transaction( + mock_chain: &mut MockChain, + faucet: Account, + params: &FaucetTestParams, +) -> 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()) + .compile_tx_script(tx_script_code)?; let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[])? + .build_tx_context(faucet, &[], &[])? .tx_script(tx_script) + .with_source_manager(source_manager) .build()?; - let executed_transaction = tx_context.execute_blocking()?; - - prove_and_verify_transaction(executed_transaction.clone())?; + Ok(tx_context.execute().await?) +} - let fungible_asset: Asset = FungibleAsset::new(faucet.id(), amount.into())?.into(); +/// Verifies minted output note matches expectations +pub fn verify_minted_output_note( + executed_transaction: &ExecutedTransaction, + faucet: &Account, + params: &FaucetTestParams, +) -> anyhow::Result<()> { + let fungible_asset: Asset = FungibleAsset::new(faucet.id(), params.amount.into())?.into(); let output_note = executed_transaction.output_notes().get_note(0).clone(); - let assets = NoteAssets::new(vec![fungible_asset])?; - let id = NoteId::new(recipient, assets.commitment()); + let id = NoteId::new(params.recipient, assets.commitment()); assert_eq!(output_note.id(), id); assert_eq!( output_note.metadata(), - &NoteMetadata::new(faucet.id(), NoteType::Private, tag, note_execution_hint, aux)? + &NoteMetadata::new( + faucet.id(), + params.note_type, + params.tag, + params.note_execution_hint, + params.aux + )? ); Ok(()) } -#[test] -fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyhow::Result<()> { +// TESTS MINT FUNGIBLE ASSET +// ================================================================================================ + +/// Tests that minting assets on an existing faucet succeeds. +#[tokio::test] +async fn minting_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth, "TST", 200, None)?; + let mut mock_chain = builder.build()?; + + 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), + 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)?; + + Ok(()) +} + +#[tokio::test] +async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyhow::Result<()> { // CONSTRUCT AND EXECUTE TX (Failure) // -------------------------------------------------------------------------------------------- let mut builder = MockChain::builder(); - let faucet = builder.add_existing_faucet(Auth::BasicAuth, "TST", 200, None)?; + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth, "TST", 200, None)?; let mock_chain = builder.build()?; let recipient = Word::from([0, 1, 2, 3u32]); @@ -128,7 +203,8 @@ fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyhow::Res .build_tx_context(faucet.id(), &[], &[])? .tx_script(tx_script) .build()? - .execute_blocking(); + .execute() + .await; // Execute the transaction and get the witness assert_transaction_executor_error!( @@ -138,51 +214,76 @@ fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyhow::Res Ok(()) } -// TESTS BURN FUNGIBLE ASSET +// TESTS FOR NEW FAUCET EXECUTION ENVIRONMENT // ================================================================================================ -#[test] -fn prove_faucet_contract_burn_fungible_asset_succeeds() -> anyhow::Result<()> { +/// Tests that minting assets on a new faucet succeeds. +#[tokio::test] +async fn minting_fungible_asset_on_new_faucet_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - let faucet = builder.add_existing_faucet(Auth::BasicAuth, "TST", 200, Some(100))?; + let faucet = builder.create_new_faucet(Auth::BasicAuth, "TST", 200)?; let mut mock_chain = builder.build()?; - let fungible_asset = FungibleAsset::new(faucet.id(), 100).unwrap(); + 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), + note_type: NoteType::Private, + amount: Felt::new(100), + }; - // 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)); + params + .tag + .validate(params.note_type) + .expect("note tag should support private notes"); - // Check that the faucet reserved slot has been correctly initialized. - // The already issued amount should be 100. - assert_eq!(faucet.get_token_issuance().unwrap(), Felt::new(100)); + let executed_transaction = + execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?; + verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?; + + Ok(()) +} + +// TESTS BURN FUNGIBLE ASSET +// ================================================================================================ + +/// Tests that burning a fungible asset on an existing faucet succeeds and proves the transaction. +#[tokio::test] +async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth, "TST", 200, Some(100))?; + + let fungible_asset = FungibleAsset::new(faucet.id(), 100).unwrap(); // need to create a note with the fungible asset to be burned - let note_script = " + let burn_note_script_code = " # burn the asset begin dropw - - # pad the stack before call - padw padw padw padw - # => [pad(16)] - - exec.::miden::note::get_assets drop - mem_loadw - # => [ASSET, pad(12)] + # => [] call.::miden::contracts::faucets::basic_fungible::burn + # => [ASSET] # truncate the stack - dropw dropw dropw dropw + dropw end "; - let note = get_note_with_fungible_asset_and_script(fungible_asset, note_script); + let note = get_note_with_fungible_asset_and_script(fungible_asset, burn_note_script_code); - mock_chain.add_pending_note(OutputNote::Full(note.clone())); - mock_chain.prove_next_block()?; + 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 the faucet reserved slot has been correctly initialized. + // The already issued amount should be 100. + assert_eq!(faucet.get_token_issuance().unwrap(), Felt::new(100)); // CONSTRUCT AND EXECUTE TX (Success) // -------------------------------------------------------------------------------------------- @@ -190,7 +291,8 @@ fn prove_faucet_contract_burn_fungible_asset_succeeds() -> anyhow::Result<()> { let executed_transaction = mock_chain .build_tx_context(faucet.id(), &[note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; // Prove, serialize/deserialize and verify the transaction prove_and_verify_transaction(executed_transaction.clone())?; @@ -199,3 +301,383 @@ fn prove_faucet_contract_burn_fungible_asset_succeeds() -> anyhow::Result<()> { assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id()); Ok(()) } + +// TEST PUBLIC NOTE CREATION DURING NOTE CONSUMPTION +// ================================================================================================ + +/// Tests that a public note can be created during note consumption by fetching the note script +/// from the data store. This test verifies the functionality added in issue #1972. +/// +/// The test creates a note that calls the faucet's `distribute` function to create a PUBLIC +/// P2ID output note. The P2ID script is fetched from the data store during transaction execution. +#[tokio::test] +async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth, "TST", 200, None)?; + + // 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 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()) + .compile_note_script(output_note_script_code)?; + + let serial_num = Word::default(); + let target_account_suffix = recipient_account_id.suffix(); + let target_account_prefix = recipient_account_id.prefix().as_felt(); + + // Adding extra 0 values to inputs to test trial unhashing in extract_note_inputs fn + let note_inputs = NoteInputs::new(vec![ + target_account_suffix, + target_account_prefix, + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(0), + Felt::new(1), + ])?; + + let note_recipient = + NoteRecipient::new(serial_num, output_note_script.clone(), note_inputs.clone()); + + 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 expected_note = Note::new(NoteAssets::new(vec![asset.into()])?, metadata, note_recipient); + + let trigger_note_script_code = format!( + " + use.miden::note + + begin + # Build recipient hash from SERIAL_NUM, SCRIPT_ROOT, and INPUTS_COMMITMENT + push.{script_root} + # => [SCRIPT_ROOT] + + push.{serial_num} + # => [SERIAL_NUM, SCRIPT_ROOT] + + # Store note inputs in memory at address 0 + # First word: inputs[0..4] + push.{input0}.{input1}.{input2}.{input3} + mem_storew_be.0 dropw + # Memory[0] = [input0, input1, input2, input3] + + # Second word: inputs[4..8] + push.{input4}.{input5}.{input6}.{input7} + mem_storew_be.4 dropw + # Memory[1] = [input4, input5, input6, input7] + + push.8 push.0 + # => [inputs_ptr, num_inputs, SERIAL_NUM, SCRIPT_ROOT] + + exec.note::build_recipient + # => [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] + + call.::miden::contracts::faucets::basic_fungible::distribute + # => [note_idx, pad(15)] + + # Truncate the stack + dropw dropw dropw dropw + end + ", + note_type = note_type as u8, + input0 = note_inputs.values()[0], + input1 = note_inputs.values()[1], + input2 = note_inputs.values()[2], + input3 = note_inputs.values()[3], + input4 = note_inputs.values()[4], + input5 = note_inputs.values()[5], + input6 = note_inputs.values()[6], + input7 = note_inputs.values()[7], + 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, + ); + + // Create the trigger note that will call distribute + 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)) + .serial_number(Word::from([1, 2, 3, 4u32])) + .code(trigger_note_script_code) + .build()?; + + builder.add_output_note(OutputNote::Full(trigger_note.clone())); + let mock_chain = builder.build()?; + + // Execute the transaction - this should fetch the output note script from the data store. + // Note: There is intentionally no call to extend_expected_output_notes here, so the + // transaction host is forced to request the script from the data store during execution. + let executed_transaction = mock_chain + .build_tx_context(faucet.id(), &[trigger_note.id()], &[])? + .add_note_script(output_note_script) + .with_source_manager(source_manager) + .build()? + .execute() + .await?; + + // Verify that a PUBLIC note was created + assert_eq!(executed_transaction.output_notes().num_notes(), 1); + let output_note = executed_transaction.output_notes().get_note(0); + + // Extract the full note from the OutputNote enum + let full_note = match output_note { + OutputNote::Full(note) => note, + _ => panic!("Expected OutputNote::Full variant"), + }; + + // Verify the output note is public + assert_eq!(full_note.metadata().note_type(), NoteType::Public); + + // Verify the output note contains the minted fungible asset + let expected_asset = FungibleAsset::new(faucet.id(), amount.into())?; + let expected_asset_obj = Asset::from(expected_asset); + assert!(full_note.assets().iter().any(|asset| asset == &expected_asset_obj)); + + // Verify the note was created by the faucet + assert_eq!(full_note.metadata().sender(), faucet.id()); + + // Verify the note inputs commitment matches the expected commitment + assert_eq!( + full_note.recipient().inputs().commitment(), + note_inputs.commitment(), + "Output note inputs commitment should match expected inputs commitment" + ); + + // Verify the output note ID matches the expected note ID + assert_eq!(full_note.id(), expected_note.id()); + + // Verify nonce was incremented + assert_eq!(executed_transaction.account_delta().nonce_delta(), Felt::new(1)); + + Ok(()) +} + +// TESTS NETWORK FAUCET +// ================================================================================================ + +/// Tests minting on network faucet +#[tokio::test] +async fn network_faucet_mint() -> 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))?; + + // Create a target account to consume the minted note + let mut target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + // 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)); + + // 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(); + 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())); + + // Check that the faucet reserved slot has been correctly initialized. + // The already issued amount should be 50. + assert_eq!(faucet.get_token_issuance().unwrap(), Felt::new(50)); + + // CREATE MINT NOTE USING STANDARD NOTE + // -------------------------------------------------------------------------------------------- + + let amount = Felt::new(75); + let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); + let tag = NoteTag::for_local_use_case(0, 0).unwrap(); + let aux = Felt::new(27); + let note_execution_hint = NoteExecutionHint::on_block_slot(5, 6, 7); + let note_type = NoteType::Private; + let serial_num = Word::default(); + + let p2id_mint_output_note = create_p2id_note_exact( + faucet.id(), + target_account.id(), + vec![mint_asset], + note_type, + aux, + serial_num, + ) + .unwrap(); + let recipient = p2id_mint_output_note.recipient().digest(); + + // Use the standard MINT note script + let note_script = WellKnownNote::MINT.script(); + + // Create the note inputs for MINT note (reversed order) + let inputs = NoteInputs::new(vec![ + recipient[0], + recipient[1], + recipient[2], + recipient[3], + note_execution_hint.into(), + note_type.into(), + aux, + tag.into(), + amount, + ])?; + + // Create the MINT note using the standard script + let mint_note_metadata = + NoteMetadata::new(faucet_owner_account_id, note_type, tag, note_execution_hint, aux)?; + let mint_note_assets = NoteAssets::new(vec![])?; // Empty assets for mint note + let serial_num = Word::from([1, 2, 3, 4u32]); // Random serial number + let mint_note_recipient = NoteRecipient::new(serial_num, note_script, inputs); + let mint_note = Note::new(mint_note_assets, mint_note_metadata, mint_note_recipient); + + // Add the MINT note to the mock chain + builder.add_output_note(OutputNote::Full(mint_note.clone())); + let mut mock_chain = builder.build()?; + + // EXECUTE MINT NOTE AGAINST NETWORK FAUCET + // -------------------------------------------------------------------------------------------- + let tx_context = mock_chain.build_tx_context(faucet.id(), &[mint_note.id()], &[])?.build()?; + let executed_transaction = tx_context.execute().await?; + + // Check that a 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(faucet.id(), amount.into())?; + let assets = NoteAssets::new(vec![expected_asset.into()])?; + let expected_note_id = NoteId::new(recipient, assets.commitment()); + + assert_eq!(output_note.id(), expected_note_id); + assert_eq!(output_note.metadata().sender(), faucet.id()); + + // 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 + // -------------------------------------------------------------------------------------------- + // Execute transaction to consume the output note with the target account + 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?; + + // Apply the delta to the target account and verify the asset was added to the account's vault + target_account.apply_delta(consume_executed_transaction.account_delta())?; + + // Verify the account's vault now contains the expected fungible asset + let balance = target_account.vault().get_balance(faucet.id())?; + assert_eq!(balance, expected_asset.amount(),); + + Ok(()) +} + +// TESTS FOR FAUCET PROCEDURE COMPATIBILITY +// ================================================================================================ + +/// Tests that basic and network fungible faucets have the same burn procedure digest. +/// This is required for BURN notes to work with both faucet types. +#[test] +fn test_faucet_burn_procedures_are_identical() { + // Both faucet types must export the same burn procedure with identical MAST roots + // so that a single BURN note script can work with either faucet type + assert_eq!( + BasicFungibleFaucet::burn_digest(), + NetworkFungibleFaucet::burn_digest(), + "Basic and network fungible faucets must have the same burn procedure digest" + ); +} + +/// Tests burning on network faucet +#[tokio::test] +async fn network_faucet_burn() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let faucet_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let mut faucet = + builder.add_existing_network_faucet("NET", 200, faucet_owner_account_id, Some(100))?; + + let burn_amount = 100u64; + let fungible_asset = FungibleAsset::new(faucet.id(), burn_amount).unwrap(); + + // CREATE BURN NOTE USING STANDARD NOTE SCRIPT + // -------------------------------------------------------------------------------------------- + // Use the standard BURN note script + let note_script = WellKnownNote::BURN.script(); + + // Create the burn note using the standard script + let burn_note_metadata = NoteMetadata::new( + faucet_owner_account_id, + NoteType::Public, + NoteTag::for_local_use_case(0, 0)?, + NoteExecutionHint::Always, + Felt::new(0), + )?; + let burn_note_assets = NoteAssets::new(vec![fungible_asset.into()])?; + let serial_num = Word::from([5, 6, 7, 8u32]); + let inputs = NoteInputs::new(vec![]).unwrap(); + let burn_note_recipient = NoteRecipient::new(serial_num, note_script, inputs); + let note = Note::new(burn_note_assets, burn_note_metadata, burn_note_recipient); + + builder.add_output_note(OutputNote::Full(note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // Check the initial token issuance before burning + let initial_issuance = faucet.get_token_issuance().unwrap(); + assert_eq!(initial_issuance, Felt::new(100)); + + // EXECUTE BURN NOTE AGAINST NETWORK FAUCET + // -------------------------------------------------------------------------------------------- + let tx_context = mock_chain.build_tx_context(faucet.id(), &[note.id()], &[])?.build()?; + let executed_transaction = tx_context.execute().await?; + + // Check that the burn was successful - no output notes should be created for burn + assert_eq!(executed_transaction.output_notes().num_notes(), 0); + + // Verify the transaction was executed successfully + assert_eq!(executed_transaction.account_delta().nonce_delta(), Felt::new(1)); + assert_eq!(executed_transaction.input_notes().get_note(0).id(), note.id()); + + // Apply the delta to the faucet account and verify the token issuance decreased + faucet.apply_delta(executed_transaction.account_delta())?; + let final_issuance = faucet.get_token_issuance().unwrap(); + assert_eq!(final_issuance, Felt::new(initial_issuance.as_int() - burn_amount)); + + Ok(()) +} diff --git a/crates/miden-testing/tests/scripts/fee.rs b/crates/miden-testing/tests/scripts/fee.rs index 4182de86ba..70622e4819 100644 --- a/crates/miden-testing/tests/scripts/fee.rs +++ b/crates/miden-testing/tests/scripts/fee.rs @@ -14,8 +14,8 @@ use crate::prove_and_verify_transaction; /// This is an interesting test case because the prover needs to apply the fee asset to the account /// delta in order to prove the correct delta commitment. Once we have other tests with fees, this /// test may become obsolete. -#[test] -fn prove_account_creation_with_fees() -> anyhow::Result<()> { +#[tokio::test] +async fn prove_account_creation_with_fees() -> anyhow::Result<()> { let amount = 10_000; let mut builder = MockChain::builder().verification_base_fee(50); let account = builder.create_new_wallet(Auth::IncrNonce)?; @@ -25,7 +25,8 @@ fn prove_account_creation_with_fees() -> anyhow::Result<()> { let tx = chain .build_tx_context(account, &[fee_note.id()], &[])? .build()? - .execute_blocking() + .execute() + .await .context("failed to execute account-creating transaction")?; let expected_fee = tx.compute_fee(); diff --git a/crates/miden-testing/tests/scripts/p2id.rs b/crates/miden-testing/tests/scripts/p2id.rs index 87decdbfb3..dda0578692 100644 --- a/crates/miden-testing/tests/scripts/p2id.rs +++ b/crates/miden-testing/tests/scripts/p2id.rs @@ -20,8 +20,8 @@ use crate::prove_and_verify_transaction; /// We test the Pay to script with 2 assets to test the loop inside the script. /// So we create a note containing two assets that can only be consumed by the target account. -#[test] -fn p2id_script_multiple_assets() -> anyhow::Result<()> { +#[tokio::test] +async fn p2id_script_multiple_assets() -> anyhow::Result<()> { // Create assets let fungible_asset_1: Asset = FungibleAsset::mock(123); let fungible_asset_2: Asset = @@ -50,10 +50,11 @@ fn p2id_script_multiple_assets() -> anyhow::Result<()> { let executed_transaction = mock_chain .build_tx_context(target_account.id(), &[note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; // vault delta - let target_account_after: Account = Account::from_parts( + let target_account_after: Account = Account::new_existing( target_account.id(), AssetVault::new(&[fungible_asset_1, fungible_asset_2]).unwrap(), target_account.storage().clone(), @@ -74,7 +75,8 @@ fn p2id_script_multiple_assets() -> anyhow::Result<()> { let executed_transaction_2 = mock_chain .build_tx_context(malicious_account.id(), &[], &[note])? .build()? - .execute_blocking(); + .execute() + .await; // Check that we got the expected result - TransactionExecutorError assert_transaction_executor_error!(executed_transaction_2, ERR_P2ID_TARGET_ACCT_MISMATCH); @@ -82,8 +84,8 @@ fn p2id_script_multiple_assets() -> anyhow::Result<()> { } /// Consumes an existing note with a new account -#[test] -fn prove_consume_note_with_new_account() -> anyhow::Result<()> { +#[tokio::test] +async fn prove_consume_note_with_new_account() -> anyhow::Result<()> { // Create assets let fungible_asset: Asset = FungibleAsset::mock(123); @@ -110,10 +112,11 @@ fn prove_consume_note_with_new_account() -> anyhow::Result<()> { let executed_transaction = mock_chain .build_tx_context(target_account.clone(), &[note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; // Apply delta to the target account to verify it is no longer new - let target_account_after: Account = Account::from_parts( + let target_account_after: Account = Account::new_existing( target_account.id(), AssetVault::new(&[fungible_asset]).unwrap(), target_account.storage().clone(), @@ -131,8 +134,8 @@ fn prove_consume_note_with_new_account() -> anyhow::Result<()> { /// Consumes two existing notes (with an asset from a faucet for a combined total of 123 tokens) /// with a basic account -#[test] -fn prove_consume_multiple_notes() -> anyhow::Result<()> { +#[tokio::test] +async fn prove_consume_multiple_notes() -> anyhow::Result<()> { let fungible_asset_1: Asset = FungibleAsset::mock(100); let fungible_asset_2: Asset = FungibleAsset::mock(23); @@ -157,7 +160,7 @@ fn prove_consume_multiple_notes() -> anyhow::Result<()> { .build_tx_context(account.id(), &[note_1.id(), note_2.id()], &[])? .build()?; - let executed_transaction = tx_context.execute_blocking()?; + let executed_transaction = tx_context.execute().await?; account.apply_delta(executed_transaction.account_delta())?; let resulting_asset = account.vault().assets().next().unwrap(); @@ -171,8 +174,8 @@ fn prove_consume_multiple_notes() -> anyhow::Result<()> { } /// Consumes two existing notes and creates two other notes in the same transaction -#[test] -fn test_create_consume_multiple_notes() -> anyhow::Result<()> { +#[tokio::test] +async fn test_create_consume_multiple_notes() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let mut account = @@ -219,14 +222,14 @@ fn test_create_consume_multiple_notes() -> anyhow::Result<()> { let tx_script_src = &format!( " - use.miden::tx + use.miden::output_note begin push.{recipient_1} push.{note_execution_hint_1} push.{note_type_1} push.0 # aux push.{tag_1} - call.tx::create_note + call.output_note::create push.{asset_1} call.::miden::contracts::wallets::basic::move_asset_to_note @@ -237,7 +240,7 @@ fn test_create_consume_multiple_notes() -> anyhow::Result<()> { push.{note_type_2} push.0 # aux push.{tag_2} - call.tx::create_note + call.output_note::create push.{asset_2} call.::miden::contracts::wallets::basic::move_asset_to_note @@ -267,7 +270,7 @@ fn test_create_consume_multiple_notes() -> anyhow::Result<()> { .tx_script(tx_script) .build()?; - let executed_transaction = tx_context.execute_blocking()?; + let executed_transaction = tx_context.execute().await?; assert_eq!(executed_transaction.output_notes().num_notes(), 2); diff --git a/crates/miden-testing/tests/scripts/p2ide.rs b/crates/miden-testing/tests/scripts/p2ide.rs index 15d9bcc197..d149ced65f 100644 --- a/crates/miden-testing/tests/scripts/p2ide.rs +++ b/crates/miden-testing/tests/scripts/p2ide.rs @@ -15,8 +15,8 @@ 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 -#[test] -fn p2ide_script_success_without_reclaim_or_timelock() -> anyhow::Result<()> { +#[tokio::test] +async fn p2ide_script_success_without_reclaim_or_timelock() -> anyhow::Result<()> { let reclaim_height = None; // if 0, means it is not reclaimable let timelock_height = None; // if 0 means it is not timelocked @@ -33,7 +33,8 @@ fn p2ide_script_success_without_reclaim_or_timelock() -> anyhow::Result<()> { let executed_transaction_1 = mock_chain .build_tx_context(malicious_account.id(), &[], slice::from_ref(&p2ide_note))? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(executed_transaction_1, ERR_P2IDE_RECLAIM_DISABLED); @@ -41,9 +42,10 @@ fn p2ide_script_success_without_reclaim_or_timelock() -> anyhow::Result<()> { let executed_transaction_2 = mock_chain .build_tx_context(target_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; - let target_account_after: Account = Account::from_parts( + let target_account_after: Account = Account::new_existing( target_account.id(), AssetVault::new(&[fungible_asset])?, target_account.storage().clone(), @@ -59,8 +61,8 @@ fn p2ide_script_success_without_reclaim_or_timelock() -> anyhow::Result<()> { } /// Test that the P2IDE note can have a timelock that unlocks before the reclaim block height -#[test] -fn p2ide_script_success_timelock_unlock_before_reclaim_height() -> anyhow::Result<()> { +#[tokio::test] +async fn p2ide_script_success_timelock_unlock_before_reclaim_height() -> anyhow::Result<()> { let reclaim_height = Some(BlockNumber::from(5u32)); let timelock_height = None; @@ -78,9 +80,10 @@ fn p2ide_script_success_timelock_unlock_before_reclaim_height() -> anyhow::Resul let executed_transaction_1 = mock_chain .build_tx_context(target_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; - let target_account_after: Account = Account::from_parts( + let target_account_after: Account = Account::new_existing( target_account.id(), AssetVault::new(&[fungible_asset])?, target_account.storage().clone(), @@ -97,8 +100,8 @@ fn p2ide_script_success_timelock_unlock_before_reclaim_height() -> anyhow::Resul /// Test that the P2IDE note can have a timelock set and reclaim functionality /// disabled. -#[test] -fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { +#[tokio::test] +async fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { let reclaim_height = None; let timelock_height = BlockNumber::from(5u32); let P2ideTestSetup { @@ -121,7 +124,8 @@ fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { &[], )? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_reclaim, ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED); @@ -134,7 +138,8 @@ fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { &[], )? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_spend, ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED); @@ -142,7 +147,8 @@ fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { let early_reclaim = mock_chain .build_tx_context(sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_reclaim, ERR_P2IDE_RECLAIM_DISABLED); @@ -150,9 +156,10 @@ fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { let final_tx = mock_chain .build_tx_context(target_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; - let target_after = Account::from_parts( + let target_after = Account::new_existing( target_account.id(), AssetVault::new(&[fungible_asset])?, target_account.storage().clone(), @@ -169,8 +176,8 @@ fn p2ide_script_timelocked_reclaim_disabled() -> anyhow::Result<()> { /// before the timelock expires. Creating a P2IDE note with a reclaim block height that is /// less than the timelock block height would be the same as creating a P2IDE note /// where the reclaim block height is equal to the timelock block height -#[test] -fn p2ide_script_reclaim_fails_before_timelock_expiry() -> anyhow::Result<()> { +#[tokio::test] +async fn p2ide_script_reclaim_fails_before_timelock_expiry() -> anyhow::Result<()> { let reclaim_height = BlockNumber::from(1u32); let timelock_height = BlockNumber::from(5u32); @@ -188,7 +195,8 @@ fn p2ide_script_reclaim_fails_before_timelock_expiry() -> anyhow::Result<()> { let executed_transaction_1 = mock_chain .build_tx_context_at(1, sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!( executed_transaction_1, @@ -199,9 +207,10 @@ fn p2ide_script_reclaim_fails_before_timelock_expiry() -> anyhow::Result<()> { let executed_transaction_2 = mock_chain .build_tx_context_at(timelock_height, sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; - let sender_account_after: Account = Account::from_parts( + let sender_account_after: Account = Account::new_existing( sender_account.id(), AssetVault::new(&[fungible_asset])?, sender_account.storage().clone(), @@ -218,8 +227,8 @@ fn p2ide_script_reclaim_fails_before_timelock_expiry() -> anyhow::Result<()> { } /// Test that the P2IDE note can have timelock and reclaim functionality -#[test] -fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { +#[tokio::test] +async fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { let reclaim_height = BlockNumber::from(10u32); let timelock_height = BlockNumber::from(7u32); @@ -237,7 +246,8 @@ fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { let early_reclaim = mock_chain .build_tx_context(sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_reclaim, ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED); @@ -245,7 +255,8 @@ fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { let early_spend = mock_chain .build_tx_context(target_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_spend, ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED); @@ -256,7 +267,8 @@ fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { let early_reclaim = mock_chain .build_tx_context(sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_reclaim, ERR_P2IDE_RECLAIM_HEIGHT_NOT_REACHED); @@ -267,7 +279,8 @@ fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { let executed_transaction_1 = mock_chain .build_tx_context(malicious_account.id(), &[], slice::from_ref(&p2ide_note))? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!( executed_transaction_1, @@ -278,9 +291,10 @@ fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { let final_tx = mock_chain .build_tx_context(target_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; - let target_after = Account::from_parts( + let target_after = Account::new_existing( target_account.id(), AssetVault::new(&[fungible_asset])?, target_account.storage().clone(), @@ -294,8 +308,8 @@ fn p2ide_script_reclaimable_timelockable() -> anyhow::Result<()> { } /// Test that the P2IDE note can be reclaimed after timelock -#[test] -fn p2ide_script_reclaim_success_after_timelock() -> anyhow::Result<()> { +#[tokio::test] +async fn p2ide_script_reclaim_success_after_timelock() -> anyhow::Result<()> { let reclaim_height = BlockNumber::from(5); let timelock_height = BlockNumber::from(3); @@ -311,7 +325,8 @@ fn p2ide_script_reclaim_success_after_timelock() -> anyhow::Result<()> { let early_reclaim = mock_chain .build_tx_context(sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking(); + .execute() + .await; assert_transaction_executor_error!(early_reclaim, ERR_P2IDE_TIMELOCK_HEIGHT_NOT_REACHED); @@ -322,9 +337,10 @@ fn p2ide_script_reclaim_success_after_timelock() -> anyhow::Result<()> { let final_tx = mock_chain .build_tx_context(sender_account.id(), &[p2ide_note.id()], &[])? .build()? - .execute_blocking()?; + .execute() + .await?; - let sender_after = Account::from_parts( + let sender_after = Account::new_existing( sender_account.id(), AssetVault::new(&[fungible_asset])?, sender_account.storage().clone(), diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index 1a20d206ca..e18f44942d 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -24,8 +24,8 @@ use miden_testing::{Auth, MockChain}; /// has the [`BasicWallet`][wallet] interface. /// /// [wallet]: miden_lib::account::interface::AccountComponentInterface::BasicWallet -#[test] -fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { +#[tokio::test] +async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { let sent_asset = FungibleAsset::mock(10); let mut builder = MockChain::builder(); @@ -64,7 +64,8 @@ fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { .tx_script(send_note_transaction_script) .extend_expected_output_notes(vec![OutputNote::Full(note)]) .build()? - .execute_blocking()?; + .execute() + .await?; // assert that the removed asset is in the delta let mut removed_assets: BTreeMap<_, _> = executed_transaction @@ -87,11 +88,11 @@ fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { /// has the [`BasicFungibleFaucet`][faucet] interface. /// /// [faucet]: miden_lib::account::interface::AccountComponentInterface::BasicFungibleFaucet -#[test] -fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { +#[tokio::test] +async fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let sender_basic_fungible_faucet_account = - builder.add_existing_faucet(Auth::BasicAuth, "POL", 200, None)?; + 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); @@ -127,6 +128,7 @@ fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { .tx_script(send_note_transaction_script) .extend_expected_output_notes(vec![OutputNote::Full(note)]) .build()? - .execute_blocking()?; + .execute() + .await?; Ok(()) } diff --git a/crates/miden-testing/tests/scripts/swap.rs b/crates/miden-testing/tests/scripts/swap.rs index be79f104d2..f5aa7ebe25 100644 --- a/crates/miden-testing/tests/scripts/swap.rs +++ b/crates/miden-testing/tests/scripts/swap.rs @@ -24,8 +24,8 @@ use miden_testing::{Auth, MockChain}; use crate::prove_and_verify_transaction; /// Creates a SWAP note from the transaction script and proves and verifies the transaction. -#[test] -pub fn prove_send_swap_note() -> anyhow::Result<()> { +#[tokio::test] +pub async fn prove_send_swap_note() -> anyhow::Result<()> { let payback_note_type = NoteType::Private; let SwapTestSetup { mock_chain, @@ -40,14 +40,14 @@ pub fn prove_send_swap_note() -> anyhow::Result<()> { let tx_script_src = &format!( " - use.miden::tx + use.miden::output_note begin push.{recipient} push.{note_execution_hint} push.{note_type} push.0 # aux push.{tag} - call.tx::create_note + call.output_note::create push.{asset} call.::miden::contracts::wallets::basic::move_asset_to_note @@ -69,7 +69,8 @@ pub fn prove_send_swap_note() -> anyhow::Result<()> { .tx_script(tx_script) .extend_expected_output_notes(vec![OutputNote::Full(swap_note.clone())]) .build()? - .execute_blocking()?; + .execute() + .await?; sender_account .apply_delta(create_swap_note_tx.account_delta()) @@ -98,8 +99,8 @@ pub fn prove_send_swap_note() -> anyhow::Result<()> { /// payback note. The payback note is consumed by the original sender of the SWAP note. /// /// Both transactions are proven and verified. -#[test] -fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { +#[tokio::test] +async fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { let payback_note_type = NoteType::Private; let SwapTestSetup { mock_chain, @@ -118,7 +119,8 @@ fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { .build_tx_context(target_account.id(), &[swap_note.id()], &[]) .context("failed to build tx context")? .build()? - .execute_blocking()?; + .execute() + .await?; target_account .apply_delta(consume_swap_note_tx.account_delta()) @@ -144,7 +146,8 @@ fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { .build_tx_context(sender_account.id(), &[], &[full_payback_note]) .context("failed to build tx context")? .build()? - .execute_blocking()?; + .execute() + .await?; sender_account .apply_delta(consume_payback_tx.account_delta()) @@ -163,8 +166,8 @@ fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { // Creates a swap note with a public payback note, then consumes it to complete the swap // The target account receives the offered asset and creates a public payback note for the sender -#[test] -fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { +#[tokio::test] +async fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { let payback_note_type = NoteType::Public; let SwapTestSetup { mock_chain, @@ -197,7 +200,8 @@ fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { .context("failed to build tx context")? .extend_expected_output_notes(vec![OutputNote::Full(payback_p2id_note)]) .build()? - .execute_blocking()?; + .execute() + .await?; target_account.apply_delta(consume_swap_note_tx.account_delta())?; @@ -221,7 +225,8 @@ fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { .build_tx_context(sender_account.id(), &[], &[full_payback_note]) .context("failed to build tx context")? .build()? - .execute_blocking()?; + .execute() + .await?; sender_account.apply_delta(consume_payback_tx.account_delta())?; @@ -231,12 +236,12 @@ fn consume_swap_note_public_payback_note() -> anyhow::Result<()> { /// Tests that a SWAP note offering asset A and requesting asset B can be matched against a SWAP /// note offering asset B and requesting asset A. -#[test] -fn settle_coincidence_of_wants() -> anyhow::Result<()> { +#[tokio::test] +async fn settle_coincidence_of_wants() -> anyhow::Result<()> { // Create two different assets for the swap let faucet0 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; let faucet1 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1)?; - let asset_a = FungibleAsset::new(faucet0, 10_000)?.into(); + let asset_a = FungibleAsset::new(faucet0, 10_777)?.into(); let asset_b = FungibleAsset::new(faucet1, 10)?.into(); let mut builder = MockChain::builder(); @@ -272,7 +277,8 @@ fn settle_coincidence_of_wants() -> anyhow::Result<()> { .build_tx_context(matcher_account.id(), &[swap_note_1.id(), swap_note_2.id()], &[]) .context("failed to build tx context")? .build()? - .execute_blocking()?; + .execute() + .await?; // VERIFY PAYBACK NOTES WERE CREATED CORRECTLY // -------------------------------------------------------------------------------------------- @@ -327,7 +333,7 @@ fn setup_swap_test(payback_note_type: NoteType) -> anyhow::Result .add_swap_note(sender_account.id(), offered_asset, requested_asset, payback_note_type) .unwrap(); - builder.add_note(OutputNote::Full(swap_note.clone())); + builder.add_output_note(OutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; Ok(SwapTestSetup { diff --git a/crates/miden-testing/tests/wallet/mod.rs b/crates/miden-testing/tests/wallet/mod.rs index 52a5d7f096..c23dc0ae40 100644 --- a/crates/miden-testing/tests/wallet/mod.rs +++ b/crates/miden-testing/tests/wallet/mod.rs @@ -1,7 +1,7 @@ use miden_lib::AuthScheme; use miden_lib::account::wallets::create_basic_wallet; use miden_objects::Word; -use miden_objects::crypto::dsa::rpo_falcon512::SecretKey; +use miden_objects::account::auth::AuthSecretKey; use rand_chacha::ChaCha20Rng; use rand_chacha::rand_core::SeedableRng; @@ -16,8 +16,8 @@ fn wallet_creation() { let seed = [0_u8; 32]; let mut rng = ChaCha20Rng::from_seed(seed); - let sec_key = SecretKey::with_rng(&mut rng); - let pub_key = sec_key.public_key(); + let sec_key = AuthSecretKey::new_rpo_falcon512_with_rng(&mut rng); + let pub_key = sec_key.public_key().to_commitment(); let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key }; // we need to use an initial seed to create the wallet account @@ -29,8 +29,7 @@ fn wallet_creation() { let account_type = AccountType::RegularAccountImmutableCode; let storage_mode = AccountStorageMode::Private; - let (wallet, _) = - create_basic_wallet(init_seed, auth_scheme, account_type, storage_mode).unwrap(); + 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()], @@ -41,6 +40,5 @@ fn wallet_creation() { assert!(wallet.is_regular_account()); assert_eq!(wallet.code().commitment(), expected_code_commitment); - let pub_key_word: Word = pub_key.into(); - assert_eq!(wallet.storage().get_item(0).unwrap(), pub_key_word); + assert_eq!(wallet.storage().get_item(0).unwrap(), Word::from(pub_key)); } diff --git a/crates/miden-tx-batch-prover/Cargo.toml b/crates/miden-tx-batch-prover/Cargo.toml index 390f6767bc..6b214a120b 100644 --- a/crates/miden-tx-batch-prover/Cargo.toml +++ b/crates/miden-tx-batch-prover/Cargo.toml @@ -10,7 +10,7 @@ name = "miden-tx-batch-prover" readme = "README.md" repository.workspace = true rust-version.workspace = true -version = "0.11.5" +version.workspace = true [lib] bench = false @@ -18,6 +18,7 @@ bench = false [features] default = ["std"] std = ["miden-objects/std", "miden-tx/std"] +testing = [] [dependencies] miden-objects = { workspace = true } diff --git a/crates/miden-tx-batch-prover/README.md b/crates/miden-tx-batch-prover/README.md index 85d4babb79..37378de9be 100644 --- a/crates/miden-tx-batch-prover/README.md +++ b/crates/miden-tx-batch-prover/README.md @@ -4,4 +4,4 @@ This crate contains tools for executing and proving Miden transaction batches. ## License -This project is [MIT licensed](../LICENSE). +This project is [MIT licensed](../../LICENSE). 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 73c8a05f71..3636f61ac5 100644 --- a/crates/miden-tx-batch-prover/src/local_batch_prover.rs +++ b/crates/miden-tx-batch-prover/src/local_batch_prover.rs @@ -22,14 +22,48 @@ impl LocalBatchProver { /// Attempts to prove the [`ProposedBatch`] into a [`ProvenBatch`]. /// + /// Currently we don't perform any recursive proving. For now, this function runs a native + /// verifier for each transaction separately, and outputs a `ProvenBatch` object if all of the + /// individual proofs verify. + /// /// # Errors /// /// Returns an error if: /// - a proof of any transaction in the batch fails to verify. pub fn prove(&self, proposed_batch: ProposedBatch) -> Result { + let verifier = TransactionVerifier::new(self.proof_security_level); + + for tx in proposed_batch.transactions() { + verifier.verify(tx).map_err(|source| { + ProvenBatchError::TransactionVerificationFailed { + transaction_id: tx.id(), + source: Box::new(source), + } + })?; + } + + self.prove_inner(proposed_batch) + } + + /// Proves the provided [`ProposedBatch`] into a [`ProvenBatch`], **without verifying batches + /// and proving the block**. + /// + /// This is exposed for testing purposes. + #[cfg(any(feature = "testing", test))] + pub fn prove_dummy( + &self, + proposed_batch: ProposedBatch, + ) -> Result { + self.prove_inner(proposed_batch) + } + + /// Converts a proposed batch into a proven batch. + /// + /// For now, this doesn't do anything interesting. + fn prove_inner(&self, proposed_batch: ProposedBatch) -> Result { let tx_headers = proposed_batch.transaction_headers(); let ( - transactions, + _transactions, block_header, _block_chain, _authenticatable_unauthenticated_notes, @@ -40,17 +74,6 @@ impl LocalBatchProver { batch_expiration_block_num, ) = proposed_batch.into_parts(); - let verifier = TransactionVerifier::new(self.proof_security_level); - - for tx in transactions { - verifier.verify(&tx).map_err(|source| { - ProvenBatchError::TransactionVerificationFailed { - transaction_id: tx.id(), - source: Box::new(source), - } - })?; - } - ProvenBatch::new( id, block_header.commitment(), diff --git a/crates/miden-tx/Cargo.toml b/crates/miden-tx/Cargo.toml index fee785d750..86c3c5006b 100644 --- a/crates/miden-tx/Cargo.toml +++ b/crates/miden-tx/Cargo.toml @@ -10,7 +10,7 @@ name = "miden-tx" readme = "README.md" repository.workspace = true rust-version.workspace = true -version = "0.11.5" +version.workspace = true [features] concurrent = ["miden-prover/concurrent", "std"] @@ -34,7 +34,7 @@ thiserror = { workspace = true } tokio = { features = ["rt"], workspace = true } [dev-dependencies] -anyhow = { default-features = false, features = ["backtrace", "std"], version = "1.0" } +anyhow = { features = ["backtrace", "std"], workspace = true } assert_matches = { workspace = true } miden-assembly = { workspace = true } miden-tx = { features = ["testing"], path = "." } diff --git a/crates/miden-tx/README.md b/crates/miden-tx/README.md index 04dd88e7cc..9808957a3d 100644 --- a/crates/miden-tx/README.md +++ b/crates/miden-tx/README.md @@ -42,4 +42,4 @@ verifier.verify(proven_transaction); ## License -This project is [MIT licensed](../LICENSE). +This project is [MIT licensed](../../LICENSE). diff --git a/crates/miden-tx/src/auth/mod.rs b/crates/miden-tx/src/auth/mod.rs index 160d2962c0..7dd2be0a4c 100644 --- a/crates/miden-tx/src/auth/mod.rs +++ b/crates/miden-tx/src/auth/mod.rs @@ -5,5 +5,3 @@ pub use tx_authenticator::{ TransactionAuthenticator, UnreachableAuth, }; - -pub mod signatures; diff --git a/crates/miden-tx/src/auth/signatures/mod.rs b/crates/miden-tx/src/auth/signatures/mod.rs deleted file mode 100644 index d934d4deb4..0000000000 --- a/crates/miden-tx/src/auth/signatures/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use alloc::vec::Vec; - -use miden_objects::Hasher; -use miden_objects::crypto::dsa::rpo_falcon512::{self, Polynomial}; -use miden_processor::{Felt, Word}; -use rand::Rng; - -use crate::AuthenticationError; - -/// Retrieves a Falcon signature over a message. -/// -/// Gets as input a [Word] containing a secret key, and a [Word] representing a message and -/// outputs 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: -/// -/// 1. The challenge point at which we evaluate the polynomials in the subsequent three bullet -/// points, i.e. `h`, `s2` and `pi`, to check the product relationship. -/// 2. The expanded public key represented as the coefficients of a polynomial `h` of degree < 512. -/// 3. The signature represented as the coefficients of a polynomial `s2` of degree < 512. -/// 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. -/// -/// # Errors -/// Will return an error if either: -/// - The secret key is malformed due to either incorrect length or failed decoding. -/// - The signature generation failed. -pub fn get_falcon_signature( - key: &rpo_falcon512::SecretKey, - message: Word, - rng: &mut R, -) -> Result, AuthenticationError> { - // Generate the signature - let sig = key.sign_with_rng(message, rng); - // The signature is composed of a nonce and a polynomial s2 - // The nonce is represented as 8 field elements. - let nonce = sig.nonce(); - // We convert the signature to a polynomial - let s2 = sig.sig_poly(); - // We also need in the VM the expanded key corresponding to the public key that was provided - // via the operand stack - let h = key.compute_pub_key_poly().0; - // Lastly, for the probabilistic product routine that is part of the verification procedure, - // we need to compute the product of the expanded key and the signature polynomial in - // the ring of polynomials with coefficients in the Miden field. - let pi = Polynomial::mul_modulo_p(&h, s2); - - // We now push the expanded key, the signature polynomial, and the product of the - // expanded key and the signature polynomial to the advice stack. We also push - // the challenge point at which the previous polynomials will be evaluated. - // Finally, we push the nonce needed for the hash-to-point algorithm. - - let mut polynomials: Vec = - h.coefficients.iter().map(|a| Felt::from(a.value() as u32)).collect(); - polynomials.extend(s2.coefficients.iter().map(|a| Felt::from(a.value() as u32))); - polynomials.extend(pi.iter().map(|a| Felt::new(*a))); - - let digest_polynomials = Hasher::hash_elements(&polynomials); - let challenge = (digest_polynomials[0], digest_polynomials[1]); - - let mut result: Vec = vec![challenge.0, challenge.1]; - result.extend_from_slice(&polynomials); - result.extend_from_slice(&nonce.to_elements()); - - result.reverse(); - Ok(result) -} diff --git a/crates/miden-tx/src/auth/tx_authenticator.rs b/crates/miden-tx/src/auth/tx_authenticator.rs index 609c1fe09a..e163497b81 100644 --- a/crates/miden-tx/src/auth/tx_authenticator.rs +++ b/crates/miden-tx/src/auth/tx_authenticator.rs @@ -1,18 +1,14 @@ use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::string::ToString; -use alloc::sync::Arc; use alloc::vec::Vec; -use miden_objects::account::AuthSecretKey; +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 rand::Rng; -use tokio::sync::RwLock; -use super::signatures::get_falcon_signature; use crate::errors::AuthenticationError; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; @@ -140,9 +136,9 @@ pub trait TransactionAuthenticator { /// signature computation. fn get_signature( &self, - pub_key: Word, + pub_key_commitment: PublicKeyCommitment, signing_inputs: &SigningInputs, - ) -> impl FutureMaybeSend, AuthenticationError>>; + ) -> impl FutureMaybeSend>; } /// A placeholder type for the generic trait bound of `TransactionAuthenticator<'_,'_,_,T>` @@ -159,9 +155,9 @@ impl TransactionAuthenticator for UnreachableAuth { #[allow(clippy::manual_async_fn)] fn get_signature( &self, - _pub_key: Word, + _pub_key_commitment: PublicKeyCommitment, _signing_inputs: &SigningInputs, - ) -> impl FutureMaybeSend, AuthenticationError>> { + ) -> impl FutureMaybeSend> { async { unreachable!("Type `UnreachableAuth` must not be instantiated") } } } @@ -171,43 +167,34 @@ impl TransactionAuthenticator for UnreachableAuth { /// Represents a signer for [AuthSecretKey] keys. #[derive(Clone, Debug)] -pub struct BasicAuthenticator { +pub struct BasicAuthenticator { /// pub_key |-> secret_key mapping - keys: BTreeMap, - rng: Arc>, + keys: BTreeMap, } -impl BasicAuthenticator { - #[cfg(feature = "std")] - pub fn new(keys: &[(Word, AuthSecretKey)]) -> BasicAuthenticator { - use rand::SeedableRng; - use rand::rngs::StdRng; - - let rng = StdRng::from_os_rng(); - BasicAuthenticator::::new_with_rng(keys, rng) - } - - pub fn new_with_rng(keys: &[(Word, AuthSecretKey)], rng: R) -> Self { +impl BasicAuthenticator { + pub fn new(keys: &[AuthSecretKey]) -> Self { let mut key_map = BTreeMap::new(); - for (word, secret_key) in keys { - key_map.insert(*word, secret_key.clone()); + for secret_key in keys { + let pub_key = secret_key.public_key().to_commitment(); + key_map.insert(pub_key, secret_key.clone()); } - BasicAuthenticator { - keys: key_map, - rng: Arc::new(RwLock::new(rng)), - } + BasicAuthenticator { keys: key_map } } - /// Returns a reference to the keys map. Map keys represent the public keys, and values - /// represent the secret keys that the authenticator would use to sign messages. - pub fn keys(&self) -> &BTreeMap { + /// 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 { &self.keys } } -impl TransactionAuthenticator for BasicAuthenticator { - /// Gets a signature over a message, given a public key. +impl TransactionAuthenticator for BasicAuthenticator { + /// Gets a signature over a message, given a public key commitment. + /// /// The key should be included in the `keys` map and should be a variant of [AuthSecretKey]. /// /// Supported signature schemes: @@ -218,22 +205,15 @@ impl TransactionAuthenticator for BasicAuthenticator { /// [`AuthenticationError::UnknownPublicKey`] is returned. fn get_signature( &self, - pub_key: Word, + pub_key_commitment: PublicKeyCommitment, signing_inputs: &SigningInputs, - ) -> impl FutureMaybeSend, AuthenticationError>> { + ) -> impl FutureMaybeSend> { let message = signing_inputs.to_commitment(); async move { - let mut rng = self.rng.write().await; - match self.keys.get(&pub_key) { - Some(key) => match key { - AuthSecretKey::RpoFalcon512(falcon_key) => { - get_falcon_signature(falcon_key, message, &mut *rng) - }, - }, - None => Err(AuthenticationError::UnknownPublicKey(format!( - "public key {pub_key} is not contained in the authenticator's keys", - ))), + match self.keys.get(&pub_key_commitment) { + Some(key) => Ok(key.sign(message)), + None => Err(AuthenticationError::UnknownPublicKey(pub_key_commitment)), } } } @@ -246,9 +226,9 @@ impl TransactionAuthenticator for () { #[allow(clippy::manual_async_fn)] fn get_signature( &self, - _pub_key: Word, + _pub_key_commitment: PublicKeyCommitment, _signing_inputs: &SigningInputs, - ) -> impl FutureMaybeSend, AuthenticationError>> { + ) -> impl FutureMaybeSend> { async { Err(AuthenticationError::RejectedSignature( "default authenticator cannot provide signatures".to_string(), @@ -260,22 +240,18 @@ impl TransactionAuthenticator for () { #[cfg(test)] mod test { use miden_lib::utils::{Deserializable, Serializable}; - use miden_objects::account::AuthSecretKey; - use miden_objects::crypto::dsa::rpo_falcon512::SecretKey; + use miden_objects::account::auth::AuthSecretKey; use miden_objects::{Felt, Word}; use super::SigningInputs; #[test] fn serialize_auth_key() { - let secret_key = SecretKey::new(); - let auth_key = AuthSecretKey::RpoFalcon512(secret_key.clone()); + let auth_key = AuthSecretKey::new_rpo_falcon512(); let serialized = auth_key.to_bytes(); let deserialized = AuthSecretKey::read_from_bytes(&serialized).unwrap(); - match deserialized { - AuthSecretKey::RpoFalcon512(key) => assert_eq!(secret_key.to_bytes(), key.to_bytes()), - } + assert_eq!(auth_key, deserialized); } #[test] diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index d7d6b998c5..7bb66698bd 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -1,24 +1,29 @@ use alloc::boxed::Box; 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; +use miden_objects::note::{NoteId, NoteMetadata}; use miden_objects::transaction::TransactionSummary; use miden_objects::{ AccountDeltaError, AccountError, + AssetError, Felt, + NoteError, ProvenTransactionError, TransactionInputError, TransactionOutputError, Word, }; -use miden_processor::ExecutionError; +use miden_processor::{DeserializationError, ExecutionError}; use miden_verifier::VerificationError; use thiserror::Error; @@ -27,20 +32,8 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum NoteCheckerError { - #[error("transaction preparation failed: {0}")] - TransactionPreparationFailed(#[source] TransactionExecutorError), - #[error("transaction execution prologue failed: {0}")] - PrologueExecutionFailed(#[source] TransactionExecutorError), - #[error("transaction execution epilogue failed: {0}")] - EpilogueExecutionFailed(#[source] TransactionExecutorError), - #[error("transaction note execution failed on note index {failed_note_index}: {error}")] - NoteExecutionFailed { - failed_note_index: usize, - error: TransactionExecutorError, - }, - - // new variants were created instead of modifying existing ones do decrease the number of - // merge conflicts during the next release + #[error("invalid input note count {0} is out of range)")] + InputNoteCountOutOfRange(usize), #[error("transaction preparation failed: {0}")] TransactionPreparation(#[source] TransactionExecutorError), #[error("transaction execution prologue failed: {0}")] @@ -162,6 +155,8 @@ pub enum TransactionProverError { // 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. @@ -217,6 +212,139 @@ pub enum TransactionHostError { AccountProcedureInfoCreationFailed(#[source] AccountError), } +// TRANSACTION KERNEL ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum TransactionKernelError { + #[error("failed to add asset to account delta")] + AccountDeltaAddAssetFailed(#[source] AccountDeltaError), + #[error("failed to remove asset from account delta")] + AccountDeltaRemoveAssetFailed(#[source] AccountDeltaError), + #[error("failed to add asset to note")] + 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" + )] + MissingAuthenticator, + #[error("failed to generate signature")] + SignatureGenerationFailed(#[source] AuthenticationError), + #[error("transaction returned unauthorized event but a commitment did not match: {0}")] + TransactionSummaryCommitmentMismatch(#[source] Box), + #[error("failed to construct transaction summary")] + TransactionSummaryConstructionFailed(#[source] Box), + #[error("asset data extracted from the stack by event handler `{handler}` is not well formed")] + MalformedAssetInEventHandler { + handler: &'static str, + source: AssetError, + }, + #[error( + "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" + )] + MalformedNoteScript { + data: Vec, + source: DeserializationError, + }, + #[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), + #[error( + "public note with metadata {0:?} and recipient digest {1} is missing details in the advice provider" + )] + PublicNoteMissingDetails(NoteMetadata, Word), + #[error( + "note input data in advice provider contains fewer elements ({actual}) than specified ({specified}) by its inputs length" + )] + TooFewElementsForNoteInputs { specified: u64, actual: u64 }, + #[error("account procedure with procedure root {0} is not in the account procedure index map")] + UnknownAccountProcedure(Word), + #[error("code commitment {0} is not in the account procedure index map")] + UnknownCodeCommitment(Word), + #[error("account storage slots number is missing in memory at address {0}")] + AccountStorageSlotsNumMissing(u32), + #[error("account nonce can only be incremented once")] + NonceCanOnlyIncrementOnce, + #[error("failed to convert fee asset into fungible asset")] + FailedToConvertFeeAsset(#[source] AssetError), + #[error( + "failed to get inputs for foreign account {foreign_account_id} from data store at reference block {ref_block}" + )] + GetForeignAccountInputs { + foreign_account_id: AccountId, + ref_block: BlockNumber, + // thiserror will return this when calling Error::source on TransactionKernelError. + source: DataStoreError, + }, + #[error( + "failed to get vault asset witness from data store for vault root {vault_root} and vault_key {asset_key}" + )] + GetVaultAssetWitness { + vault_root: Word, + asset_key: AssetVaultKey, + // thiserror will return this when calling Error::source on TransactionKernelError. + source: DataStoreError, + }, + #[error( + "failed to get storage map witness from data store for map root {map_root} and map_key {map_key}" + )] + GetStorageMapWitness { + map_root: Word, + map_key: Word, + // thiserror will return this when calling Error::source on TransactionKernelError. + source: DataStoreError, + }, + #[error( + "native asset amount {account_balance} in the account vault is not sufficient to cover the transaction fee of {tx_fee}" + )] + InsufficientFee { account_balance: u64, tx_fee: u64 }, + /// This variant signals that a signature over the contained commitments is required, but + /// missing. + #[error("transaction requires a signature")] + Unauthorized(Box), + /// A generic error returned when the transaction kernel did not behave as expected. + #[error("{message}")] + Other { + message: Box, + // thiserror will return this when calling Error::source on TransactionKernelError. + source: Option>, + }, +} + +impl TransactionKernelError { + /// Creates a custom error using the [`TransactionKernelError::Other`] variant from an error + /// message. + pub fn other(message: impl Into) -> Self { + let message: String = message.into(); + Self::Other { message: message.into(), source: None } + } + + /// Creates a custom error using the [`TransactionKernelError::Other`] variant from an error + /// message and a source error. + pub fn other_with_source( + message: impl Into, + source: impl Error + Send + Sync + 'static, + ) -> Self { + let message: String = message.into(); + Self::Other { + message: message.into(), + source: Some(Box::new(source)), + } + } +} + // DATA STORE ERROR // ================================================================================================ @@ -226,6 +354,8 @@ 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}")] @@ -264,10 +394,10 @@ impl DataStoreError { pub enum AuthenticationError { #[error("signature rejected: {0}")] RejectedSignature(String), - #[error("unknown public key: {0}")] - UnknownPublicKey(String), + #[error("public key `{0}` is not contained in the authenticator's keys")] + UnknownPublicKey(PublicKeyCommitment), /// Custom error variant for implementors of the - /// [`TransactionAuthenticatior`](crate::auth::TransactionAuthenticator) trait. + /// [`TransactionAuthenticator`](crate::auth::TransactionAuthenticator) trait. #[error("{error_msg}")] Other { error_msg: Box, @@ -312,4 +442,8 @@ mod error_assertions { fn _assert_authentication_error_bounds(err: AuthenticationError) { _assert_error_is_send_sync_static(err); } + + fn _assert_transaction_kernel_error_bounds(err: TransactionKernelError) { + _assert_error_is_send_sync_static(err); + } } diff --git a/crates/miden-tx/src/executor/data_store.rs b/crates/miden-tx/src/executor/data_store.rs index b521112c5e..c9b18f08f5 100644 --- a/crates/miden-tx/src/executor/data_store.rs +++ b/crates/miden-tx/src/executor/data_store.rs @@ -1,8 +1,10 @@ use alloc::collections::BTreeSet; -use miden_objects::account::{Account, AccountId}; +use miden_objects::account::{AccountId, PartialAccount, StorageMapWitness}; +use miden_objects::asset::{AssetVaultKey, AssetWitness}; use miden_objects::block::{BlockHeader, BlockNumber}; -use miden_objects::transaction::PartialBlockchain; +use miden_objects::note::NoteScript; +use miden_objects::transaction::{AccountInputs, PartialBlockchain}; use miden_processor::{FutureMaybeSend, MastForestStore, Word}; use crate::DataStoreError; @@ -30,7 +32,54 @@ pub trait DataStore: MastForestStore { &self, account_id: AccountId, ref_blocks: BTreeSet, - ) -> impl FutureMaybeSend< - Result<(Account, Option, BlockHeader, PartialBlockchain), DataStoreError>, - >; + ) -> impl FutureMaybeSend>; + + /// Returns a partial foreign account state together with a witness, proving its validity in the + /// specified transaction reference block. + fn get_foreign_account_inputs( + &self, + foreign_account_id: AccountId, + ref_block: BlockNumber, + ) -> impl FutureMaybeSend>; + + /// Returns a witness for an asset 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( + &self, + account_id: AccountId, + vault_root: Word, + vault_key: AssetVaultKey, + ) -> impl FutureMaybeSend>; + + /// Returns a witness for a storage map item identified by `map_key` in the requested account's + /// storage with the requested storage `map_root`. + /// + /// Note that the `map_key` needs to be hashed in order to get the actual key into the storage + /// map. + /// + /// 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 storage map item possible. + fn get_storage_map_witness( + &self, + account_id: AccountId, + map_root: Word, + map_key: Word, + ) -> impl FutureMaybeSend>; + + /// Returns a note script with the specified root. + /// + /// This method will try to find a note script with the specified root in the data store, + /// and if not found, return an error. + /// + /// # 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. + fn get_note_script( + &self, + script_root: Word, + ) -> impl FutureMaybeSend>; } diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index 68caa653ec..0c80bbb83b 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -1,16 +1,18 @@ -use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::errors::TransactionKernelError; -use miden_lib::transaction::TransactionEvent; -use miden_objects::account::{AccountDelta, PartialAccount}; +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::FungibleAsset; -use miden_objects::block::FeeParameters; +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, @@ -19,12 +21,12 @@ use miden_processor::{ EventError, FutureMaybeSend, MastForest, - MastForestStore, ProcessState, }; -use crate::AccountProcedureIndexMap; use crate::auth::{SigningInputs, TransactionAuthenticator}; +use crate::errors::TransactionKernelError; +use crate::host::note_builder::OutputNoteBuilder; use crate::host::{ ScriptMastForestStore, TransactionBaseHost, @@ -32,6 +34,7 @@ use crate::host::{ TransactionEventHandling, TransactionProgress, }; +use crate::{AccountProcedureIndexMap, DataStore, DataStoreError}; // TRANSACTION EXECUTOR HOST // ================================================================================================ @@ -45,7 +48,7 @@ use crate::host::{ /// execution. pub struct TransactionExecutorHost<'store, 'auth, STORE, AUTH> where - STORE: MastForestStore, + STORE: DataStore, AUTH: TransactionAuthenticator, { /// The underlying base transaction host. @@ -55,6 +58,14 @@ where /// not present in the `generated_signatures` field. authenticator: Option<&'auth AUTH>, + /// The reference block of the transaction. + ref_block: BlockNumber, + + /// The foreign account code that was lazy loaded during transaction execution. + /// + /// This is required for re-executing the transaction, e.g. as part of transaction proving. + accessed_foreign_account_code: Vec, + /// 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 @@ -62,9 +73,6 @@ where /// authenticator that produced it. generated_signatures: BTreeMap>, - /// The balance of the native asset in the account at the beginning of transaction execution. - initial_native_asset: FungibleAsset, - /// The source manager to track source code file span information, improving any MASM related /// error messages. source_manager: Arc, @@ -72,7 +80,7 @@ where impl<'store, 'auth, STORE, AUTH> TransactionExecutorHost<'store, 'auth, STORE, AUTH> where - STORE: MastForestStore + Sync, + STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync, { // CONSTRUCTORS @@ -86,32 +94,9 @@ where scripts_mast_store: ScriptMastForestStore, acct_procedure_index_map: AccountProcedureIndexMap, authenticator: Option<&'auth AUTH>, - fee_parameters: &FeeParameters, + ref_block: BlockNumber, source_manager: Arc, ) -> Self { - // TODO: Once we have lazy account loading, this should be loaded in on_tx_fee_computed to - // avoid the use of PartialVault entirely, which in the future, may or may not track - // all assets in the account at this point. Here we assume it does track _all_ assets of the - // account. - let initial_native_asset = { - let native_asset = FungibleAsset::new(fee_parameters.native_asset_id(), 0) - .expect("native asset ID should be a valid fungible faucet ID"); - - // Map Asset to FungibleAsset. - // SAFETY: We requested a fungible vault key, so if Some is returned, it should be a - // fungible asset. - // A returned error means the vault does not track or does not contain the asset. - // However, since in practice, the partial vault represents the entire account vault, - // we can assume the second case. A returned None means the asset's amount is - // zero. - // So in both Err and None cases, use the default native_asset with amount 0. - account - .vault() - .get(native_asset.vault_key()) - .map(|asset| asset.map(|asset| asset.unwrap_fungible()).unwrap_or(native_asset)) - .unwrap_or(native_asset) - }; - let base_host = TransactionBaseHost::new( account, input_notes, @@ -123,8 +108,9 @@ where Self { base_host, authenticator, + ref_block, + accessed_foreign_account_code: Vec::new(), generated_signatures: BTreeMap::new(), - initial_native_asset, source_manager, } } @@ -140,6 +126,53 @@ where // EVENT HANDLERS // -------------------------------------------------------------------------------------------- + /// Handles a request for a foreign account by querying the data store for its account inputs. + async fn on_foreign_account_requested( + &mut self, + foreign_account_id: AccountId, + ) -> Result, TransactionKernelError> { + let foreign_account_inputs = self + .base_host + .store() + .get_foreign_account_inputs(foreign_account_id, self.ref_block) + .await + .map_err(|err| TransactionKernelError::GetForeignAccountInputs { + foreign_account_id, + ref_block: self.ref_block, + source: err, + })?; + + 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, + ) + })?; + + 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, + ) + })?; + + // Add the foreign account's code to the list of accessed code. + self.accessed_foreign_account_code.push(foreign_account_inputs.code().clone()); + + Ok(tx_advice_inputs.into_advice_mutations().collect()) + } + /// Pushes a signature to the advice stack as a response to the `AuthRequest` event. /// /// The signature is requested from the host's authenticator. @@ -152,9 +185,10 @@ where self.authenticator.ok_or(TransactionKernelError::MissingAuthenticator)?; let signature: Vec = authenticator - .get_signature(pub_key_hash, &signing_inputs) + .get_signature(PublicKeyCommitment::from(pub_key_hash), &signing_inputs) .await - .map_err(|err| TransactionKernelError::SignatureGenerationFailed(Box::new(err)))?; + .map_err(TransactionKernelError::SignatureGenerationFailed)? + .to_prepared_signature(); let signature_key = Hasher::merge(&[pub_key_hash, signing_inputs.to_commitment()]); @@ -163,63 +197,240 @@ where Ok(vec![AdviceMutation::extend_stack(signature)]) } - /// Handles the [`TransactionEvent::EpilogueTxFeeComputed`] and returns an error if the account - /// cannot pay the fee. - fn on_tx_fee_computed( + /// Handles the [`TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount`] and returns an error + /// if the account cannot pay the fee. + async fn on_before_tx_fee_removed_from_account( &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"), + ); + // Compute the current balance of the native asset in the account based on the initial value // and the delta. - let current_native_asset = { - let native_asset_amount_delta = self + let current_fee_asset = { + let fee_asset_amount_delta = self .base_host .account_delta_tracker() .vault_delta() .fungible() - .amount(&self.initial_native_asset.faucet_id()) + .amount(&initial_fee_asset.faucet_id()) .unwrap_or(0); // SAFETY: Initial native asset faucet ID should be a fungible faucet and amount should // be less than MAX_AMOUNT as checked by the account delta. - let native_asset_delta = FungibleAsset::new( - self.initial_native_asset.faucet_id(), - native_asset_amount_delta.unsigned_abs(), + let fee_asset_delta = FungibleAsset::new( + initial_fee_asset.faucet_id(), + fee_asset_amount_delta.unsigned_abs(), ) .expect("faucet ID and amount should be valid"); // SAFETY: These computations are essentially the same as the ones executed by the // transaction kernel, which should have aborted if they weren't valid. - if native_asset_amount_delta > 0 { - self.initial_native_asset - .add(native_asset_delta) + if fee_asset_amount_delta > 0 { + initial_fee_asset + .add(fee_asset_delta) .expect("transaction kernel should ensure amounts do not exceed MAX_AMOUNT") } else { - self.initial_native_asset - .sub(native_asset_delta) + initial_fee_asset + .sub(fee_asset_delta) .expect("transaction kernel should ensure amount is not negative") } }; // Return an error if the balance in the account does not cover the fee. - if current_native_asset.amount() < fee_asset.amount() { + if current_fee_asset.amount() < fee_asset.amount() { return Err(TransactionKernelError::InsufficientFee { - account_balance: current_native_asset.amount(), + account_balance: current_fee_asset.amount(), tx_fee: fee_asset.amount(), }); } - Ok(Vec::new()) + Ok(asset_witness_to_advice_mutation(asset_witness)) + } + + /// Handles a request for a storage map witness by querying the data store for a merkle path. + /// + /// Note that we request witnesses against the _initial_ map root of the accounts. See also + /// [`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, + 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) + .await + .map_err(|err| TransactionKernelError::GetStorageMapWitness { + map_root, + map_key, + source: err, + })?; + + // Get the nodes in the proof and insert them into the merkle store. + let merkle_store_ext = + AdviceMutation::extend_merkle_store(storage_map_witness.authenticated_nodes()); + + let smt_proof = SmtProof::from(storage_map_witness); + let map_ext = AdviceMutation::extend_map(AdviceMap::from_iter([( + smt_proof.leaf().hash(), + smt_proof.leaf().to_elements(), + )])); + + Ok(vec![merkle_store_ext, map_ext]) + } + + /// Handles a request to an asset witness by querying the data store for a merkle path. + /// + /// ## Native Account + /// + /// For the native account we always request witnesses for the initial vault root, because the + /// data store only has the state of the account vault at the beginning of the transaction. + /// Since the vault root can change as the transaction progresses, this means the witnesses + /// may become _partially_ or fully outdated. To see why they can only be _partially_ outdated, + /// consider the following example: + /// + /// ```text + /// A A' + /// / \ / \ + /// B C -> B' C + /// / \ / \ / \ / \ + /// D E F G D E' F G + /// ``` + /// + /// Leaf E was updated to E', in turn updating nodes B and A. If we now request the merkle path + /// to G against root A (the initial vault root), we'll get nodes F and B. F is a node in the + /// updated tree, while B is not. We insert both into the merkle store anyway. Now, if the + /// transaction attempts to verify the merkle path to G, it can do so because F and B' are in + /// the merkle store. Note that B' is in the store because the transaction inserted it into the + /// merkle store as part of updating E, not because we inserted it. B is present in the store, + /// but is simply ignored for the purpose of verifying G's inclusion. + /// + /// ## Foreign Accounts + /// + /// Foreign accounts are read-only and so they cannot change throughout transaction execution. + /// This means their _current_ vault root is always equivalent to their _initial_ vault root. + /// So, for foreign accounts, just like for the native account, we also always request + /// witnesses for the initial vault root. + async fn on_account_vault_asset_witness_requested( + &self, + current_account_id: AccountId, + vault_root: Word, + asset_key: AssetVaultKey, + ) -> Result, TransactionKernelError> { + let asset_witness = self + .base_host + .store() + .get_vault_asset_witness(current_account_id, vault_root, asset_key) + .await + .map_err(|err| TransactionKernelError::GetVaultAssetWitness { + vault_root, + asset_key, + source: err, + })?; + + Ok(asset_witness_to_advice_mutation(asset_witness)) + } + + /// Handles a request for a [`NoteScript`] by querying the [`DataStore`]. + /// + /// The script is fetched from the data store and used to build a [`NoteRecipient`], which is + /// then used to create an [`OutputNoteBuilder`]. This function is only called for public notes + /// where the script is not already available in the advice provider. + async fn on_note_script_requested( + &mut self, + 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) => { + 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([( + 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(mutations) } /// Consumes `self` and returns the account delta, output notes, generated signatures and /// transaction progress. + #[allow(clippy::type_complexity)] pub fn into_parts( self, - ) -> (AccountDelta, Vec, BTreeMap>, TransactionProgress) { - let (account_delta, output_notes, tx_progress) = self.base_host.into_parts(); - - (account_delta, output_notes, self.generated_signatures, tx_progress) + ) -> ( + AccountDelta, + InputNotes, + Vec, + Vec, + BTreeMap>, + TransactionProgress, + ) { + let (account_delta, input_notes, output_notes, tx_progress) = self.base_host.into_parts(); + + ( + account_delta, + input_notes, + output_notes, + self.accessed_foreign_account_code, + self.generated_signatures, + tx_progress, + ) } } @@ -228,13 +439,9 @@ where impl BaseHost for TransactionExecutorHost<'_, '_, STORE, AUTH> where - STORE: MastForestStore, + STORE: DataStore, AUTH: TransactionAuthenticator, { - fn get_mast_forest(&self, procedure_root: &Word) -> Option> { - self.base_host.get_mast_forest(procedure_root) - } - fn get_label_and_source_file( &self, location: &Location, @@ -248,19 +455,23 @@ where impl AsyncHost for TransactionExecutorHost<'_, '_, STORE, AUTH> where - STORE: MastForestStore + Sync, + STORE: DataStore + Sync, AUTH: TransactionAuthenticator + Sync, { + fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend>> { + let mast_forest = self.base_host.get_mast_forest(node_digest); + async move { mast_forest } + } + fn on_event( &mut self, process: &ProcessState, - event_id: u32, ) -> 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 = TransactionEvent::try_from(event_id) - .map_err(EventError::from) - .and_then(|transaction_event| self.base_host.handle_event(process, transaction_event)); + let event_handling_result = self.base_host.handle_event(process, event_id); async move { let event_handling = event_handling_result?; @@ -276,10 +487,70 @@ where .on_auth_requested(pub_key_hash, signing_inputs) .await .map_err(EventError::from), - TransactionEventData::TransactionFeeComputed { fee_asset } => { - self.on_tx_fee_computed(fee_asset).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) }, + TransactionEventData::AccountVaultAssetWitness { + current_account_id, + vault_root, + asset_key, + } => self + .on_account_vault_asset_witness_requested( + current_account_id, + vault_root, + asset_key, + ) + .await + .map_err(EventError::from), + TransactionEventData::AccountStorageMapWitness { + current_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, + ) + .await + .map_err(EventError::from), } } } } + +// HELPER FUNCTIONS +// ================================================================================================ + +/// 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 { + // 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()); + + let smt_proof = SmtProof::from(asset_witness); + let map_ext = AdviceMutation::extend_map(AdviceMap::from_iter([( + smt_proof.leaf().hash(), + smt_proof.leaf().to_elements(), + )])); + + vec![merkle_store_ext, map_ext] +} diff --git a/crates/miden-tx/src/executor/mod.rs b/crates/miden-tx/src/executor/mod.rs index 70598a5af7..c85b8037da 100644 --- a/crates/miden-tx/src/executor/mod.rs +++ b/crates/miden-tx/src/executor/mod.rs @@ -1,16 +1,13 @@ use alloc::collections::BTreeSet; use alloc::sync::Arc; -use alloc::vec::Vec; -use miden_lib::errors::TransactionKernelError; use miden_lib::transaction::TransactionKernel; use miden_objects::account::AccountId; use miden_objects::assembly::DefaultSourceManager; use miden_objects::assembly::debuginfo::SourceManagerSync; use miden_objects::asset::Asset; -use miden_objects::block::{BlockHeader, BlockNumber}; +use miden_objects::block::BlockNumber; use miden_objects::transaction::{ - AccountInputs, ExecutedTransaction, InputNote, InputNotes, @@ -26,6 +23,7 @@ pub use miden_processor::{ExecutionOptions, MastForestStore}; use super::TransactionExecutorError; use crate::auth::TransactionAuthenticator; +use crate::errors::TransactionKernelError; use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore}; mod exec_host; @@ -37,9 +35,9 @@ pub use data_store::DataStore; mod notes_checker; pub use notes_checker::{ FailedNote, + MAX_NUM_CHECKER_NOTES, NoteConsumptionChecker, NoteConsumptionInfo, - NoteConsumptionStatus, }; // TRANSACTION EXECUTOR @@ -181,24 +179,27 @@ where notes: InputNotes, tx_args: TransactionArgs, ) -> Result { - let (mut host, tx_inputs, stack_inputs, advice_inputs) = - self.prepare_transaction(account_id, block_ref, notes, &tx_args, None).await?; + let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?; + + let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?; let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); - let (stack_outputs, advice_provider) = processor + let output = processor .execute(&TransactionKernel::main(), &mut host) .await .map_err(map_execution_error)?; + let stack_outputs = output.stack; + let advice_provider = output.advice; // The stack is not necessary since it is being reconstructed when re-executing. - let (_stack, advice_map, merkle_store) = advice_provider.into_parts(); + let (_stack, advice_map, merkle_store, _pc_requests) = advice_provider.into_parts(); let advice_inputs = AdviceInputs { map: advice_map, store: merkle_store, ..Default::default() }; - build_executed_transaction(advice_inputs, tx_args, tx_inputs, stack_outputs, host) + build_executed_transaction(advice_inputs, tx_inputs, stack_outputs, host) } // SCRIPT EXECUTION @@ -218,27 +219,22 @@ where block_ref: BlockNumber, tx_script: TransactionScript, advice_inputs: AdviceInputs, - foreign_account_inputs: Vec, ) -> Result<[Felt; 16], TransactionExecutorError> { - let tx_args = TransactionArgs::new(Default::default(), foreign_account_inputs) - .with_tx_script(tx_script); - - let (mut host, _, stack_inputs, advice_inputs) = self - .prepare_transaction( - account_id, - block_ref, - InputNotes::default(), - &tx_args, - Some(advice_inputs), - ) - .await?; + let mut tx_args = TransactionArgs::default().with_tx_script(tx_script); + tx_args.extend_advice_inputs(advice_inputs); + + let notes = InputNotes::default(); + let tx_inputs = self.prepare_tx_inputs(account_id, block_ref, notes, tx_args).await?; + + let (mut host, stack_inputs, advice_inputs) = self.prepare_transaction(&tx_inputs).await?; let processor = FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs); - let (stack_outputs, _advice_provider) = processor + let output = processor .execute(&TransactionKernel::tx_script_main(), &mut host) .await .map_err(TransactionExecutorError::TransactionProgramExecutionFailed)?; + let stack_outputs = output.stack; Ok(*stack_outputs) } @@ -246,43 +242,47 @@ where // HELPER METHODS // -------------------------------------------------------------------------------------------- - /// Prepares the data needed for transaction execution. - /// - /// Preparation includes loading transaction inputs from the data store, validating them, and - /// instantiating a transaction host. - async fn prepare_transaction( + // Validates input notes and account inputs after retrieving transaction inputs from the store. + // + // This method has a one-to-many call relationship with the `prepare_transaction` method. This + // method needs to be called only once in order to allow many transactions to be prepared based + // on the transaction inputs returned by this method. + async fn prepare_tx_inputs( &self, account_id: AccountId, block_ref: BlockNumber, - notes: InputNotes, - tx_args: &TransactionArgs, - init_advice_inputs: Option, - ) -> Result< - ( - TransactionExecutorHost<'store, 'auth, STORE, AUTH>, - TransactionInputs, - StackInputs, - AdviceInputs, - ), - TransactionExecutorError, - > { - let mut ref_blocks = validate_input_notes(¬es, block_ref)?; + input_notes: InputNotes, + tx_args: TransactionArgs, + ) -> Result { + let mut ref_blocks = validate_input_notes(&input_notes, block_ref)?; ref_blocks.insert(block_ref); - let (account, seed, ref_block, mmr) = self + let (account, block_header, blockchain) = self .data_store .get_transaction_inputs(account_id, ref_blocks) .await .map_err(TransactionExecutorError::FetchTransactionInputsFailed)?; - validate_account_inputs(tx_args, &ref_block)?; + let tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes) + .map_err(TransactionExecutorError::InvalidTransactionInputs)? + .with_tx_args(tx_args); - let tx_inputs = TransactionInputs::new(account, seed, ref_block, mmr, notes) - .map_err(TransactionExecutorError::InvalidTransactionInputs)?; + Ok(tx_inputs) + } - let (stack_inputs, advice_inputs) = - TransactionKernel::prepare_inputs(&tx_inputs, tx_args, init_advice_inputs) - .map_err(TransactionExecutorError::ConflictingAdviceMapEntry)?; + /// Prepares the data needed for transaction execution. + /// + /// Preparation includes loading transaction inputs from the data store, validating them, and + /// instantiating a transaction host. + async fn prepare_transaction( + &self, + tx_inputs: &TransactionInputs, + ) -> Result< + (TransactionExecutorHost<'store, 'auth, STORE, AUTH>, StackInputs, AdviceInputs), + TransactionExecutorError, + > { + let (stack_inputs, tx_advice_inputs) = TransactionKernel::prepare_inputs(tx_inputs) + .map_err(TransactionExecutorError::ConflictingAdviceMapEntry)?; // This reverses the stack inputs (even though it doesn't look like it does) because the // fast processor expects the reverse order. @@ -294,28 +294,30 @@ where let input_notes = tx_inputs.input_notes(); let script_mast_store = ScriptMastForestStore::new( - tx_args.tx_script(), + tx_inputs.tx_script(), input_notes.iter().map(|n| n.note().script()), ); - let acct_procedure_index_map = - AccountProcedureIndexMap::from_transaction_params(&tx_inputs, tx_args, &advice_inputs) + // 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)?; let host = TransactionExecutorHost::new( - &tx_inputs.account().into(), + tx_inputs.account(), input_notes.clone(), self.data_store, script_mast_store, - acct_procedure_index_map, + account_procedure_index_map, self.authenticator, - tx_inputs.block_header().fee_parameters(), + tx_inputs.block_header().block_num(), self.source_manager.clone(), ); - let advice_inputs = advice_inputs.into_advice_inputs(); + let advice_inputs = tx_advice_inputs.into_advice_inputs(); - Ok((host, tx_inputs, stack_inputs, advice_inputs)) + Ok((host, stack_inputs, advice_inputs)) } } @@ -325,15 +327,20 @@ where /// Creates a new [ExecutedTransaction] from the provided data. fn build_executed_transaction( mut advice_inputs: AdviceInputs, - tx_args: TransactionArgs, tx_inputs: TransactionInputs, stack_outputs: StackOutputs, host: TransactionExecutorHost, ) -> 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, output_notes, generated_signatures, tx_progress) = - host.into_parts(); + let ( + pre_fee_account_delta, + _input_notes, + output_notes, + accessed_foreign_account_code, + generated_signatures, + tx_progress, + ) = host.into_parts(); let tx_outputs = TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes) @@ -364,7 +371,7 @@ fn build_executed_transaction Result<(), TransactionExecutorError> { - // Validate that foreign account inputs are anchored in the reference block - for foreign_account in tx_args.foreign_account_inputs() { - let computed_account_root = foreign_account.compute_account_root().map_err(|err| { - TransactionExecutorError::InvalidAccountWitness(foreign_account.id(), err) - })?; - if computed_account_root != ref_block.account_root() { - return Err(TransactionExecutorError::ForeignAccountNotAnchoredInReference( - foreign_account.id(), - )); - } - } - Ok(()) -} - /// Validates that input notes were not created after the reference block. /// /// Returns the set of block numbers required to execute the provided notes. diff --git a/crates/miden-tx/src/executor/notes_checker.rs b/crates/miden-tx/src/executor/notes_checker.rs index 3aabc46e7e..e6a033cb60 100644 --- a/crates/miden-tx/src/executor/notes_checker.rs +++ b/crates/miden-tx/src/executor/notes_checker.rs @@ -1,12 +1,14 @@ +use alloc::collections::BTreeMap; use alloc::vec::Vec; -use miden_lib::note::well_known_note::WellKnownNote; +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}; +use miden_objects::transaction::{InputNote, InputNotes, TransactionArgs, TransactionInputs}; use miden_processor::fast::FastProcessor; +use miden_prover::AdviceInputs; use super::TransactionExecutor; use crate::auth::TransactionAuthenticator; @@ -14,6 +16,14 @@ use crate::errors::TransactionCheckerError; use crate::executor::map_execution_error; use crate::{DataStore, NoteCheckerError, TransactionExecutorError}; +// CONSTANTS +// ================================================================================================ + +/// Maximum number of notes that can be checked at once. +/// +/// Fixed at an amount that should keep each run of note consumption checking to a maximum of ~50ms. +pub const MAX_NUM_CHECKER_NOTES: usize = 20; + // NOTE CONSUMPTION INFO // ================================================================================================ @@ -21,12 +31,12 @@ use crate::{DataStore, NoteCheckerError, TransactionExecutorError}; #[derive(Debug)] pub struct FailedNote { pub note: Note, - pub error: Option, + pub error: TransactionExecutorError, } impl FailedNote { /// Constructs a new `FailedNote`. - pub fn new(note: Note, error: Option) -> Self { + pub fn new(note: Note, error: TransactionExecutorError) -> Self { Self { note, error } } } @@ -76,38 +86,47 @@ where /// This function attempts to find the maximum set of notes that can be successfully executed /// together by the target account. /// - /// If some notes succeed but others fail, the failed notes are removed from the candidate set + /// Because of the runtime complexity involved in this function, a limited range of + /// [`MAX_NUM_CHECKER_NOTES`] input notes is allowed. + /// + /// If some notes succeed and others fail, the failed notes are removed from the candidate set /// and the remaining notes (successful + unattempted) are retried in the next iteration. This /// process continues until either all remaining notes succeed or no notes can be successfully /// executed /// - /// # Example Execution Flow - /// - /// Given notes A, B, C, D, E: + /// For example, given notes A, B, C, D, E, the execution flow would be as follows: /// - Try [A, B, C, D, E] → A, B succeed, C fails → Remove C, try again. /// - Try [A, B, D, E] → A, B, D succeed, E fails → Remove E, try again. /// - Try [A, B, D] → All succeed → Return successful=[A, B, D], failed=[C, E]. /// - /// # Returns + /// If a failure occurs at the epilogue phase of the transaction execution, the relevant set of + /// otherwise-successful notes are retried in various combinations in an attempt to find a + /// combination that passes the epilogue phase successfully. /// - /// Returns [`NoteConsumptionInfo`] containing: - /// - `successful`: Notes that can be consumed together by the account. - /// - `failed`: Notes that failed during execution attempts. + /// Returns a list of successfully consumed notes and a list of failed notes. pub async fn check_notes_consumability( &self, target_account_id: AccountId, block_ref: BlockNumber, - input_notes: InputNotes, + mut notes: Vec, tx_args: TransactionArgs, ) -> Result { + let num_notes = notes.len(); + if num_notes == 0 || num_notes > MAX_NUM_CHECKER_NOTES { + return Err(NoteCheckerError::InputNoteCountOutOfRange(num_notes)); + } // Ensure well-known notes are ordered first. - let mut notes = input_notes.into_vec(); - notes.sort_unstable_by_key(|note| WellKnownNote::from_note(note.note()).is_none()); - let notes = InputNotes::::new_unchecked(notes); + notes.sort_unstable_by_key(|note| WellKnownNote::from_note(note).is_none()); - // Attempt to find an executable set of notes. - self.find_executable_notes_by_elimination(target_account_id, block_ref, notes, tx_args) + let notes = InputNotes::from(notes); + let tx_inputs = self + .0 + .prepare_tx_inputs(target_account_id, block_ref, notes, tx_args) .await + .map_err(NoteCheckerError::TransactionPreparation)?; + + // Attempt to find an executable set of notes. + self.find_executable_notes_by_elimination(tx_inputs).await } /// Checks whether the provided input note could be consumed by the provided account by @@ -123,23 +142,33 @@ where /// or in the epilogue. pub async fn can_consume( &self, - account_id: AccountId, + target_account_id: AccountId, block_ref: BlockNumber, note: InputNote, tx_args: TransactionArgs, ) -> Result { - // TODO: apply the static analysis before executing the tx + // return the consumption status if we manage to determine it from the well-known note + if let Some(well_known_note) = WellKnownNote::from_note(note.note()) + && let Some(consumption_status) = + well_known_note.is_consumable(note.note(), target_account_id, block_ref) + { + return Ok(consumption_status); + } - // try to consume the provided note - match self - .try_execute_notes( - account_id, + // Prepare transaction inputs. + let mut tx_inputs = self + .0 + .prepare_tx_inputs( + target_account_id, block_ref, InputNotes::new_unchecked(vec![note]), - &tx_args, + tx_args, ) .await - { + .map_err(NoteCheckerError::TransactionPreparation)?; + + // try to consume the provided note + match self.try_execute_notes(&mut tx_inputs).await { // execution succeeded Ok(()) => Ok(NoteConsumptionStatus::Consumable), Err(tx_checker_error) => { @@ -154,7 +183,7 @@ where }, // execution failed during the note processing TransactionCheckerError::NoteExecution { .. } => { - Ok(NoteConsumptionStatus::Unconsumable) + Ok(NoteConsumptionStatus::UnconsumableConditions) }, // execution failed during the epilogue TransactionCheckerError::EpilogueExecution(epilogue_error) => { @@ -174,12 +203,13 @@ where /// succeeded or failed to execute. async fn find_executable_notes_by_elimination( &self, - target_account_id: AccountId, - block_ref: BlockNumber, - notes: InputNotes, - tx_args: TransactionArgs, + mut tx_inputs: TransactionInputs, ) -> Result { - let mut candidate_notes = notes.into_vec(); + let mut candidate_notes = tx_inputs + .input_notes() + .iter() + .map(|note| note.clone().into_note()) + .collect::>(); let mut failed_notes = Vec::new(); // Attempt to execute notes in a loop. Reduce the set of notes based on failures until @@ -187,25 +217,17 @@ where // further reduced. loop { // Execute the candidate notes. - match self - .try_execute_notes( - target_account_id, - block_ref, - InputNotes::::new_unchecked(candidate_notes.clone()), - &tx_args, - ) - .await - { + tx_inputs.set_input_notes(candidate_notes.clone()); + match self.try_execute_notes(&mut tx_inputs).await { Ok(()) => { // A full set of successful notes has been found. - let successful = - candidate_notes.into_iter().map(InputNote::into_note).collect::>(); + let successful = candidate_notes; return Ok(NoteConsumptionInfo::new(successful, failed_notes)); }, Err(TransactionCheckerError::NoteExecution { failed_note_index, error }) => { // SAFETY: Failed note index is in bounds of the candidate notes. - let failed_note = candidate_notes.remove(failed_note_index).into_note(); - failed_notes.push(FailedNote::new(failed_note, Some(error))); + let failed_note = candidate_notes.remove(failed_note_index); + failed_notes.push(FailedNote::new(failed_note, error)); // All possible candidate combinations have been attempted. if candidate_notes.is_empty() { @@ -216,11 +238,9 @@ where Err(TransactionCheckerError::EpilogueExecution(_)) => { let consumption_info = self .find_largest_executable_combination( - target_account_id, - block_ref, candidate_notes, failed_notes, - &tx_args, + tx_inputs, ) .await; return Ok(consumption_info); @@ -243,14 +263,12 @@ where /// set. async fn find_largest_executable_combination( &self, - target_account_id: AccountId, - block_ref: BlockNumber, - input_notes: Vec, + mut remaining_notes: Vec, mut failed_notes: Vec, - tx_args: &TransactionArgs, + mut tx_inputs: TransactionInputs, ) -> NoteConsumptionInfo { - let mut successful_notes: Vec = Vec::new(); - let mut remaining_notes = input_notes; + let mut successful_notes = Vec::new(); + let mut failed_note_index = BTreeMap::new(); // Iterate by note count: try 1 note, then 2, then 3, etc. for size in 1..=remaining_notes.len() { @@ -260,45 +278,37 @@ where } // Try adding each remaining note to the current successful combination. - let mut test_notes = successful_notes.clone(); for (idx, note) in remaining_notes.iter().enumerate() { - test_notes.push(note.clone()); - - match self - .try_execute_notes( - target_account_id, - block_ref, - InputNotes::::new_unchecked(test_notes.clone()), - tx_args, - ) - .await - { + successful_notes.push(note.clone()); + + tx_inputs.set_input_notes(successful_notes.clone()); + match self.try_execute_notes(&mut tx_inputs).await { Ok(()) => { + // The successfully added note might have failed earlier. Remove it from the + // failed list. + failed_note_index.remove(¬e.id()); // This combination succeeded; remove the most recently added note from // the remaining set. remaining_notes.remove(idx); - successful_notes = test_notes; break; }, - _ => { + Err(error) => { // This combination failed; remove the last note from the test set and // continue to next note. - test_notes.pop(); + let failed_note = + successful_notes.pop().expect("successful notes should not be empty"); + // Record the failed note (overwrite previous failures for the relevant + // note). + failed_note_index + .insert(failed_note.id(), FailedNote::new(failed_note, error.into())); }, } } } - // Convert successful InputNotes to Notes. - let successful = successful_notes.into_iter().map(InputNote::into_note).collect::>(); - - // Update failed_notes with notes that weren't included in successful combination. - // TODO: Replace `None` with meaningful error for `FailedNote` below. - let newly_failed_notes = - remaining_notes.into_iter().map(|note| FailedNote::new(note.into_note(), None)); - failed_notes.extend(newly_failed_notes); - - NoteConsumptionInfo::new(successful, failed_notes) + // Append failed notes to the list of failed notes provided as input. + failed_notes.extend(failed_note_index.into_values()); + NoteConsumptionInfo::new(successful_notes, failed_notes) } /// Attempts to execute a transaction with the provided input notes. @@ -308,25 +318,17 @@ where /// or a specific [`NoteExecutionError`] indicating where and why the execution failed. async fn try_execute_notes( &self, - account_id: AccountId, - block_ref: BlockNumber, - notes: InputNotes, - tx_args: &TransactionArgs, + tx_inputs: &mut TransactionInputs, ) -> Result<(), TransactionCheckerError> { - if notes.is_empty() { + if tx_inputs.input_notes().is_empty() { return Ok(()); } - // TODO: ideally, we should prepare the inputs only once for the whole note consumption - // check (rather than doing this every time when we try to execute some subset of notes), - // but we currently cannot do this because transaction preparation includes input notes; - // we should refactor the preparation process to separate input note preparation from the - // rest, and then we can prepare the rest of the inputs once for the whole check - let (mut host, _, stack_inputs, advice_inputs) = self - .0 - .prepare_transaction(account_id, block_ref, notes, tx_args, None) - .await - .map_err(TransactionCheckerError::TransactionPreparation)?; + let (mut host, stack_inputs, advice_inputs) = + self.0 + .prepare_transaction(tx_inputs) + .await + .map_err(TransactionCheckerError::TransactionPreparation)?; let processor = FastProcessor::new_with_advice_inputs(stack_inputs.as_slice(), advice_inputs); @@ -336,7 +338,19 @@ where .map_err(map_execution_error); match result { - Ok(_) => Ok(()), + Ok(execution_output) => { + // Set the advice inputs from the successful execution as advice inputs for + // reexecution. This avoids calls to the data store (to load data lazily) that have + // already been done as part of this execution. + let (_, advice_map, merkle_store, _) = execution_output.advice.into_parts(); + let advice_inputs = AdviceInputs { + map: advice_map, + store: merkle_store, + ..Default::default() + }; + tx_inputs.set_advice_inputs(advice_inputs); + Ok(()) + }, Err(error) => { let notes = host.tx_progress().note_execution(); @@ -380,32 +394,9 @@ fn handle_epilogue_error(epilogue_error: TransactionExecutorError) -> NoteConsum | TransactionExecutorError::MissingAuthenticator => { // Both these cases signal that there is a probability that the provided note could be // consumed if the authentication is provided. - NoteConsumptionStatus::UnconsumableWithoutAuthorization + NoteConsumptionStatus::ConsumableWithAuthorization }, // TODO: apply additional checks to get the verbose error reason - _ => NoteConsumptionStatus::Unconsumable, + _ => NoteConsumptionStatus::UnconsumableConditions, } } - -// HELPER STRUCTURES -// ================================================================================================ - -/// Describes if a note could be consumed under a specific conditions: target account state -/// and block height. -/// -/// The status does not account for any authorization that may be required to consume the -/// note, nor does it indicate whether the account has sufficient fees to consume it. -#[derive(Debug, PartialEq)] -pub enum NoteConsumptionStatus { - /// The note can be consumed by the account at the specified block height. - Consumable, - /// The note can be consumed by the account after the required block height is achieved. - ConsumableAfter(BlockNumber), - /// The note cannot be consumed by the account without the authorization. - UnconsumableWithoutAuthorization, - /// The note cannot be consumed by the account at the specified conditions (i.e., block - /// height and account state). - Unconsumable, - /// The note cannot be consumed by the specified account under any conditions. - Incompatible, -} diff --git a/crates/miden-tx/src/host/account_delta_tracker.rs b/crates/miden-tx/src/host/account_delta_tracker.rs index 7035e051fd..279d806f05 100644 --- a/crates/miden-tx/src/host/account_delta_tracker.rs +++ b/crates/miden-tx/src/host/account_delta_tracker.rs @@ -1,4 +1,10 @@ -use miden_objects::account::{AccountDelta, AccountId, AccountStorageHeader, AccountVaultDelta}; +use miden_objects::account::{ + AccountCode, + AccountDelta, + AccountId, + AccountVaultDelta, + PartialAccount, +}; use miden_objects::{Felt, FieldElement, ZERO}; use crate::host::storage_delta_tracker::StorageDeltaTracker; @@ -20,16 +26,24 @@ pub struct AccountDeltaTracker { account_id: AccountId, storage: StorageDeltaTracker, vault: AccountVaultDelta, + code: Option, nonce_delta: Felt, } impl AccountDeltaTracker { /// Returns a new [AccountDeltaTracker] instantiated for the specified account. - pub fn new(account_id: AccountId, storage_header: AccountStorageHeader) -> Self { + pub fn new(account: &PartialAccount) -> Self { + let code = if account.is_new() { + Some(account.code().clone()) + } else { + None + }; + Self { - account_id, - storage: StorageDeltaTracker::new(storage_header), + account_id: account.id(), + storage: StorageDeltaTracker::new(account), vault: AccountVaultDelta::default(), + code, nonce_delta: ZERO, } } @@ -72,5 +86,6 @@ impl AccountDeltaTracker { AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta) .expect("account delta created in delta tracker should be valid") + .with_code(self.code) } } diff --git a/crates/miden-tx/src/host/account_procedures.rs b/crates/miden-tx/src/host/account_procedures.rs index dfbf0b83ea..c4c664977f 100644 --- a/crates/miden-tx/src/host/account_procedures.rs +++ b/crates/miden-tx/src/host/account_procedures.rs @@ -1,13 +1,8 @@ -use alloc::string::ToString; - use miden_lib::transaction::memory::{ACCOUNT_STACK_TOP_PTR, ACCT_CODE_COMMITMENT_OFFSET}; -use miden_lib::transaction::{TransactionAdviceInputs, TransactionKernelError}; -use miden_objects::account::{AccountCode, AccountProcedureInfo}; -use miden_objects::transaction::{TransactionArgs, TransactionInputs}; -use miden_processor::AdviceInputs; +use miden_objects::account::AccountCode; -use super::{BTreeMap, Felt, ProcessState, Word}; -use crate::errors::TransactionHostError; +use super::{BTreeMap, ProcessState, Word}; +use crate::errors::{TransactionHostError, TransactionKernelError}; // ACCOUNT PROCEDURE INDEX MAP // ================================================================================================ @@ -15,42 +10,48 @@ use crate::errors::TransactionHostError; /// A map of maps { acct_code_commitment |-> { proc_root |-> proc_index } } for all known /// procedures of account interfaces for all accounts expected to be invoked during transaction /// execution. +#[derive(Debug, Clone, Default)] pub struct AccountProcedureIndexMap(BTreeMap>); impl AccountProcedureIndexMap { - /// Returns a new [AccountProcedureIndexMap] instantiated with account procedures present in - /// the provided advice provider. - /// - /// Note: `account_code_commitments` iterator should include both native account code and - /// foreign account codes commitments - pub fn new( - account_code_commitments: impl IntoIterator, - adv_provider: &AdviceInputs, + /// Returns a new [`AccountProcedureIndexMap`] instantiated with account procedures from the + /// provided iterator of [`AccountCode`]. + pub fn new<'code>( + account_codes: impl IntoIterator, ) -> Result { - let mut result = BTreeMap::new(); + let mut index_map = Self::default(); - for code_commitment in account_code_commitments { - let account_procs_map = build_account_procedure_map(code_commitment, adv_provider)?; - result.insert(code_commitment, account_procs_map); + 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)?; + } } - Ok(Self(result)) + Ok(index_map) } - /// Builds an [`AccountProcedureIndexMap`] for the specified transaction inputs and arguments. + /// Inserts the procedures from the provided [`AccountCode`] into the advice inputs, using + /// [`AccountCode::commitment`] as the key. /// - /// The resulting instance will map all account code commmitments to a mapping of + /// 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 easy procedure index lookups on runtime. - pub fn from_transaction_params( - tx_inputs: &TransactionInputs, - tx_args: &TransactionArgs, - tx_advice_inputs: &TransactionAdviceInputs, - ) -> Result { - let mut account_code_commitments = tx_args.to_foreign_account_code_commitments(); - account_code_commitments.insert(tx_inputs.account().code().commitment()); + /// transaction, enabling fast procedure index lookups at runtime. + pub fn insert_code(&mut self, code: &AccountCode) -> Result<(), TransactionHostError> { + 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); + } + + self.0.insert(code.commitment(), procedure_map); - Self::new(account_code_commitments, tx_advice_inputs.as_advice_inputs()) + Ok(()) } /// Returns index of the procedure whose root is currently at the top of the operand stack in @@ -60,7 +61,7 @@ impl AccountProcedureIndexMap { /// 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 current account code commitment + // get active account code commitment let code_commitment = { let account_stack_top_ptr = process .get_mem_value(process.ctx(), ACCOUNT_STACK_TOP_PTR) @@ -73,15 +74,15 @@ impl AccountProcedureIndexMap { .try_into() .expect("account stack top pointer should be less than u32::MAX"), ) - .expect("Current account pointer was not initialized") + .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("current account code commitment was not initialized") + .expect("active account code commitment was not initialized") }; - let proc_root = process.get_stack_word(0); + let proc_root = process.get_stack_word_be(1); self.0 .get(&code_commitment) @@ -91,62 +92,3 @@ impl AccountProcedureIndexMap { .ok_or(TransactionKernelError::UnknownAccountProcedure(proc_root)) } } - -// HELPER FUNCTIONS -// ================================================================================================ - -fn build_account_procedure_map( - code_commitment: Word, - advice_inputs: &AdviceInputs, -) -> Result, TransactionHostError> { - // get the account procedures from the advice_map - let proc_data = advice_inputs.map.get(&code_commitment).ok_or_else(|| { - TransactionHostError::AccountProcedureIndexMapError( - "failed to read account procedure data from the advice provider".to_string(), - ) - })?; - - let mut account_procs_map = BTreeMap::new(); - - // sanity checks - - // check that there are procedures in the account code - if proc_data.is_empty() { - return Err(TransactionHostError::AccountProcedureIndexMapError( - "account code does not contain any procedures.".to_string(), - )); - } - - // check that procedure data have a correct length - if proc_data.len() % AccountProcedureInfo::NUM_ELEMENTS_PER_PROC != 0 { - return Err(TransactionHostError::AccountProcedureIndexMapError( - "account procedure data has invalid length.".to_string(), - )); - } - - // One procedure requires 8 values to represent - let num_procs = proc_data.len() / AccountProcedureInfo::NUM_ELEMENTS_PER_PROC; - - // check that the account code does not contain too many procedures - if num_procs > AccountCode::MAX_NUM_PROCEDURES { - return Err(TransactionHostError::AccountProcedureIndexMapError( - "account code contains too many procedures.".to_string(), - )); - } - - for (proc_idx, proc_info) in - proc_data.chunks_exact(AccountProcedureInfo::NUM_ELEMENTS_PER_PROC).enumerate() - { - let proc_info_array: [Felt; AccountProcedureInfo::NUM_ELEMENTS_PER_PROC] = - proc_info.try_into().expect("Failed conversion into procedure info array."); - - let procedure = AccountProcedureInfo::try_from(proc_info_array) - .map_err(TransactionHostError::AccountProcedureInfoCreationFailed)?; - - let proc_idx = u8::try_from(proc_idx).expect("Invalid procedure index."); - - account_procs_map.insert(*procedure.mast_root(), proc_idx); - } - - Ok(account_procs_map) -} diff --git a/crates/miden-tx/src/host/kernel_process.rs b/crates/miden-tx/src/host/kernel_process.rs new file mode 100644 index 0000000000..5140cc52e7 --- /dev/null +++ b/crates/miden-tx/src/host/kernel_process.rs @@ -0,0 +1,233 @@ +use miden_lib::transaction::memory::{ + ACCOUNT_STACK_TOP_PTR, + ACTIVE_INPUT_NOTE_PTR, + NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR, +}; +use miden_objects::account::AccountId; +use miden_objects::note::{NoteId, NoteInputs}; +use miden_objects::{Word, ZERO}; +use miden_processor::{EventError, ExecutionError, Felt, ProcessState}; + +use crate::errors::TransactionKernelError; + +// TRANSACTION KERNEL PROCESS +// ================================================================================================ + +pub(super) trait TransactionKernelProcess { + fn get_active_account_id(&self) -> Result; + + fn get_num_storage_slots(&self) -> Result; + + fn get_vault_root(&self, vault_root_ptr: Felt) -> Result; + + fn get_active_note_id(&self) -> Result, EventError>; + + fn read_note_recipient_info_from_adv_map( + &self, + recipient_digest: Word, + ) -> Result<(NoteInputs, Word, Word), TransactionKernelError>; + + fn read_note_inputs_from_adv_map( + &self, + inputs_commitment: &Word, + ) -> Result; + + fn has_advice_map_entry(&self, key: Word) -> bool; +} + +impl<'a> TransactionKernelProcess for ProcessState<'a> { + /// Returns the ID of the currently active account. + fn get_active_account_id(&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") + })?; + let account_stack_top_ptr = u32::try_from(account_stack_top_ptr).map_err(|_| { + TransactionKernelError::other("account stack top ptr should fit into a u32") + })?; + + 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") + })?; + + let active_account_id_and_nonce = self + .get_mem_word(self.ctx(), active_account_ptr) + .map_err(|_| { + TransactionKernelError::other("active account ptr should be word-aligned") + })? + .ok_or_else(|| { + TransactionKernelError::other("active account id should be initialized") + })?; + + AccountId::try_from([active_account_id_and_nonce[1], active_account_id_and_nonce[0]]) + .map_err(|_| { + TransactionKernelError::other( + "active account id ptr should point to a valid account ID", + ) + }) + } + + /// Returns the number of storage slots initialized for the active account. + /// + /// # Errors + /// Returns an error if the memory location supposed to contain the account storage slot number + /// has not been initialized. + fn get_num_storage_slots(&self) -> Result { + let num_storage_slots_felt = self + .get_mem_value(self.ctx(), NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR) + .ok_or(TransactionKernelError::AccountStorageSlotsNumMissing( + NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR, + ))?; + + Ok(num_storage_slots_felt.as_int()) + } + + /// 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> { + // 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) { + Some(addr) => addr, + None => return Ok(None), + }; + // convert note address into u32 + let note_address = u32::try_from(note_address_felt).map_err(|_| { + EventError::from(format!( + "failed to convert {note_address_felt} into a memory address (u32)" + )) + })?; + // if `note_address` == 0 note execution has ended and there is no valid note address + if note_address == 0 { + Ok(None) + } else { + Ok(self + .get_mem_word(self.ctx(), note_address) + .map_err(ExecutionError::MemoryError)? + .map(NoteId::from)) + } + } + + /// Returns the vault root at the provided pointer. + fn get_vault_root(&self, vault_root_ptr: Felt) -> Result { + 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}" + )) + })?; + self.get_mem_word(self.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" + )) + }) + } + + fn read_note_recipient_info_from_adv_map( + &self, + recipient_digest: Word, + ) -> Result<(NoteInputs, Word, Word), TransactionKernelError> { + let (sn_script_hash, inputs_commitment) = + read_double_word_from_adv_map(self, recipient_digest)?; + let (sn_hash, script_root) = read_double_word_from_adv_map(self, sn_script_hash)?; + let (serial_num, _) = read_double_word_from_adv_map(self, sn_hash)?; + + let inputs = self.read_note_inputs_from_adv_map(&inputs_commitment)?; + + Ok((inputs, script_root, serial_num)) + } + + /// Extracts and validates note inputs from the advice provider using trial unhashing. + /// + /// This function tries to determine the correct number of inputs by: + /// 1. Finding the last non-zero element as a starting point + /// 2. Building NoteInputs and checking if the hash matches inputs_commitment + /// 3. If not, incrementing num_inputs and trying again (up to 6 more times) + /// 4. If num_inputs grows to the size of inputs_data and there's still no match, returning an + /// error + fn read_note_inputs_from_adv_map( + &self, + inputs_commitment: &Word, + ) -> Result { + let inputs_data = self.advice_provider().get_mapped_values(inputs_commitment); + + let inputs = match inputs_data { + None => NoteInputs::default(), + Some(inputs) => { + // Start with the last non-zero element as a hint + let initial_num_inputs = + inputs.iter().rposition(|&x| x != ZERO).map(|pos| pos + 1).unwrap_or(0); + + // Try different input counts using trial unhashing + let mut num_inputs = initial_num_inputs; + + loop { + let candidate_inputs = NoteInputs::new(inputs[0..num_inputs].to_vec()) + .map_err(TransactionKernelError::MalformedNoteInputs)?; + + if candidate_inputs.commitment() == *inputs_commitment { + return Ok(candidate_inputs); + } + + num_inputs += 1; + if num_inputs > inputs.len() { + break; + } + } + + // If we've exhausted all attempts, return an error + return Err(TransactionKernelError::InvalidNoteInputs { + expected: *inputs_commitment, + actual: NoteInputs::new(inputs[0..num_inputs.min(inputs.len())].to_vec()) + .map(|i| i.commitment()) + .unwrap_or_default(), + }); + }, + }; + + Ok(inputs) + } + + fn has_advice_map_entry(&self, key: Word) -> bool { + self.advice_provider().get_mapped_values(&key).is_some() + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Reads a double word (two [`Word`]s, 8 [`Felt`]s total) from the advice map. +/// +/// # Errors +/// Returns an error if the key is not present in the advice map or if the data is malformed +/// (not exactly 8 elements). +fn read_double_word_from_adv_map( + process: &ProcessState, + key: Word, +) -> Result<(Word, Word), TransactionKernelError> { + let data = process + .advice_provider() + .get_mapped_values(&key) + .ok_or_else(|| TransactionKernelError::MalformedRecipientData(vec![]))?; + + if data.len() != 8 { + return Err(TransactionKernelError::MalformedRecipientData(data.to_vec())); + } + + let first_word = Word::new([data[0], data[1], data[2], data[3]]); + let second_word = Word::new([data[4], data[5], data[6], data[7]]); + + Ok((first_word, second_word)) +} diff --git a/crates/miden-tx/src/host/link_map.rs b/crates/miden-tx/src/host/link_map.rs index 3bfe0d5ce2..de96421edf 100644 --- a/crates/miden-tx/src/host/link_map.rs +++ b/crates/miden-tx/src/host/link_map.rs @@ -2,6 +2,7 @@ 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}; // LINK MAP @@ -16,11 +17,11 @@ use miden_processor::{AdviceMutation, ContextId, EventError, ProcessState}; /// # Warning /// /// The functions on this type assume that the provided map_ptr points to a valid map in the -/// provided process. If those assumptions are violated, the functions may panic. -#[derive(Debug, Clone, Copy)] +/// provided memory viewer. If those assumptions are violated, the functions may panic. +#[derive(Clone, Copy)] pub struct LinkMap<'process> { map_ptr: u32, - process: &'process ProcessState<'process>, + mem: &'process MemoryViewer<'process>, } impl<'process> LinkMap<'process> { @@ -28,10 +29,10 @@ impl<'process> LinkMap<'process> { // -------------------------------------------------------------------------------------------- /// Creates a new link map from the provided map_ptr in the provided process. - pub fn new(map_ptr: Felt, process: &'process ProcessState<'process>) -> Self { + pub fn new(map_ptr: Felt, mem: &'process MemoryViewer<'process>) -> Self { let map_ptr: u32 = map_ptr.try_into().expect("map_ptr must be a valid u32"); - Self { map_ptr, process } + Self { map_ptr, mem } } // PUBLIC METHODS @@ -42,15 +43,11 @@ 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> { - let map_ptr = process.get_stack_item(0); - let map_key = Word::from([ - process.get_stack_item(4), - process.get_stack_item(3), - process.get_stack_item(2), - process.get_stack_item(1), - ]); + let map_ptr = process.get_stack_item(1); + let map_key = process.get_stack_word_be(2); - let link_map = LinkMap::new(map_ptr, process); + let mem_viewer = MemoryViewer::ProcessState(process); + let link_map = LinkMap::new(map_ptr, &mem_viewer); let (set_op, entry_ptr) = link_map.compute_set_operation(LexicographicWord::from(map_key)); @@ -65,15 +62,11 @@ impl<'process> LinkMap<'process> { /// 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> { - let map_ptr = process.get_stack_item(0); - let map_key = Word::from([ - process.get_stack_item(4), - process.get_stack_item(3), - process.get_stack_item(2), - process.get_stack_item(1), - ]); - - let link_map = LinkMap::new(map_ptr, process); + let map_ptr = process.get_stack_item(1); + let map_key = process.get_stack_word_be(2); + + let mem_viewer = MemoryViewer::ProcessState(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([ @@ -103,7 +96,7 @@ impl<'process> LinkMap<'process> { // Returns None if the value was either not yet initialized or points to 0. // It can point to 0 for example if a get operation is executed before a set operation, // which initializes the value in memory to 0 but does not change it. - self.get_kernel_mem_value(self.map_ptr).and_then(|head_ptr| { + self.mem.get_kernel_mem_element(self.map_ptr).and_then(|head_ptr| { if head_ptr == ZERO { None } else { @@ -130,23 +123,29 @@ impl<'process> LinkMap<'process> { /// Returns the key of the entry at the given pointer. fn key(&self, entry_ptr: u32) -> LexicographicWord { LexicographicWord::from( - self.get_kernel_mem_word(entry_ptr + 4).expect("entry pointer should be valid"), + self.mem + .get_kernel_mem_word(entry_ptr + 4) + .expect("entry pointer should be valid"), ) } /// Returns the values of the entry at the given pointer. fn value(&self, entry_ptr: u32) -> (Word, Word) { - let value0 = - self.get_kernel_mem_word(entry_ptr + 8).expect("entry pointer should be valid"); - let value1 = - self.get_kernel_mem_word(entry_ptr + 12).expect("entry pointer should be valid"); + let value0 = self + .mem + .get_kernel_mem_word(entry_ptr + 8) + .expect("entry pointer should be valid"); + let value1 = self + .mem + .get_kernel_mem_word(entry_ptr + 12) + .expect("entry pointer should be valid"); (value0, value1) } /// Returns the metadata of the entry at the given pointer. fn metadata(&self, entry_ptr: u32) -> EntryMetadata { let entry_metadata = - self.get_kernel_mem_word(entry_ptr).expect("entry pointer should be valid"); + self.mem.get_kernel_mem_word(entry_ptr).expect("entry pointer should be valid"); let map_ptr = entry_metadata[0]; let map_ptr = map_ptr.try_into().expect("entry_ptr should point to a u32 map_ptr"); @@ -222,19 +221,6 @@ impl<'process> LinkMap<'process> { }; (get_op, entry_ptr) } - - // HELPER METHODS - // -------------------------------------------------------------------------------------------- - - fn get_kernel_mem_value(&self, addr: u32) -> Option { - self.process.get_mem_value(ContextId::root(), addr) - } - - fn get_kernel_mem_word(&self, addr: u32) -> Option { - self.process - .get_mem_word(ContextId::root(), addr) - .expect("address should be word-aligned") - } } // LINK MAP ITER @@ -307,3 +293,63 @@ enum SetOperation { InsertAtHead = 1, InsertAfterEntry = 2, } + +// MEMORY VIEWER +// ================================================================================================ + +/// A abstraction over ways to view a process' memory. +/// +/// More specifically, it allows using a [`LinkMap`] both with a [`ProcessState`], i.e. a process +/// that is actively executing and also an [`ExecutionOutput`], i.e. a process that has finished +/// execution. +/// +/// This should all go away again once we change a LinkMap's implementation to be based on an actual +/// map type instead of viewing a process' memory directly. +pub enum MemoryViewer<'mem> { + ProcessState(&'mem ProcessState<'mem>), + ExecutionOutputs(&'mem ExecutionOutput), +} + +impl<'mem> MemoryViewer<'mem> { + /// Reads an element from transaction kernel memory. + fn get_kernel_mem_element(&self, addr: u32) -> Option { + match self { + MemoryViewer::ProcessState(process_state) => { + process_state.get_mem_value(ContextId::root(), addr) + }, + MemoryViewer::ExecutionOutputs(_execution_output) => { + // 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; + + Some(self.get_kernel_mem_word(word_addr)?[idx as usize]) + }, + } + } + + /// Reads a word from transaction kernel memory. + fn get_kernel_mem_word(&self, addr: u32) -> Option { + match self { + MemoryViewer::ProcessState(process_state) => process_state + .get_mem_word(ContextId::root(), addr) + .expect("address should be word-aligned"), + MemoryViewer::ExecutionOutputs(execution_output) => { + let tx_kernel_context = ContextId::root(); + let clk = 0u32; + let err_ctx = (); + + // Note that this never returns None even if the location is uninitialized, but the + // link map does not rely on this. + Some( + execution_output + .memory + .read_word(tx_kernel_context, Felt::from(addr), clk.into(), &err_ctx) + .expect("expected address to be word-aligned"), + ) + }, + } + } +} diff --git a/crates/miden-tx/src/host/mod.rs b/crates/miden-tx/src/host/mod.rs index e1fdbd7787..c62091420c 100644 --- a/crates/miden-tx/src/host/mod.rs +++ b/crates/miden-tx/src/host/mod.rs @@ -1,32 +1,44 @@ mod account_delta_tracker; use account_delta_tracker::AccountDeltaTracker; - mod storage_delta_tracker; mod link_map; -pub use link_map::LinkMap; +pub use link_map::{LinkMap, MemoryViewer}; mod account_procedures; pub use account_procedures::AccountProcedureIndexMap; -mod note_builder; +pub(crate) mod note_builder; +use miden_lib::StdLibrary; use note_builder::OutputNoteBuilder; +mod kernel_process; +use kernel_process::TransactionKernelProcess; + mod script_mast_forest_store; pub use script_mast_forest_store::ScriptMastForestStore; mod tx_progress; + use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::memory::{CURRENT_INPUT_NOTE_PTR, NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR}; -use miden_lib::transaction::{TransactionEvent, TransactionEventError, TransactionKernelError}; -use miden_objects::account::{AccountDelta, PartialAccount}; -use miden_objects::asset::{Asset, FungibleAsset}; -use miden_objects::note::NoteId; +use miden_lib::transaction::{EventId, TransactionEvent, TransactionEventError}; +use miden_objects::account::{ + AccountCode, + AccountDelta, + AccountHeader, + AccountId, + AccountStorageHeader, + PartialAccount, + StorageMap, + StorageSlotType, +}; +use miden_objects::asset::{Asset, AssetVault, AssetVaultKey, FungibleAsset}; +use miden_objects::note::{NoteId, NoteInputs, NoteMetadata, NoteRecipient, NoteScript}; use miden_objects::transaction::{ InputNote, InputNotes, @@ -38,10 +50,11 @@ use miden_objects::transaction::{ use miden_objects::vm::RowIndex; use miden_objects::{Hasher, Word}; use miden_processor::{ + AdviceError, AdviceMutation, ContextId, EventError, - ExecutionError, + EventHandlerRegistry, Felt, MastForest, MastForestStore, @@ -50,6 +63,7 @@ use miden_processor::{ pub use tx_progress::TransactionProgress; use crate::auth::SigningInputs; +use crate::errors::{TransactionHostError, TransactionKernelError}; // TRANSACTION BASE HOST // ================================================================================================ @@ -64,6 +78,12 @@ pub struct TransactionBaseHost<'store, STORE> { /// include input note scripts and the transaction script, but not account code. scripts_mast_store: ScriptMastForestStore, + /// The header of the account at the beginning of transaction execution. + initial_account_header: AccountHeader, + + /// The storage header of the native account at the beginning of transaction execution. + initial_account_storage_header: AccountStorageHeader, + /// Account state changes accumulated during transaction execution. /// /// The delta is updated by event handlers. @@ -84,6 +104,9 @@ pub struct TransactionBaseHost<'store, STORE> { /// /// The progress is updated event handlers. tx_progress: TransactionProgress, + + /// Handle the VM default events _before_ passing it to user defined ones. + stdlib_handlers: EventHandlerRegistry, } impl<'store, STORE> TransactionBaseHost<'store, STORE> @@ -101,17 +124,28 @@ where scripts_mast_store: ScriptMastForestStore, acct_procedure_index_map: AccountProcedureIndexMap, ) -> Self { + let stdlib_handlers = { + let mut registry = EventHandlerRegistry::new(); + + let stdlib = StdLibrary::default(); + for (event_id, handler) in stdlib.handlers() { + registry + .register(event_id, handler) + .expect("There are no duplicates in the stdlibrary handlers"); + } + registry + }; Self { mast_store, scripts_mast_store, - account_delta: AccountDeltaTracker::new( - account.id(), - account.storage().header().clone(), - ), + initial_account_header: account.into(), + initial_account_storage_header: account.storage().header().clone(), + account_delta: AccountDeltaTracker::new(account), acct_procedure_index_map, output_notes: BTreeMap::default(), input_notes, tx_progress: TransactionProgress::default(), + stdlib_handlers, } } @@ -132,6 +166,18 @@ where &self.tx_progress } + /// Returns a reference to the initial account header of the native account, which represents + /// the state at the beginning of the transaction. + pub fn initial_account_header(&self) -> &AccountHeader { + &self.initial_account_header + } + + /// Returns a reference to the initial storage header of the native account, which represents + /// the state at the beginning of the transaction. + pub fn initial_account_storage_header(&self) -> &AccountStorageHeader { + &self.initial_account_storage_header + } + /// Returns a reference to the account delta tracker of this transaction host. pub fn account_delta_tracker(&self) -> &AccountDeltaTracker { &self.account_delta @@ -143,7 +189,6 @@ where } /// Returns the input notes consumed in this transaction. - #[allow(unused)] pub fn input_notes(&self) -> InputNotes { self.input_notes.clone() } @@ -155,10 +200,47 @@ where } /// Consumes `self` and returns the account delta, output notes and transaction progress. - pub fn into_parts(self) -> (AccountDelta, Vec, TransactionProgress) { + pub fn into_parts( + self, + ) -> (AccountDelta, InputNotes, Vec, TransactionProgress) { let output_notes = self.output_notes.into_values().map(|builder| builder.build()).collect(); - (self.account_delta.into_delta(), output_notes, self.tx_progress) + ( + self.account_delta.into_delta(), + self.input_notes, + output_notes, + self.tx_progress, + ) + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Inserts an output note builder at the specified index. + /// + /// # Errors + /// 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, + note_builder: OutputNoteBuilder, + ) -> Result<(), TransactionKernelError> { + if self.output_notes.contains_key(¬e_idx) { + return Err(TransactionKernelError::other(format!( + "Attempted to create note builder for note index {} twice", + note_idx + ))); + } + self.output_notes.insert(note_idx, note_builder); + Ok(()) + } + + /// Returns a mutable reference to the [`AccountProcedureIndexMap`]. + pub fn load_foreign_account_code( + &mut self, + account_code: &AccountCode, + ) -> Result<(), TransactionHostError> { + self.acct_procedure_index_map.insert_code(account_code) } // EVENT HANDLERS @@ -169,30 +251,58 @@ where pub(super) fn handle_event( &mut self, process: &ProcessState, - transaction_event: TransactionEvent, + 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 as u32))); + return Err(Box::new(TransactionEventError::NotRootContext(transaction_event))); } let advice_mutations = match transaction_event { - TransactionEvent::AccountVaultBeforeAddAsset => Ok(TransactionEventHandling::Handled(Vec::new())), + 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 => Ok(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 => Ok(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())) }, @@ -209,7 +319,7 @@ where }, TransactionEvent::NoteBeforeCreated => Ok(TransactionEventHandling::Handled(Vec::new())), - TransactionEvent::NoteAfterCreated => self.on_note_after_created(process).map(|_| 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())), @@ -235,7 +345,7 @@ where }, TransactionEvent::NoteExecutionStart => { - let note_id = Self::get_current_note_id(process)?.expect( + 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); @@ -259,19 +369,27 @@ where self.tx_progress.start_epilogue(process.clk()); Ok(TransactionEventHandling::Handled(Vec::new())) } - TransactionEvent::EpilogueTxCyclesObtained => { + 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::EpilogueTxFeeComputed => self.on_tx_fee_computed(process), + 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::LinkMapSetEvent => { + TransactionEvent::LinkMapSet => { return LinkMap::handle_set_event(process).map(TransactionEventHandling::Handled); }, - TransactionEvent::LinkMapGetEvent => { + TransactionEvent::LinkMapGet => { return LinkMap::handle_get_event(process).map(TransactionEventHandling::Handled); }, TransactionEvent::Unauthorized => { @@ -284,9 +402,31 @@ where Ok(advice_mutations) } + /// 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, + })) + } + /// Pushes a signature to the advice stack as a response to the `AuthRequest` event. /// - /// Expected stack state: `[MESSAGE, PUB_KEY]` + /// Expected stack state: `[event, MESSAGE, PUB_KEY]` /// /// 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 @@ -296,8 +436,8 @@ where &self, process: &ProcessState, ) -> Result { - let message = process.get_stack_word(0); - let pub_key_hash = process.get_stack_word(1); + 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)?; @@ -334,9 +474,7 @@ where /// /// Expected stack state: /// - /// ```text - /// [MESSAGE] - /// ``` + /// `[event, MESSAGE]` /// /// Expected advice map state: /// @@ -344,7 +482,7 @@ where /// MESSAGE -> [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT] /// ``` fn on_unauthorized(&self, process: &ProcessState) -> TransactionKernelError { - let msg = process.get_stack_word(0); + let msg = process.get_stack_word_be(1); let tx_summary = match self.build_tx_summary(process, msg) { Ok(s) => s, @@ -360,18 +498,19 @@ where TransactionKernelError::Unauthorized(Box::new(tx_summary)) } - /// Extracts all necessary data to handle [`TransactionEvent::EpilogueTxFeeComputed`]. + /// Extracts all necessary data to handle + /// [`TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount`]. /// /// Expected stack state: /// /// ```text - /// [FEE_ASSET] + /// `[event, FEE_ASSET]` /// ``` - fn on_tx_fee_computed( + fn on_before_tx_fee_removed_from_account( &self, process: &ProcessState, ) -> Result { - let fee_asset = process.get_stack_word(0); + let fee_asset = process.get_stack_word_be(1); let fee_asset = FungibleAsset::try_from(fee_asset) .map_err(TransactionKernelError::FailedToConvertFeeAsset)?; @@ -380,31 +519,64 @@ where )) } - /// Creates a new [OutputNoteBuilder] from the data on the operand stack and stores it into the - /// `output_notes` field of this [`TransactionBaseHost`]. + /// 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: `[NOTE_METADATA, RECIPIENT, ...]` + /// Expected stack state: `[event, NOTE_METADATA, note_ptr, RECIPIENT, note_idx]` fn on_note_after_created( &mut self, process: &ProcessState, - ) -> Result<(), TransactionKernelError> { - let stack = process.get_stack_state(); - // # => [NOTE_METADATA] - - let note_idx: usize = stack[9].as_int() as usize; - - assert_eq!(note_idx, self.output_notes.len(), "note index mismatch"); - - let note_builder = OutputNoteBuilder::new(stack, process.advice_provider())?; + ) -> 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 + }; - self.output_notes.insert(note_idx, note_builder); + let note_builder = OutputNoteBuilder::new(metadata, recipient_digest, recipient)?; + self.insert_output_note_builder(note_idx, note_builder)?; - Ok(()) + Ok(TransactionEventHandling::Handled(Vec::new())) } /// Adds an asset at the top of the [OutputNoteBuilder] identified by the note pointer. /// - /// Expected stack state: [ASSET, note_ptr, num_of_assets, note_idx] + /// Expected stack state: `[event, ASSET, note_ptr, num_of_assets, note_idx]` fn on_note_before_add_asset( &mut self, process: &ProcessState, @@ -412,11 +584,12 @@ where let stack = process.get_stack_state(); //# => [ASSET, note_ptr, num_of_assets, note_idx] - let note_idx = stack[6].as_int(); + 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 = Asset::try_from(process.get_stack_word(0)).map_err(|source| { + 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, @@ -435,7 +608,7 @@ where /// Loads the index of the procedure root onto the advice stack. /// - /// Expected stack state: [PROC_ROOT, ...] + /// Expected stack state: `[event, PROC_ROOT, ...]` fn on_account_push_procedure_index( &mut self, process: &ProcessState, @@ -460,16 +633,16 @@ where /// Extracts information from the process state about the storage slot being updated and /// records the latest value of this storage slot. /// - /// Expected stack state: [slot_index, NEW_SLOT_VALUE, CURRENT_SLOT_VALUE, ...] + /// Expected stack state: `[event, slot_index, NEW_SLOT_VALUE, CURRENT_SLOT_VALUE, ...]` 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(0); + let slot_index = process.get_stack_item(1); // get number of storage slots initialized by the account - let num_storage_slot = Self::get_num_storage_slots(process)?; + let num_storage_slot = process.get_num_storage_slots()?; if slot_index.as_int() >= num_storage_slot { return Err(TransactionKernelError::InvalidStorageSlotIndex { @@ -479,20 +652,10 @@ where } // get the value to which the slot is being updated - let new_slot_value = Word::new([ - process.get_stack_item(4), - process.get_stack_item(3), - process.get_stack_item(2), - process.get_stack_item(1), - ]); + let new_slot_value = process.get_stack_word_be(2); // get the current value for the slot - let current_slot_value = Word::new([ - process.get_stack_item(8), - process.get_stack_item(7), - process.get_stack_item(6), - process.get_stack_item(5), - ]); + let current_slot_value = process.get_stack_word_be(6); self.account_delta.storage().set_item( slot_index.as_int() as u8, @@ -503,19 +666,117 @@ where 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, + ) + } + + /// 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, + }, + )) + } + } + /// Extracts information from the process state about the storage map being updated and /// records the latest values of this storage map. /// - /// Expected stack state: [slot_index, KEY, PREV_MAP_VALUE, NEW_MAP_VALUE] + /// Expected stack state: `[event, slot_index, KEY, PREV_MAP_VALUE, NEW_MAP_VALUE]` 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(0); + let slot_index = process.get_stack_item(1); // get number of storage slots initialized by the account - let num_storage_slot = Self::get_num_storage_slots(process)?; + let num_storage_slot = process.get_num_storage_slots()?; if slot_index.as_int() >= num_storage_slot { return Err(TransactionKernelError::InvalidStorageSlotIndex { @@ -525,28 +786,13 @@ where } // get the KEY to which the slot is being updated - let key = Word::new([ - process.get_stack_item(4), - process.get_stack_item(3), - process.get_stack_item(2), - process.get_stack_item(1), - ]); + let key = process.get_stack_word_be(2); // get the previous VALUE of the slot - let prev_map_value = Word::new([ - process.get_stack_item(8), - process.get_stack_item(7), - process.get_stack_item(6), - process.get_stack_item(5), - ]); + let prev_map_value = process.get_stack_word_be(6); // get the VALUE to which the slot is being updated - let new_map_value = Word::new([ - process.get_stack_item(12), - process.get_stack_item(11), - process.get_stack_item(10), - process.get_stack_item(9), - ]); + let new_map_value = process.get_stack_word_be(10); self.account_delta.storage().set_map_item( slot_index.as_int() as u8, @@ -564,12 +810,12 @@ where /// 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: [ASSET, ...] + /// Expected stack state: `[event, ASSET, ...]` pub fn on_account_vault_after_add_asset( &mut self, process: &ProcessState, ) -> Result<(), TransactionKernelError> { - let asset: Asset = process.get_stack_word(0).try_into().map_err(|source| { + let asset: Asset = process.get_stack_word_be(1).try_into().map_err(|source| { TransactionKernelError::MalformedAssetInEventHandler { handler: "on_account_vault_after_add_asset", source, @@ -583,15 +829,53 @@ where 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) + } + /// 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: [ASSET, ...] + /// Expected stack state: `[event, ASSET, ...]` pub fn on_account_vault_after_remove_asset( &mut self, process: &ProcessState, ) -> Result<(), TransactionKernelError> { - let asset: Asset = process.get_stack_word(0).try_into().map_err(|source| { + let asset: Asset = process.get_stack_word_be(1).try_into().map_err(|source| { TransactionKernelError::MalformedAssetInEventHandler { handler: "on_account_vault_after_remove_asset", source, @@ -605,54 +889,96 @@ where Ok(()) } - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Returns the ID of the currently executing input note, or None if the note execution hasn't - /// started yet or has already ended. + /// 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. /// - /// # Errors - /// Returns an error if the address of the currently executing input note is invalid (e.g., - /// greater than `u32::MAX`). - fn get_current_note_id(process: &ProcessState) -> Result, EventError> { - // get the note address in `Felt` or return `None` if the address hasn't been accessed - // previously. - let note_address_felt = match process.get_mem_value(process.ctx(), CURRENT_INPUT_NOTE_PTR) { - Some(addr) => addr, - None => return Ok(None), - }; - // convert note address into u32 - let note_address = u32::try_from(note_address_felt).map_err(|_| { - EventError::from(format!( - "failed to convert {note_address_felt} into a memory address (u32)" + /// 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" )) })?; - // if `note_address` == 0 note execution has ended and there is no valid note address - if note_address == 0 { - Ok(None) - } else { - Ok(process - .get_mem_word(process.ctx(), note_address) - .map_err(ExecutionError::MemoryError)? - .map(NoteId::from)) - } + self.on_account_vault_asset_accessed(process, vault_key, vault_root) } - /// Returns the number of storage slots initialized for the current account. + /// 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. /// - /// # Errors - /// Returns an error if the memory location supposed to contain the account storage slot number - /// has not been initialized. - fn get_num_storage_slots(process: &ProcessState) -> Result { - let num_storage_slots_felt = process - .get_mem_value(process.ctx(), NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR) - .ok_or(TransactionKernelError::AccountStorageSlotsNumMissing( - NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR, - ))?; + /// 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)?; - Ok(num_storage_slots_felt.as_int()) + 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, + }, + )) + } + } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + /// Builds a [TransactionSummary] by extracting data from the advice provider and validating /// commitments against the host's state. pub(crate) fn build_tx_summary( @@ -719,16 +1045,16 @@ where } } -/// Extracts a word from a slice of field elements. -pub(crate) fn extract_word(commitments: &[Felt], start: usize) -> Word { - Word::from([ - commitments[start], - commitments[start + 1], - commitments[start + 2], - commitments[start + 3], - ]) +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. @@ -754,4 +1080,79 @@ pub(super) enum TransactionEventData { /// 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, + )), + } +} + +/// 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 ae921bd120..d4016f6e65 100644 --- a/crates/miden-tx/src/host/note_builder.rs +++ b/crates/miden-tx/src/host/note_builder.rs @@ -1,19 +1,8 @@ -use alloc::boxed::Box; -use alloc::vec::Vec; - use miden_objects::asset::Asset; -use miden_objects::note::{ - Note, - NoteAssets, - NoteInputs, - NoteMetadata, - NoteRecipient, - NoteScript, - PartialNote, -}; -use miden_processor::AdviceProvider; +use miden_objects::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, PartialNote}; -use super::{Felt, OutputNote, TransactionKernelError, Word}; +use super::{OutputNote, Word}; +use crate::errors::TransactionKernelError; // OUTPUT NOTE BUILDER // ================================================================================================ @@ -31,96 +20,35 @@ impl OutputNoteBuilder { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- - /// Returns a new [OutputNoteBuilder] read from the provided stack state and advice provider. - /// - /// The stack is expected to be in the following state: - /// - /// [NOTE_METADATA, RECIPIENT] - /// - /// Detailed note info such as assets and recipient (when available) are retrieved from the - /// advice provider. + /// Returns a new [OutputNoteBuilder] from the provided metadata, recipient digest, and optional + /// recipient. /// /// # Errors - /// Returns an error if: - /// - Note type specified via the stack is malformed. - /// - Sender account ID specified via the stack is invalid. - /// - A combination of note type, sender account ID, and note tag do not form a valid - /// [NoteMetadata] object. - /// - Recipient information in the advice provider is present but is malformed. - /// - A non-private note is missing recipient details. + /// Returns an error if the note is public but no recipient is provided. pub fn new( - stack: Vec, - adv_provider: &AdviceProvider, + metadata: NoteMetadata, + recipient_digest: Word, + recipient: Option, ) -> Result { - // read note metadata info from the stack and build the metadata object - let metadata_word = Word::from([stack[3], stack[2], stack[1], stack[0]]); - let metadata: NoteMetadata = metadata_word - .try_into() - .map_err(TransactionKernelError::MalformedNoteMetadata)?; - - // read recipient digest from the stack and try to build note recipient object if there is - // enough info available in the advice provider - let recipient_digest = Word::new([stack[8], stack[7], stack[6], stack[5]]); - - // This method returns an error if the mapped value is not found. - let recipient = if let Some(data) = adv_provider.get_mapped_values(&recipient_digest) { - if data.len() != 13 { - return Err(TransactionKernelError::MalformedRecipientData(data.to_vec())); - } - let inputs_commitment = Word::new([data[1], data[2], data[3], data[4]]); - let script_root = Word::new([data[5], data[6], data[7], data[8]]); - let serial_num = Word::from([data[9], data[10], data[11], data[12]]); - let script_data = adv_provider.get_mapped_values(&script_root).unwrap_or(&[]); - - let inputs_data = adv_provider.get_mapped_values(&inputs_commitment); - let inputs = match inputs_data { - None => NoteInputs::default(), - Some(inputs) => { - let num_inputs = data[0].as_int() as usize; - - // There must be at least `num_inputs` elements in the advice provider data, - // otherwise it is an error. - // - // It is possible to have more elements because of padding. The extra elements - // will be discarded below, and later their contents will be validated by - // computing the commitment and checking against the expected value. - if num_inputs > inputs.len() { - return Err(TransactionKernelError::TooFewElementsForNoteInputs { - specified: num_inputs as u64, - actual: inputs.len() as u64, - }); - } - - NoteInputs::new(inputs[0..num_inputs].to_vec()) - .map_err(TransactionKernelError::MalformedNoteInputs)? - }, - }; - - if inputs.commitment() != inputs_commitment { - return Err(TransactionKernelError::InvalidNoteInputs { - expected: inputs_commitment, - actual: inputs.commitment(), - }); - } - - let script = NoteScript::try_from(script_data).map_err(|source| { - TransactionKernelError::MalformedNoteScript { - data: script_data.to_vec(), - source: Box::new(source), - } - })?; - let recipient = NoteRecipient::new(serial_num, script, inputs); - - Some(recipient) - } else if metadata.is_private() { - None - } else { - // if there are no recipient details and the note is not private, return an error + // For public notes, we must have a recipient + if !metadata.is_private() && recipient.is_none() { 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, 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 c6949dd0a3..071f84208b 100644 --- a/crates/miden-tx/src/host/script_mast_forest_store.rs +++ b/crates/miden-tx/src/host/script_mast_forest_store.rs @@ -12,6 +12,7 @@ use miden_processor::MastForestStore; /// /// A [ScriptMastForestStore] is meant to exclusively store MAST forests related to both /// transaction and input note scripts. +#[derive(Debug, Clone, Default)] pub struct ScriptMastForestStore { mast_forests: BTreeMap>, advice_map: AdviceMap, diff --git a/crates/miden-tx/src/host/storage_delta_tracker.rs b/crates/miden-tx/src/host/storage_delta_tracker.rs index 317cb9d569..c5dabc8a4d 100644 --- a/crates/miden-tx/src/host/storage_delta_tracker.rs +++ b/crates/miden-tx/src/host/storage_delta_tracker.rs @@ -1,7 +1,13 @@ use alloc::collections::BTreeMap; use miden_objects::Word; -use miden_objects::account::{AccountStorageDelta, AccountStorageHeader}; +use miden_objects::account::{ + AccountStorageDelta, + AccountStorageHeader, + PartialAccount, + StorageMap, + StorageSlotType, +}; /// Keeps track of the initial storage of an account during transaction execution. /// @@ -30,13 +36,53 @@ impl StorageDeltaTracker { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Constructs a new initial account storage from a storage header. - pub fn new(storage_header: AccountStorageHeader) -> Self { - Self { - storage_header, + /// Constructs a new initial storage delta from the provided account. + /// + /// If the account is new, inserts the storage entries into the delta analogously to the + /// transaction kernel delta. + pub fn new(account: &PartialAccount) -> Self { + let initial_storage_header = if account.is_new() { + empty_storage_header_from_account(account) + } else { + account.storage().header().clone() + }; + + let mut storage_delta_tracker = Self { + storage_header: initial_storage_header, init_maps: BTreeMap::new(), delta: AccountStorageDelta::new(), + }; + + // 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 { + StorageSlotType::Value => { + // Note that we can insert the value unconditionally as the delta will be + // normalized before the commitment is computed. + storage_delta_tracker.set_item(slot_idx, Word::empty(), *value); + }, + StorageSlotType::Map => { + let storage_map = account + .storage() + .maps() + .find(|map| map.root() == *value) + .expect("storage map should be present in partial storage"); + + storage_map.entries().for_each(|(key, value)| { + storage_delta_tracker.set_map_item( + slot_idx, + *key, + Word::empty(), + *value, + ); + }); + }, + }, + ); } + + storage_delta_tracker } // PUBLIC MUTATORS @@ -117,3 +163,17 @@ impl StorageDeltaTracker { .expect("storage delta should still be valid since no new values were added") } } + +/// 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 + .storage() + .header() + .slots() + .map(|(slot_type, _)| match slot_type { + StorageSlotType::Value => (*slot_type, Word::empty()), + StorageSlotType::Map => (*slot_type, StorageMap::new().root()), + }) + .collect(); + AccountStorageHeader::new(slots) +} diff --git a/crates/miden-tx/src/host/tx_progress.rs b/crates/miden-tx/src/host/tx_progress.rs index 9c54eb11dc..fc8be54d65 100644 --- a/crates/miden-tx/src/host/tx_progress.rs +++ b/crates/miden-tx/src/host/tx_progress.rs @@ -14,6 +14,7 @@ pub struct TransactionProgress { note_execution: Vec<(NoteId, CycleInterval)>, tx_script_processing: CycleInterval, epilogue: CycleInterval, + auth_procedure: CycleInterval, /// The cycle count of the processor at the point where compute_fee called clk to obtain the /// transaction's cycle count. /// @@ -34,6 +35,7 @@ impl TransactionProgress { note_execution: Vec::new(), tx_script_processing: CycleInterval::default(), epilogue: CycleInterval::default(), + auth_procedure: CycleInterval::default(), epilogue_after_tx_cycles_obtained: None, } } @@ -61,6 +63,10 @@ impl TransactionProgress { &self.epilogue } + pub fn auth_procedure(&self) -> &CycleInterval { + &self.auth_procedure + } + // STATE MUTATORS // -------------------------------------------------------------------------------------------- @@ -102,6 +108,14 @@ impl TransactionProgress { self.epilogue.set_start(cycle); } + pub fn start_auth_procedure(&mut self, cycle: RowIndex) { + self.auth_procedure.set_start(cycle); + } + + pub fn end_auth_procedure(&mut self, cycle: RowIndex) { + self.auth_procedure.set_end(cycle); + } + pub fn epilogue_after_tx_cycles_obtained(&mut self, cycle: RowIndex) { self.epilogue_after_tx_cycles_obtained = Some(cycle); } @@ -133,6 +147,8 @@ impl From for TransactionMeasurements { let epilogue = tx_progress.epilogue().len(); + let auth_procedure = tx_progress.auth_procedure().len(); + // Compute the number of cycles that where not captured by the call to clk. let after_tx_cycles_obtained = if let Some(epilogue_after_tx_cycles_obtained) = tx_progress.epilogue_after_tx_cycles_obtained @@ -149,6 +165,7 @@ impl From for TransactionMeasurements { note_execution, tx_script_processing, epilogue, + auth_procedure, after_tx_cycles_obtained, } } diff --git a/crates/miden-tx/src/lib.rs b/crates/miden-tx/src/lib.rs index ad7c89fd6f..8b3f225574 100644 --- a/crates/miden-tx/src/lib.rs +++ b/crates/miden-tx/src/lib.rs @@ -6,23 +6,21 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -pub use miden_objects::transaction::TransactionInputs; - mod executor; pub use executor::{ DataStore, ExecutionOptions, FailedNote, + MAX_NUM_CHECKER_NOTES, MastForestStore, NoteConsumptionChecker, NoteConsumptionInfo, - NoteConsumptionStatus, TransactionExecutor, TransactionExecutorHost, }; mod host; -pub use host::{AccountProcedureIndexMap, LinkMap, ScriptMastForestStore}; +pub use host::{AccountProcedureIndexMap, LinkMap, MemoryViewer, ScriptMastForestStore}; mod prover; pub use prover::{ @@ -41,6 +39,7 @@ pub use errors::{ DataStoreError, NoteCheckerError, TransactionExecutorError, + TransactionKernelError, TransactionProverError, TransactionVerifierError, }; diff --git a/crates/miden-tx/src/prover/mod.rs b/crates/miden-tx/src/prover/mod.rs index 92235070b1..29c3d5f576 100644 --- a/crates/miden-tx/src/prover/mod.rs +++ b/crates/miden-tx/src/prover/mod.rs @@ -3,15 +3,20 @@ 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::{ + InputNote, + InputNotes, OutputNote, ProvenTransaction, ProvenTransactionBuilder, - TransactionWitness, + TransactionInputs, + TransactionOutputs, }; pub use miden_prover::ProvingOptions; -use miden_prover::prove; +use miden_prover::{ExecutionProof, Word, prove}; use super::TransactionProverError; use crate::host::{AccountProcedureIndexMap, ScriptMastForestStore}; @@ -39,94 +44,27 @@ impl LocalTransactionProver { proof_options, } } -} - -impl Default for LocalTransactionProver { - fn default() -> Self { - Self { - mast_store: Arc::new(TransactionMastStore::new()), - proof_options: Default::default(), - } - } -} -impl LocalTransactionProver { - pub fn prove( + fn build_proven_transaction( &self, - tx_witness: TransactionWitness, + input_notes: &InputNotes, + tx_outputs: TransactionOutputs, + pre_fee_account_delta: AccountDelta, + account: PartialAccount, + ref_block_num: BlockNumber, + ref_block_commitment: Word, + proof: ExecutionProof, ) -> Result { - let mast_store = self.mast_store.clone(); - let proof_options = self.proof_options.clone(); - - let TransactionWitness { tx_inputs, tx_args, advice_witness } = tx_witness; - - let account = tx_inputs.account(); - let input_notes = tx_inputs.input_notes(); - let ref_block_num = tx_inputs.block_header().block_num(); - let ref_block_commitment = tx_inputs.block_header().commitment(); - - // execute and prove - let (stack_inputs, advice_inputs) = - TransactionKernel::prepare_inputs(&tx_inputs, &tx_args, Some(advice_witness)) - .map_err(TransactionProverError::ConflictingAdviceMapEntry)?; - - // load the store with account/note/tx_script MASTs - self.mast_store.load_account_code(account.code()); - for account_inputs in tx_args.foreign_account_inputs() { - self.mast_store.load_account_code(account_inputs.code()); - } - - let script_mast_store = ScriptMastForestStore::new( - tx_args.tx_script(), - input_notes.iter().map(|n| n.note().script()), - ); - - let mut host = { - let acct_procedure_index_map = AccountProcedureIndexMap::from_transaction_params( - &tx_inputs, - &tx_args, - &advice_inputs, - ) - .map_err(TransactionProverError::TransactionHostCreationFailed)?; - - TransactionProverHost::new( - &account.into(), - input_notes.clone(), - mast_store.as_ref(), - script_mast_store, - acct_procedure_index_map, - ) - }; - - let advice_inputs = advice_inputs.into_advice_inputs(); - - let (stack_outputs, proof) = prove( - &TransactionKernel::main(), - stack_inputs, - advice_inputs.clone(), - &mut host, - proof_options, - ) - .map_err(TransactionProverError::TransactionProgramExecutionFailed)?; - - // 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, output_notes, _tx_progress) = host.into_parts(); - let tx_outputs = - TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes) - .map_err(TransactionProverError::TransactionOutputConstructionFailed)?; - // erase private note information (convert private full notes to just headers) let output_notes: Vec<_> = tx_outputs.output_notes.iter().map(OutputNote::shrink).collect(); // Compute the commitment of the pre-fee delta, which goes into the proven transaction, // since it is the output of the transaction and so is needed for proof verification. - let pre_fee_delta_commitment = pre_fee_account_delta.to_commitment(); + let pre_fee_delta_commitment: Word = pre_fee_account_delta.to_commitment(); let builder = ProvenTransactionBuilder::new( account.id(), - account.init_commitment(), + account.initial_commitment(), tx_outputs.account.commitment(), pre_fee_delta_commitment, ref_block_num, @@ -145,20 +83,9 @@ impl LocalTransactionProver { .remove_asset(Asset::from(tx_outputs.fee)) .map_err(TransactionProverError::RemoveFeeAssetFromDelta)?; - // If the account is on-chain, add the update details. - let builder = match account.is_onchain() { + let builder = match account.has_public_state() { true => { - let account_update_details = if account.is_new() { - let mut account = account.clone(); - account - .apply_delta(&post_fee_account_delta) - .map_err(TransactionProverError::AccountDeltaApplyFailed)?; - - AccountUpdateDetails::New(account) - } else { - AccountUpdateDetails::Delta(post_fee_account_delta) - }; - + let account_update_details = AccountUpdateDetails::Delta(post_fee_account_delta); builder.account_update_details(account_update_details) }, false => builder, @@ -166,4 +93,97 @@ impl LocalTransactionProver { builder.build().map_err(TransactionProverError::ProvenTransactionBuildFailed) } + + pub fn prove( + &self, + 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)?; + + self.mast_store.load_account_code(tx_inputs.account().code()); + for account_code in tx_inputs.foreign_account_code() { + self.mast_store.load_account_code(account_code); + } + + let script_mast_store = ScriptMastForestStore::new( + tx_inputs.tx_script(), + tx_inputs.input_notes().iter().map(|n| n.note().script()), + ); + + 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( + &partial_account, + input_notes, + self.mast_store.as_ref(), + script_mast_store, + account_procedure_index_map, + ); + + let advice_inputs = advice_inputs.into_advice_inputs(); + + let (stack_outputs, proof) = prove( + &TransactionKernel::main(), + stack_inputs, + advice_inputs.clone(), + &mut host, + self.proof_options.clone(), + ) + .map_err(TransactionProverError::TransactionProgramExecutionFailed)?; + + // 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 tx_outputs = + TransactionKernel::from_transaction_parts(&stack_outputs, &advice_inputs, output_notes) + .map_err(TransactionProverError::TransactionOutputConstructionFailed)?; + + self.build_proven_transaction( + &input_notes, + tx_outputs, + pre_fee_account_delta, + partial_account, + ref_block.block_num(), + ref_block.commitment(), + proof, + ) + } +} + +impl Default for LocalTransactionProver { + fn default() -> Self { + Self { + mast_store: Arc::new(TransactionMastStore::new()), + proof_options: Default::default(), + } + } +} + +#[cfg(any(feature = "testing", test))] +impl LocalTransactionProver { + pub fn prove_dummy( + &self, + executed_transaction: miden_objects::transaction::ExecutedTransaction, + ) -> Result { + let (tx_inputs, tx_outputs, account_delta, _) = executed_transaction.into_parts(); + + let (partial_account, ref_block, _, input_notes, _) = tx_inputs.into_parts(); + + self.build_proven_transaction( + &input_notes, + tx_outputs, + account_delta, + partial_account, + ref_block.block_num(), + ref_block.commitment(), + ExecutionProof::new_dummy(), + ) + } } diff --git a/crates/miden-tx/src/prover/prover_host.rs b/crates/miden-tx/src/prover/prover_host.rs index 2204cd3884..6f948217eb 100644 --- a/crates/miden-tx/src/prover/prover_host.rs +++ b/crates/miden-tx/src/prover/prover_host.rs @@ -1,8 +1,7 @@ -use alloc::boxed::Box; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_lib::transaction::TransactionEvent; +use miden_lib::transaction::EventId; use miden_objects::Word; use miden_objects::account::{AccountDelta, PartialAccount}; use miden_objects::assembly::debuginfo::Location; @@ -72,7 +71,9 @@ where } /// Consumes `self` and returns the account delta, output notes and transaction progress. - pub fn into_parts(self) -> (AccountDelta, Vec, TransactionProgress) { + pub fn into_parts( + self, + ) -> (AccountDelta, InputNotes, Vec, TransactionProgress) { self.base_host.into_parts() } } @@ -84,10 +85,6 @@ impl BaseHost for TransactionProverHost<'_, STORE> where STORE: MastForestStore, { - fn get_mast_forest(&self, procedure_root: &Word) -> Option> { - self.base_host.get_mast_forest(procedure_root) - } - fn get_label_and_source_file( &self, _location: &Location, @@ -103,14 +100,14 @@ impl SyncHost for TransactionProverHost<'_, STORE> where STORE: MastForestStore, { - fn on_event( - &mut self, - process: &ProcessState, - event_id: u32, - ) -> Result, EventError> { - let transaction_event = TransactionEvent::try_from(event_id).map_err(Box::new)?; + fn get_mast_forest(&self, node_digest: &Word) -> Option> { + self.base_host.get_mast_forest(node_digest) + } + + 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, transaction_event)? { + 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 @@ -121,6 +118,14 @@ where 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. diff --git a/crates/miden-tx/src/verifier/mod.rs b/crates/miden-tx/src/verifier/mod.rs index f9f51a10b5..396e92dd85 100644 --- a/crates/miden-tx/src/verifier/mod.rs +++ b/crates/miden-tx/src/verifier/mod.rs @@ -25,7 +25,7 @@ impl TransactionVerifier { Self { tx_program_info, proof_security_level } } - /// Verifies the provided [ProvenTransaction] against the transaction kernel. + /// Verifies the provided [`ProvenTransaction`] against the transaction kernel. /// /// # Errors /// Returns an error if: diff --git a/docs/.gitignore b/docs/.gitignore index 7585238efe..e4068b5b4b 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,3 @@ -book +.docusaurus/ +build/ +node_modules/ diff --git a/docs/book.toml b/docs/book.toml deleted file mode 100644 index fa063417bd..0000000000 --- a/docs/book.toml +++ /dev/null @@ -1,17 +0,0 @@ -[book] -authors = ["Miden contributors"] -description = "Description and core structures for the Miden protocol" -language = "en" -multilingual = false -title = "The Miden protocol" - -[output.html] -additional-css = ["custom.css"] -git-repository-url = "https://github.com/0xMiden/miden-base" - -[preprocessor.katex] -after = ["links"] - -[preprocessor.alerts] - -[output.linkcheck] diff --git a/docs/custom.css b/docs/custom.css deleted file mode 100644 index 3f6f556d6c..0000000000 --- a/docs/custom.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - --content-max-width: 1000px; -} diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts new file mode 100644 index 0000000000..ef8eee7646 --- /dev/null +++ b/docs/docusaurus.config.ts @@ -0,0 +1,129 @@ +import type { Config } from "@docusaurus/types"; +import { themes as prismThemes } from "prism-react-renderer"; + +// If your content lives in docs/src, set DOCS_PATH='src'; else '.' +const DOCS_PATH = + process.env.DOCS_PATH || (require("fs").existsSync("src") ? "src" : "."); + +const config: Config = { + title: "Docs Dev Preview", + url: "http://localhost:3000", + baseUrl: "/", + trailingSlash: false, + + // Minimal classic preset: docs only, autogenerated sidebars, same math plugins as prod + presets: [ + [ + "classic", + { + docs: { + path: DOCS_PATH, // '../docs' is implied because we are already inside docs/ + routeBasePath: "/", // mount docs at root for quick preview + sidebarPath: "./sidebars.ts", + remarkPlugins: [require("remark-math")], + rehypePlugins: [require("rehype-katex")], + versions: { + current: { + label: `unstable`, + }, + }, + }, + blog: false, + pages: false, + theme: { + customCss: "./styles.css", + }, + }, + ], + ], + + plugins: [ + [ + "@cmfcmf/docusaurus-search-local", + { + // whether to index docs pages + indexDocs: true, + + // whether to index blog pages + indexBlog: false, + + // whether to index static pages + indexPages: false, + + // language of your documentation, see next section + language: "en", + + // setting this to "none" will prevent the default CSS to be included. The default CSS + // comes from autocomplete-theme-classic, which you can read more about here: + // https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-theme-classic/ + style: undefined, + + // lunr.js-specific settings + lunr: { + // When indexing your documents, their content is split into "tokens". + // Text entered into the search box is also tokenized. + // This setting configures the separator used to determine where to split the text into tokens. + // By default, it splits the text at whitespace and dashes. + // + // Note: Does not work for "ja" and "th" languages, since these use a different tokenizer. + tokenizerSeparator: /[\s\-]+/, + // https://lunrjs.com/guides/customising.html#similarity-tuning + // + // This parameter controls the importance given to the length of a document and its fields. This + // value must be between 0 and 1, and by default it has a value of 0.75. Reducing this value + // reduces the effect of different length documents on a term's importance to that document. + b: 0.75, + // This controls how quickly the boost given by a common word reaches saturation. Increasing it + // will slow down the rate of saturation and lower values result in quicker saturation. The + // default value is 1.2. If the collection of documents being indexed have high occurrences + // of words that are not covered by a stop word filter, these words can quickly dominate any + // similarity calculation. In these cases, this value can be reduced to get more balanced results. + k1: 1.2, + // By default, we rank pages where the search term appears in the title higher than pages where + // the search term appears in just the text. This is done by "boosting" title matches with a + // higher value than content matches. The concrete boosting behavior can be controlled by changing + // the following settings. + titleBoost: 5, + contentBoost: 1, + tagsBoost: 3, + parentCategoriesBoost: 2, // Only used when indexing is enabled for categories + }, + }, + ], + ], + + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + { + colorMode: { + defaultMode: "light", + disableSwitch: true, + }, + prism: { + theme: prismThemes.oneLight, + darkTheme: prismThemes.oneDark, + additionalLanguages: ["rust", "solidity", "toml", "yaml"], + }, + navbar: { + logo: { + src: "img/logo.png", + alt: "Miden Logo", + height: 240, + }, + title: "MIDEN", + items: [ + { + type: "docsVersionDropdown", + position: "left", + dropdownActiveClassDisabled: true, + }, + { + href: "https://github.com/0xMiden/", + label: "GitHub", + position: "right", + }, + ], + }, + }, +}; +export default config; diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000000..cd68240f34 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,19971 @@ +{ + "name": "@miden/docs-dev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@miden/docs-dev", + "devDependencies": { + "@cmfcmf/docusaurus-search-local": "^2.0.0", + "@docusaurus/core": "^3", + "@docusaurus/preset-classic": "^3", + "rehype-katex": "^7", + "remark-math": "^6" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.30.tgz", + "integrity": "sha512-QdrSUryr/CLcsCISokLHOImcHj1adGXk1yy4B3qipqLhcNc33Kj/O/3crI790Qp85oDx7sc4vm7R4raf9RA/kg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.10.tgz", + "integrity": "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.54.tgz", + "integrity": "sha512-wbszephe+WR7y8DXwYN/SMr56pwU1N505/h3fOTz4NPHCW0sex6LzZ7DuhFR0J6ij3n3sLA11aEliFEa6B2FiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.10", + "ai": "5.0.54", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.4.0.tgz", + "integrity": "sha512-N0blWT/C0KOZ/OJ9GXBX66odJZlrYjMj3M+01y8ob1mjBFnBaBo7gOCyHBDQy60+H4pJXp3pSGlJOqJIueBH+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@algolia/autocomplete-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-js/-/autocomplete-js-1.19.4.tgz", + "integrity": "sha512-ZkwsuTTIEuw+hbsIooMrNLvTVulUSSKqJT3ZeYYd//kA5fHaFf2/T0BDmd9qSGxZRhT5WS8AJYjFARLmj5x08g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.19.4", + "@algolia/autocomplete-preset-algolia": "1.19.4", + "@algolia/autocomplete-shared": "1.19.4", + "htm": "^3.1.1", + "preact": "^10.13.2" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.5.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-js/node_modules/@algolia/autocomplete-core": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.4.tgz", + "integrity": "sha512-yVwXLrfwQ3dAndY12j1pfa0oyC5hTDv+/dgwvVHj57dY3zN6PbAmcHdV5DOOdGJrCMXff+fsPr8G2Ik8zWOPTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.4", + "@algolia/autocomplete-shared": "1.19.4" + } + }, + "node_modules/@algolia/autocomplete-js/node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.4.tgz", + "integrity": "sha512-K6TQhTKxx0Es1ZbjlAQjgm/QLDOtKvw23MX0xmpvO7AwkmlmaEXo2PwHdVSs3Bquv28CkO2BYKks7jVSIdcXUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.4" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-js/node_modules/@algolia/autocomplete-shared": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.4.tgz", + "integrity": "sha512-V7tYDgRXP0AqL4alwZBWNm1HPWjJvEU94Nr7Qa2cuPcIAbsTAj7M/F/+Pv/iwOWXl3N7tzVzNkOWm7sX6JT1SQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.19.4.tgz", + "integrity": "sha512-WhX4mYosy7yBDjkB6c/ag+WKICjvV2fqQv/+NWJlpvnk2JtMaZByi73F6svpQX945J+/PxpQe8YIRBZHuYsLAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.4" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia/node_modules/@algolia/autocomplete-shared": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.4.tgz", + "integrity": "sha512-V7tYDgRXP0AqL4alwZBWNm1HPWjJvEU94Nr7Qa2cuPcIAbsTAj7M/F/+Pv/iwOWXl3N7tzVzNkOWm7sX6JT1SQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-theme-classic": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.4.tgz", + "integrity": "sha512-/qE8BETNFbul4WrrUyBYgaaKcgFPk0Px9FDKADnr3HlIkXquRpcFHTxXK16jdwXb33yrcXaAVSQZRfUUSSnxVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.25.2.tgz", + "integrity": "sha512-tA1rqAafI+gUdewjZwyTsZVxesl22MTgLWRKt1+TBiL26NiKx7SjRqTI3pzm8ngx1ftM5LSgXkVIgk2+SRgPTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.25.2" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.25.2.tgz", + "integrity": "sha512-E+aZwwwmhvZXsRA1+8DhH2JJIwugBzHivASTnoq7bmv0nmForLyH7rMG5cOTiDK36DDLnKq1rMGzxWZZ70KZag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.25.2.tgz", + "integrity": "sha512-KYcenhfPKgR+WJ6IEwKVEFMKKCWLZdnYuw08+3Pn1cxAXbJcTIKjoYgEXzEW6gJmDaau2l55qNrZo6MBbX7+sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.25.2" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.38.0.tgz", + "integrity": "sha512-15d6zv8vtj2l9pnnp/EH7Rhq3/snCCHRz56NnX6xIUPrbJl5gCsIYXAz8C2IEkwOpoDb0r5G6ArY2gKdVMNezw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.25.2.tgz", + "integrity": "sha512-IfRGhBxvjli9mdexrCxX2N4XT9NBN3tvZK5zCaL8zkDcgsthiM9WPvGIZS/pl/FuXB7hA0lE5kqOzsQDP6OmGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.25.2", + "@algolia/client-search": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-common": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.25.2.tgz", + "integrity": "sha512-HXX8vbJPYW29P18GxciiwaDpQid6UhpPP9nW9WE181uGUgFhyP5zaEkYWf9oYBrjMubrGwXi5YEzJOz6Oa4faA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-search": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.25.2.tgz", + "integrity": "sha512-pO/LpVnQlbJpcHRk+AroWyyFnh01eOlO6/uLZRUmYvr/hpKZKxI6n7ufgTawbo0KrAu2CePfiOkStYOmDuRjzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.25.2", + "@algolia/requester-common": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.38.0.tgz", + "integrity": "sha512-jJIbYAhYvTG3+gEAP5Q5Dp6PFJfUR+atz5rsqm5KjAKK+faLFdHJbM2IbOo0xdyGd+SH259MzfQKLJ9mZZ27dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.38.0.tgz", + "integrity": "sha512-aMCXzVPGJTeQnVU3Sdf30TfMN2+QyWcjfPTCCHyqVVgjPipb6RnK40aISGoO+rlYjh9LunDsNVFLwv+JEIF8bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.38.0.tgz", + "integrity": "sha512-4c3FbpMiJX+VcaAj0rYaQdTLS/CkrdOn4hW+5y1plPov7KC7iSHai/VBbirmHuAfW1hVPCIh1w/4erKKTKuo+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.38.0.tgz", + "integrity": "sha512-FzLs6c8TBL4FSgNfnH2NL7O33ktecGiaKO4ZFG51QYORUzD5d6YwB9UBteaIYu/sgFoEdY57diYU4vyBH8R6iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.38.0.tgz", + "integrity": "sha512-7apiahlgZLvOqrh0+hAYAp/UWjqz6AfSJrCwnsoQNzgIT09dLSPIKREelkuQeUrKy38vHWWpSQE3M0zWSp/YrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.38.0.tgz", + "integrity": "sha512-PTAFMJOpVtJweExEYYgdmSCC6n4V/R+ctDL3fRQy77ulZM/p+zMLIQC9c7HCQE1zqpauvVck3f2zYSejaUTtrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.38.0.tgz", + "integrity": "sha512-qGSUGgceJHGyJLZ06bFLwVe2Tpf9KwabmoBjFvFscVmMmU5scKya6voCYd9bdX7V0Xy1qya9MGbmTm4zlLuveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/logger-common": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.25.2.tgz", + "integrity": "sha512-aUXpcodoIpLPsnVc2OHgC9E156R7yXWLW2l+Zn24Cyepfq3IvmuVckBvJDpp7nPnXkEzeMuvnVxQfQsk+zP/BA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/logger-console": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.25.2.tgz", + "integrity": "sha512-H3Y+UB0Ty0htvMJ6zDSufhFTSDlg3Pyj3AXilfDdDRcvfhH4C/cJNVm+CTaGORxL5uKABGsBp+SZjsEMTyAunQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/logger-common": "4.25.2" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.38.0.tgz", + "integrity": "sha512-VnCtAUcHirvv/dDHg9jK1Z5oo4QOC5FKDxe40x8qloru2qDcjueT34jiAsB0gRos3VWf9v4iPSYTqMIFOcADpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.38.0.tgz", + "integrity": "sha512-fqgeU9GqxQorFUeGP4et1MyY28ccf9PCeciHwDPSbPYYiTqBItHdUIiytsNpjC5Dnc0RWtuXWCltLwSw9wN/bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.38.0.tgz", + "integrity": "sha512-nAUKbv4YQIXbpPi02AQvSPisD5FDDbT8XeYSh9HFoYP0Z3IpBLLDg7R4ahPvzd7gGsVKgEbXzRPWESXSji5yIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.25.2.tgz", + "integrity": "sha512-Q4wC3sgY0UFjV3Rb3icRLTpPB5/M44A8IxzJHM9PNeK1T3iX7X/fmz7ATUYQYZTpwHCYATlsQKWiTpql1hHjVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.38.0.tgz", + "integrity": "sha512-bkuAHaadC6OxJd3SVyQQnU1oJ9G/zdCqua7fwr1tJDrA/v7KzeS5np4/m6BuRUpTgVgFZHSewGnMcgj9DLBoaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.38.0.tgz", + "integrity": "sha512-yHDKZTnMPR3/4bY0CVC1/uRnnbAaJ+pctRuX7G/HflBkKOrnUBDEGtQQHzEfMz2FHZ/tbCL+Q9r6mvwTSGp8nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.25.2.tgz", + "integrity": "sha512-yw3RLHWc6V+pbdsFtq8b6T5bJqLDqnfKWS7nac1Vzcmgvs/V/Lfy7/6iOF9XRilu5aBDOBHoP1SOeIDghguzWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.25.2", + "@algolia/logger-common": "4.25.2", + "@algolia/requester-common": "4.25.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", + "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", + "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cmfcmf/docusaurus-search-local/-/docusaurus-search-local-2.0.0.tgz", + "integrity": "sha512-WfgqJN5VXeyhcAVTF4isXFNCvZXdOlQZIsXbgKPEmXcXykV+YfPX4UgKJ4J/MuKtaKQ8SGjfeXhg2clhdmwHVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-js": "^1.8.2", + "@algolia/autocomplete-theme-classic": "^1.8.2", + "@algolia/client-search": "^4.12.0", + "algoliasearch": "^4.12.0", + "cheerio": "^1.0.0", + "clsx": "^2.0.0", + "lunr-languages": "^1.4.0", + "mark.js": "^8.11.1", + "tslib": "^2.6.3" + }, + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "nodejieba": "^2.5.0 || ^3.0.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "nodejieba": { + "optional": true + } + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/client-analytics": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.25.2.tgz", + "integrity": "sha512-4Yxxhxh+XjXY8zPyo+h6tQuyoJWDBn8E3YLr8j+YAEy5p+r3/5Tp+ANvQ+hNaQXbwZpyf5d4ViYOBjJ8+bWNEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.25.2", + "@algolia/client-search": "4.25.2", + "@algolia/requester-common": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/client-common": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.25.2.tgz", + "integrity": "sha512-HXX8vbJPYW29P18GxciiwaDpQid6UhpPP9nW9WE181uGUgFhyP5zaEkYWf9oYBrjMubrGwXi5YEzJOz6Oa4faA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/client-personalization": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.25.2.tgz", + "integrity": "sha512-K81PRaHF77mHv2u8foWTHnIf5c+QNf/SnKNM7rB8JPi7TMYi4E5o2mFbgdU1ovd8eg9YMOEAuLkl1Nz1vbM3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.25.2", + "@algolia/requester-common": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/client-search": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.25.2.tgz", + "integrity": "sha512-pO/LpVnQlbJpcHRk+AroWyyFnh01eOlO6/uLZRUmYvr/hpKZKxI6n7ufgTawbo0KrAu2CePfiOkStYOmDuRjzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.25.2", + "@algolia/requester-common": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/recommend": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.25.2.tgz", + "integrity": "sha512-puRrGeXwAuVa4mLdvXvmxHRFz9MkcCOLPcjz7MjU4NihlpIa+lZYgikJ7z0SUAaYgd6l5Bh00hXiU/OlX5ffXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/cache-browser-local-storage": "4.25.2", + "@algolia/cache-common": "4.25.2", + "@algolia/cache-in-memory": "4.25.2", + "@algolia/client-common": "4.25.2", + "@algolia/client-search": "4.25.2", + "@algolia/logger-common": "4.25.2", + "@algolia/logger-console": "4.25.2", + "@algolia/requester-browser-xhr": "4.25.2", + "@algolia/requester-common": "4.25.2", + "@algolia/requester-node-http": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/requester-browser-xhr": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.25.2.tgz", + "integrity": "sha512-aAjfsI0AjWgXLh/xr9eoR8/9HekBkIER3bxGoBf9d1XWMMoTo/q92Da2fewkxwLE6mla95QJ9suJGOtMOewXXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/@algolia/requester-node-http": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.25.2.tgz", + "integrity": "sha512-Ja/FYB7W9ZM+m8UrMIlawNUAKpncvb9Mo+D8Jq5WepGTUyQ9CBYLsjwxv9O8wbj3TSWqTInf4uUBJ2FKR8G7xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/algoliasearch": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.25.2.tgz", + "integrity": "sha512-lYx98L6kb1VvXypbPI7Z54C4BJB2VT5QvOYthvPq6/POufZj+YdyeZSKjoLBKHJgGmYWQTHOKtcCTdKf98WOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/cache-browser-local-storage": "4.25.2", + "@algolia/cache-common": "4.25.2", + "@algolia/cache-in-memory": "4.25.2", + "@algolia/client-account": "4.25.2", + "@algolia/client-analytics": "4.25.2", + "@algolia/client-common": "4.25.2", + "@algolia/client-personalization": "4.25.2", + "@algolia/client-search": "4.25.2", + "@algolia/logger-common": "4.25.2", + "@algolia/logger-console": "4.25.2", + "@algolia/recommend": "4.25.2", + "@algolia/requester-browser-xhr": "4.25.2", + "@algolia/requester-common": "4.25.2", + "@algolia/requester-node-http": "4.25.2", + "@algolia/transporter": "4.25.2" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@cmfcmf/docusaurus-search-local/node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", + "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.1.0.tgz", + "integrity": "sha512-nuNKGjHj/FQeWgE9t+i83QD/V67QiaAmGY7xS9TVCRUiCqSljOgIKlsLoQZKKVwEG8f+OWKdznzZkJxGZ7d06A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.1.0.tgz", + "integrity": "sha512-4GHI7TT3sJZ2Vs4Kjadv7vAkMrTsJqHvzvxO3JA7UT8iPRKaDottG5o5uNshPWhVVaBYPC35Ukf8bfCotGpjSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/css": "4.1.0", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.0.tgz", + "integrity": "sha512-QcZ+Rey0OvlLK9SPN4/+VWL+ut/tuADVdunA1fmC96fytdYjatdJrcw1koYdp/c+3k6lVYlwg9DDVNDecyLCAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/runtime-corejs3": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.9.0", + "@docusaurus/utils": "3.9.0", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.0.tgz", + "integrity": "sha512-HaRLSmiwnJQ3uHBV3rd/BRDM9S/nHAshRk54djRZ+RX9ze4ONuFAovdD5es20ZDj7PRTjo38GVnBtHvuL/SwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.9.0", + "@docusaurus/cssnano-preset": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.11.0", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.2", + "null-loader": "^4.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^6.0.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.0.tgz", + "integrity": "sha512-sEJ4MW/zuh1MfPORCRbSwnW/PjsVmOigWwBU6clcxm221/CNdnI/XqgfBrl2jj/zocSdNoQM8E3IP2W8dygi6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.9.0", + "@docusaurus/bundler": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/mdx-loader": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "detect-port": "^1.5.1", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "execa": "5.1.1", + "fs-extra": "^11.1.1", + "html-tags": "^3.3.1", + "html-webpack-plugin": "^5.6.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "open": "^8.4.0", + "p-map": "^4.0.0", + "prompts": "^2.4.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-router": "^5.3.4", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.4", + "semver": "^7.5.4", + "serve-handler": "^6.1.6", + "tinypool": "^1.0.2", + "tslib": "^2.6.0", + "update-notifier": "^6.0.2", + "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-server": "^5.2.2", + "webpack-merge": "^6.0.1" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mdx-js/react": "^3.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.0.tgz", + "integrity": "sha512-prCJXUcoJZBlovJzSFkfnfWr1gXd53VZfE+17fIpUWS6Zioc7WE4FPoXPi5ldAGZ8brhXre5xQ8NWDE90XP9yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^6.1.2", + "postcss": "^8.5.4", + "postcss-sort-media-queries": "^5.2.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/logger": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.0.tgz", + "integrity": "sha512-lDtThsocWTF8ZrVF01ltfctA/xgtD/3oXWqEkKIDzF4fCWsWXH7hC4LCqT23xSuxZTIo8N+y02XSPvA/8DLInw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.0.tgz", + "integrity": "sha512-9bfJYdkZFE+REwevkT4CYdTJ2f6ydgkbUFylkzTXrNGtBXtx25TRJGdn2cVzm3eVkeWdJrGkG/ypwrIWnbu5UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "@mdx-js/mdx": "^3.0.0", + "@slorber/remark-comment": "^1.0.0", + "escape-html": "^1.0.3", + "estree-util-value-to-estree": "^3.0.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-raw": "^7.0.0", + "remark-directive": "^3.0.0", + "remark-emoji": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "stringify-object": "^3.3.0", + "tslib": "^2.6.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0", + "url-loader": "^4.1.1", + "vfile": "^6.0.1", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.0.tgz", + "integrity": "sha512-0ucYr79FpTCebN+l3ZlKqoW7HbMqSKT8JdsEg6QoUtxD3C7trF6KZiK/X6Yh+xekO1w3zzXYgPcIYTF2DV81tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.0", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.0.tgz", + "integrity": "sha512-XZXJ/rQgi2jT0XWNXOnSKooJgtGHPzkjaBjww6K9PD+YevNMTP9U8Y5sA7cLA5Bwuqrpee4i8NO3tSrjhhDW5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/mdx-loader": "3.9.0", + "@docusaurus/theme-common": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.0.tgz", + "integrity": "sha512-PP+iDJg+lj4cn/7GbbmiguaQ8OX08YxnzQ17KqRC4ufJm11jdyXD33wA7vVtbeG/BkkgkiB/K7YyPHCPwmfVhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/mdx-loader": "3.9.0", + "@docusaurus/module-type-aliases": "3.9.0", + "@docusaurus/theme-common": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "@types/react-router-config": "^5.0.7", + "combine-promises": "^1.1.0", + "fs-extra": "^11.1.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "tslib": "^2.6.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.0.tgz", + "integrity": "sha512-ngetCpAZuivlaHC0l8a5KoK6PQWGuZ8742VwK7dbXeIW0Y70P4xwuocBdsCIQ9J6nB9rlTXRYMpNVyYyCpD7/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/mdx-loader": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.0.tgz", + "integrity": "sha512-giPTCjEzeaamMn8EHY/oDvsPDxF5ei1/q5lPUFQLldbc65jFQ1k6pPwKjtOznYy3TSfClCF1F1DNpYWIx7B5LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.0.tgz", + "integrity": "sha512-DuFOZya+bcrYiL54qBEn2rdKuoWWNyOV5IoHI2MURLzwuYaKu/J9Gi618XUsj3N3qvqG2uxWQiXZcs9ecfMadA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "fs-extra": "^11.1.1", + "react-json-view-lite": "^2.3.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.0.tgz", + "integrity": "sha512-mUXvpasTDR2pXdnkkhGxEgB9frVAvLGc+T3fp6SGT2F+YoEQtjcmz9D43zubQawLn+W1KEhoj+vPusYe+HAl+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.0.tgz", + "integrity": "sha512-L4tCKYnmcyLV6VQs7XWQ3r7YSllagAU2GylZzdvz7NRMcXE12uSW5MCC2aSltbk09MYlqrYv1Ntp+ESsMvptYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "@types/gtag.js": "^0.0.12", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.0.tgz", + "integrity": "sha512-+jWO3tkrvsMUKQ69KTIj9ZBf8sKY5kodLcP4yIaEkPzfWq9IEpE+ekQCtFWlrAmkJUtSxbjHK6HNZZkUNwwq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.0.tgz", + "integrity": "sha512-QOyLooWuF+On4q2RDGVZtKY0tlfdZwD9e/p7g1sJLUfOwN518V2Bo+kZtU82Or42SCKjyJ0lhSqAUOZfbeFhFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "fs-extra": "^11.1.1", + "sitemap": "^7.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.0.tgz", + "integrity": "sha512-pUZIfnhFtAYDmDwimFiBY+sxNUigyJnKbCwI9pTiXr3uGp43CsSsN8gwl/i8jBmqfsZzvNnGZNxc75wy9v6RXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.0.tgz", + "integrity": "sha512-nLoiDxf8bDNNxDSZ28+pFfSfT+QRi08Pn2K0zIvbjkM/X/otMs4ho0K8+2FpoLOoGApifaSuNfJXpGYnQV3rGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/plugin-content-blog": "3.9.0", + "@docusaurus/plugin-content-docs": "3.9.0", + "@docusaurus/plugin-content-pages": "3.9.0", + "@docusaurus/plugin-css-cascade-layers": "3.9.0", + "@docusaurus/plugin-debug": "3.9.0", + "@docusaurus/plugin-google-analytics": "3.9.0", + "@docusaurus/plugin-google-gtag": "3.9.0", + "@docusaurus/plugin-google-tag-manager": "3.9.0", + "@docusaurus/plugin-sitemap": "3.9.0", + "@docusaurus/plugin-svgr": "3.9.0", + "@docusaurus/theme-classic": "3.9.0", + "@docusaurus/theme-common": "3.9.0", + "@docusaurus/theme-search-algolia": "3.9.0", + "@docusaurus/types": "3.9.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.0.tgz", + "integrity": "sha512-RToUIabJOyX41nMIxkFn8LPeA+uHgySzyd6Ak/gsINqWHHTLDMoYPxBUmNm3S+okcfuMI54kNYvD6TY+6TIYDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/mdx-loader": "3.9.0", + "@docusaurus/module-type-aliases": "3.9.0", + "@docusaurus/plugin-content-blog": "3.9.0", + "@docusaurus/plugin-content-docs": "3.9.0", + "@docusaurus/plugin-content-pages": "3.9.0", + "@docusaurus/theme-common": "3.9.0", + "@docusaurus/theme-translations": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "infima": "0.2.0-alpha.45", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.5.4", + "prism-react-renderer": "^2.3.0", + "prismjs": "^1.29.0", + "react-router-dom": "^5.3.4", + "rtlcss": "^4.1.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.0.tgz", + "integrity": "sha512-pqNoQgttIpk7Ndm6N8OGbhi+1wBIQXQPYM7bPf1HDraXfvVpOzhcDty1yyK4coPWl0M7NxednZvKw4atfQ70Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.9.0", + "@docusaurus/module-type-aliases": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^2.3.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.0.tgz", + "integrity": "sha512-nbY7ZJVA10kTiBLJtscxK1aECeYvYFz+Sno9PkCE9KeFXqRDr6omtNmLVkbvyl4b6xgz+6yOIBdO/idLPVDpWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "^3.9.0 || ^4.1.0", + "@docusaurus/core": "3.9.0", + "@docusaurus/logger": "3.9.0", + "@docusaurus/plugin-content-docs": "3.9.0", + "@docusaurus/theme-common": "3.9.0", + "@docusaurus/theme-translations": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-validation": "3.9.0", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", + "clsx": "^2.0.0", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.0.tgz", + "integrity": "sha512-4HUELBsE+rhtlnR1MsaNB9nJXPFZANeDQa5If1GfFVlis5mWUfdmXmbGangR7PfpK2tc56UETMtzjKrX5L5UWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/types": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.0.tgz", + "integrity": "sha512-0klJLhHFHqkYoxIVp1LD7dnU1ASRTfSX+HFDiELOdz+YQUkOSfuU5hDa46zD8bLxrYffCb8FtJI7Z6BWAmVodg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.95.0", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.0.tgz", + "integrity": "sha512-wpVRQbDhXxqbb1llhkpu++aD4UdHHQ5M7J8DmJELDphlwmpI44TdS5elQZOsjzPfGyITZyQLekcDXjyteJ0/bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.0", + "@docusaurus/types": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "github-slugger": "^1.5.0", + "globby": "^11.1.0", + "gray-matter": "^4.0.3", + "jiti": "^1.20.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "p-queue": "^6.6.2", + "prompts": "^2.4.2", + "resolve-pathname": "^3.0.0", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.0.tgz", + "integrity": "sha512-zpmzRn2mniMnrx8ZEYyyDsr0/7EksVgUXL9IuODp0DSK+R19nDGCY7w2NaMGRmGnrQQKsT3t0NDZzBk0V6N9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.0.tgz", + "integrity": "sha512-xpVLdFPpsE5dYuE7hOtghccCrRWRhM6tUQ4YpfSy5snCDWgROITG5Mj22fGstd/HBqTzKD8NFs7qPPs42qjgWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.0", + "@docusaurus/utils": "3.9.0", + "@docusaurus/utils-common": "3.9.0", + "fs-extra": "^11.2.0", + "joi": "^17.9.2", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", + "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", + "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@slorber/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/gtag.js": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", + "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", + "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ai": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.54.tgz", + "integrity": "sha512-eM3EH4VVCWRMfs17r8HF8RtCN/+vBdpWOQoHSVooIfB0BZerOHyrktrVoDP6G6xatUzGLTvJT3rMKLkbPTLPBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "1.0.30", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.10", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.38.0.tgz", + "integrity": "sha512-8VJKIzheeI9cjuVJhU1hYEVetOTe7LvA+CujAI7yqvYsPtZfVEvv1pg9AeFNtHBg/ZoSLGU5LPijhcY5l3Ea9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.4.0", + "@algolia/client-abtesting": "5.38.0", + "@algolia/client-analytics": "5.38.0", + "@algolia/client-common": "5.38.0", + "@algolia/client-insights": "5.38.0", + "@algolia/client-personalization": "5.38.0", + "@algolia/client-query-suggestions": "5.38.0", + "@algolia/client-search": "5.38.0", + "@algolia/ingestion": "1.38.0", + "@algolia/monitoring": "1.38.0", + "@algolia/recommend": "5.38.0", + "@algolia/requester-browser-xhr": "5.38.0", + "@algolia/requester-fetch": "5.38.0", + "@algolia/requester-node-http": "5.38.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz", + "integrity": "sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", + "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", + "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz", + "integrity": "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.4.2.tgz", + "integrity": "sha512-PzjkRkRUS+IHDJohtxkIczlxPPZqRo0nXplsYXOMBRPjcVRjj1W4DfvRgshUYTVuUigU7ptVYkFJQ7abUB0nyg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-port": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.224", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", + "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz", + "integrity": "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", + "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", + "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "dev": true, + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", + "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr-languages": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.14.0.tgz", + "integrity": "sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA==", + "dev": true, + "license": "MPL-1.1" + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", + "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.46.1.tgz", + "integrity": "sha512-2wjHDg7IjP+ufAqqqSxjiNePFDrvWviA79ajUwG9lkHhk3HzZOLBzzoUG8cx9vCagj6VvBQD7oXuLuFz5LaAOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-math/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-math/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "dev": true, + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.4.0.tgz", + "integrity": "sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.0", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.21", + "browserslist": "^4.26.0", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.4.2", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/preact": { + "version": "10.27.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", + "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", + "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.2", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.0", + "unified": "^11.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", + "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", + "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", + "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^7.0.0", + "chalk": "^5.0.1", + "configstore": "^6.0.0", + "has-yarn": "^3.0.0", + "import-lazy": "^4.0.0", + "is-ci": "^3.0.1", + "is-installed-globally": "^0.4.0", + "is-npm": "^6.0.0", + "is-yarn-global": "^0.4.0", + "latest-version": "^7.0.0", + "pupa": "^3.1.0", + "semver": "^7.3.7", + "semver-diff": "^4.0.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-loader/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpackbar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", + "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "figures": "^3.2.0", + "markdown-table": "^2.0.0", + "pretty-time": "^1.1.0", + "std-env": "^3.7.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "webpack": "3 || 4 || 5" + } + }, + "node_modules/webpackbar/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpackbar/node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpackbar/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpackbar/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000000..a59fc51af1 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,16 @@ +{ + "name": "@miden/docs-dev", + "private": true, + "scripts": { + "start:dev": "docusaurus start --port 3000 --host 0.0.0.0", + "build:dev": "docusaurus build", + "serve:dev": "docusaurus serve build" + }, + "devDependencies": { + "@cmfcmf/docusaurus-search-local": "^2.0.0", + "@docusaurus/core": "^3", + "@docusaurus/preset-classic": "^3", + "rehype-katex": "^7", + "remark-math": "^6" + } +} diff --git a/docs/sidebars.ts b/docs/sidebars.ts new file mode 100644 index 0000000000..da531529d6 --- /dev/null +++ b/docs/sidebars.ts @@ -0,0 +1,13 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +/** + * Autogenerate entire sidebar tree from /docs + * This will include: + * - Static docs from this aggregator repo + * - All vendor repos' docs synced into subfolders + */ +const sidebars: SidebarsConfig = { + docs: [{ type: "autogenerated", dirName: "." }], +}; + +export default sidebars; diff --git a/docs/src/EXPORTED.md b/docs/src/EXPORTED.md deleted file mode 100644 index 32ab7d5b2e..0000000000 --- a/docs/src/EXPORTED.md +++ /dev/null @@ -1,17 +0,0 @@ - - -# Summary - -- [Protocol](./index.md) - - [Accounts](./account/overview.md) - - [ID](./account/id.md) - - [Address](./account/address.md) - - [Code](./account/code.md) - - [Storage](./account/storage.md) - - [Component](./account/component.md) - - [Note](./note.md) - - [Asset](./asset.md) - - [Transaction](./transaction.md) - - [State](./state.md) - - [Blockchain](./blockchain.md) - - [Miden Protocol Library](./protocol_library.md) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md deleted file mode 100644 index 66c7abe9f2..0000000000 --- a/docs/src/SUMMARY.md +++ /dev/null @@ -1,17 +0,0 @@ - - -# Summary - -- [Introduction](./index.md) -- [Accounts](./account/overview.md) - - [ID](./account/id.md) - - [Address](./account/address.md) - - [Code](./account/code.md) - - [Storage](./account/storage.md) - - [Component](./account/component.md) -- [Notes](./note.md) -- [Assets](./asset.md) -- [Transactions](./transaction.md) -- [State](./state.md) -- [Blockchain](./blockchain.md) -- [Miden Protocol Library](./protocol_library.md) diff --git a/docs/src/_category_.yml b/docs/src/_category_.yml new file mode 100644 index 0000000000..6a45f5651f --- /dev/null +++ b/docs/src/_category_.yml @@ -0,0 +1,4 @@ +label: "Protocol" +# Determines where this documentation section appears relative to other sections on the main documentation page (which is the parent of this folder in the miden-docs repository) +position: 3 +collapsed: true diff --git a/docs/src/account/_category_.yml b/docs/src/account/_category_.yml new file mode 100644 index 0000000000..8383b5d1df --- /dev/null +++ b/docs/src/account/_category_.yml @@ -0,0 +1,4 @@ +label: "Accounts" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 2 +collapsed: true diff --git a/docs/src/account/address.md b/docs/src/account/address.md index b2ee2b135b..79b457c3c3 100644 --- a/docs/src/account/address.md +++ b/docs/src/account/address.md @@ -1,12 +1,12 @@ -# Address - -> [!Note] -> A human-readable identifier for `Account`s or public keys. +--- +sidebar_position: 3 +--- +# Address ## Purpose -An address is an identifier that facilitates sending and receiving of [notes](../note.md). It serves three main purposes explained in this section. +An address is an extension to account IDs and other identifiers that facilitates sending and receiving of [notes](../note). It serves three main purposes explained in this section. ### Communicating receiver information @@ -16,17 +16,18 @@ 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.md#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 `Address::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 `Address::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 `Address::PublicKey` type must encode the public key.* +- 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.* These different address mechanisms provide different levels of privacy and security: -- `Address::AccountId`: the receiver is uniquely identifiable, but they are the only ones who can consume the note. -- `Address::PoW`: the receiver is not revealed publicly, but potentially many entities can consume the note. The receiver has an advantage by specifying the salt. -- `Address::PublicKey`: the receiver `AccountId` is not revealed publicly, only their public key. A fresh `Address::PublicKey` can be used for receiving each note, resulting in increased privacy. +- `AddressId::AccountId`: the receiver is uniquely identifiable, but they are the only ones 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. -> [!Note] -> The "Pay-to-PoW" and "Pay-to-Public-Key" notes and the corresponding address types are for illustration purposes only. They are not part of the Miden library. +:::note +The "Pay-to-PoW" and "Pay-to-Public-Key" notes and the corresponding address types are for illustration purposes only. They are not part of the Miden library. +::: ### Communicating channel information @@ -34,56 +35,72 @@ 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 [`Address::AccountId`](./address.md#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#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. ### 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.md#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. +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. -## Relationship to Identifiers +## Structure + +An address consists of two parts: +- An identifier that determines what the address fundamentally points to, e.g. an account ID or, in the future, a public key. +- Routing parameters, that customize how a sender creates notes for the receiver, or in other words, how they are routed. + +The separation between these two parts is represented by an underscore (`_`) in the encoded address: + +```text +mm1arp0azyk9jugtgpnnhle8daav58nczzr_qpgqqwcfx0p + | | + account ID routing parameters +``` + +### Relationship to Identifiers + +The routing parameters in an address can encode exactly one account interface, which is a deliberate limitation to keep the size of addresses small. Users can generate multiple addresses for the same identifier like account ID or public key, in order to communicate different interfaces to senders. In other words, there could be multiple different addresses that point to the same account, each encoding a different interface. So, the relationship from addresses to their underlying identifiers is n-to-1. + +As an example, these two addresses contain the same account ID but different routing parameters: + +```text +mm1arp0azyk9jugtgpnnhle8daav58nczzr_qpgqqwcfx0p +mm1arp0azyk9jugtgpnnhle8daav58nczzr_qzsqqd4avz7 +``` + +### Address Types + +The supported **address types** are: +- `AddressId::AccountId` (type `232`): An address pointing to an account ID. + - Choosing `232` as the type byte means that all addresses that encode an account ID start with `mm1a`, where `a` conveniently indicates "account". + +:::note +Adding a public key-based address type is planned. +::: -An address can encode exactly one account interface, which is a deliberate limitation to keep address sizes small. Users can generate multiple addresses for the same identifier like account ID or public key, in order to communicate different interfaces to senders. In other words, there could be multiple different addresses that point to the same account, each encoding a different interface. So, the relationship from addresses to their underlying identifiers is n-to-1. +### Routing Parameters -## Types & Interfaces +The supported routing parameters are detailed in this section. -An address encodes an address type and an address interface: -- The type determines what the address fundamentally points to, e.g. an account ID or, in the future, a public key. -- The interface informs the sender of the capabilities of the receiver's account. +:::note +Adding an encryption key routing parameter is planned. +::: -> [!Note] -> Adding a public key-based address type is planned. +#### Address Interface -The currently supported **address types** are: -- `Address::AccountId` (type `0`): An address pointing to an account ID. +The address interface informs the sender of the capabilities of the [receiver account's interface](./code#interface). -The currently supported **address interfaces** are: -- `Unspecified` (type `0`): No interface is specified. Used for addresses where the interface is unknown. -- `BasicWallet` (type `1`): The standard basic wallet interface. See the [account code](./code.md#interface) docs for details. +The supported **address interfaces** are: +- `BasicWallet` (type `0`): The standard basic wallet interface. See the [account code](./code#interface) docs for details. -### `Address::AccountId` +#### Note Tag Length -The account ID address points to an account ID and also allows specifying the [note tag](../note.md#note-discovery) length. This tag length preference determines how many bits of the account ID are encoded into note tags of notes targeted to this address. This lets the owner of the account choose their level of privacy. A higher tag length makes the account more uniquely identifiable and reduces privacy, while a shorter length increases privacy at the cost of matching more notes published onchain. +The note tag length routing parameter allows specifying the length of the [note tag](../note#note-discovery) that the sender should create. This parameter determines how many bits of the account ID are encoded into note tags of notes targeted to this address. This lets the owner of the account 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. ## Encoding -An address is encoded in [**bech32 format**](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki), which has the following benefits: -- Built-in error detection via checksum algorithm -- Human-readable prefix indicates network type -- Less prone to errors when typed or spoken compared to hex format - -Examples of bech32-encoded addresses that encode the same `Address::AccountId` are: -- `mm1qqttmuqgxur0jup8j8luck774rcqq58se2m`, with the `Unspecified` interface. -- `mm1qqttmuqgxur0jup8j8luck774rcqz8z36ek`, with the `BasicWallet` interface. - -The structure of a bech32-encoded address is: -- [Human-readable prefix](https://github.com/satoshilabs/slips/blob/master/slip-0173.md) that -determines the network: - - `mm` (indicates **M**iden **M**ainnet) - - `mtst` (indicates Miden Testnet) - - `mdev` (indicates Miden Devnet) -- Separator: `1` -- Data part with integrated checksum - -The data part is where the underlying address type is encoded (e.g. `Address::AccountId` with `BasicWallet`). +The two parts of an address are encoded as follows: +- The identifier is encoded in [**bech32**](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki). See the [account ID encoding](id.md#encoding) section for details. +- The routing parameters are encoded in bech32 as well, but without the HRP or `1` separator. + - This means the routing parameter string's alphabet is consistent with that of the address ID. + - It also means the routing parameters have their own checksum, which is important so address ID and routing parameters can be separated at any time without causing validation issues. diff --git a/docs/src/account/code.md b/docs/src/account/code.md index 0a047d74c9..2f5eedf9e5 100644 --- a/docs/src/account/code.md +++ b/docs/src/account/code.md @@ -1,9 +1,15 @@ +--- +sidebar_position: 4 +title: "Code" +--- + # Account Code -> [!Note] -> A collection of procedures defining the `Account`'s programmable interface. +:::note +A collection of procedures defining the `Account`'s programmable interface. +::: -Every Miden `Account` is essentially a smart contract. The `Code` defines the account's procedures, which can be invoked through both [note scripts](../note.md#script) and [transaction scripts](../transaction.md#inputs). Key characteristics include: +Every Miden `Account` is essentially a smart contract. The `Code` defines the account's procedures, which can be invoked through both [note scripts](../note#script) and [transaction scripts](../transaction#inputs). Key characteristics include: - **Mutable access:** Only the `Account`'s own procedures can modify its storage and vault. All state changes — such as updating storage slots or transferring assets — must occur through these procedures. - **Function commitment:** Each function can be called by its [MAST](https://0xMiden.github.io/miden-vm/user_docs/assembly/main.html) root. The root represents the underlying code tree as a 32-byte commitment. This ensures integrity which means a function's behavior cannot change without changing the MAST root. @@ -11,13 +17,14 @@ 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](./component.md). 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.md#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::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. ## Authentication Authenticating a transaction, and therefore the changes to the account, is done with an _authentication procedure_. Every account's code must provide exactly one authentication procedure. It is automatically called during the transaction epilogue, i.e. after all note scripts and the transaction script have been executed. Such an authentication procedure typically inspects the transaction and then decides whether a signature is required to authenticate the changes. It does this by: + - checking which account procedures have been called - Example: Authentication is required if the `distribute` procedure was called but not if `burn` was called. - inspecting the account delta. @@ -26,8 +33,8 @@ Such an authentication procedure typically inspects the transaction and then dec - checking whether notes have been consumed. - checking whether notes have been created. -Recall that an [account's nonce](overview.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. +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 -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. \ No newline at end of file +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. diff --git a/docs/src/account/component.md b/docs/src/account/components.md similarity index 71% rename from docs/src/account/component.md rename to docs/src/account/components.md index 7a5c620c56..b304910423 100644 --- a/docs/src/account/component.md +++ b/docs/src/account/components.md @@ -1,6 +1,11 @@ +--- +sidebar_position: 6 +title: "Components" +--- + # Account 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.md) and [storage](./storage.md). +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. @@ -30,7 +35,7 @@ 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 +description = "This component showcases the component template format, and the different ways of providing valid values to it." version = "1.0.0" supported-types = ["FungibleFaucet"] @@ -58,12 +63,24 @@ 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" } } + { + key = "0xDE0B1140012A9FD912F18AD9EC85E40F4CB697AE", + value = { + name = "value_placeholder", + description = "This value will be defined at the moment of instantiation" + } + } ] +[[storage]] +name = "procedure_thresholds" +description = "Map which stores procedure thresholds (PROC_ROOT -> signature threshold)" +slot = 3 +type = "map" + [[storage]] name = "multislot_entry" -slots = [3,4] +slots = [4,5] values = [ ["0x1","0x2","0x3","0x4"], ["50000","60000","70000","80000"] @@ -72,7 +89,7 @@ values = [ #### Specifying values and their types -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. +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. 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. @@ -127,16 +144,21 @@ In the above example, the first and second storage entries are single-slot value ##### Storage map entries -[Storage maps](./storage.md#map-slots) consist of key-value pairs, where both keys and values are single words. +[Storage maps](./storage#map-slots) consist of key-value pairs, where both keys and values are single words. Storage map entries can specify the following fields: - `slot`: Specifies the slot index in which the root of the map will be placed -- `values`: Contains a list of map entries, defined by a `key` and `value` +- `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. -Where keys and values are word values, which can be defined as placeholders. - -In the example, the third storage entry defines a storage map. +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. ##### Multi-slot value @@ -147,4 +169,26 @@ For multi-slot values, the following fields are expected: - `slots`: Specifies the list of contiguous slots that the value comprises - `values`: Contains the initial storage value for the specified slots -Placeholders can currently not be defined for multi-slot values. In our example, the fourth entry defines a two-slot value. +Placeholders can currently not be defined for multi-slot values. In our example, the fifth entry defines a two-slot value. + +#### Initializing placeholder 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()`. + +For example, the templated map entry above can be populated from TOML as follows: + +```toml +procedure_thresholds = [ + { + key = "0xd2d1b6229d7cfb9f2ada31c5cb61453cf464f91828e124437c708eec55b9cd07", + value = "0x00000000000000000000000000000000000000000000000000000000000001" + }, + { + key = "0x2217cd9963f742fc2d131d86df08f8a2766ed17b73f1519b8d3143ad1c71d32d", + value = ["0", "0", "0", "2"] + } +] +``` + +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. diff --git a/docs/src/account/id.md b/docs/src/account/id.md index 56ebefff07..ee8362b429 100644 --- a/docs/src/account/id.md +++ b/docs/src/account/id.md @@ -1,7 +1,13 @@ +--- +sidebar_position: 2 +title: "ID" +--- + # Account ID -> [!Note] -> An immutable and unique identifier for the `Account`. +:::note +An immutable and unique identifier for the `Account`. +::: The `Account` ID is a 120-bit long number. This identifier is designed to contain the metadata of an account. The metadata includes the [account type](#account-type), [account storage mode](#account-storage-mode) and the version of the `Account`. This metadata is included in the ID to ensure it can be read without needing the full account state. @@ -15,17 +21,18 @@ There are two main categories of accounts in Miden: **basic accounts** and **fau - **Basic Accounts:** Basic Accounts may be either mutable or immutable: - - *Mutable:* Code can be changed after deployment. - - *Immutable:* Code cannot be changed once deployed. + + - _Mutable:_ Code can be changed after deployment. + - _Immutable:_ Code cannot be changed once deployed. - **Faucets:** Faucets are always immutable and can be specialized by the type of assets they issue: - - *Fungible Faucet:* Can issue fungible [assets](../asset.md). - - *Non-fungible Faucet:* Can issue non-fungible [assets](../asset.md). + - _Fungible Faucet:_ Can issue fungible [assets](../asset). + - _Non-fungible Faucet:_ Can issue non-fungible [assets](../asset). ### Account storage mode -Users can choose whether their accounts are stored publicly or privately. The preference is encoded in the third and fourth most significant bits of the account's [ID](#id): +Users can choose whether their accounts are stored publicly or privately. The preference is encoded in the third and fourth most significant bits of the account's ID: - **Public Accounts:** The account's state is stored on-chain, similar to how accounts are stored in public blockchains like Ethereum. @@ -38,13 +45,28 @@ Users can choose whether their accounts are stored publicly or privately. The pr ## Encoding +:::info +Bech32 is the preferred encoding format and should be used for user-facing applications like wallets or websites. +::: + An `Account` ID can be encoded in different formats: -1. [**Address**](./address.md#types--interfaces): - - Used when sending or receiving notes or assets. - - Used to communicate the [account interface](./code.md#interface) between sender and receiver. +1. [**Bech32**](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) (user-facing): + - Example: `mm1apk5f8jqxnadegr46xtklmm78qhdgkwc` + - **Benefits**: + - Built-in error detection via checksum algorithm + - Human-readable prefix indicates network ID + - Less prone to transcription errors + - **Structure**: + - [Human-readable prefix](https://github.com/satoshilabs/slips/blob/master/slip-0173.md) that + determines the network: + - `mm` (indicates **M**iden **M**ainnet) + - `mtst` (indicates Miden Testnet) + - `mdev` (indicates Miden Devnet) + - Separator: `1` + - Data part with integrated checksum 2. **Hexadecimal**: - - Example: `0xd345c9766a2d5e606477a5676b049a` - - Frequently used encoding for blockchain addresses - - Used to identify accounts in command-line interfaces or explorers. + - Example: `0xd7585ada5ab5d2b01c77fad88c0ae4` + - Frequently used encoding for blockchain addresses + - Used to identify accounts in command-line interfaces or explorers. diff --git a/docs/src/account/overview.md b/docs/src/account/index.md similarity index 79% rename from docs/src/account/overview.md rename to docs/src/account/index.md index 2e1df1014a..e43aaee065 100644 --- a/docs/src/account/overview.md +++ b/docs/src/account/index.md @@ -1,6 +1,10 @@ +--- +sidebar_position: 1 +--- + # Accounts / Smart Contracts -An `Account` represents the primary entity in Miden. It is capable of holding assets, storing data, and executing custom code. Each `Account` is a smart contract with a programmable interface through which note and transaction scripts can interact with the account's state and assets. By executing [transactions](../transaction.md) against an account, its state can be modified. +An `Account` represents the primary entity in Miden. It is capable of holding assets, storing data, and executing custom code. Each `Account` is a smart contract with a programmable interface through which note and transaction scripts can interact with the account's state and assets. By executing [transactions](../transaction) against an account, its state can be modified. ## What is the purpose of an account? @@ -10,8 +14,8 @@ In Miden's hybrid UTXO- and account-based model, accounts enable the creation of An `Account` is composed of several core parts, illustrated below: -

- Account diagram +

+ Account diagram

These parts are: @@ -24,15 +28,17 @@ These parts are: ### Vault -> [!Note] -> A collection of [assets](../asset.md) stored by the `Account`. +:::note +A collection of [assets](../asset.md) stored by the `Account`. +::: Large amounts of fungible and non-fungible assets can be stored in the account's vault. ### Nonce -> [!Note] -> A counter incremented with each state update to the `Account`. +:::note +A counter incremented with each state update to the `Account`. +::: The nonce ensures that an account has a unique _commitment_ (or "hash") after every transaction, even if it contains the same assets and has the same storage state. That in turn allows ordering of transactions and prevents replay attacks. Whenever the state of an account changes in a transaction, its nonce must be incremented and it can only be incremented exactly once per transaction. @@ -40,7 +46,7 @@ Note that a transaction does not always change the state of an account. For inst ## Account creation -For an `Account` to be recognized by the network, it must exist in the [account database](../state.md#account-database) maintained by Miden node(s). +For an `Account` to be recognized by the network, it must exist in the [account database](../state#account-database) maintained by Miden node(s). However, a user can locally create a new `Account` ID before it's recognized network-wide. The typical process might be: @@ -49,5 +55,5 @@ However, a user can locally create a new `Account` ID before it's recognized net 3. Alice shares the new ID with Bob to receive an asset. 4. Bob executes a transaction against his account, creating a note with assets for Alice. 5. Alice consumes Bob's note in a transaction against her new account to claim the asset. This -transaction is the first transaction against Alice's account and so it will register the account -ID in the account database. + transaction is the first transaction against Alice's account and so it will register the account + ID in the account database. diff --git a/docs/src/account/storage.md b/docs/src/account/storage.md index 0dac97a6a3..06146ef588 100644 --- a/docs/src/account/storage.md +++ b/docs/src/account/storage.md @@ -1,22 +1,28 @@ +--- +sidebar_position: 5 +title: "Storage" +--- + # Account Storage -> [!Note] -> A flexible, arbitrary data store within the `Account`. +:::note +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. - **Value slots:** Contains 32 bytes of arbitrary data. -- **Map slots:** Contains a [StorageMap](#storagemap), a key-value store where both keys and values are 32 bytes. The slot's value is a commitment to the entire map. +- **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. -An account's storage is typically the result of merging multiple [account components](./component.md). +An account's storage is typically the result of merging multiple [account components](./components). ## Value Slots -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.md#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). ## 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 `account::get_map_item`), its inclusion is proven using a Merkle proof. +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. Key properties of `StorageMap`: diff --git a/docs/src/asset.md b/docs/src/asset.md index 35825b11cc..ce0e15ec85 100644 --- a/docs/src/asset.md +++ b/docs/src/asset.md @@ -1,13 +1,17 @@ +--- +sidebar_position: 4 +--- + # Assets -An `Asset` is a unit of value that can be transferred from one [account](account/overview.md) to another using [notes](note.md). +An `Asset` is a unit of value that can be transferred from one [account](./account) to another using [notes](note). ## What is the purpose of an asset? -In Miden, assets serve as the primary means of expressing and transferring value between [accounts](account/overview.md) through [notes](note.md). They are designed with four key principles in mind: +In Miden, assets serve as the primary means of expressing and transferring value between [accounts](./account) through [notes](note). They are designed with four key principles in mind: 1. **Parallelizable exchange:** - By managing ownership and transfers directly at the account level instead of relying on global structures like ERC20 contracts, accounts can exchange assets concurrently, boosting scalability and efficiency. + By managing ownership and transfers directly at the account level instead of relying on global structures like ERC20 contracts, accounts can exchange assets concurrently, boosting scalability and efficiency. 2. **Self-sovereign ownership:** Assets are stored in the accounts directly. This ensures that users retain complete control over their assets. @@ -15,25 +19,27 @@ In Miden, assets serve as the primary means of expressing and transferring value 3. **Censorship resistance:** Users can transact freely and privately with no single contract or entity controlling `Asset` transfers. This reduces the risk of censored transactions, resulting in a more open and resilient system. -4. **Flexible fee payment:** - Unlike protocols that require a specific base `Asset` for fees, Miden allows users to pay fees in any supported `Asset`. This flexibility simplifies the user experience. +4. **Fee payment in native asset:** + Transaction fees are paid in the chain's native asset as defined by the current reference block's fee parameters. See [Fees](fees.md). ## Native asset -> [!Note] -> All data structures following the Miden asset model that can be exchanged. +:::note +All data structures following the Miden asset model that can be exchanged. +::: -Native assets adhere to the Miden `Asset` model (encoding, issuance, storage). Every native `Asset` is encoded using 32 bytes, including both the [ID](account/id.md) of the issuing account and the `Asset` details. +Native assets adhere to the Miden `Asset` model (encoding, issuance, storage). Every native `Asset` is encoded using 32 bytes, including both the [ID](./account/id) of the issuing account and the `Asset` details. ### Issuance -> [!Note] -> Only [faucet](account/id.md#account-type) accounts can issue assets. +:::note +Only [faucet](./account/id#account-type) accounts can issue assets. +::: Faucets can issue either fungible or non-fungible assets as defined at account creation. The faucet's code specifies the `Asset` minting conditions: i.e., how, when, and by whom these assets can be minted. Once minted, they can be transferred to other accounts using notes. -

- Asset issuance +

+ Asset issuance

### Type @@ -48,10 +54,10 @@ Non-fungible assets are encoded by hashing the `Asset` data into 32 bytes and pl ### Storage -[Accounts](account/overview.md) and [notes](note.md) have vaults used to store assets. Accounts use a sparse Merkle tree as a vault while notes use a simple list. This enables an account to store a practically unlimited number of assets while a note can only store 255 assets. +[Accounts](./account) and [notes](note) have vaults used to store assets. Accounts use a sparse Merkle tree as a vault while notes use a simple list. This enables an account to store a practically unlimited number of assets while a note can only store 255 assets. -

- Asset storage +

+ Asset storage

### Burning @@ -60,7 +66,8 @@ Assets in Miden can be burned through various methods, such as rendering them un ## Alternative asset models -> [!Note] -> All data structures not following the Miden asset model that can be exchanged. +:::note +All data structures not following the Miden asset model that can be exchanged. +::: Miden is flexible enough to support other `Asset` models. For example, developers can replicate Ethereum’s ERC20 pattern, where fungible `Asset` ownership is recorded in a single account. To transact, users send a note to that account, triggering updates in the global hashmap state. diff --git a/docs/src/blockchain.md b/docs/src/blockchain.md index 7db24cb4b1..e6b616886c 100644 --- a/docs/src/blockchain.md +++ b/docs/src/blockchain.md @@ -1,22 +1,26 @@ +--- +sidebar_position: 7 +--- + # Blockchain -The Miden blockchain protocol describes how the [state](state.md) progresses through blocks, which are containers that aggregate account state changes and their proofs, together with created and consumed notes. Blocks represent the delta of the global state between two time periods, and each is accompanied by a corresponding proof that attests to the correctness of all state transitions it contains. The current global state can be derived by applying all the blocks to the genesis state. +The Miden blockchain protocol describes how the [state](state) progresses through blocks, which are containers that aggregate account state changes and their proofs, together with created and consumed notes. Blocks represent the delta of the global state between two time periods, and each is accompanied by a corresponding proof that attests to the correctness of all state transitions it contains. The current global state can be derived by applying all the blocks to the genesis state. Miden's blockchain protocol aims for the following: - **Proven transactions**: All included transactions have already been proven and verified when they reach the block. - **Fast genesis syncing**: New nodes can efficiently sync to the tip of the chain. -

- Execution diagram +

+ Execution diagram

## Batch production To reduce the required space on the blockchain, transaction proofs are not directly put into blocks. First, they are batched together by verifying them in the batch producer. The purpose of the batch producer is to generate a single proof that some number of proven transactions have been verified. This involves recursively verifying individual transaction proofs inside the Miden VM. As with any program that runs in the Miden VM, there is a proof of correct execution running the Miden verifier to verify transaction proofs. This results into a single batch proof. -

- Batch diagram +

+ Batch diagram

The batch producer aggregates transactions sequentially by verifying that their proofs and state transitions are correct. More specifically, the batch producer ensures: @@ -42,7 +46,7 @@ The block producer ensures: 8. **Note erasure of erasable notes**: If an erasable note is created and consumed in different batches, it is erased now. If, however, an erasable note is consumed but not created within the block, the batch it contains is rejected. The Miden operator's mempool should preemptively filter such transactions. In final `Block` contains: -- The commitments to the current global [state](state.md). +- The commitments to the current global [state](state). - The newly created nullifiers. - The commitments to newly created notes. - The new state commitments for affected private accounts. @@ -50,16 +54,18 @@ In final `Block` contains: The `Block` proof attests to the correct state transition from the previous `Block` commitment to the next, and therefore to the change in Miden's global state. -

- Block diagram +

+ Block diagram

-> [!Tip] -> -> **Block Contents:** -> - **State updates**: Contains only the hashes of updated elements. For example, for each updated account, a tuple is recorded as `([account id], [new account hash])`. -> - **ZK Proof**: This proof attests that, given a state commitment from the previous `Block`, a set of valid batches was executed that resulted in the new state commitment. -> - The `Block` also includes the full account and note data for public accounts and notes. For example, if account `123` is a public account that has been updated, you would see a record in the **state updates** section as `(123, 0x456..)`, and the full new state of this account (which should hash to `0x456..`) would be included in a separate section. +:::tip + +**Block Contents:** +- **State updates**: Contains only the hashes of updated elements. For example, for each updated account, a tuple is recorded as `([account id], [new account hash])`. +- **ZK Proof**: This proof attests that, given a state commitment from the previous `Block`, a set of valid batches was executed that resulted in the new state commitment. +- The `Block` also includes the full account and note data for public accounts and notes. For example, if account `123` is a public account that has been updated, you would see a record in the **state updates** section as `(123, 0x456..)`, and the full new state of this account (which should hash to `0x456..`) would be included in a separate section. + +::: ## Verifying blocks diff --git a/docs/src/fees.md b/docs/src/fees.md new file mode 100644 index 0000000000..f6400c3284 --- /dev/null +++ b/docs/src/fees.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 5.1 +--- + +# Fees + +Miden transactions pay a fee that is computed and charged automatically by the transaction kernel during the epilogue. + +## How fees are computed + +- The fee depends on the number of VM cycles the transaction executes and grows logarithmically with that count. +- The kernel estimates the number of verification cycles by taking log2 of the estimated total execution cycles (rounded up). The result is then multiplied by the `verification_base_fee` from the reference block’s fee parameters. +- In other words, the fee is proportional to the logarithm of the transaction’s number of execution cycles, scaled by the base verification fee defined in the block header. + +## Which asset is used to pay fees + +- Fees are paid in the chain’s native asset, defined by the current reference block’s fee parameters. +- The native asset is chosen once as part of the genesis block and then copied to every newly created block, which means the native asset stays consistent for a given network. + +## How fees are paid + +- Users should ensure their account’s vault holds sufficient balance of the native asset to cover the fee. The fee is charged automatically; no explicit transaction kernel API must be called. +- If the account does not contain enough of the native asset to cover the computed fee, the transaction fails during the epilogue. diff --git a/docs/src/index.md b/docs/src/index.md index 1c08f60b8b..947d63c334 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,4 +1,9 @@ -# Miden architecture overview +--- +sidebar_position: 1 +title: Overview +--- + +# Miden Architecture Overview Miden’s architecture departs considerably from typical blockchain designs to support privacy and parallel transaction execution. @@ -8,9 +13,9 @@ However, user generated zero-knowledge proofs allow state transitions, e.g. tran ## Miden design goals -* High throughput: The ability to process a high number of transactions (state changes) over a given time interval. -* Privacy: The ability to keep data known to one’s self and anonymous while processing and/or storing it. -* Asset safety: Maintaining a low risk of mistakes or malicious behavior leading to asset loss. +- High throughput: The ability to process a high number of transactions (state changes) over a given time interval. +- Privacy: The ability to keep data known to one’s self and anonymous while processing and/or storing it. +- Asset safety: Maintaining a low risk of mistakes or malicious behavior leading to asset loss. ## Actor model @@ -24,23 +29,23 @@ Miden uses _accounts_ and _notes_, both of which hold assets. Accounts consume a ### Accounts -An [Account](account/overview.md) can hold assets and define rules how assets can be transferred. Accounts can represent users or autonomous smart contracts. The [account chapter](account/overview.md) describes the design of an account, its storage types, and creating an account. +An [Account](account/index.md) can hold assets and define rules how assets can be transferred. Accounts can represent users or autonomous smart contracts. The [account chapter](account/index.md) describes the design of an account, its storage types, and creating an account. ### Notes -A [Note](note.md) is a message that accounts send to each other. A note stores assets and a script that defines how the note can be consumed. The [note chapter](note.md) describes the design, the storage types, and the creation of a note. +A [Note](note) is a message that accounts send to each other. A note stores assets and a script that defines how the note can be consumed. The [note chapter](note) describes the design, the storage types, and the creation of a note. ### Assets -An [Asset](asset.md) can be fungible and non-fungible. They are stored in the owner’s account itself or in a note. The [asset chapter](asset.md) describes asset issuance, customization, and storage. +An [Asset](asset) can be fungible and non-fungible. They are stored in the owner's account itself or in a note. The [asset chapter](asset) describes asset issuance, customization, and storage. ### Transactions -A [Transactions](transaction.md) describe the production and consumption of notes by a single account. +A [Transactions](transaction) describe the production and consumption of notes by a single account. -Executing a transaction always results in a STARK proof. +Executing a transaction always results in a STARK proof. -The [transaction chapter](transaction.md) describes the transaction design and implementation, including an in-depth discussion of how transaction execution happens in the transaction kernel program. +The [transaction chapter](transaction) describes the transaction design and implementation, including an in-depth discussion of how transaction execution happens in the transaction kernel program. #### Accounts produce and consume notes to communicate @@ -54,11 +59,11 @@ Miden's state model captures the individual states of all accounts and notes, an ### State model -[State](state.md) describes everything that is the case at a certain point in time. Individual states of accounts or notes can be stored on-chain and off-chain. This chapter describes the three different state databases in Miden. +[State](state) describes everything that is the case at a certain point in time. Individual states of accounts or notes can be stored on-chain and off-chain. This chapter describes the three different state databases in Miden. ### Blockchain -The [Blockchain](blockchain.md) defines how state progresses as aggregated-state-updates in batches, blocks, and epochs. The [blockchain chapter](blockchain.md) describes the execution model and how blocks are built. +The [Blockchain](blockchain) defines how state progresses as aggregated-state-updates in batches, blocks, and epochs. The [blockchain chapter](blockchain) describes the execution model and how blocks are built. ##### Operators capture and progress state diff --git a/docs/src/note.md b/docs/src/note.md index 9745d1dcb2..785133e78e 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -1,66 +1,75 @@ +--- +sidebar_position: 3 +--- + # Notes -A `Note` is the medium through which [Accounts](account/overview.md) communicate. A `Note` holds assets and defines how they can be consumed. +A `Note` is the medium through which [Accounts](account/index.md) communicate. A `Note` holds assets and defines how they can be consumed. ## What is the purpose of a note? -In Miden's hybrid UTXO and account-based model notes represent UTXO's which enable parallel transaction execution and privacy through asynchronous local `Note` production and consumption. +In Miden's hybrid UTXO and account-based model notes represent UTXO's which enable parallel transaction execution and privacy through asynchronous local `Note` production and consumption. ## Note core components A `Note` is composed of several core components, illustrated below: -

- Note diagram +

+ Note diagram

These components are: -1. [Assets](#assets) -2. [Script](#script) -3. [Inputs](#inputs) -4. [Serial number](#serial-number) +1. [Assets](#assets) +2. [Script](#script) +3. [Inputs](#inputs) +4. [Serial number](#serial-number) 5. [Metadata](#metadata) ### Assets -> [!Note] -> An [asset](asset.md) container for a `Note`. +:::note +An [asset](asset) container for a `Note`. +::: A `Note` can contain from 0 up to 256 different assets. These assets represent fungible or non-fungible tokens, enabling flexible asset transfers. ### Script -> [!Note] -> The code executed when the `Note` is consumed. +:::note +The code executed when the `Note` is consumed. +::: Each `Note` has a script that defines the conditions under which it can be consumed. When accounts consume notes in transactions, `Note` scripts call the account’s interface functions. This enables all sorts of operations beyond simple asset transfers. The Miden VM’s Turing completeness allows for arbitrary logic, making `Note` scripts highly versatile. There is no limit to the amount of code a `Note` can hold. ### Inputs -> [!Note] -> Arguments passed to the `Note` script during execution. +:::note +Arguments passed to the `Note` script during execution. +::: -A `Note` can have up to 128 input values, which adds up to a maximum of 1 KB of data. The `Note` script can access these inputs. They can convey arbitrary parameters for `Note` consumption. +A `Note` can have up to 128 input values, which adds up to a maximum of 1 KB of data. The `Note` script can access these inputs. They can convey arbitrary parameters for `Note` consumption. ### Serial number -> [!Note] -> A unique and immutable identifier for the `Note`. +:::note +A unique and immutable identifier for the `Note`. +::: The serial number has two main purposes. Firstly by adding some randomness to the `Note` it ensures it's uniqueness, secondly in private notes it helps prevent linkability between the note's hash and its nullifier. The serial number should be a random 32 bytes number chosen by the user. If leaked, the note’s nullifier can be easily computed, potentially compromising privacy. ### Metadata -> [!Note] -> Additional `Note` information. +:::note +Additional `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. ## Note Lifecycle -

- Note lifecycle +

+ Note lifecycle

The `Note` lifecycle proceeds through four primary phases: **creation**, **validation**, **discovery**, and **consumption**. Creation and consumption requires two separate transactions. Throughout this process, notes function as secure, privacy-preserving vehicles for asset transfers and logic execution. @@ -74,9 +83,9 @@ Accounts can create notes in a transaction. The `Note` exists if it is included #### Note storage mode -As with [accounts](account/overview.md), notes can be stored either publicly or privately: +As with [accounts](account/index.md), notes can be stored either publicly or privately: -- **Public mode:** The `Note` data is stored in the [note database](state.md#note-database), making it fully visible on-chain. +- **Public mode:** The `Note` data is stored in the [note database](state#note-database), making it fully visible on-chain. - **Private mode:** Only the `Note`’s hash is stored publicly. The `Note`’s actual data remains off-chain, enhancing privacy. ### Note validation @@ -110,7 +119,7 @@ hash(hash(hash(serial_num, [0; 4]), script_root), input_commitment) Only those who know the RECIPIENT’s pre-image can consume the `Note`. For private notes, this ensures an additional layer of control and privacy, as only parties with the correct data can claim the `Note`. -The [transaction prologue](transaction.md) requires all necessary data to compute the `Note` hash. This setup allows scenario-specific restrictions on who may consume a `Note`. +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. @@ -130,9 +139,8 @@ This achieves the following properties: That means if a `Note` is private and the operator stores only the note's hash, only those with the `Note` details know if this `Note` has been consumed already. Zcash first [introduced](https://zcash.github.io/orchard/design/nullifiers.html#nullifiers) this approach. - -

- Nullifier diagram +

+ Nullifier diagram

## Standard Note Types @@ -144,6 +152,7 @@ The miden-base repository provides several standard note scripts that implement The P2ID note script implements a simple pay-to-account-ID pattern. It adds all assets from the note to a specific target account. **Key characteristics:** + - **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 @@ -156,6 +165,7 @@ The P2ID note script implements a simple pay-to-account-ID pattern. It adds all The P2IDE note script extends P2ID with additional features including time-locking and reclaim functionality. **Key characteristics:** + - **Purpose:** Advanced asset transfer with time-lock and reclaim capabilities - **Inputs:** Requires exactly 4 note inputs: - Target account ID @@ -167,6 +177,7 @@ The P2IDE note script extends P2ID with additional features including time-locki - **Requirements:** Account must expose the `miden::contracts::wallets::basic::receive_asset` procedure **Use cases:** + - Escrow-like payments with time constraints - Conditional payments that can be reclaimed if not consumed - Time-delayed transfers @@ -176,6 +187,7 @@ The P2IDE note script extends P2ID with additional features including time-locki 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: - Requested asset details @@ -198,4 +210,4 @@ The SWAP note script implements atomic asset swapping functionality. - **Use SWAP** for atomic asset exchanges between parties - **Create custom scripts** for specialized use cases not covered by standard types -These standard note types provide a foundation for common operations while maintaining the flexibility to create custom note scripts for specialized requirements. \ No newline at end of file +These standard note types provide a foundation for common operations while maintaining the flexibility to create custom note scripts for specialized requirements. diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index e859e4b63d..aa49c87157 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -1,19 +1,25 @@ +--- +sidebar_position: 8 +--- + # Miden Protocol Library The Miden protocol library provides a set of procedures that wrap transaction kernel procedures to provide a more convenient interface for common operations. These can be invoked by account code, note scripts, and transaction scripts, though some have restriction from where they can be called. The procedures are organized into modules corresponding to different functional areas. ## Contexts +Here and in other places we use the notion of _active account_: it is the account which is currently being accessed. + The Miden VM contexts from which procedures can be called are: - **Account**: Can only be called from native or foreign accounts. - - **Native**: Can only be called when the current account is the native account. + - **Native**: Can only be called when the active account is the native account. - **Auth**: Can only be called from the authentication procedure. Since it is called on the native account, it implies **Native** and **Account**. - - **Faucet**: Can only be called when the current account is a faucet. + - **Faucet**: Can only be called when the active account is a faucet. - **Note**: Can only be called from a note script. - **Any**: Can be called from any context. -If a procedure has multiple context requirements they are combined using `&`. For instance, "Native & Account" means the procedure can only be called when the current account is the native one _and_ only from the account context. +If a procedure has multiple context requirements they are combined using `&`. For instance, "Native & Account" means the procedure can only be called when the active account is the native one _and_ only from the account context. ## Implementation @@ -21,107 +27,138 @@ 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. -## Account Procedures (`miden::account`) - -Account procedures can be used to read and write to account storage, add or remove assets from the vault and fetch or compute commitments. - -| Procedure | Description | Context | -| --- | --- | --- | -| `get_id` | Returns the account ID of the current account.

Inputs: `[]`
Outputs: `[account_id_prefix, account_id_suffix]` | Any | -| `get_nonce` | Returns the nonce of the current account. Always returns the initial nonce as it can only be incremented in auth procedures.

Inputs: `[]`
Outputs: `[nonce]` | Any | -| `incr_nonce` | Increments the account nonce by one and returns the new nonce. Can only be called from auth procedures.

Inputs: `[]`
Outputs: `[final_nonce]` | Auth | -| `get_initial_commitment` | Returns the native account commitment at the beginning of the transaction.

Inputs: `[]`
Outputs: `[INIT_COMMITMENT]` | Any | -| `compute_current_commitment` | Computes and returns the account commitment from account data stored in memory.

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

Inputs: `[]`
Outputs: `[DELTA_COMMITMENT]` | Auth | -| `get_item` | Gets an item from the account storage.

Inputs: `[index]`
Outputs: `[VALUE]` | Account | -| `set_item` | Sets an item in the account storage.

Inputs: `[index, VALUE]`
Outputs: `[OLD_VALUE]` | Native & 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 | -| `set_map_item` | Sets VALUE under the specified KEY within the map contained in the given account storage slot.

Inputs: `[index, KEY, VALUE]`
Outputs: `[OLD_MAP_ROOT, OLD_MAP_VALUE]` | Native & Account | -| `get_code_commitment` | Gets the account code commitment of the current account.

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

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

Inputs: `[]`
Outputs: `[STORAGE_COMMITMENT]` | Account | -| `get_balance` | Returns the balance of the fungible asset associated with the provided faucet_id in the current account's vault.

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

Inputs: `[ASSET]`
Outputs: `[has_asset]` | Any | -| `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 | -| `get_initial_vault_root` | Returns the vault root of the native account at the beginning of the transaction.

Inputs: `[]`
Outputs: `[INIT_VAULT_ROOT]` | Any | -| `get_vault_root` | Returns the vault root of the current account.

Inputs: `[]`
Outputs: `[VAULT_ROOT]` | Any | -| `was_procedure_called` | Returns 1 if a procedure was called during transaction execution, and 0 otherwise.

Inputs: `[PROC_ROOT]`
Outputs: `[was_called]` | Any | - -## Note Procedures (`miden::note`) - -Note procedures can be used to fetch data from the note that is currently being processed. - -| Procedure | Description | Context | -| --- | --- | --- | -| `get_assets` | Writes the assets of the currently executing note into memory starting at the specified address.

Inputs: `[dest_ptr]`
Outputs: `[num_assets, dest_ptr]` | Note | -| `get_inputs` | Loads the note's inputs to the specified memory address.

Inputs: `[dest_ptr]`
Outputs: `[num_inputs, dest_ptr]` | Note | -| `get_sender` | Returns the sender of the note currently being processed.

Inputs: `[]`
Outputs: `[sender_id_prefix, sender_id_suffix]` | Note | -| `get_serial_number` | Returns the serial number of the note currently being processed.

Inputs: `[]`
Outputs: `[SERIAL_NUMBER]` | Note | -| `get_script_root` | Returns the script root of the note currently being processed.

Inputs: `[]`
Outputs: `[SCRIPT_ROOT]` | Note | -| `compute_inputs_commitment` | Computes the commitment to the note inputs starting at the specified memory address.

Inputs: `[inputs_ptr, num_inputs]`
Outputs: `[COMMITMENT]` | Any | -| `add_assets_to_account` | Adds all assets from the currently executing note to the account vault.

Inputs: `[]`
Outputs: `[]` | Note | +## Active account Procedures (`miden::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. + +| Procedure | Description | Context | +| -------------------------------- | ----------------------------- | ----------------------------- | +| `get_id` | Returns the ID of the active account.

**Inputs:** `[]`
**Outputs:** `[account_id_prefix, account_id_suffix]` | Any | +| `get_nonce` | Returns the nonce of the active account. Always returns the initial nonce as it can only be incremented in auth procedures.

**Inputs:** `[]`
**Outputs:** `[nonce]` | Any | +| `get_initial_commitment` | Returns the active account commitment at the beginning of the transaction.

**Inputs:** `[]`
**Outputs:** `[INIT_COMMITMENT]` | Any | +| `compute_commitment` | Computes and returns the account commitment from account data stored in memory.

**Inputs:** `[]`
**Outputs:** `[ACCOUNT_COMMITMENT]` | Any | +| `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_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 | +| `get_initial_vault_root` | Returns the vault root of the active account at the beginning of the transaction.

**Inputs:** `[]`
**Outputs:** `[INIT_VAULT_ROOT]` | Any | +| `get_vault_root` | Returns the vault root of the active account.

**Inputs:** `[]`
**Outputs:** `[VAULT_ROOT]` | Any | +| `get_num_procedures` | Returns the number of procedures in the active account.

**Inputs:** `[]`
**Outputs:** `[num_procedures]` | Any | +| `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 can be used to write to storage, add or remove assets from the vault and compute delta commitment of the native account. + +| Procedure | Description | Context | +| ------------------------------ | ------------------------------ | ------------------------------ | +| `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 | +| `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 can be used to fetch data from the note that is currently being processed by the transaction kernel. + +| Procedure | Description | Context | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `get_assets` | Writes the [assets](note.md#assets) of the active note into memory starting at the specified address.

**Inputs:** `[dest_ptr]`
**Outputs:** `[num_assets, dest_ptr]` | Note | +| `get_recipient` | Returns the [recipient](note.md#note-recipient-restricting-consumption) of the active note.

**Inputs:** `[]`
**Outputs:** `[RECIPIENT]` | Note | +| `get_inputs` | Writes the note's [inputs](note.md#inputs) to the specified memory address.

**Inputs:** `[dest_ptr]`
**Outputs:** `[num_inputs, dest_ptr]` | Note | +| `get_metadata` | Returns the [metadata](note.md#metadata) of the active note.

**Inputs:** `[]`
**Outputs:** `[METADATA]` | Note | +| `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 can be used to fetch data on input notes consumed by the transaction. -| Procedure | Description | Context | -| --- | --- | --- | -| `get_assets_info` | Returns the information about assets in the input note with the specified index.

Inputs: `[note_index]`
Outputs: `[ASSETS_COMMITMENT, num_assets]` | Any | -| `get_assets` | Writes the assets of the input 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 | -| `get_recipient` | Returns the [recipient](note.md#note-recipient-restricting-consumption) of the input note with the specified index.

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

Inputs: `[note_index]`
Outputs: `[METADATA]` | Any | +| Procedure | Description | Context | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `get_assets_info` | Returns the information about [assets](note.md#assets) in the input note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[ASSETS_COMMITMENT, num_assets]` | Any | +| `get_assets` | Writes the [assets](note.md#assets) of the input 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 | +| `get_recipient` | Returns the [recipient](note.md#note-recipient-restricting-consumption) of the input note with the specified index.

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

**Inputs:** `[note_index]`
**Outputs:** `[METADATA]` | Any | +| `get_sender` | Returns the sender of the input note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[sender_id_prefix, sender_id_suffix]` | Any | +| `get_inputs_info` | Returns the [inputs](note.md#inputs) commitment and length of the input note with the specified index.

**Inputs:** `[note_index]`
**Outputs:** `[NOTE_INPUTS_COMMITMENT, num_inputs]` | Any | +| `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 can be used to fetch data on output notes created by the transaction. -| Procedure | Description | Context | -| --- | --- | --- | -| `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 | -| `get_recipient` | Returns the [recipient](note.md#note-recipient-restricting-consumption) of the output note with the specified index.

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

Inputs: `[note_index]`
Outputs: `[METADATA]` | Any | +| 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 | +| `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 | +| `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 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 manage transaction-level operations including note creation, context switching, and reading transaction metadata. -| Procedure | Description | Context | -| --- | --- | --- | -| `get_block_number` | Returns the block number of the transaction reference block.

Inputs: `[]`
Outputs: `[num]` | Any | -| `get_block_commitment` | Returns the block commitment of the reference block.

Inputs: `[]`
Outputs: `[BLOCK_COMMITMENT]` | Any | -| `get_block_timestamp` | Returns the timestamp of the reference block for this transaction.

Inputs: `[]`
Outputs: `[timestamp]` | Any | -| `get_input_notes_commitment` | Returns the input notes commitment hash.

Inputs: `[]`
Outputs: `[INPUT_NOTES_COMMITMENT]` | Any | -| `get_output_notes_commitment` | Returns the output notes commitment hash.

Inputs: `[]`
Outputs: `[OUTPUT_NOTES_COMMITMENT]` | Any | -| `get_num_input_notes` | Returns the total number of input notes consumed by this transaction.

Inputs: `[]`
Outputs: `[num_input_notes]` | Any | -| `get_num_output_notes` | Returns the current number of output notes created in this transaction.

Inputs: `[]`
Outputs: `[num_output_notes]` | Any | -| `create_note` | Creates a new note and returns the index of the note.

Inputs: `[tag, aux, note_type, execution_hint, RECIPIENT]`
Outputs: `[note_idx]` | Native & Account | -| `add_asset_to_note` | Adds the ASSET to the note specified by the index.

Inputs: `[ASSET, note_idx]`
Outputs: `[ASSET, note_idx]` | Native | -| `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 | -| `execute_foreign_procedure` | Executes the provided procedure against the foreign account.

Inputs: `[foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, , pad(n)]`
Outputs: `[]` | Any | -| `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 | +| Procedure | Description | Context | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `get_block_number` | Returns the block number of the transaction reference block.

**Inputs:** `[]`
**Outputs:** `[num]` | Any | +| `get_block_commitment` | Returns the block commitment of the reference block.

**Inputs:** `[]`
**Outputs:** `[BLOCK_COMMITMENT]` | Any | +| `get_block_timestamp` | Returns the timestamp of the reference block for this transaction.

**Inputs:** `[]`
**Outputs:** `[timestamp]` | Any | +| `get_input_notes_commitment` | Returns the input notes commitment hash.

**Inputs:** `[]`
**Outputs:** `[INPUT_NOTES_COMMITMENT]` | Any | +| `get_output_notes_commitment` | Returns the output notes commitment hash.

**Inputs:** `[]`
**Outputs:** `[OUTPUT_NOTES_COMMITMENT]` | Any | +| `get_num_input_notes` | Returns the total number of input notes consumed by this transaction.

**Inputs:** `[]`
**Outputs:** `[num_input_notes]` | Any | +| `get_num_output_notes` | Returns the current number of output notes created in this transaction.

**Inputs:** `[]`
**Outputs:** `[num_output_notes]` | Any | +| `execute_foreign_procedure` | Executes the provided procedure against the foreign account.

**Inputs:** `[foreign_account_id_prefix, foreign_account_id_suffix, FOREIGN_PROC_ROOT, , pad(n)]`
**Outputs:** `[]` | Any | +| `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 allow reading and writing to faucet accounts to mint and burn assets. -| Procedure | Description | Context | -| --- | --- | --- | -| `mint` | Mint an asset from the faucet the transaction is being executed against.

Inputs: `[ASSET]`
Outputs: `[ASSET]` | Native & Account & Faucet | -| `burn` | Burn an asset from the faucet the transaction is being executed against.

Inputs: `[ASSET]`
Outputs: `[ASSET]` | Native & Account & Faucet | -| `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 | +| Procedure | Description | Context | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| `create_fungible_asset` | Creates a fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[amount]`
**Outputs:** `[ASSET]` | Faucet | +| `create_non_fungible_asset` | Creates a non-fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[DATA_HASH]`
**Outputs:** `[ASSET]` | Faucet | +| `mint` | Mint an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET]`
**Outputs:** `[ASSET]` | Native & Account & Faucet | +| `burn` | Burn an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET]`
**Outputs:** `[ASSET]` | Native & Account & Faucet | +| `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 provide utilities for creating fungible and non-fungible assets. -| Procedure | Description | Context | -| --- | --- | --- | -| `build_fungible_asset` | Builds a fungible asset for the specified fungible faucet and amount.

Inputs: `[faucet_id_prefix, faucet_id_suffix, amount]`
Outputs: `[ASSET]` | Any | -| `create_fungible_asset` | Creates a fungible asset for the faucet the transaction is being executed against.

Inputs: `[amount]`
Outputs: `[ASSET]` | Faucet | -| `build_non_fungible_asset` | Builds a non-fungible asset for the specified non-fungible faucet and data hash.

Inputs: `[faucet_id_prefix, DATA_HASH]`
Outputs: `[ASSET]` | Any | -| `create_non_fungible_asset` | Creates a non-fungible asset for the faucet the transaction is being executed against.

Inputs: `[DATA_HASH]`
Outputs: `[ASSET]` | Faucet | +| Procedure | Description | Context | +| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `build_fungible_asset` | Builds a fungible asset for the specified fungible faucet and amount.

**Inputs:** `[faucet_id_prefix, faucet_id_suffix, amount]`
**Outputs:** `[ASSET]` | Any | +| `build_non_fungible_asset` | Builds a non-fungible asset for the specified non-fungible faucet and data hash.

**Inputs:** `[faucet_id_prefix, DATA_HASH]`
**Outputs:** `[ASSET]` | Any | diff --git a/docs/src/state.md b/docs/src/state.md index 10dd4818f3..6257959f54 100644 --- a/docs/src/state.md +++ b/docs/src/state.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 6 +--- + # State The `State` describes the current condition of all accounts, notes, nullifiers and their statuses. Reflecting the "current reality" of the protocol at any given time. @@ -25,8 +29,8 @@ The Miden node maintains three databases to describe `State`: 2. Notes 3. Nullifiers -

- State +

+ State

### Account database @@ -38,11 +42,11 @@ The accounts database has two main purposes: This is done using an authenticated data structure, a sparse Merkle tree. -

- Account DB +

+ Account DB

-As described in the [account ID section](account/id.md#account-storage-mode), accounts can have different storage modes: +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. @@ -51,12 +55,13 @@ Private accounts significantly reduce storage overhead. A private account contri The storage contribution of a public account depends on the amount of data it stores. -> [!Warning] -> In Miden, when the user is the custodian of their account `State` (in the case of a private account), losing this `State` amounts to losing their funds, similar to losing a private key. +:::warning +In Miden, when the user is the custodian of their account `State` (in the case of a private account), losing this `State` amounts to losing their funds, similar to losing a private key. +::: ### Note database -As described in the [notes section](note.md), there are two types of notes: +As described in the [notes section](note), there are two types of notes: - **Public notes:** where the entire note content is stored on-chain. - **Private notes:** where only the note’s commitment is stored on-chain. @@ -72,21 +77,22 @@ Using a Merkle Mountain Range (append-only accumulator) is important for two rea Both of these properties are needed for supporting local transactions using client-side proofs and privacy. In an append-only data structure, witness data does not become stale when the data structure is updated. That means users can generate valid proofs even if they don’t have the latest `State` of this database; so there is no need to query the operator on a constantly changing `State`. -

- Note DB +

+ Note DB

### Nullifier database -Each [note](note.md) has an associated nullifier which enables the tracking of whether its associated note has been consumed or not, preventing double-spending. +Each [note](note) has an associated nullifier which enables the tracking of whether its associated note has been consumed or not, preventing double-spending. To prove that a note has not been consumed, the operator must provide a Merkle path to the corresponding node and show that the node’s value is 0. Since nullifiers are 32 bytes each, the sparse Merkle tree height must be sufficient to represent all possible nullifiers. Operators must maintain the entire nullifier set to compute the new tree root after inserting new nullifiers. For each nullifier we also record the block in which it was created. This way "unconsumed" nullifiers have block 0, but all consumed nullifiers have a non-zero block. -> [!Note] -> Nullifiers in Miden break linkability between privately stored notes and their consumption details. To know the [note’s nullifier](note.md#note-nullifier-ensuring-private-consumption), one must know the note’s data. +:::note +Nullifiers in Miden break linkability between privately stored notes and their consumption details. To know the [note's nullifier](note#note-nullifier-ensuring-private-consumption), one must know the note's data. +::: -

- Nullifier DB +

+ Nullifier DB

## Additional information @@ -95,8 +101,8 @@ To prove that a note has not been consumed, the operator must provide a Merkle p In most blockchains, most smart contracts and decentralized applications (e.g., AAVE, Uniswap) need public shared `State`. Public shared `State` is also available on Miden and can be represented as in the following example: -

- Example: AMM transactions +

+ Example: AMM transactions

In this diagram, multiple participants interact with a common, publicly accessible `State` (the AMM in the center). The figure illustrates how notes are created and consumed: diff --git a/docs/src/theme/Admonition/Icon/Danger.tsx b/docs/src/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 0000000000..ab14d7dec8 --- /dev/null +++ b/docs/src/theme/Admonition/Icon/Danger.tsx @@ -0,0 +1,19 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Danger"; + +export default function AdmonitionIconDanger(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/docs/src/theme/Admonition/Icon/Info.tsx b/docs/src/theme/Admonition/Icon/Info.tsx new file mode 100644 index 0000000000..59e48a5216 --- /dev/null +++ b/docs/src/theme/Admonition/Icon/Info.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Info"; + +export default function AdmonitionIconInfo(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/docs/src/theme/Admonition/Icon/Note.tsx b/docs/src/theme/Admonition/Icon/Note.tsx new file mode 100644 index 0000000000..d7c524b3a4 --- /dev/null +++ b/docs/src/theme/Admonition/Icon/Note.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Note"; + +export default function AdmonitionIconNote(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/docs/src/theme/Admonition/Icon/Tip.tsx b/docs/src/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 0000000000..219bb8d0a6 --- /dev/null +++ b/docs/src/theme/Admonition/Icon/Tip.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Tip"; + +export default function AdmonitionIconTip(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/docs/src/theme/Admonition/Icon/Warning.tsx b/docs/src/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 0000000000..f96398d118 --- /dev/null +++ b/docs/src/theme/Admonition/Icon/Warning.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Warning"; + +export default function AdmonitionIconCaution(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/docs/src/theme/Admonition/Layout/index.tsx b/docs/src/theme/Admonition/Layout/index.tsx new file mode 100644 index 0000000000..7b2c170d89 --- /dev/null +++ b/docs/src/theme/Admonition/Layout/index.tsx @@ -0,0 +1,51 @@ +import React, { type ReactNode } from "react"; +import clsx from "clsx"; +import { ThemeClassNames } from "@docusaurus/theme-common"; + +import type { Props } from "@theme/Admonition/Layout"; + +import styles from "./styles.module.css"; + +function AdmonitionContainer({ + type, + className, + children, +}: Pick & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function AdmonitionHeading({ icon, title }: Pick) { + return ( +
+ {icon} + {/* {title} */} +
+ ); +} + +function AdmonitionContent({ children }: Pick) { + return children ? ( +
{children}
+ ) : null; +} + +export default function AdmonitionLayout(props: Props): ReactNode { + const { type, icon, title, children, className } = props; + return ( + + {title || icon ? : null} + {children} + + ); +} diff --git a/docs/src/theme/Admonition/Layout/styles.module.css b/docs/src/theme/Admonition/Layout/styles.module.css new file mode 100644 index 0000000000..88df7e639b --- /dev/null +++ b/docs/src/theme/Admonition/Layout/styles.module.css @@ -0,0 +1,35 @@ +.admonition { + margin-bottom: 1em; +} + +.admonitionHeading { + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / + var(--ifm-heading-line-height) var(--ifm-heading-font-family); + text-transform: uppercase; +} + +/* Heading alone without content (does not handle fragment content) */ +.admonitionHeading:not(:last-child) { + margin-bottom: 0.3rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon { + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; +} + +.admonitionIcon svg { + display: inline-block; + height: 1.6em; + width: 1.6em; + fill: var(--ifm-alert-foreground-color); +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/docs/src/theme/Admonition/Type/Caution.tsx b/docs/src/theme/Admonition/Type/Caution.tsx new file mode 100644 index 0000000000..b570a37a9d --- /dev/null +++ b/docs/src/theme/Admonition/Type/Caution.tsx @@ -0,0 +1,32 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Caution'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + caution + + ), +}; + +// TODO remove before v4: Caution replaced by Warning +// see https://github.com/facebook/docusaurus/issues/7558 +export default function AdmonitionTypeCaution(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/docs/src/theme/Admonition/Type/Danger.tsx b/docs/src/theme/Admonition/Type/Danger.tsx new file mode 100644 index 0000000000..49901fa91c --- /dev/null +++ b/docs/src/theme/Admonition/Type/Danger.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Danger'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconDanger from '@theme/Admonition/Icon/Danger'; + +const infimaClassName = 'alert alert--danger'; + +const defaultProps = { + icon: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/docs/src/theme/Admonition/Type/Info.tsx b/docs/src/theme/Admonition/Type/Info.tsx new file mode 100644 index 0000000000..018e0a16d7 --- /dev/null +++ b/docs/src/theme/Admonition/Type/Info.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Info'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconInfo from '@theme/Admonition/Icon/Info'; + +const infimaClassName = 'alert alert--info'; + +const defaultProps = { + icon: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/docs/src/theme/Admonition/Type/Note.tsx b/docs/src/theme/Admonition/Type/Note.tsx new file mode 100644 index 0000000000..c99e03857f --- /dev/null +++ b/docs/src/theme/Admonition/Type/Note.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Note'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconNote from '@theme/Admonition/Icon/Note'; + +const infimaClassName = 'alert alert--secondary'; + +const defaultProps = { + icon: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/docs/src/theme/Admonition/Type/Tip.tsx b/docs/src/theme/Admonition/Type/Tip.tsx new file mode 100644 index 0000000000..18604a5e9f --- /dev/null +++ b/docs/src/theme/Admonition/Type/Tip.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Tip'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconTip from '@theme/Admonition/Icon/Tip'; + +const infimaClassName = 'alert alert--success'; + +const defaultProps = { + icon: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/docs/src/theme/Admonition/Type/Warning.tsx b/docs/src/theme/Admonition/Type/Warning.tsx new file mode 100644 index 0000000000..61d9597b61 --- /dev/null +++ b/docs/src/theme/Admonition/Type/Warning.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Warning'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + warning + + ), +}; + +export default function AdmonitionTypeWarning(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/docs/src/theme/Admonition/Types.tsx b/docs/src/theme/Admonition/Types.tsx new file mode 100644 index 0000000000..2a1001900a --- /dev/null +++ b/docs/src/theme/Admonition/Types.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; +import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; +import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; +import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; +import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; +import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; +import type AdmonitionTypes from '@theme/Admonition/Types'; + +const admonitionTypes: typeof AdmonitionTypes = { + note: AdmonitionTypeNote, + tip: AdmonitionTypeTip, + info: AdmonitionTypeInfo, + warning: AdmonitionTypeWarning, + danger: AdmonitionTypeDanger, +}; + +// Undocumented legacy admonition type aliases +// Provide hardcoded/untranslated retrocompatible label +// See also https://github.com/facebook/docusaurus/issues/7767 +const admonitionAliases: typeof AdmonitionTypes = { + secondary: (props) => , + important: (props) => , + success: (props) => , + caution: AdmonitionTypeCaution, +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/docs/src/theme/Admonition/index.tsx b/docs/src/theme/Admonition/index.tsx new file mode 100644 index 0000000000..8f4225da2f --- /dev/null +++ b/docs/src/theme/Admonition/index.tsx @@ -0,0 +1,21 @@ +import React, {type ComponentType, type ReactNode} from 'react'; +import {processAdmonitionProps} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Admonition'; +import AdmonitionTypes from '@theme/Admonition/Types'; + +function getAdmonitionTypeComponent(type: string): ComponentType { + const component = AdmonitionTypes[type]; + if (component) { + return component; + } + console.warn( + `No admonition component found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionTypes.info!; +} + +export default function Admonition(unprocessedProps: Props): ReactNode { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; +} diff --git a/docs/src/transaction.md b/docs/src/transaction.md index 97d16510d5..72fe743cf5 100644 --- a/docs/src/transaction.md +++ b/docs/src/transaction.md @@ -1,14 +1,18 @@ +--- +sidebar_position: 5 +--- + # Transactions -A `Transaction` in Miden is the state transition of a single account. A `Transaction` takes as input a single [account](account/overview.md) and zero or more [notes](note.md), and outputs the same account with an updated state, together with zero or more notes. Transactions in Miden are Miden VM programs, their execution resulting in the generation of a zero-knowledge proof. +A `Transaction` in Miden is the state transition of a single account. A `Transaction` takes as input a single [account](./account) and zero or more [notes](note), and outputs the same account with an updated state, together with zero or more notes. Transactions in Miden are Miden VM programs, their execution resulting in the generation of a zero-knowledge proof. Miden's `Transaction` model aims for the following: - **Parallel transaction execution**: Accounts can update their state independently from each other and in parallel. - **Private transaction execution**: Client-side `Transaction` proving allows the network to verify transactions validity with zero-knowledge. -

- Transaction diagram +

+ Transaction diagram

Compared to most blockchains, where a `Transaction` typically involves more than one account (e.g., sender and receiver), a `Transaction` in Miden involves a single account. To illustrate, Alice sends 5 ETH to Bob. In Miden, sending 5 ETH from Alice to Bob takes two transactions, one in which Alice creates a note containing 5 ETH and one in which Bob consumes that note and receives the 5 ETH. This model removes the need for a global lock on the blockchain's state, enabling Miden to process transactions in parallel. @@ -21,8 +25,8 @@ A simple transaction currently takes about 1-2 seconds on a MacBook Pro. It take Every `Transaction` describes the process of an account changing its state. This process is described as a Miden VM program, resulting in the generation of a zero-knowledge proof. Transactions are being executed in a specified sequence, in which several notes and a transaction script can interact with an account. -

- Transaction program +

+ Transaction program

### Inputs @@ -39,14 +43,15 @@ A `Transaction` requires several inputs: ### Flow 1. **Prologue** - Executes at the beginning of a transaction. It validates on-chain commitments against the provided data. This is to ensure that the transaction executes against a valid on-chain recorded state of the account and to be consumed notes. Notes to be consumed must be registered on-chain — except for [erasable notes](note.md) which can be consumed without block inclusion. + Executes at the beginning of a transaction. It validates on-chain commitments against the provided data. This is to ensure that the transaction executes against a valid on-chain recorded state of the account and to be consumed notes. Notes to be consumed must be registered on-chain — except for [erasable notes](note) which can be consumed without block inclusion. 2. **Note processing** - Notes are executed sequentially against the account, following a sequence defined by the executor. To execute a note means processing the note script that calls methods exposed on the account interface. Notes must be consumed fully, which means that all assets must be transferred into the account or into other created notes. [Note scripts](note.md#script) can invoke the account interface during execution. They can push assets into the account's vault, create new notes, set a transaction expiration, and read from or write to the account’s storage. Any method they call must be explicitly exposed by the account interface. Note scripts can also invoke methods of foreign accounts to read their state. + Notes are executed sequentially against the account, following a sequence defined by the executor. To execute a note means processing the note script that calls methods exposed on the account interface. Notes must be consumed fully, which means that all assets must be transferred into the account or into other created notes. [Note scripts](note#script) can invoke the account interface during execution. They can push assets into the account's vault, create new notes, set a transaction expiration, and read from or write to the account's storage. Any method they call must be explicitly exposed by the account interface. Note scripts can also invoke methods of foreign accounts to read their state. 3. **Transaction script processing** `Transaction` scripts are an optional piece of code defined by the executor which interacts with account methods after all notes have been executed. For example, `Transaction` scripts can be used to sign the `Transaction` (e.g., sign the transaction by incrementing the nonce of the account, without which, the transaction would fail), to mint tokens from a faucet, create notes, or modify account storage. `Transaction` scripts can also invoke methods of foreign accounts to read their state. 4. **Epilogue** Completes the execution, resulting in an updated account state and a generated zero-knowledge proof. The validity of the resulting transaction is ensured by a combination of user-defined and protocol-defined checks: - - The account's [authentication procedure](account/code.md#authentication) is called to authorize the transaction. + - The account's [authentication procedure](account/code#authentication) is called to authorize the transaction. + - The transaction fee is computed and removed from the account's vault in the chain's native asset. See [Fees](fees). - The account's state must have changed, or at least one input note must have been consumed to make the transaction non-empty. - If the account's state has changed, the `nonce` must have been incremented to prevent replay attacks. - Additionally, the sum of all input assets must be equal to the sum of all output assets (if the account is not a faucet). @@ -59,9 +64,9 @@ To illustrate the `Transaction` protocol, we provide two examples for a basic `T ### Creating a P2ID note -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.md#inputs). +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 `account::get_id` are always exposed. +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. 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. @@ -75,11 +80,11 @@ To start the transaction process, the executor fetches and prepares all the inpu In the transaction's prologue the data is being authenticated by re-hashing the provided values and comparing them to the blockchain's data (this is how private data can be used and verified during the execution of transaction without actually revealing it to the network). -Then the P2ID note script is being executed. The script starts by reading the note inputs `note::get_inputs` — in our case the account ID of the intended target account. It checks if the provided target account ID equals the account ID of the executing account. This is the first time the note invokes a method exposed by the `Transaction` kernel, `account::get_id`. +Then the P2ID note script is being executed. The script starts by reading the note inputs `active_note::get_inputs` — in our case the account ID of the intended target account. It checks if the provided target account ID equals the account ID of the executing account. This is the first time the note invokes a method exposed by the `Transaction` kernel, `active_account::get_id`. -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 `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. +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_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. 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. @@ -113,23 +118,25 @@ The ability to facilitate both, local and network transactions, **is one of the --- -> [!Tip] -> -> - Usually, notes that are consumed in a `Transaction` must be recorded on-chain in order for the `Transaction` to succeed. However, Miden supports **erasable notes** which are notes that can be consumed in a `Transaction` before being registered on-chain. For example, one can build a sub-second order book by allowing its traders to build faster transactions that depend on each other and are being validated or erased in batches. -> -> - There is no nullifier check during a `Transaction`. Nullifiers are checked by the Miden operator during `Transaction` verification. So at the local level, there is "double spending." If a note was already spent, i.e. there exists a nullifier for that note, the block producer would never include the `Transaction` as it would make the block invalid. -> -> - One of the main reasons for separating execution and proving steps is to allow _stateless provers_; i.e., the executed `Transaction` has all the data it needs to re-execute and prove a `Transaction` without database access. This supports easier proof-generation distribution. -> -> - 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. -> -> - 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. -> -> - An example of the right usage of `Transaction` arguments is the consumption of a **Swap** note. Those notes allow asset exchange based on predefined conditions. Example: -> - The note's consumption condition is defined as "anyone can consume this note to take `X` units of asset A if they simultaneously create a note sending Y units of asset B back to the creator." If an executor wants to buy only a fraction `(X-m)` of asset A, they provide this amount via transaction arguments. The executor would provide the value `m`. The note script then enforces the correct transfer: -> - A new note is created returning `Y-((m*Y)/X)` of asset B to the sender. -> - A second note is created, holding the remaining `(X-m)` of asset A for future consumption. -> -> - When executing a `Transaction` the max number of VM cycles is **$2^{30}$**. +:::tip + +- Usually, notes that are consumed in a `Transaction` must be recorded on-chain in order for the `Transaction` to succeed. However, Miden supports **erasable notes** which are notes that can be consumed in a `Transaction` before being registered on-chain. For example, one can build a sub-second order book by allowing its traders to build faster transactions that depend on each other and are being validated or erased in batches. + +- There is no nullifier check during a `Transaction`. Nullifiers are checked by the Miden operator during `Transaction` verification. So at the local level, there is "double spending." If a note was already spent, i.e. there exists a nullifier for that note, the block producer would never include the `Transaction` as it would make the block invalid. + +- One of the main reasons for separating execution and proving steps is to allow _stateless provers_; i.e., the executed `Transaction` has all the data it needs to re-execute and prove a `Transaction` without database access. This supports easier proof-generation distribution. + +- 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. + +- 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. + +- An example of the right usage of `Transaction` arguments is the consumption of a **Swap** note. Those notes allow asset exchange based on predefined conditions. Example: + - The note's consumption condition is defined as "anyone can consume this note to take `X` units of asset A if they simultaneously create a note sending Y units of asset B back to the creator." If an executor wants to buy only a fraction `(X-m)` of asset A, they provide this amount via transaction arguments. The executor would provide the value `m`. The note script then enforces the correct transfer: + - A new note is created returning `Y-((m*Y)/X)` of asset B to the sender. + - A second note is created, holding the remaining `(X-m)` of asset A for future consumption. + +- When executing a `Transaction` the max number of VM cycles is **$2^{30}$**. + +::: diff --git a/docs/static/img/custom_caret.svg b/docs/static/img/custom_caret.svg new file mode 100644 index 0000000000..0480cf1c4f --- /dev/null +++ b/docs/static/img/custom_caret.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico new file mode 100644 index 0000000000..732d352226 Binary files /dev/null and b/docs/static/img/favicon.ico differ diff --git a/docs/static/img/logo.png b/docs/static/img/logo.png new file mode 100644 index 0000000000..35e9497563 Binary files /dev/null and b/docs/static/img/logo.png differ diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000000..297366633a --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,1046 @@ +/* Import Google Fonts */ +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap"); + +/* ============================ + 0) SEMANTIC TOKENS (your brand) + ============================ */ + +:root { + /* ---- Brand palette (LIGHT) ---- */ + --color-brand: #ff5500; /* TODO: replace with your primary */ + --color-brand-600: #ff6a26; /* TODO */ + --color-brand-700: #e24c00; /* TODO */ + --color-brand-800: #b63c00; /* TODO */ + --color-secondary: #102445; + + --color-accent: #1764a8; /* TODO secondary/accent */ + --color-success: #00871d; + --color-warning: #ff5500; + --color-danger: #ff0000; + + /* ---- Neutral & Surfaces ---- */ + --color-bg: #ffffff; + --color-surface: #f9f9f9; + --color-elevated: color-mix(in oklab, #000 5%, transparent); + --color-border: color-mix(in oklab, #000 20%, transparent); + --color-text: #363636; + --color-muted: color-mix(in oklab, var(--color-text) 70%, transparent); + --color-link: var(--color-brand); + + /* ---- Typography ---- */ + --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, + "Liberation Mono", monospace; + --font-size-base: 14px; + --line-height-base: 150%; + --letter-spacing-base: 0; + + /* ---- Radii & Spacing ---- */ + --radius-xs: 5px; + --radius-md: 5px; + --radius-lg: 5px; + --radius-xl: 5px; + + /* Design system spacing - exact pixel values */ + --space-4px: 4px; /* Inline code padding */ + --space-16px: 16px; /* Standard paragraph, list, heading spacing */ + --space-24px: 24px; /* H1 to first content, H2 to large components, list indent */ + --space-32px: 32px; /* Section breaks, major structural spacing */ + --space-40px: 40px; /* Major structural reset (lower bound) */ + --space-48px: 48px; /* Major structural reset (upper bound) */ + + /* Legacy spacing variables for compatibility */ + --space-1: 0.25rem; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.25rem; /* 20px */ + --space-6: 1.5rem; /* 24px */ + --space-8: 2rem; /* 32px */ + --space-10: 2.5rem; /* 40px */ + + /* ---- Shadows ---- */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.12); + + /* ---- Container widths ---- */ + + /* Common layout sizes */ + --navbar-height: 64px; + + /* Docs sidebar width */ + --doc-sidebar-width: 250px !important; +} + +/* ========================================== + 1) MAP SEMANTIC → INFIMA (core variables) + ========================================== */ + +:root { + /* Brand */ + --ifm-color-primary: var(--color-brand); + --ifm-color-secondary: var(--color-secondary); + --ifm-color-primary-dark: var(--color-brand-700); + --ifm-color-primary-darker: var(--color-brand-800); + --ifm-color-primary-darkest: var(--color-brand-800); + --ifm-color-primary-light: var(--color-brand-600); + --ifm-color-primary-lighter: var(--color-brand-600); + --ifm-color-primary-lightest: var(--color-brand-600); + + /* Text & surfaces */ + --ifm-color-content: var(--color-text); + --ifm-color-content-secondary: var(--color-muted); + --ifm-link-color: var(--color-link); + --ifm-background-color: var(--color-bg); + --ifm-background-surface-color: var(--color-surface); + + /* Typography */ + --ifm-font-family-base: var(--font-sans); + --ifm-font-family-monospace: var(--font-mono); + --ifm-font-size-base: var(--font-size-base); + --ifm-line-height-base: var(--line-height-base); + --ifm-font-weight-base: 400; + --ifm-heading-font-weight: 700; + + /* Heading typography specifications */ + --ifm-h1-font-size: 28px; + --ifm-h1-line-height: 32px; + --ifm-h2-font-size: 24px; + --ifm-h2-line-height: 28px; + --ifm-h3-font-size: 20px; + --ifm-h3-line-height: 24px; + --ifm-h4-font-size: 16px; + --ifm-h4-line-height: 16px; + + /* Code typography specifications */ + --ifm-code-font-size: 14px; + --ifm-code-line-height: 150%; + --ifm-code-letter-spacing: -2%; + --ifm-code-padding-horizontal: 4px; + --ifm-code-padding-vertical: 4px; + + /* Layout & spacing */ + --ifm-spacing-horizontal: var(--space-4); + --ifm-global-radius: var(--radius-md); + + /* Component radii (Infima) */ + --ifm-breadcrumb-border-radius: var(--radius-md); + --ifm-button-border-radius: var(--radius-md); + --ifm-card-border-radius: var(--radius-md); + --ifm-alert-border-radius: var(--radius-md); + --ifm-badge-border-radius: var(--radius-md); + --ifm-tabs-border-radius: var(--radius-md); + --ifm-table-border-radius: var(--radius-md); + --ifm-code-border-radius: var(--radius-md); + + /* Sizes */ + --ifm-container-width: var(--container-width); + --ifm-navbar-height: var(--navbar-height); + --ifm-menu-link-padding-horizontal: 6px; + --ifm-menu-link-padding-vertical: 12px; + + /* Borders & shadows */ + --ifm-border-color: var(--color-border); + --ifm-card-box-shadow: var(--shadow-md); + --ifm-global-shadow-lw: var(--shadow-sm); + --ifm-global-shadow-md: var(--shadow-md); + --ifm-global-shadow-tl: var(--shadow-lg); + + /* Tables */ + --ifm-table-cell-padding: 0.75rem; + + /* Alerts */ + --ifm-alert-background-color: var(--color-elevated); + --ifm-alert-border-color: var(--color-border); + + /* Breadcrumbs */ + --ifm-breadcrumb-color-active: var(--color-text); + --ifm-breadcrumb-item-background-active: var(--color-elevated); +} + +/* ========================================== + 2) BASE ELEMENTS + ========================================== */ + +html, +body { + font-family: var(--ifm-font-family-base); + font-size: var(--ifm-font-size-base); + font-weight: var(--ifm-font-weight-base); + line-height: var(--ifm-line-height-base); + color: var(--ifm-color-content); + background: var(--ifm-background-color); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +/* Remove underlines from all links by default */ +a { + color: var(--ifm-link-color); + text-decoration: none; +} + +/* Add underlines only to links within text content */ +p a, +li a, +blockquote a, +.theme-doc-markdown a, +.markdown a, +article a, +main a { + text-decoration: underline; + text-decoration-color: var(--ifm-link-color); + text-underline-offset: 0.2em; +} + +/* Hover effects for text links */ +p a:hover, +li a:hover, +blockquote a:hover, +.theme-doc-markdown a:hover, +.markdown a:hover, +article a:hover, +main a:hover { + text-decoration-color: currentColor; +} + +/* Ensure navigation links have no underlines */ +.navbar a, +.theme-doc-sidebar-container a, +.menu__link, +.breadcrumbs__link, +.pagination-nav__link, +.table-of-contents a { + text-decoration: none !important; +} + +/* Hover effects for navigation links */ +.navbar a:hover, +.theme-doc-sidebar-container a:hover, +.menu__link:hover, +.breadcrumbs__link:hover, +.pagination-nav__link:hover, +.table-of-contents a:hover { + text-decoration: none !important; +} + +/* Headings - exact design specifications */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-sans); + letter-spacing: 0em; + font-weight: bold; +} + +/* H1: 28px font, 32px line height */ +h1 { + font-size: var(--ifm-h1-font-size); + line-height: var(--ifm-h1-line-height); +} + +/* H2: 24px font, 28px line height */ +h2 { + font-size: var(--ifm-h2-font-size); + line-height: var(--ifm-h2-line-height); +} + +/* H3: 20px font, 24px line height */ +h3 { + font-size: var(--ifm-h3-font-size); + line-height: var(--ifm-h3-line-height); +} + +/* H4: 16px line height */ +h4 { + font-size: var(--ifm-h4-font-size); + line-height: var(--ifm-h4-line-height); +} + +h5 { + font-size: 1.125rem; + line-height: 20px; +} + +h6 { + font-size: 1rem; + text-transform: none; + line-height: 16px; +} + +/* Bold text styling */ +strong, +b { + font-weight: 600; + color: var(--ifm-color-content); +} + +/* Body/Paragraph: 14px, 130% line height, Regular */ +p { + font-size: var(--ifm-font-size-base); + line-height: var(--line-height-base); + font-weight: 400; + letter-spacing: 0em; +} + +/* Links (text): 14px, 130% line height, Semibold */ +a { + line-height: 130%; +} + +/* List/Bulletpoints: 14-16px, 150% line height, Regular */ +ul { + list-style-type: disc; + line-height: 150%; +} + +ul li::marker { + color: var(--color-border); +} + +/* Nested unordered lists */ +ul ul { + list-style-type: circle; +} + +ul ul ul { + list-style-type: square; +} + +/* Ordered lists: 14-16px, 150% line height, Regular */ +ol { + list-style-type: decimal; + line-height: 150%; + font-size: var(--font-size-base); + font-weight: 400; +} + +/* Nested ordered lists keep default styling */ +ol ol { + list-style-type: lower-alpha; +} + +ol ol ol { + list-style-type: lower-roman; +} + +/* Blockquote */ +blockquote { + border-left: 3px solid var(--ifm-color-primary); + background: var(--ifm-background-surface-color); + padding: var(--space-4); + border-radius: var(--radius-md); + color: var(--ifm-color-content); +} + +/* Images */ +img { + border-radius: var(--radius-md); +} + +/* ========================================== + 3) NAVBAR & FOOTER (scoped to stable classes) + ========================================== */ + +/* ThemeClassNames.layout.navbar.container */ +.theme-layout-navbar { + height: var(--ifm-navbar-height); + backdrop-filter: saturate(1.1) blur(8px); + border-bottom: 1px solid var(--ifm-border-color) !important; + background: color-mix(in oklab, var(--ifm-background-color) 75%, transparent); + backdrop-filter: saturate(1.1) blur(8px); + box-shadow: none; +} + +/* ThemeClassNames.layout.footer.container */ +.theme-layout-footer { + --ifm-footer-background-color: var(--color-elevated); + background: var(--ifm-footer-background-color); + color: var(--ifm-color-content); +} + +/* ========================================== + 4) SIDEBAR (Docs) + ========================================== */ + +/* ThemeClassNames.docs.docSidebarContainer */ +.theme-doc-sidebar-container { + border-right: 1px solid var(--ifm-border-color) !important; + padding: 5px; + font-size: 0.875rem; /* Sidebar-specific font size */ +} + +/* Main docs content area - wider to fill space */ +.theme-doc-markdown { + max-width: 100%; /* Use full available width */ +} + +/* ThemeClassNames.docs.docSidebarMenu */ +.theme-doc-sidebar-menu .menu__link { + font-family: var(--font-sans); + letter-spacing: -2%; + border-radius: var(--radius-md); + padding: var(--ifm-menu-link-padding-vertical) + var(--ifm-menu-link-padding-horizontal); +} + +.theme-doc-sidebar-menu .menu__link--active { + color: var(--ifm-color-primary); + background: color-mix( + in oklab, + var(--ifm-background-surface-color) 70%, + transparent + ); +} +.theme-doc-sidebar-menu .menu__link--active:hover { + background: var(--ifm-background-surface-color); +} + +/* Category collapsible */ +.menu__list-item-collapsible { + border-radius: var(--radius-md); +} + +/* ========================================== + 5) BREADCRUMBS + ========================================== */ + +/* ThemeClassNames.docs.docBreadcrumbs */ +.theme-doc-breadcrumbs .breadcrumbs__link { + line-height: 150%; + border-radius: var(--radius-md); + padding: var(--ifm-menu-link-padding-horizontal) + var(--ifm-menu-link-padding-vertical); +} + +.theme-doc-breadcrumbs .breadcrumbs__item--active .breadcrumbs__link { + color: var(--ifm-color-primary); +} + +/* ========================================== + 6) BUTTONS + ========================================== */ + +.button { + border-radius: var(--ifm-button-border-radius); + font-weight: 600; + letter-spacing: 0.01em; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05); + line-height: 150%; +} +.button--outline:not(.button--link) { + border-color: color-mix(in oklab, var(--ifm-color-primary) 70%, transparent); +} +.button--link { + color: var(--ifm-link-color); +} + +/* ========================================== + 7) BADGES + ========================================== */ + +.badge { + border-radius: var(--ifm-badge-border-radius); + border: 1px solid var(--ifm-border-color); + background: var(--ifm-background-surface-color); +} + +/* ========================================== + 8) ALERTS (Admonitions) + ========================================== */ + +.theme-admonition { + display: flex; + align-items: flex-start; /* icon aligns with text top */ + gap: 0.75rem; /* spacing between icon and text */ + padding: 1rem 1.25rem; +} + +/* ThemeClassNames.common.admonition */ +.theme-admonition.alert { + --ifm-alert-background-color: var(--color-elevated); + --ifm-alert-border-color: var(--ifm-color-primary); + --ifm-alert-background-color-highlight: #fff; + + font-family: var(--font-mono); + border: 1px solid var(--color-elevated); + border-top: 5px solid var(--ifm-alert-border-color); /* top border accent */ + border-left: none; /* disable default */ + border-radius: var(--ifm-alert-border-radius); + box-shadow: none; + background: var(--ifm-alert-background-color); + color: var(--ifm-color-content); + /* padding: 1rem 1.25rem; */ +} + +/* NOTE */ +.theme-admonition.alert--secondary { + --ifm-alert-border-color: #df9f26; + border-top-color: #df9f26; +} +/* TIP */ +.theme-admonition.alert--success { + --ifm-alert-background-color: var(--color-elevated); + border-top-color: var(--ifm-color-success) !important; +} +/* INFO / IMPORTANT */ +.theme-admonition.alert--info { + --ifm-alert-border-color: #1764a8; + border-top-color: #1764a8; +} +/* WARNING */ +.theme-admonition.alert--warning { + --ifm-alert-border-color: var(--color-warning); + border-top-color: var(--color-warning); +} + +/* DANGER */ +.theme-admonition.alert--danger { + --ifm-alert-border-color: var(--color-danger); + border-top-color: var(--color-danger); +} + +.theme-admonition ul li::marker { + color: var(--ifm-color-primary); +} + +/* Exclude list margins from alert blocks */ +.theme-admonition ul, +.theme-admonition ol { + margin-top: 0 !important; /* Smaller margins for lists in alert blocks */ + margin-bottom: 0 !important; + padding-left: 1.5rem !important; /* Standard padding for lists in alerts */ +} + +/* Nested lists in alert blocks should have no extra margin */ +.theme-admonition ul ul, +.theme-admonition ol ol, +.theme-admonition ul ol, +.theme-admonition ol ul { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +/* VERSION BANNER */ +.theme-doc-version-banner { + --ifm-alert-background-color: var(--color-elevated); + --ifm-alert-border-color: var(--color-warning); + + font-family: var(--font-mono); + border: 1px solid var(--color-elevated); + border-top: 6px solid var(--ifm-alert-border-color); + border-left: none; /* disable default */ + color: var(--ifm-color-content); +} + +/* ========================================== + 9) TABS + ========================================== */ + +/* ThemeClassNames.tabs.container */ +.theme-tabs-container .tabs { + --ifm-tabs-padding-vertical: 0.375rem; + --ifm-tabs-padding-horizontal: 0.75rem; +} +.theme-tabs-container .tabs__item { + border-radius: var(--ifm-tabs-border-radius); +} +.theme-tabs-container .tabs__item--active { + background: var(--ifm-background-surface-color); + box-shadow: inset 0 0 0 2px + color-mix(in oklab, var(--ifm-color-primary) 24%, transparent); +} + +/* ========================================== + 10) CARDS + ========================================== */ + +.card { + border-radius: var(--ifm-card-border-radius); + box-shadow: var(--ifm-card-box-shadow); + border: 1px solid var(--ifm-border-color); + background: var(--ifm-background-surface-color); +} + +/* ========================================== + 11) TABLES + ========================================== */ + +/* Apply border radius to the entire table */ +table { + border-radius: 5px; /* Adjust this value to change corner rounding */ +} + +tbody tr { + border-top: none; + background: var(--ifm-background-surface-color); +} + +/* ========================= + 12. Code block card + header + ========================= */ + +.theme-code-block { + border: 1px solid var(--ifm-border-color); + border-radius: 5px; + position: relative; +} + +/* Target CSS module class using attribute selector to handle hashed names */ +.theme-code-block [class*="codeBlockTitle"] { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + font-family: var(--font-sans) !important; + font-size: 0.875rem !important; + font-weight: 500 !important; + letter-spacing: 0.02rem !important; + color: var(--color-base) !important; + border-bottom: 1px solid var(--ifm-border-color) !important; +} + +/* Body */ +.theme-code-block pre { + margin: 0; + background: color-mix( + in oklab, + var(--color-surface) 96%, + var(--color-bg, #fff) + ); + padding: 16px 18px; +} + +/* Code text - design specifications */ +.theme-code-block code { + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + font-family: var(--font-mono); + font-size: var(--ifm-code-font-size); /* 14px */ + line-height: var(--ifm-code-line-height); /* 24px */ + letter-spacing: var(--ifm-code-letter-spacing); /* -2% */ + color: var(--color-text); +} + +/* Inline code - design specifications */ +code { + font-family: var(--font-mono); + font-size: 12px; /* 14px */ + line-height: 24px; /* 24px */ + letter-spacing: var(--ifm-code-letter-spacing); /* -2% */ + font-weight: 400; +} + +/* Highlighted lines / magic comments */ +.token-line.highlighted, +.code-block-highlighted-line { + background: color-mix(in oklab, var(--ifm-color-primary) 12%, transparent); +} + +/* Optional: softer $ prompt tint in bash */ +.theme-code-block[data-language="bash"] pre code .token.operator { + opacity: 0.9; +} + +/* Dark tweaks */ +[data-theme="dark"] .theme-code-block .codeBlockTitle { + background: color-mix(in oklab, var(--color-surface) 85%, transparent); +} + +/* ========================================== + 12.1) DETAILS/SUMMARY (Expandable Sections) - styled like code blocks + ========================================== */ + +/* Scope to docs content only */ +.theme-doc-markdown details { + border: 1px solid var(--ifm-border-color); + border-radius: 5px; + background: var(--ifm-background-surface-color); + margin: 0.75rem 0 1rem; + overflow: hidden; /* crisp rounded corners */ + padding: 0; +} + +/* The clickable header row */ +.theme-doc-markdown details > summary { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + font-weight: 500; + text-transform: uppercase; + font-family: var(--font-sans); + line-height: 1.5; + cursor: pointer; + list-style: none; + user-select: none; + color: var(--color-text); + border-bottom: 1px solid transparent; +} + +/* Hide native disclosure marker */ +.theme-doc-markdown details > summary::marker, +.theme-doc-markdown details > summary::-webkit-details-marker { + display: none; + content: ""; +} + +/* Custom caret icon */ +.theme-doc-markdown details > summary::before { + content: ""; + width: 3px; + height: 3px; + flex-shrink: 0; + -webkit-mask: url("/img/custom_caret.svg") no-repeat center / contain; + mask: url("/img/custom_caret.svg") no-repeat center / contain; + background: currentColor; + transition: transform 0.15s ease; + transform: rotate(0deg); + transform-origin: center center; /* Rotate around its own center */ + opacity: 0.8; + position: relative; + top: 0; +} + +.theme-doc-markdown details[open] > summary { + border-bottom-color: var(--ifm-border-color); +} + +/* Rotate caret when details is open */ +.theme-doc-markdown details[open] > summary::before { + transform: rotate(90deg); + transform-origin: center center; /* Keep center rotation when open */ +} + +/* Body content area */ +.theme-doc-markdown details > :not(summary) { + padding: 1rem; +} + +/* Code block inside details (optional polish) */ +.theme-doc-markdown details pre { + margin: 0; + background: color-mix(in oklab, var(--color-surface) 96%, var(--color-bg)); + border-radius: 6px; +} + +.theme-doc-markdown details > summary { + border-top: none !important; + box-shadow: none !important; +} + +[class^="collapsibleContent_"], +[class*=" collapsibleContent_"] { + padding-top: 0 !important; + border-top: none !important; + box-shadow: none !important; +} + +[class^="collapsibleContent_"]::before, +[class*=" collapsibleContent_"]::before { + display: none !important; + content: none !important; +} + +/* ========================================== + 13) PAGINATION + ========================================== */ + +.pagination-nav__link { + border: 1px solid var(--ifm-border-color); + border-radius: var(--radius-md); + box-shadow: var(--ifm-global-shadow-lw); + background: var(--ifm-background-surface-color); + transition: all 0.2s ease; +} + +/* Hide "Previous" and "Next" labels */ +.pagination-nav__sublabel { + display: none; +} + +/* Main pagination link text - uppercase */ +.pagination-nav__label { + text-transform: uppercase; + font-weight: 500; + letter-spacing: 0.02em; + font-size: 1.2rem; + font-family: var(--font-mono); + color: color-mix(in oklab, #292929bf 75%, #000); +} + +/* Arrow text characters (>> and <<) */ +.pagination-nav__link--prev::before, +.pagination-nav__link--next::after { + color: var(--ifm-color-primary) !important; + font-weight: 700 !important; + font-size: 1.25rem !important; +} + +/* If arrows are in a separate element */ +.pagination-nav__link .pagination-nav__icon, +.pagination-nav__link--prev .pagination-nav__icon, +.pagination-nav__link--next .pagination-nav__icon { + color: var(--ifm-color-primary) !important; +} + +/* ========================================== + 14) TOC (right sidebar) + ========================================== */ + +/* TOC links styling */ +.table-of-contents { + font-family: var(--font-sans); + letter-spacing: 0.01rem; + border-left: 1px solid var(--ifm-border-color); + padding-left: var(--space-4); + font-size: 0.875rem; +} + +.table-of-contents__link { + color: var(--color-text); + display: block; + padding: 4px 0; +} + +.table-of-contents__link--active { + color: var(--ifm-color-primary); + font-weight: 500; +} + +/* ========================================== + 15) VERSION INDICATOR STYLING + ========================================== */ + +/* Style the version indicator that appears above page content */ +.theme-doc-version-badge { + font-size: 0.75rem !important; + font-weight: 600 !important; + color: var(--ifm-color-primary) !important; + background: color-mix( + in oklab, + var(--ifm-color-primary) 12%, + transparent + ) !important; + border: 1px solid + color-mix(in oklab, var(--ifm-color-primary) 30%, transparent) !important; + border-radius: var(--radius-md) !important; + /* line-height: 0 !important; */ + padding-top: 0.37rem !important; + display: inline-block !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +/* ========================================== + 16) LOCAL SEARCH STYLING + ========================================== */ + +/* Local search modal styling - integrate with design system */ +.aa-Panel { + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + border: 1px solid var(--color-border); + background: var(--color-bg); +} + +.aa-Form { + border-radius: var(--radius-md); + background: var(--color-surface); + border: 1px solid var(--color-border); +} + +.aa-Input { + font-family: var(--font-sans); + font-size: var(--font-size-base); + color: var(--color-text); + background: transparent; +} + +.aa-Item { + border-radius: var(--radius-md); + color: var(--color-text); +} + +.aa-Item[aria-selected="true"] { + background: var(--color-surface); +} + +.aa-ItemContentTitle { + font-family: var(--font-sans); + font-weight: 600; + color: var(--color-text); +} + +.aa-ItemContentDescription { + font-family: var(--font-sans); + color: var(--color-muted); +} + +/* ========================================== + 17) NUMERED LISTS + ========================================== */ + +/* === DOCUSAURUS "VitePress-style" numbered steps === */ +.steps { + counter-reset: step-counter; + list-style: none; + padding-left: 1.5rem; + margin-left: 2.5rem; /* Space for number badges and line */ + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} +.steps > ol, +.steps > ul { + list-style: none; + margin-left: 0; + padding-left: 0; +} +.steps li { + counter-increment: step-counter; + position: relative; + margin-bottom: 2rem; + min-height: 2rem; +} + +/* Number badge - dynamically aligns with first element */ +.steps li::before { + content: counter(step-counter); + position: absolute; + left: -4rem; /* Align with .steps margin-left */ + top: 0.15rem; /* Slight offset for visual centering with text baseline */ + width: 2rem; + min-height: 2rem; + border-radius: 0px; + background: var(--ifm-background-surface-color); + color: var(--color-text); + font-size: 1rem; + font-family: var(--font-mono); + padding: 0.3rem 0.4rem; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--ifm-border-color); + z-index: 2; + flex-shrink: 0; + line-height: 1; +} + +/* All headings inside steps get proper spacing */ +.steps li > h1, +.steps li > h2, +.steps li > h3, +.steps li > h4, +.steps li > h5, +.steps li > h6 { + margin-top: 0rem !important; + margin-bottom: 1.5rem !important; + line-height: 1.3 !important; + padding-top: 0.7rem !important; +} /* Specific h5 styling (most common) */ +.steps li > h5 { + font-size: 1.15rem !important; + font-weight: 600 !important; +} /* Style for all content inside step items */ +.steps li > * { + margin-bottom: 1rem; +} +.steps li > *:last-child { + margin-bottom: 0; +} + +/* Paragraphs right after headings - reduce space */ +.steps li > h1 + p, +.steps li > h2 + p, +.steps li > h3 + p, +.steps li > h4 + p, +.steps li > h5 + p, +.steps li > h6 + p { + margin-top: -0.75rem; +} + +/* First paragraph without heading */ +.steps li > p:first-child { + margin-top: 0; +} /* Code blocks within steps */ +.steps li pre, +.steps li .theme-code-block { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +/* Lists within steps */ +.steps li ul, +.steps li ol { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + padding-left: 1.5rem; +} /* Admonitions within steps */ +.steps li .theme-admonition { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +/* ========================================== + 18) DESIGN SYSTEM SPACING SPECIFICATIONS + ========================================== */ + +/* Apply design system spacing to documentation content */ +.theme-doc-markdown h1 { + margin-bottom: var(--space-24px); /* H1 → First content block: 24px */ +} + +.theme-doc-markdown h2 { + margin-top: var( + --space-32px + ); /* H2 → previous section (major section break): 32px */ + margin-bottom: var(--space-16px); /* H2 → Paragraph below: 16px */ +} + +.theme-doc-markdown h3 { + margin-bottom: var(--space-16px); /* H3 → Paragraph below: 16px */ +} + +.theme-doc-markdown h4 { + margin-bottom: var(--space-16px); /* H4 → Paragraph below: 16px */ +} + +/* Paragraph spacing */ +.theme-doc-markdown p { + margin-bottom: var(--space-16px); /* Paragraph → Paragraph: 16px */ +} + +/* List spacing */ +.theme-doc-markdown ul, +.theme-doc-markdown ol { + margin-top: var(--space-16px); /* Paragraph → List: 16px */ + margin-bottom: var(--space-16px); /* List → Next Paragraph: 16px */ + padding-left: var(--space-24px); /* List indent: 24px */ +} + +/* H2 to large components (images, tables, diagrams) */ +.theme-doc-markdown h2 + img, +.theme-doc-markdown h2 + table, +.theme-doc-markdown h2 + .theme-admonition { + margin-top: var(--space-24px); /* H2 → large component: 24px */ +} + +/* Major structural reset for dramatic transitions */ +.theme-doc-markdown > *:first-child { + margin-top: 0; +} + +/* Section breaks - ensure consistent spacing between major sections */ +.theme-doc-markdown h1 + h2 { + margin-top: var(--space-32px); /* Major section break */ +} diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000000..920d7a6523 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,8 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [".docusaurus", "build"] +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..6744e56e15 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "1.90" +components = ["clippy", "rust-src", "rustfmt"] +profile = "minimal" +targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check-msrv.sh b/scripts/check-msrv.sh new file mode 100755 index 0000000000..0bde2955f0 --- /dev/null +++ b/scripts/check-msrv.sh @@ -0,0 +1,153 @@ +#!/bin/bash +set -e +set -o pipefail + +# Enhanced MSRV checking script for workspace repository +# Checks MSRV for each workspace member and provides helpful error messages + +# ---- utilities -------------------------------------------------------------- + +check_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "ERROR: Required command '$1' is not installed or not in PATH" + exit 1 + fi +} + +# Check required commands +check_command "cargo" +check_command "jq" +check_command "rustup" +check_command "sed" +check_command "grep" +check_command "awk" + +# Portable in-place sed (GNU/macOS); usage: sed_i 's/foo/bar/' file +# shellcheck disable=SC2329 # used quoted +sed_i() { + if sed --version >/dev/null 2>&1; then + sed -i "$@" + else + sed -i '' "$@" + fi +} + +# ---- repo root -------------------------------------------------------------- + +# Get the directory where this script is located and change to the parent directory +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$DIR/.." + +echo "Checking MSRV for workspace members..." + +# ---- metadata -------------------------------------------------------------- + +metadata_json="$(cargo metadata --no-deps --format-version 1)" +workspace_root="$(printf '%s' "$metadata_json" | jq -r '.workspace_root')" + +failed_packages="" + +# Iterate actual workspace packages with manifest paths and (maybe) rust_version +# Fields per line (TSV): id name manifest_path rust_version_or_empty +while IFS=$'\t' read -r pkg_id package_name manifest_path rust_version; do + # Derive package directory (avoid external dirname for portability) + package_dir="${manifest_path%/*}" + if [[ -z "$package_dir" || "$package_dir" == "$manifest_path" ]]; then + package_dir="." + fi + + echo "Checking $package_name ($pkg_id) in $package_dir" + + if [[ ! -f "$package_dir/Cargo.toml" ]]; then + echo "WARNING: No Cargo.toml found in $package_dir, skipping..." + continue + fi + + # Prefer cargo metadata's effective rust_version if present + current_msrv="$rust_version" + if [[ -z "$current_msrv" ]]; then + # If the crate inherits: rust-version.workspace = true + if grep -Eq '^\s*rust-version\.workspace\s*=\s*true\b' "$package_dir/Cargo.toml"; then + # Read from workspace root [workspace.package] + current_msrv="$(grep -Eo '^\s*rust-version\s*=\s*"[^"]+"' "$workspace_root/Cargo.toml" | head -n1 | sed -E 's/.*"([^"]+)".*/\1/')" + if [[ -n "$current_msrv" ]]; then + echo " Using workspace MSRV: $current_msrv" + fi + fi + fi + + if [[ -z "$current_msrv" ]]; then + echo "WARNING: No rust-version found (package or workspace) for $package_name" + continue + fi + + echo " Current MSRV: $current_msrv" + + # Try to verify the MSRV + if ! cargo msrv verify --manifest-path "$package_dir/Cargo.toml" >/dev/null 2>&1; then + echo "ERROR: MSRV check failed for $package_name" + failed_packages="$failed_packages $package_name" + + echo "Searching for correct MSRV for $package_name..." + + # Determine the currently-installed stable toolchain version (e.g., "1.81.0") + latest_stable="$(rustup run stable rustc --version 2>/dev/null | awk '{print $2}')" + if [[ -z "$latest_stable" ]]; then latest_stable="1.81.0"; fi + + # Search for the actual MSRV starting from the current one + if actual_msrv=$(cargo msrv find \ + --manifest-path "$package_dir/Cargo.toml" \ + --min "$current_msrv" \ + --max "$latest_stable" \ + --output-format minimal 2>/dev/null); then + echo " Found actual MSRV: $actual_msrv" + echo "" + echo "ERROR SUMMARY for $package_name:" + echo " Package: $package_name" + echo " Directory: $package_dir" + echo " Current (incorrect) MSRV: $current_msrv" + echo " Correct MSRV: $actual_msrv" + echo "" + echo "TO FIX:" + echo " Update rust-version in $package_dir/Cargo.toml from \"$current_msrv\" to \"$actual_msrv\"" + echo "" + echo " Or run this command (portable in-place edit):" + echo " sed_i 's/^\\s*rust-version\\s*=\\s*\"$current_msrv\"/rust-version = \"$actual_msrv\"/' \"$package_dir/Cargo.toml\"" + else + echo " Could not determine correct MSRV automatically" + echo "" + echo "ERROR SUMMARY for $package_name:" + echo " Package: $package_name" + echo " Directory: $package_dir" + echo " Current (incorrect) MSRV: $current_msrv" + echo " Could not automatically determine correct MSRV" + echo "" + echo "TO FIX:" + echo " Run manually: cargo msrv find --manifest-path \"$package_dir/Cargo.toml\"" + fi + echo "-------------------------------------------------------------------------------" + else + echo "OK: MSRV check passed for $package_name" + fi + echo "" + +done < <( + printf '%s' "$metadata_json" \ + | jq -r '. as $m + | $m.workspace_members[] + | . as $id + | ($m.packages[] | select(.id == $id) + | [ .id, .name, .manifest_path, (.rust_version // "") ] | @tsv)' +) + +if [[ -n "$failed_packages" ]]; then + echo "MSRV CHECK FAILED" + echo "" + echo "The following packages have incorrect MSRV settings:$failed_packages" + echo "" + echo "Please fix the rust-version fields in the affected Cargo.toml files as shown above." + exit 1 +else + echo "ALL WORKSPACE MEMBERS PASSED MSRV CHECKS!" + exit 0 +fi \ No newline at end of file