Skip to content

fix: build P2ID asset from vault entry to match callback-flag vault keys#346

Open
0xnullifier wants to merge 3 commits into
OpenZeppelin:mainfrom
0xnullifier:utkarsh/p2id-vault-key-asset
Open

fix: build P2ID asset from vault entry to match callback-flag vault keys#346
0xnullifier wants to merge 3 commits into
OpenZeppelin:mainfrom
0xnullifier:utkarsh/p2id-vault-key-asset

Conversation

@0xnullifier

@0xnullifier 0xnullifier commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #336.

Assets with AssetCallbackFlag::Enabled encode the flag in their vault key, so an asset built with new FungibleAsset(faucet, amount) does not match the vault entry even for the same faucet id. buildP2idTransactionRequest now resolves the outgoing asset from the sender's vault (account.vault().fungibleAssets()) and rebuilds it via FungibleAsset.fromVaultKey(vaultKey, amount), threading the sending Account through the builder and its callers (Multisig.createP2idProposal, the metadata rebuild path, and the browser example).

Why this does not break existing Multisig flows

  • Co-signer verification stays consistent. verifyProposalMetadataBinding rebuilds the request and immediately re-executes it via executeForSummary against the web client's store. Any state in which the new vault lookup could fail (asset absent) is state in which that execution already failed before this change — the lookup introduces no new failure mode, it only surfaces the same one earlier with a clearer message.
  • Rebuilt requests remain byte-identical to what was signed. The vault key for a live balance cannot change representation between proposal creation and verification, so the request reconstructed from metadata still matches the bound tx_summary commitment.
  • Pre-existing pending proposals reverify unchanged. For assets without the callback flag, FungibleAsset.fromVaultKey(vaultKey, amount) produces the same bytes as the old new FungibleAsset(faucet, amount) construction.
  • State freshness follows the existing contract. this.account is refreshed by syncState(), which callers already must invoke after executed transactions (consuming a funding note, etc.) for other flows to work; this change adds no new sync requirement.
  • The public Multisig API is unchanged. Only the exported helper buildP2idTransactionRequest gained a parameter, and it should have not have any known consumers

Summary by CodeRabbit

  • Bug Fixes

    • Improved multisignature P2ID transaction construction by using the account’s available faucet asset.
    • Added validation and clearer failure handling when the required asset is unavailable.
    • Ensured transaction proposals and rebuilt transactions use consistent account information.
  • Tests

    • Expanded coverage for matching vault assets and missing-asset scenarios.
  • Chores

    • Updated the Miden SDK dependency to version 0.15.8 across examples and the multisig client.

Assets with AssetCallbackFlag::Enabled encode the flag in their vault key,
so an asset built with `new FungibleAsset(faucet, amount)` does not match
the vault entry even for the same faucet id. Resolve the asset from
`account.vault().fungibleAssets()` via `FungibleAsset.fromVaultKey` instead,
threading the account through `buildP2idTransactionRequest` callers.

Also bumps @miden-sdk/miden-sdk to 0.15.8 and adds tests pinning the
fromVaultKey usage and the vault-miss error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a07d47f-e28d-4326-8d22-a91cb31eb0de

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

P2ID transaction construction now resolves fungible assets from the sender account’s vault and rebuilds them from vault keys. Multisig clients and browser examples pass the account through proposal-building flows, while Miden SDK dependencies move to version 0.15.8.

Changes

P2ID vault asset flow

