multi: stable canonical id for on-chain send (#610) - #843
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a stable, durable send_job_id (derived from the pending intent ID) for on-chain send and cooperative-leave operations. This ID is returned in SendOnChainResponse and persisted as the row ID in the swap wallet, allowing the operation handle to survive restarts and round seals. The forfeit-driven completion logic is updated to correlate using the retained consumed outpoint (vtxo_outpoint) instead of the row ID, with fallback support for legacy rows. There are no review comments, so I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
a3909b4 to
d8f58f5
Compare
Add the stable leave-job id (hex of the pending-intent id) to SendOnChainResponse so callers can key an on-chain send's activity row by a handle that survives the round seal and a restart, rather than the consumed VTXO outpoint (#610).
Regenerated via make rpc.
handleSendOnChain returns the derived PendingIntentID on the submitted response so the RPC layer can expose it as the send's stable id. The dry-run preview leaves it zero.
Populate SendOnChainResponse.send_job_id with the hex of the wallet's pending-intent id, empty for a dry-run preview.
Use the daemon's returned send_job_id as the on-chain-send (cooperative leave) activity row's canonical id, so it survives the round seal and a restart and represents a multi-input sweep as one row, instead of the consumed VTXO outpoint (#610). The first consumed outpoint is retained in vtxo_outpoint and the forfeit-driven completion matches on it. Falls back to the outpoint when the daemon returns no id.
d8f58f5 to
edfe5cf
Compare
Re-keying the on-chain-send row to the hex send_job_id broke its completion path. decorateExitEntry fed entry.GetId() to the daemon's GetUnrollStatus, which parses its argument as a txid:vout outpoint and returns InvalidArgument for a bare hash. The error aborted before the Found=false fall-through to decorateCooperativeLeaveEntry, so every cooperative-leave row was stranded PENDING and never flipped to COMPLETE even after the leave round confirmed — a regression from the parseable txid:vout id, which returned Found=false and completed. Look up GetUnrollStatus by a real outpoint: the id for unilateral-exit and legacy rows, else the retained vtxo_outpoint; with no queryable outpoint the row cannot be a unilateral exit, so treat it as a cooperative leave. Also stop decorateCooperativeLeaveEntry from falling its correlation outpoint back to a non-outpoint id. The test harness could not catch this: the fake GetUnrollStatus never validated the outpoint and the only hash-id test called decorateCooperativeLeaveEntry directly, bypassing the broken gate. The fake now rejects a non-outpoint like the daemon, and a new test drives a hash-keyed leave row through decorateExitEntry to COMPLETE.
The comment called it a durable handle that survives a restart, which overstates it: the id is deterministic (reproducible from the same inputs) and stable across a restart, but it is a correlation id, not a lookup handle — no RPC resolves it and the send's terminal status is not reconciled by id across a restart yet. Document that, that it is non-secret and recomputable (must not gate a privileged action), and that empty means preview/dry-run or an older daemon, not a submitted send from a current daemon.
Generated by make rpc from the reworded send_job_id comment. Comment only; no wire change.
Fourth PR in the C1 series (#774), stacked on #842.
Fixes the flagship symptom of the epic, #610: a
send --onchain(cooperative-leave) activity row is keyed by the consumed VTXO outpoint, which is destroyed when the round seals — so the handle vanishes (activity inspectreturns NotFound), it can't survive a restart, and one outpoint slot can't represent a multi-input sweep.Key realization
The daemon already derives and persists a stable id for the send: the
PendingIntentID(wallet/pending_intent.go) — asha256(kind + sorted consumed-outpoints + payload). It is deterministic (stable across restart), 32 bytes, and represents the whole multi-input set as one id. It was simply never returned to the wallet layer. So this reuses it rather than inventing new daemon persistence.Change
SendOnChainResponsegainssend_job_id(proto).handleSendOnChainsurfaces the already-derivedPendingIntentIDon its submitted response (zero on a dry-run preview); the RPC handler hex-encodes it intosend_job_id.send_job_id(falling back to the first consumed outpoint when the daemon returns none, preserving old behavior). The first consumed outpoint is retained invtxo_outpoint, anddecorateCooperativeLeaveEntrynow matches the forfeit-driven completion on that retained outpoint rather than the id.doc.goV1 LIMITATIONS updated.Scope / deferred (C2)
This fixes the id. The live, cross-restart terminal reconciliation under that id — projecting forfeit/settlement into the store rather than relying on the startup backfill — is C2 (a VTXO-forfeiture / unroll-status reconciler, like the credit poll loop). DEPOSIT's stable id is the sibling follow-up (PR5).
Tests
wallet: the submittedSendOnChainResponsecarries the persistedPendingIntentID(extends the persist-before-registration test).PendingIntentIDdeterminism is already covered.swapwallet:leaveEntryStubuses the leave-job id as the row id and retains the first outpoint invtxo_outpoint(plus the empty-id fallback);decorateCooperativeLeaveEntryflips to COMPLETE on the retained outpoint, not the id.make rpc/make build/make lint-changed-local/make commitmsg-lint, and thewallet+swapwallet+dbunit suites pass. End-to-end (send --onchain, restart,activity inspect) is manual/itest, deferred.