feat(sdk): import proposal-embedded notes as a recovery primitive (#415) - #424
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
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. |
| const multisig = await client.load(accountId, signer); | ||
| const proposals = await multisig.syncProposals(); | ||
|
|
||
| const outcomes = await importNotesFromProposals(midenClient, proposals, { |
There was a problem hiding this comment.
probably there should be single exposed recovery method in multisig where user can pass recovery strategies he wants to use
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
we should avoid needing to pass rpc url, it should be reused from client,
There was a problem hiding this comment.
rust uses client method, ts uses non client method
let proposals = client.list_proposals().await?;
let outcomes = client.import_notes_from_proposals(&proposals).await;
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
| pub async fn import_notes_from_proposals( | ||
| &mut self, | ||
| proposals: &[Proposal], | ||
| ) -> Vec<NoteImportOutcome> { |
| const consumed = new Set( | ||
| consumedRecords.map((record) => | ||
| normalizeHexWord(record.details().recipient().digest().toHex()), | ||
| ), | ||
| ); | ||
| for (const entry of imported) { | ||
| if (consumed.has(entry.recipientDigestHex)) { |
| /** Human-readable detail for non-imported statuses. */ | ||
| reason?: string; |
| /// Human-readable detail for non-imported statuses. | ||
| pub reason: Option<String>, |
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.
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.
0af9534 to
5aeb37f
Compare
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.
5aeb37f to
0c79960
Compare
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.
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_notesproposals (#229), plus a node-fetched inclusion proof — so it works for private notes without the node ever holding the body.MultisigClient::import_notes_from_proposals(&[Proposal]) -> Vec<NoteImportOutcome>(newclient/recovery.rs)importNotesFromProposals(midenClient, proposals, { midenRpcEndpoint, rpc? })(newsrc/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_notesbatches are atomic, so one bad note must not sink the rest) → per-noteNoteImportOutcomewithstatus∈imported | already-present | already-consumed | not-committed | invalid | failedand aretryableflag. No per-note problem aborts the batch; the Rust method returns a plainVecbecause nothing can fail the whole call.Key behaviors (live-tested against testnet)
Expectedstate with their tag tracked so a later sync picks them up once they commit; reportednot-committed/retryable. On the TS side the WASM details file cannot carry a tag, so the tag is registered viaaddTagbefore the import — tag-first so a failure never leaves an untagged dead record.already-consumedinstead of pretending they were recovered.NoteFilterhas no details-commitment variant); an ID-only lookup would re-import them forever.retryable: true.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
Multisigconvenience method delegating to it would be a natural follow-up.