Layer / File(s) Summary
Vault-key asset builder and tests
packages/miden-multisig-client/src/transaction/p2id.ts, packages/miden-multisig-client/src/transaction/p2id.test.ts, packages/miden-multisig-client/package.json
buildP2idTransactionRequest accepts an account, finds the matching vault asset, reconstructs it with FungibleAsset.fromVaultKey, throws when absent, and tests both paths.
Account propagation through proposal flows
packages/miden-multisig-client/src/multisig.ts, examples/_shared/multisig-browser/src/multisigApi.ts, examples/*/package.json
Multisig and browser proposal flows pass the account to P2ID request construction; example dependencies use Miden SDK 0.15.8.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Multisig
  participant P2IDBuilder
  participant AccountVault
  participant FungibleAsset
  Multisig->>P2IDBuilder: build request with account, faucet, and amount
  P2IDBuilder->>AccountVault: find matching fungible asset
  AccountVault-->>P2IDBuilder: return vault key
  P2IDBuilder->>FungibleAsset: fromVaultKey(vault key, amount)
  FungibleAsset-->>P2IDBuilder: return reconstructed asset
Loading

Possibly related PRs

Suggested reviewers: haseebrabbani, zeljkox

Poem

I’m a bunny with a vault-key trail,
Rebuilding assets without fail.
P2ID hops through account and note,
With SDK bumps to keep afloat.
“From the vault!” I cheer and flee—
Clean transactions for you and me.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements #336 by resolving P2ID assets from the sender vault via vault keys, requiring the account, and throwing when no match exists.
Out of Scope Changes check ✅ Passed The dependency bumps and browser/client call-site updates are aligned with the vault-key P2ID fix and support the stated objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: constructing the P2ID asset from the vault entry to preserve callback-flag vault keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/miden-multisig-client/src/transaction/p2id.ts (1)

73-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract vault-asset resolution into a named helper.

This adds lookup, validation, and conversion steps to the request builder, alongside prohibited // implementation comments. A resolveFungibleAssetFromVault(account, faucet, amount) helper makes the behavior self-describing and keeps the builder focused on request construction.

Proposed refactor
+function resolveFungibleAssetFromVault(
+  account: Account,
+  faucet: AccountId,
+  amount: bigint,
+): FungibleAsset {
+  const faucetHex = faucet.toString();
+  const assetFromVault = account
+    .vault()
+    .fungibleAssets()
+    .find(asset => asset.faucetId().toString() === faucetHex);
+
+  if (!assetFromVault) {
+    throw new Error('Asset not found in vault, cannot do P2ID transaction');
+  }
+
+  return FungibleAsset.fromVaultKey(assetFromVault.vaultKey(), amount);
+}
+
-  // Build the asset from the vault entry rather than `new FungibleAsset(faucet, amount)`:
-  // assets with `AssetCallbackFlag::Enabled` encode the flag in their vault key, so a
-  // directly constructed asset would not match the vault entry even for the same faucet id.
-  const faucetHex = faucet.toString();
-  const assetFromVault = account
-    .vault()
-    .fungibleAssets()
-    .find(asset => asset.faucetId().toString() === faucetHex);
-  if (!assetFromVault) throw new Error('Asset not found in vault, cannot do P2ID transaction');
-  const asset = FungibleAsset.fromVaultKey(assetFromVault.vaultKey(), amount);
+  const asset = resolveFungibleAssetFromVault(account, faucet, amount);

As per coding guidelines, “Prefer small, single-purpose functions over long multi-step procedures” and “Do not add inline comments (// ...) in implementation code in multisig SDKs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/miden-multisig-client/src/transaction/p2id.ts` around lines 73 - 82,
Extract the vault lookup, missing-asset validation, and FungibleAsset conversion
from the request builder into a named resolveFungibleAssetFromVault(account,
faucet, amount) helper. Update the builder to call this helper, preserving the
existing faucet matching and vault-key-based construction behavior, and remove
the inline implementation comments.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/miden-multisig-client/src/transaction/p2id.ts`:
- Around line 73-82: Extract the vault lookup, missing-asset validation, and
FungibleAsset conversion from the request builder into a named
resolveFungibleAssetFromVault(account, faucet, amount) helper. Update the
builder to call this helper, preserving the existing faucet matching and
vault-key-based construction behavior, and remove the inline implementation
comments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df9a9e35-0ca2-4ee9-8b94-5a0a311a0ae4

📥 Commits

Reviewing files that changed from the base of the PR and between a30ffa4 and b133af5.

⛔ Files ignored due to path filters (4)
  • examples/_shared/multisig-browser/package-lock.json is excluded by !**/package-lock.json
  • examples/smoke-web/package-lock.json is excluded by !**/package-lock.json
  • examples/web/package-lock.json is excluded by !**/package-lock.json
  • packages/miden-multisig-client/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • examples/_shared/multisig-browser/package.json
  • examples/_shared/multisig-browser/src/multisigApi.ts
  • examples/smoke-web/package.json
  • examples/web/package.json
  • packages/miden-multisig-client/package.json
  • packages/miden-multisig-client/src/multisig.ts
  • packages/miden-multisig-client/src/transaction/p2id.test.ts
  • packages/miden-multisig-client/src/transaction/p2id.ts

@haseebrabbani

Copy link
Copy Markdown
Collaborator

@0xnullifier The fix looks right, but the vault lookup and the execution read different state: the lookup uses this.account (a snapshot only refreshed by Multisig.syncState()), while executeForSummary runs against the web client's store, which other flows update directly (e.g. the finalize path calls webClient.syncState() without refreshing this.account).

Concrete regression window: fund a fresh multisig, finalize the consume-notes proposal, then call createP2idProposal without an explicit multisig.syncState() — the store has the asset but the snapshot doesn't, so this now throws Asset not found in vault where it previously succeeded. Same skew applies to the co-signer rebuild path.

Could you pass the store's account at both multisig.ts call sites instead?

ts const account = await webClient.getAccount(AccountId.fromHex(this._accountId)); ​

That makes the lookup source identical to what executeForSummary executes against, and the "no new failure mode" argument in the description becomes exactly true.

@codecov-commenter

codecov-commenter commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@a30ffa4). Learn more about missing BASE report.
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #346   +/-   ##
=======================================
  Coverage        ?   78.04%           
=======================================
  Files           ?      167           
  Lines           ?    31904           
  Branches        ?        0           
=======================================
  Hits            ?    24900           
  Misses          ?     7004           
  Partials        ?        0           

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a30ffa4...533100c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…pshot

The vault lookup previously read this.account, a snapshot only refreshed
by Multisig.syncState(), while executeForSummary runs against the web
client's store, which other flows (e.g. consume-notes finalize) update
directly. Funding a fresh multisig and finalizing before an explicit
syncState() left the asset in the store but not the snapshot, throwing
'Asset not found in vault' where it previously succeeded.

Both multisig.ts call sites (createP2idProposal and the co-signer
rebuild path) now fetch the account via webClient.getAccount, falling
back to the snapshot only if the store has no record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…example

Centralizes the store-first account resolution (web client store with
snapshot fallback) behind a public helper, and switches the example's
P2ID call sites off the stale-prone multisig.account snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@0xnullifier

Copy link
Copy Markdown
Contributor Author

@haseebrabbani addressed your comments

@zeljkoX zeljkoX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xnullifier

The remaining difference between Rust and TS sdk is robustness. Missing faucet asset: Rust defaults to Disabled and continues (execution.rs); TS throws. Is that intended? Should they match?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle fungible assets via vault key when building P2ID notes

4 participants