Skip to content

feat(sdk): import proposal-embedded notes as a recovery primitive (#415) - #424

Merged
haseebrabbani merged 2 commits into
357-recovery-primitivesfrom
415-proposal-note-import
Aug 27, 2026
Merged

feat(sdk): import proposal-embedded notes as a recovery primitive (#415)#424
haseebrabbani merged 2 commits into
357-recovery-primitivesfrom
415-proposal-note-import

Conversation

@haseebrabbani

Copy link
Copy Markdown
Collaborator

Closes #415 (sub-issue of #357). Productizes the reconstruction path validated by spike #412.

What

Adds a recovery primitive to both SDKs that rebuilds Miden store records from the note bytes embedded in v2 consume_notes proposals (#229), plus a node-fetched inclusion proof — so it works for private notes without the node ever holding the body.

  • Rust: MultisigClient::import_notes_from_proposals(&[Proposal]) -> Vec<NoteImportOutcome> (new client/recovery.rs)
  • TS: standalone importNotesFromProposals(midenClient, proposals, { midenRpcEndpoint, rpc? }) (new src/recovery.ts)

Per unique embedded note (duplicates across proposals fold into one outcome): decode → skip notes the store already tracks → one batched inclusion-proof fetch → import individually (upstream import_notes batches are atomic, so one bad note must not sink the rest) → per-note NoteImportOutcome with statusimported | already-present | already-consumed | not-committed | invalid | failed and a retryable flag. No per-note problem aborts the batch; the Rust method returns a plain Vec because nothing can fail the whole call.

Key behaviors (live-tested against testnet)

  • Not-committed notes are recorded in Expected state with their tag tracked so a later sync picks them up once they commit; reported not-committed/retryable. On the TS side the WASM details file cannot carry a tag, so the tag is registered via addTag before the import — tag-first so a failure never leaves an untagged dead record.
  • Chain-nullified notes: miden-client stores these as metadata-less consumption-history records; the primitive reports them already-consumed instead of pretending they were recovered.
  • Metadata-less store records (details-only expected imports, consumed-external history) expose neither a note ID nor a nullifier, so existing-record matching uses details commitments (Rust) / recipient digests (TS — the WASM NoteFilter has no details-commitment variant); an ID-only lookup would re-import them forever.
  • Retryability is classified, not assumed: transient RPC failures during the proof fetch or the import's internal node calls are retryable: true.
  • Decoding is deliberately permissive: the strict note.id() == note_ids[i] binding check stays in the verify/execute path; embedded notes are self-validating, so recovery imports whatever real notes the bytes decode to.

Docs

docs/MULTISIG_SDK.md (both language sections + API tables) and both package READMEs, per AGENTS.md §9. Example wiring (demo/smoke-web) is deferred to the #357 recovery-flow orchestration, where the three primitives (#414/#415/#416) compose.

Notes for reviewers

  • The TS entry point is a standalone function because the issue specifies that signature; a Multisig convenience method delegating to it would be a natural follow-up.
  • The TS pre-import store scan reads all input notes once (WASM filter surface limitation, commented in code); extracting a shared matching helper is planned with SDK recovery primitive: historical public-note backfill by tag #416.

@haseebrabbani
haseebrabbani requested a review from zeljkoX as a code owner August 20, 2026 16:23
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: 4a9b126b-2fd2-45b2-992f-eb5312a60dad

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

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.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.08475% with 162 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rates/miden-multisig-client/src/client/recovery.rs 45.08% 162 Missing ⚠️

📢 Thoughts on this report? Let us know!

@zeljkoX

zeljkoX commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Does it makes sense to create PRs against feature branch? Given few recovery strategies it would be good to have one final review on all included and orchestration between them.

Comment thread docs/MULTISIG_SDK.md Outdated
const multisig = await client.load(accountId, signer);
const proposals = await multisig.syncProposals();

const outcomes = await importNotesFromProposals(midenClient, proposals, {

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.

probably there should be single exposed recovery method in multisig where user can pass recovery strategies he wants to use

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

agreed, will handle this in final review of base feature branch


const proposals = await multisig.syncProposals();
const outcomes = await importNotesFromProposals(midenClient, proposals, {
midenRpcEndpoint: 'https://rpc.testnet.miden.io',

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.

we should avoid needing to pass rpc url, it should be reused from client,

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.

rust uses client method, ts uses non client method

let proposals = client.list_proposals().await?;
let outcomes = client.import_notes_from_proposals(&proposals).await;

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.

Double checking, is this going to be public api? Users probably should not set rpc on method level but on client level and reuse here. Setting rpc url at client level is already supported.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds proposal-embedded note recovery to both multisig SDKs.

Changes:

  • Implements per-note recovery, deduplication, proof fetching, and status reporting.
  • Exposes the new Rust and TypeScript APIs.
  • Adds tests and consumer documentation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/miden-multisig-client/src/recovery.ts Implements TypeScript recovery.
packages/miden-multisig-client/src/recovery.test.ts Tests TypeScript recovery paths.
packages/miden-multisig-client/src/index.ts Exports the recovery API.
packages/miden-multisig-client/README.md Documents TypeScript usage.
crates/miden-multisig-client/src/client/recovery.rs Implements Rust recovery and unit tests.
crates/miden-multisig-client/src/client/mod.rs Registers and exports the Rust module.
crates/miden-multisig-client/src/lib.rs Re-exports public recovery types.
crates/miden-multisig-client/README.md Documents Rust usage.
docs/MULTISIG_SDK.md Documents both SDK recovery APIs.
Suppressed comments (2)

packages/miden-multisig-client/src/recovery.ts:145

  • This public workflow is over 220 lines and mixes decoding, deduplication, store scanning, RPC transport, per-note mutation, and post-import classification. That makes the recovery invariants difficult to audit and conflicts with the multisig guideline to separate orchestration from transformation. Extract focused helpers for candidate collection, existing-record matching, proof fetching, import, and consumed-state classification, leaving this method as orchestration.
export async function importNotesFromProposals(
  midenClient: RawClientSource,
  proposals: ReadonlyArray<Pick<Proposal, 'id' | 'metadata'>>,
  options: ImportNotesFromProposalsOptions,
): Promise<NoteImportOutcome[]> {

crates/miden-multisig-client/src/client/recovery.rs:190

  • This method combines store lookup, RPC fetching, expected-note construction, imports, and post-import state classification in one long workflow. The multisig coding guideline calls for small, single-purpose helpers and separation of orchestration from transformation; extracting these stages would make Rust/TypeScript parity and failure isolation materially easier to verify.
    pub async fn import_notes_from_proposals(
        &mut self,
        proposals: &[Proposal],
    ) -> Vec<NoteImportOutcome> {

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// Skip notes the store already tracks.
const pending: DecodedCandidate[] = [];
for (const candidate of decoded) {
const record = existing.get(candidate.idHex) ?? existing.get(candidate.recipientDigestHex);
Comment on lines +187 to +190
pub async fn import_notes_from_proposals(
&mut self,
proposals: &[Proposal],
) -> Vec<NoteImportOutcome> {
Comment on lines +336 to +342
const consumed = new Set(
consumedRecords.map((record) =>
normalizeHexWord(record.details().recipient().digest().toHex()),
),
);
for (const entry of imported) {
if (consumed.has(entry.recipientDigestHex)) {
Comment on lines +74 to +75
/** Human-readable detail for non-imported statuses. */
reason?: string;
Comment on lines +102 to +103
/// Human-readable detail for non-imported statuses.
pub reason: Option<String>,
@haseebrabbani
haseebrabbani changed the base branch from main to 357-recovery-primitives August 21, 2026 12:15
haseebrabbani added a commit that referenced this pull request Aug 21, 2026
Addresses the Copilot review on PR #424:

- TS record matching now keys on recipient digest plus a canonical asset
  fingerprint (together equivalent to the details commitment) instead of
  recipient digest alone, both in the pre-import scan and the post-import
  consumed-state check; two distinct notes can share a recipient while
  carrying different assets. Regression tests cover both collision paths.
  (Rust already keys on the actual details commitment.)
- Document that an imported outcome can carry a reason when the
  post-import consumed-state check fails (both SDKs).
- Add a Rust test driving the public import_notes_from_proposals method:
  decoding, dedup, and invalid isolation happen before any network access,
  and an unreachable node surfaces per-note retryable failures instead of
  aborting the batch.
haseebrabbani added a commit that referenced this pull request Aug 21, 2026
Addresses the Copilot review on PR #424:

- TS record matching now keys on recipient digest plus a canonical asset
  fingerprint (together equivalent to the details commitment) instead of
  recipient digest alone, both in the pre-import scan and the post-import
  consumed-state check; two distinct notes can share a recipient while
  carrying different assets. Regression tests cover both collision paths.
  (Rust already keys on the actual details commitment.)
- Document that an imported outcome can carry a reason when the
  post-import consumed-state check fails (both SDKs).
- Add a Rust test driving the public import_notes_from_proposals method:
  decoding, dedup, and invalid isolation happen before any network access,
  and an unreachable node surfaces per-note retryable failures instead of
  aborting the batch.
@haseebrabbani
haseebrabbani force-pushed the 415-proposal-note-import branch from 0af9534 to 5aeb37f Compare August 21, 2026 14:22
Adds import_notes_from_proposals (Rust) / importNotesFromProposals (TS):
rebuilds store records from the note bytes embedded in v2 consume_notes
proposals plus a node-fetched inclusion proof, so recovery works for
private notes without the node holding the body (validated by spike #412).

Per unique embedded note: decode, skip notes the store already tracks
(matched by details commitment / recipient digest + asset fingerprint so
metadata-less records are recognized), fetch proofs in one batch, import
individually (upstream batches are atomic), and classify the outcome.
Uncommitted notes are recorded as expected with their tag tracked so a
later sync picks them up; chain-nullified notes are recorded as
consumption history and reported already-consumed. No per-note problem
aborts the batch. Includes the fixes from the PR #424 Copilot review.
@haseebrabbani
haseebrabbani force-pushed the 415-proposal-note-import branch from 5aeb37f to 0c79960 Compare August 25, 2026 20:52
Review follow-up on PR #424: the standalone helper required passing the
Miden RPC endpoint although the loaded client already knows it (neither
the MidenClient facade nor the raw WASM client exposes its endpoint, so a
standalone function cannot derive it). The new Multisig method reuses the
client's endpoint and resolved retry configuration, and syncs pending
proposals from GUARDIAN when none are passed. The standalone export stays
for callers holding a raw WASM client or proposals from another source;
docs now lead with the method. Also drops a stray README section that a
shared rerere resolution replayed from another branch's conflict.

@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.

LGTM

Thanks

@haseebrabbani
haseebrabbani merged commit 5d9f69e into 357-recovery-primitives Aug 27, 2026
7 checks passed
@haseebrabbani
haseebrabbani deleted the 415-proposal-note-import branch August 27, 2026 13:04
@github-project-automation github-project-automation Bot moved this from Review to Done in OZ Development for Miden Aug 27, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

SDK recovery primitive: proposal-embedded note import

4 participants