MF2-M02: accept pre-finalize escrow version in ongoing check#765
Conversation
EnsureNoOngoingEscrowOperation treated escrow_deposit as ongoing whenever the on-chain escrow channel state version did not match the signed state version. When HandleEscrowDepositsPurged marks an escrow channel Closed without bumping StateVersion, the signed finalize state at version N+1 leaves the channel at version N, causing the function to incorrectly block receiver-side state issuance (transfer receive, app-session release). Accept escrow channel version N or N+1 vs signed state version for the escrow_deposit case. Purge queue makes the operation terminal, so waiting for the on-chain version bump is unnecessary. Compare via *v + 1 == StateVersion to avoid uint underflow when version is 0. Updated tests to reflect the relaxed gate and added regression coverage for the purge-close path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR refines the escrow-deposit finalization gate in ChangesEscrow Deposit Validation Refinement
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 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 |
nksazonov
left a comment
There was a problem hiding this comment.
Good job! The EnsureNoOngoingEscrowOperation fix correctly closes the blocking path described in the audit finding, and the new test cases cover both the purge scenario and the in-flight finalize case.
Out-of-diff note — secondary finding from the audit (not in this diff):
The audit finding also noted: "state_version returned by GetEscrowChannel() and GetChannels() will not be up to date with the finalize state while the escrow channel is closed." HandleEscrowDepositsPurged (event_handlers/service.go) still only sets channel.Status = ChannelStatusClosed without advancing StateVersion, and the doc comment confirms this is intentional. However, since API consumers querying a purged escrow channel via GetEscrowChannel or GetChannels will see state_version = N (initiate version) rather than the finalize version N+1, it would be worth adding a note to the handler's doc comment or the GetEscrowChannel response docs explaining that state_version on a purged escrow reflects the initiate state, not the finalize state. That way callers aren't silently surprised by the discrepancy.
GetEscrowChannel callers can see a Closed escrow channel whose state_version is the initiate version (N) rather than the finalize version (N+1) when the on-chain purge queue closed it without a signed FINALIZE_ESCROW_DEPOSIT. Document this on the nitronode handler, sdk/go and sdk/ts clients, and the get_escrow_channel entry in docs/api.yaml so consumers aren't surprised. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Yeah, fair point. Added a doc note on the nitronode |
ihsraham
left a comment
There was a problem hiding this comment.
Looks close, but I’d hold approval on one boundary in the escrow gate. The one-version-behind case needs to be tied to a terminal escrow channel.
| // behind (initiate state; finalize/purge has not landed yet, but is terminal | ||
| // per on-chain purge queue, so do not block receiver-side state issuance). | ||
| // Compare via *v + 1 == StateVersion to avoid uint underflow when version is 0. | ||
| if result.EscrowChannelVersion == nil || |
There was a problem hiding this comment.
[P1/blocking] This accepts the one-version-behind escrow_deposit case without checking the escrow channel is actually terminal. An active Open or Challenged escrow at version N with a signed finalize state at N+1 has the same shape as the purged case, so receiver-side issuance can proceed while the escrow finalization is still ongoing and then hide that escrow state behind a newer receiver state.
Could we include ec.status in this query and only allow the N+1 vs N case when the escrow channel is Closed? Exact version match can stay allowed, and tests should keep Open/Challenged one-behind cases blocking while Closed one-behind allows.
There was a problem hiding this comment.
Agree the shape-collision is real for one status, disagree on the fix.
Closed-only would block receiver-side issuance for ~3h after every escrow deposit, since the happy path is Open at N + signed N+1 sitting off-chain until the on-chain purge queue fires. Per the protocol, finalize is not enforced on-chain in the happy path — the co-signed N+1 is terminal off-chain, and HandleEscrowDepositsPurged deliberately preserves state_version at N.
The risky case is Challenged: on-chain dispute is live, node is racing to submit N+1 finalize on the escrow chain, and several failure modes (tx doesn't land in time, reorg, post-expiry replay against an INITIATE-settled escrow chain) can rewind real funds even though both parties co-signed N+1. Receiver-side state stacked on that baseline encumbers a credit that may not hold.
Pushed f7b4a59 — ec.status is now in the query and the one-behind branch allows {Open, Closed} and blocks Challenged (and any other status). Test coverage split accordingly.
`EnsureNoOngoingEscrowOperation` previously allowed any `escrow_deposit` signed state where the on-chain escrow channel version was exactly one behind, regardless of channel status. That matches the purged terminal case but also matches a `Challenged` escrow where the on-chain resolution is still racing — finalize tx may not land in time, the escrow chain may settle at INITIATE, and replaying N+1 later can violate engine invariants. Receiver-side state stacked on N+1 in that window encumbers a credit that may rewind. Gate the one-behind branch on escrow channel status: - Open → allow (protocol-intended steady state until purge) - Closed → allow (post-purge or post-finalize) - other → block (Challenged/Closing/Void) Exact-match still allows unconditionally. Tests split the one-behind coverage into Open/Closed/Challenged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ihsraham
left a comment
There was a problem hiding this comment.
Thanks, this addresses the escrow status boundary and the added coverage pins the intended Open/Closed vs Challenged behavior.
- MF2-I02: fix: add SESSION_KEY_AUTH_TYPEHASH to session key authorization payload (#767) - MF2-M02: accept pre-finalize escrow version in ongoing check (#765) - MF2-L03: docs: document home chain migration as not yet active off-chain (#769) - MF2-I03: reject reused home channel ID explicitly (#766) - MF2-H03: guard challenge_rescue against post-Finalize close (#768) - MF2-C02: bound ledger values to Solidity uint256/int256 ranges (#764) - MF2-C01: preserve post-Finalize closing marker across challenge cycle (#762) - MF2-M01: fix(sdk): use keccak256(utf8) for session key application ID hashing (#761) - MF2-H01: harden receiver-state issuance and dispute resolution (#759) - MF2-L01: gate transfer_send during channel creation (#763)
Summary
EnsureNoOngoingEscrowOperationnow status-gates the one-behind branch forTransitionTypeEscrowDeposit. The signed N+1 finalize state sits off-chain while the on-chain channel stays at INITIATE version N until the purge queue fires; that is the protocol-intended steady state, so we cannot requireClosed. The branch now allows status ∈ {Open, Closed} and blocks otherwise.HandleEscrowDepositsPurgedmarks the escrow channelClosedwhile preservingStateVersionat the initiate version, which previously causedEnsureNoOngoingEscrowOperationto reportescrow deposit finalization is still ongoingand block transfer receives and app-session releases.Challengedone-behind is now blocked. Co-signed N+1 is value-binding between parties, but on-chain resolution is still racing — finalize tx may not land, the escrow chain may settle at INITIATE, and replaying N+1 later could violate engine invariants. Stacking receiver-side state on N+1 in that window risks encumbering a credit that may rewind.*v + 1 == StateVersionto avoid uint underflow when version is 0.HandleEscrowDepositsPurgedorStateVersionhandling. Other transitions (escrow_lock, mutual_lock, escrow_withdraw) untouched.Test plan
go vet ./nitronode/store/database/...go test ./nitronode/store/database/... -count=1TestDBStore_EnsureNoOngoingEscrowOperationcases: