Channel Lifecycle
State Transitions Overview
The lifecycle of a channel moves through well-defined states depending on how participants interact with the custody contract.
Use the sections below for details on each phase.
Creation Phase
Purpose: Initiate a new channel with specified participants and initial funding.
Process:
- The Creator:
- Constructs a Channel configuration with participants, adjudicator, challenge period, and nonce
- Prepares an initial State with application-specific app data
- Defines expected token deposits for all participants in
state.allocations - Signs the computed packedState of this initial state
- Includes Creator's signature in
state.sigsat position 0 - Calls either
create(...)ordepositAndCreate(...)function with the channel configuration and initial signed state
If the Creator obtains the second participant's signature on the initial state before calling create(), they can supply both signatures in state.sigs (positions 0 and 1). When the contract detects sigs.length == 2:
- It verifies both signatures
- Locks funds from both participants
- Transitions directly to
ACTIVEstatus (skippingINITIAL) - Emits both
JoinedandOpenedevents
This "implicit join" is the recommended approach for faster channel activation and reduced gas costs (single transaction instead of two).
- The contract:
- Verifies the Creator's signature on the funding packedState
- Verifies Creator has sufficient balance to fund their allocation
- Locks the Creator's funds according to the allocation
- Sets the channel status to
INITIAL - Emits a
Createdevent with channelId, channel configuration, and expected deposits
The first participant address is usually different from the caller (EOA or contract), thus enabling channel operation delegation. This can be fruitful as users can fund channels for other ones.
Joining Phase
There are two ways to open a channel:
- Modern/Recommended: Provide ALL signatures in
create()→ channel immediately ACTIVE (see Architecture) - Legacy/Manual: Provide only creator's signature in
create()→ status INITIAL → separatejoin()calls → ACTIVE
This section documents flow #2. Most implementations use flow #1.
Purpose: Allow other participants to join and fund the channel (when using separate join flow).
Process:
-
Each non-Creator participant:
- Verifies the channelId and expected allocations
- Signs the same funding packedState
- Calls the
joinfunction with channelId, their participant index, and signature
-
The contract:
- Verifies the participant's signature against the funding packedState
- Confirms the signer matches the expected participant at the given index
- Locks the participant's funds according to the allocation
- Tracks the actual deposit in the channel metadata
- Emits a
Joinedevent with channelId and participant index
-
When all participants have joined, the contract:
- Verifies that all expected deposits are fulfilled
- Sets the channel status to
ACTIVE - Emits an
Openedevent with channelId
The channel becomes operational only when ALL participants have successfully joined and funded their allocations.
Active Phase
Purpose: Enable off-chain state updates while channel is operational.
Off-Chain Updates
Participants:
- Exchange and sign state updates off-chain via the Nitro RPC protocol
- Maintain a record of the latest valid state
- Use application-specific data in the
state.datafield
Each new state:
- May update allocations when assets are transferred (though allocations can remain unchanged between states, e.g., game moves without fund transfers)
- MUST be signed by the necessary participants according to adjudicator rules
- MUST comply with the validation rules of the channel's adjudicator
The on-chain contract remains unchanged during the active phase unless participants choose to checkpoint a state.
During the active phase, state updates occur entirely off-chain with zero gas costs and sub-second latency.
Checkpointing
Purpose: Record a state on-chain without entering dispute mode.
Process:
-
Any participant:
- Calls the
checkpointfunction with a valid state and required proofs
- Calls the
-
The contract:
- Verifies the submitted state via the adjudicator
- If valid and more recent than the previously checkpointed state, stores it
- Emits a
Checkpointedevent with channelId
Checkpointing is optional but recommended for long-lived channels or after significant value transfers.
Closure - Cooperative
Purpose: Close channel to distribute locked funds, after all participants have agreed on the final state.
Process:
-
Any participant:
- Prepare a final State with
intentequal toFINALIZE. - Collects signatures from all participants on this final state
- Calls the
closefunction with channelId, final state, and any required proofs
- Prepare a final State with
-
The contract:
- Verifies all participant signatures on the closing packedState
- Verifies the state has
intentequal toFINALIZE. - Distributes funds according to the final state's allocations
- Sets the channel status to
FINAL - Deletes the channel metadata
- Emits a
Closedevent
This is the preferred closure method as it is fast and gas-efficient. It requires only one transaction and completes immediately without a challenge period.
Closure - Challenge-Response
Purpose: Handle closure when participants disagree or one party is unresponsive.
Challenge Process
- To initiate a challenge, a participant:
- Calls the
challengefunction with their latest valid state and required proofs
- Calls the
The participant's latest state may only exist off-chain and not be known on-chain yet. The challenge process brings this off-chain state on-chain for validation.
- The contract:
- Verifies the submitted state via the adjudicator
- If valid, stores the state and starts the challenge period
- Sets a challenge expiration timestamp (current time + challenge duration)
- Sets the channel status to
DISPUTE - Emits a
Challengedevent with channelId and expiration time
Resolving Challenge with Checkpoint
During the challenge period, any participant:
- Submits a more recent valid state by calling
checkpoint() - If the new state is valid and more recent (as determined by the adjudicator or IComparable interface), the contract updates the stored state, resets the challenge period, and returns the channel to
ACTIVEstatus
Challenge Period Elapse
After the challenge period expires, any participant:
- Call
closewith an empty candidate and proof to distribute funds according to the last valid challenged state
The contract:
- Verifies the challenge period has elapsed
- Distributes funds according to the challenged state's allocations
- Sets channel status to
FINAL - Deletes the channel metadata
- Emits a
Closedevent
The challenge mechanism gives parties time to prove they have a newer state. If no one responds with a newer state, the challenged state is assumed correct.
Complete Challenge-Response Flow:
Resize Protocol
Purpose: Adjust funds locked in the channel by locking or unlocking funds without closing the channel.
Process:
- Any participant:
- Calls the
resizefunction with:- The channelId (remains unchanged)
- A candidate State with:
intent=StateIntent.RESIZEversion= precedingState.version + 1data= ABI-encodedint256[]containing delta amounts (positive for deposit, negative for withdrawal) respectively for participantsallocations= Allocation[] after resize (absolute amounts)- Signatures from ALL participants (consensus required)
- An array of proof states containing the previous state (
version-1) first and its proof later in the array
- Calls the
The participant depositing must have at least the corresponding amount in their Custody ledger account (available balance) to lock additional funds to the channel.
-
The contract:
- Verifies the channel is in ACTIVE status
- Verifies all participants have signed the resize state
- Decodes delta amounts from
candidate.data - Validates adjudicator approves the preceding state
- For positive deltas: Locks additional funds from custody account
- For negative deltas: Unlocks funds back to custody account
- Updates expected deposits to match new allocations
- Emits
Resized(channelId, deltaAllocations)event
-
The channel:
- channelId remains UNCHANGED (same channel persists)
- Status remains ACTIVE throughout
- Version increments by 1
- No new channel is created
Use Cases:
- Increasing funds locked in the channel (positive delta: adding funds)
- Decreasing funds locked in the channel (negative delta: removing funds)
- Adjusting fund distribution while maintaining channel continuity
The resize operation updates the channel in place. The channelId stays the same, and the channel remains ACTIVE throughout. This differs from closing and reopening, which would create a new channel.
It is possible to combine a transfer (change of allocations among participants) with a resize operation. For example:
- Previous state allocations:
[5, 10] - Desired transfer: 2 tokens from second to first participant →
[7, 8] - Additional changes: first participant withdraws all 7, second participant deposits 6
- Delta amounts:
[-7, 6] - Resize state allocations:
[0, 14]
Rule: sum(allocations_resize_state) = sum(allocations_prev_state) + sum(delta_amounts)
For this example: 14 = 15 + (-1) ✓
State Transition Summary
The complete channel lifecycle state machine:
Valid Transitions:
| From | To | Trigger | Requirements |
|---|---|---|---|
| VOID | INITIAL | create() | Creator signature, sufficient balance, INITIALIZE intent |
| INITIAL | ACTIVE | join() | All participants joined and funded |
| ACTIVE | ACTIVE | checkpoint() | Valid newer state |
| ACTIVE | ACTIVE | resize() | All signatures, valid deltas, sufficient balance |
| ACTIVE | DISPUTE | challenge() | Valid state newer than latest known on-chain |
| ACTIVE | FINAL | close() | All signatures, FINALIZE intent |
| DISPUTE | ACTIVE | checkpoint() | Valid newer state |
| DISPUTE | FINAL | close() | Challenge period expired |
| FINAL | VOID | Automatic | Metadata deleted |
When a channel reaches FINAL status, the channel metadata is deleted from the chain and funds are distributed according to the final state allocations.