release: Merge main into v0.1.x-branch - #1072
Conversation
In this commit, we add a RefreshFeeEstimate message to RefreshVTXOsResponse so a dry_run refresh can carry an advisory, itemized operator-fee preview for the selected VTXOs. Today the refresh path never surfaces the fee anywhere: the binding amount is set by the server-issued JoinRoundQuote at seal time and auto-accepted against the client's MaxOperatorFeeSat cap, so the user only learns the charge afterwards via the fee history. The per-outpoint rows carry the amount and remaining lifetime the daemon resolved for each VTXO (removing the manual amount / remaining-blocks input the standalone fees estimate command requires) plus the operator's liquidity / on-chain share / margin breakdown. Rows are deliberately NOT waiver-adjusted so their components always sum to their total; the selection-level free_refresh_eligible flag and zeroed total express the free-late-refresh waiver instead, mirroring the operator's all-or-nothing seal-time rule from lumos#675. estimate_error keeps dry_run usable as a validity probe when the operator or chain height is unavailable: the preview still returns and the caller is told the numbers are absent rather than zero. The daemon-side population of the new field lands in the next commit. Part of #986.
In this commit, we populate the new RefreshVTXOsResponse fee estimate on the dry-run path. The daemon resolves every selected VTXO to its full descriptor, computes each remaining lifetime from the chain tip, and fetches the operator's itemized quote through the existing EstimateFee proxy, deduped on (amount, remaining blocks). The remaining-blocks figure is clamped to 1 for expiring VTXOs because the operator treats zero as "price the full sweep-delay lifetime". The explicit-outpoint path now looks targets up in the VTXO store instead of only parsing strings: an unknown or non-live outpoint surfaces as InvalidArgument instead of echoing back as a plausible preview, mirroring the LiveState filter the --all path already applies — the real refresh can never execute either, and dry_run is the validity probe the CLI consent prompt trusts. Explicit selections are also deduped (order-preserving) so a repeated outpoint can neither double-count in the estimate nor register a doomed duplicate forfeit pair with the wallet. The free-late-refresh waiver (lumos#675) is applied locally: the operator's EstimateFee prices every refresh as paid, so quoting a free late refresh through it alone would over-quote. The daemon already caches the advertised window in its operator terms and knows a refresh selection is the pure one-for-one renewal shape the waiver requires, so a selection fully inside the window previews with an explicit zero total while the rows keep the ordinary paid quote to show what the waiver saves. The estimate is strictly best-effort and degrades all-or-nothing: every quote is fetched and vetted (no negative or beyond-money- supply values, no overflowing selection total) before any component is written to a row, an unreachable operator or chain backend sets estimate_error while the preview and the locally computed waiver verdict survive, and the total uses explicit proto presence — it is only set when meaningful, so a degraded estimate can never be misread as a free refresh. The dry-run branch also moves ahead of the wallet-ready gate, matching the LeaveVTXOs ordering rule that pure-argument validation and previews must not depend on wallet state (the H-5 fix's comment already claimed this parity). A side effect is that an empty selection=all dry run now honestly reports status "preview" instead of "queued". The real refresh path still gates on wallet readiness before queuing. Part of #986.
In this commit, we render the dry-run fee estimate on the refresh command. The itemized numbers stay in the JSON body on stdout; stderr gains a short human-readable summary so an operator reading the terminal sees the headline cost — or the free-refresh-window verdict, or the degraded-mode warning — without stdout consumers having to strip prose. Every wording branch repeats that the value is advisory and the binding fee is set at seal time, and the degraded branch explicitly says a fee still applies so a missing estimate is never read as a free refresh. VTXOs below the operator's minimum viable amount add a count-level warning, mirroring the fees estimate command's below-dust handling. A non-empty dry-run preview that carries no estimate at all can only come from a daemon predating the feature, so the CLI warns about that skew explicitly instead of silently dropping the preview the flag help promises. The fees estimate help now points refresh users at `ark vtxos refresh --dry_run`, which resolves each selected VTXO's amount and remaining lifetime automatically instead of requiring both by hand. Part of #986.
In this commit, we stop dispatching a real refresh without consent. Before this change the number a dry run can now surface was not actionable: by the time a user read anything, the refresh was queued, auto-joined, and priced by the auto-accepted seal-time quote, with no cancel surface anywhere in between. The refresh command now mirrors the leave --all consent pattern: --yes skips the gate for scripted use, non-interactive stdin refuses to prompt and directs the caller to --yes or --dry_run (so agents are never blocked, per the issue's acceptance criteria), and only an interactive TTY prompts. The interactive prompt shows the advisory estimate first — fetched through the same RPC in dry-run form — so the operator consents to a number rather than a mystery fee, and the prompt goes to stderr: stdout stays reserved for the JSON body, so a piped invocation can never swallow the question and read as a hung command. A preview rejected as InvalidArgument aborts outright (the real dispatch would reject the same request shape); any other preview failure degrades to prompting with an explicit "still charged the seal-time fee" warning, so a broken estimate path never makes refreshes unconfirmable and a missing estimate is never read as free. An empty selection (refresh --all with no live VTXOs) skips the prompt entirely — there is no fee to consent to, and warning about a charge for a no-op would be false. The schema registry entry gains the yes parameter and now names the fee in its description, so schema consumers see the same contract as the flag surface. getDaemonClient becomes a package-level indirection (mirroring stdinIsTTY) so wiring tests can drive the full command path against an in-process bufconn daemon and pin that the gate runs before any dispatch: a refused invocation must reach the daemon zero times. Part of #986.
In this commit, we move the leave --all confirmation prompt from stdout to stderr, matching the refresh confirmation gate and the package's stream-split invariant: stdout is reserved for the JSON body, diagnostics go to stderr. Before this change, piping the command (e.g. into jq) on an interactive terminal swallowed the prompt into the pipe — the command read as hung, and a user who typed y blind fed prose into the JSON consumer. Part of #986.
In this commit, we extend the refresh fee-consent contract to the MCP surface. The MCP tool calls the daemon directly rather than executing the cobra command, so the CLI gate never ran there: an agent could queue a fee-incurring refresh with no warning, while the schema registry — documented as the shared source of truth for CLI commands and MCP tools — promised that a real refresh requires yes. The tool description also never mentioned the fee. The ark.vtxos.refresh tool now takes a yes acknowledgement: a real (non-dry-run) call without it returns an immediate, actionable error naming both the dry_run:true preview and the yes:true acknowledgement path — nothing can block on MCP, matching the CLI's non-interactive refusal — and the description now names the seal-time fee and the preview-first flow. The dry-run preview carries the same itemized fee estimate as every other surface. Part of #986.
In this commit, we document the refresh-fee visibility surface. The CLI guide's refresh section now names the operator fee and the seal-time quote that binds it, describes the dry-run estimate (per-outpoint amounts and lifetimes resolved by the daemon, the free-refresh-window verdict, the degraded estimate_error mode), and covers the interactive confirmation plus the non-interactive --yes requirement, with examples updated to match. The waveclicommands agent docs gain the refresh consent-gate invariant — CLI prompt, non-interactive refusal, the MCP tool's yes argument, and the absent-not-zero wire semantics of a degraded total — alongside the existing leave --all and recovery escalate prompt-refusal postures. The waved agent docs gain the dry-run estimation invariant (pre-wallet-gate ordering, store-resolved live-only outpoints, quote dedupe and validation, locally computed waiver, degrade-only failure mode). The repo's own agent-facing refresh examples in the waved skill and CONTEXT.md pick up --dry_run/--yes so an agent following them is not stopped cold by the new non-interactive refusal. Both members of each touched CLAUDE/AGENTS pair receive the same edit; the waved pair's pre-existing divergence on unrelated OOR text is left to the doc-gardening workflow. Part of #986.
In this commit, we fix the client-side operator fee reconciliation to count locally owned outputs from the BUILT VTXOs rather than the intent requests. Under the seal-time fee handshake, an intent's amount is the pre-fee target: the server's quote shaves the operator fee off the leaf at seal time, and the FSM intentionally no longer subtracts it client side. Summing intent amounts therefore cancels the round's inputs exactly, computeClientOperatorFee returns zero for every fee-charging round, and the FeePaidMsg emission is silently suppressed — no boarding_fee_paid or refresh_fee_paid row was ever landing in the fee ledger. The built ClientVTXO carries the sealed leaf value (the quote residual extracted by leafNonAnchorAmount), so input minus output over the sealed amounts yields the true fee. Foreign directed-send recipient slots never materialize as owned VTXOs, so their intent amount remains the only local record of their value and still counts as-is. A new regression test pins the shape observed on regtest: a leave round forfeiting 149,745 sats into a 10,000 sat leave plus a 139,204 sat sealed change VTXO must book the 541 sat operator fee, and a fully funded foreign recipient slot must leave that fee unchanged.
In this commit, we move the boarding fee leg's credit side from vtxo_balance to wallet_balance so the chart of accounts nets correctly against the amounts the round actually books. The boarding vtxo_received leg carries the SEALED (post-fee) VTXO value under the seal-time fee handshake, which means vtxo_balance already lands on the true VTXO holding without any fee adjustment. Crediting the fee from vtxo_balance on top of that would understate the VTXO layer by the fee; crediting it from wallet_balance instead completes the gross wallet outflow — deposit in, sealed value plus fee out — leaving every account at its true balance. Refresh fees keep their vtxo_balance credit: a refresh fee is carved out of forfeited VTXO value, and the paired gross send/receive legs cancel, so the fee leg is the only real movement there. The fee_ledger doc's boarding and refresh walkthroughs are updated to describe the sealed-amount legs and the per-flow credit accounts.
In this commit, we add migration 000015, which grows ledger_entries a round_uuid TEXT column mirroring the raw 16-byte round_id BLOB in the canonical lowercase UUID form that rounds.round_id and vtxos.forfeit_round_id already store. The two subsystems historically persisted the same identifier in different encodings, and no BLOB to TEXT conversion exists in the SQL dialect subset shared by SQLite and Postgres (hex() vs encode()), so nothing could join ledger rows against the round-adjacent tables in plain SQL. The TEXT mirror closes that gap once, for every present and future join. New inserts stamp the column in the LedgerStoreDB adapter via roundUUIDText, so no ledger actor or message change is needed. Existing rows are converted by a Go post-migration step registered for version 15 — the string formatting is the part SQL cannot express portably — wired into both store constructors through the previously dormant makePostStepCallbacks machinery. The per-round backfill UPDATE guards on round_uuid IS NULL, so a crash-interrupted run re-executes as a no-op. The generated sqlc code for the accounting queries is regenerated alongside: the insert gains the new column, the ledger list queries return it, and the two backfill helper queries land.
In this commit, we extend the ListVTXOsByStatus settlement join with the forfeit round's operator fee. The query already LEFT JOINs the round that forfeited each VTXO to surface the settling commitment txid and confirmation height; a second LEFT JOIN against a fee-totals subquery (SUM of boarding_fee_paid and refresh_fee_paid grouped by round_uuid, keyed on forfeit_round_id) now rides along in the same single query, so the read costs no extra roundtrip. The figure is round-level: every VTXO forfeited in the same round reports the same total, so consumers must attribute it once, never sum it across VTXOs. It reads zero for fee-free rounds and for ledger rows predating the round_uuid backfill. vtxo.Settlement grows a FeeSat field populated by the by-status row converter on both the full and light read paths, and the settlement store test now proves the sum covers exactly the settling round's fee event types — the vtxo_sent row in the same round and a fee booked against an unrelated round contribute nothing.
In this commit, we add fee_sat to the VTXOSettlement message and populate it in descriptorToProto from the settlement the store join now carries. A FORFEITED VTXO's settlement thereby reports where the forfeit round confirmed AND what the round cost, giving wallet-layer consumers a single carrier for completing a cooperative-leave row with its on-chain coordinates and fee. The field mirrors the store contract: a round-level total repeated on every VTXO the round forfeited, zero against fee-free rounds and ledgers predating fee-by-round attribution.
In this commit, we complete the fee plumbing the activity surface was missing: when a cooperative-leave EXIT row completes off its forfeited source VTXO, the settlement now carries the forfeit round's operator fee, and applyCooperativeLeaveForfeited stamps it onto the row's fee_sat. The projection pass then persists it into the canonical activity store, so both the live table and the durable row agree. Sweep-all sends need one more step. A sweep's pending amount is the gross drained balance with the fee still baked in — the binding fee is unknown until the round seals — so displaying a fee next to that gross amount would double-count it. The onchain request now records a sweep_all marker (set by leaveEntryStub from the prepared intent), and completion nets the settled fee back out of the amount. Every completed EXIT thus reads the same way: amount is the value delivered to the destination, fee is the cost on top, and their sum is the true outflow. A bounded send's amount is already the exact destination value, so it is left untouched. Verified end-to-end on an arktest regtest topology: a 10,000 sat bounded send completes as amount -10,000 / fee 541, and a sweep-all of a 139,204 sat wallet completes as amount -138,816 / fee 388, with the destination wallet receiving exactly those amounts on chain.
In this commit, we keep the newly emitted boarding_fee_paid ledger rows out of the wallet activity view. The daemon's unified history typed them 'boarding' all along, but the mapping was dead code while the round actor's fee computation suppressed every fee row; with fee emission fixed, each boarding round's fee leg surfaced as a phantom PENDING DEPOSIT row for the fee amount (observed as a 255 sat 'ledger-N' deposit on regtest). Only the wallet_utxo_created subtype is a user-facing deposit. The fee already reaches the user through the real deposit row's fee_sat attribution, so the accounting leg is classified out of the wallet surface entirely.
In this commit, we bring the per-package agent docs in line with the fee-attribution changes: the db package map documents migration 000015 and the round_uuid mirror column, the ledger doc describes the per-flow fee credit accounts and the joinable round linkage, and the swapwallet doc records the cooperative-leave EXIT fee stamping and sweep-all amount netting invariant.
In this commit, we add a read path for the unilateral exit cost the ledger already records: handleExitCost books an onchain_fee_paid leg after the final sweep confirms, keyed by the exited VTXO's outpoint-derived idempotency key. ExitIdempotencyKey exports that key derivation, and LedgerStoreDB.GetConfirmedExitCost sums the fee legs under it — the partial unique index scopes on (key, event_type, accounts), so the send leg sharing the same key never contributes. An exit that has not confirmed (or predates exit-cost accounting) reads zero.
In this commit, we add exit_cost_sat to GetUnrollStatusResponse and stamp it in the daemon on both status paths (live registry and the persisted-job fallback) whenever a job reads COMPLETED. Unlike the estimate breakdown a detailed probe projects via enrichExitFees, this is the settled figure from the ledger's confirmed onchain_fee_paid exit leg, so it needs no fee-rate estimation or lineage resolution and is cheap enough for the plain per-row status lookups the activity surface issues. The stamp is best-effort: a missing ledger store or a read failure logs at debug and never fails the status query.
In this commit, we close the remaining FEE 0 gap on the activity surface: a completed unilateral exit now carries the settled exit cost the daemon reports on GetUnrollStatus. applyUnrollStatus stamps it onto the row's fee_sat and nets it back out of the gross VTXO amount, so a unilateral EXIT reads the same way as a completed cooperative leave: amount is the value delivered on chain, fee is the cost on top, and their sum is the gross VTXO value that left Ark custody. A zero cost (old daemon, or an exit predating exit-cost accounting) leaves the row exactly as before, preserving prior behavior for historical exits.
In this commit, we harden the EXIT completion paths against the ordering gap between the status that completes a row and the ledger commit that carries its fee. The forfeit status (VTXO actor) and the round's FeePaidMsg (durable ledger actor) are independent fire-and-forget Tells from the same round-actor turn, and the unroll path likewise only guarantees the ExitCostMsg is enqueued before the terminal handoff. A reconcile pass landing in that window would complete the row at fee 0, durably project it, and drop the pending record — freezing the exact fee-0 symptom this series eliminates. Two guards close the window. First, a COMPLETE projection carrying a zero fee retains its pending record for a bounded number of passes (feeZeroClearGracePasses) before clearing, so the row stays derivable long enough for a fee that commits milliseconds later to be re-read and re-projected; genuinely fee-free rounds pay only a few redundant decorations. Second, mergeActivityContext treats a recorded fee as sticky: no producer legitimately moves a settled fee back to zero, so a later fee-0 projection (racing pass, transient ledger read error) restores the stored fee together with its coupled netted amount instead of regressing the row to gross/fee-0. The reconciler scenario test drives the race end to end: pass one completes at fee 0 and retains the record, pass two observes the late-committed fee, heals the stored row, and clears.
In this commit, we make the receive session fail fast when a same-Ark p2p event arrives for a session whose route quote attached credit or padded the expected vHTLC above the invoice amount. The direct p2p vHTLC is funded entirely by the sender, so there is no output that can carry the swap server's custodial credit top-up; such an event is always the product of a server that took the p2p path against a credit-bound intent. Previously the acceptance check only compared the event amount against the invoice amount, which still matches on a credit-attach plan (the attached credit lives in the vHTLC padding, not the invoice amount). The session would accept the event, switch to the in-ark rail (which intentionally skips the out-swap ACK), and then poll for a sub-floor vHTLC that can never be funded. On the production instance this surfaced as an endless "script not registered for principal" poll while the swap server held the incoming HTLC waiting on an ACK the session would never send. Failing terminally with an explicit reason surfaces the conflict immediately and abandons the session's per-hash mailbox along with it: the envelope is never acked, but a failed session never pulls that mailbox again, so nothing redelivers. This is the client-side half of the fix; the server-side gate that stops offering the p2p path for credit-bound intents lands in swapdk-server.
wallet: attribute round operator fees to onchain withdrawal activity
In this commit, we add the wire surface for the wavelength#844 status reconcile: a client->server QueryRoundStatus RPC and the ClientRoundStatusReport push event that answers it, carrying a RoundLifecycleStatus classification (in-flight, broadcast, confirmed, or dead). A client that has already sent its per-VTXO forfeit signatures cannot release the forfeit reservations on a round-failure notification alone: the operator may hold fully-signed forfeit txs, and a blind release risks a double-spend if the round's commitment later confirms. The saving grace is that a forfeit tx spends a connector output of the commitment, and the operator persists a finalized round atomically with its VTXOs before the commitment is ever broadcast. A ROUND_STATUS_DEAD answer (no live FSM, no durable row) therefore proves the commitment can never confirm, which is exactly the proof-of-death that makes the release safe. The generated code is regenerated via make rpc.
In this commit, we fix wavelength#844: a round failure arriving after the client's forfeit signatures have left the box no longer strands the VTXO in Forfeiting for the life of the batch. The releaseForfeitsOnFailure wrapper (the #653 fix) deliberately stops at PartialSigsSentState, because past that point the operator may hold fully-signed forfeit txs and a blind release risks a double-spend. The result was that InputSigSentState had no release path at all: the coin sat stranded until the #823 startup sweep or batch expiry rescued it. The fix is a status reconcile rather than a table edit. A BoardingFailed that lands in InputSigSentState with forfeits at stake now parks in the state (PendingFailure) while a QueryRoundStatusOutbox probes the operator for the round's authoritative lifecycle status. Only a dead answer, meaning the round never finalized so its commitment can never confirm, fails the round through releaseForfeitsOnFailure, which returns the inputs to LiveState and retires the originating job on a terminal-for-job code. Any other answer holds the reservations. The same probe covers the lumos#618 silence door, where a crashed operator never sends a failure at all: a status-reconcile timeout (armed when the forfeit signatures are emitted, re-armed per probe, re-armed on restart reload) fires in the silence and drives the identical query. The timeout alone never releases; only the operator's answer does. Boarding-only rounds keep the old immediate-failure behavior, and a non-positive StatusReconcileTimeout opts out entirely. The timer only arms when forfeits are actually at stake, matching the gate every consumer applies, and repeated unanswered probes back off exponentially (capped at 16x the base window) so an operator that predates the status RPC sees a bounded probe cadence rather than a fixed-rate loop forever. The dead-answer release also spells out its trust boundary in the code: the proof of death is the operator's own self-report, sound against an honest-but-faulty operator (the failure mode this reconcile exists for), while the commitment confirmation watch stays registered so a fraudulent later broadcast still surfaces as a detected conflict.
In this commit, we register the inbound dispatch route for ClientRoundStatusReport, so the operator's answer to a QueryRoundStatus probe reaches the round FSM as a RoundStatusReported event through the same push-event path the other round messages use. Without the route the daemon would drop the report on the floor and the reconcile would spin on its retry timeout forever.
In this commit, we close the restart gap in the status-reconcile release. The reconcile keys every decision on Intents.Forfeits, but that set only ever lived in memory: dbRoundToDomainRound and reconstructInputSigSentState rebuilt boarding intents alone, so after a restart a forfeit-bearing round looked boarding-only. The re-arm guard in the actor's reload loop never fired, and even a hand-armed timer would have released nothing, since the dead-answer path releases the (empty) in-memory forfeit list. The strand the reconcile exists to fix simply reopened across every restart. The durable ground truth was already there: MarkVTXOForfeiting stamps each Forfeiting VTXO row with the binding forfeit_round_id. We add a ListForfeitingVTXOsByRound query over those rows and rebuild the forfeit set in both reload paths. The rebuilt requests carry the outpoint and amount with no custom spend paths, which is exactly right: custom (caller-supplied) forfeit inputs never enter the wallet store and their signing contexts die with the process, so everything the query returns is a standard wallet forfeit for the release path. The new TestRoundStoreReloadRebuildsForfeitSet pins the behavior against a real store: commit an InputSigSent round, mark two VTXOs Forfeiting against it, and require both reload paths to surface the pair (and only the pair) as standard forfeits.
In this commit, we make the wallet activity feed explain the Ark top-up transfer that funds a credit-backed pay. Paying a sub-dust invoice routes through the swap server's credit account: the wallet first funds the account with an OOR at the operator's dust floor, then debits the invoice amount from the resulting balance. The ledger row of that funding OOR previously surfaced as a bare outgoing transfer with no connection to the payment (issue #989's "unrecognized outgoing transfer ledger-46"), and the surplus that remained as credit was invisible outside the Balance RPC's credit fields. The credit registry summary now carries the delegated top-up OOR session id and top-up amount from the durable operation record. The history merger indexes pay operations by that session id (in both hex orientations, so the lookup is independent of the recorded display convention) and relabels the matching ledger row: the counterparty becomes "credit" and the progress metadata carries a "credit_topup" phase label plus the payment hash of the pay the top-up funded. The amount is intentionally left untouched, since the row records the real VTXO outflow; the surplus above the paid amount remains visible as the wallet's credit balance, which Balance already surfaces through credit_available_sat / credit_reserved_sat.
sdk/swaps+swapwallet: reject credit-bound in-ark events, label credit top-ups
round: release forfeit reservations via a status reconcile on round death
In this commit, we extend the wire surface for the credit-aware in-ark settlement rail designed in swapdk-server#233. InArkHtlcEvent grows requested_amount_sat and attached_credit_sat, mirroring OutSwapHtlcEvent: a credit-shaped event carries the padded vHTLC amount in amount_sat together with the invoice amount and the credit portion, so the receiver can validate the full triplet against its route quote instead of hard-rejecting any padded event. RequestChannelIdRequest gains supports_in_ark_credit, letting the receiver prove it understands credit-shaped in-ark events at route registration time. The server records the capability on the receive intent; without it, a credit-attach receive keeps the existing fall-through behavior, so mixed fleets never see an event shape they cannot validate.
In this commit, we teach the receive session to settle a credit-attach receive over the swap-server-funded in-ark leg from swapdk-server#233. A credit-shaped InArkHtlcEvent (one carrying requested_amount_sat and attached_credit_sat) is validated against the session's route quote triplet exactly like the out-swap acceptance path: the requested amount must match the invoice, the attached credit must match the plan, and amount_sat must equal the padded vHTLC amount. Any mismatch fails the session terminally. Legacy direct p2p events keep the existing guards: a credit-bound session still rejects them fast, since a sender-funded vHTLC cannot carry the server's custodial credit. The rail split also moves the ACK decision off the settlement type alone: the legacy p2p rail keeps skipping the out-swap server ACK (the sender funded before the event was published, so there is nothing to gate), while a credit-shaped in-ark session sends it, because the swap server funds the padded vHTLC only after the receiver durably accepted the event. The claim policy is unchanged in shape: the event's sender key (the swap server's funding key on this rail) fills the sender slot and the Ark operator stays in the server slot, so funding validation and the claim path work as they do for every other padded vHTLC. RequestChannelID now always advertises supports_in_ark_credit, since this client validates the credit shape; servers without the capability field ignore it, and servers with it only publish credit-shaped events to receivers that set it.
In this commit, we address two findings from review of the poll decay. The config comment enumerated the events the fallback poll covers as missed wakes, restarts, and external enqueues, and that list was incomplete. A nacked message becomes eligible again when its retry delay elapses, and nothing signals the wake channel at that moment, so the poll is its only discovery path too. Under the old fixed cadence a retry surfaced within a second of becoming due; under the decay it surfaces up to the ceiling late. At-least-once delivery is unaffected and this is a tradeoff we are taking knowingly, but it is real latency that was not written down, and a deployment whose retry delays matter should size the ceiling against them. The same paragraph now also states the crash-recovered lease case, which is LeaseDuration plus up to the ceiling. We also clamp a decay that overflows. Doubling cur past MaxInt64 wraps to a negative duration, which the existing "greater than ceiling" comparison does not catch and which timer.Reset treats as fire-immediately, turning the backoff into a tight spin against the store. Reaching it needs a ceiling over roughly a hundred and forty years, so nothing can hit this today, but the same argument that keeps the redundant first branch in decay applies here: the method should be correct on its own terms rather than only correct given the callers it happens to have. The overflow is pinned deterministically rather than left to the property test. Widening the property's ceiling domain was the first attempt and it did not work: reaching the overflow by repeated doubling from a realistic floor takes more than forty decays, so the property never got there and the mutant survived with the clamp removed. The deterministic case steps cur to just past the halfway mark and kills it.
In this commit, we scope the non-transactional route skip to the envelopes that actually reach the dispatch table. The skip sits ahead of the kind switch, so it also caught a KIND_RESPONSE whose route happened to be marked even when a live waiter was registered for it. Such a response never resolves to a dispatcher at all: dispatchBatch hands it to the waiter in memory and breaks. The skip was therefore able to drop a response a caller was blocked on, leaving it to time out on its own deadline. Only the legacy path can present one, since splitIngressEnvelopes peels waiter-backed responses off before the fold. It is also unreachable in the current wiring, because marked routes carry operator-to-client requests while waiters belong to client-to-operator RPCs, so the two service and method spaces do not overlap. That was an unstated invariant holding up a silent drop, which is not where we want one, so resolvesToNonTxDispatcher now states the rule and a test holds it. The new test bounds its wait rather than blocking on the future, so the regression it guards shows up as a failure instead of a hang.
In this commit, we hold the wiring invariant the ingress hoist depends on and that no type can express. NonTxRoutes is trusted rather than checked: an EnvelopeDispatcher is an opaque closure, so nothing downstream can tell a mux bridge from a durable Tell. buildRPCDispatchers writes both maps through one helper so the two cannot drift by hand, but that is a convention, and the cost of breaking it is not a stall. A durable Tell hoisted out of the fold commits ahead of the pull cursor, and the re-pull after a crash in that window enqueues a second copy under a fresh ID that receiver-side dedup cannot collapse. So we assert the result instead of the convention: every marked route resolves to a dispatcher, and never to one the EventRouter contributed. Marking an OOR event route fails the test with the route named.
In this commit, we narrow the MaxInFlightUnary doc comment to match the mechanism. It read as though the cap bounded every unary RPC the client has outstanding, when it counts live UnaryFacade waiters in the response registry and nothing else. The durable egress paths do not register an in-memory waiter and are not gated by admitUnary. That is the right scope rather than an oversight: a durable response with no waiter left falls through to durable route dispatch and is redelivered, while a live unary response with no waiter is acked into the void, and it is the discarding this cap exists to bound. Worth stating, since a reader who assumed the broader reading would go looking for a bound on durable egress that is not there and should not be.
actordelivery: retry serialization failures in the durable actor commit tx
…ency-keys multi: mint one idempotency key per logical mailbox RPC
db: run read-only Postgres transactions at REPEATABLE READ
…dispatch serverconn: keep inbound RPC dispatch out of the ingress write tx
baselib/actor: decay the idle mailbox poll instead of polling at a fixed rate
…y-gates wavecli: gate raw money movement
Every expiry decision is derived from blocksRemaining = BatchExpiry - currentHeight and BatchExpiry was trusted unconditionally. It is copied verbatim from the wire by the incoming-VTXO handler, so a zero arriving from the operator reads back as "expired by the entire height of the chain" and classifies a brand-new VTXO as expired. Nothing validated it at ingress and nothing guarded the arithmetic. Add HasUsableBatchExpiry and reject three shapes: a non-positive expiry, and an expiry earlier than the height the VTXO was created at, since a VTXO cannot expire before it existed. CheckExpiry now returns the new ExpiryStatusUnknown for those rather than ExpiryStatusExpired. Unknown is deliberately its own status rather than folding into either extreme. Reporting "expired" surrenders live funds on the strength of a corrupt field; reporting "safe" silently skips the refresh a real deadline needs. LiveState holds the VTXO live and warns, so a data fault neither retires the coin nor wedges the actor on every block. The status is appended last so the existing numeric values are unchanged, and the remaining call sites already treat anything that is not Critical/Expired/NeedsRefresh as inaction. The incoming handler now drops an event carrying an unusable expiry instead of materializing it. Dropping is the safer failure: the wallet still re-derives the VTXO from ListVTXOsByScripts, which reads the authoritative expiry off the server's round row, whereas a poisoned expiry persists locally and is never rewritten.
CalculateCriticalThreshold sized the unilateral-exit window from the commitment-tree depth and the CSV delay alone. It ignored ChainDepth, the number of OOR checkpoint hops between the commitment and the VTXO. Those hops are not free. Each is a recovery transaction that must confirm before the exit's final CSV even starts, and they are strictly sequential because each checkpoint spends the previous one. unroll already budgets fees this way, one recovery tx per hop, so the time budget disagreed with the fee budget. The threshold exists precisely so a client never has to race the operator's sweep, and it was under-sized for exactly the deep OOR chains that need the most room. Factor the sequential transaction count into exitTxDepth: the deepest tree path, since parallel ancestry fragments confirm concurrently and the worst branch sets the pace, plus one transaction per OOR hop. A negative hop count is treated as zero rather than being allowed to shorten the budget.
Identify the witness path that actually spent each watched VTXO instead of inferring intent from chain height. Operator batch sweeps can then retire expired watches without being escalated as client fraud.
A round is checkpointed at input_sig_sent, the point of no return, and can confirm long afterwards. The confirmation handler derives each new VTXO's absolute batch expiry as confirmation_height + sweep_delay but the delay lived only in the in-memory FSM state. A daemon restart between checkpoint and confirmation rebuilt InputSigSentState without it, so the resumed round computed an expiry of confirmation_height + 0 and stamped every VTXO it created with BatchExpiry == CreatedHeight. The wallet reads that back as already expired, which retires a VTXO that was created seconds earlier. Add a sweep_delay column to the rounds table, carry the value on round.Round, and restore it onto both the round record and the FSM state. The upsert only adopts an incoming delay when it is non-zero, since the value is fixed for the life of a round and a later checkpoint must not clear what an earlier one recorded. Rounds checkpointed before this migration have no recorded delay. For those the confirmation path now leaves the expiry unstamped rather than stamping a wrong one, and logs at error level. An unstamped expiry classifies as ExpiryStatusUnknown, so the VTXO stays live and spendable with only expiry monitoring disabled, and the authoritative expiry is still recoverable from the operator's indexer.
Keep expired value recoverable but outside the spendable set. After chain catch-up, replay the synchronized tip through the ordinary refresh and forfeit flow while preserving in-flight locks and restart recovery. Co-authored-by: sputn1ck <kon@kon.ninja>
vtxoStatusToProto had no case for the new expired status, so an expired VTXO was reported to clients as VTXO_STATUS_UNSPECIFIED. Expiry is not terminal — the value is recovered by forfeiting the VTXO in an ordinary round — so a wallet UI has to be able to tell "expired, recoverable, not counted in spendable balance" apart from "the daemon does not know what this is". Add VTXO_STATUS_EXPIRED and map it in both directions.
Allow explicit and automatic expired refresh intents through the daemon. Count quarantined value against the boarding limit and preview the exact one-for-one recovery as fee-free. Co-authored-by: sputn1ck <kon@kon.ninja>
client: Recover expired VTXOs through normal refresh
A confirmed boarding balance is split into VTXO pieces sized by the operator's per-VTXO maximum, so a large deposit under terms like MaxUserBalance / MaxVTXOAmount = 500 mints ~500 outputs in a single round. Boarding signs one MuSig2 nonce per VTXO -- an O(N) step the operator collects within a bounded round window -- so that fanout can overrun the window. The round then fails "recoverable", the deposit never converts, and the board-intent replayer re-issues the identical oversized shape on every retry, stranding the deposit forever (#858). Cap the per-round fanout at a configurable DefaultMaxVTXOsPerBoardRound (128) in clampBoardingAmount: board at most the cap's worth of max-size VTXOs and route the remainder to the existing change leave output, which re-boards over successive rounds. 128 keeps the per-round signing well inside a typical window even on the slowest serial-signing backend while still boarding a large balance in a handful of rounds; operators with a tighter window can lower it via WithMaxVTXOsPerBoardRound. The cap is a deliberately conservative local assumption because the operator does not advertise its nonce-collection deadline; a reactive back-off that shrinks the target on a nonce-collection-timeout failure is a natural follow-up.
wallet: cap per-round boarding fanout to bound nonce signing (#858)
Bring the v0.1.x release line to main commit da2f544 while preserving the release version.
Keep intentional merge topology when CI rebases a pull request onto its target branch. This avoids replaying main as a flat series during release-branch synchronization.
There was a problem hiding this comment.
Pull request overview
This PR merges the recorded main tip into v0.1.x-branch via a two-parent merge commit to keep full ancestry while preserving the release-line version state. The resulting tree largely tracks main (with release-version preservation), bringing in a broad set of protocol, wallet/daemon, database, CLI, and operational improvements that have accumulated on main.
Changes:
- Extend VTXO lifecycle modeling and RPC surface (new
EXPIREDstatus; recoverable-vs-live store splits; additional settlement/exit-cost fields). - Improve reliability/operability across subsystems (unary in-flight admission control, keepalive for swap-server gRPC, fraud watcher sweep-provenance handling, durable actor idle polling backoff).
- Update storage layer and tooling (new DB migrations and post-migration backfills; read-only Postgres isolation policy change; extensive
wavecliUX/automation contracts: kebab-case flags + aliases,--request-json, bounded RPC contexts, prompt/consent gating).
Reviewed changes
Copilot reviewed 211 out of 211 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| waverpc/daemon.proto | Add EXPIRED status and new fee/cost fields |
| waved/wallet_recovery_test.go | Recovery indexer retry/idempotency tests |
| waved/wallet_ops_test.go | Update store mock for new interface |
| waved/vtxo_status_expired_test.go | RPC round-trip test for expired status |
| waved/rpc_server.go | Map expired status; surface fees/exit-cost |
| waved/rpc_refresh_estimate_test.go | Refresh estimate expectations for expired behavior |
| waved/mailbox_nontx_routes_test.go | Assert non-tx route wiring invariants |
| waved/incoming_metadata.go | Add retry seam + idempotency handling for pages |
| wallet/wallet.go | Add max VTXOs per boarding round option |
| vtxo/states.go | Introduce ExpiredState; track last-checked height |
| vtxo/messages.go | Add manager reconcile-expiry message types |
| vtxo/manager_admission_test.go | Test admission behavior for expired VTXOs |
| vtxo/interfaces.go | Add expired status + recoverable listing + settlement fee |
| vtxo/incoming_handler.go | Guard against unusable batch expiry |
| vtxo/incoming_handler_test.go | Test dropping events with invalid expiry |
| vtxo/harness_test.go | Mock store implements recoverable listing |
| vtxo/filter.go | Exclude expired from pending balance sum |
| vtxo/filter_test.go | Add expired coverage to pending balance test |
| vtxo/CLAUDE.md | Document expired lifecycle invariants |
| vtxo/AGENTS.md | Mirror expired lifecycle invariants documentation |
| vtxo/actor.go | Restore expired state; rollback status logic |
| vtxo/actor_test.go | Add expired catchup/rollback behavior tests |
| unroll/actor_test.go | Update VTXO store mock interface |
| txconfirm/states.go | Track confirm block hash through FSM states |
| txconfirm/messages.go | Include confirmation block hash in TxConfirmed |
| txconfirm/fsm_types.go | Add helpers for confirm block hash extraction |
| txconfirm/actor_test.go | Update notifier call signature for block hash |
| systest/send_vtxo_test.go | Preserve ArkProtocolVersion in mailbox replies |
| swapwallet/runtime.go | Retain fee-zero completions briefly to heal lag |
| swapwallet/runtime_test.go | Update leave entry stub usage |
| swapwallet/router.go | Persist sweep-all marker on leave requests |
| swapwallet/reconciler_test.go | Test fee healing after ledger lag |
| swapwallet/projector.go | Prevent fee regression; bounded clear logic |
| swapwallet/normalize.go | Extend leaveEntryStub; net exit cost on completion |
| swapwallet/normalize_test.go | Update leaveEntryStub tests for new arg |
| swapwallet/errors.go | Add ErrCreditReceiveUnavailable sentinel |
| swapwallet/errors_grpc.go | Map new sentinel to gRPC + reason |
| swapwallet/errors_grpc_test.go | Ensure sentinel mapping coverage |
| swapwallet/CLAUDE.md | Document EXIT fee shaping (coop + unroll) |
| swapclientserver/service.go | Add gRPC keepalive params for swap server |
| serverconn/unary_facade.go | Add in-flight admission; idempotency key minting |
| serverconn/AGENTS.md | Document non-tx dispatch invariants and fold rules |
| serverconn/actor.go | Implement unary in-flight admission logic |
| sdk/wavewalletdk/errors.go | Add SDK sentinel parity for credit receive unavailable |
| sdk/wavewalletdk/errmap.go | Map new reason → SDK sentinel |
| sdk/wavewalletdk/errmap_roundtrip_test.go | Extend reason/sentinel/code contract tests |
| rpc/wavewalletrpc/wallet.proto | Add sweep_all; clarify comments |
| rpc/wavewalletrpc/failure_reasons.go | Add CREDIT_RECEIVE_UNAVAILABLE reason |
| round/interfaces.go | Persist per-round sweep delay field |
| round/fees_invariants_test.go | Pin sealed-owned amount fee computation regression |
| README.md | Publish MIT license badge + licensing text |
| oor/local_persistence_handler_test.go | Update store fixture for recoverable listing |
| mailbox/rpc/CLAUDE.md | Document Retry/NewIdempotencyKey semantics |
| mailbox/conn/response_registry.go | Add waiter count for admission gating |
| mailbox/conn/response_registry_test.go | Test waiter count + stale pruning |
| mailbox/conn/CLAUDE.md | Document waiter count as operational metric |
| LICENSE | Add MIT license text |
| ledger/handlers.go | Correct boarding fee credit account; export exit key |
| ledger/handlers_test.go | Update boarding fee posting expectations |
| ledger/CLAUDE.md | Document round_uuid + fee account semantics |
| ledger/actor.go | Update fee bookkeeping documentation |
| fraud/messages.go | Add full spending tx + input index to spend msg |
| fraud/CLAUDE.md | Document sweep-provenance escalation rules |
| fraud/AGENTS.md | Mirror sweep-provenance escalation rules |
| fraud/actor.go | Suppress escalation for proven operator sweeps |
| docs/index.md | Add Postgres isolation doc link |
| docs/fee_ledger.md | Update fee ledger accounting narrative |
| docs/dev_rpc_cli_builder.md | Document kebab-case flags + aliases |
| Dockerfile | Add GOTAGS build arg; build with tags |
| db/vtxo_store.go | Add recoverable listing + settlement fee projection |
| db/sqlite.go | Wire post-migration callbacks for SQLite |
| db/sqlc/schemas/generated_schema.sql | Regenerate schema (round_uuid, sweep_delay, indexes) |
| db/sqlc/queries/vtxo.sql | Add settlement fee subquery; add recoverable query |
| db/sqlc/queries/round.sql | Persist sweep_delay; avoid clearing on later checkpoints |
| db/sqlc/queries/fee_accounting.sql | Add round_uuid, exit cost query, backfill helpers |
| db/sqlc/models.go | Add RoundUuid + SweepDelay to sqlc models |
| db/sqlc/migrations/000016_round_sweep_delay.up.sql | Add rounds.sweep_delay column |
| db/sqlc/migrations/000016_round_sweep_delay.down.sql | Drop rounds.sweep_delay column |
| db/sqlc/migrations/000015_ledger_round_uuid.up.sql | Add ledger_entries.round_uuid + index |
| db/sqlc/migrations/000015_ledger_round_uuid.down.sql | Drop round_uuid index/column |
| db/round_store.go | Persist/restore sweep_delay through checkpoints |
| db/round_store_test.go | Test sweep_delay round-trip + non-erasure |
| db/postgres.go | Wire post-migration callbacks for Postgres |
| db/post_migration_checks.go | Implement round_uuid backfill post-step |
| db/migrations.go | Bump latest migration version to 16 |
| db/ledger_store.go | Write round_uuid on inserts; add confirmed exit cost lookup |
| db/interfaces.go | Relax Postgres read-only isolation; export RandRetryDelay |
| db/interfaces_test.go | Test isolation-level selection + readonly BeginTx |
| db/interfaces_postgres_test.go | Assert server-applied tx isolation/read-only modes |
| db/CLAUDE.md | Document migrations 15/16 and isolation policy |
| db/AGENTS.md | Update migration version + sweep_delay notes |
| credit/registry.go | Add dedicated receive admit timeout + logging |
| credit/registry_test.go | Test receive-timeout fallback behavior |
| credit/config.go | Add ReceiveAdmitTimeout + default value |
| cmd/wavecli/waveclicommands/wallet_table.go | TTY detection + CLI error classification |
| cmd/wavecli/waveclicommands/wallet_password_test.go | Add password input source tests |
| cmd/wavecli/waveclicommands/wallet_client.go | Make dry-run preview a success-path JSON write |
| cmd/wavecli/waveclicommands/swap_removal_test.go | Require send.prepare method in schema |
| cmd/wavecli/waveclicommands/send_result.go | Update inspect command references |
| cmd/wavecli/waveclicommands/schema_registry_ark_observable.go | Add join + watch bounds schema params |
| cmd/wavecli/waveclicommands/rpc_context.go | Add per-RPC timeout context helper |
| cmd/wavecli/waveclicommands/rpc_context_test.go | Test default timeout + disabling + watch bounds |
| cmd/wavecli/waveclicommands/prompt.go | Centralize prompt/consent gating on stderr |
| cmd/wavecli/waveclicommands/prompt_test.go | Test prompt I/O streams + CI/no-input behavior |
| cmd/wavecli/waveclicommands/oor_destination_test.go | Update flag spelling expectations; root command wiring |
| cmd/wavecli/waveclicommands/json_input.go | Rename request input flag to --request-json |
| cmd/wavecli/waveclicommands/json_input_test.go | Test json vs request-json direction contract |
| cmd/wavecli/waveclicommands/flag_normalization.go | Add snake_case → kebab-case normalization |
| cmd/wavecli/waveclicommands/flag_normalization_test.go | Ensure canonical kebab-case; keep snake aliases |
| cmd/wavecli/waveclicommands/exit_codes.go | Add confirmation-required exit code; refine classification |
| cmd/wavecli/waveclicommands/exit_codes_test.go | Test new exit-code mappings (auth, args, consent) |
| cmd/wavecli/waveclicommands/devrpc/types.go | Add RPCContext hook to devrpc config |
| cmd/wavecli/waveclicommands/devrpc/describe.go | Emit canonical kebab-case flag paths |
| cmd/wavecli/waveclicommands/devrpc/describe_test.go | Ensure schema uses kebab-case paths |
| cmd/wavecli/waveclicommands/devrpc/command.go | Support --request-json + bounded RPC contexts |
| cmd/wavecli/waveclicommands/devrpc/command_test.go | Update raw request-json test coverage |
| cmd/wavecli/waveclicommands/devrpc/CLAUDE.md | Document bounded RPC contexts + canonical flags |
| cmd/wavecli/waveclicommands/devrpc/AGENTS.md | Mirror bounded RPC contexts + canonical flags |
| cmd/wavecli/waveclicommands/cmd_vtxos_tty_test.go | Add interactive stdin test helper; tighten TTY rules |
| cmd/wavecli/waveclicommands/cmd_vtxos_autojoin_test.go | Update kebab-case flag names in docs/tests |
| cmd/wavecli/waveclicommands/cmd_unlock.go | Add password-stdin; rename wallet-password-file; bound RPC ctx |
| cmd/wavecli/waveclicommands/cmd_sweep.go | Add --yes gate; use bounded RPC ctx; request-json parsing |
| cmd/wavecli/waveclicommands/cmd_sweep_wallet.go | Add --yes gate; use bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_send_test.go | Improve confirmation error UX; dry-run preview JSON test |
| cmd/wavecli/waveclicommands/cmd_schema.go | Expand schema help text for automation |
| cmd/wavecli/waveclicommands/cmd_recv.go | Rename amt-hint flag; use bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_recovery.go | Use bounded RPC ctx; centralize escalation prompt |
| cmd/wavecli/waveclicommands/cmd_oor.go | Remove local normalization; use bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_mcp.go | Add yes gate to MCP send tools |
| cmd/wavecli/waveclicommands/cmd_list.go | Auto-json format with --json; TTY hint; bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_inspect.go | Auto-json format with --json; bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_getinfo.go | Use bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_fees.go | Use bounded RPC ctx; update docs for dry-run spelling |
| cmd/wavecli/waveclicommands/cmd_exit.go | Use bounded RPC ctx; update dry-run exit semantics |
| cmd/wavecli/waveclicommands/cmd_board.go | Use bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_balance.go | Use bounded RPC ctx |
| cmd/wavecli/waveclicommands/cmd_ark.go | Update docs; use bounded RPC ctx |
| cmd/wavecli/waveclicommands/client.go | Wrap macaroon load errors with stable prefix |
| cmd/wavecli/main.go | Install signal-aware root context for cobra |
| cmd/wavecli/CLAUDE.md | Update exit-code documentation |
| cmd/wavecli/AGENTS.md | Mirror exit-code documentation |
| baselib/actor/durable_actor.go | Add MaxPollInterval plumbing to durable actors |
| baselib/actor/durable_actor_test.go | Test poll interval floor/ceiling plumbing |
| baselib/actor/CLAUDE.md | Document idle poll floor/ceiling behavior |
| baselib/actor/AGENTS.md | Mirror idle poll floor/ceiling behavior |
| .github/actions/rebase/action.yml | Preserve merges when rebasing CI branch |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| cost, err := r.server.ledgerStore.GetConfirmedExitCost(ctx, outpoint) | ||
| if err != nil { | ||
| r.server.log.WarnS(ctx, "Confirmed exit cost lookup failed", | ||
| err, | ||
| slog.String("outpoint", outpoint.String()), | ||
| ) | ||
|
|
||
| return | ||
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b449f29708
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| treeDepthBuffer := exitTxDepth(vtxo) * c.TreeDepthMultiplier | ||
| csvBuffer := int32(vtxo.RelativeExpiry) | ||
| safeExitBuffer := treeDepthBuffer + csvBuffer |
There was a problem hiding this comment.
Saturate the exit-threshold arithmetic before narrowing
When an incoming VTXO advertises a sufficiently large positive ChainDepth, exitTxDepth returns math.MaxInt32, but multiplying that int32 by TreeDepthMultiplier and adding RelativeExpiry can overflow before the threshold is compared. With the defaults, MaxInt32*6+144 wraps negative, so the critical threshold falls back to 36 blocks instead of conservatively forcing an immediate exit; malformed or hostile indexer metadata can therefore make a deep OOR lineage wait until it no longer has time to unroll. Perform the multiply/add in a wider type and saturate the final result.
AGENTS.md reference: vtxo/AGENTS.md:L113-L117
Useful? React with 👍 / 👎.
| _, becameExpired := transition.NewState.(*ExpiredState) | ||
| _, wasExpired := transition.PriorState.(*ExpiredState) | ||
| if !becameExpired || wasExpired { | ||
| continue |
There was a problem hiding this comment.
Refresh cached status when reconcile relives a VTXO
If a persisted Expired VTXO becomes live again at the current tip, such as after a reorg, ExpiredState.ProcessEvent persists Live but this condition immediately continues without updating liveDescriptors. initFraudWatcher subsequently consumes that snapshot, and fraud.shouldTrackDescriptor skips the still-Expired descriptor even though its actor and database row are now live, leaving an OOR coin without ancestry-spend watches until it is rematerialized. Update the cached descriptor for the Expired -> Live transition before continuing.
AGENTS.md reference: vtxo/AGENTS.md:L137-L142
Useful? React with 👍 / 👎.
| }, | ||
| } | ||
|
|
||
| r.runtime.projectAndEmit(context.WithoutCancel(ctx), entry) |
There was a problem hiding this comment.
Bound the detached failed-receive projection
When credit admission fails while the caller context remains live, this removes every cancellation and deadline before synchronously entering projectAndEmit, which takes projectMu and performs database reads/writes. If the activity store blocks on a database lock or outage, canceling the Recv RPC cannot release the call or the mutex, so all other activity projections can remain wedged. Preserve the detached cleanup intent with an explicit finite timeout rather than an unbounded WithoutCancel context.
AGENTS.md reference: AGENTS.md:L75-L81
Useful? React with 👍 / 👎.
Summary
Merge
mainatda2f5441intov0.1.x-branchat7cbef62bthrough a dedicated integration branch.This intentionally makes every commit reachable from the recorded
maintip an ancestor of the release branch, while preserving the release line's version state. GitHub may show 151 main-side commits because the branches have diverged; this PR does not recreate those commits or cherry-pick them individually.Strategy
v0.1.x-branch.maintip with a two-parent merge commit.mainimplementations.0.1.0; bump to0.1.1separately.Result
60399d487cbef62bda2f5441mainexcept forbuild/version.go.Validation
make sqlcmake fmt-changedmake lint-localmake unit timeout=30mmake systestSYSTEST_TIMEOUT=20m make systest db=postgresmake tidy-module-checkmake commitmsg-lint range="origin/main..HEAD"HEAD.da2f5441isbuild/version.go.