live-store: owner-tagged atomic single-spend domain (off-chain + on-chain), part of #1159 - #1162
bitcoin-coder-bob wants to merge 22 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (7)
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review. WalkthroughThe change replaces local off-chain transaction locking with owner-scoped atomic input claims. It updates in-memory and Redis stores, rebuilds Redis claims at startup, integrates claim release into intent lifecycle operations, and expands application, integration, and end-to-end coverage. ChangesOff-chain claim lifecycle
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SubmissionService
participant VTXORepository
participant OffChainTxStore
participant IntentStore
SubmissionService->>VTXORepository: Refresh spent VTXO state
VTXORepository-->>SubmissionService: Return current input status
SubmissionService->>OffChainTxStore: Atomically claim inputs
OffChainTxStore-->>SubmissionService: Return claim status and conflict outpoint
SubmissionService->>IntentStore: Queue accepted intent
IntentStore-->>SubmissionService: Return insertion result
SubmissionService->>OffChainTxStore: Release claims during cleanup
Suggested reviewers: Merge Risk: 🟡 Moderate · up to This change adds shared atomic input claims for off-chain and on-chain spending, but unresolved compilation and claim-lifecycle failure paths could prevent validation or allow duplicate/conflicting spends or stuck funds. Resolve these paths before merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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 |
…tore Adds a claim primitive over the same spent-input set that Add/Includes/Remove use, for spends that carry no checkpoint txs (on-chain Arkade cosigns, #1159). ClaimOutpoints registers a batch of outpoints only if none is already present, returning the first conflicting one otherwise; ReleaseOutpoints removes them. - inmemory: two passes under one write lock, so the claim is all-or-nothing and atomic within the process. - redis: a single Lua check-and-set, so concurrent on-chain claims are atomic across processes (the separate SIsMember+SAdd path is not). Includes is unchanged and remains the shared read, so an on-chain claim is visible to the off-chain guard and vice-versa. Scope limit worth stating: the off-chain Add writer is still a non-atomic check-then-set, so cross-process mutual exclusion between the off-chain and on-chain writers is not yet guaranteed; that, plus also consulting the Intents domain, is required before the cosign path ships. Purely additive: the new methods are unused until then. Part of #1159.
…ter atomic Unifies off-chain and on-chain single-spend into ONE atomic conflict domain across processes (#1159). The input set becomes owner-tagged (outpoint -> owning arkTxid): inmemory map[string]string, redis SET -> HASH via a new key. Both writers go through one owner-tagged compare-and-set (redis Lua, inmemory single lock): a different owner conflicts, the same owner is idempotent. - Add returns a tri-state (ClaimFresh / ClaimAlreadyOwned / ClaimConflict) and claims every checkpoint TxIn (unchanged registration set, so the multi-input case is preserved exactly, not narrowed to TxIn[0]). - ClaimOutpoints/ReleaseOutpoints gain an owner param; Release/Remove are owner-scoped so one tx cannot drop another's protection. - SubmitOffchainTx: delete offchainTxMu and its recheck loop; the atomic store is now the sole cross-process barrier. On ClaimAlreadyOwned (a concurrent or retried same-arkTxid submit) it returns the accepted result without appending a second OffchainTxAccepted event, preserving the single-accept invariant. Rollout note: the redis key changes type (SET -> HASH), so it uses a new key name to avoid WRONGTYPE on a mixed-version deploy. Off-chain submissions should be quiesced across the upgrade window, since an in-flight tx loses its input protection at the instant of the version switch. Part of #1159.
The concurrent submit txs fixture listed 7 destination pkscripts, but three of them were byte-identical copies of the same value, so only 5 were really distinct. That was a fixture slip in a hand-edited hex block. The ark txid is computed over the unsigned tx, and signatures live in the taproot witness, so a shared destination pkscript over the same vtxo input produces the same unsigned tx and therefore the same arkTxid. Under the owner-tagged single-spend domain a same-arkTxid resubmission is an idempotent re-claim, not a conflict, so those duplicates returned success instead of VTXO_ALREADY_SPENT and the success count came out as 2 or 3 depending on the backend. No double spend occurred, one txid means one spend, the fixture simply was not testing distinct double spends. Derive the destinations from freshly generated keys via script.P2TRScript so all 7 submissions are genuinely different ark txs contending for one vtxo. The assertions are unchanged, still exactly 1 success and len(destinations)-1 errors.
6f15678 to
ad348b9
Compare
|
This PR has been open for 3+ days without review. @bitcoin-coder-bob is anyone looking at this? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
This PR has been open for 9 days without review. @bitcoin-coder-bob is anyone looking at this? (Part of the #1159 VTXO live-store stack — #1160 and #1161 also awaiting review.)
|
This PR (and the stack #1160 → #1161 → #1162) has been open for 19+ days without review. @bitcoin-coder-bob is anyone looking at this? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — #1162
live-store: owner-tagged atomic single-spend domain
Core change
Replaces the per-process offchainTxMu mutex + recheck-then-add pattern with an owner-tagged atomic claim at the store layer. The inmemory implementation uses its own write lock; the Redis implementation uses Lua scripts, making the conflict check + registration a single server-side atomic step that is safe across multiple arkd processes.
Lua scripts
The scripts are correct:
claimScriptandaddScriptcheck all outpoints for different-owner conflicts first (returns immediately without registering anything on conflict), then check if the owner already holds all of them, then register the batch. The two-pass structure is correct for Lua atomicity: since the script runs atomically on Redis, no interleaving between the conflict check and the HSET is possible.removeScriptandreleaseScriptonly delete outpoints whose owner matches — correct scoped deletion.- The
anyNewlogic correctly distinguishes ClaimAlreadyOwned (all already owned by same owner) from ClaimFresh (at least one new).
Key rename
offChainInputsSetKey (Redis SET) → offChainInputsHashKey (Redis HASH) with a new key name. This avoids a WRONGTYPE error on a mixed-version rolling deploy since the old SET is left intact until it expires. Well handled.
ClaimAlreadyOwned (idempotent retry)
When Add returns ClaimAlreadyOwned, the service skips applying the Accepted event (changes = nil) and returns the accepted result. This correctly handles a concurrent or retried submit of the same arkTxid without emitting a duplicate event.
ClaimOutpoints / ReleaseOutpoints
New methods for the on-chain Arkade cosign path (#1159). The same conflict domain — good, the mutual exclusion is enforced at the store layer regardless of the path.
Notes
- The inmemory
Removepreviously deleted inputs unconditionally; it now only deletes inputs owned by the arkTxid. This is stricter and correct. - The Redis
Removeloads the tx first to get inputs — if the tx is absent (already removed), it still callsremoveScriptwith no outpoints (no-op). Safe. parseClaimResultparsing from the Lua return array was not shown; worth confirming the error handling is correct for unexpected return shapes.
Verdict: Design is sound, atomicity is correct, and the idempotent-retry case is well handled. Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — owner-tagged atomic single-spend domain
🔒 PROTOCOL-CRITICAL — please ensure a second human reviews before merging. This touches the core double-spend prevention for off-chain transactions.
What this does: Replaces a per-process mutex + read-check-then-add pattern in SubmitOffchainTx with a single atomic owner-tagged claim step. The new OffChainTxStore.Add returns a ClaimStatus tri-state (Fresh/AlreadyOwned/Conflict) and accepts no external lock. The same conflict domain is extended for on-chain spends via ClaimOutpoints/ReleaseOutpoints.
Design correctness:
- The old pattern had a TOCTOU window: read (
Includes), release lock, concurrent submit claims the same VTXO. The new atomic claim eliminates that window. - In-memory backend: the
sync.RWMutexwrapping the map makes the claim atomic at the process level. ✓ - Redis backend: a Lua script (not shown in diff, but referenced via
addScript,claimScript, etc.) makes the claim atomic across processes. The design note in the port interface makes this explicit. Assuming the Lua script itself is correct, the approach is sound. ClaimAlreadyOwnedreturning the accepted result without re-applying the event is correct idempotency for same-arkTxid retries.- Removing
offchainTxMufrom the service struct is safe given the store is now the single authority — make sure no other call site depended on that mutex outsideSubmitOffchainTx(the diff removes it from test structs as well, which suggests it was only used here).
Things to verify before merging:
- The Lua scripts (
addScript,claimScript,removeScript,releaseScript) — not in this diff but critical to correctness. Ensure they are also reviewed. - Confirm
Remove(called on finalization) correctly releases inputs so they don't permanently block the outpoints. - The
ClaimAlreadyOwnedpath setschanges = nil, which drops any new event. Verify there are no side effects from the earlier part ofSubmitOffchainTxthat need reverting when we take this path.
Test coverage: The e2e double-spend test is updated with distinct randomized destinations (fixing a pre-existing ambiguity where same-destination txids would be identical). Good improvement.
Verdict: Approach is sound and a clear security improvement over the mutex pattern. Human review of the Lua scripts and the change = nil path is essential before merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
This PR has been open 28 days without review. @bitcoin-coder-bob is anyone looking at the single-spend domain stack (#1159)?
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha ad348b9
This PR converts OffChainTxStore.Add from a plain write to an owner-tagged atomic claim, and adds ClaimOutpoints/ReleaseOutpoints for on-chain Arkade spends. It also removes the per-process offchainTxMu mutex in SubmitOffchainTx in favour of the store's atomic barrier.
Correctness of the core atomic claim logic:
The inmemory claimLocked function is a clean single-lock implementation:
- First pass: reject if any outpoint is held by a different owner (returns
ClaimConflictand the conflicting outpoint, registers nothing). - After conflict pass: if all present outpoints are owned by
owner, check for any new ones. If none,ClaimAlreadyOwned. Otherwise register all andClaimFresh.
This is correct and all-or-nothing within the lock.
SubmitOffchainTx service change:
- Removes the explicit re-check loop (was prone to TOCTOU between process-local lock and the store) and the
offchainTxMumutex. ClaimAlreadyOwnedpath correctly clearschangesto avoid emitting a duplicateAcceptedevent — good.ClaimConflictreturns the conflicting outpoint in the error, which is an improvement for diagnostics.
One question: The Remove path now only deletes inputs owned by this arkTxid. If a concurrent Add by a different owner races with Remove (e.g. after a partial round failure), is there a scenario where inputs stay claimed beyond their intended lifetime? Specifically: can ClaimAlreadyOwned on a re-submit prevent the original owner's Remove from cleaning up? Looking at the logic: Remove deletes only entries where owner == arkTxid, so a different-owner conflict would never be cleaned up by the original owner's Remove. But by design, a conflict returns ClaimConflict and the new owner never gets registered — so there's no cross-owner input left dangling. This looks correct.
Test coverage is good: disjoint claim, idempotent re-claim, all-or-nothing conflict, owner-scoped release, and a concurrent stress test.
Looks ready. Not protocol-critical in the tree-signing sense, but double-spend prevention is critical.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
First review — #1162 (owner-tagged atomic single-spend domain)
This PR introduces an important architectural change: the OffChainTxStore becomes the canonical cross-process single-spend conflict domain, shared by both off-chain and on-chain (Arkade) spends, with owner-tagged outpoint registration.
Design assessment:
- The
ClaimStatusenum (ClaimFresh / ClaimAlreadyOwned / ClaimConflict) is a clear, well-named three-way outcome. The semantics are well-documented in the interface comment. - Moving from a per-process
offchainTxMu+ recheck pattern to an atomic store operation (Redis Lua / in-memory lock) is the correct progression — it's the only option that works across processes. ClaimAlreadyOwned(idempotent retry by the same owner) is handled inSubmitOffchainTxby settingchanges = nilso no duplicate Accepted event is written. This is correct.- The new
ClaimOutpoints/ReleaseOutpointsmethods for on-chain Arkade UTXOs (issue #1159) extend the same domain cleanly.
Questions / observations:
- The in-memory store's snapshot in
Get(maps.Clone) is a good race fix, butSigningContext.AggregatedNoncesis a slice and isn't cloned. If callers mutate it concurrently, there's still a data race — worth checking if that path exists. - When
ReleaseOutpointsis called for an owner, outpoints held by a different owner are left untouched — this is the documented behaviour. Confirm that the on-chain flow that calls this never races with an off-chain claim of the same outpoint in a way that could release the wrong owner's claim. - The PR description mentions this is part of #1159 — dependent PRs (#1161, #1160) should be reviewed in sequence before this lands.
Overall the design is sound. The atomic store semantics are correct.
…single-spend-claim Two conflicts in service.go, both from master landing race fixes this branch had not seen. The struct: this branch removes offchainTxMu, master adds feeBumpMtx. Both are taken, but offchainTxMu is restored rather than dropped, because master also added a second use of it in RegisterIntent (ArkLabsHQ/arkd#62) guarding the conflict-domain check against a concurrent SubmitOffchainTx. Dropping the field would have silently deleted that fix. The owner-tagged claim replaces it in the next commit. SubmitOffchainTx: master's block is the offchainTxMu recheck-then-add that the atomic claim already replaces, so that part is dropped, but it also carries the DB re-read from ArkLabsHQ/arkd#71, which the claim does not replace. An accepted offchain tx is evicted from the cache once projected, so the earlier read can be stale and the claim has nothing left to conflict against. That re-read is kept.
…TxMu RegisterIntent now claims its vtxo inputs through ClaimOutpoints instead of checking Includes under offchainTxMu. The claim is atomic across processes, as Add is, where the mutex only serialised one process, and it reserves rather than only checking, so it also stops two intents registering the same vtxo. That makes offchainTxMu redundant and it is removed. Reserving creates an obligation the old read-only check did not have: a claim that is never released leaves the vtxo permanently unspendable, and on redis that survives a restart. Every path that drops an intent now releases its claims: round completion, where the round carries the intents Pop selected; delete by intent proof; admin delete and delete-all; and a failed Intents().Push, so a rejected registration leaves nothing behind. Intents still queued for a later round keep their claims, which is what we want. Release is per-owner and idempotent, so releasing a claim already gone, or one since taken by someone else, is a no-op. Failures are logged rather than returned, since every caller is dropping the intent regardless. This rests on every registered intent eventually being selected by Pop. Pop skips intents with no receivers, which RegisterIntent cannot produce because Intent.validate rejects an empty output set. The claim site says so, since making receivers optional would silently reintroduce the leak.
…single-spend-claim No conflicts. Picks up the trivy scan fix (#1172), which this branch needs for a green Build and Scan: the pull_request workflow builds a merge commit, so a fresh run inherits master's no-cache-filters, but re-running the existing run reuses the old merge commit and keeps failing.
|
This PR has been open 32+ days without a formal review. @bitcoin-coder-bob — is this in the review queue? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — #1162 (sha bf4c653, updated from ad348b9)
live-store: owner-tagged atomic single-spend domain
Protocol-critical — changes the SubmitOffchainTx double-spend barrier. Human sign-off required.
Key changes since sha ad348b9
offchainTxMuper-process mutex removed fromservicestructSubmitOffchainTxdrops the per-process recheck-under-mutex pattern; the store'sAdd()is now the single barrierAdd()returns a status enum:ClaimFresh,ClaimConflict,ClaimAlreadyOwned- Retry path for idempotent submits:
ClaimAlreadyOwnedreturns success without re-emittingAccepted releaseClaimsOfIntents/releaseClaimsOfIntentIdsadded;DeleteIntentsnow calls release before delete- Comprehensive test in
intent_claims_test.go
Looks correct
- The claim tag being the intent/tx owner id (not global) is the right design: cross-owner release would clobber valid claims
ClaimAlreadyOwnedpath setschanges = nilbefore returning — no duplicate event in projections- The "re-read before claiming" comment and reference to ArkLabsHQ/arkd#71 give good context
- Tests verify per-owner keying, lookup-by-id path, boarding-only intent (no inputs = no claims), lookup failure isolation, and delete-all claim release
One concern to verify
The script_watch_test.go change removes offchainTxMu: &sync.Mutex{} from service initialization. Confirm no other test or code path in the file initializes service with a mutex expectation that would now panic or dereference nil.
Question
In the ClaimConflict branch, conflict.String() is used in the error — does this always produce a useful outpoint string, or can it return an empty/zero value for non-vtxo input types (e.g. boarding inputs)?
Verdict: Looks ready. The atomicity move from per-process mutex to store-level claim is the right architectural direction for a multi-process deployment.
|
Answering the two items from the last pass, plus what I found on the red check. 1.
|
…single-spend-claim
|
Correction to my previous comment. I said master's integration CI was broken by #1110 and that this branch would stay red on #1161 at What still stands is the part that matters here: master at I have merged current master into this branch ( |
|
This PR (live-store owner-tagged single-spend domain) has been open for 5+ weeks without a review. @bitcoin-coder-bob — is this blocked on #1163 or another dependency? |
Only intents registered on the round were released, so an intent Pop selected and then dropped (spent boarding input, liquidity abort, failed re-push, crash before the round was stored) kept its vtxos claimed forever. Reconcile from the selected set instead: release every popped intent no longer queued.
…single-spend-claim
|
This PR has been open for 36+ days without review. @bitcoin-coder-bob the owner-tagged atomic single-spend domain PR is getting stale — is it blocked on anything? |
|
Changes were requested 41+ days ago. @bitcoin-coder-bob need any help addressing the feedback on the live-store owner-tagged single-spend domain? |
|
Changes were requested 41+ days ago. @bitcoin-coder-bob is this still in progress? |
…budget Add relies on a re-add of the same arkTxid carrying the same inputs: were a changed set ever to arrive, the inputs only the previous body named would stay claimed, because Remove finds a tx's inputs through the body it replaced. That cannot happen, since an arkTxid is the txid of a tx committing to the checkpoint outputs it spends, and the caller rebuilds and compares before calling. Written down on the port so the next reader does not have to re-derive it. The rebuild failing on its own deadline now says so, with the budget, instead of surfacing a wrapped context error.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Follow-up pass — 1 commit since 562db18 (1058a24). All 3 prior issues are now closed. No new findings.
Prior-issue closure
1. Blocking — Duplicate release path (service vs. admin) → Fixed (562db18, unchanged)
Shared helpers in intent_claims.go mechanically enforce the invariant. Closed.
2. Minor — Misleading rebuild log count → Fixed (562db18, unchanged)
Counter increments only when added > 0. Closed.
3. Non-blocking — 2-minute rebuild timeout operator documentation → Fixed (1058a24)
NewOffChainTxStore now emits a timeout-specific error:
"failed to rebuild offchain tx inputs from stored txs within %s: %v" (with rebuildTimeout interpolated) when ctx.Err() != nil after rebuildInputs returns, and falls through to the generic message when the context is live. A docstring on NewOffChainTxStore also explains why the constructor fails fatally rather than degrading (partial rebuild = ClaimFresh answers for every in-flight tx it didn't reach). This is the right place to put it. Closed.
Incremental review of 1058a24
This commit is documentation and error-message only. No logic was changed.
internal/core/ports/live_store.go:87–95
The new comment on Add correctly states the arkTxid invariant: because an arkTxid is the txid of the ark transaction committing to the checkpoint outputs it spends, a different input set implies a different arkTxid, so the same-owner idempotence path can only be reached with identical inputs. The documented failure mode ("inputs only the previous body named would stay claimed") is accurate and useful — it describes exactly what Remove would miss if the invariant were ever broken. No issue.
internal/infrastructure/live-store/redis/offchain_txs.go:149–159
The ctx.Err() != nil check is correct for distinguishing deadline expiry from other errors. One edge case is benign: if rebuildInputs returns a non-context error that coincides with the deadline having just passed, the operator sees the "within timeout" message rather than the underlying error — but %v in the message still includes the wrapped error text, so it is not silenced. Acceptable.
No protocol, security, or correctness findings in this increment. The single-spend enforcement path reviewed in prior passes is unchanged.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — live-store: updated (claim release on DeleteIntents)
Since my last review (sha ad348b9), the key addition is intent_claims.go and the rework of DeleteIntents in the admin service.
The bug fixed: the previous DeleteIntents implementation wiped the store directly or called Delete without releasing claims first. If an intent holding vtxo claims was deleted without releasing them, those vtxos would remain claimed indefinitely — locked out of future rounds with no recovery path.
Fix is correct:
- Snapshot intents first, then release their claims, then delete exactly that snapshot by id. This avoids the TOCTOU risk of releasing an intent that was registered after the snapshot (whose claim was never released and which round-start reconcile can't see since it was never popped).
releaseClaimsOfIntentslogs failures rather than returning them, because the delete path proceeds regardless, and a lost release is a stale claim rather than a caller-visible error.- The test
"the service and the admin service release identically"is particularly valuable: it pins the contract that both code paths release through the same helper, preventing drift.
Redis init: the NewLiveStore error is now surfaced — a silent init failure would produce a broken live store.
Test coverage is comprehensive. Looks ready to merge. ✓
|
Changes were requested on 2026-07-27 (44 days ago) with no update. @bitcoin-coder-bob need any help addressing the feedback? |
Nineteen comments used a colon where a conjunction or a second sentence reads better, including the five ARGV layout notes on the Lua scripts. The ClaimOutpoints doc carried an issue number, and one comment in SubmitOffchainTx described the mutex-guarded check the claim replaced. Both are facts about the change rather than about the code, so they belong in the commit message and the PR description. The redis key comment kept its substance, since a deliberately new key name that avoids a WRONGTYPE during a mixed-version rollout is the kind of decision that looks arbitrary without it. Comments only. No statement changed.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana follow-up review — b0ca2a6
Status: One cosmetic commit since the last pass; 3 of 3 prior code findings are fixed, 1 ops item remains open. No new code findings.
What changed
Commit b0ca2a6b0b ("live-store: drop colon syntax and an issue reference from the comments") rewrites comment text across seven files — grammar, punctuation, and prose style. No logic is touched in any of those 20 changed lines.
Prior findings — close-out
Fixed — wire.OutPoint.String() vs domain.Outpoint.String() format mismatch
internal/infrastructure/live-store/redis/offchain_txs.go:420–425
checkpointInputs now constructs a domain.Outpoint{Txid: ..., VOut: ...} and calls .String() on it, with an explicit comment tying it to ClaimOutpoints, ReleaseOutpoints, and Includes. Fixed at the baseline commit (1058a24003) by the "fail closed on a failed rebuild, parse checkpoint inputs all or nothing" commit, predating this increment.
Fixed — offchainTxMu removal, no nil-deref survivors
Answered by @bitcoin-coder-bob: the struct field, every initialisation (including script_watch_test.go), and every call site are gone. The only remaining mention is a comment in service.go describing what the claim replaced. Confirmed clean.
Fixed / No longer applicable — conflict.String() shape on boarding inputs
Answered by @bitcoin-coder-bob: claims are only taken over VTXO inputs. A boarding-only intent takes no claims and is covered by its own test. The ClaimConflict branch is therefore unreachable for non-VTXO input types; no zero-value or empty conflict outpoint can surface there.
Still open (ops) — Redis key-type migration window
The key rename from offChainTxStore:inputs (SET) to offChainTxStore:inputsByOwner (HASH) avoids a WRONGTYPE error on a mixed-version rolling deploy, but there is a brief window during the upgrade where in-flight off-chain txs that accepted under the old version retain their stored body without re-registration in the new hash until the first restart of an upgraded instance. The PR description explains the rebuildInputs startup path that closes this, and the commit logs describe the fallback. This needs ops sign-off on the upgrade sequencing (quiesce or accept the window) — it is not a code defect, and nothing in this increment changes the situation.
No new findings from the increment. Protocol-critical flag from the prior pass stands — human sign-off before merge is still the right call.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — live-store: owner-tagged intent claim release
Protocol-critical (vtxo claim management). Flagging for human sign-off.
This PR was reviewed at sha 49ec1a83 on 2026-09-04 (questions about AggregatedNonces clone race and ReleaseOutpoints race). Re-reviewing the updated diff.
What changed since last review:
DeleteIntents(admin) now snapshots intents, releases their claims, then deletes exactly the snapshot. This fixes the race where a delete-all could wipe an intent that registered after the snapshot, leaving its vtxo claims stranded.releaseClaimsOfIntentsis now a shared helper used by both theserviceandadminServicepaths, preventing them from drifting.intent_claims.go+ comprehensive tests inintent_claims_test.gocovering: per-owner release, lookup-by-id, boarding-only intents (nothing to release), lookup failures, round-start selected-intent release, delete-by-proof, and the delete-all snapshot race.- Redis live store constructor now returns an error (
NewLiveStore→errchecked in config).
Looks solid.
- The snapshot-then-delete pattern in admin DeleteIntents correctly handles the race. The test "admin delete-all keeps an intent registered after the snapshot" directly exercises it with an
afterViewAllhook — well done. releaseClaimsOfIntentsis idempotent: failed releases are logged rather than surfaced, which is correct since the caller drops the intent regardless.- The "the service and admin service release identically" test pins the two paths against each other, which is exactly the right regression guard.
Previously raised questions:
- AggregatedNonces clone race — is that concern addressed in this diff or a follow-up?
- ReleaseOutpoints concurrency — the owner-tagging design means concurrent releases for different owners are independent; a concurrent release for the same owner on the same outpoint is idempotent. Looks addressed by the design.
Looks ready pending confirmation that the AggregatedNonces concern from the previous review has been resolved.
The colon check keyed on a lower-case letter following the colon, so a sentence continuing with a capital slipped through.
Both sat at the end of a comment line, where the first pass only looked for a colon mid-sentence. Repairing them also needed the clause on the following line, which was left starting lower case.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana follow-up — #1162 (d860341, incremental from b0ca2a6)
Status: 2 commits since last pass, both comment-only (no logic changes). 3 prior issues fixed, 1 still open, 1 silently dropped in earlier passes now acknowledged.
Prior issue tracking
1. offchainTxMu nil-dereference concern — Fixed
Confirmed by @bitcoin-coder-bob: the field and every initialisation site are gone. No code path can dereference it. Closed.
2. conflict.String() for non-vtxo input types — Fixed
Confirmed by @bitcoin-coder-bob: boarding-only intents take no claims and cannot reach the ClaimConflict branch. The conflict value is always a typed *domain.Outpoint. Closed.
3. Comment colon style — Fixed (commits e94baf3, d860341)
These two commits are the entire incremental diff. They fix the remaining mid-sentence and end-of-line colons in redis/offchain_txs.go and live_store_test.go that earlier passes missed. All three comment-only commits collectively address the prose style feedback. No logic changed.
4. Rolling-upgrade / Redis key-type quiescing — Still open
The old offChainInputsSetKey (Redis SET) is not migrated to the new offChainInputsHashKey (Redis HASH). A rolling deploy leaves a window where an in-flight tx retains its stored body but loses input-claim coverage when the old key expires. @bitcoin-coder-bob explicitly flagged this as needing ops sign-off rather than code review. That sign-off has not appeared in this thread. This must be resolved — via a documented upgrade runbook, a one-time migration step, or explicit acceptance — before the PR lands.
5. SigningContext.AggregatedNonces slice not deep-copied — Acknowledged as silently dropped
I raised this in the first pass ("the AggregatedNonces slice inside the maps.Clone snapshot isn't cloned; concurrent mutation is still a data race if that path exists") and then dropped it from subsequent reviews without closure. The incremental diff does not touch that code, so I cannot close it here. Before merge, confirm whether any call site mutates the nonce slice after Get returns, and if so add a slices.Clone or equivalent. If no call site mutates it, note that explicitly so future reviewers do not re-raise it.
Incremental diff assessment
No logic was changed in these two commits. The substantive review remains as filed. The single blocking item is rolling-upgrade ops sign-off (item 4 above).
|
Changes were requested 45+ days ago. @bitcoin-coder-bob need any help addressing the feedback? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — #1162 (sha d860341)
live-store: owner-tagged atomic single-spend domain (updated)
This is an update since my previous review. The new intent_claims.go / intent_claims_test.go files are the main addition.
What changed / review
releaseClaimsOfIntents helper (intent_claims.go):
- Centralises the claim-release logic that previously had to be duplicated between service and admin paths.
- Release is per-owner and idempotent — a claim already gone or taken by someone else is left alone. Correct.
- Failure is logged-not-returned on purpose: the caller always drops the intent regardless, so a stale claim (bad but survivable) is better than surfacing an error that hides the real operation.
DeleteIntents fix (admin.go):
- Old code:
DeleteAllwhen no IDs passed, which wiped the store atomically without releasing claims. - New code: snapshot → release claims → delete by ID. Correctly avoids deleting an intent registered after the snapshot (whose claim was never released).
- Test
admin delete-all keeps an intent registered after the snapshotexercises this race explicitly — good.
config.go fix:
NewLiveStorenow returns(LiveStore, error), and the error is propagated. This was a latent bug (unchecked init failure for Redis live store).
Tests (intent_claims_test.go):
Very comprehensive: covers boarding-only intents (no release), lookup failure (no panic, no wrong release), round-start selected-intent release (only non-queued ones), delete-by-proof, admin delete-all with concurrent registration, and the symmetry test that proves the service and admin service use the same release path. Coverage is excellent.
Minor note
The releaseClaimsOfSelectedIntents and releaseClaimsOfIntentIds methods on service — these appear in tests but weren't in the diff snippet. Assuming they call through to releaseClaimsOfIntents as the tests imply.
Looks ready to merge. The original review concern (unchecked DeleteAll leaking claims) is addressed.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — #1162 live-store: owner-tagged atomic single-spend domain
The DeleteIntents fix and releaseClaimsOfIntents consolidation look correct.
DeleteIntents fix (admin.go). The old code called DeleteAll (wipe the store) or Delete(ids) without releasing claims first. The new path snapshots with ViewAll, releases claims for each intent under its own id, then deletes exactly the snapshotted set. The comment explaining the race is accurate: a wipe would also drop an intent registered after the snapshot whose claims were never released, leaving vtxos claimed forever. This is the right fix.
releaseClaimsOfIntents (intent_claims.go). Extracting the release logic to a package-level helper shared by all delete paths is the right call. The comment on "two copies of this that drifted apart" explains the motivation clearly. Error on release is logged rather than returned — intentional, the comment gives the right reasoning.
Tests (intent_claims_test.go). The table covers: per-owner release, id-only lookup path, boarding-only intent (no claims), lookup failure, round-start selected-intent release, and delete-by-proof. The claimIntentStore.selected vs all test for round start is the tricky one and it's correct: only intents in selected but not in all (i.e., those not re-queued) should release.
One small observation: the recordingOffchainTxStore mock accumulates releases per-owner in a map. A test that calls release twice with the same owner would not detect duplicate releases. Not a blocking concern, but worth keeping in mind if new callers are added.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review · sha d860341f
Summary: Fixes a claim-leak bug in DeleteIntents and centralises the intent-claim-release logic into a single helper that both the service and admin service share.
Correctness of the fix:
The old DeleteAll() path wiped the store atomically, which could delete an intent registered after the snapshot started, leaving its vtxo claims held with no round cycle to release them. The fix snapshots first (ViewAll), releases claims against that snapshot, then deletes only the snapshotted IDs. This is the correct ordering.
The afterViewAll injection in the test ("admin delete-all keeps an intent registered after the snapshot") cleanly demonstrates the race that was possible and verifies that post-snapshot intents survive.
Observations:
releaseIntentClaimslogs rather than returns an error on release failure. The comment explains why: the caller drops the intent regardless, and a failed release leaves a stale claim rather than a caller-visible error. This is a deliberate trade-off and the rationale is clear.- The
releaseClaimsOfSelectedIntentslogic for round-start is a nice fix: it only releases intents that were selected but are no longer queued, not all selected intents, preventing double-release of a re-pushed intent. releaseClaimsOfIntents/intentsOfare well-decomposed. The cross-test ("the service and the admin service release identically") is an excellent guard against drift between the two paths.- Removal of
offchainTxMufromservice: this field disappears from the struct, initialisation, and tests. Confirm that no other branch still touches it.
Verdict: Looks ready. This is an important correctness fix with excellent test coverage. The shared claim-release helpers are the right architecture.
|
Changes were requested 7+ weeks ago on this PR. @bitcoin-coder-bob need any help addressing the feedback? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha d860341
What this does: three related correctness fixes in one PR:
-
DeleteIntents claim leak fix (admin): Previously with no ids would call , dropping intents without releasing their vtxo claims. Now it snapshots via , releases claims from the snapshot, then deletes by the snapshot's ids. A registration racing between the snapshot and the delete is not deleted (correct — its claim is still active). The test verifies this precisely.
-
** RWMutex:** A new R/W mutex brackets registrations (R-locked for duration of each registration) and is write-locked while a finished batch's claims are released. This closes the window where a registration could read an input as unspent after the batch settled but before its projection landed. The naming is clear and the locking pattern is correct.
-
** → :** Generalises from waiting on a single batch id to waiting on all currently-tracked batches. A batch that doesn't land its projection within the timeout stays tracked (correct — the next call covers it). Previously a single missed projection could permanently drop tracking.
-
Remove : The field is removed from struct and test fixtures. The protection it provided is now handled by the RWMutex above.
Tests are comprehensive — the suite covers every path including the race-condition scenario and the 'service and admin service release identically' invariant test, which is exactly the right thing to pin.
One nit: in , logs rather than returning the error 'since the caller is always on a path that drops the intent regardless'. The comment is a multi-line block with an awkward line break ('since the caller\n// is always…'). Minor style issue only.
Overall: this is a careful correctness fix with excellent tests. Looks ready. Human sign-off required before merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha d860341 — PROTOCOL CRITICAL
DeleteIntents fix:
The old path called DeleteAll / Delete directly, which wiped the store without releasing conflict-domain claims. The new path: (1) snapshots intents via ViewAll, (2) releases their claims, (3) deletes exactly that snapshot. This closes the claim-leak on admin delete.
The comments in the code correctly identify the two failure modes of the old approach:
- An intent registered after the snapshot would have its claims freed but never be deleted.
- The selected-intent set read by
reconcilewould be dropped, leaking claims of round-in-progress intents.
New intent_claims.go:
releaseIntentClaimsis idempotent and owner-tagged — a release under the wrong id is a no-op, not a corruption.releaseClaimsOfIntentsis package-level so every claim-dropping path shares one implementation — drift between paths was the root cause of prior leaks.- Logging instead of returning errors on release failure is correct: the intent is being dropped anyway, a lost release is recoverable, and returning an error would block the delete.
Test coverage:
Tests exercise: correct owner tagging, id-based lookup, boarding intents (no vtxo inputs → no release), store-down path, and the round-start reconcile case. Good.
Looks ready to merge. ✅ (human sign-off for protocol-critical change)
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — arkade-os/arkd #1162 — live-store: owner-tagged atomic single-spend domain (sha d860341)
PROTOCOL-ADJACENT — intent claim management, liveness of vtxo spend paths — human review recommended.
Summary: The core change is a correctness fix in DeleteIntents and the centralisation of claim release. Looks ready to merge.
The bug being fixed:
The old DeleteAll path wiped the intent store, which could delete an intent registered after the snapshot and whose vtxo claims were never released. Those claims would then be permanently held, making the vtxos unspendable. The new path: snapshot → release claims by id → delete exactly the snapshot. A registration racing between snapshot and delete is left intact.
New intent_claims.go:
releaseClaimsOfIntentsis shared by bothserviceandadminService, eliminating the risk of the two paths drifting.releaseIntentClaimslogs rather than returns errors (correct — the caller is always dropping the intent anyway; a lost release is a stale claim, not a fatal error, and reporting it as fatal would be worse).
Tests are comprehensive: covers the main paths (by-id, by-proof, delete-all, delete-all keeps post-snapshot registration, error handling, cross-service consistency). The "service and admin service release identically" test is particularly valuable for catching future divergence.
Stale redis live store error handling: the diff shows liveStoreSvc, err = redislivestore.NewLiveStore(...) where the original ignored the error. Good fix.
No concerns. Looks ready.
|
Changes were requested 50+ days ago. @bitcoin-coder-bob need any help addressing the feedback on the single-spend domain? |
|
Changes were requested on this PR (live-store atomic single-spend domain) over 7 weeks ago with no update since the last push. @bitcoin-coder-bob — need any help addressing the feedback? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Live-store correctness fix for intent deletion.
What changed and why it matters
The old DeleteAll path wiped the store, which would:
- Drop intents registered after the snapshot was taken, leaving their vtxos claimed with no round to release them.
- Remove the selected-intent set that round-start reconcile reads, leaking claims from the running round.
The fix snapshots intents first, releases their claims (owner-tagged and idempotent), then deletes the exact snapshot by id. Any intent registered between snapshot and delete survives untouched.
Tests are excellent
TestIntentClaimRelease covers the full surface area: delete-by-proof, delete-all safety (intent registered after snapshot is kept), the service-vs-admin-service equivalence test, and the round-start selected-intent release. The equivalence test in particular is a good guard against the two paths drifting.
One comment on error handling
releaseIntentClaims logs rather than returning the error, with the rationale that a failed release leaves a stale claim (bad but recoverable) while turning it into a hard error would block intent deletion entirely (worse). That's a reasonable trade, but the comment should note what "stale claim" means for the operator — specifically that the vtxo stays locked until the claim TTL expires or the server restarts, whichever applies to the live-store backend.
Minor nit: intentsOf strips timing metadata to get to the domain type. It would be clearer as a method on TimedIntent rather than a package-level function, but that is a style preference.
Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — 2026-09-18
Summary
Fixes a claim-leak bug in DeleteIntents: the old code called Intents().DeleteAll() or Delete() without releasing the vtxo claims those intents held, leaving their inputs permanently claimed. The fix snapshots the intents first, calls releaseClaimsOfIntents, then deletes exactly that snapshot.
What looks good
- The fix correctly handles the race: intents registered after the snapshot are not in the delete list, so their claims are never released — they remain safe.
- The new intent_claims.go helper is package-level, shared by both the service and the admin service, with a test that verifies both paths produce identical release behaviour. This is exactly the right way to prevent drift.
- Extensive test coverage: boarding-only intents (no claims to release), lookup failure, round-start selected-intent release, delete-by-proof, and the critical service-vs-admin equivalence test.
- The ViewAll-then-Delete pattern is correctly atomic in spirit: if Delete fails, claims are already released (idempotent), which is the safe direction.
Minor
- The comment in admin.go explaining why DeleteAll was replaced (would silently drop running-round state) is excellent.
Looks ready to merge. The test that verifies service and admin service release identically is particularly valuable.
|
Changes were requested some time ago. @bitcoin-coder-bob need any help addressing the feedback? |
|
My review on 2026-09-18 noted this looks ready to merge. @bitcoin-coder-bob — is there anything blocking a final approval here? |
|
Changes were requested 2+ days ago with no code update. @bitcoin-coder-bob need any help addressing the feedback? |
|
Changes were requested 7+ weeks ago (opened 2026-07-27). @bitcoin-coder-bob need any help addressing the feedback? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — #1162 (sha d860341) — catchup pass
Prior reviews at ad348b9 and bf4c653 flagged the protocol-critical nature of this PR. This pass covers changes since bf4c653.
New in this diff
-
NewLiveStorereturns error —NewOffChainTxStorenow returns an error andNewLiveStorepropagates it. The caller inconfig.gohandles it correctly. This closes the gap where a store-init failure was silently swallowed. -
DeleteIntentssnapshot-then-release-then-delete — Replaces the old wipe-all path. The key invariant: the intent list is snapshotted first, claims are released under each intent's own owner-id, then only the snapshotted ids are deleted. A new intent registered after the snapshot isn't deleted and its claims aren't freed spuriously. ✅ -
intent_claims.go—releaseClaimsOfIntentsandreleaseIntentClaimsare correctly owner-tagged and idempotent. Moving this to a package-level helper ensures all intent-drop paths use the same release logic. ✅ -
e2e test — Destinations are now randomly generated, avoiding the same-txid-idempotent problem in the double-spend test. Good fix.
Previously raised issues — status
- Redis Lua atomic check-and-claim: addressed in prior commits.
offchainTxMuremoval: done.- The prior CHANGES_REQUESTED concerned issues in those earlier commits. Nothing in the current diff reintroduces them.
Remaining concern
Confirm that releaseClaimsOfIntents is also called from the round-start reconcile path when intents are evicted, not only from DeleteIntents. If a round ends and intents are pruned without going through DeleteIntents, claims could leak.
Part of the M3 (on-chain co-sign) milestone of #1159:
ClaimOutpoints/ReleaseOutpoints(atomic claim over the shared spent-input set).RegisterIntent, retiringoffchainTxMuentirely.What and why
The off-chain writer was non-atomic cross-process: redis
Adddid a separateSIsMemberthenSAddunder a per-process mutex, so on redis with >1 arkd an off-chain checkpoint spend and an on-chain cosign of the same vtxo could both succeed. Two hazards a naive fix would hit are handled below.outpoint -> owning arkTxid. inmemorymap[string]string; redisSET -> HASH(new key).Add,ClaimOutpoints) go through one owner-tagged compare-and-set (redis Lua / inmemory single lock): a different owner conflicts, the same owner is idempotent.Addreturns a tri-state (ClaimFresh/ClaimAlreadyOwned/ClaimConflict).Release/Removeare owner-scoped, so one tx cannot drop another's protection (proven: the owner-tagged claim forbids two owners holding the same outpoint, and the delete guards onvalue == owner).SubmitOffchainTxdeletesoffchainTxMuand its recheck loop, the atomic store is now the sole cross-process barrier.The two hazards, and how they're handled
spentVtxoKeysis onlyTxIn[0]of each checkpoint tx, butAddregisters everyTxIn. Preserved exactly, the claim keys off the checkpointTxInset (unchanged from the original loop), neverspentVtxoKeys, so it's a strict superset and nothing narrows. (Multi-input checkpoints are currently unreachable via the rebuild gate, but the code handles them by construction.)Addwould false-reject a legitimate same-arkTxid retry (async projection lag lets it re-reachAdd). The owner tag makes the same owner idempotent. And deleting the mutex would newly let a same-process concurrent same-txid submit append twoOffchainTxAcceptedevents, so onClaimAlreadyOwnedthe caller returns the accepted result without appending a second accept event, restoring the single-accept invariant.Why redis Lua scripts, and a request to reviewers
Please review the four Lua scripts in
internal/infrastructure/live-store/redis/offchain_txs.goline by line. I am not fluent in redis Lua scripting, I wrote these by reasoning from the redis docs and verified them against a live redis through the conformance suite, but I cannot vouch for them the way I can for the Go code. Treat them as the least-reviewed part of this PR.Why scripts rather than the
WATCH+TxPipelinedpattern the other redis stores use:Addhas to return fresh, already-owned or conflict at the same instant it registers the inputs. WithWATCHthe owners are read client-side, the decision is made, andEXECfails if anyone touched the key in between, so the loser retries, re-reads, and only then sees the conflict. A script decides and writes in one server-side step and answers every caller in one round trip.WATCHturns contention into retries, and the store's retry loops treat every error as retryable, so a conflict could burn the whole retry budget and surface as an internal error instead of a clean conflict. That is why the "exactly one of 64 concurrent claimers wins" test is a clean assertion here.confirmScriptinconfirmation_sessions.goalready uses a Lua script for the same reason, a conditional write that depends on a read.What the scripts cost, so reviewers can weigh it:
TestLiveStoreImplementations/redis/OffChainTxStorecovers it.addScriptandremoveScripttouch two keys with no hash tag, which Redis Cluster rejects. The oldWATCHacross the same two keys had the identical limitation, and the store uses a single-node client, so nothing regressed.claimScriptandaddScriptduplicate the two-pass conflict check. They could be one script with the tx body as an optional argument. Open to doing that if a reviewer prefers it.Things worth checking in the scripts specifically: that a missing hash field compares as Lua
falseand notnilin both the conflict pass and theanyNewpass, that a conflict returns before anyHSETso nothing is partially registered, that the already-owned path inaddScriptis the intended behaviour when it still overwrites the tx body, and that the{status, conflict}reply shape matches whatparseClaimResultexpects for all three outcomes.Claim lifecycle in
RegisterIntent(please scrutinise this)RegisterIntentpreviously checkedIncludesunderoffchainTxMu. It now claims its vtxo inputs throughClaimOutpoints, which is atomic across processes rather than within one, and reserves rather than only checking, so it also stops two intents registering the same vtxo.offchainTxMuis therefore gone.Reserving carries an obligation the read-only check did not: a claim never released leaves the vtxo permanently unspendable, and on redis that survives a restart. It fails silently, no error is raised anywhere, the vtxo simply stops being spendable. Every path that drops an intent releases: round start reconciles from the popped set (below), delete by intent proof, admin delete and delete-all (which deletes exactly the snapshot it released, by id, so an intent registered in between keeps its claim and the selected-intent set the reconcile reads is never wiped), plus a failed
Intents().Pushso a rejected registration leaves nothing behind. Intents still queued for a later round deliberately keep their claims.Round start releases the claims of every intent the last
Popselected that is no longer queued. A popped intent ends up registered on the round, re-pushed to the queue, or dropped (spent boarding input, liquidity abort, failed re-push), and only a re-pushed one is still queued under the same id. Reconciling from the popped set covers all of those in one place and mirrors howDeleteVtxosalready reconciles the intent vtxo index at the same point. Both backends persist the selected set, so it also covers a crash betweenPopand the round upsert on redis. An earlier revision released only the round's registered intents, which leaked the claims of dropped ones.One assumption holds this together, not enforced by the type system:
Popskips intents with no receivers, whichRegisterIntentcannot currently produce becauseIntent.validaterejects an empty output set. Making receivers optional (see the commented-outIntentStore.Update) would reintroduce the leak. The claim site carries this warning in a comment.For ops: upgrade, rollback, and what to watch
What changes in redis (inmemory backend: nothing, a restart clears it)
offChainTxStore:inputsByOwner, a HASH. Field is the outpoint astxid:vout, value is the owner: a 64-hex ark txid for an in-flight off-chain spend, or a UUID intent id for a queued intent.offChainTxStore:inputs(a SET) is no longer read or written. It has no TTL, so it lingers until deleted.offChainTxStore:txsis unchanged.DeleteIntents). They persist across restarts, exactly as the old SET did.offChainTxStore:txs. Each stored body's checkpoint inputs are re-registered under its ark txid withHSETNX, so an existing owner is never overwritten, and through a script that first checks the body still exists, so aRemoveby another instance in between cannot leave inputs registered that nothing could clear. This is the migration from the old SET, and it also restores what a rollback runbook deleted. It logsre-registered N spent input(s) from M stored offchain tx(s)at info level when it added anything. If it cannot complete, arkd refuses to start withfailed to init redis live store: failed to rebuild offchain tx inputs from stored txs, because a store serving after a partial rebuild would answer fresh for the inputs of every in-flight tx it did not reach. It has two minutes, and that cap is fatal rather than degrading: on a large backlog over a slow redis link the process exits instead of serving unprotected, which is deliberate but worth knowing on call. A stored tx whose checkpoint txs do not all parse is skipped as a whole with a warning, never registered in part.Upgrade
SubmitOffchainTxandRegisterIntentat the edge until the last old instance is gone. A tx an old instance accepts during the roll only reaches the new hash when a new instance next starts.SubmitOffchainTxuntil they settle. No action needed. Upgrading between rounds with an empty queue sidesteps this.DEL offChainTxStore:inputs. Cosmetic.Rollback
DEL offChainTxStore:inputsByOwnerwhile no new-version instance runs. Claims taken before the rollback, for txs and intents that then settled under the old version, would otherwise block those vtxos. The rebuild at the next start puts back the entries that belong to still in-flight txs, so the delete only sheds stale ones.What to watch
HLEN offChainTxStore:inputsByOwner. It should track the number of queued intent vtxo inputs plus in-flight off-chain spends. Growth that does not fall back after rounds complete means a claim leak.VTXO_ALREADY_SPENTfor a vtxo the DB shows unspent: message... already spentfrom the off-chain path,... is currently being spentfrom intent registration.failed to release conflict-domain claims of intent,failed to get selected intents to release their claims,failed to view intents ... to release their claims.INTERNAL_ERRORand nothing is accepted unprotected. The intent path logsfailed to claim intent inputs in the conflict domainat error level, the off-chain path has no log line. In practice this means redis is unreachable.Includescheck, so it needs the manual clear below. Same as with the old SET.Clearing a stuck vtxo
HGET offChainTxStore:txs <arkTxid>shows the in-flight tx. If the DB already has that tx accepted or finalized, or has no record of it at all, the entry is stale. Clear both:HDEL offChainTxStore:inputsByOwner <txid>:<vout>andHDEL offChainTxStore:txs <arkTxid>.ListIntentsshows whether it is still queued. If it is, adminDeleteIntentsreleases it properly. If it is not,HDEL offChainTxStore:inputsByOwner <txid>:<vout>.Merge ordering
Merge this one last of the three open #1159 PRs (#1160, #1161, #1162), timed with a deploy, since it carries the redis key change and the stop-all upgrade sequence in the ops section above. There is no code dependency on the other two and no conflict with either: #1161 shares no files with this PR, and #1160's
service.gohunks sit in a different part ofSubmitOffchainTx(a trial merge is clean). #1161 goes first because of its migration timestamp, see its description. If #1160 merges before this, merge master here again before reading CI.Test plan
go build ./...,make lint(0 issues), gofmt cleanTestLiveStoreImplementations) under-raceon both inmemory and redis:Addtri-state (fresh then already-owned), cross-domain conflict, owner-scoped release, all-or-nothing batch conflict, re-claim after release, exactly one of 64 concurrent distinct-owner claimers wins, 64 same-owner claimers never conflict. The redis Lua path runs all of these against a live redis, including the concurrency cases.internal/core/application+internal/interfacesuites green (accept path changed)RegisterIntentneeds a real vtxo input through full validation and is covered by review onlyTestOffChainTxStoreon a live redis: rebuilds from a body stored without an owner entry (then a different owner conflicts and the tx itself is idempotent), never overwrites an existing owner, skips a malformed body, no-op when empty, does not re-register the inputs of a body removed meanwhile, skips a tx with one malformed checkpoint tx as a whole, fails to construct when the rebuild cannot run, and runs a script at least once with zero retries. The first and third fail with the rebuild disabled, the removed-meanwhile case fails with the body check dropped from the script, the whole-tx skip fails with partial parsing restoredAddrejects a tx with a malformed checkpoint tx and stores nothing-raceon the full live-storeDraft
Foundational slice of the #1159 co-sign milestone; kept draft pending the rest of M3 (ark-lib builder, guarded cosign method that also checks
Intents().IncludesAny, anchor+CPFP broadcast) and team review of the rollout requirement.Summary by CodeRabbit
Bug Fixes
Reliability