diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md index 0d56fa2c74..617d3eba11 100644 --- a/.claude/agents/code-reviewer.md +++ b/.claude/agents/code-reviewer.md @@ -90,9 +90,13 @@ then the incompleteness itself is NOT a Critical or Important finding - the chan ## Output Format ``` +: [see the closing instructions at the end of this file. This must +be the literal first thing in your response, substituting the real token for what's +inside the angle brackets - do not write this line verbatim.] + ## Review Summary -**Verdict:** APPROVE | REQUEST CHANGES +**Verdict:** APPROVE | BLOCK **Overview:** [1-2 sentences summarizing the change and overall assessment] @@ -109,6 +113,14 @@ then the incompleteness itself is NOT a Critical or Important finding - the chan - [Specific positive observation - always include at least one] ``` +### Empty Sections + +If `### Critical Issues` or `### Important Issues` has no findings, its first line must +open with `None.` - either bare, or with a period-terminated explanation on the same +line; further explanation on the lines below is fine either way. The rule cuts the +other way too: if the section has any real finding, do not write `None.` anywhere in +it - list the finding(s) directly. A section never contains both. + ## Rules 1. Every Critical and Important finding must include a specific fix recommendation @@ -119,6 +131,7 @@ then the incompleteness itself is NOT a Critical or Important finding - the chan 6. Be direct. "This will panic when the vec is empty" not "this might possibly be a concern" 7. New code without tests is always a finding 8. Respect the user's intent. Your prompt may name what the user asked for this session - treat deliberate, explicitly-requested choices as intended, not mistakes, and don't recommend reversing them. Intent does not excuse a real defect: a genuine correctness bug or exploitable risk stays Critical or Important even when requested. Downgrade to a Nit only when your objection is stylistic or defensive-programming preference, not a real defect. +9. Never mix `None.` with a real finding in the same section (see Empty Sections above). `### Critical Issues` / `### Important Issues` either has `None.` as its entire content, or lists real findings with no `None.` line anywhere in it - never both. **Critical and Important findings block the merge; Nits are surfaced but do not block.** Address the blocking findings before pushing. diff --git a/.claude/agents/security-reviewer.md b/.claude/agents/security-reviewer.md index bc003e7661..d728de6c45 100644 --- a/.claude/agents/security-reviewer.md +++ b/.claude/agents/security-reviewer.md @@ -95,6 +95,10 @@ then classify it as a NOTE, not CRITICAL or WARNING. Surfacing it keeps it visib ## Output Format ``` +: [see the Verdicts below. This must be the literal first thing in +your response, substituting the real token for what's inside the angle brackets - do +not write this line verbatim.] + ## Adversarial Security Review **Verdict:** BLOCK | CLEAN @@ -118,9 +122,19 @@ then classify it as a NOTE, not CRITICAL or WARNING. Surfacing it keeps it visib - **BLOCK** - Any Critical or Warning finding. Do not merge until addressed. - **CLEAN** - No Critical or Warning findings (Notes, if any, are surfaced but do not block). Safe to merge. +### Empty Sections + +If `### Critical Findings` or `### Warnings` has no findings, its first line must open +with `None.` - either bare, or with a period-terminated explanation on the same line +("None. I tried X, Y, Z..."); further diligence notes on the lines below are fine +either way. The rule cuts the other way too: if the section has any real finding, do +not write `None.` anywhere in it - list the finding(s) directly. A section never +contains both. + ## Anti-Patterns - Do NOT Do These - **"LGTM, no issues found"** - Be skeptical if you found nothing, but don't fabricate findings. If a change is genuinely clean, use the CLEAN verdict. +- **Mixing `None.` with a real finding in the same section** - `### Warnings` / `### Critical Findings` either has `None.` as its entire content, or lists real findings with no `None.` line anywhere in it. Never both. - **Pulling punches** - "This might possibly be a minor concern" is useless. Say what's wrong. - **Restating the diff** - "This function was added" is not a finding. What's WRONG with it? - **Cosmetic-only findings** - Reporting style issues while missing a panic is worse than no review. diff --git a/.claude/hooks/_review.py b/.claude/hooks/_review.py index 1003136fa9..aaa7ac7ae3 100644 --- a/.claude/hooks/_review.py +++ b/.claude/hooks/_review.py @@ -10,6 +10,13 @@ BLOCK on ### Critical Issues | ### Critical Findings ### Important Issues | ### Warnings IGNORE ### Nits | ### Notes | ### What's Done Well | ### Summary + +Both prompts also require the response to open with a bare `BLOCK:` / +`CLEAN:` / `APPROVE:` token. `_evaluate_reviewer` treats its absence as +malformed output (blocks), and blocks on a leading `BLOCK:` even when the +section-based count comes back 0 - a backstop for a section that opens with +`None.` but is mistakenly followed by real findings; see +`_count_blocking_findings`. """ from __future__ import annotations @@ -29,8 +36,21 @@ _SECOND_LEVEL = re.compile(r"^##[^#]|^## ") # A bullet line `-` or `*` followed by content. _BULLET = re.compile(r"^\s*[-*]\s+\S") -# Absence markers we explicitly do NOT count as findings. -_ABSENCE = re.compile(r"^\s*[-*]\s+(None|N/A|n/a)\.?\s*$") +# A line that is just "None"/"N/A" - bare, bolded, or with a period- +# terminated explanation on the same line ("None. I tried X, Y, Z..."). No +# ":" terminator (too easy for a real finding like "None: no authz check" +# to slip through) - a real finding like "None of the callers validate X" +# never matches either way, since there's no "." or end-of-line right after +# "None". +_ABSENCE = re.compile(r"^\s*(?:[-*]\s+)?\**(none|n/a)\**(?:\.\**(?:\s.*)?|\s*)$", re.IGNORECASE) +# The mandatory leading token both prompts require, tolerating markdown bold +# around it (the same habit `_ABSENCE` above tolerates) so the same model +# bolding its own verdict doesn't turn a clean review into a spurious block. +# Matched as its own line anywhere in the text before the review body starts +# (see `_leading_verdict`) rather than strictly at byte 0, since a model +# occasionally prefaces it with a sentence or two before the formatted +# token line. +_LEADING_VERDICT = re.compile(r"^\s*\**(BLOCK|CLEAN|APPROVE)\**\s*:", re.IGNORECASE | re.MULTILINE) @dataclass @@ -115,7 +135,9 @@ def _run_reviewer(agent: str, prompt: str, cwd: str | None) -> tuple[int, str, s def _evaluate_reviewer(result: ReviewerResult) -> tuple[bool, str]: """Return `(cleared, rendered)` for one reviewer. `cleared` is False if - this reviewer blocks (crash, malformed output, or a blocking finding).""" + this reviewer blocks: a crash, malformed output (no `### ` sections, or + no leading verdict token), a blocking finding, or a self-reported BLOCK + verdict despite a 0 count.""" lines = [f"=== {result.name} ==="] if result.returncode != 0: @@ -127,16 +149,33 @@ def _evaluate_reviewer(result: ReviewerResult) -> tuple[bool, str]: return False, "\n".join(lines) if not _looks_like_review(result.stdout): - lines.append(f"{result.name}: empty or malformed output; treating as block.") + lines.append(f"{result.name}: empty output or no `### ` sections found; treating as block.") if result.stdout: lines.append(result.stdout) return False, "\n".join(lines) + leading = _leading_verdict(result.stdout) + if not leading: + lines.append( + f"{result.name}: response did not open with a `BLOCK:`/`CLEAN:`/`APPROVE:` token " + "as its prompt requires; treating as block." + ) + lines.append(result.stdout) + return False, "\n".join(lines) + lines.append(result.stdout) count = _count_blocking_findings(result.stdout) if count > 0: lines.append(f"{result.name}: {count} blocking finding(s) (Critical/Important/Warning).") return False, "\n".join(lines) + + if leading.group(1).upper() == "BLOCK": + lines.append( + f"{result.name}: 0 structured findings counted, but the agent's own leading " + "verdict says BLOCK; treating as block." + ) + return False, "\n".join(lines) + lines.append(f"{result.name}: no blocking findings (nits/notes do not block).") return True, "\n".join(lines) @@ -145,26 +184,51 @@ def _looks_like_review(text: str) -> bool: return bool(text.strip()) and any(line.startswith("### ") for line in text.splitlines()) +def _leading_verdict(text: str) -> re.Match[str] | None: + """Find the mandatory leading token, searching only the text before the + first `##`/`### ` heading - i.e. the preamble the prompts require it to + open with. A plain `.match()` at byte 0 is too strict: a model + occasionally writes a sentence or two before the token line even though + told to lead with it. Restricting the search to the preamble (rather + than the whole document) keeps a quoted example of the token deeper in + the review body from being mistaken for the real one. + """ + preamble = text.split("\n##", 1)[0] + return _LEADING_VERDICT.search(preamble) + + def _count_blocking_findings(text: str) -> int: """Walk the reviewer's markdown line by line. Count bullets that appear under `### Critical Issues / ### Important Issues / ### Warnings` - headings, treating any other `### ` heading or `##` heading as the end of - the current section. Bullets matching `- None.` / `- N/A` are explicitly - skipped — those are absence markers, not findings. + headings, treating any other `### ` heading or `##` heading as the end + of the current section. A section is cleared - and the rest of its + lines ignored - as soon as its first non-blank content line matches + `_ABSENCE`. A stray absence marker elsewhere in the section only skips + itself, so a real finding followed by a later `- None.` isn't + double-counted. """ count = 0 in_block = False + section_cleared = False + saw_content = False for line in text.splitlines(): if _SECOND_LEVEL.match(line): in_block = False continue if _ANY_THIRD_LEVEL.match(line): in_block = bool(_BLOCKING_HEADINGS.match(line)) + section_cleared = False + saw_content = False + continue + if not in_block or section_cleared: continue - if not in_block: + if not line.strip(): continue if _ABSENCE.match(line): + if not saw_content: + section_cleared = True continue + saw_content = True if _BULLET.match(line): count += 1 return count diff --git a/.claude/hooks/tests/test_hooks.py b/.claude/hooks/tests/test_hooks.py index d4efc10e7b..b63e3b26b6 100644 --- a/.claude/hooks/tests/test_hooks.py +++ b/.claude/hooks/tests/test_hooks.py @@ -189,6 +189,232 @@ def test_count_blocking_findings_ignores_absence_markers() -> None: assert _review._count_blocking_findings(review) == 0 +def test_count_blocking_findings_ignores_notes_after_bare_none() -> None: + """Reproduces the reported bug: the reviewer opens the section with a bare + "None." line, then explains what it tried below it. Those bullets must + not count as findings.""" + import _review + + review = "\n".join( + [ + "### Warnings", + "", + "None.", + "", + "I specifically tried and failed to break the following:", + "- Attachment-shadowing.", + "- Private/malformed targets.", + "### Notes", + "- consider adding a regression test", + ] + ) + assert _review._count_blocking_findings(review) == 0 + + +def test_count_blocking_findings_ignores_notes_after_same_line_none() -> None: + """Reproduces the exact reported bug verbatim: "None." and the + explanation share one line, e.g. "None. I specifically tried and failed + to break the following:", followed by diligence bullets. Those bullets + must not count as findings.""" + import _review + + review = "\n".join( + [ + "### Warnings", + "", + "None. I specifically tried and failed to break the following:", + "", + "- **Attachment-shadowing.** `ensure_presence` validates every attachment.", + "- **Private/malformed targets.** `TryFrom` terminates in `NetworkAccountTarget::new`.", + "### Notes", + "- consider adding a regression test", + ] + ) + assert _review._count_blocking_findings(review) == 0 + + +def test_count_blocking_findings_does_not_treat_none_of_as_absence() -> None: + """A finding starting with the word "None" (e.g. "None of the callers + validate this") is not an absence marker - the trailing text keeps it + off the exact-line match, so it must still be counted.""" + import _review + + review = "\n".join( + [ + "### Important Issues", + "- None of the new branches are covered by a test.", + ] + ) + assert _review._count_blocking_findings(review) == 1 + + +def test_count_blocking_findings_treats_bold_none_as_absence_marker() -> None: + """Reviewers habitually bold things, and the period can land inside or + outside the closing `**` (`**None.**` or `**None**.`). Both, bare or + bulleted, must clear the section exactly like a plain `None.` would.""" + import _review + + review = "\n".join( + [ + "### Warnings", + "**None.**", + "- Attachment-shadowing details ruled out.", + "### Critical Issues", + "- **None.**", + "- Private targets ruled out.", + ] + ) + assert _review._count_blocking_findings(review) == 0 + + +def test_count_blocking_findings_counts_real_finding_before_trailing_none() -> None: + """A real finding bullet followed later by a stray `- None.` bullet must + count once, not twice - the `None.` special-case only applies when it's + the section's first content line; elsewhere it's just skipped, not + treated as clearing anything.""" + import _review + + review = "\n".join( + [ + "### Important Issues", + "- foo.rs:10 will panic on empty input", + "- None.", + ] + ) + assert _review._count_blocking_findings(review) == 1 + + +def test_count_blocking_findings_clearing_does_not_leak_into_next_section() -> None: + """A `None.`-cleared section must not suppress a *different* blocking + section that follows it - `section_cleared` has to reset on every new + `### ` heading, not just stay set once tripped.""" + import _review + + review = "\n".join( + [ + "### Warnings", + "None. I tried the following:", + "- attachment shadowing ruled out", + "### Critical Issues", + "- foo.rs:10 unchecked unwrap panics on empty input", + ] + ) + assert _review._count_blocking_findings(review) == 1 + + +# _evaluate_reviewer's verdict backstop: a section can be structurally +# cleared (a "None." opener followed by content that reads as ordinary +# bullets) while genuinely containing real findings further down - the +# section-clearing logic in _count_blocking_findings can't tell diligence +# bullets from real ones once a section is cleared. The agent's own leading +# verdict token is a second, independent signal that catches this case even +# when the structured count comes back 0. +def test_evaluate_reviewer_blocks_on_self_reported_block_despite_zero_count() -> None: + import _review + + stdout = ( + "BLOCK:\n\n" + "## Adversarial Security Review\n\n" + "### Warnings\n" + "None. But actually see the Critical Findings below.\n\n" + "### Notes\n" + "- unrelated note\n" + ) + result = _review.ReviewerResult(name="SECURITY REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, rendered = _review._evaluate_reviewer(result) + assert cleared is False + assert "verdict says BLOCK" in rendered + + +@pytest.mark.parametrize("token", ["CLEAN:", "APPROVE:"]) +def test_evaluate_reviewer_clears_on_zero_count_for_either_clean_token(token: str) -> None: + """Both `CLEAN:` (security-reviewer) and `APPROVE:` (code-reviewer) must + clear a 0-count review.""" + import _review + + stdout = f"{token}\n\n## Review Summary\n\n### Critical Issues\nNone.\n### Important Issues\nNone.\n" + result = _review.ReviewerResult(name="REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, _rendered = _review._evaluate_reviewer(result) + assert cleared is True + + +def test_evaluate_reviewer_blocks_when_leading_token_is_missing() -> None: + """Without the mandatory leading token, block with a diagnosable reason + instead of silently running with the backstop disabled.""" + import _review + + stdout = "## Adversarial Security Review\n\n### Warnings\nNone.\n### Notes\n- fine\n" + result = _review.ReviewerResult(name="SECURITY REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, rendered = _review._evaluate_reviewer(result) + assert cleared is False + assert "did not open with a" in rendered + + +@pytest.mark.parametrize("token", ["**APPROVE:**", "**APPROVE**:"]) +def test_evaluate_reviewer_clears_on_bolded_leading_token(token: str) -> None: + """The same model that bolds diligence notes bolds its own leading + token too - must not turn a clean review into a spurious block.""" + import _review + + stdout = f"{token}\n\n## Review Summary\n\n### Critical Issues\nNone.\n### Important Issues\nNone.\n" + result = _review.ReviewerResult(name="CODE REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, _rendered = _review._evaluate_reviewer(result) + assert cleared is True + + +def test_evaluate_reviewer_clears_when_token_follows_a_preamble_sentence() -> None: + """Reproduces an observed failure: the reviewer wrote a sentence or two + (e.g. noting it couldn't run the test suite) before its own leading + token line, instead of leading with the bare token as instructed. The + token must still be found - not just at byte 0.""" + import _review + + stdout = ( + "I could not execute the test suite in this session, so I traced the " + "changes by hand instead.\n\n" + "APPROVE:\n\n" + "## Review Summary\n\n### Critical Issues\nNone.\n### Important Issues\nNone.\n" + ) + result = _review.ReviewerResult(name="CODE REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, _rendered = _review._evaluate_reviewer(result) + assert cleared is True + + +def test_evaluate_reviewer_ignores_token_quoted_later_in_the_review_body() -> None: + """The preamble search must not reach past the first `##` heading - a + token-shaped line appearing on its own line deeper in the review body + (e.g. an illustrative example, which would match `_LEADING_VERDICT` in + isolation) must not be mistaken for the real leading token when the real + one is genuinely missing.""" + import _review + + stdout = ( + "## Review Summary\n\n" + "### Nits\n" + "- An example of what a leading token line looks like:\n" + "CLEAN:\n" + "- (illustrative only, not this response's own declaration)\n" + "### Critical Issues\nNone.\n### Important Issues\nNone.\n" + ) + result = _review.ReviewerResult(name="CODE REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, rendered = _review._evaluate_reviewer(result) + assert cleared is False + assert "did not open with a" in rendered + + +def test_evaluate_reviewer_count_takes_precedence_over_self_reported_clean() -> None: + """A structured finding always blocks, even if the agent's own leading + token contradicts it (e.g. says CLEAN). The count check must run before + the verdict-based backstop, not after.""" + import _review + + stdout = "CLEAN:\n\n## Review Summary\n\n### Critical Issues\n- foo.rs:1 real bug\n" + result = _review.ReviewerResult(name="CODE REVIEWER", returncode=0, stdout=stdout, stderr="") + cleared, rendered = _review._evaluate_reviewer(result) + assert cleared is False + assert "1 blocking finding(s)" in rendered + + # pre_pr_review reviews the whole PR against the integration branch. Verify the # base resolves to origin/HEAD when set and falls back to origin/next otherwise. def _fake_proc(returncode: int = 0, stdout: str = "", stderr: str = "") -> SimpleNamespace: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b053bc2f0..39ab965924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v0.17.0 (TBD) + +### Features + +### Changes + +- [BREAKING] Changed asset callbacks into validation-only interfaces that return no asset value; the transaction kernel retains and uses the original value, preventing callbacks from modifying it. The kernel commitment changes ([#3505](https://github.com/0xMiden/protocol/issues/3505), [#3513](https://github.com/0xMiden/protocol/pull/3513)). +- [BREAKING] Extracted the shared `MastForestScript` type and `MastForestScriptError` backing `NoteScript` / `TransactionScript`, moving `TransactionScript` into `transaction::script` ([#3516](https://github.com/0xMiden/protocol/pull/3516)). +- Documented the RBAC freeze-only actor pattern on `Authority` and added test coverage pinning that a `FREEZER` can trip the emergency switch but can never unfreeze the account ([#3520](https://github.com/0xMiden/protocol/pull/3520)). +- [BREAKING] Moved the internal shared helpers of `miden::protocol::input_note`, `miden::protocol::active_note`, and the note memory-write helpers into private `input_note_internal` and `note_internal` modules ([#3501](https://github.com/0xMiden/protocol/pull/3501)). +- [BREAKING] Refactored `AccountVaultDelta` to track generic assets. `FungibleAssetDelta`, `NonFungibleAssetDelta` and `NonFungibleDeltaAction` were removed ([3485](https://github.com/0xMiden/protocol/pull/3485)). + +### Fixes + ## v0.16.0 (2026-08-06) ### Features @@ -23,6 +37,8 @@ ### Changes +- [BREAKING] Moved the `note_tag` MASM module from `miden::standards::note_tag` to `miden::standards::note::note_tag` ([#3310](https://github.com/0xMiden/protocol/issues/3310)). +- [BREAKING] Moved the `note_creator` account component MASM namespace from `miden::standards::components::wallets::note_creator` to `miden::standards::components::note::note_creator`, and moved the Rust `NoteCreator` type from `account::wallets` to `account::note_creator` ([#3310](https://github.com/0xMiden/protocol/issues/3310)). - [BREAKING] Bind the standard config notes to their target account: `OwnerConfigNote`, `PauseConfigNote`, `RbacConfigNote`, `FaucetPolicyConfigNote`, `AllowlistConfigNote`, `BlocklistConfigNote` and `FaucetMetadataConfigNote` now carry a `NetworkAccountTarget` attachment for that account ([#3433](https://github.com/0xMiden/protocol/issues/3433), [#3455](https://github.com/0xMiden/protocol/pull/3455)). - [BREAKING] BURN notes now store and validate the asset passed to `receive_and_burn`, and target its faucet with a `NetworkAccountTarget` attachment ([#2343](https://github.com/0xMiden/protocol/issues/2343)). - [BREAKING] Moved the generic EVM-bridging helpers from `miden-agglayer` into `miden-standards`: the `agglayer::common` MASM modules now live at `miden::standards::utils`, `miden::standards::assets::conversion` and `miden::standards::interop::eth`. Corresponding Rust types moved to `miden_standards::interop::eth` ([#3423](https://github.com/0xMiden/protocol/pull/3423)). @@ -60,6 +76,7 @@ Added a new `INPUT_NOTE_INDEX_LOOKUP_EVENT` that lets transaction hosts provide - Fixed `faucet::mint` and `faucet::burn` failing when the asset's witness in the input vault had not already been loaded, which happened when minting into a faucet whose vault held other assets, or when burning an asset the transaction had not otherwise accessed; both procedures now request the witness from the host before updating the input vault ([#3409](https://github.com/0xMiden/protocol/pull/3409)). - Enforced the canonical encoding of `Authority` role map values on read: `Authority::try_from_storage` now rejects a procedure-role value word whose reserved felts (`value[1..=3]`) are non-zero, matching the value-slot check and completing the fix started in [#3209](https://github.com/0xMiden/protocol/pull/3209) ([#3415](https://github.com/0xMiden/protocol/pull/3415)). +- Fixed `RoleBasedAccessControl` role administration becoming permanently unmanageable when a role's admin was delegated to a memberless role ([#3476](https://github.com/0xMiden/protocol/pull/3476)). - [BREAKING] The transaction kernel now asserts that asset callbacks return the asset value they received, aborting with `ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT` otherwise; previously, offsetting callback transformations could redistribute value between outputs while passing the epilogue's aggregate conservation check. The kernel commitment changes ([#3442](https://github.com/0xMiden/protocol/issues/3442)). - Restricted indexed input-note asset removal to the native account's context while preserving active-note self-removal. As a consequence, note scripts and transaction scripts can no longer remove input-note assets by index directly, and neither can foreign accounts invoked through FPI; indexed removal must go through a procedure of the native account ([#3445](https://github.com/0xMiden/protocol/issues/3445)). - Fixed the PSWAP note-fill asset to its own payback note ([#3469](https://github.com/0xMiden/protocol/pull/3469)). diff --git a/Cargo.lock b/Cargo.lock index 5df5fab090..606da64970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -794,18 +794,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstyle", "clap_lex", @@ -2098,7 +2098,7 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "alloy-sol-types", "fs-err", @@ -2188,7 +2188,7 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "miden-protocol", "thiserror", @@ -2533,7 +2533,7 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "anyhow", "assert_matches", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "miden-protocol-build-utils" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "fs-err", "miden-assembly", @@ -2620,7 +2620,7 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "anyhow", "assert_matches", @@ -2661,7 +2661,7 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "anyhow", "assert_matches", @@ -2689,7 +2689,7 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "bon", "miden-agglayer", @@ -2703,7 +2703,7 @@ dependencies = [ [[package]] name = "miden-tx-batch" -version = "0.16.0-rc.3" +version = "0.17.0" dependencies = [ "miden-processor", "miden-protocol", @@ -4980,18 +4980,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5cdace176f..ab4d95e074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ homepage = "https://miden.xyz" license = "MIT" repository = "https://github.com/0xMiden/protocol" rust-version = "1.96.1" -version = "0.16.0-rc.3" +version = "0.17.0" [profile.release] codegen-units = 1 @@ -37,14 +37,14 @@ lto = true [workspace.dependencies] # Workspace crates -miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.16.0-rc.3" } -miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.16.0-rc.3" } -miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.16.0-rc.3" } -miden-protocol-build-utils = { default-features = false, path = "crates/miden-protocol-build-utils", version = "0.16.0-rc.3" } -miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.16.0-rc.3" } -miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.16.0-rc.3" } -miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.16.0-rc.3" } -miden-tx-batch = { default-features = false, path = "crates/miden-tx-batch", version = "0.16.0-rc.3" } +miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.17" } +miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.17" } +miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.17" } +miden-protocol-build-utils = { default-features = false, path = "crates/miden-protocol-build-utils", version = "0.17" } +miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.17" } +miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.17" } +miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.17" } +miden-tx-batch = { default-features = false, path = "crates/miden-tx-batch", version = "0.17" } # Miden dependencies miden-assembly = { default-features = false, version = "0.29" } diff --git a/README.md b/README.md index 7302aabc13..4f0d70be62 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ If you want to join the technical discussion or learn more about the project, pl ## Status and features -Miden is currently on release v0.13. This is an early version of the protocol and its components. We expect to keep making changes (including breaking changes) to all components. +Miden is still in an early stage. See the [changelog](CHANGELOG.md) for release notes. +We expect to keep making changes (including breaking changes) to all components. ### Feature highlights @@ -44,12 +45,18 @@ Miden is currently on release v0.13. This is an early version of the protocol an ## Project structure -| Crate | Description | -| ----------------------------------------- | --------------------------------------------------------------------------------------- | -| [miden-protocol](crates/miden-protocol) | Contains core components defining the Miden protocol, including the transaction kernel. | -| [miden-standards](crates/miden-standards) | Contains the code of Miden's standardized smart contracts. | -| [miden-tx](crates/miden-tx) | Contains tools for creating, executing, and proving Miden rollup transactions. | -| [bench-tx](bin/bench-tx) | Contains transaction execution and proving benchmarks. | +| Crate | Description | +| --- | --- | +| [miden-agglayer](crates/miden-agglayer) | AggLayer components for the Miden protocol. | +| [miden-block-prover](crates/miden-block-prover) | Block execution and proving tools. | +| [miden-protocol](crates/miden-protocol) | Core protocol components, including the protocol kernels. | +| [miden-protocol-build-utils](crates/miden-protocol-build-utils) | Build-time MASM helpers. | +| [miden-standards](crates/miden-standards) | Standardized smart contracts. | +| [miden-testing](crates/miden-testing) | Testing tools for Miden transactions, batches, and blocks. | +| [miden-tx](crates/miden-tx) | Transaction creation, execution, and proving tools. | +| [miden-tx-batch](crates/miden-tx-batch) | Transaction batch execution, proving, and verification tools. | +| [bench-note-checker](bin/bench-note-checker) | Note consumability benchmarks for the transaction executor. | +| [bench-transaction](bin/bench-transaction) | Transaction execution and proving benchmarks. | ## Make commands diff --git a/bin/bench-transaction/bench-tx.json b/bin/bench-transaction/bench-tx.json index fb56fd3f3f..e527c79111 100644 --- a/bin/bench-transaction/bench-tx.json +++ b/bin/bench-transaction/bench-tx.json @@ -1,23 +1,23 @@ { "consume single P2ID note with Falcon signing": { "prologue": 3754, - "notes_processing": 2161, + "notes_processing": 2184, "note_execution": { - "0x5cbede6b9f04c8219271e3221e97adaa749f01afe9888a9924d52307ccb22b1c": 2119 + "0x5cbede6b9f04c8219271e3221e97adaa749f01afe9888a9924d52307ccb22b1c": 2142 }, "tx_script_processing": 42, "epilogue": { - "total": 73710, - "auth_procedure": 72598 + "total": 73761, + "auth_procedure": 72646 }, "trace": { - "core_rows": 79711, - "chiplets_rows": 11195, - "range_rows": 20245, + "core_rows": 79785, + "chiplets_rows": 11248, + "range_rows": 20327, "chiplets_shape": { - "hasher_rows": 8168, + "hasher_rows": 8216, "bitwise_rows": 584, - "memory_rows": 2380, + "memory_rows": 2385, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -25,23 +25,23 @@ }, "consume single P2ID note with ECDSA signing": { "prologue": 3754, - "notes_processing": 2161, + "notes_processing": 2184, "note_execution": { - "0x747a592f85aaf459c7a36387a89ef937b2d88941e2b535fe49b20ae9fb1e71a4": 2119 + "0x747a592f85aaf459c7a36387a89ef937b2d88941e2b535fe49b20ae9fb1e71a4": 2142 }, "tx_script_processing": 42, "epilogue": { - "total": 6072, - "auth_procedure": 4960 + "total": 6123, + "auth_procedure": 5008 }, "trace": { - "core_rows": 12073, - "chiplets_rows": 5452, - "range_rows": 1543, + "core_rows": 12147, + "chiplets_rows": 5505, + "range_rows": 1541, "chiplets_shape": { - "hasher_rows": 3816, + "hasher_rows": 3864, "bitwise_rows": 840, - "memory_rows": 733, + "memory_rows": 738, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -49,24 +49,24 @@ }, "consume two P2ID notes with Falcon signing": { "prologue": 5025, - "notes_processing": 4504, + "notes_processing": 4550, "note_execution": { - "0x0ff983e6b5e8a0a04e7375b87d8e811ad51de36406f871e1d9d4e219f0f8abfb": 2334, - "0x60bba08165a03bfcf1febebf16679a0661b4d2672d9341545034f86c5c03a295": 2119 + "0x0ff983e6b5e8a0a04e7375b87d8e811ad51de36406f871e1d9d4e219f0f8abfb": 2357, + "0x60bba08165a03bfcf1febebf16679a0661b4d2672d9341545034f86c5c03a295": 2142 }, "tx_script_processing": 42, "epilogue": { - "total": 73638, - "auth_procedure": 72562 + "total": 73689, + "auth_procedure": 72610 }, "trace": { - "core_rows": 83253, - "chiplets_rows": 13219, - "range_rows": 20293, + "core_rows": 83350, + "chiplets_rows": 13291, + "range_rows": 20321, "chiplets_shape": { - "hasher_rows": 9704, + "hasher_rows": 9768, "bitwise_rows": 928, - "memory_rows": 2524, + "memory_rows": 2532, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -74,24 +74,24 @@ }, "consume two P2ID notes with ECDSA signing": { "prologue": 5025, - "notes_processing": 4504, + "notes_processing": 4550, "note_execution": { - "0x0ff983e6b5e8a0a04e7375b87d8e811ad51de36406f871e1d9d4e219f0f8abfb": 2334, - "0x60bba08165a03bfcf1febebf16679a0661b4d2672d9341545034f86c5c03a295": 2119 + "0x0ff983e6b5e8a0a04e7375b87d8e811ad51de36406f871e1d9d4e219f0f8abfb": 2357, + "0x60bba08165a03bfcf1febebf16679a0661b4d2672d9341545034f86c5c03a295": 2142 }, "tx_script_processing": 42, "epilogue": { - "total": 6000, - "auth_procedure": 4924 + "total": 6051, + "auth_procedure": 4972 }, "trace": { - "core_rows": 15615, - "chiplets_rows": 7476, - "range_rows": 1487, + "core_rows": 15712, + "chiplets_rows": 7548, + "range_rows": 1477, "chiplets_shape": { - "hasher_rows": 5352, + "hasher_rows": 5416, "bitwise_rows": 1184, - "memory_rows": 877, + "memory_rows": 885, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -101,19 +101,19 @@ "prologue": 1881, "notes_processing": 35, "note_execution": {}, - "tx_script_processing": 1844, + "tx_script_processing": 1888, "epilogue": { - "total": 75244, - "auth_procedure": 73205 + "total": 75295, + "auth_procedure": 73253 }, "trace": { - "core_rows": 79048, - "chiplets_rows": 10846, - "range_rows": 20363, + "core_rows": 79143, + "chiplets_rows": 10910, + "range_rows": 20199, "chiplets_shape": { - "hasher_rows": 7944, + "hasher_rows": 8000, "bitwise_rows": 544, - "memory_rows": 2295, + "memory_rows": 2303, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -123,19 +123,19 @@ "prologue": 1881, "notes_processing": 35, "note_execution": {}, - "tx_script_processing": 1844, + "tx_script_processing": 1888, "epilogue": { - "total": 7606, - "auth_procedure": 5567 + "total": 7657, + "auth_procedure": 5615 }, "trace": { - "core_rows": 11410, - "chiplets_rows": 5103, - "range_rows": 1341, + "core_rows": 11505, + "chiplets_rows": 5167, + "range_rows": 1347, "chiplets_shape": { - "hasher_rows": 3592, + "hasher_rows": 3648, "bitwise_rows": 800, - "memory_rows": 648, + "memory_rows": 656, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -143,23 +143,23 @@ }, "consume CLAIM note (L1 to Miden)": { "prologue": 3957, - "notes_processing": 28344, + "notes_processing": 28614, "note_execution": { - "0xbdfc5507d93648b80a38dc73a200a09635e05ad56f11c8faf6d5403cb0d393ee": 28302 + "0x7ba705db8a8a71392d468e19b6d1abf206ca3358bba4dc2bf3dd92b5187856dc": 28572 }, "tx_script_processing": 42, "epilogue": { - "total": 16518, - "auth_procedure": 11600 + "total": 16671, + "auth_procedure": 11750 }, "trace": { - "core_rows": 48905, - "chiplets_rows": 19128, - "range_rows": 3425, + "core_rows": 49328, + "chiplets_rows": 19484, + "range_rows": 3391, "chiplets_shape": { - "hasher_rows": 12312, + "hasher_rows": 12624, "bitwise_rows": 2752, - "memory_rows": 4001, + "memory_rows": 4045, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -167,23 +167,23 @@ }, "consume CLAIM note (L2 to Miden)": { "prologue": 3957, - "notes_processing": 38506, + "notes_processing": 38776, "note_execution": { - "0xee8c28c427c5a7b02c43ee3512550d029ad914f8ccd4770e62abbf7ad18dc631": 38464 + "0x927c8823d42f4ca75bfe60aedf56d3c2dc30ed4ee9db3657a63ade6bdaecc1e0": 38734 }, "tx_script_processing": 42, "epilogue": { - "total": 16518, - "auth_procedure": 11600 + "total": 16671, + "auth_procedure": 11750 }, "trace": { - "core_rows": 59067, - "chiplets_rows": 21878, - "range_rows": 3585, + "core_rows": 59490, + "chiplets_rows": 22234, + "range_rows": 3589, "chiplets_shape": { - "hasher_rows": 13864, + "hasher_rows": 14176, "bitwise_rows": 3008, - "memory_rows": 4943, + "memory_rows": 4987, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -191,23 +191,23 @@ }, "consume B2AGG note (bridge-out)": { "prologue": 4881, - "notes_processing": 116251, + "notes_processing": 118011, "note_execution": { - "0x747971468f66129ecef049ba8c86fa71f87ce9730dcb5c49939446186373d21e": 116209 + "0x2ca2d2a9fbfd77c6817c923b1d797f92e3f31c3aa9b828b2ad869e5de4e3dd9f": 117969 }, "tx_script_processing": 42, "epilogue": { - "total": 26285, - "auth_procedure": 11685 + "total": 26438, + "auth_procedure": 11835 }, "trace": { - "core_rows": 147503, - "chiplets_rows": 69206, - "range_rows": 4685, + "core_rows": 149416, + "chiplets_rows": 70533, + "range_rows": 4665, "chiplets_shape": { - "hasher_rows": 55352, + "hasher_rows": 56440, "bitwise_rows": 3528, - "memory_rows": 10263, + "memory_rows": 10502, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -215,23 +215,23 @@ }, "consume B2AGG note (bridge-out, 2^31 leaves)": { "prologue": 4881, - "notes_processing": 114554, + "notes_processing": 116304, "note_execution": { - "0x747971468f66129ecef049ba8c86fa71f87ce9730dcb5c49939446186373d21e": 114512 + "0x2ca2d2a9fbfd77c6817c923b1d797f92e3f31c3aa9b828b2ad869e5de4e3dd9f": 116262 }, "tx_script_processing": 42, "epilogue": { - "total": 25997, - "auth_procedure": 11685 + "total": 26150, + "auth_procedure": 11835 }, "trace": { - "core_rows": 145518, - "chiplets_rows": 68227, - "range_rows": 4657, + "core_rows": 147421, + "chiplets_rows": 69546, + "range_rows": 4729, "chiplets_shape": { - "hasher_rows": 54504, + "hasher_rows": 55584, "bitwise_rows": 3528, - "memory_rows": 10132, + "memory_rows": 10371, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -239,23 +239,23 @@ }, "consume B2AGG note (bridge-out, 2^31-1 leaves)": { "prologue": 4881, - "notes_processing": 60205, + "notes_processing": 61655, "note_execution": { - "0x747971468f66129ecef049ba8c86fa71f87ce9730dcb5c49939446186373d21e": 60163 + "0x2ca2d2a9fbfd77c6817c923b1d797f92e3f31c3aa9b828b2ad869e5de4e3dd9f": 61613 }, "tx_script_processing": 42, "epilogue": { - "total": 17357, - "auth_procedure": 11685 + "total": 17510, + "auth_procedure": 11835 }, "trace": { - "core_rows": 82529, - "chiplets_rows": 38249, - "range_rows": 3559, + "core_rows": 84132, + "chiplets_rows": 39448, + "range_rows": 3563, "chiplets_shape": { - "hasher_rows": 28456, + "hasher_rows": 29416, "bitwise_rows": 3528, - "memory_rows": 6202, + "memory_rows": 6441, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -263,23 +263,23 @@ }, "consume P2ID note (network account)": { "prologue": 3719, - "notes_processing": 2161, + "notes_processing": 2184, "note_execution": { - "0x9ffdc9ed78028f7eda93f7c44287cb1f719d69069d2d8669025fc760c7da567a": 2119 + "0x9ffdc9ed78028f7eda93f7c44287cb1f719d69069d2d8669025fc760c7da567a": 2142 }, "tx_script_processing": 42, "epilogue": { - "total": 12541, - "auth_procedure": 9367 + "total": 12698, + "auth_procedure": 9521 }, "trace": { - "core_rows": 18507, - "chiplets_rows": 8127, - "range_rows": 1467, + "core_rows": 18687, + "chiplets_rows": 8300, + "range_rows": 1459, "chiplets_shape": { - "hasher_rows": 6144, + "hasher_rows": 6312, "bitwise_rows": 824, - "memory_rows": 1096, + "memory_rows": 1101, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -287,23 +287,23 @@ }, "consume P2ID note (16 assets, network account)": { "prologue": 12314, - "notes_processing": 29805, + "notes_processing": 30173, "note_execution": { - "0x27098731a1d1d6d12551117a4e3c77fa946491a977105e8d33f70a367fbceda7": 29763 + "0x27098731a1d1d6d12551117a4e3c77fa946491a977105e8d33f70a367fbceda7": 30131 }, "tx_script_processing": 42, "epilogue": { - "total": 15020, - "auth_procedure": 9686 + "total": 15177, + "auth_procedure": 9840 }, "trace": { - "core_rows": 57225, - "chiplets_rows": 32864, - "range_rows": 3635, + "core_rows": 57750, + "chiplets_rows": 33266, + "range_rows": 3631, "chiplets_shape": { - "hasher_rows": 24416, + "hasher_rows": 24768, "bitwise_rows": 5504, - "memory_rows": 2881, + "memory_rows": 2931, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -311,23 +311,23 @@ }, "consume P2IDE note (claim, network account)": { "prologue": 3719, - "notes_processing": 2286, + "notes_processing": 2309, "note_execution": { - "0x05847ac68b2b527d6932ebc07325c6e4346cacba2c8eca17334cabaed180fe50": 2244 + "0x05847ac68b2b527d6932ebc07325c6e4346cacba2c8eca17334cabaed180fe50": 2267 }, "tx_script_processing": 42, "epilogue": { - "total": 12541, - "auth_procedure": 9367 + "total": 12698, + "auth_procedure": 9521 }, "trace": { - "core_rows": 18632, - "chiplets_rows": 8155, - "range_rows": 1497, + "core_rows": 18812, + "chiplets_rows": 8336, + "range_rows": 1495, "chiplets_shape": { - "hasher_rows": 6168, + "hasher_rows": 6344, "bitwise_rows": 824, - "memory_rows": 1100, + "memory_rows": 1105, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -335,23 +335,23 @@ }, "consume P2IDE note (claim, 16 assets, network account)": { "prologue": 12314, - "notes_processing": 29930, + "notes_processing": 30298, "note_execution": { - "0x3cb8d89cd690317eadc9777eb377d0bc75e44c5f3b76b4484a3928c9fc0bb16f": 29888 + "0x3cb8d89cd690317eadc9777eb377d0bc75e44c5f3b76b4484a3928c9fc0bb16f": 30256 }, "tx_script_processing": 42, "epilogue": { - "total": 15020, - "auth_procedure": 9686 + "total": 15177, + "auth_procedure": 9840 }, "trace": { - "core_rows": 57350, - "chiplets_rows": 32900, - "range_rows": 3645, + "core_rows": 57875, + "chiplets_rows": 33302, + "range_rows": 3617, "chiplets_shape": { - "hasher_rows": 24448, + "hasher_rows": 24800, "bitwise_rows": 5504, - "memory_rows": 2885, + "memory_rows": 2935, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -359,23 +359,23 @@ }, "consume P2IDE note (reclaim, network account)": { "prologue": 3822, - "notes_processing": 2338, + "notes_processing": 2361, "note_execution": { - "0xbb2f39f6cb6f735050be9911032bebb082c4477c01920a05bdf81449a2a2531f": 2296 + "0xbb2f39f6cb6f735050be9911032bebb082c4477c01920a05bdf81449a2a2531f": 2319 }, "tx_script_processing": 42, "epilogue": { - "total": 12541, - "auth_procedure": 9367 + "total": 12698, + "auth_procedure": 9521 }, "trace": { - "core_rows": 18787, - "chiplets_rows": 8186, - "range_rows": 1483, + "core_rows": 18967, + "chiplets_rows": 8367, + "range_rows": 1539, "chiplets_shape": { - "hasher_rows": 6192, + "hasher_rows": 6368, "bitwise_rows": 824, - "memory_rows": 1107, + "memory_rows": 1112, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -383,23 +383,23 @@ }, "consume SWAP note (public payback, network account)": { "prologue": 3719, - "notes_processing": 4511, + "notes_processing": 4578, "note_execution": { - "0xd642acc2180f1ea6f6fbd1bce45b076d517ed595c4f3c3bd9bce22ec66e9e5c7": 4469 + "0xd642acc2180f1ea6f6fbd1bce45b076d517ed595c4f3c3bd9bce22ec66e9e5c7": 4536 }, "tx_script_processing": 42, "epilogue": { - "total": 14232, - "auth_procedure": 9907 + "total": 14389, + "auth_procedure": 10061 }, "trace": { - "core_rows": 22548, - "chiplets_rows": 10187, - "range_rows": 1753, + "core_rows": 22772, + "chiplets_rows": 10390, + "range_rows": 1759, "chiplets_shape": { - "hasher_rows": 7688, + "hasher_rows": 7880, "bitwise_rows": 1192, - "memory_rows": 1244, + "memory_rows": 1255, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -407,23 +407,23 @@ }, "consume SWAP note (private payback, network account)": { "prologue": 3719, - "notes_processing": 3997, + "notes_processing": 4064, "note_execution": { - "0x6d44d87b75165cdc5794a3dbc5cdba77277e5241ccd2e332e9d78b47f58a29af": 3955 + "0x6d44d87b75165cdc5794a3dbc5cdba77277e5241ccd2e332e9d78b47f58a29af": 4022 }, "tx_script_processing": 42, "epilogue": { - "total": 14232, - "auth_procedure": 9907 + "total": 14389, + "auth_procedure": 10061 }, "trace": { - "core_rows": 22034, - "chiplets_rows": 10066, - "range_rows": 1761, + "core_rows": 22258, + "chiplets_rows": 10269, + "range_rows": 1737, "chiplets_shape": { - "hasher_rows": 7584, + "hasher_rows": 7776, "bitwise_rows": 1192, - "memory_rows": 1227, + "memory_rows": 1238, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -431,23 +431,23 @@ }, "consume PSWAP note (full fill, network account)": { "prologue": 3719, - "notes_processing": 7033, + "notes_processing": 7100, "note_execution": { - "0x8258ed08b1106ffafca1fa325d7588834fea990bd9e6c4600b871ef6e2708192": 6991 + "0x8258ed08b1106ffafca1fa325d7588834fea990bd9e6c4600b871ef6e2708192": 7058 }, "tx_script_processing": 42, "epilogue": { - "total": 14240, - "auth_procedure": 9907 + "total": 14397, + "auth_procedure": 10061 }, "trace": { - "core_rows": 25078, - "chiplets_rows": 10875, - "range_rows": 1795, + "core_rows": 25302, + "chiplets_rows": 11078, + "range_rows": 1751, "chiplets_shape": { - "hasher_rows": 8128, + "hasher_rows": 8320, "bitwise_rows": 1264, - "memory_rows": 1420, + "memory_rows": 1431, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -455,23 +455,23 @@ }, "consume PSWAP note (partial fill, network account)": { "prologue": 3719, - "notes_processing": 9574, + "notes_processing": 9662, "note_execution": { - "0x8258ed08b1106ffafca1fa325d7588834fea990bd9e6c4600b871ef6e2708192": 9532 + "0x8258ed08b1106ffafca1fa325d7588834fea990bd9e6c4600b871ef6e2708192": 9620 }, "tx_script_processing": 42, "epilogue": { - "total": 15654, - "auth_procedure": 10130 + "total": 15811, + "auth_procedure": 10284 }, "trace": { - "core_rows": 29033, - "chiplets_rows": 12580, - "range_rows": 1991, + "core_rows": 29278, + "chiplets_rows": 12802, + "range_rows": 2001, "chiplets_shape": { - "hasher_rows": 9296, + "hasher_rows": 9504, "bitwise_rows": 1640, - "memory_rows": 1581, + "memory_rows": 1595, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -479,23 +479,23 @@ }, "consume MINT note (fungible faucet, network account)": { "prologue": 5447, - "notes_processing": 7263, + "notes_processing": 7585, "note_execution": { - "0x61eb35dd4fa928f4b4ef9be8a1c8ef62b6f904483a96904854cbd23a95a58456": 7221 + "0x7cb085b49226958234c26ea8f79155940e3a985b932c3ce66b76e9fd1420e456": 7543 }, "tx_script_processing": 42, "epilogue": { - "total": 19031, - "auth_procedure": 9662 + "total": 19188, + "auth_procedure": 9816 }, "trace": { - "core_rows": 31827, - "chiplets_rows": 12355, - "range_rows": 2815, + "core_rows": 32306, + "chiplets_rows": 12689, + "range_rows": 2797, "chiplets_shape": { - "hasher_rows": 9120, + "hasher_rows": 9408, "bitwise_rows": 1144, - "memory_rows": 2028, + "memory_rows": 2074, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -503,23 +503,23 @@ }, "consume MINT note (non-fungible faucet, network account)": { "prologue": 5496, - "notes_processing": 9800, + "notes_processing": 10133, "note_execution": { - "0xf17cb88ebe534db086dc129df0773363e7ecc8c492acb4a92b000ecb1605bfcd": 9758 + "0xa488b4dd26ce659555153165a2b5dd830383a226748abc325f19cc39e914ad8c": 10091 }, "tx_script_processing": 42, "epilogue": { - "total": 19324, - "auth_procedure": 9671 + "total": 19481, + "auth_procedure": 9825 }, "trace": { - "core_rows": 34706, - "chiplets_rows": 13480, - "range_rows": 2945, + "core_rows": 35196, + "chiplets_rows": 13817, + "range_rows": 2943, "chiplets_shape": { - "hasher_rows": 10128, + "hasher_rows": 10416, "bitwise_rows": 1144, - "memory_rows": 2145, + "memory_rows": 2194, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -527,23 +527,23 @@ }, "consume BURN note (network account)": { "prologue": 6046, - "notes_processing": 4584, + "notes_processing": 4758, "note_execution": { - "0x90c1f0a2c27744efd107c653e6dba134c6713257427980e503580fa4931dc935": 4542 + "0x652872e4aa6a0681c4fa88e107ca15726c52b521e30ab84049a8eaf3e4422026": 4716 }, "tx_script_processing": 42, "epilogue": { - "total": 17773, - "auth_procedure": 9441 + "total": 17930, + "auth_procedure": 9595 }, "trace": { - "core_rows": 28489, - "chiplets_rows": 11174, - "range_rows": 2559, + "core_rows": 28820, + "chiplets_rows": 11433, + "range_rows": 2609, "chiplets_shape": { - "hasher_rows": 8392, + "hasher_rows": 8624, "bitwise_rows": 856, - "memory_rows": 1863, + "memory_rows": 1890, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -551,23 +551,23 @@ }, "consume FAUCET_POLICY_CONFIG note (network account)": { "prologue": 5400, - "notes_processing": 5301, + "notes_processing": 5407, "note_execution": { - "0xc7d1d6d6dc6a738a3115fb8aec12c2fb9767356f2c67fd08a7163d7aadc1e7e0": 5259 + "0xc7d1d6d6dc6a738a3115fb8aec12c2fb9767356f2c67fd08a7163d7aadc1e7e0": 5365 }, "tx_script_processing": 42, "epilogue": { - "total": 17626, - "auth_procedure": 9432 + "total": 17783, + "auth_procedure": 9586 }, "trace": { - "core_rows": 28413, - "chiplets_rows": 10164, - "range_rows": 2589, + "core_rows": 28676, + "chiplets_rows": 10392, + "range_rows": 2577, "chiplets_shape": { - "hasher_rows": 7768, + "hasher_rows": 7976, "bitwise_rows": 560, - "memory_rows": 1773, + "memory_rows": 1793, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -575,23 +575,47 @@ }, "consume FAUCET_METADATA_CONFIG note (network account)": { "prologue": 4824, - "notes_processing": 6043, + "notes_processing": 6294, "note_execution": { - "0x8ea187c948675fe48a1ef5dcf25edeb636f501de68daffcad465c87fc418d30b": 6001 + "0x8ea187c948675fe48a1ef5dcf25edeb636f501de68daffcad465c87fc418d30b": 6252 }, "tx_script_processing": 42, "epilogue": { - "total": 16281, - "auth_procedure": 9351 + "total": 16438, + "auth_procedure": 9505 }, "trace": { - "core_rows": 27234, - "chiplets_rows": 9828, - "range_rows": 2407, + "core_rows": 27642, + "chiplets_rows": 10162, + "range_rows": 2403, "chiplets_shape": { - "hasher_rows": 7400, + "hasher_rows": 7696, "bitwise_rows": 568, - "memory_rows": 1797, + "memory_rows": 1835, + "kernel_rom_rows": 62, + "ace_rows": 0 + } + } + }, + "consume MIN_BURN_AMOUNT_CONFIG note (network account)": { + "prologue": 5457, + "notes_processing": 2514, + "note_execution": { + "0xd1e581d8109fee2c5b6e8c4d5600506cdf6128e7d40bb27236f6b383e8208898": 2472 + }, + "tx_script_processing": 42, + "epilogue": { + "total": 17930, + "auth_procedure": 9595 + }, + "trace": { + "core_rows": 25987, + "chiplets_rows": 9641, + "range_rows": 2471, + "chiplets_shape": { + "hasher_rows": 7304, + "bitwise_rows": 560, + "memory_rows": 1714, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -599,23 +623,23 @@ }, "consume ALLOWLIST_CONFIG note (network account)": { "prologue": 5457, - "notes_processing": 3029, + "notes_processing": 3088, "note_execution": { - "0x96371aba3cc701dee73e96fbbc51f87d716641a06ccc6e455168e7a4ae443e94": 2987 + "0x96371aba3cc701dee73e96fbbc51f87d716641a06ccc6e455168e7a4ae443e94": 3046 }, "tx_script_processing": 42, "epilogue": { - "total": 17949, - "auth_procedure": 9441 + "total": 18106, + "auth_procedure": 9595 }, "trace": { - "core_rows": 26521, - "chiplets_rows": 10074, - "range_rows": 2489, + "core_rows": 26737, + "chiplets_rows": 10269, + "range_rows": 2499, "chiplets_shape": { - "hasher_rows": 7672, + "hasher_rows": 7856, "bitwise_rows": 560, - "memory_rows": 1779, + "memory_rows": 1790, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -623,23 +647,23 @@ }, "consume BLOCKLIST_CONFIG note (network account)": { "prologue": 5457, - "notes_processing": 3029, + "notes_processing": 3088, "note_execution": { - "0xe13a4c39807ed4265ac1abb9de8509a3f9a23bfca58de66624560c04c164ccc4": 2987 + "0xe13a4c39807ed4265ac1abb9de8509a3f9a23bfca58de66624560c04c164ccc4": 3046 }, "tx_script_processing": 42, "epilogue": { - "total": 17949, - "auth_procedure": 9441 + "total": 18106, + "auth_procedure": 9595 }, "trace": { - "core_rows": 26521, - "chiplets_rows": 10074, - "range_rows": 2479, + "core_rows": 26737, + "chiplets_rows": 10269, + "range_rows": 2481, "chiplets_shape": { - "hasher_rows": 7672, + "hasher_rows": 7856, "bitwise_rows": 560, - "memory_rows": 1779, + "memory_rows": 1790, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -647,23 +671,23 @@ }, "consume PAUSE_CONFIG note (network account)": { "prologue": 3327, - "notes_processing": 2470, + "notes_processing": 2529, "note_execution": { - "0x76891cc44cdb15d05251ecfe69b337f1281360a49b8b302640565382d9307143": 2428 + "0x76891cc44cdb15d05251ecfe69b337f1281360a49b8b302640565382d9307143": 2487 }, "tx_script_processing": 42, "epilogue": { - "total": 12588, - "auth_procedure": 9126 + "total": 12745, + "auth_procedure": 9280 }, "trace": { - "core_rows": 18471, - "chiplets_rows": 7250, - "range_rows": 1519, + "core_rows": 18687, + "chiplets_rows": 7453, + "range_rows": 1529, "chiplets_shape": { - "hasher_rows": 5488, + "hasher_rows": 5680, "bitwise_rows": 560, - "memory_rows": 1139, + "memory_rows": 1150, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -671,23 +695,23 @@ }, "consume OWNER_CONFIG note (network account)": { "prologue": 3186, - "notes_processing": 2499, + "notes_processing": 2558, "note_execution": { - "0xff8dc68047d1df36aa4b3b1130779084c9216aac8f9c8dfcdfbd1f82cbe8ec40": 2457 + "0xff8dc68047d1df36aa4b3b1130779084c9216aac8f9c8dfcdfbd1f82cbe8ec40": 2516 }, "tx_script_processing": 42, "epilogue": { - "total": 12294, - "auth_procedure": 9108 + "total": 12451, + "auth_procedure": 9262 }, "trace": { - "core_rows": 18065, - "chiplets_rows": 7146, - "range_rows": 1531, + "core_rows": 18281, + "chiplets_rows": 7341, + "range_rows": 1529, "chiplets_shape": { - "hasher_rows": 5400, + "hasher_rows": 5584, "bitwise_rows": 576, - "memory_rows": 1107, + "memory_rows": 1118, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -695,23 +719,23 @@ }, "consume RBAC_CONFIG note (network account)": { "prologue": 3365, - "notes_processing": 4761, + "notes_processing": 5422, "note_execution": { - "0xe12e2ed4219660c99f775b11d3ec2aabf69352a7d43fe25b94c63f9afe55199b": 4719 + "0xa36b3c9088c716fd7295a35f01a53acab54c7ea5d85feefbecb1f9f4b8699307": 5380 }, "tx_script_processing": 42, "epilogue": { - "total": 13115, - "auth_procedure": 9135 + "total": 13272, + "auth_procedure": 9289 }, "trace": { - "core_rows": 21327, - "chiplets_rows": 9286, - "range_rows": 1697, + "core_rows": 22145, + "chiplets_rows": 9797, + "range_rows": 1709, "chiplets_shape": { - "hasher_rows": 7256, + "hasher_rows": 7720, "bitwise_rows": 576, - "memory_rows": 1391, + "memory_rows": 1438, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -719,23 +743,23 @@ }, "consume NETWORK_ACCOUNT_CONFIG note (network account)": { "prologue": 3270, - "notes_processing": 3002, + "notes_processing": 3061, "note_execution": { - "0x963ba76870db04bbab4fd2fa4ed3939aa38dd709634296b681beac6723728d1e": 2960 + "0x963ba76870db04bbab4fd2fa4ed3939aa38dd709634296b681beac6723728d1e": 3019 }, "tx_script_processing": 42, "epilogue": { - "total": 12607, - "auth_procedure": 9117 + "total": 12764, + "auth_procedure": 9271 }, "trace": { - "core_rows": 18965, - "chiplets_rows": 7796, - "range_rows": 1583, + "core_rows": 19181, + "chiplets_rows": 7999, + "range_rows": 1595, "chiplets_shape": { - "hasher_rows": 5984, + "hasher_rows": 6176, "bitwise_rows": 560, - "memory_rows": 1189, + "memory_rows": 1200, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -743,23 +767,23 @@ }, "consume CONSTANT_FEE_POLICY_CONFIG note (network account)": { "prologue": 3317, - "notes_processing": 3687, + "notes_processing": 3782, "note_execution": { - "0xba847903cc257dbbe09852d5269201716661c41ad25494c8252ff15b423cc687": 3645 + "0xba847903cc257dbbe09852d5269201716661c41ad25494c8252ff15b423cc687": 3740 }, "tx_script_processing": 42, "epilogue": { - "total": 12754, - "auth_procedure": 9126 + "total": 12911, + "auth_procedure": 9280 }, "trace": { - "core_rows": 19844, - "chiplets_rows": 8050, - "range_rows": 1615, + "core_rows": 20096, + "chiplets_rows": 8283, + "range_rows": 1579, "chiplets_shape": { - "hasher_rows": 6168, + "hasher_rows": 6384, "bitwise_rows": 560, - "memory_rows": 1259, + "memory_rows": 1276, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -774,17 +798,17 @@ }, "tx_script_processing": 42, "epilogue": { - "total": 15249, - "auth_procedure": 12219 + "total": 15455, + "auth_procedure": 12422 }, "trace": { - "core_rows": 21301, - "chiplets_rows": 9142, - "range_rows": 1543, + "core_rows": 21507, + "chiplets_rows": 9347, + "range_rows": 1593, "chiplets_shape": { - "hasher_rows": 6976, + "hasher_rows": 7176, "bitwise_rows": 888, - "memory_rows": 1215, + "memory_rows": 1220, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -792,23 +816,23 @@ }, "consume FEE_SPONSORSHIP note (reclaim)": { "prologue": 3605, - "notes_processing": 2920, + "notes_processing": 2943, "note_execution": { - "0x10ecf93babe366a86b031e4bc407ec2e05f08b8be402539782e54f239aca3222": 2878 + "0x10ecf93babe366a86b031e4bc407ec2e05f08b8be402539782e54f239aca3222": 2901 }, "tx_script_processing": 42, "epilogue": { - "total": 10374, - "auth_procedure": 8159 + "total": 10461, + "auth_procedure": 8243 }, "trace": { - "core_rows": 16985, - "chiplets_rows": 7640, - "range_rows": 1609, + "core_rows": 17095, + "chiplets_rows": 7725, + "range_rows": 1583, "chiplets_shape": { - "hasher_rows": 5568, + "hasher_rows": 5648, "bitwise_rows": 1144, - "memory_rows": 865, + "memory_rows": 870, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -816,23 +840,23 @@ }, "consume CLAIM note (L1 to Miden, with fee payment)": { "prologue": 3957, - "notes_processing": 28344, + "notes_processing": 28614, "note_execution": { - "0xbdfc5507d93648b80a38dc73a200a09635e05ad56f11c8faf6d5403cb0d393ee": 28302 + "0x7ba705db8a8a71392d468e19b6d1abf206ca3358bba4dc2bf3dd92b5187856dc": 28572 }, "tx_script_processing": 42, "epilogue": { - "total": 20610, - "auth_procedure": 14310 + "total": 20799, + "auth_procedure": 14496 }, "trace": { - "core_rows": 52997, - "chiplets_rows": 21208, - "range_rows": 3529, + "core_rows": 53456, + "chiplets_rows": 21604, + "range_rows": 3533, "chiplets_shape": { - "hasher_rows": 13904, + "hasher_rows": 14256, "bitwise_rows": 3088, - "memory_rows": 4153, + "memory_rows": 4197, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -840,23 +864,23 @@ }, "consume CLAIM note (L2 to Miden, with fee payment)": { "prologue": 3957, - "notes_processing": 38506, + "notes_processing": 38776, "note_execution": { - "0xee8c28c427c5a7b02c43ee3512550d029ad914f8ccd4770e62abbf7ad18dc631": 38464 + "0x927c8823d42f4ca75bfe60aedf56d3c2dc30ed4ee9db3657a63ade6bdaecc1e0": 38734 }, "tx_script_processing": 42, "epilogue": { - "total": 20610, - "auth_procedure": 14310 + "total": 20799, + "auth_procedure": 14496 }, "trace": { - "core_rows": 63159, - "chiplets_rows": 23958, - "range_rows": 3737, + "core_rows": 63618, + "chiplets_rows": 24354, + "range_rows": 3725, "chiplets_shape": { - "hasher_rows": 15456, + "hasher_rows": 15808, "bitwise_rows": 3344, - "memory_rows": 5095, + "memory_rows": 5139, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -864,23 +888,23 @@ }, "consume B2AGG note (bridge-out, with fee payment)": { "prologue": 4881, - "notes_processing": 116251, + "notes_processing": 118011, "note_execution": { - "0x747971468f66129ecef049ba8c86fa71f87ce9730dcb5c49939446186373d21e": 116209 + "0x2ca2d2a9fbfd77c6817c923b1d797f92e3f31c3aa9b828b2ad869e5de4e3dd9f": 117969 }, "tx_script_processing": 42, "epilogue": { - "total": 30377, - "auth_procedure": 14395 + "total": 30566, + "auth_procedure": 14581 }, "trace": { - "core_rows": 151595, - "chiplets_rows": 71286, - "range_rows": 4753, + "core_rows": 153544, + "chiplets_rows": 72653, + "range_rows": 4767, "chiplets_shape": { - "hasher_rows": 56944, + "hasher_rows": 58072, "bitwise_rows": 3864, - "memory_rows": 10415, + "memory_rows": 10654, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -888,23 +912,23 @@ }, "consume B2AGG note (bridge-out, 2^31-1 leaves, with fee payment)": { "prologue": 4881, - "notes_processing": 60205, + "notes_processing": 61655, "note_execution": { - "0x747971468f66129ecef049ba8c86fa71f87ce9730dcb5c49939446186373d21e": 60163 + "0x2ca2d2a9fbfd77c6817c923b1d797f92e3f31c3aa9b828b2ad869e5de4e3dd9f": 61613 }, "tx_script_processing": 42, "epilogue": { - "total": 21449, - "auth_procedure": 14395 + "total": 21638, + "auth_procedure": 14581 }, "trace": { - "core_rows": 86621, - "chiplets_rows": 40329, - "range_rows": 3737, + "core_rows": 88260, + "chiplets_rows": 41568, + "range_rows": 3713, "chiplets_shape": { - "hasher_rows": 30048, + "hasher_rows": 31048, "bitwise_rows": 3864, - "memory_rows": 6354, + "memory_rows": 6593, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -912,23 +936,23 @@ }, "consume CONFIG_AGG_BRIDGE note (with fee payment)": { "prologue": 4265, - "notes_processing": 13388, + "notes_processing": 13724, "note_execution": { - "0x51eb958cc6ce003f866bed3f618c3a530dcb515bf791949db7707b662797a8a7": 13346 + "0x51eb958cc6ce003f866bed3f618c3a530dcb515bf791949db7707b662797a8a7": 13682 }, "tx_script_processing": 42, "epilogue": { - "total": 15996, - "auth_procedure": 9270 + "total": 16153, + "auth_procedure": 9424 }, "trace": { - "core_rows": 33735, - "chiplets_rows": 14581, - "range_rows": 2465, + "core_rows": 34228, + "chiplets_rows": 14978, + "range_rows": 2415, "chiplets_shape": { - "hasher_rows": 11584, + "hasher_rows": 11928, "bitwise_rows": 616, - "memory_rows": 2318, + "memory_rows": 2371, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -936,23 +960,23 @@ }, "consume DEREGISTER_AGG_FAUCET note (with fee payment)": { "prologue": 4324, - "notes_processing": 12687, + "notes_processing": 12969, "note_execution": { - "0xdc8ae96d95b6c8bba383e0829a1de57827736349bf2a65d29b98a0b8ce8de44f": 12645 + "0xdc8ae96d95b6c8bba383e0829a1de57827736349bf2a65d29b98a0b8ce8de44f": 12927 }, "tx_script_processing": 42, "epilogue": { - "total": 15996, - "auth_procedure": 9270 + "total": 16153, + "auth_procedure": 9424 }, "trace": { - "core_rows": 33093, - "chiplets_rows": 14272, - "range_rows": 2357, + "core_rows": 33532, + "chiplets_rows": 14628, + "range_rows": 2313, "chiplets_shape": { - "hasher_rows": 11400, + "hasher_rows": 11712, "bitwise_rows": 608, - "memory_rows": 2201, + "memory_rows": 2245, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -960,23 +984,23 @@ }, "consume UPDATE_GER note (with fee payment)": { "prologue": 4265, - "notes_processing": 4391, + "notes_processing": 4504, "note_execution": { - "0x83a2de49840c9bf985e0b919a89e3ce2e632036932a345ad4faf40bccf37b61f": 4349 + "0x83a2de49840c9bf985e0b919a89e3ce2e632036932a345ad4faf40bccf37b61f": 4462 }, "tx_script_processing": 42, "epilogue": { - "total": 15196, - "auth_procedure": 9270 + "total": 15353, + "auth_procedure": 9424 }, "trace": { - "core_rows": 23938, - "chiplets_rows": 9597, - "range_rows": 2057, + "core_rows": 24208, + "chiplets_rows": 9841, + "range_rows": 2053, "chiplets_shape": { - "hasher_rows": 7360, + "hasher_rows": 7584, "bitwise_rows": 560, - "memory_rows": 1614, + "memory_rows": 1634, "kernel_rom_rows": 62, "ace_rows": 0 } @@ -984,23 +1008,23 @@ }, "consume REMOVE_GER note (with fee payment)": { "prologue": 4324, - "notes_processing": 5409, + "notes_processing": 5568, "note_execution": { - "0xffa3eac6e83fdd664e9dca686e96d17b331a77437f1a5cbb3521b261fab833b9": 5367 + "0xffa3eac6e83fdd664e9dca686e96d17b331a77437f1a5cbb3521b261fab833b9": 5526 }, "tx_script_processing": 42, "epilogue": { - "total": 15232, - "auth_procedure": 9270 + "total": 15389, + "auth_procedure": 9424 }, "trace": { - "core_rows": 25051, - "chiplets_rows": 9887, - "range_rows": 2061, + "core_rows": 25367, + "chiplets_rows": 10161, + "range_rows": 2077, "chiplets_shape": { - "hasher_rows": 7568, + "hasher_rows": 7816, "bitwise_rows": 560, - "memory_rows": 1696, + "memory_rows": 1722, "kernel_rom_rows": 62, "ace_rows": 0 } diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index 5dc0d0ed70..765ab5d2a5 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -272,7 +272,6 @@ fn tx_create_single_p2id_note_with_auth(auth_scheme: AuthScheme) -> Result Result [tag, note_type, RECIPIENT, pad(16)] - call.note_creator::create_note + call.basic_wallet::create_note # => [note_idx, pad(21)] # move the asset to the note diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm index 7ebcbeaac6..3f09c08d27 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm @@ -5,8 +5,8 @@ use miden::protocol::note use {NOTE_TYPE_PUBLIC} from miden::protocol::note use miden::protocol::output_note use miden::standards::assets::fungible_asset -use miden::standards::note_tag -use {DEFAULT_TAG} from miden::standards::note_tag +use miden::standards::note::note_tag +use {DEFAULT_TAG} from miden::standards::note::note_tag use miden::standards::notes::p2id use miden::standards::attachments::network_account_target use {ALWAYS} from miden::standards::note::execution_hint diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index fd7486d1ab..21fd2c5946 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -6,7 +6,7 @@ use miden::protocol::note use miden::standards::assets::fungible_asset use miden::standards::data_structures::double_word_array use miden::standards::attachments::network_account_target -use {DEFAULT_TAG} from miden::standards::note_tag +use {DEFAULT_TAG} from miden::standards::note::note_tag use {ALWAYS} from miden::standards::note::execution_hint use {MemoryAddress} from miden::protocol::types use miden::protocol::output_note diff --git a/crates/miden-agglayer/src/costs/table.rs b/crates/miden-agglayer/src/costs/table.rs index 9c56faeadc..2b76b74377 100644 --- a/crates/miden-agglayer/src/costs/table.rs +++ b/crates/miden-agglayer/src/costs/table.rs @@ -2,20 +2,20 @@ // Values are maxima across the benchmarked paths; see `miden_standards::note::costs` for the // caveats on what they do and do not cover. -/// Cycles of consuming a CLAIM note: L1 origin 52953, L2 origin 63115 (maximum). -pub const CLAIM_CONSUMPTION_CYCLES: u32 = 63115; +/// Cycles of consuming a CLAIM note: L1 origin 53412, L2 origin 63574 (maximum). +pub const CLAIM_CONSUMPTION_CYCLES: u32 = 63574; -/// Cycles of consuming a B2AGG note: empty frontier 151551 (maximum), 2^31-1 leaves 86577. -pub const B2AGG_CONSUMPTION_CYCLES: u32 = 151551; +/// Cycles of consuming a B2AGG note: empty frontier 153500 (maximum), 2^31-1 leaves 88216. +pub const B2AGG_CONSUMPTION_CYCLES: u32 = 153500; /// Cycles of consuming a CONFIG_AGG_BRIDGE note (single benchmarked path). -pub const CONFIG_AGG_BRIDGE_CONSUMPTION_CYCLES: u32 = 33691; +pub const CONFIG_AGG_BRIDGE_CONSUMPTION_CYCLES: u32 = 34184; /// Cycles of consuming a DEREGISTER_AGG_FAUCET note (single benchmarked path). -pub const DEREGISTER_AGG_FAUCET_CONSUMPTION_CYCLES: u32 = 33049; +pub const DEREGISTER_AGG_FAUCET_CONSUMPTION_CYCLES: u32 = 33488; /// Cycles of consuming an UPDATE_GER note (single benchmarked path). -pub const UPDATE_GER_CONSUMPTION_CYCLES: u32 = 23894; +pub const UPDATE_GER_CONSUMPTION_CYCLES: u32 = 24164; /// Cycles of consuming a REMOVE_GER note (single benchmarked path). -pub const REMOVE_GER_CONSUMPTION_CYCLES: u32 = 25007; +pub const REMOVE_GER_CONSUMPTION_CYCLES: u32 = 25323; diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm index a2db9e8e57..17cc60ead0 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm @@ -644,24 +644,24 @@ end #! added. #! - the vault already contains the same non-fungible asset. pub proc add_asset_to_vault - swapw dupw.1 - # => [ASSET_ID, ASSET_VALUE, ASSET_ID] + # retain the original asset while the callback validates a copy + dupw.1 dupw.1 + # => [ASSET_ID, ASSET_VALUE, ASSET_ID, ASSET_VALUE] exec.callbacks::on_before_asset_added_to_account - swapw - # => [ASSET_ID, PROCESSED_ASSET_VALUE] + # => [ASSET_ID, ASSET_VALUE] # duplicate the asset ID for the later event and delta update swapw dupw.1 - # => [ASSET_ID, PROCESSED_ASSET_VALUE, ASSET_ID] + # => [ASSET_ID, ASSET_VALUE, ASSET_ID] # push the account vault root ptr exec.memory::get_account_vault_root_ptr movdn.8 - # => [ASSET_ID, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_ID] + # => [ASSET_ID, ASSET_VALUE, account_vault_root_ptr, ASSET_ID] # emit event to signal that an asset is going to be added to the account vault emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT - # => [ASSET_ID, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_ID] + # => [ASSET_ID, ASSET_VALUE, account_vault_root_ptr, ASSET_ID] # add the asset to the account vault exec.asset_vault::add_asset diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/callbacks.masm index 7d7c90520d..511f1d947f 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/callbacks.masm @@ -7,15 +7,9 @@ use miden::core::word # CONSTANTS # ================================================================================================== -const ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT = - "asset callback output value must match its input value" - # The index of the local memory slot that contains the procedure root of the callback. const CALLBACK_PROC_ROOT_LOC = 0 -# The index of the local memory slot that contains the original asset value passed to the callback. -const CALLBACK_ASSET_VALUE_LOC = 4 - # The name of the storage slot where the procedure root for the on_before_asset_added_to_account callback # is stored. pub const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT = @@ -42,17 +36,11 @@ pub const ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_ROOT_SLOT = #! - If the callback storage slot contains the empty word. #! #! Inputs: [ASSET_ID, ASSET_VALUE] -#! Outputs: [PROCESSED_ASSET_VALUE] +#! Outputs: [] #! #! Where: #! - ASSET_ID is the asset ID of the asset being added. #! - ASSET_VALUE is the value of the asset being added. -#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original -#! ASSET_VALUE if callbacks are disabled. The callback is required to return the asset -#! value it received, so this is always equal to ASSET_VALUE. -#! -#! Panics if: -#! - the callback returns an asset value different from the one it received. pub proc on_before_asset_added_to_account # derive the callback flag from the asset metadata, carried in the asset ID exec.asset::id_to_has_callbacks @@ -65,13 +53,12 @@ pub proc on_before_asset_added_to_account push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] exec.invoke_callback - # => [PROCESSED_ASSET_VALUE] + # => [] else - # drop asset ID - dropw - # => [ASSET_VALUE] + dropw dropw + # => [] end - # => [PROCESSED_ASSET_VALUE] + # => [] end #! Invokes the `on_before_asset_added_to_note` callback on the faucet that issued the asset, @@ -83,18 +70,12 @@ end #! - If the callback storage slot contains the empty word. #! #! Inputs: [ASSET_ID, ASSET_VALUE, note_idx] -#! Outputs: [PROCESSED_ASSET_VALUE] +#! Outputs: [] #! #! Where: #! - ASSET_ID is the asset ID of the asset being added. #! - ASSET_VALUE is the value of the asset being added. #! - note_idx is the index of the output note the asset is being added to. -#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original -#! ASSET_VALUE if callbacks are disabled. The callback is required to return the asset -#! value it received, so this is always equal to ASSET_VALUE. -#! -#! Panics if: -#! - the callback returns an asset value different from the one it received. pub proc on_before_asset_added_to_note # derive the callback flag from the asset metadata, carried in the asset ID exec.asset::id_to_has_callbacks @@ -103,37 +84,30 @@ pub proc on_before_asset_added_to_note if.true push.ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_ROOT_SLOT[0..2] exec.invoke_callback - # => [PROCESSED_ASSET_VALUE] + # => [] else - # drop asset ID and note index - dropw movup.4 drop - # => [ASSET_VALUE] + dropw dropw drop + # => [] end - # => [PROCESSED_ASSET_VALUE] + # => [] end #! Invokes a callback by starting a foreign context against the faucet, reading the callback #! procedure root from the provided slot ID in the faucet's storage, and invoking it via `dyncall`. #! #! If the faucet does not have the callback storage slot, or if the slot contains the empty word, -#! the callback is skipped and the original ASSET_VALUE is returned. +#! the callback is skipped and its inputs are consumed. #! #! custom_data should be set to 0 for the account callback and to note_idx for the note callback. #! #! Inputs: [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data] -#! Outputs: [PROCESSED_ASSET_VALUE] +#! Outputs: [] #! #! Where: #! - slot_id* is the ID of the slot that contains the callback procedure root. #! - ASSET_ID is the asset ID of the asset being added. #! - ASSET_VALUE is the value of the asset being added. -#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original -#! ASSET_VALUE if no callback is configured. The callback is required to return the asset -#! value it received, so this is always equal to ASSET_VALUE. -#! -#! Panics if: -#! - the callback returns an asset value different from the one it received. -@locals(8) +@locals(4) proc invoke_callback exec.maybe_start_faucet_callback_context # => [was_foreign_context_started, should_invoke_callback, PROC_ROOT, ASSET_ID, ASSET_VALUE, custom_data] @@ -148,10 +122,6 @@ proc invoke_callback loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw # => [ASSET_ID, ASSET_VALUE, custom_data, was_foreign_context_started] - # save the original asset value for the post-callback equality check - swapw loc_storew_le.CALLBACK_ASSET_VALUE_LOC swapw - # => [ASSET_ID, ASSET_VALUE, custom_data, was_foreign_context_started] - # pad the stack to 16 for the call repeat.7 push.0 movdn.9 @@ -161,28 +131,19 @@ proc invoke_callback # invoke the callback locaddr.CALLBACK_PROC_ROOT_LOC dyncall - # => [PROCESSED_ASSET_VALUE, pad(12), was_foreign_context_started] - - # truncate the stack after the call - swapdw dropw dropw swapw dropw - # => [PROCESSED_ASSET_VALUE, was_foreign_context_started] + # => [pad(16), was_foreign_context_started] - # assert that the callback returned the asset value unchanged - dupw padw loc_loadw_le.CALLBACK_ASSET_VALUE_LOC - assert_eqw.err=ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT - # => [PROCESSED_ASSET_VALUE, was_foreign_context_started] + dropw dropw dropw dropw + # => [was_foreign_context_started] else - # drop proc root, asset ID and custom_data - dropw dropw movup.4 drop - # => [ASSET_VALUE, was_foreign_context_started] + # drop proc root and callback inputs + dropw dropw dropw drop + # => [was_foreign_context_started] end - # => [PROCESSED_ASSET_VALUE, was_foreign_context_started] - - movup.4 - # => [was_foreign_context_started, PROCESSED_ASSET_VALUE] + # => [was_foreign_context_started] exec.maybe_end_faucet_callback_context - # => [PROCESSED_ASSET_VALUE] + # => [] end #! Prepares the invocation of a faucet callback. diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/output_note.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/output_note.masm index 470b58fb62..9c63693631 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/output_note.masm @@ -249,23 +249,21 @@ pub proc add_asset emit.NOTE_BEFORE_ADD_ASSET_EVENT # => [ASSET_ID, ASSET_VALUE, note_idx] - # prepare the stack for the callback - swapw dupw.1 - # => [ASSET_ID, ASSET_VALUE, ASSET_ID, note_idx] - - dup.12 movdn.8 - # => [ASSET_ID, ASSET_VALUE, note_idx, ASSET_ID, note_idx] + # retain the original asset and note index while the callback validates a copy + repeat.9 + dup.8 + end + # => [ASSET_ID, ASSET_VALUE, note_idx, ASSET_ID, ASSET_VALUE, note_idx] # invoke the callback exec.callbacks::on_before_asset_added_to_note - swapw - # => [ASSET_ID, PROCESSED_ASSET_VALUE, note_idx] + # => [ASSET_ID, ASSET_VALUE, note_idx] movup.8 exec.memory::get_output_note_ptr dup - # => [note_ptr, note_ptr, ASSET_ID, PROCESSED_ASSET_VALUE] + # => [note_ptr, note_ptr, ASSET_ID, ASSET_VALUE] movdn.9 movdn.9 - # => [ASSET_ID, PROCESSED_ASSET_VALUE, note_ptr, note_ptr] + # => [ASSET_ID, ASSET_VALUE, note_ptr, note_ptr] # add the asset to the note exec.add_asset_raw diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/api.masm b/crates/miden-protocol/asm/kernels/transaction/lib/api.masm index aaf6aa5f7c..0768f17ed0 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/api.masm @@ -101,7 +101,7 @@ proc assert_auth_procedure_origin padw caller # => [CALLER] - # assert that the caller is from the user context + # assert that the caller is from the auth context exec.account::assert_auth_procedure # => [] end @@ -121,8 +121,8 @@ end #! - INIT_COMMITMENT is the initial account 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 account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_get_initial_commitment @@ -186,8 +186,8 @@ end #! - DELTA_COMMITMENT is the commitment to the account delta. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. #! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! - the vault delta or storage patch is not empty but the nonce increment is zero. #! #! Invocation: dynexec @@ -220,8 +220,8 @@ end #! - STORAGE_UPGRADE_COMMITMENT is the commitment to the account storage upgrade. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. #! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_upgrade @@ -325,7 +325,7 @@ end #! 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 current active account is not the native account of the transaction. #! - the invocation of this procedure does not originate from the authentication procedure #! of the account. #! - the nonce has already been incremented. @@ -381,8 +381,8 @@ end #! - 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 account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_get_initial_storage_commitment @@ -466,7 +466,8 @@ end #! #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_set_item @@ -521,7 +522,8 @@ end #! #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_get_initial_item @@ -557,7 +559,8 @@ end #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_get_initial_map_item @@ -591,8 +594,8 @@ end #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. -#! - the procedure is called from a non-account context. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_set_map_item @@ -622,8 +625,8 @@ end #! - INIT_VAULT_ROOT is the initial account vault root. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. #! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_get_initial_vault_root @@ -687,7 +690,8 @@ end #! - the total value of the fungible asset is greater than or equal to 2^63 after the new asset was #! added. #! - the vault already contains the same non-fungible asset. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_add_asset @@ -718,7 +722,8 @@ end #! - the fungible asset is not found in the vault. #! - the amount of the fungible asset in the vault is less than the amount to be removed. #! - the non-fungible asset is not found in the vault. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_remove_asset @@ -772,8 +777,8 @@ end #! - ASSET_VALUE is the value of the asset from the vault, which can be the EMPTY_WORD if it isn't present. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. #! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_get_initial_asset @@ -803,7 +808,7 @@ end #! Panics if: #! - the invocation of this procedure does not originate from the account context. #! - the procedure root is not part of the account code. -#! - the invocation of this procedure does not originate from the native account. +#! - the current active account is not the native account of the transaction. #! #! Invocation: dynexec pub proc account_was_procedure_called @@ -938,7 +943,8 @@ end #! - ASSET_VALUE is the value of the asset to mint. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! - the asset issuer is not the faucet the transaction is being executed against. #! - the asset is not well formed. #! - For fungible faucets: @@ -971,7 +977,8 @@ end #! - ASSET_VALUE is the value of the asset to burn. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! - the asset issuer is not the faucet the transaction is being executed against. #! - the asset is not well formed. #! - For fungible faucets: diff --git a/crates/miden-protocol/asm/protocol/src/active_note.masm b/crates/miden-protocol/asm/protocol/src/active_note.masm index 59717545a9..0a0ce12437 100644 --- a/crates/miden-protocol/asm/protocol/src/active_note.masm +++ b/crates/miden-protocol/asm/protocol/src/active_note.masm @@ -1,8 +1,7 @@ use miden::protocol_utils::mem -use {INPUT_NOTE_GET_METADATA_OFFSET, INPUT_NOTE_GET_RECIPIENT_OFFSET, INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET, INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET, INPUT_NOTE_GET_STORAGE_INFO_OFFSET} - from miden::protocol::kernel_proc_offsets use miden::protocol::note -use miden::protocol::input_note +use miden::protocol::input_note_internal +use miden::protocol::note_internal use {NOTE_TYPE_PRIVATE, NOTE_TYPE_PUBLIC} from miden::protocol::note use {AccountId, Bool, MemoryAddress, NoteId, NoteMetadata, NoteRecipient, NoteScriptRoot} from miden::protocol::types @@ -50,7 +49,7 @@ pub proc get_initial_assets(dest_ptr: MemoryAddress) -> u8 push.0.1 # => [is_active_note = 1, note_index = 0, dest_ptr] - exec.input_note::get_initial_assets_raw + exec.input_note_internal::get_initial_assets_raw # => [num_assets] end @@ -75,7 +74,7 @@ pub proc get_initial_assets_info push.0.1 # => [is_active_note = 1, note_index = 0] - exec.input_note::get_initial_assets_info_raw + exec.input_note_internal::get_initial_assets_info_raw # => [ASSETS_COMMITMENT, num_assets] end @@ -125,7 +124,7 @@ pub proc get_asset push.0 swap push.1 # => [is_active_note = 1, asset_index, note_index = 0] - exec.input_note::get_asset_raw + exec.input_note_internal::get_asset_raw # => [ASSET_ID, ASSET_VALUE] end @@ -160,7 +159,7 @@ pub proc remove_asset push.0 movdn.8 push.1 # => [is_active_note = 1, ASSET_ID, ASSET_VALUE, note_index = 0] - exec.input_note::remove_asset_raw + exec.input_note_internal::remove_asset_raw # => [FINAL_ASSET_VALUE] end @@ -196,7 +195,7 @@ pub proc remove_all_assets push.0.1 # => [is_active_note = 1, note_index = 0, dest_ptr] - exec.input_note::remove_all_assets_raw + exec.input_note_internal::remove_all_assets_raw # => [num_assets] end @@ -213,22 +212,11 @@ end #! #! Invocation: exec pub proc get_recipient() -> NoteRecipient - # 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)] - - push.INPUT_NOTE_GET_RECIPIENT_OFFSET - # => [offset, is_active_note = 1, pad(14)] - - syscall.exec_kernel_proc - # => [RECIPIENT, pad(12)] + # push a placeholder note_index (ignored when is_active_note = 1) and the active note flag + push.0.1 + # => [is_active_note = 1, note_index = 0] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_recipient_raw # => [RECIPIENT] end @@ -249,7 +237,7 @@ pub proc get_note_id() -> NoteId push.0.1 # => [is_active_note = 1, note_index = 0] - exec.input_note::get_note_id_raw + exec.input_note_internal::get_note_id_raw # => [NOTE_ID] end @@ -271,23 +259,11 @@ end #! #! Invocation: exec pub proc get_storage(dest_ptr: MemoryAddress) -> u16 - # 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] - - push.INPUT_NOTE_GET_STORAGE_INFO_OFFSET - # => [offset, is_active_note = 1, pad(14), dest_ptr] - - syscall.exec_kernel_proc - # => [NOTE_STORAGE_COMMITMENT, num_storage_items, pad(11), dest_ptr] + # push a placeholder note_index (ignored when is_active_note = 1) and the active note flag + push.0.1 + # => [is_active_note = 1, note_index = 0, dest_ptr] - # clean the stack - swapdw dropw dropw - movup.5 drop movup.5 drop movup.5 drop + exec.input_note_internal::get_storage_info_raw # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr] # save num_storage_items for the return value @@ -312,22 +288,11 @@ end #! #! Invocation: exec pub proc get_metadata() -> NoteMetadata - # 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)] - - push.INPUT_NOTE_GET_METADATA_OFFSET - # => [offset, is_active_note = 1, pad(14)] - - syscall.exec_kernel_proc - # => [METADATA, pad(12)] + # push a placeholder note_index (ignored when is_active_note = 1) and the active note flag + push.0.1 + # => [is_active_note = 1, note_index = 0] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_metadata_raw # => [METADATA] end @@ -411,22 +376,11 @@ end #! #! Invocation: exec pub proc get_serial_number() -> word - # 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)] - - push.INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET - # => [offset, is_active_note = 1, pad(14)] - - syscall.exec_kernel_proc - # => [SERIAL_NUMBER, pad(12)] + # push a placeholder note_index (ignored when is_active_note = 1) and the active note flag + push.0.1 + # => [is_active_note = 1, note_index = 0] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_serial_number_raw # => [SERIAL_NUMBER] end @@ -443,22 +397,11 @@ end #! #! Invocation: exec pub proc get_script_root() -> NoteScriptRoot - # 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)] - - push.INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET - # => [offset, is_active_note = 1, pad(14)] - - syscall.exec_kernel_proc - # => [SCRIPT_ROOT, pad(12)] + # push a placeholder note_index (ignored when is_active_note = 1) and the active note flag + push.0.1 + # => [is_active_note = 1, note_index = 0] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_script_root_raw # => [SCRIPT_ROOT] end @@ -531,7 +474,7 @@ pub proc get_attachments_commitment() -> word push.0.1 # => [is_active_note = 1, note_index = 0] - exec.input_note::get_attachments_commitment_raw + exec.input_note_internal::get_attachments_commitment_raw # => [ATTACHMENTS_COMMITMENT] end @@ -554,7 +497,7 @@ pub proc write_attachment_commitments_to_memory(dest_ptr: MemoryAddress) -> u8 exec.get_attachments_commitment # => [ATTACHMENTS_COMMITMENT, dest_ptr] - exec.note::write_attachment_commitments_to_memory + exec.note_internal::write_attachment_commitments_to_memory # => [num_attachments] end @@ -590,7 +533,7 @@ pub proc write_attachment_to_memory(dest_ptr: MemoryAddress, attachment_idx: u8) locaddr.0 swap # => [num_attachments, attachment_commitments_ptr, attachment_idx, dest_ptr] - exec.note::write_indexed_attachment_to_memory + exec.note_internal::write_indexed_attachment_to_memory # => [num_words] end diff --git a/crates/miden-protocol/asm/protocol/src/input_note.masm b/crates/miden-protocol/asm/protocol/src/input_note.masm index 826600a644..f5db91d8db 100644 --- a/crates/miden-protocol/asm/protocol/src/input_note.masm +++ b/crates/miden-protocol/asm/protocol/src/input_note.masm @@ -1,8 +1,8 @@ use miden::core::word -use miden::protocol::tx -use {INPUT_NOTE_GET_ASSET_OFFSET, INPUT_NOTE_GET_ATTACHMENTS_COMMITMENT_OFFSET, INPUT_NOTE_GET_INITIAL_ASSETS_INFO_OFFSET, INPUT_NOTE_GET_METADATA_OFFSET, INPUT_NOTE_GET_NOTE_ID_OFFSET, INPUT_NOTE_GET_RECIPIENT_OFFSET, INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET, INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET, INPUT_NOTE_GET_STORAGE_INFO_OFFSET, INPUT_NOTE_REMOVE_ALL_ASSETS_OFFSET, INPUT_NOTE_REMOVE_ASSET_OFFSET} - from miden::protocol::kernel_proc_offsets +use miden::protocol::input_note_internal use miden::protocol::note +use miden::protocol::note_internal +use miden::protocol::tx use {AccountId, Bool, MemoryAddress, NoteId, NoteMetadata, NoteRecipient, NoteScriptRoot} from miden::protocol::types @@ -54,40 +54,7 @@ pub proc get_initial_assets swap push.0 # => [is_active_note = 0, note_index, dest_ptr] - exec.get_initial_assets_raw - # => [num_assets] -end - -#! Writes the initial assets of the input note with the specified index or the active note, -#! depending on the provided flag, into memory starting at the specified address. -#! -#! This is the shared implementation used by both `input_note::get_initial_assets` and -#! `active_note::get_initial_assets`. See `input_note::get_initial_assets` for details on the -#! memory requirements and layout at `dest_ptr`. -#! -#! Inputs: [is_active_note, note_index, dest_ptr] -#! Outputs: [num_assets] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - note_index is the index of the input note. -#! - dest_ptr is the memory address to write the assets. -#! - num_assets is the number of assets the note was created with. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! -#! Invocation: exec -pub proc get_initial_assets_raw - exec.get_initial_assets_info_raw - # => [ASSETS_COMMITMENT, num_assets, dest_ptr] - - # save num_assets for the return value - dup.4 movdn.6 - # => [ASSETS_COMMITMENT, num_assets, dest_ptr, num_assets] - - # write the assets stored in the advice map to the specified memory pointer - exec.note::write_assets_to_memory + exec.input_note_internal::get_initial_assets_raw # => [num_assets] end @@ -112,48 +79,7 @@ pub proc get_initial_assets_info(note_index: u16) -> (word, u8) push.0 # => [is_active_note = 0, note_index] - exec.get_initial_assets_info_raw - # => [ASSETS_COMMITMENT, num_assets] -end - -#! Returns the initial assets information of the input note with the specified index or the -#! active note, depending on the provided flag. -#! -#! This is the shared implementation used by both `input_note::get_initial_assets_info` and -#! `active_note::get_initial_assets_info`. -#! -#! Inputs: [is_active_note, note_index] -#! Outputs: [ASSETS_COMMITMENT, num_assets] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - note_index is the index of the input note. -#! - num_assets is the number of assets the note was created with. -#! - ASSETS_COMMITMENT is a sequential hash of the assets the note was created with. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! -#! Invocation: exec -pub proc get_initial_assets_info_raw - push.0 movdn.2 - # => [is_active_note, note_index, 0] - - push.INPUT_NOTE_GET_INITIAL_ASSETS_INFO_OFFSET - # => [offset, is_active_note, note_index, 0] - - # pad the stack - padw swapw padw padw swapdw - # => [offset, is_active_note, note_index, pad(13)] - - syscall.exec_kernel_proc - # => [ASSETS_COMMITMENT, num_assets, pad(11)] - - # clean the stack - swapdw dropw dropw - repeat.3 - movup.5 drop - end + exec.input_note_internal::get_initial_assets_info_raw # => [ASSETS_COMMITMENT, num_assets] end @@ -204,44 +130,7 @@ pub proc get_asset push.0 # => [is_active_note = 0, asset_index, note_index] - exec.get_asset_raw - # => [ASSET_ID, ASSET_VALUE] -end - -#! Returns the asset at the specified index in the input note with the specified index or the -#! active note, depending on the provided flag. -#! -#! This is the shared implementation used by both `input_note::get_asset` and -#! `active_note::get_asset`. -#! -#! Inputs: [is_active_note, asset_index, note_index] -#! Outputs: [ASSET_ID, ASSET_VALUE] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - asset_index is the index of the asset to return. -#! - note_index is the index of the input note. -#! - ASSET_ID is the asset ID of the asset at the specified index. -#! - ASSET_VALUE is the value of the asset at the specified index. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! - the asset index is greater or equal to the number of assets in the note. -#! -#! Invocation: exec -pub proc get_asset_raw - push.INPUT_NOTE_GET_ASSET_OFFSET - # => [offset, is_active_note, asset_index, note_index] - - # pad the stack - padw swapw padw padw swapdw - # => [offset, is_active_note, asset_index, note_index, pad(12)] - - syscall.exec_kernel_proc - # => [ASSET_ID, ASSET_VALUE, pad(8)] - - # clean the stack - swapdw dropw dropw + exec.input_note_internal::get_asset_raw # => [ASSET_ID, ASSET_VALUE] end @@ -277,50 +166,7 @@ pub proc remove_asset push.0 # => [is_active_note = 0, ASSET_ID, ASSET_VALUE, note_index] - exec.remove_asset_raw - # => [FINAL_ASSET_VALUE] -end - -#! Removes an asset from the input note with the specified index or the active note, depending on -#! the provided flag. -#! -#! This is the shared implementation used by both `input_note::remove_asset` and -#! `active_note::remove_asset`. -#! -#! Inputs: [is_active_note, ASSET_ID, ASSET_VALUE, note_index] -#! Outputs: [FINAL_ASSET_VALUE] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - ASSET_ID is the asset ID of the asset to remove from the note. -#! - ASSET_VALUE is the value of the asset to remove from the note. -#! - note_index is the index of the input note. -#! - FINAL_ASSET_VALUE is the value of the asset remaining in the note after removal, which is -#! the EMPTY_WORD if the entire asset was removed. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! - the asset's composition is Custom (not yet supported). -#! - the asset is not present in the note. -#! - the asset is not composable and is not present in the note with the exact value. -#! - the amount of the fungible asset in the note is less than the amount to be removed. -#! -#! Invocation: exec -pub proc remove_asset_raw - push.INPUT_NOTE_REMOVE_ASSET_OFFSET - # => [offset, is_active_note, ASSET_ID, ASSET_VALUE, note_index] - - # pad the stack - repeat.5 - push.0 movdn.11 - end - # => [offset, is_active_note, ASSET_ID, ASSET_VALUE, note_index, pad(5)] - - syscall.exec_kernel_proc - # => [FINAL_ASSET_VALUE, pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::remove_asset_raw # => [FINAL_ASSET_VALUE] end @@ -357,57 +203,7 @@ pub proc remove_all_assets swap push.0 # => [is_active_note = 0, note_index, dest_ptr] - exec.remove_all_assets_raw - # => [num_assets] -end - -#! Removes all remaining assets from the input note with the specified index or the active note, -#! depending on the provided flag, and writes them into memory starting at the specified address. -#! -#! This is the shared implementation used by both `input_note::remove_all_assets` and -#! `active_note::remove_all_assets`. See `input_note::remove_all_assets` for details on the -#! memory requirements and layout at `dest_ptr`. -#! -#! Inputs: [is_active_note, note_index, dest_ptr] -#! Outputs: [num_assets] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - note_index is the index of the input note. -#! - dest_ptr is the memory address to write the assets. -#! - num_assets is the number of assets removed by this procedure. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! -#! Invocation: exec -pub proc remove_all_assets_raw(is_active_note: Bool, dest_ptr: MemoryAddress, note_index: u16) -> u8 - push.0 movdn.2 - # => [is_active_note, note_index, 0, dest_ptr] - - push.INPUT_NOTE_REMOVE_ALL_ASSETS_OFFSET - # => [offset, is_active_note, note_index, 0, dest_ptr] - - # pad the stack - padw swapw padw padw swapdw - # => [offset, is_active_note, note_index, pad(13), dest_ptr] - - syscall.exec_kernel_proc - # => [ASSETS_COMMITMENT, num_assets, pad(11), dest_ptr] - - # clean the stack - swapdw dropw dropw - repeat.3 - movup.5 drop - end - # => [ASSETS_COMMITMENT, num_assets, dest_ptr] - - # save num_assets for the return value - dup.4 movdn.6 - # => [ASSETS_COMMITMENT, num_assets, dest_ptr, num_assets] - - # write the removed assets stored in the advice map to the specified memory pointer - exec.note::write_assets_to_memory + exec.input_note_internal::remove_all_assets_raw # => [num_assets] end @@ -425,27 +221,10 @@ end #! #! Invocation: exec pub proc get_recipient(note_index: u16) -> NoteRecipient - # start padding the stack - 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] - - push.INPUT_NOTE_GET_RECIPIENT_OFFSET - # => [offset, is_active_note = 0, note_index, 0] - - # pad the stack - padw swapw padw padw swapdw - # => [offset, note_index, pad(14)] - - syscall.exec_kernel_proc - # => [RECIPIENT, pad(12)] + # => [is_active_note = 0, note_index] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_recipient_raw # => [RECIPIENT] end @@ -466,44 +245,7 @@ pub proc get_note_id(note_index: u16) -> NoteId push.0 # => [is_active_note = 0, note_index] - exec.get_note_id_raw - # => [NOTE_ID] -end - -#! Returns the ID of the input note with the specified index or the active note, depending on the -#! provided flag. -#! -#! This is the shared implementation used by both `input_note::get_note_id` and -#! `active_note::get_note_id`. -#! -#! Inputs: [is_active_note, note_index] -#! Outputs: [NOTE_ID] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - note_index is the index of the input note. -#! - NOTE_ID is the ID of the specified input note, cached by the transaction prologue. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! -#! Invocation: exec -pub proc get_note_id_raw(is_active_note: Bool, note_index: u16) -> NoteId - push.0 movdn.2 - # => [is_active_note, note_index, 0] - - push.INPUT_NOTE_GET_NOTE_ID_OFFSET - # => [offset, is_active_note, note_index, 0] - - # pad the stack - padw swapw padw padw swapdw - # => [offset, is_active_note, note_index, pad(13)] - - syscall.exec_kernel_proc - # => [NOTE_ID, pad(12)] - - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_note_id_raw # => [NOTE_ID] end @@ -596,27 +338,10 @@ end #! #! Invocation: exec pub proc get_metadata(note_index: u16) -> NoteMetadata - # 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] - - push.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)] + # => [is_active_note = 0, note_index] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_metadata_raw # => [METADATA] end @@ -657,30 +382,10 @@ end #! #! Invocation: exec pub proc get_storage_info(note_index: u16) -> (word, u16) - # 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] - - push.INPUT_NOTE_GET_STORAGE_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_STORAGE_COMMITMENT, num_storage_items, pad(11)] + # => [is_active_note = 0, note_index] - # clean the stack - swapdw dropw dropw - repeat.3 - movup.5 drop - end + exec.input_note_internal::get_storage_info_raw # => [NOTE_STORAGE_COMMITMENT, num_storage_items] end @@ -698,27 +403,10 @@ end #! #! Invocation: exec pub proc get_script_root(note_index: u16) -> NoteScriptRoot - # 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] - - push.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)] + # => [is_active_note = 0, note_index] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_script_root_raw # => [SCRIPT_ROOT] end @@ -736,27 +424,10 @@ end #! #! Invocation: exec pub proc get_serial_number(note_index: u16) -> word - # 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] - - push.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)] + # => [is_active_note = 0, note_index] - # clean the stack - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_serial_number_raw # => [SERIAL_NUMBER] end @@ -781,44 +452,7 @@ pub proc get_attachments_commitment(note_index: u16) -> word push.0 # => [is_active_note = 0, note_index] - exec.get_attachments_commitment_raw - # => [ATTACHMENTS_COMMITMENT] -end - -#! Returns the commitment over all attachments of the input note with the specified index or the -#! active note, depending on the provided flag. -#! -#! This is the shared implementation used by both `input_note::get_attachments_commitment` and -#! `active_note::get_attachments_commitment`. -#! -#! Inputs: [is_active_note, note_index] -#! Outputs: [ATTACHMENTS_COMMITMENT] -#! -#! Where: -#! - is_active_note is 0 for indexed access, 1 for the currently active note. -#! - note_index is the index of the input note. -#! - ATTACHMENTS_COMMITMENT is the commitment to all attachments of the note, or the EMPTY_WORD if -#! the note does not have any attachments. -#! -#! Panics if: -#! - the note index is greater or equal to the total number of input notes. -#! -#! Invocation: exec -pub proc get_attachments_commitment_raw(is_active_note: Bool, note_index: u16) -> word - push.0 movdn.2 - # => [is_active_note, note_index, 0] - - push.INPUT_NOTE_GET_ATTACHMENTS_COMMITMENT_OFFSET - # => [offset, is_active_note, note_index, 0] - - padw swapw padw padw swapdw - # => [offset, is_active_note, note_index, pad(13)] - - syscall.exec_kernel_proc - # => [ATTACHMENTS_COMMITMENT, pad(12)] - - # clean the stack, keeping only the commitment - swapdw dropw dropw swapw dropw + exec.input_note_internal::get_attachments_commitment_raw # => [ATTACHMENTS_COMMITMENT] end @@ -843,7 +477,7 @@ pub proc write_attachment_commitments_to_memory(dest_ptr: MemoryAddress, note_in swap exec.get_attachments_commitment # => [ATTACHMENTS_COMMITMENT, dest_ptr] - exec.note::write_attachment_commitments_to_memory + exec.note_internal::write_attachment_commitments_to_memory # => [num_attachments] end @@ -884,7 +518,7 @@ pub proc write_attachment_to_memory( locaddr.0 swap # => [num_attachments, attachment_commitments_ptr, attachment_idx, dest_ptr] - exec.note::write_indexed_attachment_to_memory + exec.note_internal::write_indexed_attachment_to_memory # => [num_words] end diff --git a/crates/miden-protocol/asm/protocol/src/input_note_internal.masm b/crates/miden-protocol/asm/protocol/src/input_note_internal.masm new file mode 100644 index 0000000000..2b5fd7ad6c --- /dev/null +++ b/crates/miden-protocol/asm/protocol/src/input_note_internal.masm @@ -0,0 +1,498 @@ +use {INPUT_NOTE_GET_ASSET_OFFSET, INPUT_NOTE_GET_ATTACHMENTS_COMMITMENT_OFFSET, INPUT_NOTE_GET_INITIAL_ASSETS_INFO_OFFSET, INPUT_NOTE_GET_METADATA_OFFSET, INPUT_NOTE_GET_NOTE_ID_OFFSET, INPUT_NOTE_GET_RECIPIENT_OFFSET, INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET, INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET, INPUT_NOTE_GET_STORAGE_INFO_OFFSET, INPUT_NOTE_REMOVE_ALL_ASSETS_OFFSET, INPUT_NOTE_REMOVE_ASSET_OFFSET} + from miden::protocol::kernel_proc_offsets +use miden::protocol::note_internal +use {Asset, AssetId, AssetValue, Bool, MemoryAddress, NoteId, NoteMetadata, NoteRecipient, NoteScriptRoot} + from miden::protocol::types + +# PROCEDURES +# ================================================================================================= +# +# Internal shared implementations of the `miden::protocol::input_note` and +# `miden::protocol::active_note` APIs. Each procedure takes an `is_active_note` flag: 0 selects the +# input note at the specified index, 1 selects the currently active note (in which case the note +# index is ignored). + +# ASSETS +# ================================================================================================= + +#! Writes the initial assets of the input note with the specified index or the active note, +#! depending on the provided flag, into memory starting at the specified address. +#! +#! This is the shared implementation used by both `input_note::get_initial_assets` and +#! `active_note::get_initial_assets`. See `input_note::get_initial_assets` for details on the +#! memory requirements and layout at `dest_ptr`. +#! +#! Inputs: [is_active_note, note_index, dest_ptr] +#! Outputs: [num_assets] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - dest_ptr is the memory address to write the assets. +#! - num_assets is the number of assets the note was created with. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! +#! Invocation: exec +pub proc get_initial_assets_raw(is_active_note: Bool, note_index: u16, dest_ptr: MemoryAddress) -> u8 + exec.get_initial_assets_info_raw + # => [ASSETS_COMMITMENT, num_assets, dest_ptr] + + # save num_assets for the return value + dup.4 movdn.6 + # => [ASSETS_COMMITMENT, num_assets, dest_ptr, num_assets] + + # write the assets stored in the advice map to the specified memory pointer + exec.note_internal::write_assets_to_memory + # => [num_assets] +end + +#! Returns the initial assets information of the input note with the specified index or the +#! active note, depending on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_initial_assets_info` and +#! `active_note::get_initial_assets_info`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [ASSETS_COMMITMENT, num_assets] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - num_assets is the number of assets the note was created with. +#! - ASSETS_COMMITMENT is a sequential hash of the assets the note was created with. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! +#! Invocation: exec +pub proc get_initial_assets_info_raw(is_active_note: Bool, note_index: u16) -> (word, u8) + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_INITIAL_ASSETS_INFO_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [ASSETS_COMMITMENT, num_assets, pad(11)] + + # clean the stack + swapdw dropw dropw + repeat.3 + movup.5 drop + end + # => [ASSETS_COMMITMENT, num_assets] +end + +#! Returns the asset at the specified index in the input note with the specified index or the +#! active note, depending on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_asset` and +#! `active_note::get_asset`. +#! +#! Inputs: [is_active_note, asset_index, note_index] +#! Outputs: [ASSET_ID, ASSET_VALUE] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - asset_index is the index of the asset to return. +#! - note_index is the index of the input note. +#! - ASSET_ID is the asset ID of the asset at the specified index. +#! - ASSET_VALUE is the value of the asset at the specified index. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! - the asset index is greater or equal to the number of assets in the note. +#! +#! Invocation: exec +pub proc get_asset_raw(is_active_note: Bool, asset_index: u16, note_index: u16) -> Asset + push.INPUT_NOTE_GET_ASSET_OFFSET + # => [offset, is_active_note, asset_index, note_index] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, asset_index, note_index, pad(12)] + + syscall.exec_kernel_proc + # => [ASSET_ID, ASSET_VALUE, pad(8)] + + # clean the stack + swapdw dropw dropw + # => [ASSET_ID, ASSET_VALUE] +end + +#! Removes an asset from the input note with the specified index or the active note, depending on +#! the provided flag. +#! +#! This is the shared implementation used by both `input_note::remove_asset` and +#! `active_note::remove_asset`. +#! +#! Inputs: [is_active_note, ASSET_ID, ASSET_VALUE, note_index] +#! Outputs: [FINAL_ASSET_VALUE] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - ASSET_ID is the asset ID of the asset to remove from the note. +#! - ASSET_VALUE is the value of the asset to remove from the note. +#! - note_index is the index of the input note. +#! - FINAL_ASSET_VALUE is the value of the asset remaining in the note after removal, which is +#! the EMPTY_WORD if the entire asset was removed. +#! +#! Panics if: +#! - is_active_note is false and the active account is not the native account. +#! - is_active_note is false and the invocation does not originate from the account context. +#! - the note index is greater or equal to the total number of input notes. +#! - the asset's composition is Custom (not yet supported). +#! - the asset is not present in the note. +#! - the asset is not composable and is not present in the note with the exact value. +#! - the amount of the fungible asset in the note is less than the amount to be removed. +#! +#! Invocation: exec +pub proc remove_asset_raw( + is_active_note: Bool, + asset_id: AssetId, + asset_value: AssetValue, + note_index: u16 +) -> AssetValue + push.INPUT_NOTE_REMOVE_ASSET_OFFSET + # => [offset, is_active_note, ASSET_ID, ASSET_VALUE, note_index] + + # pad the stack + repeat.5 + push.0 movdn.11 + end + # => [offset, is_active_note, ASSET_ID, ASSET_VALUE, note_index, pad(5)] + + syscall.exec_kernel_proc + # => [FINAL_ASSET_VALUE, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [FINAL_ASSET_VALUE] +end + +#! Removes all remaining assets from the input note with the specified index or the active note, +#! depending on the provided flag, and writes them into memory starting at the specified address. +#! +#! This is the shared implementation used by both `input_note::remove_all_assets` and +#! `active_note::remove_all_assets`. See `input_note::remove_all_assets` for details on the +#! memory requirements and layout at `dest_ptr`. +#! +#! Inputs: [is_active_note, note_index, dest_ptr] +#! Outputs: [num_assets] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - dest_ptr is the memory address to write the assets. +#! - num_assets is the number of assets removed by this procedure. +#! +#! Panics if: +#! - is_active_note is false and the active account is not the native account. +#! - is_active_note is false and the invocation does not originate from the account context. +#! - the note index is greater or equal to the total number of input notes. +#! +#! Invocation: exec +pub proc remove_all_assets_raw(is_active_note: Bool, note_index: u16, dest_ptr: MemoryAddress) -> u8 + push.0 movdn.2 + # => [is_active_note, note_index, 0, dest_ptr] + + push.INPUT_NOTE_REMOVE_ALL_ASSETS_OFFSET + # => [offset, is_active_note, note_index, 0, dest_ptr] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13), dest_ptr] + + syscall.exec_kernel_proc + # => [ASSETS_COMMITMENT, num_assets, pad(11), dest_ptr] + + # clean the stack + swapdw dropw dropw + repeat.3 + movup.5 drop + end + # => [ASSETS_COMMITMENT, num_assets, dest_ptr] + + # save num_assets for the return value + dup.4 movdn.6 + # => [ASSETS_COMMITMENT, num_assets, dest_ptr, num_assets] + + # write the removed assets stored in the advice map to the specified memory pointer + exec.note_internal::write_assets_to_memory + # => [num_assets] +end + +# ACCESSORS +# ================================================================================================= + +#! Returns the recipient of the input note with the specified index or the active note, depending +#! on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_recipient` and +#! `active_note::get_recipient`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [RECIPIENT] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - RECIPIENT is the commitment to the input note's script, storage, the serial number. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! +#! Invocation: exec +pub proc get_recipient_raw(is_active_note: Bool, note_index: u16) -> NoteRecipient + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_RECIPIENT_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [RECIPIENT, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [RECIPIENT] +end + +#! Returns the metadata of the input note with the specified index or the active note, depending +#! on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_metadata` and +#! `active_note::get_metadata`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [METADATA] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - 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. +#! +#! Invocation: exec +pub proc get_metadata_raw(is_active_note: Bool, note_index: u16) -> NoteMetadata + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_METADATA_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [METADATA, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [METADATA] +end + +#! Returns the inputs commitment and length of the input note with the specified index or the +#! active note, depending on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_storage_info` and +#! `active_note::get_storage`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [NOTE_STORAGE_COMMITMENT, num_storage_items] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - NOTE_STORAGE_COMMITMENT is the inputs commitment of the specified input note. +#! - num_storage_items 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 +pub proc get_storage_info_raw(is_active_note: Bool, note_index: u16) -> (word, u16) + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_STORAGE_INFO_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [NOTE_STORAGE_COMMITMENT, num_storage_items, pad(11)] + + # clean the stack + swapdw dropw dropw + repeat.3 + movup.5 drop + end + # => [NOTE_STORAGE_COMMITMENT, num_storage_items] +end + +#! Returns the script root of the input note with the specified index or the active note, +#! depending on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_script_root` and +#! `active_note::get_script_root`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [SCRIPT_ROOT] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - 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 +pub proc get_script_root_raw(is_active_note: Bool, note_index: u16) -> NoteScriptRoot + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, 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 or the active note, +#! depending on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_serial_number` and +#! `active_note::get_serial_number`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [SERIAL_NUMBER] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - 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 +pub proc get_serial_number_raw(is_active_note: Bool, note_index: u16) -> word + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [SERIAL_NUMBER, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [SERIAL_NUMBER] +end + +#! Returns the ID of the input note with the specified index or the active note, depending on the +#! provided flag. +#! +#! This is the shared implementation used by both `input_note::get_note_id` and +#! `active_note::get_note_id`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [NOTE_ID] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - NOTE_ID is the ID of the specified input note, cached by the transaction prologue. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! +#! Invocation: exec +pub proc get_note_id_raw(is_active_note: Bool, note_index: u16) -> NoteId + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_NOTE_ID_OFFSET + # => [offset, is_active_note, note_index, 0] + + # pad the stack + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [NOTE_ID, pad(12)] + + # clean the stack + swapdw dropw dropw swapw dropw + # => [NOTE_ID] +end + +# ATTACHMENTS +# ================================================================================================= + +#! Returns the commitment over all attachments of the input note with the specified index or the +#! active note, depending on the provided flag. +#! +#! This is the shared implementation used by both `input_note::get_attachments_commitment` and +#! `active_note::get_attachments_commitment`. +#! +#! Inputs: [is_active_note, note_index] +#! Outputs: [ATTACHMENTS_COMMITMENT] +#! +#! Where: +#! - is_active_note is 0 for indexed access, 1 for the currently active note. +#! - note_index is the index of the input note. +#! - ATTACHMENTS_COMMITMENT is the commitment to all attachments of the note, or the EMPTY_WORD if +#! the note does not have any attachments. +#! +#! Panics if: +#! - the note index is greater or equal to the total number of input notes. +#! +#! Invocation: exec +pub proc get_attachments_commitment_raw(is_active_note: Bool, note_index: u16) -> word + push.0 movdn.2 + # => [is_active_note, note_index, 0] + + push.INPUT_NOTE_GET_ATTACHMENTS_COMMITMENT_OFFSET + # => [offset, is_active_note, note_index, 0] + + padw swapw padw padw swapdw + # => [offset, is_active_note, note_index, pad(13)] + + syscall.exec_kernel_proc + # => [ATTACHMENTS_COMMITMENT, pad(12)] + + # clean the stack, keeping only the commitment + swapdw dropw dropw swapw dropw + # => [ATTACHMENTS_COMMITMENT] +end diff --git a/crates/miden-protocol/asm/protocol/src/mod.masm b/crates/miden-protocol/asm/protocol/src/mod.masm index 22fd963ca2..60be280929 100644 --- a/crates/miden-protocol/asm/protocol/src/mod.masm +++ b/crates/miden-protocol/asm/protocol/src/mod.masm @@ -11,9 +11,11 @@ pub mod auth pub mod constants pub mod faucet pub mod input_note +mod input_note_internal pub mod kernel_proc_offsets pub mod native_account pub mod note +mod note_internal pub mod output_note pub mod tx pub mod types diff --git a/crates/miden-protocol/asm/protocol/src/native_account.masm b/crates/miden-protocol/asm/protocol/src/native_account.masm index f8cf466bed..8614934379 100644 --- a/crates/miden-protocol/asm/protocol/src/native_account.masm +++ b/crates/miden-protocol/asm/protocol/src/native_account.masm @@ -50,7 +50,7 @@ end #! 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 current active account is not the native account of the transaction. #! - the invocation of this procedure does not originate from the authentication procedure #! of the account. #! - the nonce has already been incremented. @@ -97,8 +97,8 @@ end #! - DELTA_COMMITMENT is the commitment to the account delta. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. #! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! - the vault or storage patch is not empty but the nonce increment is zero. pub proc compute_delta_commitment() -> word # pad the stack @@ -159,7 +159,8 @@ end #! - INIT_COMMITMENT is the initial account 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 account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_commitment() -> word @@ -192,7 +193,8 @@ end #! - 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 account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_storage_commitment() -> word @@ -220,7 +222,8 @@ end #! - INIT_VAULT_ROOT is the initial account vault root. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_vault_root() -> word @@ -255,7 +258,8 @@ end #! #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc set_item(slot_id: StorageSlotId, value: word) -> word @@ -290,8 +294,8 @@ end #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. -#! - the procedure is called from a non-account context. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc set_map_item(slot_id: StorageSlotId, key: StorageMapKey, value: word) -> word @@ -329,7 +333,8 @@ end #! #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_item(slot_id: StorageSlotId) -> word @@ -366,7 +371,8 @@ end #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. #! - the slot item at index is not a map. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_map_item(slot_id: StorageSlotId, key: StorageMapKey) -> word @@ -467,7 +473,8 @@ end #! present. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_asset(asset_id: AssetId) -> AssetValue @@ -498,7 +505,8 @@ end #! - has_asset is a boolean indicating whether the account vault had the asset. #! #! Panics if: -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc has_initial_asset(asset_id: AssetId) -> Bool diff --git a/crates/miden-protocol/asm/protocol/src/note.masm b/crates/miden-protocol/asm/protocol/src/note.masm index ea65283e5c..56a865faaf 100644 --- a/crates/miden-protocol/asm/protocol/src/note.masm +++ b/crates/miden-protocol/asm/protocol/src/note.masm @@ -1,21 +1,17 @@ use miden::protocol::account_id use miden::core::crypto::hashes::poseidon2 -use miden::core::mem -use {WORD_NUM_ELEMENTS} from miden::protocol::constants use {AccountId, Bool, MemoryAddress, NoteMetadata, NoteRecipient, NoteScriptRoot, NoteTag, NoteType} from miden::protocol::types pub use {ATTACHMENT_SCHEME_NONE, MAX_ATTACHMENT_SCHEME, MAX_ATTACHMENT_TOTAL_WORDS, MAX_ATTACHMENT_WORDS, MAX_NOTE_STORAGE_ITEMS, NOTE_TYPE_PRIVATE, NOTE_TYPE_PUBLIC} from miden::protocol_utils::note -# ERRORS +# ERRORS # ================================================================================================= const ERR_PROLOGUE_NOTE_NUM_STORAGE_ITEMS_EXCEEDED_LIMIT = "number of note storage exceeded the maximum limit of 1024" -const ERR_OUTPUT_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS = "attachment index out of bounds" - # NOTE UTILITY PROCEDURES # ================================================================================================= @@ -50,166 +46,6 @@ pub proc compute_storage_commitment(storage_ptr: MemoryAddress, num_storage_item # => [STORAGE_COMMITMENT] end -#! Writes the assets data stored in the advice map to the memory specified by the provided -#! destination pointer. -#! -#! Inputs: -#! Operand stack: [ASSETS_COMMITMENT, num_assets, dest_ptr] -#! Advice map: { -#! ASSETS_COMMITMENT: [[ASSETS_DATA]] -#! } -#! Outputs: -#! Operand stack: [] -pub proc write_assets_to_memory(assets_commitment: word, num_assets: u8, dest_ptr: MemoryAddress) - # load the asset data from the advice map to the advice stack - adv.push_mapval - # OS => [ASSETS_COMMITMENT, num_assets, dest_ptr] - # AS => [[ASSETS_DATA]] - - movup.5 movup.5 - # OS => [num_assets, dest_ptr, ASSETS_COMMITMENT] - # AS => [[ASSETS_DATA]] - - # each asset takes up two words, so num_words = 2 * num_assets - # this also guarantees we pass an even number to pipe_double_words_preimage_to_memory - mul.2 - # OS => [num_words, dest_ptr, ASSETS_COMMITMENT] - # AS => [[ASSETS_DATA]] - - # write the data from the advice stack into memory - exec.mem::pipe_double_words_preimage_to_memory drop - # OS => [] - # AS => [] -end - -#! Writes the attachment commitments stored in the advice map to memory specified by the provided -#! destination pointer. -#! -#! Inputs: -#! Operand stack: [ATTACHMENTS_COMMITMENT, dest_ptr] -#! Advice map: { -#! ATTACHMENTS_COMMITMENT: [[ATTACHMENT_COMMITMENT]] -#! } -#! Outputs: -#! Operand stack: [num_attachments] -pub proc write_attachment_commitments_to_memory( - attachments_commitment: word, - dest_ptr: MemoryAddress -) -> u8 - # push the individual ATTACHMENT commitments from the advice map onto the advice stack - adv.push_mapvaln - # OS => [ATTACHMENTS_COMMITMENT, dest_ptr] - # AS => [num_elements, [ATTACHMENT_COMMITMENT]] - - # SAFETY: if the provided num_elements is invalid, the commitment check would fail in - # pipe_preimage_to_memory so we assume validity and only do basic checks to protect against - # invalid advice inputs. - adv_push u32assert.err="invalid attachment num_elements advice input" - u32divmod.WORD_NUM_ELEMENTS - # OS => [remainder, num_words, ATTACHMENTS_COMMITMENT, dest_ptr] - # AS => [[ATTACHMENT_COMMITMENT]] - - # assert that num_elements is a multiple of WORD_NUM_ELEMENTS - eq.0 assert.err="attachment commitments num_elements is not a multiple of WORD_NUM_ELEMENTS" - # OS => [num_words, ATTACHMENTS_COMMITMENT, dest_ptr] - # AS => [[ATTACHMENT_COMMITMENT]] - - # store the number of words as the number of attachments for return - swap.5 dup.5 - # OS => [num_words, dest_ptr, ATTACHMENTS_COMMITMENT, num_attachments] - # AS => [[ATTACHMENT_COMMITMENT]] - - # pipe attachment commitments to memory and validate they match the ATTACHMENTS_COMMITMENT - exec.mem::pipe_preimage_to_memory drop - # => [num_attachments] -end - -#! Writes a single attachment's data stored in the advice map to the memory specified by the -#! provided destination pointer. -#! -#! Inputs: -#! Operand stack: [ATTACHMENT_COMMITMENT, dest_ptr] -#! Advice map: { -#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], -#! } -#! Outputs: -#! Operand stack: [num_words] -#! -#! Where: -#! - ATTACHMENT_COMMITMENT is the hash commitment to the attachment elements. -#! - dest_ptr is the memory address to which to write the attachment data. -#! - num_words is the number of words in the attachment. -pub proc write_attachment_to_memory(attachment_commitment: word, dest_ptr: MemoryAddress) -> u16 - # push the number of attachment elements from the advice map onto the advice stack - adv.push_mapvaln - # OS => [ATTACHMENT_COMMITMENT, dest_ptr] - # AS => [num_elements, [ATTACHMENT_ELEMENTS]] - - # SAFETY: if the provided num_elements is invalid, the commitment check would fail in - # pipe_preimage_to_memory so we assume validity and only do basic checks to protect against - # invalid advice inputs. - adv_push u32assert.err="invalid attachment num_elements advice input" - u32divmod.WORD_NUM_ELEMENTS - # OS => [remainder, num_words, ATTACHMENT_COMMITMENT, dest_ptr] - # AS => [[ATTACHMENT_ELEMENTS]] - - # assert that num_elements is a multiple of WORD_NUM_ELEMENTS - eq.0 assert.err="attachment num_elements is not a multiple of WORD_NUM_ELEMENTS" - # OS => [num_words, ATTACHMENT_COMMITMENT, dest_ptr] - # AS => [[ATTACHMENT_ELEMENTS]] - - swap.5 dup.5 - # OS => [num_words, dest_ptr, ATTACHMENT_COMMITMENT, num_words] - # AS => [[ATTACHMENT_ELEMENTS]] - - # pipe the attachment data into memory, validating against ATTACHMENT_COMMITMENT - exec.mem::pipe_preimage_to_memory drop - # => [num_words] -end - -#! Writes the attachment with the provided index from the provided attachment commitments to the -#! memory specified by the destination pointer. -#! -#! Inputs: [num_attachments, attachment_commitments_ptr, attachment_idx, dest_ptr] -#! Outputs: [num_words] -#! -#! Where: -#! - attachment_idx is the index of the attachment to retrieve. -#! - attachment_commitments_ptr is a pointer to the attachment commitments in memory. -#! - dest_ptr is the memory address to which to write the attachment data. -#! - num_attachments is the number of attachments. -#! - num_words is the number of words in the attachment. -#! -#! Panics if: -#! - the attachment index is greater or equal to the number of attachments. -#! - the sequential hash over the attachment data in the advice inputs does not match the -#! attachment commitment. -#! -#! Invocation: exec -pub proc write_indexed_attachment_to_memory( - num_attachments: u8, - attachment_commitments_ptr: MemoryAddress, - attachment_idx: u8, - dest_ptr: MemoryAddress -) -> u16 - # assert attachment_idx < num_attachments - dup.2 swap u32assert2.err=ERR_OUTPUT_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS - u32lt assert.err=ERR_OUTPUT_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS - # => [attachment_commitments_ptr, attachment_idx, dest_ptr] - - # compute the memory address of the attachment commitment: - # commitment_ptr = attachment_commitments_ptr + attachment_idx * WORD_NUM_ELEMENTS - swap mul.WORD_NUM_ELEMENTS add - # => [commitment_ptr, dest_ptr] - - # load the ATTACHMENT_COMMITMENT from memory - padw movup.4 mem_loadw_le - # => [ATTACHMENT_COMMITMENT, dest_ptr] - - exec.write_attachment_to_memory - # => [num_words] -end - #! Computes the recipient hash from note storage, script root, and serial number. #! #! This procedure computes the commitment of the note storage and then uses it to calculate the note diff --git a/crates/miden-protocol/asm/protocol/src/note_internal.masm b/crates/miden-protocol/asm/protocol/src/note_internal.masm new file mode 100644 index 0000000000..e1bd668ebd --- /dev/null +++ b/crates/miden-protocol/asm/protocol/src/note_internal.masm @@ -0,0 +1,174 @@ +use miden::core::mem +use {WORD_NUM_ELEMENTS} from miden::protocol::constants +use {MemoryAddress} from miden::protocol::types + +# ERRORS +# ================================================================================================= + +const ERR_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS = "attachment index out of bounds" + +# PROCEDURES +# ================================================================================================= +# +# Internal memory-write helpers shared by the `miden::protocol::input_note`, +# `miden::protocol::active_note`, and `miden::protocol::output_note` APIs. + +#! Writes the assets data stored in the advice map to the memory specified by the provided +#! destination pointer. +#! +#! Inputs: +#! Operand stack: [ASSETS_COMMITMENT, num_assets, dest_ptr] +#! Advice map: { +#! ASSETS_COMMITMENT: [[ASSETS_DATA]] +#! } +#! Outputs: +#! Operand stack: [] +pub proc write_assets_to_memory(assets_commitment: word, num_assets: u8, dest_ptr: MemoryAddress) + # load the asset data from the advice map to the advice stack + adv.push_mapval + # OS => [ASSETS_COMMITMENT, num_assets, dest_ptr] + # AS => [[ASSETS_DATA]] + + movup.5 movup.5 + # OS => [num_assets, dest_ptr, ASSETS_COMMITMENT] + # AS => [[ASSETS_DATA]] + + # each asset takes up two words, so num_words = 2 * num_assets + # this also guarantees we pass an even number to pipe_double_words_preimage_to_memory + mul.2 + # OS => [num_words, dest_ptr, ASSETS_COMMITMENT] + # AS => [[ASSETS_DATA]] + + # write the data from the advice stack into memory + exec.mem::pipe_double_words_preimage_to_memory drop + # OS => [] + # AS => [] +end + +#! Writes the attachment commitments stored in the advice map to memory specified by the provided +#! destination pointer. +#! +#! Inputs: +#! Operand stack: [ATTACHMENTS_COMMITMENT, dest_ptr] +#! Advice map: { +#! ATTACHMENTS_COMMITMENT: [[ATTACHMENT_COMMITMENT]] +#! } +#! Outputs: +#! Operand stack: [num_attachments] +pub proc write_attachment_commitments_to_memory( + attachments_commitment: word, + dest_ptr: MemoryAddress +) -> u8 + # push the individual ATTACHMENT commitments from the advice map onto the advice stack + adv.push_mapvaln + # OS => [ATTACHMENTS_COMMITMENT, dest_ptr] + # AS => [num_elements, [ATTACHMENT_COMMITMENT]] + + # SAFETY: if the provided num_elements is invalid, the commitment check would fail in + # pipe_preimage_to_memory so we assume validity and only do basic checks to protect against + # invalid advice inputs. + adv_push u32assert.err="invalid attachment num_elements advice input" + u32divmod.WORD_NUM_ELEMENTS + # OS => [remainder, num_words, ATTACHMENTS_COMMITMENT, dest_ptr] + # AS => [[ATTACHMENT_COMMITMENT]] + + # assert that num_elements is a multiple of WORD_NUM_ELEMENTS + eq.0 assert.err="attachment commitments num_elements is not a multiple of WORD_NUM_ELEMENTS" + # OS => [num_words, ATTACHMENTS_COMMITMENT, dest_ptr] + # AS => [[ATTACHMENT_COMMITMENT]] + + # store the number of words as the number of attachments for return + swap.5 dup.5 + # OS => [num_words, dest_ptr, ATTACHMENTS_COMMITMENT, num_attachments] + # AS => [[ATTACHMENT_COMMITMENT]] + + # pipe attachment commitments to memory and validate they match the ATTACHMENTS_COMMITMENT + exec.mem::pipe_preimage_to_memory drop + # => [num_attachments] +end + +#! Writes a single attachment's data stored in the advice map to the memory specified by the +#! provided destination pointer. +#! +#! Inputs: +#! Operand stack: [ATTACHMENT_COMMITMENT, dest_ptr] +#! Advice map: { +#! ATTACHMENT_COMMITMENT: [[ATTACHMENT_ELEMENTS]], +#! } +#! Outputs: +#! Operand stack: [num_words] +#! +#! Where: +#! - ATTACHMENT_COMMITMENT is the hash commitment to the attachment elements. +#! - dest_ptr is the memory address to which to write the attachment data. +#! - num_words is the number of words in the attachment. +pub proc write_attachment_to_memory(attachment_commitment: word, dest_ptr: MemoryAddress) -> u16 + # push the number of attachment elements from the advice map onto the advice stack + adv.push_mapvaln + # OS => [ATTACHMENT_COMMITMENT, dest_ptr] + # AS => [num_elements, [ATTACHMENT_ELEMENTS]] + + # SAFETY: if the provided num_elements is invalid, the commitment check would fail in + # pipe_preimage_to_memory so we assume validity and only do basic checks to protect against + # invalid advice inputs. + adv_push u32assert.err="invalid attachment num_elements advice input" + u32divmod.WORD_NUM_ELEMENTS + # OS => [remainder, num_words, ATTACHMENT_COMMITMENT, dest_ptr] + # AS => [[ATTACHMENT_ELEMENTS]] + + # assert that num_elements is a multiple of WORD_NUM_ELEMENTS + eq.0 assert.err="attachment num_elements is not a multiple of WORD_NUM_ELEMENTS" + # OS => [num_words, ATTACHMENT_COMMITMENT, dest_ptr] + # AS => [[ATTACHMENT_ELEMENTS]] + + swap.5 dup.5 + # OS => [num_words, dest_ptr, ATTACHMENT_COMMITMENT, num_words] + # AS => [[ATTACHMENT_ELEMENTS]] + + # pipe the attachment data into memory, validating against ATTACHMENT_COMMITMENT + exec.mem::pipe_preimage_to_memory drop + # => [num_words] +end + +#! Writes the attachment with the provided index from the provided attachment commitments to the +#! memory specified by the destination pointer. +#! +#! Inputs: [num_attachments, attachment_commitments_ptr, attachment_idx, dest_ptr] +#! Outputs: [num_words] +#! +#! Where: +#! - attachment_idx is the index of the attachment to retrieve. +#! - attachment_commitments_ptr is a pointer to the attachment commitments in memory. +#! - dest_ptr is the memory address to which to write the attachment data. +#! - num_attachments is the number of attachments. +#! - num_words is the number of words in the attachment. +#! +#! Panics if: +#! - the attachment index is greater or equal to the number of attachments. +#! - the sequential hash over the attachment data in the advice inputs does not match the +#! attachment commitment. +#! +#! Invocation: exec +pub proc write_indexed_attachment_to_memory( + num_attachments: u8, + attachment_commitments_ptr: MemoryAddress, + attachment_idx: u8, + dest_ptr: MemoryAddress +) -> u16 + # assert attachment_idx < num_attachments + dup.2 swap u32assert2.err=ERR_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS + u32lt assert.err=ERR_NOTE_ATTACHMENT_IDX_OUT_OF_BOUNDS + # => [attachment_commitments_ptr, attachment_idx, dest_ptr] + + # compute the memory address of the attachment commitment: + # commitment_ptr = attachment_commitments_ptr + attachment_idx * WORD_NUM_ELEMENTS + swap mul.WORD_NUM_ELEMENTS add + # => [commitment_ptr, dest_ptr] + + # load the ATTACHMENT_COMMITMENT from memory + padw movup.4 mem_loadw_le + # => [ATTACHMENT_COMMITMENT, dest_ptr] + + exec.write_attachment_to_memory + # => [num_words] +end diff --git a/crates/miden-protocol/asm/protocol/src/output_note.masm b/crates/miden-protocol/asm/protocol/src/output_note.masm index 88f9cc464f..6915fc239b 100644 --- a/crates/miden-protocol/asm/protocol/src/output_note.masm +++ b/crates/miden-protocol/asm/protocol/src/output_note.masm @@ -1,4 +1,5 @@ use miden::protocol::note +use miden::protocol::note_internal use miden::core::crypto::hashes::poseidon2 use {WORD_NUM_ELEMENTS} from miden::protocol::constants use {Asset, Bool, MemoryAddress, NoteId, NoteMetadata, NoteRecipient, NoteTag, NoteType} @@ -119,7 +120,7 @@ pub proc get_assets(dest_ptr: MemoryAddress, note_index: u16) -> u8 # => [ASSETS_COMMITMENT, num_assets, dest_ptr, num_assets] # write the assets stored in the advice map to the specified memory pointer - exec.note::write_assets_to_memory + exec.note_internal::write_assets_to_memory # => [num_assets] end @@ -505,7 +506,7 @@ pub proc write_attachment_commitments_to_memory(dest_ptr: MemoryAddress, note_in swap exec.get_attachments_commitment # => [ATTACHMENTS_COMMITMENT, dest_ptr] - exec.note::write_attachment_commitments_to_memory + exec.note_internal::write_attachment_commitments_to_memory # => [num_attachments] end @@ -546,6 +547,6 @@ pub proc write_attachment_to_memory( locaddr.0 swap # => [num_attachments, attachment_commitments_ptr, attachment_idx, dest_ptr] - exec.note::write_indexed_attachment_to_memory + exec.note_internal::write_indexed_attachment_to_memory # => [num_words] end diff --git a/crates/miden-protocol/src/account/delta/mod.rs b/crates/miden-protocol/src/account/delta/mod.rs index 6d5c2ec835..96a5a4f330 100644 --- a/crates/miden-protocol/src/account/delta/mod.rs +++ b/crates/miden-protocol/src/account/delta/mod.rs @@ -18,12 +18,7 @@ mod delta_op; pub use delta_op::AssetDeltaOperation; mod vault; -pub use vault::{ - AccountVaultDelta, - FungibleAssetDelta, - NonFungibleAssetDelta, - NonFungibleDeltaAction, -}; +pub use vault::{AccountVaultDelta, AssetDelta}; // ACCOUNT DELTA // ================================================================================================ diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index f1e5923929..c55bd663b7 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -3,8 +3,6 @@ use alloc::collections::btree_map::Entry; use alloc::string::ToString; use alloc::vec::Vec; -use miden_core::Word; - use super::{ AccountDeltaError, ByteReader, @@ -13,132 +11,156 @@ use super::{ DeserializationError, Serializable, }; -use crate::Felt; use crate::account::delta::AssetDeltaOperation; -use crate::asset::{Asset, AssetId, FungibleAsset, NonFungibleAsset}; +use crate::asset::{Asset, AssetId}; +use crate::{Felt, Word}; + +// ASSET DELTA +// ================================================================================================ + +/// The change of a single asset in an [`AccountVaultDelta`]. +/// +/// The asset is the magnitude of the change while the operation gives its direction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AssetDelta { + delta_op: AssetDeltaOperation, + asset: Asset, +} + +impl AssetDelta { + /// Creates a new [`AssetDelta`] by which the vault changed under the given operation. + pub fn new(delta_op: AssetDeltaOperation, asset: Asset) -> Self { + Self { delta_op, asset } + } + + /// Returns the operation of this delta. + pub fn delta_op(&self) -> AssetDeltaOperation { + self.delta_op + } + + /// Returns the asset by which the vault changed. + pub fn asset(&self) -> Asset { + self.asset + } + + /// Returns the ID of the asset by which the vault changed. + pub fn asset_id(&self) -> AssetId { + self.asset.id() + } +} // ACCOUNT VAULT DELTA // ================================================================================================ -/// [AccountVaultDelta] stores the difference between the initial and final account vault states. +/// [`AccountVaultDelta`] stores the difference between the initial and final account vault states. +/// +/// The difference is represented as a map of [`AssetDelta`]s keyed by the ID of the asset they +/// change. The [`AssetId`] orders the assets in the same way as the in-kernel account delta. /// -/// The difference is represented as follows: -/// - fungible: a binary tree map of fungible asset balance changes in the account vault. -/// - non_fungible: a binary tree map of non-fungible assets that were added to or removed from the -/// account vault. +/// ## Purpose +/// +/// The purpose of a vault delta is to represent the changes to the vault that a transaction results +/// in and provide a way to commit to and sign these changes. Unlike an +/// [`AccountVaultPatch`](crate::account::AccountVaultPatch), a delta cannot be applied to an +/// account and multiple deltas cannot be merged, since that isn't necessary for signing. +/// +/// ## Limitations +/// +/// The delta does not include the functionality to merge or split assets. This would mainly be +/// needed to merge deltas, which isn't supported. Additionally, once custom assets are supported, +/// their merge and split logic will be defined in the issuing faucet, and the delta would not be +/// able to (easily) invoke this logic. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct AccountVaultDelta { - fungible: FungibleAssetDelta, - non_fungible: NonFungibleAssetDelta, + delta: BTreeMap, } impl AccountVaultDelta { /// Domain separator for assets in the account delta commitment. pub(in crate::account) const DOMAIN: Felt = Felt::new_unchecked(3); - /// Validates and creates an [AccountVaultDelta] with the given fungible and non-fungible asset - /// deltas. + /// Validates and creates an [`AccountVaultDelta`] from the given asset deltas. /// /// # Errors - /// Returns an error if the delta does not pass the validation. - pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self { - Self { fungible, non_fungible } - } + /// + /// Returns an error if the same asset is changed by more than one delta. + pub fn new( + asset_deltas: impl IntoIterator, + ) -> Result { + let mut delta = BTreeMap::new(); + + for asset_delta in asset_deltas { + match delta.entry(asset_delta.asset_id()) { + Entry::Vacant(entry) => { + entry.insert(asset_delta); + }, + Entry::Occupied(entry) => { + return Err(AccountDeltaError::DuplicateAssetDelta(*entry.key())); + }, + } + } - /// Returns a reference to the fungible asset delta. - pub fn fungible(&self) -> &FungibleAssetDelta { - &self.fungible + Ok(Self { delta }) } - /// Returns a reference to the non-fungible asset delta. - pub fn non_fungible(&self) -> &NonFungibleAssetDelta { - &self.non_fungible + /// Inserts an asset delta, overwriting the previous delta of the same asset. + /// + /// Returns the overwritten delta, if the asset was already present. + pub fn insert(&mut self, asset_delta: AssetDelta) -> Option { + self.delta.insert(asset_delta.asset_id(), asset_delta) } /// Returns true if this vault delta contains no updates. pub fn is_empty(&self) -> bool { - self.fungible.is_empty() && self.non_fungible.is_empty() + self.delta.is_empty() } - /// Tracks asset addition. - pub fn add_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> { - match asset { - Asset::Fungible(asset) => self.fungible.add(asset), - Asset::NonFungible(asset) => self.non_fungible.add(asset), - } + /// Returns the number of assets changed in this delta. + pub fn num_assets(&self) -> usize { + self.delta.len() } - /// Tracks asset removal. - pub fn remove_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> { - match asset { - Asset::Fungible(asset) => self.fungible.remove(asset), - Asset::NonFungible(asset) => self.non_fungible.remove(asset), - } + /// Returns an iterator over the asset deltas, sorted by asset ID. + pub fn iter(&self) -> impl Iterator { + self.delta.values() } /// Returns an iterator over the added assets in this delta. - pub fn added_assets(&self) -> impl Iterator + '_ { - self.fungible - .0 - .iter() - .filter(|&(_, &value)| value >= 0) - .map(|(asset_id, &diff)| { - Asset::Fungible( - FungibleAsset::new(asset_id.faucet_id(), diff.unsigned_abs()).unwrap(), - ) - }) - .chain( - self.non_fungible - .filter_by_action(NonFungibleDeltaAction::Add) - .map(Asset::NonFungible), - ) + pub fn added_assets(&self) -> impl Iterator + '_ { + self.filter_by_op(AssetDeltaOperation::Add) } /// Returns an iterator over the removed assets in this delta. - pub fn removed_assets(&self) -> impl Iterator + '_ { - self.fungible - .0 - .iter() - .filter(|&(_, &value)| value < 0) - .map(|(asset_id, &diff)| { - Asset::Fungible( - FungibleAsset::new(asset_id.faucet_id(), diff.unsigned_abs()).unwrap(), - ) - }) - .chain( - self.non_fungible - .filter_by_action(NonFungibleDeltaAction::Remove) - .map(Asset::NonFungible), - ) + pub fn removed_assets(&self) -> impl Iterator + '_ { + self.filter_by_op(AssetDeltaOperation::Remove) } /// Appends the vault delta to the given `elements` from which the delta commitment will be /// computed. pub(super) fn append_delta_elements(&self, elements: &mut Vec) { - // Add added and removed assets to a map to sort by asset ID. + self.append_asset_section(AssetDeltaOperation::Add, elements); + self.append_asset_section(AssetDeltaOperation::Remove, elements); + } - // TODO(unified_delta): Refactor the internal asset delta structure to match the tx kernel - // internals and to make this extra allocation unnecessary. - let added_assets = BTreeMap::from_iter( - self.added_assets().map(|asset| (asset.id(), asset.to_value_word())), - ); - let removed_assets = BTreeMap::from_iter( - self.removed_assets().map(|asset| (asset.id(), asset.to_value_word())), - ); + // HELPER FUNCTIONS + // --------------------------------------------------------------------------------------------- - Self::add_asset_section(AssetDeltaOperation::Add, added_assets, elements); - Self::add_asset_section(AssetDeltaOperation::Remove, removed_assets, elements); + /// Returns an iterator over all assets that were changed by the provided operation. + fn filter_by_op(&self, delta_op: AssetDeltaOperation) -> impl Iterator + '_ { + self.delta + .values() + .filter(move |asset_delta| asset_delta.delta_op() == delta_op) + .map(AssetDelta::asset) } - fn add_asset_section( - delta_op: AssetDeltaOperation, - assets: BTreeMap, - elements: &mut Vec, - ) { - let num_changed_assets = assets.len(); - for (asset_id, asset_value) in assets { - elements.extend_from_slice(asset_id.to_word().as_elements()); - elements.extend_from_slice(asset_value.as_elements()); + /// Appends the assets changed by the provided operation, followed by the section's trailer. + /// + /// The trailer is omitted if the operation did not change any asset. + fn append_asset_section(&self, delta_op: AssetDeltaOperation, elements: &mut Vec) { + let mut num_changed_assets = 0; + for asset in self.filter_by_op(delta_op) { + elements.extend_from_slice(&asset.as_elements()); + num_changed_assets += 1; } if num_changed_assets != 0 { @@ -156,380 +178,96 @@ impl AccountVaultDelta { } } -#[cfg(any(feature = "testing", test))] -impl AccountVaultDelta { - /// Creates an [AccountVaultDelta] from the given iterators. - pub fn from_iters( - added_assets: impl IntoIterator, - removed_assets: impl IntoIterator, - ) -> Self { - let mut fungible = FungibleAssetDelta::default(); - let mut non_fungible = NonFungibleAssetDelta::default(); - - for asset in added_assets { - match asset { - Asset::Fungible(asset) => { - fungible.add(asset).unwrap(); - }, - Asset::NonFungible(asset) => { - non_fungible.add(asset).unwrap(); - }, - } - } - - for asset in removed_assets { - match asset { - Asset::Fungible(asset) => { - fungible.remove(asset).unwrap(); - }, - Asset::NonFungible(asset) => { - non_fungible.remove(asset).unwrap(); - }, - } - } - - Self { fungible, non_fungible } - } -} - impl Serializable for AccountVaultDelta { fn write_into(&self, target: &mut W) { - target.write(&self.fungible); - target.write(&self.non_fungible); - } - - fn get_size_hint(&self) -> usize { - self.fungible.get_size_hint() + self.non_fungible.get_size_hint() - } -} - -impl Deserializable for AccountVaultDelta { - fn read_from(source: &mut R) -> Result { - let fungible = source.read()?; - let non_fungible = source.read()?; - - Ok(Self::new(fungible, non_fungible)) - } -} - -// FUNGIBLE ASSET DELTA -// ================================================================================================ - -/// A binary tree map of fungible asset balance changes in the account vault. -/// -/// The [`AssetId`] orders the assets in the same way as the in-kernel account delta which -/// uses a link map. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct FungibleAssetDelta(BTreeMap); - -impl FungibleAssetDelta { - /// Validates and creates a new fungible asset delta. - /// - /// # Errors - /// Returns an error if the delta does not pass the validation. - pub fn new(map: BTreeMap) -> Result { - Self::validate(&map)?; - - Ok(Self(map)) - } - - /// Adds a new fungible asset to the delta. - /// - /// # Errors - /// Returns an error if the delta would overflow. - pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> { - let amount: i64 = asset.amount().as_i64(); - self.add_delta(asset.id(), amount) - } - - /// Removes a fungible asset from the delta. - /// - /// # Errors - /// Returns an error if the delta would overflow. - pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> { - let amount: i64 = asset.amount().as_i64(); - self.add_delta(asset.id(), -amount) - } - - /// Returns the amount of the fungible asset with the given asset ID. - pub fn amount(&self, asset_id: &AssetId) -> Option { - self.0.get(asset_id).copied() - } - - /// Returns the number of fungible assets affected in the delta. - pub fn num_assets(&self) -> usize { - self.0.len() - } - - /// Returns true if this vault delta contains no updates. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Returns an iterator over the (key, value) pairs of the map. - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } - - // HELPER FUNCTIONS - // --------------------------------------------------------------------------------------------- - - /// Updates the provided map with the provided key and amount. If the final amount is 0, - /// the entry is removed. - /// - /// # Errors - /// Returns an error if the delta would overflow. - fn add_delta(&mut self, asset_id: AssetId, delta: i64) -> Result<(), AccountDeltaError> { - match self.0.entry(asset_id) { - Entry::Vacant(entry) => { - // Only track non-zero amounts. - if delta != 0 { - entry.insert(delta); - } - }, - Entry::Occupied(mut entry) => { - let old = *entry.get(); - let new = old.checked_add(delta).ok_or( - AccountDeltaError::FungibleAssetDeltaOverflow { - faucet_id: asset_id.faucet_id(), - current: old, - delta, - }, - )?; - - if new == 0 { - entry.remove(); - } else { - *entry.get_mut() = new; - } - }, - } - - Ok(()) - } - - /// Checks whether this vault delta is valid. - /// - /// # Errors - /// Returns an error if one or more fungible assets' faucet IDs are invalid. - fn validate(map: &BTreeMap) -> Result<(), AccountDeltaError> { - for asset_id in map.keys() { - if !asset_id.composition().is_fungible() { - return Err(AccountDeltaError::NotAFungibleFaucetId(asset_id.faucet_id())); - } - } - - Ok(()) - } -} - -impl Serializable for FungibleAssetDelta { - fn write_into(&self, target: &mut W) { - target.write_usize(self.0.len()); - // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. - // We should update this code (and deserialization as well) once it supports signed - // integers. - target.write_many(self.0.iter().map(|(asset_id, &delta)| (*asset_id, delta as u64))); - } - - fn get_size_hint(&self) -> usize { - let entries_size: usize = self - .0 - .keys() - .map(|id| { - // amount is serialized as a u64 - id.get_size_hint() + core::mem::size_of::() - }) - .sum(); - - self.0.len().get_size_hint() + entries_size - } -} - -impl Deserializable for FungibleAssetDelta { - fn read_from(source: &mut R) -> Result { - let num_fungible_assets = source.read_usize()?; - // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. - // We should update this code (and serialization as well) once it supports signed - // integers. - let map = source - .read_many_iter::<(AssetId, u64)>(num_fungible_assets)? - .map(|result| result.map(|(asset_id, delta_as_u64)| (asset_id, delta_as_u64 as i64))) - .collect::>()?; - - Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} + target.write_usize(self.added_assets().count()); + target.write_many(self.added_assets()); -// NON-FUNGIBLE ASSET DELTA -// ================================================================================================ - -/// A binary tree map of non-fungible asset changes (addition and removal) in the account vault. -/// -/// The [`AssetId`] orders the assets in the same way as the in-kernel account delta which -/// uses a link map. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct NonFungibleAssetDelta(BTreeMap); - -impl NonFungibleAssetDelta { - /// Creates a new non-fungible asset delta. - pub const fn new(map: BTreeMap) -> Self { - Self(map) - } - - /// Adds a new non-fungible asset to the delta. - /// - /// # Errors - /// Returns an error if the delta already contains the asset addition. - pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> { - self.apply_action(asset, NonFungibleDeltaAction::Add) - } - - /// Removes a non-fungible asset from the delta. - /// - /// # Errors - /// Returns an error if the delta already contains the asset removal. - pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> { - self.apply_action(asset, NonFungibleDeltaAction::Remove) - } - - /// Returns the number of non-fungible assets affected in the delta. - pub fn num_assets(&self) -> usize { - self.0.len() - } - - /// Returns true if this vault delta contains no updates. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Returns an iterator over the (key, value) pairs of the map. - pub fn iter(&self) -> impl Iterator { - self.0 - .iter() - .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action)) - } - - // HELPER FUNCTIONS - // --------------------------------------------------------------------------------------------- - - /// Updates the provided map with the provided key and action. - /// If the action is the opposite to the previous one, the entry is removed. - /// - /// # Errors - /// Returns an error if the delta already contains the provided key and action. - fn apply_action( - &mut self, - asset: NonFungibleAsset, - action: NonFungibleDeltaAction, - ) -> Result<(), AccountDeltaError> { - match self.0.entry(asset.id()) { - Entry::Vacant(entry) => { - entry.insert((asset, action)); - }, - Entry::Occupied(entry) => { - let (_prev_asset, previous_action) = *entry.get(); - if previous_action == action { - // Asset cannot be added nor removed twice. - return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset)); - } - // Otherwise they cancel out. - entry.remove(); - }, - } - - Ok(()) - } - - /// Returns an iterator over all keys that have the provided action. - fn filter_by_action( - &self, - action: NonFungibleDeltaAction, - ) -> impl Iterator + '_ { - self.0 - .iter() - .filter(move |&(_, (_asset, cur_action))| cur_action == &action) - .map(|(_key, (asset, _action))| *asset) - } -} - -impl Serializable for NonFungibleAssetDelta { - fn write_into(&self, target: &mut W) { - let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect(); - let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect(); - - target.write_usize(added.len()); - target.write_many(added.iter()); - - target.write_usize(removed.len()); - target.write_many(removed.iter()); + target.write_usize(self.removed_assets().count()); + target.write_many(self.removed_assets()); } fn get_size_hint(&self) -> usize { - let added = self.filter_by_action(NonFungibleDeltaAction::Add).count(); - let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count(); + let added_size: usize = self.added_assets().map(|asset| asset.get_size_hint()).sum(); + let removed_size: usize = self.removed_assets().map(|asset| asset.get_size_hint()).sum(); - added.get_size_hint() - + removed.get_size_hint() - + added * NonFungibleAsset::SERIALIZED_SIZE - + removed * NonFungibleAsset::SERIALIZED_SIZE + 2 * 0usize.get_size_hint() + added_size + removed_size } } -impl Deserializable for NonFungibleAssetDelta { +impl Deserializable for AccountVaultDelta { fn read_from(source: &mut R) -> Result { - let mut map = BTreeMap::new(); - - let num_added = source.read_usize()?; - for _ in 0..num_added { - let added_asset: NonFungibleAsset = source.read()?; - map.insert(added_asset.id(), (added_asset, NonFungibleDeltaAction::Add)); + let num_added_assets = source.read_usize()?; + // The capacity is not reserved upfront since the number of assets is not yet validated + // against the remaining bytes at this point. + let mut asset_deltas = Vec::new(); + for asset in source.read_many_iter::(num_added_assets)? { + asset_deltas.push(AssetDelta::new(AssetDeltaOperation::Add, asset?)); } - let num_removed = source.read_usize()?; - for _ in 0..num_removed { - let removed_asset: NonFungibleAsset = source.read()?; - map.insert(removed_asset.id(), (removed_asset, NonFungibleDeltaAction::Remove)); + let num_removed_assets = source.read_usize()?; + for asset in source.read_many_iter::(num_removed_assets)? { + asset_deltas.push(AssetDelta::new(AssetDeltaOperation::Remove, asset?)); } - Ok(Self::new(map)) + Self::new(asset_deltas).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum NonFungibleDeltaAction { - Add, - Remove, -} - // TESTS // ================================================================================================ #[cfg(test)] mod tests { - use super::{AccountVaultDelta, Deserializable, Serializable}; - use crate::account::AccountId; - use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; - use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; + use alloc::string::ToString; + use alloc::vec::Vec; + + use assert_matches::assert_matches; + + use super::{AccountVaultDelta, Deserializable, DeserializationError, Serializable}; + use crate::asset::{FungibleAsset, NonFungibleAsset}; + use crate::errors::AccountDeltaError; + use crate::utils::serde::ByteWriter; #[test] - fn test_serde_account_vault() { - let asset_0 = FungibleAsset::mock(100); - let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]); - let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]); + fn account_vault_delta_serde() -> anyhow::Result<()> { + let empty_delta = AccountVaultDelta::default(); + assert!(empty_delta.is_empty()); + let serialized = empty_delta.to_bytes(); + assert_eq!(AccountVaultDelta::read_from_bytes(&serialized)?, empty_delta); + assert_eq!(empty_delta.get_size_hint(), serialized.len()); + + let delta = AccountVaultDelta::from_iters( + [FungibleAsset::mock(100), NonFungibleAsset::mock(&[10, 21, 32, 43])], + [NonFungibleAsset::mock(&[54, 65])], + ); + assert!(!delta.is_empty()); let serialized = delta.to_bytes(); - let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap(); - assert_eq!(deserialized, delta); + assert_eq!(AccountVaultDelta::read_from_bytes(&serialized)?, delta); + assert_eq!(delta.get_size_hint(), serialized.len()); + + Ok(()) } + /// A crafted byte stream that changes the same asset in both the added and the removed section + /// must be rejected rather than silently collapsing into a single entry. #[test] - fn test_is_empty_account_vault() { - let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into(); + fn account_vault_delta_deserialization_rejects_duplicate_asset() -> anyhow::Result<()> { + let asset = NonFungibleAsset::mock(&[10, 21, 32, 43]); + + let mut bytes = Vec::new(); + bytes.write_usize(1); + bytes.write(asset); + bytes.write_usize(1); + bytes.write(asset); + + let error = AccountVaultDelta::read_from_bytes(&bytes) + .expect_err("delta with a duplicate asset should not deserialize"); - assert!(AccountVaultDelta::default().is_empty()); - assert!(!AccountVaultDelta::from_iters([asset], []).is_empty()); - assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty()); + let expected = AccountDeltaError::DuplicateAssetDelta(asset.id()).to_string(); + assert_matches!(error, DeserializationError::InvalidValue(message) if message == expected); + + Ok(()) } } diff --git a/crates/miden-protocol/src/account/mod.rs b/crates/miden-protocol/src/account/mod.rs index 35b3f9b5ca..182564631c 100644 --- a/crates/miden-protocol/src/account/mod.rs +++ b/crates/miden-protocol/src/account/mod.rs @@ -1,7 +1,8 @@ use alloc::string::ToString; use alloc::vec::Vec; -use crate::asset::{Asset, AssetVault}; +use crate::account::delta::AssetDeltaOperation; +use crate::asset::AssetVault; use crate::crypto::SequentialCommit; use crate::errors::AccountError; use crate::utils::serde::{ @@ -58,13 +59,7 @@ pub use patch::{ }; pub mod delta; -pub use delta::{ - AccountDelta, - AccountVaultDelta, - FungibleAssetDelta, - NonFungibleAssetDelta, - NonFungibleDeltaAction, -}; +pub use delta::{AccountDelta, AccountVaultDelta, AssetDelta}; pub mod storage; pub use storage::{ @@ -417,24 +412,11 @@ impl TryFrom for AccountDelta { let storage_patch = AccountStoragePatch::from_raw(slot_deltas) .expect("number of slot patches is bounded by the account's storage slots"); - 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); + // SAFETY: The assets in the account vault are unique, so no asset is changed twice. + let vault_delta = AccountVaultDelta::new( + vault.assets().map(|asset| AssetDelta::new(AssetDeltaOperation::Add, asset)), + ) + .expect("assets in the account vault should be unique"); // The nonce of the account is the nonce delta since adding the nonce_delta to 0 would // result in the nonce. diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index a35474397d..79a8ceea06 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -41,6 +41,7 @@ use crate::note::{ NoteType, Nullifier, }; +use crate::script::MastForestScriptError; use crate::transaction::TransactionId; use crate::utils::serde::DeserializationError; use crate::vm::EventId; @@ -417,16 +418,8 @@ pub enum NetworkIdError { pub enum AccountDeltaError { #[error("storage slot {0} was used as different slot types")] StorageSlotUsedAsDifferentTypes(StorageSlotName), - #[error("non fungible vault can neither be added nor removed twice")] - DuplicateNonFungibleVaultUpdate(NonFungibleAsset), - #[error( - "fungible asset issued by faucet {faucet_id} has delta {delta} which overflows when added to current value {current}" - )] - FungibleAssetDeltaOverflow { - faucet_id: AccountId, - current: i64, - delta: i64, - }, + #[error("asset {0} is changed by more than one asset delta")] + DuplicateAssetDelta(AssetId), #[error( "account update of type `{left_update_type}` cannot be merged with account update of type `{right_update_type}`" )] @@ -441,10 +434,6 @@ pub enum AccountDeltaError { }, #[error("non-empty account storage or vault delta with zero nonce delta is not allowed")] NonEmptyStorageOrVaultDeltaWithZeroNonceDelta, - #[error( - "asset issued by faucet {0} in fungible asset delta does not have fungible composition" - )] - NotAFungibleFaucetId(AccountId), #[error("cannot merge two full state deltas")] MergingFullStateDeltas, #[error("a full state delta must only contain storage create operations")] @@ -724,14 +713,8 @@ pub enum PartialAssetVaultError { #[derive(Debug, Error)] pub enum NoteError { - #[error("package does not contain a procedure with @note_script attribute")] - NoteScriptNoProcedureWithAttribute, - #[error("package contains multiple procedures with @note_script attribute")] - NoteScriptMultipleProceduresWithAttribute, - #[error("procedure at path '{0}' not found in package")] - NoteScriptProcedureNotFound(Box), - #[error("procedure at path '{0}' does not have @note_script attribute")] - NoteScriptProcedureMissingAttribute(Box), + #[error("error while creating note script: {0}")] + MastForestScript(#[source] MastForestScriptError), #[error("note tag length {0} exceeds the maximum of {max}", max = NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)] NoteTagLengthTooLarge(u8), #[error("duplicate fungible asset from issuer {0} in note")] @@ -868,25 +851,6 @@ impl PartialBlockchainError { } } -// TRANSACTION SCRIPT ERROR -// ================================================================================================ - -#[derive(Debug, Error)] -pub enum TransactionScriptError { - #[error("failed to assemble transaction script:\n{}", PrintDiagnostic::new(.0))] - AssemblyError(Report), - #[error("failed to convert package to transaction script:\n{}", PrintDiagnostic::new(.0))] - PackageNotProgram(Report), - #[error("package does not contain a procedure with @transaction_script attribute")] - NoProcedureWithAttribute, - #[error("package contains multiple procedures with @transaction_script attribute")] - MultipleProceduresWithAttribute, - #[error("procedure at path '{0}' not found in package")] - ProcedureNotFound(Box), - #[error("procedure at path '{0}' does not have @transaction_script attribute")] - ProcedureMissingAttribute(Box), -} - // TRANSACTION INPUT ERROR // ================================================================================================ diff --git a/crates/miden-protocol/src/lib.rs b/crates/miden-protocol/src/lib.rs index 77346cd40d..68c02aa62f 100644 --- a/crates/miden-protocol/src/lib.rs +++ b/crates/miden-protocol/src/lib.rs @@ -15,6 +15,7 @@ pub mod errors; pub mod note; pub mod package; mod protocol; +pub(crate) mod script; pub mod transaction; #[cfg(any(feature = "testing", test))] @@ -34,6 +35,7 @@ pub use miden_crypto::hash::poseidon2::Poseidon2 as Hasher; pub use miden_crypto::word; pub use miden_crypto::word::{Word, WordError}; pub use protocol::ProtocolLib; +pub use script::MastForestScriptError; pub mod assembly { pub use miden_assembly::ast::{Module, ModuleKind, ProcedureName, QualifiedProcedureName}; diff --git a/crates/miden-protocol/src/note/script.rs b/crates/miden-protocol/src/note/script.rs index 5005ec7b9a..12b7659341 100644 --- a/crates/miden-protocol/src/note/script.rs +++ b/crates/miden-protocol/src/note/script.rs @@ -7,15 +7,13 @@ use core::num::TryFromIntError; use miden_core::mast::MastNodeExt; use miden_crypto_derive::WordWrapper; use miden_mast_package::Package; -use miden_mast_package::debug_info::PackageDebugInfo; use miden_processor::LoadedMastForest; use super::Felt; use crate::assembly::Path; use crate::assembly::mast::{MastForest, MastNodeId}; use crate::errors::NoteError; -use crate::package::{loaded_mast_forest, package_debug_info}; -use crate::utils::create_external_node_forest; +use crate::script::MastForestScript; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -73,11 +71,7 @@ impl Deserializable for NoteScriptRoot { /// A note's script represents a program which must be executed for a note to be consumed. As such /// it defines the rules and side effects of consuming a given note. #[derive(Debug, Clone)] -pub struct NoteScript { - mast: Arc, - entrypoint: MastNodeId, - package_debug_info: Option>, -} +pub struct NoteScript(MastForestScript); impl NoteScript { // CONSTRUCTORS @@ -96,12 +90,7 @@ impl NoteScript { /// # Panics /// Panics if the specified entrypoint is not in the provided MAST forest. pub fn from_parts(mast: Arc, entrypoint: MastNodeId) -> Self { - assert!(mast.get_node_by_id(entrypoint).is_some()); - Self { - mast, - entrypoint, - package_debug_info: None, - } + Self(MastForestScript::from_parts(mast, entrypoint)) } /// Returns a new [NoteScript] instantiated from the provided package. @@ -114,29 +103,9 @@ impl NoteScript { /// - The package does not contain a procedure with the `@note_script` attribute. /// - The package contains multiple procedures with the `@note_script` attribute. pub fn from_package(package: &Package) -> Result { - let mut entrypoint = None; - - for export in package.manifest.exports() { - if let Some(proc_export) = export.as_procedure() { - // Check for @note_script attribute - if proc_export.attributes.has(NOTE_SCRIPT_ATTRIBUTE) { - if entrypoint.is_some() { - return Err(NoteError::NoteScriptMultipleProceduresWithAttribute); - } - entrypoint = Some( - proc_export.node.ok_or(NoteError::NoteScriptNoProcedureWithAttribute)?, - ); - } - } - } - - let entrypoint = entrypoint.ok_or(NoteError::NoteScriptNoProcedureWithAttribute)?; - - Ok(Self { - mast: package.mast_forest().clone(), - entrypoint, - package_debug_info: package_debug_info(package), - }) + let script = MastForestScript::from_package(package, NOTE_SCRIPT_ATTRIBUTE) + .map_err(NoteError::MastForestScript)?; + Ok(Self(script)) } /// Returns a new [NoteScript] containing only a reference to a procedure in the provided @@ -157,33 +126,9 @@ impl NoteScript { /// - The package does not contain a procedure at the specified path. /// - The procedure at the specified path does not have the `@note_script` attribute. pub fn from_package_reference(package: &Package, path: &Path) -> Result { - // Find the export matching the path - let export = package - .manifest - .exports() - .find(|e| e.path().as_ref() == path) - .ok_or_else(|| NoteError::NoteScriptProcedureNotFound(path.to_string().into()))?; - - // Get the procedure export and verify it has the @note_script attribute - let proc_export = export - .as_procedure() - .ok_or_else(|| NoteError::NoteScriptProcedureNotFound(path.to_string().into()))?; - - if !proc_export.attributes.has(NOTE_SCRIPT_ATTRIBUTE) { - return Err(NoteError::NoteScriptProcedureMissingAttribute(path.to_string().into())); - } - - // Get the digest of the procedure from the package - let digest = proc_export.digest; - - // Create a minimal MastForest with just an external node referencing the digest - let (mast, entrypoint) = create_external_node_forest(digest); - - Ok(Self { - mast: Arc::new(mast), - entrypoint, - package_debug_info: package_debug_info(package), - }) + let script = MastForestScript::from_package_reference(package, path, NOTE_SCRIPT_ATTRIBUTE) + .map_err(NoteError::MastForestScript)?; + Ok(Self(script)) } // PUBLIC ACCESSORS @@ -191,27 +136,27 @@ impl NoteScript { /// Returns the commitment of this note script (i.e., the script's MAST root). pub fn root(&self) -> NoteScriptRoot { - NoteScriptRoot::from_raw(self.mast[self.entrypoint].digest()) + NoteScriptRoot::from_raw(self.0.digest()) } /// Returns a reference to the [MastForest] backing this note script. pub fn mast(&self) -> Arc { - self.mast.clone() + self.0.mast() } /// Returns the MAST forest and package-owned debug information backing this note script. pub fn loaded_mast_forest(&self) -> LoadedMastForest { - loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone()) + self.0.loaded_mast_forest() } /// Returns an entrypoint node ID of the current script. pub fn entrypoint(&self) -> MastNodeId { - self.entrypoint + self.0.entrypoint() } /// Removes debug info from this note script, if any. pub fn clear_debug_info(&mut self) { - self.package_debug_info = None; + self.0.clear_debug_info(); } /// Returns a new [NoteScript] with the provided advice map entries merged into the @@ -220,22 +165,13 @@ impl NoteScript { /// This allows adding advice map entries to an already-compiled note script, /// which is useful when the entries are determined after script compilation. pub fn with_advice_map(self, advice_map: AdviceMap) -> Self { - if advice_map.is_empty() { - return self; - } - - let mast = (*self.mast).clone().with_advice_map(advice_map); - Self { - mast: Arc::new(mast), - entrypoint: self.entrypoint, - package_debug_info: self.package_debug_info, - } + Self(self.0.with_advice_map(advice_map)) } } impl PartialEq for NoteScript { fn eq(&self, other: &Self) -> bool { - self.mast == other.mast && self.entrypoint == other.entrypoint + self.0 == other.0 } } @@ -246,7 +182,7 @@ impl Eq for NoteScript {} impl From<&NoteScript> for Vec { fn from(script: &NoteScript) -> Self { - let mut bytes = script.mast.to_bytes(); + let mut bytes = script.0.mast().to_bytes(); let len = bytes.len(); // Pad the data so that it can be encoded with u32 @@ -257,7 +193,7 @@ impl From<&NoteScript> for Vec { let mut result = Vec::with_capacity(final_size); // Push the length, this is used to remove the padding later - result.push(Felt::from(u32::from(script.entrypoint))); + result.push(Felt::from(u32::from(script.0.entrypoint()))); result.push(Felt::new_unchecked(len as u64)); // A Felt can not represent all u64 values, so the data is encoded using u32. @@ -334,27 +270,17 @@ impl TryFrom> for NoteScript { impl Serializable for NoteScript { fn write_into(&self, target: &mut W) { - self.mast.write_into(target); - target.write_u32(u32::from(self.entrypoint)); + self.0.write_into(target); } fn get_size_hint(&self) -> usize { - // TODO: this is a temporary workaround. Replace mast.to_bytes().len() with - // MastForest::get_size_hint() (or a similar size-hint API) once it becomes - // available. - let mast_size = self.mast.to_bytes().len(); - let u32_size = 0u32.get_size_hint(); - - mast_size + u32_size + self.0.get_size_hint() } } impl Deserializable for NoteScript { fn read_from(source: &mut R) -> Result { - let mast = MastForest::read_from(source)?; - let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?; - - Ok(Self::from_parts(Arc::new(mast), entrypoint)) + Ok(Self(MastForestScript::read_from(source)?)) } } @@ -364,7 +290,8 @@ impl Deserializable for NoteScript { impl PrettyPrint for NoteScript { fn render(&self) -> miden_core::prettier::Document { use miden_core::prettier::*; - let entrypoint = self.mast[self.entrypoint].to_pretty_print(&self.mast); + let mast = self.0.mast(); + let entrypoint = mast[self.0.entrypoint()].to_pretty_print(&mast); indent(4, const_text("begin") + nl() + entrypoint.render()) + nl() + const_text("end") } diff --git a/crates/miden-protocol/src/script.rs b/crates/miden-protocol/src/script.rs new file mode 100644 index 0000000000..02bb2b278f --- /dev/null +++ b/crates/miden-protocol/src/script.rs @@ -0,0 +1,246 @@ +use alloc::boxed::Box; +use alloc::string::ToString; +use alloc::sync::Arc; + +use miden_assembly::Report; +use miden_assembly::diagnostics::reporting::PrintDiagnostic; +use miden_core::mast::MastNodeExt; +use miden_mast_package::Package; +use miden_mast_package::debug_info::PackageDebugInfo; +use miden_processor::LoadedMastForest; +use thiserror::Error; + +use crate::assembly::Path; +use crate::package::{loaded_mast_forest, package_debug_info}; +use crate::utils::create_external_node_forest; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; +use crate::vm::AdviceMap; +use crate::{MastForest, MastNodeId, Word}; + +// MAST FOREST SCRIPT ERROR +// ================================================================================================ + +/// Errors that can occur while resolving a `MastForestScript` from a package. +#[derive(Debug, Error)] +pub enum MastForestScriptError { + #[error("package does not contain a procedure with '@{0}' attribute")] + NoProcedureWithAttribute(Box), + #[error("package contains multiple procedures with '@{0}' attribute")] + MultipleProceduresWithAttribute(Box), + #[error("procedure at path '{0}' not found in package")] + ProcedureNotFound(Box), + #[error("procedure at path '{0}' does not have the specified attribute")] + ProcedureMissingAttribute(Box), + #[error("failed to convert package to a program:\n{}", PrintDiagnostic::new(.0))] + PackageNotProgram(Report), +} + +// MAST FOREST SCRIPT +// ================================================================================================ + +/// An executable program backed by a [MastForest] and a designated entrypoint. +/// +/// A [MastForestScript] consists of a [MastForest], a reference to the node in the forest at +/// which execution begins (the entrypoint), and optional package-owned debug information. It is the +/// shared core of [`NoteScript`](crate::note::NoteScript) and +/// [`TransactionScript`](crate::transaction::TransactionScript). +#[derive(Debug, Clone)] +pub(crate) struct MastForestScript { + mast: Arc, + entrypoint: MastNodeId, + package_debug_info: Option>, +} + +impl MastForestScript { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Returns a new [MastForestScript] instantiated from the provided components. + /// + /// # Panics + /// Panics if the specified entrypoint is not in the provided MAST forest. + pub fn from_parts(mast: Arc, entrypoint: MastNodeId) -> Self { + assert!(mast.get_node_by_id(entrypoint).is_some()); + Self { + mast, + entrypoint, + package_debug_info: None, + } + } + + /// Returns a new [MastForestScript] instantiated from the provided components and the + /// package-owned debug information of the provided package. + pub(crate) fn from_parts_with_package_debug_info( + package: &Package, + mast: Arc, + entrypoint: MastNodeId, + ) -> Self { + Self { + mast, + entrypoint, + package_debug_info: package_debug_info(package), + } + } + + /// Returns a new [MastForestScript] instantiated from the provided package. + /// + /// The package must contain exactly one procedure with the specified `attribute`, which is used + /// as the entrypoint. + pub(crate) fn from_package( + package: &Package, + attribute: &str, + ) -> Result { + let mut entrypoint = None; + + for export in package.manifest.exports() { + if let Some(proc_export) = export.as_procedure() + && proc_export.attributes.has(attribute) + { + if entrypoint.is_some() { + return Err(MastForestScriptError::MultipleProceduresWithAttribute( + attribute.into(), + )); + } + entrypoint = Some(proc_export.node.ok_or_else(|| { + MastForestScriptError::NoProcedureWithAttribute(attribute.into()) + })?); + } + } + + let entrypoint = entrypoint + .ok_or_else(|| MastForestScriptError::NoProcedureWithAttribute(attribute.into()))?; + + Ok(Self { + mast: package.mast_forest().clone(), + entrypoint, + package_debug_info: package_debug_info(package), + }) + } + + /// Returns a new [MastForestScript] containing only a reference to a procedure in the provided + /// package. + /// + /// The procedure at the specified path must have the given `attribute`. + /// + /// Note: This creates a minimal [MastForest] containing only an external node referencing the + /// procedure's digest, rather than copying the entire package. The actual procedure code is + /// resolved at runtime via the `MastForestStore`. + pub(crate) fn from_package_reference( + package: &Package, + path: &Path, + attribute: &str, + ) -> Result { + let export = package + .manifest + .exports() + .find(|e| e.path().as_ref() == path) + .ok_or_else(|| MastForestScriptError::ProcedureNotFound(path.to_string().into()))?; + + let proc_export = export + .as_procedure() + .ok_or_else(|| MastForestScriptError::ProcedureNotFound(path.to_string().into()))?; + + if !proc_export.attributes.has(attribute) { + return Err(MastForestScriptError::ProcedureMissingAttribute(path.to_string().into())); + } + + let digest = proc_export.digest; + + let (mast, entrypoint) = create_external_node_forest(digest); + + Ok(Self { + mast: Arc::new(mast), + entrypoint, + package_debug_info: package_debug_info(package), + }) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the [MastForest] backing this program. + pub fn mast(&self) -> Arc { + self.mast.clone() + } + + /// Returns the MAST forest and package-owned debug information backing this program. + pub fn loaded_mast_forest(&self) -> LoadedMastForest { + loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone()) + } + + /// Returns the digest of the entrypoint node of this program (i.e., its MAST root). + pub fn digest(&self) -> Word { + self.mast[self.entrypoint].digest() + } + + /// Returns the entrypoint node ID of this program. + pub fn entrypoint(&self) -> MastNodeId { + self.entrypoint + } + + /// Removes debug info from this program, if any. + pub fn clear_debug_info(&mut self) { + self.package_debug_info = None; + } + + /// Returns a new [MastForestScript] with the provided advice map entries merged into the + /// underlying [MastForest]. + /// + /// This allows adding advice map entries to an already-compiled program, which is useful when + /// the entries are determined after compilation. + pub fn with_advice_map(self, advice_map: AdviceMap) -> Self { + if advice_map.is_empty() { + return self; + } + + let mast = (*self.mast).clone().with_advice_map(advice_map); + Self { + mast: Arc::new(mast), + entrypoint: self.entrypoint, + package_debug_info: self.package_debug_info, + } + } +} + +impl PartialEq for MastForestScript { + fn eq(&self, other: &Self) -> bool { + self.mast == other.mast && self.entrypoint == other.entrypoint + } +} + +impl Eq for MastForestScript {} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for MastForestScript { + fn write_into(&self, target: &mut W) { + self.mast.write_into(target); + target.write_u32(u32::from(self.entrypoint)); + } + + fn get_size_hint(&self) -> usize { + // TODO: this is a temporary workaround. Replace mast.to_bytes().len() with + // MastForest::get_size_hint() (or a similar size-hint API) once it becomes + // available. + let mast_size = self.mast.to_bytes().len(); + let u32_size = 0u32.get_size_hint(); + + mast_size + u32_size + } +} + +impl Deserializable for MastForestScript { + fn read_from(source: &mut R) -> Result { + let mast = MastForest::read_from(source)?; + let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?; + + Ok(Self::from_parts(Arc::new(mast), entrypoint)) + } +} diff --git a/crates/miden-protocol/src/testing/mod.rs b/crates/miden-protocol/src/testing/mod.rs index 56c18ee0e7..8e27efe39d 100644 --- a/crates/miden-protocol/src/testing/mod.rs +++ b/crates/miden-protocol/src/testing/mod.rs @@ -19,4 +19,5 @@ pub mod storage_map_key; pub mod tx; pub mod update_details; pub mod validator_keys; +pub mod vault_delta; pub mod vault_patch; diff --git a/crates/miden-protocol/src/testing/vault_delta.rs b/crates/miden-protocol/src/testing/vault_delta.rs new file mode 100644 index 0000000000..c05e5f5f07 --- /dev/null +++ b/crates/miden-protocol/src/testing/vault_delta.rs @@ -0,0 +1,28 @@ +use crate::account::delta::AssetDeltaOperation; +use crate::account::{AccountVaultDelta, AssetDelta}; +use crate::asset::Asset; + +impl AccountVaultDelta { + // CONSTRUCTORS + // ---------------------------------------------------------------------------------------- + + /// Creates an [`AccountVaultDelta`] from the given iterators. + /// + /// # Panics + /// + /// Panics if the same asset is changed by more than one delta. + pub fn from_iters( + added_assets: impl IntoIterator, + removed_assets: impl IntoIterator, + ) -> Self { + Self::new( + added_assets + .into_iter() + .map(|added_asset| AssetDelta::new(AssetDeltaOperation::Add, added_asset)) + .chain(removed_assets.into_iter().map(|removed_asset| { + AssetDelta::new(AssetDeltaOperation::Remove, removed_asset) + })), + ) + .expect("duplicate entries passed to AccountVaultDelta::from_iters") + } +} diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index 2b5ba60afd..07ccec9210 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -11,6 +11,7 @@ mod ordered_transactions; mod outputs; mod partial_blockchain; mod proven_tx; +mod script; mod transaction_id; mod tx_args; mod tx_header; @@ -34,13 +35,9 @@ pub use outputs::{ }; pub use partial_blockchain::PartialBlockchain; pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate}; +pub use script::{TRANSACTION_SCRIPT_ATTRIBUTE, TransactionScript, TransactionScriptRoot}; pub use transaction_id::TransactionId; -pub use tx_args::{ - TRANSACTION_SCRIPT_ATTRIBUTE, - TransactionArgs, - TransactionScript, - TransactionScriptRoot, -}; +pub use tx_args::TransactionArgs; pub use tx_header::TransactionHeader; pub use tx_summary::{TransactionSummary, TransactionSummaryUserParams}; pub use verifier::TransactionVerifier; diff --git a/crates/miden-protocol/src/transaction/script.rs b/crates/miden-protocol/src/transaction/script.rs new file mode 100644 index 0000000000..a37161015a --- /dev/null +++ b/crates/miden-protocol/src/transaction/script.rs @@ -0,0 +1,353 @@ +use alloc::sync::Arc; +use core::fmt::Display; + +use miden_crypto_derive::WordWrapper; +use miden_mast_package::Package; +use miden_processor::LoadedMastForest; + +use crate::Word; +use crate::assembly::Path; +use crate::assembly::mast::{MastForest, MastNodeId}; +use crate::script::{MastForestScript, MastForestScriptError}; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; +use crate::vm::AdviceMap; + +// TRANSACTION SCRIPT ROOT +// ================================================================================================ + +/// The MAST root of a [`TransactionScript`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)] +pub struct TransactionScriptRoot(Word); + +impl From for Word { + fn from(root: TransactionScriptRoot) -> Self { + root.0 + } +} + +impl Display for TransactionScriptRoot { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Serializable for TransactionScriptRoot { + fn write_into(&self, target: &mut W) { + target.write(self.0); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + } +} + +impl Deserializable for TransactionScriptRoot { + fn read_from(source: &mut R) -> Result { + let word: Word = source.read()?; + Ok(Self::from_raw(word)) + } +} + +// TRANSACTION SCRIPT +// ================================================================================================ + +/// The attribute name used to mark the entrypoint procedure in a transaction script package. +pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script"; + +/// Transaction script. +/// +/// A transaction script is a program that is executed in a transaction after all input notes +/// have been executed. +/// +/// The [TransactionScript] object is composed of an executable program defined by a [MastForest] +/// and an associated entrypoint. +#[derive(Clone, Debug)] +pub struct TransactionScript(MastForestScript); + +impl TransactionScript { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Returns a new [TransactionScript] instantiated from the provided MAST forest and entrypoint. + /// + /// # Panics + /// Panics if the specified entrypoint is not in the provided MAST forest. + pub fn from_parts(mast: Arc, entrypoint: MastNodeId) -> Self { + Self(MastForestScript::from_parts(mast, entrypoint)) + } + + /// Creates a [TransactionScript] from a [`Package`]. + /// + /// If the package is an executable (i.e., its target type is + /// [`TargetType::Executable`](miden_mast_package::TargetType::Executable)), the program's + /// entrypoint is used as the script's entrypoint. Otherwise, the package must contain + /// exactly one procedure with the `@transaction_script` attribute, which will be used as + /// the entrypoint. + /// + /// # Errors + /// Returns an error if: + /// - An executable package cannot be converted to a program. + /// - A library package does not contain a procedure with the `@transaction_script` attribute. + /// - A library package contains multiple procedures with the `@transaction_script` attribute. + pub fn from_package(package: &Package) -> Result { + if package.is_program() { + let program = + package.try_into_program().map_err(MastForestScriptError::PackageNotProgram)?; + + return Ok(Self(MastForestScript::from_parts_with_package_debug_info( + package, + program.mast_forest().clone(), + program.entrypoint(), + ))); + } + + MastForestScript::from_package(package, TRANSACTION_SCRIPT_ATTRIBUTE).map(Self) + } + + /// Returns a new [TransactionScript] containing only a reference to a procedure in the + /// provided package. + /// + /// This method is useful when a package contains multiple transaction scripts and you need + /// to extract a specific one by its fully qualified path (e.g., + /// `::miden::standards::tx_scripts::send_notes::main`). + /// + /// The procedure at the specified path must have the `@transaction_script` attribute. + /// + /// Note: This method creates a minimal [MastForest] containing only an external node + /// referencing the procedure's digest, rather than copying the entire package. The actual + /// procedure code will be resolved at runtime via the `MastForestStore`. + /// + /// # Errors + /// Returns an error if: + /// - The package does not contain a procedure at the specified path. + /// - The procedure at the specified path does not have the `@transaction_script` attribute. + pub fn from_package_reference( + package: &Package, + path: &Path, + ) -> Result { + MastForestScript::from_package_reference(package, path, TRANSACTION_SCRIPT_ATTRIBUTE) + .map(Self) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the [MastForest] backing this transaction script. + pub fn mast(&self) -> Arc { + self.0.mast() + } + + /// Returns the MAST forest and package-owned debug information backing this transaction script. + pub fn loaded_mast_forest(&self) -> LoadedMastForest { + self.0.loaded_mast_forest() + } + + /// Returns the commitment of this transaction script (i.e., the script's MAST root). + pub fn root(&self) -> TransactionScriptRoot { + TransactionScriptRoot::from_raw(self.0.digest()) + } + + /// Returns a new [TransactionScript] with the provided advice map entries merged into the + /// underlying [MastForest]. + /// + /// This allows adding advice map entries to an already-compiled transaction script, + /// which is useful when the entries are determined after script compilation. + pub fn with_advice_map(self, advice_map: AdviceMap) -> Self { + Self(self.0.with_advice_map(advice_map)) + } +} + +impl PartialEq for TransactionScript { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for TransactionScript {} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for TransactionScript { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + } +} + +impl Deserializable for TransactionScript { + fn read_from(source: &mut R) -> Result { + Ok(Self(MastForestScript::read_from(source)?)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_core::advice::AdviceMap; + + use super::TransactionScript; + + #[test] + fn test_transaction_script_preserves_package_debug_info() { + use crate::assembly::Assembler; + + let assembler = Assembler::default(); + let package = + assembler.assemble_program("test-transaction-script", "begin nop end").unwrap(); + let script = TransactionScript::from_package(&package).unwrap(); + + assert!(script.loaded_mast_forest().package_debug_info().unwrap().is_some()); + } + + #[test] + fn test_transaction_script_with_advice_map() { + use miden_core::{Felt, Word}; + + use crate::assembly::Assembler; + + let assembler = Assembler::default(); + let package = + assembler.assemble_program("test-transaction-script", "begin nop end").unwrap(); + let script = TransactionScript::from_package(&package).unwrap(); + assert!(script.mast().advice_map().is_empty()); + + // Empty advice map should be a no-op + let original_root = script.root(); + let script = script.with_advice_map(AdviceMap::default()); + assert_eq!(original_root, script.root()); + + // Non-empty advice map should add entries + let key = Word::from([1u32, 2, 3, 4]); + let value = vec![Felt::new_unchecked(42), Felt::new_unchecked(43)]; + let mut advice_map = AdviceMap::default(); + advice_map.insert(key, value.clone()); + + let script = script.with_advice_map(advice_map); + + let mast = script.mast(); + let stored = mast.advice_map().get(&key).expect("entry should be present"); + assert_eq!(stored.as_ref(), value.as_slice()); + } + + #[test] + fn test_transaction_script_from_library_package() { + use assert_matches::assert_matches; + + use crate::script::MastForestScriptError; + use crate::testing::assembler::assemble_test_package; + use crate::utils::serde::{Deserializable, Serializable}; + + let source = " + @transaction_script + pub proc main + push.1 drop + end + "; + let package = assemble_test_package("test-tx-script", "test::tx_script", source); + + let script = TransactionScript::from_package(&package).unwrap(); + + // the script must round-trip through serialization unchanged + let bytes = script.to_bytes(); + let decoded = TransactionScript::read_from_bytes(&bytes).unwrap(); + assert_eq!(script, decoded); + + // a package without the attribute is rejected + let no_attr = assemble_test_package( + "test-tx-script-no-attr", + "test::tx_script_no_attr", + "pub proc main push.1 drop end", + ); + assert_matches!( + TransactionScript::from_package(&no_attr), + Err(MastForestScriptError::NoProcedureWithAttribute(_)) + ); + + // a package with multiple tagged procedures is rejected + let multiple = assemble_test_package( + "test-tx-script-multiple", + "test::tx_script_multiple", + "@transaction_script pub proc main_a push.1 drop end + @transaction_script pub proc main_b push.2 drop end", + ); + assert_matches!( + TransactionScript::from_package(&multiple), + Err(MastForestScriptError::MultipleProceduresWithAttribute(_)) + ); + } + + #[test] + fn test_transaction_script_from_package_reference() { + use alloc::string::ToString; + + use assert_matches::assert_matches; + + use crate::Word; + use crate::assembly::Path; + use crate::script::MastForestScriptError; + use crate::testing::assembler::assemble_test_package; + + let source = " + @transaction_script + pub proc main_a + push.1 drop + end + + @transaction_script + pub proc main_b + push.2 drop + end + + pub proc helper + push.3 drop + end + "; + let package = + assemble_test_package("test-tx-script-reference", "test::tx_script_reference", source); + + // each tagged procedure can be extracted selectively, and the resulting script's root + // matches the digest of the referenced procedure + for proc_name in ["main_a", "main_b"] { + let export = package + .manifest + .exports() + .find(|e| e.path().as_ref().to_string().ends_with(proc_name)) + .unwrap(); + let digest = export.as_procedure().unwrap().digest; + + let script = + TransactionScript::from_package_reference(&package, export.path().as_ref()) + .unwrap(); + assert_eq!(Word::from(script.root()), digest); + } + + // an unknown path is rejected + assert_matches!( + TransactionScript::from_package_reference(&package, Path::new("::foo::bar::main")), + Err(MastForestScriptError::ProcedureNotFound(_)) + ); + + // a procedure without the attribute is rejected + let helper = package + .manifest + .exports() + .find(|e| e.path().as_ref().to_string().ends_with("helper")) + .unwrap(); + assert_matches!( + TransactionScript::from_package_reference(&package, helper.path().as_ref()), + Err(MastForestScriptError::ProcedureMissingAttribute(_)) + ); + } +} diff --git a/crates/miden-protocol/src/transaction/tx_args.rs b/crates/miden-protocol/src/transaction/tx_args.rs index 0d00fca22b..6678de2c63 100644 --- a/crates/miden-protocol/src/transaction/tx_args.rs +++ b/crates/miden-protocol/src/transaction/tx_args.rs @@ -1,23 +1,13 @@ use alloc::collections::BTreeMap; -use alloc::string::ToString; -use alloc::sync::Arc; use alloc::vec::Vec; -use core::fmt::Display; -use miden_core::mast::MastNodeExt; use miden_crypto::merkle::InnerNodeInfo; -use miden_crypto_derive::WordWrapper; -use miden_mast_package::Package; -use miden_mast_package::debug_info::PackageDebugInfo; -use miden_processor::LoadedMastForest; +use super::script::TransactionScript; use super::{Felt, Hasher, Word}; +use crate::EMPTY_WORD; use crate::account::auth::{PublicKeyCommitment, Signature}; -use crate::assembly::Path; -use crate::errors::TransactionScriptError; use crate::note::{NoteId, NoteRecipient}; -use crate::package::{loaded_mast_forest, package_debug_info}; -use crate::utils::create_external_node_forest; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -26,7 +16,6 @@ use crate::utils::serde::{ Serializable, }; use crate::vm::{AdviceInputs, AdviceMap}; -use crate::{EMPTY_WORD, MastForest, MastNodeId}; // TRANSACTION ARGUMENTS // ================================================================================================ @@ -262,241 +251,9 @@ impl Deserializable for TransactionArgs { } } -// TRANSACTION SCRIPT ROOT +// TESTS // ================================================================================================ -/// The MAST root of a [`TransactionScript`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)] -pub struct TransactionScriptRoot(Word); - -impl From for Word { - fn from(root: TransactionScriptRoot) -> Self { - root.0 - } -} - -impl Display for TransactionScriptRoot { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - Display::fmt(&self.0, f) - } -} - -impl Serializable for TransactionScriptRoot { - fn write_into(&self, target: &mut W) { - target.write(self.0); - } - - fn get_size_hint(&self) -> usize { - self.0.get_size_hint() - } -} - -impl Deserializable for TransactionScriptRoot { - fn read_from(source: &mut R) -> Result { - let word: Word = source.read()?; - Ok(Self::from_raw(word)) - } -} - -// TRANSACTION SCRIPT -// ================================================================================================ - -/// The attribute name used to mark the entrypoint procedure in a transaction script package. -pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script"; - -/// Transaction script. -/// -/// A transaction script is a program that is executed in a transaction after all input notes -/// have been executed. -/// -/// The [TransactionScript] object is composed of an executable program defined by a [MastForest] -/// and an associated entrypoint. -#[derive(Clone, Debug)] -pub struct TransactionScript { - mast: Arc, - entrypoint: MastNodeId, - package_debug_info: Option>, -} - -impl TransactionScript { - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Returns a new [TransactionScript] instantiated from the provided MAST forest and entrypoint. - /// - /// # Panics - /// Panics if the specified entrypoint is not in the provided MAST forest. - pub fn from_parts(mast: Arc, entrypoint: MastNodeId) -> Self { - assert!(mast.get_node_by_id(entrypoint).is_some()); - - Self { - mast, - entrypoint, - package_debug_info: None, - } - } - - /// Creates a [TransactionScript] from a [`Package`]. - /// - /// If the package is an executable (i.e., its target type is - /// [`TargetType::Executable`](miden_mast_package::TargetType::Executable)), the program's - /// entrypoint is used as the script's entrypoint. Otherwise, the package must contain - /// exactly one procedure with the `@transaction_script` attribute, which will be used as - /// the entrypoint. - /// - /// # Errors - /// Returns an error if: - /// - An executable package cannot be converted to a program. - /// - A library package does not contain a procedure with the `@transaction_script` attribute. - /// - A library package contains multiple procedures with the `@transaction_script` attribute. - pub fn from_package(package: &Package) -> Result { - if package.is_program() { - let program = - package.try_into_program().map_err(TransactionScriptError::PackageNotProgram)?; - - return Ok(Self { - mast: program.mast_forest().clone(), - entrypoint: program.entrypoint(), - package_debug_info: package_debug_info(package), - }); - } - - let mut entrypoint = None; - - for export in package.manifest.exports() { - if let Some(proc_export) = export.as_procedure() - && proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE) - { - if entrypoint.is_some() { - return Err(TransactionScriptError::MultipleProceduresWithAttribute); - } - entrypoint = - Some(proc_export.node.ok_or(TransactionScriptError::NoProcedureWithAttribute)?); - } - } - - let entrypoint = entrypoint.ok_or(TransactionScriptError::NoProcedureWithAttribute)?; - - Ok(Self { - mast: package.mast_forest().clone(), - entrypoint, - package_debug_info: package_debug_info(package), - }) - } - - /// Returns a new [TransactionScript] containing only a reference to a procedure in the - /// provided package. - /// - /// This method is useful when a package contains multiple transaction scripts and you need - /// to extract a specific one by its fully qualified path (e.g., - /// `::miden::standards::tx_scripts::send_notes::main`). - /// - /// The procedure at the specified path must have the `@transaction_script` attribute. - /// - /// Note: This method creates a minimal [MastForest] containing only an external node - /// referencing the procedure's digest, rather than copying the entire package. The actual - /// procedure code will be resolved at runtime via the `MastForestStore`. - /// - /// # Errors - /// Returns an error if: - /// - The package does not contain a procedure at the specified path. - /// - The procedure at the specified path does not have the `@transaction_script` attribute. - pub fn from_package_reference( - package: &Package, - path: &Path, - ) -> Result { - // Find the export matching the path - let export = - package.manifest.exports().find(|e| e.path().as_ref() == path).ok_or_else(|| { - TransactionScriptError::ProcedureNotFound(path.to_string().into()) - })?; - - // Get the procedure export and verify it has the @transaction_script attribute - let proc_export = export - .as_procedure() - .ok_or_else(|| TransactionScriptError::ProcedureNotFound(path.to_string().into()))?; - - if !proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE) { - return Err(TransactionScriptError::ProcedureMissingAttribute(path.to_string().into())); - } - - // Get the digest of the procedure from the package - let digest = proc_export.digest; - - // Create a minimal MastForest with just an external node referencing the digest - let (mast, entrypoint) = create_external_node_forest(digest); - - Ok(Self { - mast: Arc::new(mast), - entrypoint, - package_debug_info: package_debug_info(package), - }) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns a reference to the [MastForest] backing this transaction script. - pub fn mast(&self) -> Arc { - self.mast.clone() - } - - /// Returns the MAST forest and package-owned debug information backing this transaction script. - pub fn loaded_mast_forest(&self) -> LoadedMastForest { - loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone()) - } - - /// Returns the commitment of this transaction script (i.e., the script's MAST root). - pub fn root(&self) -> TransactionScriptRoot { - TransactionScriptRoot::from_raw(self.mast[self.entrypoint].digest()) - } - - /// Returns a new [TransactionScript] with the provided advice map entries merged into the - /// underlying [MastForest]. - /// - /// This allows adding advice map entries to an already-compiled transaction script, - /// which is useful when the entries are determined after script compilation. - pub fn with_advice_map(self, advice_map: AdviceMap) -> Self { - if advice_map.is_empty() { - return self; - } - - let mast = (*self.mast).clone().with_advice_map(advice_map); - Self { - mast: Arc::new(mast), - entrypoint: self.entrypoint, - package_debug_info: self.package_debug_info, - } - } -} - -impl PartialEq for TransactionScript { - fn eq(&self, other: &Self) -> bool { - self.mast == other.mast && self.entrypoint == other.entrypoint - } -} - -impl Eq for TransactionScript {} - -// SERIALIZATION -// ================================================================================================ - -impl Serializable for TransactionScript { - fn write_into(&self, target: &mut W) { - self.mast.write_into(target); - target.write_u32(u32::from(self.entrypoint)); - } -} - -impl Deserializable for TransactionScript { - fn read_from(source: &mut R) -> Result { - let mast = MastForest::read_from(source)?; - let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?; - - Ok(Self::from_parts(Arc::new(mast), entrypoint)) - } -} - #[cfg(test)] mod tests { use miden_core::advice::AdviceMap; @@ -512,160 +269,4 @@ mod tests { assert_eq!(tx_args, decoded); } - - #[test] - fn test_transaction_script_preserves_package_debug_info() { - use super::TransactionScript; - use crate::assembly::Assembler; - - let assembler = Assembler::default(); - let package = - assembler.assemble_program("test-transaction-script", "begin nop end").unwrap(); - let script = TransactionScript::from_package(&package).unwrap(); - - assert!(script.loaded_mast_forest().package_debug_info().unwrap().is_some()); - } - - #[test] - fn test_transaction_script_with_advice_map() { - use miden_core::{Felt, Word}; - - use super::TransactionScript; - use crate::assembly::Assembler; - - let assembler = Assembler::default(); - let package = - assembler.assemble_program("test-transaction-script", "begin nop end").unwrap(); - let script = TransactionScript::from_package(&package).unwrap(); - assert!(script.mast().advice_map().is_empty()); - - // Empty advice map should be a no-op - let original_root = script.root(); - let script = script.with_advice_map(AdviceMap::default()); - assert_eq!(original_root, script.root()); - - // Non-empty advice map should add entries - let key = Word::from([1u32, 2, 3, 4]); - let value = vec![Felt::new_unchecked(42), Felt::new_unchecked(43)]; - let mut advice_map = AdviceMap::default(); - advice_map.insert(key, value.clone()); - - let script = script.with_advice_map(advice_map); - - let mast = script.mast(); - let stored = mast.advice_map().get(&key).expect("entry should be present"); - assert_eq!(stored.as_ref(), value.as_slice()); - } - - #[test] - fn test_transaction_script_from_library_package() { - use assert_matches::assert_matches; - - use super::TransactionScript; - use crate::errors::TransactionScriptError; - use crate::testing::assembler::assemble_test_package; - use crate::utils::serde::{Deserializable, Serializable}; - - let source = " - @transaction_script - pub proc main - push.1 drop - end - "; - let package = assemble_test_package("test-tx-script", "test::tx_script", source); - - let script = TransactionScript::from_package(&package).unwrap(); - - // the script must round-trip through serialization unchanged - let bytes = script.to_bytes(); - let decoded = TransactionScript::read_from_bytes(&bytes).unwrap(); - assert_eq!(script, decoded); - - // a package without the attribute is rejected - let no_attr = assemble_test_package( - "test-tx-script-no-attr", - "test::tx_script_no_attr", - "pub proc main push.1 drop end", - ); - assert_matches!( - TransactionScript::from_package(&no_attr), - Err(TransactionScriptError::NoProcedureWithAttribute) - ); - - // a package with multiple tagged procedures is rejected - let multiple = assemble_test_package( - "test-tx-script-multiple", - "test::tx_script_multiple", - "@transaction_script pub proc main_a push.1 drop end - @transaction_script pub proc main_b push.2 drop end", - ); - assert_matches!( - TransactionScript::from_package(&multiple), - Err(TransactionScriptError::MultipleProceduresWithAttribute) - ); - } - - #[test] - fn test_transaction_script_from_package_reference() { - use alloc::string::ToString; - - use assert_matches::assert_matches; - - use super::TransactionScript; - use crate::Word; - use crate::assembly::Path; - use crate::errors::TransactionScriptError; - use crate::testing::assembler::assemble_test_package; - - let source = " - @transaction_script - pub proc main_a - push.1 drop - end - - @transaction_script - pub proc main_b - push.2 drop - end - - pub proc helper - push.3 drop - end - "; - let package = - assemble_test_package("test-tx-script-reference", "test::tx_script_reference", source); - - // each tagged procedure can be extracted selectively, and the resulting script's root - // matches the digest of the referenced procedure - for proc_name in ["main_a", "main_b"] { - let export = package - .manifest - .exports() - .find(|e| e.path().as_ref().to_string().ends_with(proc_name)) - .unwrap(); - let digest = export.as_procedure().unwrap().digest; - - let script = - TransactionScript::from_package_reference(&package, export.path().as_ref()) - .unwrap(); - assert_eq!(Word::from(script.root()), digest); - } - - // an unknown path is rejected - assert_matches!( - TransactionScript::from_package_reference(&package, Path::new("::foo::bar::main")), - Err(TransactionScriptError::ProcedureNotFound(_)) - ); - - // a procedure without the attribute is rejected - let helper = package - .manifest - .exports() - .find(|e| e.path().as_ref().to_string().ends_with("helper")) - .unwrap(); - assert_matches!( - TransactionScript::from_package_reference(&package, helper.path().as_ref()), - Err(TransactionScriptError::ProcedureMissingAttribute(_)) - ); - } } diff --git a/crates/miden-standards/asm/components/note/note_creator/miden-project.toml b/crates/miden-standards/asm/components/note/note_creator/miden-project.toml index af4ec2395d..5c429b242d 100644 --- a/crates/miden-standards/asm/components/note/note_creator/miden-project.toml +++ b/crates/miden-standards/asm/components/note/note_creator/miden-project.toml @@ -1,10 +1,10 @@ [package] -name = "miden-standards-wallets-note-creator" +name = "miden-standards-note-note-creator" version.workspace = true [lib] kind = "account-component" -namespace = "miden::standards::components::wallets::note_creator" +namespace = "miden::standards::components::note::note_creator" path = "note_creator.masm" [dependencies] diff --git a/crates/miden-standards/asm/components/wallets/basic_wallet/basic_wallet.masm b/crates/miden-standards/asm/components/wallets/basic_wallet/basic_wallet.masm index b58a52ee1b..23638f90ac 100644 --- a/crates/miden-standards/asm/components/wallets/basic_wallet/basic_wallet.masm +++ b/crates/miden-standards/asm/components/wallets/basic_wallet/basic_wallet.masm @@ -4,4 +4,4 @@ pub use {receive_asset} from miden::standards::wallets::basic pub use {move_asset_to_note} from miden::standards::wallets::basic -pub use {create_note} from miden::standards::note::note_creator +pub use {create_note} from miden::standards::wallets::basic diff --git a/crates/miden-standards/asm/standards/access/rbac.masm b/crates/miden-standards/asm/standards/access/rbac.masm index fd9914c95b..67db3c8cac 100644 --- a/crates/miden-standards/asm/standards/access/rbac.masm +++ b/crates/miden-standards/asm/standards/access/rbac.masm @@ -17,9 +17,12 @@ # - Every role has an effective admin role. It is the role's configured # `admin_role_symbol` when set, otherwise the built-in `ADMIN` role. Only members of a # role's effective admin role may grant, revoke, or re-point (`set_role_admin`) that role. -# - Delegation is exclusive: once a role's admin is delegated to another role, the `ADMIN` -# role no longer has any authority over it. This lets a role be placed exclusively under a -# dedicated admin role and kept out of reach of the general administrator. +# - Delegation is exclusive while the delegated admin role is populated: the `ADMIN` role then has +# no authority over the delegated role. This lets a role be placed exclusively under a dedicated +# admin role and kept out of reach of the general administrator. +# - Delegation is not a one-way door: a memberless role can authorize nothing, so authority over +# the roles a memberless role administers falls back to the `ADMIN` role. Delegating to a dead +# role therefore cannot orphan a role permanently, and only emptying `ADMIN` itself is final. # - The `ADMIN` role administers itself (its own effective admin is `ADMIN`), so `ADMIN` # membership can be granted, revoked, and renounced through the standard API. # - A role is considered to "exist" when it has at least one member. Role admin @@ -141,6 +144,9 @@ end #! Pass `admin_role_symbol = 0` to clear the delegation and revert the role to #! management by the default `ADMIN` role. #! +#! Delegating to a memberless role does not put the role out of reach: authority over it falls +#! back to the `ADMIN` role for as long as the delegate stays memberless. +#! #! Inputs: [role_symbol, admin_role_symbol, pad(14)] #! Outputs: [pad(16)] #! @@ -151,7 +157,8 @@ end #! #! Panics if: #! - role_symbol is zero. -#! - the note sender does not hold the role's current effective admin role. +#! - the note sender is not authorized to administer the role (see +#! `assert_sender_is_role_admin`). #! #! Invocation: call @account_procedure @@ -186,7 +193,8 @@ end #! #! Panics if: #! - role_symbol is zero. -#! - the note sender does not hold the role's effective admin role. +#! - the note sender is not authorized to administer the role (see +#! `assert_sender_is_role_admin`). #! - the account ID is invalid. #! #! Invocation: call @@ -225,7 +233,8 @@ end #! #! Panics if: #! - role_symbol is zero. -#! - the note sender does not hold the role's effective admin role. +#! - the note sender is not authorized to administer the role (see +#! `assert_sender_is_role_admin`). #! - the account does not hold the role. #! #! Invocation: call @@ -441,22 +450,31 @@ proc get_effective_role_admin # => [effective_admin_role_symbol] end -#! Asserts that the note sender holds the role's effective admin role. +#! Asserts that the note sender is authorized to administer a role. #! -#! The effective admin is the role's configured delegated admin, or the built-in `ADMIN` -#! role when none is configured. +#! Authority rests with the role's effective admin role: its configured delegated admin, or the +#! built-in `ADMIN` role when none is configured. #! #! Inputs: [role_symbol] #! Outputs: [] #! #! Panics if: -#! - the note sender does not hold the role's effective admin role. +#! - the note sender holds neither the role's effective admin role nor, when that role is +#! memberless, the `ADMIN` role. #! #! Invocation: exec proc assert_sender_is_role_admin exec.get_effective_role_admin # => [effective_admin_role_symbol] + push.ADMIN_ROLE + dup.1 exec.get_role_member_count_internal eq.0 + # => [is_memberless, admin_role, effective_admin_role_symbol] + + # A memberless effective admin cannot authorize anything, so fall back to the `ADMIN` role. + cdrop + # => [authorizing_role_symbol] + exec.is_sender_in_role # => [has_admin_role] diff --git a/crates/miden-standards/asm/standards/assets/fungible_asset.masm b/crates/miden-standards/asm/standards/assets/fungible_asset.masm index 92713f8379..c433534bea 100644 --- a/crates/miden-standards/asm/standards/assets/fungible_asset.masm +++ b/crates/miden-standards/asm/standards/assets/fungible_asset.masm @@ -108,7 +108,8 @@ end #! #! Panics if: #! - the asset composition encoded in ASSET_ID is not fungible. -#! - the invocation of this procedure does not originate from the native account. +#! - the invocation of this procedure does not originate from the account context. +#! - the current active account is not the native account of the transaction. #! #! Invocation: exec pub proc get_initial_native_account_balance diff --git a/crates/miden-standards/asm/standards/faucets/policies/policy_manager.masm b/crates/miden-standards/asm/standards/faucets/policies/policy_manager.masm index a80359e31a..7174723488 100644 --- a/crates/miden-standards/asm/standards/faucets/policies/policy_manager.masm +++ b/crates/miden-standards/asm/standards/faucets/policies/policy_manager.masm @@ -150,16 +150,11 @@ end #! applies the pause check and invokes the active send policy. #! #! Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] -#! Outputs: [PROCESSED_ASSET_VALUE, pad(12)] -#! -#! Where: -#! - PROCESSED_ASSET_VALUE is the asset value returned by the policy, or the original ASSET_VALUE -#! if no send policy is configured. +#! Outputs: [pad(16)] #! #! Panics if: #! - the account is paused. #! - the active send policy predicate fails. -#! - the active send policy returns an asset value different from the one it received. #! #! Invocation: call @account_procedure @@ -168,7 +163,7 @@ pub proc invoke_send_policy # => [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, note_idx, pad(7)] exec.invoke_transfer_policy - # => [PROCESSED_ASSET_VALUE, pad(12)] + # => [pad(16)] end #! Returns active send policy root. @@ -221,17 +216,14 @@ end #! receive policy is configured). #! #! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)] -#! Outputs: [PROCESSED_ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Where: #! - custom_data is `0` for the receive callback. -#! - PROCESSED_ASSET_VALUE is the asset value returned by the policy, or the original ASSET_VALUE -#! if no receive policy is configured. #! #! Panics if: #! - the account is paused. #! - the active receive policy predicate fails. -#! - the active receive policy returns an asset value different from the one it received. #! #! Invocation: call @account_procedure @@ -240,7 +232,7 @@ pub proc invoke_receive_policy # => [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data, pad(7)] exec.invoke_transfer_policy - # => [PROCESSED_ASSET_VALUE, pad(12)] + # => [pad(16)] end #! Returns active receive policy root. @@ -536,23 +528,20 @@ end #! which differ only in which slot they bind. #! #! If the active root is the empty word (no policy configured for this kind), the transfer is -#! accepted unchanged and the pause check is skipped — mirroring the kernel's behavior when a -#! callback slot holds the empty word. Otherwise the account-wide pause flag is asserted (a no-op -#! when the [`Pausable`] component is not installed) and the policy is invoked via `dyncall`. +#! accepted and the pause check is skipped — mirroring the kernel's behavior when a callback slot +#! holds the empty word. Otherwise the account-wide pause flag is asserted (a no-op when the +#! [`Pausable`] component is not installed) and the policy is invoked via `dyncall`. #! #! Inputs: [slot_id_suffix, slot_id_prefix, ASSET_ID, ASSET_VALUE, custom_data, pad(7)] -#! Outputs: [PROCESSED_ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Where: #! - slot_id_{suffix, prefix} identify the storage slot holding the active policy root. #! - custom_data is `0` for the receive callback and the output note index for the send callback. -#! - PROCESSED_ASSET_VALUE is the asset value returned by the policy, or the original ASSET_VALUE -#! if no policy is configured. #! #! Panics if: #! - the account is paused. #! - the invoked policy predicate fails. -#! - the invoked policy returns an asset value different from the one it received. #! #! Invocation: exec @locals(4) @@ -564,10 +553,9 @@ proc invoke_transfer_policy # => [is_empty, POLICY_ROOT, ASSET_ID, ASSET_VALUE, custom_data, pad(7)] if.true - # No policy configured: drop the empty root and asset ID, return the asset value - # unchanged. `movup.4 drop` removes custom_data so only ASSET_VALUE is returned. - dropw dropw movup.4 drop - # => [ASSET_VALUE, pad(7)] + # No policy configured: consume the callback inputs. + dropw dropw dropw drop + # => [pad(16)] else exec.pausable::assert_not_paused # => [POLICY_ROOT, ASSET_ID, ASSET_VALUE, custom_data, pad(7)] @@ -579,6 +567,6 @@ proc invoke_transfer_policy # => [policy_root_ptr, ASSET_ID, ASSET_VALUE, custom_data, pad(7)] dyncall - # => [PROCESSED_ASSET_VALUE, pad(12)] + # => [pad(16)] end end diff --git a/crates/miden-standards/asm/standards/faucets/policies/transfer/allow_all.masm b/crates/miden-standards/asm/standards/faucets/policies/transfer/allow_all.masm index 5f619a92e2..5cc56a0328 100644 --- a/crates/miden-standards/asm/standards/faucets/policies/transfer/allow_all.masm +++ b/crates/miden-standards/asm/standards/faucets/policies/transfer/allow_all.masm @@ -9,15 +9,15 @@ #! account-wide pause check. #! #! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)] -#! Outputs: [ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Where: #! - custom_data is `0` for the receive (account) callback and the output note index for the -#! send (note) callback. This policy does not inspect it; it passes through unchanged. +#! send (note) callback. This policy does not inspect it. #! #! Invocation: call @account_procedure pub proc check_policy - dropw - # => [ASSET_VALUE, pad(12)] + dropw dropw drop + # => [pad(16)] end diff --git a/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_allowlist.masm b/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_allowlist.masm index 99919e2043..bb1398753f 100644 --- a/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_allowlist.masm +++ b/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_allowlist.masm @@ -18,16 +18,15 @@ use miden::standards::faucets::policies::transfer::allowlist #! Transfer policy that rejects transfers whose native account is not allowed on the issuing #! faucet. #! -#! The same procedure root is reusable as both send and receive policy because it only -#! consumes the top eight felts (`ASSET_ID`, `ASSET_VALUE`) and leaves the rest of the call -#! frame untouched — any `note_idx` carried in the send signature passes through unchanged. +#! The same procedure root is reusable as both send and receive policy because it does not depend +#! on custom_data. #! #! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)] -#! Outputs: [ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Where: #! - custom_data is `0` for the receive (account) callback and the output note index for the -#! send (note) callback. This policy does not inspect it; it passes through unchanged. +#! send (note) callback. This policy does not inspect it. #! #! Panics if: #! - the native account is not allowed on the issuing faucet and is not the issuing faucet @@ -63,5 +62,8 @@ pub proc check_policy dropw # => [ASSET_VALUE, custom_data, pad(7)] end - # => [ASSET_VALUE, pad(12)] + # => [ASSET_VALUE, custom_data, pad(7)] + + dropw drop + # => [pad(16)] end diff --git a/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_blocklist.masm b/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_blocklist.masm index 9b57d76e03..ae9ac6c776 100644 --- a/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_blocklist.masm +++ b/crates/miden-standards/asm/standards/faucets/policies/transfer/basic_blocklist.masm @@ -17,16 +17,15 @@ use miden::standards::faucets::policies::transfer::blocklist #! Transfer policy that rejects transfers whose native account is blocked on the issuing faucet. #! -#! The same procedure root is reusable as both send and receive policy because it only -#! consumes the top eight felts (`ASSET_ID`, `ASSET_VALUE`) and leaves the rest of the call -#! frame untouched — any `note_idx` carried in the send signature passes through unchanged. +#! The same procedure root is reusable as both send and receive policy because it does not depend +#! on custom_data. #! #! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)] -#! Outputs: [ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Where: #! - custom_data is `0` for the receive (account) callback and the output note index for the -#! send (note) callback. This policy does not inspect it; it passes through unchanged. +#! send (note) callback. This policy does not inspect it. #! #! Panics if: #! - the native account is blocked on the issuing faucet and is not the issuing faucet itself. @@ -61,5 +60,8 @@ pub proc check_policy dropw # => [ASSET_VALUE, custom_data, pad(7)] end - # => [ASSET_VALUE, pad(12)] + # => [ASSET_VALUE, custom_data, pad(7)] + + dropw drop + # => [pad(16)] end diff --git a/crates/miden-standards/asm/standards/fees/mod.masm b/crates/miden-standards/asm/standards/fees/mod.masm index cbab0f0843..29a55e9573 100644 --- a/crates/miden-standards/asm/standards/fees/mod.masm +++ b/crates/miden-standards/asm/standards/fees/mod.masm @@ -23,7 +23,7 @@ use {NOTE_TYPE_PUBLIC} from miden::protocol::note use miden::standards::assets::fungible_asset use miden::standards::attachments::network_account_target use miden::standards::note -use miden::standards::note_tag +use miden::standards::note::note_tag use miden::standards::notes::fee_sponsorship use {NUM_ASSETS as SPONSORSHIP_NUM_ASSETS, FEATURE_NOTE_ID_ITEM_OFFSET} from miden::standards::notes::fee_sponsorship use {NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME, NETWORK_ACCOUNT_TARGET_ATTACHMENT_NUM_WORDS} diff --git a/crates/miden-standards/asm/standards/mod.masm b/crates/miden-standards/asm/standards/mod.masm index 1723604cca..a6505cc852 100644 --- a/crates/miden-standards/asm/standards/mod.masm +++ b/crates/miden-standards/asm/standards/mod.masm @@ -10,7 +10,6 @@ pub mod fees pub mod inspection pub mod interop pub mod note -pub mod note_tag pub mod notes pub mod tx_scripts pub mod utils diff --git a/crates/miden-standards/asm/standards/note/mod.masm b/crates/miden-standards/asm/standards/note/mod.masm index 3a3e349e2e..e7a53fd39d 100644 --- a/crates/miden-standards/asm/standards/note/mod.masm +++ b/crates/miden-standards/asm/standards/note/mod.masm @@ -13,6 +13,7 @@ use {NoteRecipient, NoteScriptRoot} from miden::protocol::types pub mod execution_hint pub mod note_creator +pub mod note_tag # ERRORS # ================================================================================================= diff --git a/crates/miden-standards/asm/standards/note/note_creator.masm b/crates/miden-standards/asm/standards/note/note_creator.masm index f60e4832e6..2de3238579 100644 --- a/crates/miden-standards/asm/standards/note/note_creator.masm +++ b/crates/miden-standards/asm/standards/note/note_creator.masm @@ -5,9 +5,9 @@ use miden::protocol::output_note #! Creates a new output note and returns its index. #! -#! This procedure lets the wallet opt into note creation: `output_note::create` can only be invoked -#! from the account context, so accounts that want to create notes must expose it through their own -#! interface. +#! This procedure lets a note or tx script create an output note: `output_note::create` can only +#! be invoked from the account context, so accounts that want to create notes must expose it through +#! their own interface. #! #! This procedure is expected to be invoked using a `call` instruction. It makes no guarantees about #! the contents of the `PAD` elements shown below. It is the caller's responsibility to make sure diff --git a/crates/miden-standards/asm/standards/note_tag/mod.masm b/crates/miden-standards/asm/standards/note/note_tag.masm similarity index 100% rename from crates/miden-standards/asm/standards/note_tag/mod.masm rename to crates/miden-standards/asm/standards/note/note_tag.masm diff --git a/crates/miden-standards/asm/standards/notes/p2id.masm b/crates/miden-standards/asm/standards/notes/p2id.masm index 8c32786f71..d377650b7a 100644 --- a/crates/miden-standards/asm/standards/notes/p2id.masm +++ b/crates/miden-standards/asm/standards/notes/p2id.masm @@ -3,7 +3,6 @@ use miden::protocol::account_id use miden::protocol::active_note use miden::protocol::note use miden::standards::wallets::basic as basic_wallet -use miden::standards::note::note_creator # ERRORS # ================================================================================================= @@ -121,13 +120,13 @@ end #! Creates a new P2ID output note from the given inputs and returns its index. #! #! Note creation must originate from the account context, so this procedure routes creation through -#! the account's `note_creator::create_note` procedure. This lets `p2id::create_output_note` be used -#! from contexts outside the account code, such as note or transaction scripts. Callers that are -#! already inside an account procedure should instead use `p2id::prepare_note` together with +#! the account's `create_note` procedure. This lets `p2id::create_output_note` be used from contexts +#! outside the account code, such as note or transaction scripts. Callers that are already inside an +#! account procedure should instead use `p2id::prepare_note` together with #! `exec.output_note::create`. #! #! Requires that the account exposes: -#! - miden::standards::note::note_creator::create_note procedure. +#! - `miden::standards::note::note_creator::create_note` procedure. #! #! Inputs: [target_id_suffix, target_id_prefix, tag, note_type, SERIAL_NUM] #! Outputs: [note_idx] @@ -150,7 +149,7 @@ pub proc create_output_note push.0 movdn.6 push.0 movdn.6 padw padw swapdw # => [tag, note_type, RECIPIENT, pad(10)] - call.note_creator::create_note + call.basic_wallet::create_note # => [note_idx, pad(15)] movdn.15 dropw dropw dropw drop drop drop diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 60923f714f..81cc411186 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -8,10 +8,9 @@ use miden::protocol::note use miden::protocol::output_note use miden::standards::assets::fungible_asset use {MAX_AMOUNT as FUNGIBLE_ASSET_MAX_AMOUNT} from miden::standards::assets::fungible_asset -use miden::standards::note_tag +use miden::standards::note::note_tag use miden::standards::notes::p2id use miden::standards::wallets::basic as wallet -use miden::standards::note::note_creator # CONSTANTS # ================================================================================================= @@ -390,7 +389,7 @@ proc create_remainder_note push.0 movdn.6 push.0 movdn.6 padw padw swapdw # => [tag, note_type, RECIPIENT, pad(10)] - call.note_creator::create_note + call.wallet::create_note # => [note_idx, pad(15)] movdn.15 dropw dropw dropw drop drop drop @@ -788,10 +787,10 @@ end #! - `note_fill_amount`: portion of the requested asset sourced from another note in the #! same transaction (cross-swap / net-zero flow, no vault debit). #! -#! At least one of the two must be non-zero. If both are zero — which is the default in -#! network transactions where the executor does not provide note_args — the script falls back -#! to a full fill (`account_fill_amount = min_requested_amount`). If the consuming account is the note's -#! creator, the script reclaims the offered asset back to the creator's vault instead. +#! At least one of the two must be non-zero. If both are zero — which is the default in network +#! transactions where the executor does not provide note_args — the script falls back to a full fill +#! (`account_fill_amount = min_requested_amount`). If the consuming account is the note's creator, +#! the script reclaims the offered asset back to the creator's vault instead. #! #! Requires that the account exposes: #! - `miden::standards::wallets::basic::receive_asset` procedure. diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index e26b69bda4..ad7e800828 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -3,7 +3,6 @@ use miden::protocol::asset use {NOTE_TYPE_PRIVATE} from miden::protocol::note use miden::standards::notes::p2id use miden::standards::wallets::basic as wallet -use miden::standards::note::note_creator # CONSTANTS # ================================================================================================= @@ -51,9 +50,9 @@ const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset #! account id stored in plaintext. #! #! Requires that the account exposes: -#! - miden::standards::wallets::basic::receive_asset procedure. -#! - miden::standards::wallets::basic::move_asset_to_note procedure. -#! - miden::standards::note::note_creator::create_note procedure. +#! - `miden::standards::wallets::basic::receive_asset` procedure. +#! - `miden::standards::wallets::basic::move_asset_to_note` procedure. +#! - `miden::standards::note::note_creator::create_note` procedure. #! #! Inputs: [ARGS] #! Outputs: [] @@ -119,7 +118,7 @@ pub proc main push.0 movdn.6 push.0 movdn.6 padw padw swapdw # => [tag, note_type, PAYBACK_RECIPIENT, pad(10)] - call.note_creator::create_note + call.wallet::create_note # => [note_idx, pad(15)] movdn.15 dropw dropw dropw drop drop drop diff --git a/crates/miden-standards/asm/standards/notes/tx_fee.masm b/crates/miden-standards/asm/standards/notes/tx_fee.masm index 9359f3712f..d75d6219ed 100644 --- a/crates/miden-standards/asm/standards/notes/tx_fee.masm +++ b/crates/miden-standards/asm/standards/notes/tx_fee.masm @@ -2,7 +2,6 @@ use miden::protocol::active_note use miden::protocol::note use {NOTE_TYPE_PUBLIC} from miden::protocol::note use miden::standards::wallets::basic as basic_wallet -use miden::standards::note::note_creator # CONSTANTS # ================================================================================================= @@ -105,7 +104,7 @@ end #! Creates a new TX_FEE output note from the given serial number and returns its index. #! #! Note creation must originate from the account context, so this procedure routes creation through -#! the account's `note_creator::create_note` procedure. This lets `tx_fee::create_output_note` be used +#! the account's `create_note` procedure. This lets `tx_fee::create_output_note` be used #! from contexts outside the account code, such as note or transaction scripts. Callers that are #! already inside an account procedure should instead use `tx_fee::prepare_note` together with #! `exec.output_note::create`. @@ -113,7 +112,7 @@ end #! The created note is always public and carries the unique TX_FEE_NOTE_TAG note tag. #! #! Requires that the account exposes: -#! - miden::standards::note::note_creator::create_note procedure. +#! - `miden::standards::note::note_creator::create_note` procedure. #! #! Inputs: [SERIAL_NUM] #! Outputs: [note_idx] @@ -131,7 +130,7 @@ pub proc create_output_note push.0 movdn.6 push.0 movdn.6 padw padw swapdw # => [tag, note_type, RECIPIENT, pad(10)] - call.note_creator::create_note + call.basic_wallet::create_note # => [note_idx, pad(15)] movdn.15 dropw dropw dropw drop drop drop diff --git a/crates/miden-standards/asm/standards/wallets/basic.masm b/crates/miden-standards/asm/standards/wallets/basic.masm index da334e85fc..538a30d53e 100644 --- a/crates/miden-standards/asm/standards/wallets/basic.masm +++ b/crates/miden-standards/asm/standards/wallets/basic.masm @@ -7,6 +7,8 @@ use miden::protocol::active_note # PUBLIC INTERFACE # ================================================================================================= +pub use {create_note} from miden::standards::note::note_creator + #! Adds the provided asset to the active account. #! #! Inputs: [ASSET_ID, ASSET_VALUE, pad(8)] diff --git a/crates/miden-standards/src/account/access/authority.rs b/crates/miden-standards/src/account/access/authority.rs index 1c867ebdf4..ae55608a8e 100644 --- a/crates/miden-standards/src/account/access/authority.rs +++ b/crates/miden-standards/src/account/access/authority.rs @@ -110,6 +110,49 @@ const RBAC_CONTROLLED: u8 = 2; /// This flag has no effect under [`Authority::AuthControlled`], where `freeze` / `unfreeze` panic /// (there is no owner and no role graph). /// +/// # Freeze-only actor (incident-response "panic button") +/// +/// A second actor that can freeze the account in an incident but can never re-open it, or authorize +/// anything else, needs no dedicated component: it is a plain [`Authority::RbacControlled`] role +/// assignment. Map `freeze` to a role of its own, map `unfreeze` to a *different* role, and grant +/// the incident responder only the former: +/// +/// ```no_run +/// use std::collections::BTreeMap; +/// +/// use miden_protocol::account::{AccountBuilder, RoleSymbol}; +/// use miden_standards::account::access::{AccessControl, Authority}; +/// # let admin: miden_protocol::account::AccountId = unimplemented!(); +/// # let init_seed = [0u8; 32]; +/// +/// let procedure_roles = BTreeMap::from([ +/// (Authority::freeze_root(), RoleSymbol::new("FREEZER")?), +/// (Authority::unfreeze_root(), RoleSymbol::new("UNFREEZER")?), +/// ]); +/// +/// AccountBuilder::new(init_seed).with_components(AccessControl::Rbac { admin, procedure_roles }); +/// +/// // Then grant `FREEZER` to the incident responder and `UNFREEZER` to the recovery authority +/// // through the `RoleBasedAccessControl` component's `grant_role`. +/// # Ok::<(), miden_protocol::errors::RoleSymbolError>(()) +/// ``` +/// +/// This yields the intended asymmetry: freezing is available to the `FREEZER`, re-opening is not. +/// A compromised freeze-only actor can at worst deny service by freezing the account; it can never +/// keep the account open, grant roles, move assets, or invoke any other gated procedure. +/// +/// Two things to get right when wiring this up: +/// +/// - Map `unfreeze` explicitly, or leave it unmapped and keep the freeze-only actor out of `ADMIN`. +/// An unmapped procedure falls back to the `ADMIN` role, so a freeze-only actor that also holds +/// `ADMIN` could re-open the account and defeat the asymmetry. +/// - The pattern requires `RbacControlled`. Under [`Authority::OwnerControlled`] the owner is the +/// only emergency authority, and under [`Authority::AuthControlled`] there is no switch at all, +/// so an account that wants a freeze-only actor must use RBAC. +/// +/// The same shape generalizes to any "can stop, cannot start" authority: give the cancelling or +/// pausing procedure its own role and keep the resuming procedure on a separate one. +/// /// Storage layout: /// - Value slot: `[authority, is_frozen, 0, 0]`. /// - Map slot (only under RBAC): `procedure_root` → `[role_symbol, 0, 0, 0]`. diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs index bb249c31a2..14b13b67f1 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -55,12 +55,12 @@ pub enum AccessControl { /// for the administration model. /// /// `procedure_roles` assigns a role to individual authority-gated procedures, keyed by - /// procedure root (e.g. `PausableManager::pause_root()` → `PAUSER`, `unpause_root()` → - /// `UNPAUSER`, and optionally `Authority::freeze_root()` → `FREEZER`). A gated procedure - /// without an entry in `procedure_roles` falls back to the `ADMIN` role. The emergency - /// `freeze` / `unfreeze` switch resolves its role the same way, defaulting to `ADMIN`. Role - /// membership is managed through the standard RBAC API on the [`RoleBasedAccessControl`] - /// component. + /// procedure root (e.g. [`PausableManager::pause_root`] → `PAUSER`, + /// [`PausableManager::unpause_root`] → `UNPAUSER`, and optionally [`Authority::freeze_root`] → + /// `FREEZER` with [`Authority::unfreeze_root`] → `UNFREEZER`). A gated procedure without an + /// entry in `procedure_roles` falls back to the `ADMIN` role. The emergency `freeze` / + /// `unfreeze` switch resolves its role the same way, defaulting to `ADMIN`. Role membership is + /// managed through the standard RBAC API on the [`RoleBasedAccessControl`] component. Rbac { admin: AccountId, procedure_roles: BTreeMap, diff --git a/crates/miden-standards/src/account/access/rbac.rs b/crates/miden-standards/src/account/access/rbac.rs index dd83091364..aa3c996845 100644 --- a/crates/miden-standards/src/account/access/rbac.rs +++ b/crates/miden-standards/src/account/access/rbac.rs @@ -143,8 +143,8 @@ impl RoleConfig { /// accounts holding `MINTER_ADMIN` can manage the `MINTER` role but have no authority over /// `BURNER` or `PAUSER`. /// -/// Delegation is *exclusive*: once a role's admin is delegated to another role, the `ADMIN` -/// role loses all authority over it (grant, revoke, and further `set_role_admin` are then +/// Delegation is *exclusive* while the delegated admin role is populated: the `ADMIN` role then +/// has no authority over the delegated role (grant, revoke, and further `set_role_admin` are /// gated on the delegated admin). This lets a sensitive role — say a token issuer — be placed /// exclusively under a dedicated admin role and kept out of reach of the general /// administrator. To hand authority back, the current delegated admin re-points the role @@ -170,10 +170,7 @@ impl RoleConfig { /// /// The delegated admin of a role can itself be any role, including one that it admins. /// Circular relationships are possible but should be designed with care, since each role -/// can then revoke the other. Only delegate to a role that already has members, and treat -/// emptying a role's effective admin like ownership renouncement: the role stays -/// unmanageable until its effective admin is repopulated — for a self-administering role -/// (including `ADMIN`), never. +/// can then revoke the other. /// /// ## Role semantics /// @@ -231,10 +228,11 @@ impl RoleBasedAccessControl { /// - the same role is specified more than once. /// - a role is configured with neither members nor a delegated admin. /// - a role's member count exceeds [`u32::MAX`]. - /// - a role's effective admin — its delegated admin, or `ADMIN` when unset — can never hold - /// members, which would leave the role permanently unmanageable. Setting an operational role - /// without defining `ADMIN` is the common case: `ADMIN` administers itself, so nothing can - /// ever populate it. + /// - `ADMIN` is defined without members, or not defined at all, and a role's admin chain never + /// reaches a populated role, which would leave that role permanently unmanageable. A + /// populated `ADMIN` administers every role whose delegated admin is memberless, so it makes + /// any admin chain recoverable; without one, defining an operational role and no `ADMIN` is + /// the common defect, since `ADMIN` administers itself and nothing can ever populate it. #[builder] pub fn new( #[builder(field)] role_configs: Vec, @@ -256,15 +254,21 @@ impl RoleBasedAccessControl { roles.insert(config.role.clone(), config); } - // Check the effective admin of every role, not just of the explicitly delegated ones: a - // role left with the default admin is just as frozen when `ADMIN` can never hold members. - for role_config in roles.values() { - let admin = role_config.admin.clone().unwrap_or_else(Self::admin_role); - if !reaches_populated_role(&admin, &roles) { - return Err(RoleBasedAccessControlError::UnmanageableRole { - role: role_config.role.clone(), - admin, - }); + // A memberless role can authorize nothing, so authority over the roles it administers falls + // back to `ADMIN` (see `assert_sender_is_role_admin`). A populated `ADMIN` therefore keeps + // every role manageable, whatever its delegated admin looks like, and only a configuration + // without one has to stand on its own admin chains. + let admin_is_populated = + roles.get(&Self::admin_role()).is_some_and(|config| !config.members.is_empty()); + if !admin_is_populated { + for role_config in roles.values() { + let admin = role_config.admin.clone().unwrap_or_else(Self::admin_role); + if !reaches_populated_role(&admin, &roles) { + return Err(RoleBasedAccessControlError::UnmanageableRole { + role: role_config.role.clone(), + admin, + }); + } } } @@ -386,11 +390,12 @@ impl RoleBasedAccessControlBuilder< /// Returns `true` if walking the delegated-admin chain starting at `role` reaches a role defined /// with at least one member. /// -/// Only a populated role can grant members to the role below it in the chain, so a chain that -/// reaches none of them can never be acted on by anyone. A role that is not configured, or -/// configured without members, is administered by its delegated admin, defaulting to `ADMIN`. -/// Every role has exactly one admin, so the walk always ends in a cycle, which the visited set -/// terminates. +/// Only relevant when `ADMIN` has no members: a populated `ADMIN` administers every role whose +/// delegated admin is memberless, which makes any chain recoverable. Without one, only a populated +/// role can grant members to the role below it in the chain, so a chain that reaches none of them +/// can never be acted on by anyone. A role that is not configured, or configured without members, +/// is administered by its delegated admin, defaulting to `ADMIN`. Every role has exactly one admin, +/// so the walk always ends in a cycle, which the visited set terminates. fn reaches_populated_role(role: &RoleSymbol, configs: &BTreeMap) -> bool { let admin_role = RoleBasedAccessControl::admin_role(); let mut visited = BTreeSet::new(); @@ -663,6 +668,25 @@ mod tests { Ok(()) } + /// A populated `ADMIN` administers every role whose delegated admin is memberless, so even an + /// admin chain that reaches no populated role of its own leaves the role manageable. + #[test] + fn populated_admin_allows_an_otherwise_unmanageable_delegation() -> anyhow::Result<()> { + let admin = test_admin(1); + let minter_role = RoleSymbol::new("MINTER")?; + let minter_admin_role = RoleSymbol::new("MINTER_ADMIN")?; + + // MINTER_ADMIN administers itself and has no members, so nothing in MINTER's chain can be + // populated by the chain itself — ADMIN takes over administering both. + RoleBasedAccessControl::builder() + .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(admin)) + .role(RoleConfig::new(minter_role).with_admin(minter_admin_role.clone())) + .role(RoleConfig::new(minter_admin_role.clone()).with_admin(minter_admin_role)) + .build()?; + + Ok(()) + } + /// A role whose delegated admin is empty is still manageable as long as the admin itself can /// be populated, which is the case while `ADMIN` is populated. #[test] diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 94345baa98..e05025d64b 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -15,7 +15,8 @@ use crate::account::auth::{ use crate::account::faucets::FungibleFaucet; use crate::account::inspection::CodeInspection; use crate::account::interface::AccountComponentInterface; -use crate::account::wallets::{BasicWallet, NoteCreator}; +use crate::account::note_creator::NoteCreator; +use crate::account::wallets::BasicWallet; // STANDARD ACCOUNT COMPONENTS // ================================================================================================ diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index cd4239e1e4..88a221949d 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -12,7 +12,7 @@ pub enum AccountComponentInterface { /// Exposes procedures from the [`BasicWallet`][crate::account::wallets::BasicWallet] module. BasicWallet, /// Exposes the `create_note` procedure from the - /// [`NoteCreator`][crate::account::wallets::NoteCreator] component. + /// [`NoteCreator`][crate::account::note_creator::NoteCreator] component. NoteCreator, /// Exposes procedures from the /// [`FungibleFaucet`][crate::account::faucets::FungibleFaucet] module. diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index 67a24681ed..eaa78080f1 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -5,6 +5,7 @@ pub mod faucets; pub mod fees; pub mod inspection; pub mod interface; +pub mod note_creator; pub mod policies; pub mod upgrade; pub mod wallets; diff --git a/crates/miden-standards/src/account/wallets/note_creator.rs b/crates/miden-standards/src/account/note_creator.rs similarity index 77% rename from crates/miden-standards/src/account/wallets/note_creator.rs rename to crates/miden-standards/src/account/note_creator.rs index 6e41c23aee..8a70ca6998 100644 --- a/crates/miden-standards/src/account/wallets/note_creator.rs +++ b/crates/miden-standards/src/account/note_creator.rs @@ -7,14 +7,14 @@ use crate::procedure_root; // NOTE CREATOR // ================================================================================================ -account_component_code!(NOTE_CREATOR_CODE, "miden-standards-wallets-note-creator.masp"); +account_component_code!(NOTE_CREATOR_CODE, "miden-standards-note-note-creator.masp"); // PROCEDURE ROOTS // ================================================================================================ /// MASL library namespace used for procedure-root lookups. Distinct from [`NoteCreator::NAME`], /// which mirrors the standards-side MASM module path. -const NOTE_CREATOR_LIBRARY_PATH: &str = "miden::standards::components::wallets::note_creator"; +const NOTE_CREATOR_LIBRARY_PATH: &str = "miden::standards::components::note::note_creator"; // Initialize the procedure root of the `create_note` procedure of the Note Creator only once. procedure_root!( @@ -79,3 +79,20 @@ impl From for AccountComponent { ) } } + +#[cfg(test)] +mod tests { + use super::NoteCreator; + use crate::account::wallets::BasicWallet; + + /// `NoteCreator::create_note_root()` must resolve and equal `BasicWallet`'s `create_note` root. + /// + /// Resolving forces the `procedure_root!` lazy lookup, which panics if `NoteCreator::NAME` does + /// not match the component's package namespace. The equality pins the invariant that the basic + /// wallet re-exports the same `create_note` procedure (identical MAST root), which standard + /// note scripts rely on so that `NoteCreator`-only accounts can consume them. + #[test] + fn note_creator_create_note_root_matches_basic_wallet() { + assert_eq!(NoteCreator::create_note_root(), BasicWallet::create_note_root()); + } +} diff --git a/crates/miden-standards/src/account/wallets/mod.rs b/crates/miden-standards/src/account/wallets/mod.rs index 17284363ab..8244db5ccc 100644 --- a/crates/miden-standards/src/account/wallets/mod.rs +++ b/crates/miden-standards/src/account/wallets/mod.rs @@ -24,9 +24,6 @@ use crate::account::auth::{ }; use crate::procedure_root; -mod note_creator; -pub use note_creator::NoteCreator; - // BASIC WALLET // ================================================================================================ @@ -66,10 +63,10 @@ procedure_root!( /// An [`AccountComponent`] implementing a basic wallet. /// -/// It reexports the procedures from `miden::standards::wallets::basic` and -/// `miden::standards::note::create_note` modules. When linking against this component, the `miden` -/// library (i.e. [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler -/// which is the case when using [`CodeBuilder`][builder]. The procedures of this component are: +/// It reexports the procedures from `miden::standards::wallets::basic` module. When linking against +/// this component, the `miden` library (i.e. [`ProtocolLib`](miden_protocol::ProtocolLib)) must be +/// available to the assembler which is the case when using [`CodeBuilder`][builder]. The procedures +/// of this component are: /// - `receive_asset`, which can be used to add an asset to the account. /// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add /// it to the output note with the specified index. @@ -143,10 +140,11 @@ impl From for AccountComponent { /// Creates a new account with a basic wallet interface, single signature authentication and the /// specified account type. /// -/// The basic wallet interface exposes two procedures: +/// The basic wallet interface exposes three procedures: /// - `receive_asset`, which can be used to add an asset to the account. /// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add /// it to the output note with the specified index. +/// - `create_note`, which can be used to create an output note. /// /// All methods require authentication, which is provided by an [`AuthSingleSig`] component /// configured with the given approver. diff --git a/crates/miden-standards/src/note/costs/table.rs b/crates/miden-standards/src/note/costs/table.rs index 36b613620e..d0dc8056ba 100644 --- a/crates/miden-standards/src/note/costs/table.rs +++ b/crates/miden-standards/src/note/costs/table.rs @@ -2,54 +2,54 @@ // Values are maxima across the benchmarked paths; see `miden_standards::note::costs` for the // caveats on what they do and do not cover. -/// Cycles of consuming a P2ID note: 1 asset 18463, 16 assets 57181 (maximum). -pub const P2ID_CONSUMPTION_CYCLES: u32 = 57181; +/// Cycles of consuming a P2ID note: 1 asset 18643, 16 assets 57706 (maximum). +pub const P2ID_CONSUMPTION_CYCLES: u32 = 57706; -/// Cycles of consuming a P2IDE note: claim 18588, claim with 16 assets 57306 (maximum), reclaim -/// 18743. -pub const P2IDE_CONSUMPTION_CYCLES: u32 = 57306; +/// Cycles of consuming a P2IDE note: claim 18768, claim with 16 assets 57831 (maximum), reclaim +/// 18923. +pub const P2IDE_CONSUMPTION_CYCLES: u32 = 57831; -/// Cycles of consuming a SWAP note: public payback 22504 (maximum), private payback 21990. -pub const SWAP_CONSUMPTION_CYCLES: u32 = 22504; +/// Cycles of consuming a SWAP note: public payback 22728 (maximum), private payback 22214. +pub const SWAP_CONSUMPTION_CYCLES: u32 = 22728; -/// Cycles of consuming a PSWAP note: full fill 25034, partial fill 28989 (maximum). -pub const PSWAP_CONSUMPTION_CYCLES: u32 = 28989; +/// Cycles of consuming a PSWAP note: full fill 25258, partial fill 29234 (maximum). +pub const PSWAP_CONSUMPTION_CYCLES: u32 = 29234; -/// Cycles of consuming a MINT note: fungible faucet 31783, non-fungible faucet 34662 (maximum). -pub const MINT_CONSUMPTION_CYCLES: u32 = 34662; +/// Cycles of consuming a MINT note: fungible faucet 32262, non-fungible faucet 35152 (maximum). +pub const MINT_CONSUMPTION_CYCLES: u32 = 35152; /// Cycles of consuming a BURN note (single benchmarked path). -pub const BURN_CONSUMPTION_CYCLES: u32 = 28445; +pub const BURN_CONSUMPTION_CYCLES: u32 = 28776; /// Cycles of consuming a CONSTANT_FEE_POLICY_CONFIG note (single benchmarked path). -pub const CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES: u32 = 19800; +pub const CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES: u32 = 20052; /// Cycles of consuming a FAUCET_POLICY_CONFIG note (single benchmarked path). -pub const FAUCET_POLICY_CONFIG_CONSUMPTION_CYCLES: u32 = 28369; +pub const FAUCET_POLICY_CONFIG_CONSUMPTION_CYCLES: u32 = 28632; /// Cycles of consuming a FAUCET_METADATA_CONFIG note (single benchmarked path). -pub const FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES: u32 = 27190; +pub const FAUCET_METADATA_CONFIG_CONSUMPTION_CYCLES: u32 = 27598; /// Cycles of consuming a MIN_BURN_AMOUNT_CONFIG note (single benchmarked path). pub const MIN_BURN_AMOUNT_CONFIG_CONSUMPTION_CYCLES: u32 = 25943; /// Cycles of consuming an ALLOWLIST_CONFIG note (single benchmarked path). -pub const ALLOWLIST_CONFIG_CONSUMPTION_CYCLES: u32 = 26477; +pub const ALLOWLIST_CONFIG_CONSUMPTION_CYCLES: u32 = 26693; /// Cycles of consuming a BLOCKLIST_CONFIG note (single benchmarked path). -pub const BLOCKLIST_CONFIG_CONSUMPTION_CYCLES: u32 = 26477; +pub const BLOCKLIST_CONFIG_CONSUMPTION_CYCLES: u32 = 26693; /// Cycles of consuming a PAUSE_CONFIG note (single benchmarked path). -pub const PAUSE_CONFIG_CONSUMPTION_CYCLES: u32 = 18427; +pub const PAUSE_CONFIG_CONSUMPTION_CYCLES: u32 = 18643; /// Cycles of consuming an OWNER_CONFIG note (single benchmarked path). -pub const OWNER_CONFIG_CONSUMPTION_CYCLES: u32 = 18021; +pub const OWNER_CONFIG_CONSUMPTION_CYCLES: u32 = 18237; /// Cycles of consuming an RBAC_CONFIG note (single benchmarked path). -pub const RBAC_CONFIG_CONSUMPTION_CYCLES: u32 = 21283; +pub const RBAC_CONFIG_CONSUMPTION_CYCLES: u32 = 22101; /// Cycles of consuming a NETWORK_ACCOUNT_CONFIG note (single benchmarked path). -pub const NETWORK_ACCOUNT_CONFIG_CONSUMPTION_CYCLES: u32 = 18921; +pub const NETWORK_ACCOUNT_CONFIG_CONSUMPTION_CYCLES: u32 = 19137; /// Cycles of consuming a FEE_SPONSORSHIP note (single benchmarked path). -pub const FEE_SPONSORSHIP_CONSUMPTION_CYCLES: u32 = 21257; +pub const FEE_SPONSORSHIP_CONSUMPTION_CYCLES: u32 = 21463; 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 75b3063fc0..613aafba86 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -1660,10 +1660,7 @@ async fn test_was_procedure_called() -> anyhow::Result<()> { // Create mock transaction and execute let mock_tx = TestTransactionBuilder::new(account).tx_script(tx_script).build().unwrap(); - mock_tx - .execute() - .await - .map_err(|err| anyhow::anyhow!("Failed to execute transaction: {err}"))?; + mock_tx.execute().await?; Ok(()) } @@ -1865,10 +1862,7 @@ async fn test_has_procedure() -> anyhow::Result<()> { // Create mock transaction and execute let mock_tx = TestTransactionBuilder::new(account).tx_script(tx_script).build().unwrap(); - mock_tx - .execute() - .await - .map_err(|err| anyhow::anyhow!("Failed to execute transaction: {err}"))?; + mock_tx.execute().await?; Ok(()) } diff --git a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs index c31e81ad7c..be26cfe433 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -30,7 +30,6 @@ use miden_protocol::asset::{ }; use miden_protocol::block::account_tree::AccountIdKey; use miden_protocol::errors::MasmError; -use miden_protocol::errors::tx_kernel::ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT; use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; @@ -91,7 +90,7 @@ end #! Checks whether the receiving account is in the block list. If so, panics. #! #! Inputs: [ASSET_ID, ASSET_VALUE, pad(8)] -#! Outputs: [ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Invocation: call @account_procedure @@ -99,9 +98,8 @@ pub proc on_before_asset_added_to_account exec.assert_native_account_not_blocked # => [ASSET_ID, ASSET_VALUE, pad(8)] - # drop unused asset ID - dropw - # => [ASSET_VALUE, pad(12)] + dropw dropw + # => [pad(16)] end #! Callback invoked when an asset with callbacks enabled is added to an output note. @@ -109,7 +107,7 @@ end #! Checks whether the native account (the note creator) is in the block list. If so, panics. #! #! Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] -#! Outputs: [ASSET_VALUE, pad(12)] +#! Outputs: [pad(16)] #! #! Invocation: call @account_procedure @@ -117,9 +115,8 @@ pub proc on_before_asset_added_to_note exec.assert_native_account_not_blocked # => [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] - # drop unused asset ID - dropw - # => [ASSET_VALUE, note_idx, pad(7)] + dropw dropw drop + # => [pad(16)] end "#; @@ -322,7 +319,7 @@ async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs( let account_callback_masm = format!( r#" #! Inputs: [ASSET_ID, ASSET_VALUE, pad(8)] - #! Outputs: [ASSET_VALUE, pad(12)] + #! Outputs: [pad(16)] @account_procedure pub proc on_before_asset_added_to_account # Assert native account ID can be retrieved via native_account::get_id @@ -332,23 +329,19 @@ async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs( push.{wallet_id_prefix} assert_eq.err="callback received unexpected native account ID prefix" # => [ASSET_ID, ASSET_VALUE, pad(8)] - # duplicate the asset value for returning - dupw.1 swapw - # => [ASSET_ID, ASSET_VALUE, ASSET_VALUE, pad(8)] - # build the expected asset push.{amount} exec.::miden::protocol::active_account::get_id - # => [active_account_id_suffix, active_account_id_prefix, amount, ASSET_ID, ASSET_VALUE, ASSET_VALUE, pad(8)] + # => [active_account_id_suffix, active_account_id_prefix, amount, ASSET_ID, ASSET_VALUE, pad(8)] exec.::miden::standards::assets::fungible_asset::create - # => [EXPECTED_ASSET_ID, EXPECTED_ASSET_VALUE, ASSET_ID, ASSET_VALUE, ASSET_VALUE, pad(8)] + # => [EXPECTED_ASSET_ID, EXPECTED_ASSET_VALUE, ASSET_ID, ASSET_VALUE, pad(8)] movupw.2 assert_eqw.err="callback received unexpected asset ID" - # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, ASSET_VALUE, pad(8)] + # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, pad(8)] assert_eqw.err="callback received unexpected asset value" - # => [ASSET_VALUE, pad(12)] + # => [pad(16)] end "# ); @@ -381,66 +374,6 @@ async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs( Ok(()) } -/// Tests that the account callback cannot change the value of an asset added to the account -/// vault, even when offsetting rewrites would leave the aggregate totals intact. -/// -/// The two consumed notes add 200 and 100 units to the vault; the callback swaps the amounts by -/// rewriting them to 100 and 200 units. Because the vault aggregates amounts per asset ID, the -/// final vault - and thus the epilogue's conservation check - is identical either way. Unlike the -/// note path, there is also no host-side backstop: `ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT` is -/// emitted after the callback with the processed value, so the host never disagrees with the -/// kernel. The callback-boundary assertion is therefore the only enforcement on this path. -#[tokio::test] -async fn test_callback_cannot_rewrite_value_added_to_account() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; - - let account_callback_masm = r#" - #! Inputs: [ASSET_ID, ASSET_VALUE, pad(8)] - #! Outputs: [PROCESSED_ASSET_VALUE, pad(12)] - @account_procedure - pub proc on_before_asset_added_to_account - # Drop the asset ID and swap amounts 200 and 100 while preserving their total. - dropw - neg add.300 - # => [PROCESSED_ASSET_VALUE, pad(8)] - end - "#; - - let faucet = add_faucet_with_callbacks(&mut builder, Some(account_callback_masm), None)?; - let first_asset = FungibleAsset::new(faucet.id(), 200)?; - let second_asset = FungibleAsset::new(faucet.id(), 100)?; - let first_note = builder.add_p2id_note( - faucet.id(), - target_account.id(), - &[first_asset.into()], - NoteType::Public, - )?; - let second_note = builder.add_p2id_note( - faucet.id(), - target_account.id(), - &[second_asset.into()], - NoteType::Public, - )?; - - let mut mock_chain = builder.build()?; - mock_chain.prove_next_block()?; - - let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; - let result = mock_chain - .build_transaction(target_account.id()) - .authenticated_input_notes([first_note.id(), second_note.id()]) - .foreign_accounts(vec![faucet_inputs]) - .build()? - .execute() - .await; - - assert_transaction_executor_error!(result, ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT); - - Ok(()) -} - /// Tests that a blocked account cannot receive an asset with callbacks enabled. #[rstest::rstest] #[case::fungible( @@ -584,7 +517,7 @@ async fn test_on_before_asset_added_to_note_callback_receives_correct_inputs() - const ERR_WRONG_NOTE_IDX = "callback received unexpected note_idx" #! Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] - #! Outputs: [ASSET_VALUE, pad(12)] + #! Outputs: [pad(16)] @account_procedure pub proc on_before_asset_added_to_note # Assert native account ID can be retrieved via native_account::get_id @@ -598,23 +531,22 @@ async fn test_on_before_asset_added_to_note_callback_receives_correct_inputs() - dup.8 push.1 assert_eq.err=ERR_WRONG_NOTE_IDX # => [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] - # duplicate the asset value for returning - dupw.1 swapw - # => [ASSET_ID, ASSET_VALUE, ASSET_VALUE, note_idx, pad(7)] - # build the expected asset push.{amount} exec.::miden::protocol::active_account::get_id - # => [active_account_id_suffix, active_account_id_prefix, amount, ASSET_ID, ASSET_VALUE, ASSET_VALUE, note_idx, pad(7)] + # => [active_account_id_suffix, active_account_id_prefix, amount, ASSET_ID, ASSET_VALUE, note_idx, pad(7)] exec.::miden::standards::assets::fungible_asset::create - # => [EXPECTED_ASSET_ID, EXPECTED_ASSET_VALUE, ASSET_ID, ASSET_VALUE, ASSET_VALUE, note_idx, pad(7)] + # => [EXPECTED_ASSET_ID, EXPECTED_ASSET_VALUE, ASSET_ID, ASSET_VALUE, note_idx, pad(7)] movupw.2 assert_eqw.err="callback received unexpected asset ID" - # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, ASSET_VALUE, note_idx, pad(7)] + # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, note_idx, pad(7)] assert_eqw.err="callback received unexpected asset value" - # => [ASSET_VALUE, note_idx, pad(7)] + # => [note_idx, pad(7)] + + drop + # => [pad(16)] end "# ); @@ -676,84 +608,6 @@ async fn test_on_before_asset_added_to_note_callback_receives_correct_inputs() - Ok(()) } -/// Tests that callbacks cannot redistribute value between output notes while preserving the -/// transaction-wide total. -/// -/// Without a callback-boundary equality check, the callback below swaps additions of 200 and 100 -/// units. The epilogue's aggregate asset-conservation check would still see 300 -/// input and 300 output units, so it cannot enforce the per-callback invariant. -/// -/// The executor also notices the rewrite today: `NOTE_BEFORE_ADD_ASSET_EVENT` is emitted before the -/// callback runs, so the host records the pre-callback amounts and its output-notes commitment ends -/// up disagreeing with the kernel's. That reconciliation is not proof-enforced, which is why this -/// test asserts on the kernel error rather than on the resulting commitment mismatch. -#[tokio::test] -async fn test_callback_cannot_redistribute_value_between_output_notes() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; - - let note_callback_masm = r#" - #! Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] - #! Outputs: [PROCESSED_ASSET_VALUE, pad(12)] - @account_procedure - pub proc on_before_asset_added_to_note - # Drop the asset ID and swap amounts 200 and 100 while preserving their total. - dropw - neg add.300 - # => [PROCESSED_ASSET_VALUE, note_idx, pad(7)] - end - "#; - - let faucet = add_faucet_with_callbacks(&mut builder, None, Some(note_callback_masm))?; - let input_asset = FungibleAsset::new(faucet.id(), 300)?; - let first_moved_asset = FungibleAsset::new(faucet.id(), 200)?; - let second_moved_asset = FungibleAsset::new(faucet.id(), 100)?; - let input_note = builder.add_p2id_note( - faucet.id(), - target_account.id(), - &[input_asset.into()], - NoteType::Public, - )?; - - let mut mock_chain = builder.build()?; - mock_chain.prove_next_block()?; - - let tx_script = CodeBuilder::with_mock_packages().compile_tx_script(format!( - r#" - use mock::util - - @transaction_script - pub proc main - push.{first_asset_value} - push.{asset_id} - exec.util::create_default_note_with_moved_asset - - push.{second_asset_value} - push.{asset_id} - exec.util::create_default_note_with_moved_asset - end - "#, - first_asset_value = first_moved_asset.to_value_word(), - second_asset_value = second_moved_asset.to_value_word(), - asset_id = first_moved_asset.to_id_word(), - ))?; - - let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; - let result = mock_chain - .build_transaction(target_account.id()) - .authenticated_input_note(input_note.id()) - .tx_script(tx_script) - .foreign_accounts(vec![faucet_inputs]) - .build()? - .execute() - .await; - - assert_transaction_executor_error!(result, ERR_FAUCET_CALLBACK_ASSET_VALUE_MUST_MATCH_INPUT); - - Ok(()) -} - /// Tests that consuming a callbacks-enabled asset succeeds when the issuing faucet is itself the /// target of the callback. /// @@ -764,21 +618,21 @@ async fn test_faucet_with_callback_calls_itself() -> anyhow::Result<()> { let account_callback_masm = r#" #! Inputs: [ASSET_ID, ASSET_VALUE, pad(8)] - #! Outputs: [ASSET_VALUE, pad(12)] + #! Outputs: [pad(16)] @account_procedure pub proc on_before_asset_added_to_account - dropw - # => [ASSET_VALUE, pad(12)] + dropw dropw + # => [pad(16)] end "#; let note_callback_masm = r#" #! Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] - #! Outputs: [ASSET_VALUE, pad(12)] + #! Outputs: [pad(16)] @account_procedure pub proc on_before_asset_added_to_note - dropw movup.4 drop - # => [ASSET_VALUE, pad(12)] + dropw dropw drop + # => [pad(16)] end "#; 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 bb05ac9ac9..e218f08196 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 @@ -1272,7 +1272,7 @@ async fn test_add_attachment_with_invalid_num_elements_fails( let code = format!( " use miden::protocol::output_note - use {{DEFAULT_TAG}} from miden::standards::note_tag + use {{DEFAULT_TAG}} from miden::standards::note::note_tag use miden::tx_kernel_core::prologue use mock::util @@ -1305,7 +1305,7 @@ async fn test_add_attachment_with_scheme_zero_fails() -> anyhow::Result<()> { let code = " use miden::protocol::output_note - use {DEFAULT_TAG} from miden::standards::note_tag + use {DEFAULT_TAG} from miden::standards::note::note_tag use miden::tx_kernel_core::prologue use mock::util @@ -1982,7 +1982,7 @@ async fn test_add_attachments_with_too_many_overall_elements_fails() -> anyhow:: let code = format!( " use miden::protocol::output_note - use {{DEFAULT_TAG}} from miden::standards::note_tag + use {{DEFAULT_TAG}} from miden::standards::note::note_tag use miden::tx_kernel_core::prologue use mock::util diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 1474a0aea3..272bab1934 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -59,13 +59,14 @@ use miden_standards::account::access::{AccessControl, Authority, Pausable, Pausa use miden_standards::account::auth::SponsorshipPolicy; use miden_standards::account::faucets::{FungibleFaucet, NonFungibleFaucet, TokenName}; use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager}; +use miden_standards::account::note_creator::NoteCreator; use miden_standards::account::policies::{ BurnPolicy, MintPolicy, TokenPolicyManager, TransferPolicy, }; -use miden_standards::account::wallets::{BasicWallet, NoteCreator}; +use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{ BurnNote, MintNote, diff --git a/crates/miden-testing/src/standards/note_tag.rs b/crates/miden-testing/src/standards/note_tag.rs index 27af5b8c23..ccf8afa93c 100644 --- a/crates/miden-testing/src/standards/note_tag.rs +++ b/crates/miden-testing/src/standards/note_tag.rs @@ -19,7 +19,7 @@ async fn test_note_tag_account_target(#[case] tag_len: u8) -> anyhow::Result<()> let code = format!( " use miden::core::sys - use miden::standards::note_tag + use miden::standards::note::note_tag begin push.{id_prefix} @@ -53,7 +53,7 @@ async fn test_note_tag_account_target_fails_for_large_tag_len() -> anyhow::Resul let code = format!( " use miden::core::sys - use miden::standards::note_tag + use miden::standards::note::note_tag begin # account ID prefix doesn't matter for this test diff --git a/crates/miden-testing/tests/scripts/authority.rs b/crates/miden-testing/tests/scripts/authority.rs index c94014df2e..fbf230f77e 100644 --- a/crates/miden-testing/tests/scripts/authority.rs +++ b/crates/miden-testing/tests/scripts/authority.rs @@ -366,3 +366,87 @@ async fn freeze_and_unfreeze_use_distinct_roles() -> anyhow::Result<()> { Ok(()) } + +/// An actor holding only the `FREEZER` authorization, capable of nothing but freezing, +/// can flip the kill switch; it can never unfreeze the account, nor authorize any other +/// protected procedure. +#[tokio::test] +async fn freezer_can_freeze_but_cannot_unfreeze_or_authorize() -> anyhow::Result<()> { + let freezer = test_account_id(25); + let unfreezer = test_account_id(26); + + // `freeze` is the freezer's only reachable procedure: `unfreeze` carries its own role and + // `pause` is unmapped, so it falls back to ADMIN. + let roles = BTreeMap::from([ + (Authority::freeze_root(), role("FREEZER")), + (Authority::unfreeze_root(), role("UNFREEZER")), + ]); + + let admin = *ADMIN_ID; + let mut builder = MockChain::builder(); + let faucet = add_rbac_faucet(&mut builder, admin, roles, 66)?; + + let grant_freezer = build_grant_role_note(admin, &role("FREEZER"), freezer)?; + let grant_unfreezer = build_grant_role_note(admin, &role("UNFREEZER"), unfreezer)?; + let freezer_pause_note = build_pause_note(freezer)?; + let freezer_freeze_note = build_freeze_note(freezer)?; + let freezer_unfreeze_note = build_unfreeze_note(freezer)?; + let admin_unfreeze_note = build_unfreeze_note(admin)?; + let unfreezer_unfreeze_note = build_unfreeze_note(unfreezer)?; + for note in [ + &grant_freezer, + &grant_unfreezer, + &freezer_pause_note, + &freezer_freeze_note, + &freezer_unfreeze_note, + &admin_unfreeze_note, + &unfreezer_unfreeze_note, + ] { + builder.add_output_note(RawOutputNote::Full(note.clone())); + } + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + execute_note_on_faucet(&mut mock_chain, faucet.id(), &grant_freezer).await?; + execute_note_on_faucet(&mut mock_chain, faucet.id(), &grant_unfreezer).await?; + + // The freezer holds no other role, so ordinary gated procedures stay out of reach. + let freezer_pause_result = mock_chain + .build_transaction(faucet.id()) + .authenticated_input_note(freezer_pause_note.id()) + .build()? + .execute() + .await; + assert_transaction_executor_error!(freezer_pause_result, ERR_SENDER_LACKS_ROLE); + + // The freezer trips the emergency switch. + execute_note_on_faucet(&mut mock_chain, faucet.id(), &freezer_freeze_note).await?; + assert!(is_frozen(&mock_chain, faucet.id())?); + + // But it cannot re-open the account: `unfreeze` requires UNFREEZER. + let freezer_unfreeze_result = mock_chain + .build_transaction(faucet.id()) + .authenticated_input_note(freezer_unfreeze_note.id()) + .build()? + .execute() + .await; + assert_transaction_executor_error!(freezer_unfreeze_result, ERR_SENDER_LACKS_ROLE); + assert!(is_frozen(&mock_chain, faucet.id())?); + + // Neither can the ADMIN, since `unfreeze` is explicitly mapped and so never falls back to it. + let admin_unfreeze_result = mock_chain + .build_transaction(faucet.id()) + .authenticated_input_note(admin_unfreeze_note.id()) + .build()? + .execute() + .await; + assert_transaction_executor_error!(admin_unfreeze_result, ERR_SENDER_LACKS_ROLE); + assert!(is_frozen(&mock_chain, faucet.id())?); + + // Only the UNFREEZER re-opens the account. + execute_note_on_faucet(&mut mock_chain, faucet.id(), &unfreezer_unfreeze_note).await?; + assert!(!is_frozen(&mock_chain, faucet.id())?); + + Ok(()) +} diff --git a/crates/miden-testing/tests/scripts/fee_collection.rs b/crates/miden-testing/tests/scripts/fee_collection.rs index d85a29d252..a4da54c4f5 100644 --- a/crates/miden-testing/tests/scripts/fee_collection.rs +++ b/crates/miden-testing/tests/scripts/fee_collection.rs @@ -19,7 +19,8 @@ use miden_protocol::transaction::{RawOutputNote, RawOutputNotes, TransactionScri use miden_protocol::{Felt, Word}; use miden_standards::account::auth::{AuthNetworkAccount, NetworkAccount, SponsorshipPolicy}; use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicy, FeePolicyManager}; -use miden_standards::account::wallets::{BasicWallet, NoteCreator}; +use miden_standards::account::note_creator::NoteCreator; +use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ ERR_FEE_MANAGER_EXPECTED_FEE_ASSET_MISMATCH, @@ -974,7 +975,7 @@ fn create_network_notes_tx_script( use miden::protocol::output_note use miden::standards::attachments::network_account_target - use miden::standards::note_tag + use miden::standards::note::note_tag use {{NOTE_TYPE_PUBLIC}} from miden::protocol::note diff --git a/crates/miden-testing/tests/scripts/rbac/config.rs b/crates/miden-testing/tests/scripts/rbac/config.rs index ddde677ada..5d05517dc1 100644 --- a/crates/miden-testing/tests/scripts/rbac/config.rs +++ b/crates/miden-testing/tests/scripts/rbac/config.rs @@ -19,7 +19,7 @@ use miden_testing::{MockChain, assert_transaction_executor_error}; // The RBAC account and storage-getter helpers are shared with the parent `rbac` suite, which // owns the exhaustive tests of the underlying component. This suite only checks that the // RbacConfig note dispatches each action and rejects malformed notes. -use super::{create_rbac_chain, get_role_config, is_role_member, role, test_account_id}; +use super::{create_rbac_chain, get_role_admin, is_role_member, role, test_account_id}; // HELPERS // ================================================================================================ @@ -132,7 +132,7 @@ async fn set_role_admin_dispatch() -> anyhow::Result<()> { )?; let updated = execute_note_and_apply(&mock_chain, &account, note).await?; - let (_, admin_role_symbol) = get_role_config(&updated, &minter)?; + let admin_role_symbol = get_role_admin(&updated, &minter)?; assert_eq!(admin_role_symbol, mint_admin.as_element()); Ok(()) } diff --git a/crates/miden-testing/tests/scripts/rbac/mod.rs b/crates/miden-testing/tests/scripts/rbac/mod.rs index b5618498a7..abcf3ce920 100644 --- a/crates/miden-testing/tests/scripts/rbac/mod.rs +++ b/crates/miden-testing/tests/scripts/rbac/mod.rs @@ -82,6 +82,17 @@ pub(super) fn get_role_config( Ok((word[0], word[1])) } +/// Returns the number of accounts holding the role, per on-chain storage. +pub(super) fn get_role_member_count(account: &Account, role: &RoleSymbol) -> anyhow::Result { + Ok(get_role_config(account, role)?.0) +} + +/// Returns the role's delegated admin role symbol, or `Felt::ZERO` when it is unset, per on-chain +/// storage. +pub(super) fn get_role_admin(account: &Account, role: &RoleSymbol) -> anyhow::Result { + Ok(get_role_config(account, role)?.1) +} + pub(crate) fn is_role_member( account: &Account, role: &RoleSymbol, @@ -420,7 +431,7 @@ async fn test_rbac_grant_role_sets_membership() -> anyhow::Result<()> { let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; assert!(is_role_member(&granted, &minter, member)?); - let (member_count, _) = get_role_config(&granted, &minter)?; + let member_count = get_role_member_count(&granted, &minter)?; assert_eq!(member_count, Felt::ONE); Ok(()) @@ -443,7 +454,7 @@ async fn test_rbac_grant_existing_member_is_noop() -> anyhow::Result<()> { let regrant_note = build_note(admin, grant_minter_to_member)?; let regranted = execute_note_and_apply(&mock_chain, &granted, ®rant_note).await?; - let (member_count, _) = get_role_config(®ranted, &minter)?; + let member_count = get_role_member_count(®ranted, &minter)?; assert_eq!(member_count, Felt::from(1u32)); assert!(is_role_member(®ranted, &minter, member)?); @@ -462,21 +473,21 @@ async fn test_rbac_member_count_tracks_grants_and_revokes() -> anyhow::Result<() let first_grant = build_note(admin, grant_role_script(&pauser, alice))?; let updated = execute_note_and_apply(&mock_chain, &account, &first_grant).await?; - assert_eq!(get_role_config(&updated, &pauser)?.0, Felt::from(1u32)); + assert_eq!(get_role_member_count(&updated, &pauser)?, Felt::from(1u32)); let second_grant = build_note(admin, grant_role_script(&pauser, bob))?; let updated = execute_note_and_apply(&mock_chain, &updated, &second_grant).await?; - assert_eq!(get_role_config(&updated, &pauser)?.0, Felt::from(2u32)); + assert_eq!(get_role_member_count(&updated, &pauser)?, Felt::from(2u32)); let revoke_alice = build_note(admin, revoke_role_script(&pauser, alice))?; let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_alice).await?; - assert_eq!(get_role_config(&updated, &pauser)?.0, Felt::from(1u32)); + assert_eq!(get_role_member_count(&updated, &pauser)?, Felt::from(1u32)); assert!(!is_role_member(&updated, &pauser, alice)?); assert!(is_role_member(&updated, &pauser, bob)?); let revoke_bob = build_note(admin, revoke_role_script(&pauser, bob))?; let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_bob).await?; - assert_eq!(get_role_config(&updated, &pauser)?.0, Felt::from(0u32)); + assert_eq!(get_role_member_count(&updated, &pauser)?, Felt::from(0u32)); assert!(!is_role_member(&updated, &pauser, bob)?); Ok(()) @@ -570,7 +581,7 @@ async fn test_rbac_revoke_role_clears_membership() -> anyhow::Result<()> { let revoke_note = build_note(admin, revoke_role_script(&burner, member))?; let revoked = execute_note_and_apply(&mock_chain, &granted, &revoke_note).await?; assert!(!is_role_member(&revoked, &burner, member)?); - assert_eq!(get_role_config(&revoked, &burner)?.0, Felt::from(0u32)); + assert_eq!(get_role_member_count(&revoked, &burner)?, Felt::from(0u32)); Ok(()) } @@ -715,7 +726,7 @@ async fn test_rbac_set_role_admin_does_not_create_role() -> anyhow::Result<()> { let (user_count, user_admin) = get_role_config(&updated, &user_role)?; assert_eq!(user_count, Felt::from(0u32)); assert_eq!(user_admin, Felt::from(&manager_role)); - let (manager_count, _) = get_role_config(&updated, &manager_role)?; + let manager_count = get_role_member_count(&updated, &manager_role)?; assert_eq!(manager_count, Felt::from(0u32)); Ok(()) @@ -734,7 +745,7 @@ async fn test_rbac_granting_admin_role_does_not_change_target_role_admin_config( let set_admin_note = build_note(admin, set_role_admin_script(&user_role, Some(&manager_role)))?; let updated = execute_note_and_apply(&mock_chain, &account, &set_admin_note).await?; - assert_eq!(get_role_config(&updated, &user_role)?.1, Felt::from(&manager_role)); + assert_eq!(get_role_admin(&updated, &user_role)?, Felt::from(&manager_role)); let grant_manager_note = build_note(admin, grant_role_script(&manager_role, delegate))?; let updated = execute_note_and_apply(&mock_chain, &updated, &grant_manager_note).await?; @@ -905,7 +916,7 @@ async fn test_rbac_admin_can_renounce_admin_role() -> anyhow::Result<()> { let renounce_admin2_note = build_note(admin2, renounce_role_script(&admin_role))?; let updated = execute_note_and_apply(&mock_chain, &updated, &renounce_admin2_note).await?; assert!(!is_role_member(&updated, &admin_role, admin2)?); - assert_eq!(get_role_config(&updated, &admin_role)?.0, Felt::from(0u32)); + assert_eq!(get_role_member_count(&updated, &admin_role)?, Felt::from(0u32)); // ADMIN is now unmanageable: granting an ADMIN-administered role fails for everyone. let orphan_grant_note = build_note(admin2, grant_role_script(&pauser, orphan))?; @@ -1044,3 +1055,99 @@ async fn test_rbac_self_administered_role_survives_admin_renounce() -> anyhow::R Ok(()) } + +/// Delegating to a memberless role does not put the delegated role out of `ADMIN`'s reach: a +/// memberless role can authorize nothing, so authority falls back to `ADMIN` until the delegate +/// gains its first member, at which point it takes over exclusively. +#[tokio::test] +async fn test_rbac_admin_retains_authority_while_delegated_admin_is_memberless() +-> anyhow::Result<()> { + let admin = test_account_id(210); + let mint_admin_member = test_account_id(211); + let member = test_account_id(212); + let second_member = test_account_id(213); + + let minter = role("MINTER"); + let mint_admin = role("MINT_ADMIN"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + // MINTER is delegated to MINT_ADMIN, which has no members — an unpopulated or mistyped role. + let set_admin_note = build_note(admin, set_role_admin_script(&minter, Some(&mint_admin)))?; + let updated = execute_note_and_apply(&mock_chain, &account, &set_admin_note).await?; + assert_eq!(get_role_member_count(&updated, &mint_admin)?, Felt::ZERO); + + // ADMIN keeps authority over MINTER while MINTER's delegated admin is memberless. + let grant_minter_note = build_note(admin, grant_role_script(&minter, member))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_minter_note).await?; + assert!(is_role_member(&updated, &minter, member)?); + + // Once MINT_ADMIN gains a member it administers MINTER exclusively, locking ADMIN out again. + let grant_admin_note = build_note(admin, grant_role_script(&mint_admin, mint_admin_member))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_admin_note).await?; + + let admin_grant_note = build_note(admin, grant_role_script(&minter, second_member))?; + let result = mock_chain + .build_transaction(updated.clone()) + .unauthenticated_input_note(admin_grant_note) + .build()? + .execute() + .await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROLE_ADMIN); + + let delegate_grant_note = + build_note(mint_admin_member, grant_role_script(&minter, second_member))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &delegate_grant_note).await?; + assert!(is_role_member(&updated, &minter, second_member)?); + + Ok(()) +} + +/// Regression test: a delegated role does not become permanently unmanageable when its admin chain +/// empties out. Authority over the roles a memberless role administers falls back to `ADMIN`, which +/// can then manage the delegated role, re-point its delegation, and repopulate the dead admin role. +#[tokio::test] +async fn test_rbac_admin_recovers_role_from_dead_admin_chain() -> anyhow::Result<()> { + let admin = test_account_id(214); + let mint_admin_member = test_account_id(215); + let member = test_account_id(216); + + let minter = role("MINTER"); + let mint_admin = role("MINT_ADMIN"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + // ADMIN delegates MINTER to MINT_ADMIN and seeds MINT_ADMIN, so MINTER is exclusively + // MINT_ADMIN's and out of ADMIN's reach. + let set_admin_note = build_note(admin, set_role_admin_script(&minter, Some(&mint_admin)))?; + let updated = execute_note_and_apply(&mock_chain, &account, &set_admin_note).await?; + let grant_admin_note = build_note(admin, grant_role_script(&mint_admin, mint_admin_member))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_admin_note).await?; + + // MINT_ADMIN administers itself, so once it empties no live role administers it either. + let self_admin_note = build_note(admin, set_role_admin_script(&mint_admin, Some(&mint_admin)))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &self_admin_note).await?; + + // MINT_ADMIN's last member renounces, so MINTER's effective admin is memberless. + let renounce_note = build_note(mint_admin_member, renounce_role_script(&mint_admin))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &renounce_note).await?; + assert_eq!(get_role_member_count(&updated, &mint_admin)?, Felt::ZERO); + + // ADMIN regains authority over MINTER: it can manage membership... + let grant_minter_note = build_note(admin, grant_role_script(&minter, member))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_minter_note).await?; + assert!(is_role_member(&updated, &minter, member)?); + + // ...and re-point the delegation back to itself. + let clear_note = build_note(admin, set_role_admin_script(&minter, None))?; + let updated = execute_note_and_apply(&mock_chain, &updated, &clear_note).await?; + assert_eq!(get_role_admin(&updated, &minter)?, Felt::ZERO); + + // The dead admin role is recoverable too: its own effective admin is memberless, so ADMIN can + // repopulate it. + let regrant_note = build_note(admin, grant_role_script(&mint_admin, mint_admin_member))?; + let updated = execute_note_and_apply(&mock_chain, &updated, ®rant_note).await?; + assert!(is_role_member(&updated, &mint_admin, mint_admin_member)?); + + Ok(()) +} diff --git a/crates/miden-tx/src/host/account_update_tracker.rs b/crates/miden-tx/src/host/account_update_tracker.rs index 43de27f93c..cba65620fc 100644 --- a/crates/miden-tx/src/host/account_update_tracker.rs +++ b/crates/miden-tx/src/host/account_update_tracker.rs @@ -1,9 +1,16 @@ use miden_protocol::Felt; -use miden_protocol::account::{AccountCode, AccountDelta, AccountId, AccountPatch, PartialAccount}; +use miden_protocol::account::{ + AccountCode, + AccountDelta, + AccountId, + AccountPatch, + AssetDelta, + PartialAccount, +}; use crate::TransactionKernelError; use crate::host::storage_patch_tracker::StoragePatchTracker; -use crate::host::tx_event::{AssetDelta, AssetPatch}; +use crate::host::tx_event::AssetPatch; use crate::host::vault_update_tracker::VaultUpdateTracker; // ACCOUNT DELTA TRACKER @@ -62,8 +69,10 @@ impl AccountUpdateTracker { self.vault.update_patch(patch) } - /// Updates the vault delta. - pub fn update_asset_delta(&mut self, delta: AssetDelta) { + /// Updates the vault delta, overwriting the previous delta of the same asset. + /// + /// Returns the overwritten delta, if the asset was already present. + pub fn update_asset_delta(&mut self, delta: AssetDelta) -> Option { self.vault.update_delta(delta) } diff --git a/crates/miden-tx/src/host/mod.rs b/crates/miden-tx/src/host/mod.rs index 32e30e0335..7aacebd438 100644 --- a/crates/miden-tx/src/host/mod.rs +++ b/crates/miden-tx/src/host/mod.rs @@ -42,6 +42,7 @@ use miden_protocol::account::{ AccountId, AccountPatch, AccountStorageHeader, + AssetDelta, PartialAccount, StorageMapKey, StorageSlotHeader, @@ -68,7 +69,7 @@ pub(crate) use tx_event::{ pub use tx_progress::TransactionProgress; use crate::errors::TransactionKernelError; -use crate::host::tx_event::{AssetDelta, AssetPatch}; +use crate::host::tx_event::AssetPatch; // TRANSACTION BASE HOST // ================================================================================================ @@ -430,7 +431,13 @@ impl<'store, STORE> TransactionBaseHost<'store, STORE> { &mut self, delta: AssetDelta, ) -> Result, TransactionKernelError> { - self.update_tracker.update_asset_delta(delta); + // SAFETY: The kernel iterates the asset delta map once per computation and the host resets + // its accumulated delta before each computation, so no asset should be reported + // twice. + assert!( + self.update_tracker.update_asset_delta(delta).is_none(), + "each asset ID should be unique" + ); Ok(Vec::new()) } diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index c6ee2d904b..96b76aff37 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -8,6 +8,7 @@ use miden_protocol::account::auth::{PublicKeyCommitment, Signature}; use miden_protocol::account::delta::AssetDeltaOperation; use miden_protocol::account::{ AccountId, + AssetDelta, StorageMap, StorageMapKey, StorageSlotName, @@ -287,7 +288,7 @@ impl TransactionEvent { })?; TransactionEvent::AccountOnAssetDeltaComputation { - delta: AssetDelta { delta_op, asset }, + delta: AssetDelta::new(delta_op, asset), } }), TransactionEventId::AccountVaultBeforeGetAsset => { @@ -618,7 +619,7 @@ impl TxSummaryOrSignature { } } -// ASSET PATCH AND DELTA +// ASSET PATCH // ================================================================================================ #[derive(Debug)] @@ -630,12 +631,6 @@ pub(crate) struct AssetPatch { pub final_vault_value: Word, } -#[derive(Debug, Clone)] -pub(crate) struct AssetDelta { - pub delta_op: AssetDeltaOperation, - pub asset: Asset, -} - // RECIPIENT DATA // ================================================================================================ diff --git a/crates/miden-tx/src/host/vault_update_tracker.rs b/crates/miden-tx/src/host/vault_update_tracker.rs index 950a4826d0..31543917a4 100644 --- a/crates/miden-tx/src/host/vault_update_tracker.rs +++ b/crates/miden-tx/src/host/vault_update_tracker.rs @@ -1,18 +1,11 @@ use alloc::collections::BTreeMap; use miden_protocol::Word; -use miden_protocol::account::delta::AssetDeltaOperation; -use miden_protocol::account::{ - AccountVaultDelta, - AccountVaultPatch, - FungibleAssetDelta, - NonFungibleAssetDelta, - NonFungibleDeltaAction, -}; -use miden_protocol::asset::{Asset, AssetId}; +use miden_protocol::account::{AccountVaultDelta, AccountVaultPatch, AssetDelta}; +use miden_protocol::asset::AssetId; use crate::TransactionKernelError; -use crate::host::tx_event::{AssetDelta, AssetPatch}; +use crate::host::tx_event::AssetPatch; /// Keeps track of the updates to an account's vault during transaction execution. /// @@ -30,8 +23,8 @@ use crate::host::tx_event::{AssetDelta, AssetPatch}; /// unchanged. #[derive(Debug, Clone, Default)] pub(crate) struct VaultUpdateTracker { - /// The latest absolute [`AssetDelta`] reported by the kernel for each touched asset ID. - delta: BTreeMap, + /// The latest [`AssetDelta`] reported by the kernel for each touched asset ID. + delta: AccountVaultDelta, /// For each touched asset ID, the `(initial, final)` absolute values. The initial value is /// recorded only on the very first observation and never overwritten; the final value is /// updated on every observation. @@ -49,19 +42,21 @@ impl VaultUpdateTracker { Ok(()) } - /// Inserts an asset delta. - pub fn update_delta(&mut self, delta: AssetDelta) { - self.delta.insert(delta.asset.id(), delta); + /// Inserts an asset delta, overwriting the previous delta of the same asset. + /// + /// Returns the overwritten delta, if the asset was already present. + pub fn update_delta(&mut self, delta: AssetDelta) -> Option { + self.delta.insert(delta) } /// Clears the accumulating vault delta. pub fn reset_delta(&mut self) { - self.delta.clear(); + self.delta = AccountVaultDelta::default(); } /// Consumes self and returns the vault delta. pub fn into_delta(self) -> AccountVaultDelta { - self.build_delta() + self.delta } /// Consumes self and returns the normalized vault patch. @@ -82,41 +77,4 @@ impl VaultUpdateTracker { AccountVaultPatch::new(normalized).expect("tx kernel should only emit valid assets") } - - // HELPER FUNCTIONS - // --------------------------------------------------------------------------------------------- - - /// Builds an [`AccountVaultDelta`] from the flat per-asset delta map. - /// - /// TODO(unified_delta): Will be simplified once `AccountVaultDelta` tracks only generic assets. - fn build_delta(&self) -> AccountVaultDelta { - let mut fungible: BTreeMap = BTreeMap::new(); - let mut non_fungible: BTreeMap = BTreeMap::new(); - - for (&asset_id, asset_delta) in &self.delta { - match asset_delta.asset { - Asset::Fungible(fungible_asset) => { - let amount = fungible_asset.amount().as_i64(); - let signed_amount = match asset_delta.delta_op { - AssetDeltaOperation::Add => amount, - AssetDeltaOperation::Remove => -amount, - }; - fungible.insert(asset_id, signed_amount); - }, - Asset::NonFungible(non_fungible_asset) => { - let action = match asset_delta.delta_op { - AssetDeltaOperation::Add => NonFungibleDeltaAction::Add, - AssetDeltaOperation::Remove => NonFungibleDeltaAction::Remove, - }; - non_fungible.insert(asset_id, (non_fungible_asset, action)); - }, - } - } - - let fungible = FungibleAssetDelta::new(fungible) - .expect("tx kernel should only emit valid fungible asset deltas"); - let non_fungible = NonFungibleAssetDelta::new(non_fungible); - - AccountVaultDelta::new(fungible, non_fungible) - } } diff --git a/docs/src/account/code.md b/docs/src/account/code.md index 8899b3f7f9..f5bcdb26dd 100644 --- a/docs/src/account/code.md +++ b/docs/src/account/code.md @@ -17,7 +17,7 @@ Every Miden `Account` is essentially a smart contract. The `Code` defines the ac ## Interface -An account's code is typically the result of merging multiple [account components](./components). This results in a set of procedures that make up the _interface_ of the account. As an example, a typical wallet uses the so-called _basic wallet_ interface, which is defined in `miden::components::wallets::basic_wallet`. It consists of the `receive_asset`, `move_asset_to_note` and `create_note` procedures. If an account has this interface, i.e. this set of procedures, it can consume standard [P2ID notes](../note#p2id-pay-to-id). If it doesn't, it can't consume this type of note. So, adhering to standard interfaces such as the basic wallet will generally make an account more interoperable. +An account's code is typically the result of merging multiple [account components](./components). This results in a set of procedures that make up the _interface_ of the account. As an example, a typical wallet uses the so-called _basic wallet_ interface, which is defined in `miden::standards::components::wallets::basic_wallet`. It consists of the `receive_asset`, `move_asset_to_note` and `create_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 diff --git a/docs/src/asset.md b/docs/src/asset.md index 73212700f6..e2720c254f 100644 --- a/docs/src/asset.md +++ b/docs/src/asset.md @@ -171,27 +171,23 @@ Account components that need to add callbacks to an account's storage should use #### Callback interfaces -The transaction kernel invokes the callback on the issuing faucet and the callback receives the asset ID and value and is expected to return the processed asset value. - -:::warning -At this time, the processed asset value must be the same as the asset value, but in the future this limitation may be lifted. The transaction kernel enforces this: if a callback returns a value different from the one it received, the transaction is aborted. -::: +The transaction kernel invokes the callback on the issuing faucet as a validation hook. The callback receives the asset ID and value for inspection. The callback either completes successfully or aborts the transaction. The **account callback** receives: ``` Inputs: [ASSET_ID, ASSET_VALUE, pad(8)] -Outputs: [PROCESSED_ASSET_VALUE, pad(12)] +Outputs: [pad(16)] ``` The **note callback** receives the additional `note_idx` identifying which output note the asset is being added to: ``` Inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] -Outputs: [PROCESSED_ASSET_VALUE, pad(12)] +Outputs: [pad(16)] ``` -Both callbacks are invoked via `call`, so they must follow the convention of accepting and returning 16 stack elements (input + padding). +Both callbacks are invoked via `dyncall`, so they must follow the convention of accepting and returning 16 stack elements (input + padding). #### Callback skipping