Skip to content

MF2-M02: accept pre-finalize escrow version in ongoing check#765

Merged
philanton merged 3 commits into
fix/audit-findings-finalx2from
fix/nitronode-mf2-m02
May 19, 2026
Merged

MF2-M02: accept pre-finalize escrow version in ongoing check#765
philanton merged 3 commits into
fix/audit-findings-finalx2from
fix/nitronode-mf2-m02

Conversation

@philanton
Copy link
Copy Markdown
Contributor

@philanton philanton commented May 18, 2026

Summary

  • EnsureNoOngoingEscrowOperation now status-gates the one-behind branch for TransitionTypeEscrowDeposit. 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 require Closed. The branch now allows status ∈ {Open, Closed} and blocks otherwise.
  • Fixes the stall reported in MF2-M02: HandleEscrowDepositsPurged marks the escrow channel Closed while preserving StateVersion at the initiate version, which previously caused EnsureNoOngoingEscrowOperation to report escrow deposit finalization is still ongoing and block transfer receives and app-session releases.
  • Addresses #discussion_r3264742604: Challenged one-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.
  • Comparison uses *v + 1 == StateVersion to avoid uint underflow when version is 0.
  • No change to HandleEscrowDepositsPurged or StateVersion handling. Other transitions (escrow_lock, mutual_lock, escrow_withdraw) untouched.

Test plan

  • go vet ./nitronode/store/database/...
  • go test ./nitronode/store/database/... -count=1
  • TestDBStore_EnsureNoOngoingEscrowOperation cases:
    • chain caught up → allow
    • Open one-behind (pre-purge happy path) → allow
    • Closed one-behind (post-purge) → allow
    • Challenged one-behind → block
    • chain more than one version behind → block
    • escrow channel missing from DB → block

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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

The PR refines the escrow-deposit finalization gate in EnsureNoOngoingEscrowOperation to accept on-chain state exactly one version behind the signed state (in addition to exact equality), clarifies this behavior in documentation, and expands test coverage to validate the "one behind" allow case, "more than one behind" block case, and purge-close scenario.

Changes

Escrow Deposit Validation Refinement

Layer / File(s) Summary
Escrow Deposit Version Rule and Tests
nitronode/store/database/db_store.go, nitronode/store/database/db_store_test.go
Updated the escrow_deposit settlement rule documentation to specify exact-match and one-behind cases are both settled; modified the validation logic to check *EscrowChannelVersion + 1 == StateVersion with nil guards; and added test cases covering the one-behind allow scenario, multi-version-behind block scenario, and purge-close closed-channel scenario.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • layer-3/nitrolite#735: Introduces and uses EnsureNoOngoingEscrowOperation method that this PR refines to unblock receiver operations.

Suggested reviewers

  • ihsraham
  • nksazonov
  • dimast-x

Poem

🐇 A version behind is not too far,
When purge queues waltz where channel states are,
The deposit settles in one neat hop,
No waiting for exact—the chain can stop! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: modifying EnsureNoOngoingEscrowOperation to accept an earlier escrow version (pre-finalize) in the ongoing check, which is the primary purpose of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nitronode-mf2-m02

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@philanton philanton changed the base branch from main to fix/audit-findings-finalx2 May 18, 2026 12:44
Copy link
Copy Markdown
Contributor

@nksazonov nksazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@philanton
Copy link
Copy Markdown
Contributor Author

Yeah, fair point. Added a doc note on the nitronode GetEscrowChannel handler, the Go and TS SDK client methods, and the get_escrow_channel entry in docs/api.yaml calling out that state_version stays at the initiate version (N) on a purge-closed escrow. In 40caa39.

Copy link
Copy Markdown
Collaborator

@ihsraham ihsraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread nitronode/store/database/db_store.go Outdated
// 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 ||
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 f7b4a59ec.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>
Copy link
Copy Markdown
Collaborator

@ihsraham ihsraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this addresses the escrow status boundary and the added coverage pins the intended Open/Closed vs Challenged behavior.

@philanton philanton merged commit 38cc057 into fix/audit-findings-finalx2 May 19, 2026
7 checks passed
@philanton philanton deleted the fix/nitronode-mf2-m02 branch May 19, 2026 09:24
nksazonov added a commit that referenced this pull request May 19, 2026
- 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants