From be5b3f9176e02bf1ca680dd82a480edb30fcb1b2 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 8 Jun 2026 14:53:52 +0200 Subject: [PATCH 1/4] docs: document app session close atomicity blocking on in-flight escrow --- contracts/SECURITY.md | 24 +++++++++++++++++++++++ docs/protocol/security-and-limitations.md | 1 + protocol-description.md | 8 ++++++++ 3 files changed, 33 insertions(+) diff --git a/contracts/SECURITY.md b/contracts/SECURITY.md index fe8b92719..9cb62c3be 100644 --- a/contracts/SECURITY.md +++ b/contracts/SECURITY.md @@ -51,6 +51,30 @@ Invariant: Invariant: > Credits accrued while a channel was in `CHALLENGED` status are preserved — either reintegrated into the channel on challenge clearance, or carried into the next epoch on closure — and cannot be shadowed by a concurrently-opened replacement channel. +--- + +8. App session closure is **atomic across all participants**. The Node issues a new release receive-state on every participant's home channel as part of a single close transaction; a failure on any single participant aborts the entire close, leaving the session open and no release credits issued to anyone. + +The Node refuses to issue a release state when the recipient has an **in-flight escrow operation on the affected asset**, including: + +* an escrow lock or mutual lock that has not yet settled, +* an escrow deposit whose finalization has not yet been confirmed on-chain, +* an escrow withdrawal whose finalization has not yet been confirmed on-chain. + +The release receive-state is stacked on top of the recipient's most recent signed state. If that state encodes an unfinalized escrow, signing a release on top risks state-chain invariant violations should the escrow ultimately revert or settle to a different version than was assumed when the release was issued. The Node therefore blocks until the escrow resolves rather than risking a co-signed credit that cannot be enforced on-chain. + +Consequence: a single participant whose escrow has not yet completed — whether due to slow on-chain confirmation, an active challenge, or deliberate non-finalization — blocks the cooperative close for **all** other participants in the session. Remaining participants have two recovery paths: + +* wait for the blocking participant's escrow to resolve and retry the close, +* if the session's state machine permits intermediate updates, individually unwind their share via off-chain transfers out of the session and re-attempt closure with the remaining (non-blocked) members. + +This is an accepted trade-off for now, which may be lifted in the future: protocol safety (no release state co-signed against a potentially-reverting escrow chain) takes precedence over close-time liveness. Sessions whose participants require independent exit guarantees should be designed with state machines that support partial settlement rather than relying solely on cooperative close. + +Unlike the `CHALLENGED` channel path (rule 6), which queues incoming receive-states as unsigned entries so the channel's state history can carry the credit forward, the close path does not currently fall back to issuing an unsigned release. Extending that pattern here is a possible future improvement but is non-trivial: the safety of stacking an unsigned release on an unfinalized escrow depends on later reconciling the eventual escrow outcome with the queued credit, and the current implementation prefers refusal over partial trust. + +Invariant: +> A single participant with a pending escrow operation can block cooperative closure of an app session for all other participants until the escrow resolves; no participant receives a release credit while the close is blocked. + ## Invariants --- diff --git a/docs/protocol/security-and-limitations.md b/docs/protocol/security-and-limitations.md index 0bcb6ec3b..93c25009f 100644 --- a/docs/protocol/security-and-limitations.md +++ b/docs/protocol/security-and-limitations.md @@ -81,6 +81,7 @@ The following capabilities are not yet implemented or have acknowledged design t - Support for non-EVM blockchains - Formal verification of protocol rules - Session key off-chain scope enforcement does not apply to direct receive-state acknowledgement. Session key expiration and asset-scope restrictions are enforced by the Nitronode off-chain only; the `SessionKeyValidator` contract validates cryptographic signatures alone. A party holding a session key — even one that has expired, been revoked, or been retired — can bypass the `acknowledge` endpoint, manually sign a pending node-issued receive state, and submit it directly to the contract. This is accepted: receive states exclusively increase the user's allocation and cannot redirect funds away from the user, so out-of-scope acknowledgement carries no financial risk and preserves a recovery path when the node is unavailable. +- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant with an in-flight escrow operation on the affected asset (escrow lock, mutual lock, or escrow deposit/withdrawal awaiting on-chain finalization), because stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. See [`/contracts/SECURITY.md`](../../contracts/SECURITY.md) Behavior rule 8 for the full rationale. ## Future Improvements diff --git a/protocol-description.md b/protocol-description.md index 134b8ff26..d605ba2ca 100644 --- a/protocol-description.md +++ b/protocol-description.md @@ -154,6 +154,14 @@ These changes are reflected only in cumulative net flows until enforced on-chain --- +### App session closure and participant atomicity + +Closing an app session distributes locked allocations back to each participant by issuing a new release receive-state on every participant's home channel. The operation is **atomic across all participants** — if the Node cannot issue a release for any one participant, the entire close aborts. + +In particular, the Node refuses to issue a release to a recipient with an in-flight escrow operation on the affected asset (escrow lock, mutual lock, or escrow deposit/withdrawal awaiting on-chain finalization), so a single participant's pending escrow blocks cooperative closure for everyone else in the session until it resolves. See [`contracts/SECURITY.md`](contracts/SECURITY.md) for the full rationale and recovery paths. + +--- + ## On-chain protocol (enforcement plane) The on-chain contract is the **final arbiter** of correctness. From 3f7b89b696c9de334df6869220e5225a777635bd Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 12 Jun 2026 10:15:30 +0200 Subject: [PATCH 2/4] docs: refine app session close escrow gate description per review --- contracts/SECURITY.md | 9 ++++----- docs/protocol/security-and-limitations.md | 2 +- protocol-description.md | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/SECURITY.md b/contracts/SECURITY.md index 9cb62c3be..39b3bb13c 100644 --- a/contracts/SECURITY.md +++ b/contracts/SECURITY.md @@ -55,11 +55,10 @@ Invariant: 8. App session closure is **atomic across all participants**. The Node issues a new release receive-state on every participant's home channel as part of a single close transaction; a failure on any single participant aborts the entire close, leaving the session open and no release credits issued to anyone. -The Node refuses to issue a release state when the recipient has an **in-flight escrow operation on the affected asset**, including: +The Node refuses to issue a release state when the recipient's most recent signed state encodes an **escrow operation that `EnsureNoOngoingEscrowOperation` does not yet treat as safely settled**. In practice this covers: -* an escrow lock or mutual lock that has not yet settled, -* an escrow deposit whose finalization has not yet been confirmed on-chain, -* an escrow withdrawal whose finalization has not yet been confirmed on-chain. +* any pending `escrow_lock` or `mutual_lock` (always considered unresolved until superseded); +* `escrow_deposit` or `escrow_withdraw` whose on-chain escrow-channel version has not caught up with the signed state version — with a narrow one-version-behind allowance for `escrow_deposit` while the escrow channel is `Open` or `Closed` (the steady state during a normal finalize/purge cycle). The release receive-state is stacked on top of the recipient's most recent signed state. If that state encodes an unfinalized escrow, signing a release on top risks state-chain invariant violations should the escrow ultimately revert or settle to a different version than was assumed when the release was issued. The Node therefore blocks until the escrow resolves rather than risking a co-signed credit that cannot be enforced on-chain. @@ -70,7 +69,7 @@ Consequence: a single participant whose escrow has not yet completed — whether This is an accepted trade-off for now, which may be lifted in the future: protocol safety (no release state co-signed against a potentially-reverting escrow chain) takes precedence over close-time liveness. Sessions whose participants require independent exit guarantees should be designed with state machines that support partial settlement rather than relying solely on cooperative close. -Unlike the `CHALLENGED` channel path (rule 6), which queues incoming receive-states as unsigned entries so the channel's state history can carry the credit forward, the close path does not currently fall back to issuing an unsigned release. Extending that pattern here is a possible future improvement but is non-trivial: the safety of stacking an unsigned release on an unfinalized escrow depends on later reconciling the eventual escrow outcome with the queued credit, and the current implementation prefers refusal over partial trust. +Unlike the `CHALLENGED` channel path (rule 6) — where the release issuer **does** store unsigned releases for non-`Open` home channels so the rescue squash can pick them up at close — the close path does **not** extend that unsigned-fallback pattern to the escrow-rejected case. When `EnsureNoOngoingEscrowOperation` rejects a participant, the entire close aborts rather than queueing an unsigned release on top of an in-flight escrow. Extending the fallback here is a possible future improvement but is non-trivial: the safety of stacking an unsigned release on an unfinalized escrow depends on later reconciling the eventual escrow outcome with the queued credit, and the current implementation prefers refusal over partial trust. Invariant: > A single participant with a pending escrow operation can block cooperative closure of an app session for all other participants until the escrow resolves; no participant receives a release credit while the close is blocked. diff --git a/docs/protocol/security-and-limitations.md b/docs/protocol/security-and-limitations.md index 93c25009f..eb60b207c 100644 --- a/docs/protocol/security-and-limitations.md +++ b/docs/protocol/security-and-limitations.md @@ -81,7 +81,7 @@ The following capabilities are not yet implemented or have acknowledged design t - Support for non-EVM blockchains - Formal verification of protocol rules - Session key off-chain scope enforcement does not apply to direct receive-state acknowledgement. Session key expiration and asset-scope restrictions are enforced by the Nitronode off-chain only; the `SessionKeyValidator` contract validates cryptographic signatures alone. A party holding a session key — even one that has expired, been revoked, or been retired — can bypass the `acknowledge` endpoint, manually sign a pending node-issued receive state, and submit it directly to the contract. This is accepted: receive states exclusively increase the user's allocation and cannot redirect funds away from the user, so out-of-scope acknowledgement carries no financial risk and preserves a recovery path when the node is unavailable. -- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant with an in-flight escrow operation on the affected asset (escrow lock, mutual lock, or escrow deposit/withdrawal awaiting on-chain finalization), because stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. See [`/contracts/SECURITY.md`](../../contracts/SECURITY.md) Behavior rule 8 for the full rationale. +- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant whose latest signed state encodes an escrow operation that the off-chain gate does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit`/`escrow_withdraw` whose on-chain escrow-channel version has not caught up. Stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. See [`/contracts/SECURITY.md`](../../contracts/SECURITY.md) Behavior rule 8 for the full rationale. ## Future Improvements diff --git a/protocol-description.md b/protocol-description.md index d605ba2ca..de079f0a7 100644 --- a/protocol-description.md +++ b/protocol-description.md @@ -158,7 +158,7 @@ These changes are reflected only in cumulative net flows until enforced on-chain Closing an app session distributes locked allocations back to each participant by issuing a new release receive-state on every participant's home channel. The operation is **atomic across all participants** — if the Node cannot issue a release for any one participant, the entire close aborts. -In particular, the Node refuses to issue a release to a recipient with an in-flight escrow operation on the affected asset (escrow lock, mutual lock, or escrow deposit/withdrawal awaiting on-chain finalization), so a single participant's pending escrow blocks cooperative closure for everyone else in the session until it resolves. See [`contracts/SECURITY.md`](contracts/SECURITY.md) for the full rationale and recovery paths. +In particular, the Node refuses to issue a release to a recipient whose latest signed state encodes an escrow operation that the off-chain gate (`EnsureNoOngoingEscrowOperation`) does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit`/`escrow_withdraw` whose on-chain escrow-channel version has not caught up. A single such participant blocks cooperative closure for everyone else in the session until their escrow resolves. See [`contracts/SECURITY.md`](contracts/SECURITY.md) Behavior rule 8 for the full rationale and recovery paths. --- From 43b28ff41dc689b4b1b9fbec35a696052c754c55 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Fri, 12 Jun 2026 11:50:05 +0200 Subject: [PATCH 3/4] docs: drop SECURITY.md cross-reference from user-facing docs --- docs/protocol/security-and-limitations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/protocol/security-and-limitations.md b/docs/protocol/security-and-limitations.md index eb60b207c..1ede948ae 100644 --- a/docs/protocol/security-and-limitations.md +++ b/docs/protocol/security-and-limitations.md @@ -81,7 +81,7 @@ The following capabilities are not yet implemented or have acknowledged design t - Support for non-EVM blockchains - Formal verification of protocol rules - Session key off-chain scope enforcement does not apply to direct receive-state acknowledgement. Session key expiration and asset-scope restrictions are enforced by the Nitronode off-chain only; the `SessionKeyValidator` contract validates cryptographic signatures alone. A party holding a session key — even one that has expired, been revoked, or been retired — can bypass the `acknowledge` endpoint, manually sign a pending node-issued receive state, and submit it directly to the contract. This is accepted: receive states exclusively increase the user's allocation and cannot redirect funds away from the user, so out-of-scope acknowledgement carries no financial risk and preserves a recovery path when the node is unavailable. -- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant whose latest signed state encodes an escrow operation that the off-chain gate does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit`/`escrow_withdraw` whose on-chain escrow-channel version has not caught up. Stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. See [`/contracts/SECURITY.md`](../../contracts/SECURITY.md) Behavior rule 8 for the full rationale. +- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant whose latest signed state encodes an escrow operation that the off-chain gate does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit`/`escrow_withdraw` whose on-chain escrow-channel version has not caught up. Stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. This is an accepted trade-off favouring protocol safety over close-time liveness: every release the Node co-signs must remain enforceable on-chain, and an unfinalized escrow cannot offer that guarantee. ## Future Improvements From df445097561b2740502184a4675b5e392d2b4ef3 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 15 Jun 2026 10:20:44 +0200 Subject: [PATCH 4/4] docs: refine app session close escrow gate description per review --- docs/protocol/security-and-limitations.md | 2 +- protocol-description.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/protocol/security-and-limitations.md b/docs/protocol/security-and-limitations.md index 1ede948ae..2ff0c85f8 100644 --- a/docs/protocol/security-and-limitations.md +++ b/docs/protocol/security-and-limitations.md @@ -81,7 +81,7 @@ The following capabilities are not yet implemented or have acknowledged design t - Support for non-EVM blockchains - Formal verification of protocol rules - Session key off-chain scope enforcement does not apply to direct receive-state acknowledgement. Session key expiration and asset-scope restrictions are enforced by the Nitronode off-chain only; the `SessionKeyValidator` contract validates cryptographic signatures alone. A party holding a session key — even one that has expired, been revoked, or been retired — can bypass the `acknowledge` endpoint, manually sign a pending node-issued receive state, and submit it directly to the contract. This is accepted: receive states exclusively increase the user's allocation and cannot redirect funds away from the user, so out-of-scope acknowledgement carries no financial risk and preserves a recovery path when the node is unavailable. -- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant whose latest signed state encodes an escrow operation that the off-chain gate does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit`/`escrow_withdraw` whose on-chain escrow-channel version has not caught up. Stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. This is an accepted trade-off favouring protocol safety over close-time liveness: every release the Node co-signs must remain enforceable on-chain, and an unfinalized escrow cannot offer that guarantee. +- App session cooperative closure is atomic across all participants. The Node refuses to issue a release receive-state to any participant whose latest signed state encodes an escrow operation that the off-chain gate does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit` or `escrow_withdraw` states the gate still treats as unsafe (broadly, those whose on-chain escrow channel has not caught up, with a narrow one-version-behind allowance for `escrow_deposit` during normal finalize/purge transitions). Stacking a co-signed release on an unfinalized escrow risks state-chain invariant violations if the escrow ultimately reverts or settles to an unexpected version. As a consequence, a single participant with a pending escrow blocks cooperative close for all others in the session until their escrow resolves. Affected participants may wait for the obstruction to clear, or — where the session state machine permits intermediate updates — unwind their share individually via off-chain transfers out of the session and re-close without the blocked participant. This is an accepted trade-off favouring protocol safety over close-time liveness: every release the Node co-signs must remain enforceable on-chain, and an unfinalized escrow cannot offer that guarantee. ## Future Improvements diff --git a/protocol-description.md b/protocol-description.md index de079f0a7..524578b94 100644 --- a/protocol-description.md +++ b/protocol-description.md @@ -158,7 +158,7 @@ These changes are reflected only in cumulative net flows until enforced on-chain Closing an app session distributes locked allocations back to each participant by issuing a new release receive-state on every participant's home channel. The operation is **atomic across all participants** — if the Node cannot issue a release for any one participant, the entire close aborts. -In particular, the Node refuses to issue a release to a recipient whose latest signed state encodes an escrow operation that the off-chain gate (`EnsureNoOngoingEscrowOperation`) does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit`/`escrow_withdraw` whose on-chain escrow-channel version has not caught up. A single such participant blocks cooperative closure for everyone else in the session until their escrow resolves. See [`contracts/SECURITY.md`](contracts/SECURITY.md) Behavior rule 8 for the full rationale and recovery paths. +In particular, the Node refuses to issue a release to a recipient whose latest signed state encodes an escrow operation that the off-chain gate (`EnsureNoOngoingEscrowOperation`) does not yet treat as safely settled — covering any pending `escrow_lock`/`mutual_lock`, plus `escrow_deposit` or `escrow_withdraw` states the gate still treats as unsafe (broadly, those whose on-chain escrow channel has not caught up, with a narrow one-version-behind allowance for `escrow_deposit` during normal finalize/purge transitions). A single such participant blocks cooperative closure for everyone else in the session until their escrow resolves. See [`contracts/SECURITY.md`](contracts/SECURITY.md) Behavior rule 8 for the full rationale and recovery paths. ---