States & Channels
A state channel can be thought of as a private ledger containing balances and other arbitrary data housed in a data structure which we call a "state". The state of the channel is updated, committed to and exchanged between a fixed set of actors (which we call participants), together with some execution rules.
Info
In Nitro, participants "commit to" a state by digitially signing it.
A state channel controls funds which are locked up -- either on an L1 blockchain or on some other ledger such as another state channel.
State channel execution may always be disputed on-chain via a contract we call the Adjudicator, although this is not necessary (and is in fact a last resort). By emulating the on-chain dispute process, participants may understand which of several possible states is ultimately enforceable and therefore share an understanding about "the current state of the channel".
States
In Nitro protocol, a state is broken up into fixed and variable parts:
import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol';
struct FixedPart {
address[] participants;
uint48 channelNonce;
address appDefinition;
uint48 challengeDuration;
}
struct VariablePart {
Outcome.SingleAssetExit[] outcome; // (1)
bytes appData;
uint48 turnNum;
bool isFinal;
}
- This composite type is explained in the section on outcomes.
import * as ExitFormat from '@statechannels/exit-format';
// (1)
import {Address, Bytes, Bytes32, Uint256, Uint48, Uint64} from '@statechannels/nitro-protocol';
export interface FixedPart {
participants: Address[];
channelNonce: Uint64;
appDefinition: Address;
challengeDuration: Uint48;
}
export interface VariablePart {
outcome: ExitFormat.Exit; // (2)
appData: Bytes;
turnNum: Uint48;
isFinal: boolean;
}
Bytes32
,Bytes
,Address
,Uint256
,Uint64
are aliases to the Javascriptstring
type. They are respresented as hex strings.Uint48
is aliased to anumber
.- This composite type is explained in the section on outcomes.
import (
"github.com/statechannels/go-nitro/channel/state/outcome"
"github.com/statechannels/go-nitro/types" // (1)
)
type (
FixedPart struct {
Participants []types.Address
ChannelNonce uint64
AppDefinition types.Address
ChallengeDuration uint32
}
VariablePart struct {
AppData types.Bytes
Outcome outcome.Exit // (2)
TurnNum uint64
IsFinal bool
}
)
types.Address
is an alias to go-ethereum'scommon.Address
type.types.Bytes32
is an alias to go-ethereum'scommon.Hash
type.- This composite type is explained in the section on outcomes.
Info
States are usually submitted to the blockchain as a single fixed part and multiple (signed) variable parts. This is known as a "support proof".
Let's take each property in turn:
Fixed Part
Participants
This is a list of Ethereum addresses, each derived from an ECDSA private key in the usual manner. Each address represents a participant in the state channel who is able to commit to state updates and thereby cause the channel to finalize on chain.
Warning
Before joining a state channel, you (or your off-chain software) should check that it has length at least 2, but no more than 255, and include a public key (account) that you control. Each entry should be a nonzero ethereum address.
ChannelNonce
This is a unique number used to differentiate channels with an otherwise identical FixedPart
. For example, if the same participants want to run the same kind of channel as a previous channel, they can choose a new ChannelNonce
to prevent state updates for the original channel from being replayed on the new one.
Warning
You should never join a channel which re-uses a channel ID.
AppDefinition
This is an Ethereum address where a Nitro application has been deployed. This is a contract conforming to the ForceMoveApp
and defining application rules.
Warning
You should have confidence that the application is not malicious or suffering from security flaws. You should inspect the source code (which should be publically available and verifiable) or appeal to a trusted authority to do this.
ChallengeDuration
This is duration (in seconds) of the challenge-response window. If a challenge is raised on chain at time t
, the channel will finalize at t + ChallengeDuration
unless cleared by a subqsequent on-chain transaction.
Warning
This should be at least 1 block time (~15 seconds on mainnet) and less than 2^48-1
seconds. Whatever it is set to, the channel should be closed long before 2^48 - 1 - challengeDuration
. In practice we recommend somewhere between 5 minutes and 5 months.
Variable Part
Outcome
This describes how funds will be disbursed if the channel were to finalize in the current state. See the section on Outcomes.
AppData
The AppData is optional data which may be interpreted by the Nitro application and affect the execution rules of the channel -- see the section on application rules. For example, it could describe the state of a chess board or include the hash of a secret.
TurnNum
The turn number is the mechanism by which newer states take precedence over older ones. The turn number usually increments as the channel progresses.
Warning
The turn number must not exceed 281,474,976,710,655 because then it will overflow on chain. It should not exceed 4,294,967,295 because it may then overflow off-chain. It is very unlikely a channel would ever have this many updates.
IsFinal
This is a boolean flag which allows the channel execution rules to be bypassed and for the channel to be finalized "instantly" without waiting for the challenge-response window to lapse.
Warning
As soon as an isFinal=true
state is enabled (that is to say, you cannot prevent it from becoming supported) it is not safe to continue executing the state channel. It should be finalized immediately.
Channel IDs
Channels are identified by the hash of the FixedPart
of the state (those parts that may not vary):
bytes32 channelId = keccak256(
abi.encode(
fixedPart.participants,
fixedPart.channelNonce,
fixedPart.appDefinition,
fixedPart.challengeDuration
)
);
State commitments
To commit to a state, a hash is formed as follows:
bytes32 stateHash = keccak256(abi.encode(
channelId,
variablePart.appData,
variablePart.outcome,
variablePart.turnNum,
variablePart.isFinal
));
and this hash is signed using an dedicated Ethereum private key generated solely for the purpose of executing state channel(s) and not otherwise controlling funds on chain. The signature has the following type:
You can sign a state using these utilities:
SignedVariableParts
Signatures on a state hash by different participants are often bundled up with the variable part of the state when being submitted to chain.
Support proofs
A support proof is any bundle of information sufficient for the chain to verify that a given channel state is legitimate. They usually consist of FixedPart
, plus a singular SignedVariablePart
named candidate
, plus an array of SignedVariableParts
named proof
.
An basic example of a support proof would be a single state signed by every participant. For an intuition around more complicated support proofs, see Putting the 'state' in state channels.
RecoveredVariableParts
The adjudicator smart contract will recover the signer from each signature on a SignedVariablePart
, and convert the resulting list into a signedBy
bitmask indicating which participant has signed that particular state. The bitfield is bundled with the VariablePart
into a RecoveredVariablePart
:
```Go // INitroTypesRecoveredVariablePart is an auto generated low-level Go binding around an user-defined struct. type INitroTypesRecoveredVariablePart struct { VariablePart INitroTypesVariablePart SignedBy *big.Int }
```
before being passed to the application execution rules (which do not need to do any signature recovery of their own).
For example, if a channel has three participants and they all signed the state in question, we would have signedBy = 0b111 = 7
. If only participant with index 0
had signed, we would have signedBy = 0b001 = 1
.
```