Execute state transitions off chain
A state channel can be thought of as an emergent property of data (which we call state
) exchanged between a fixed set of actors (which we call participants
). The participants have the ability to digitially sign the states that they exchange, and they also keep track of the underlying chainId
and channelNonce
to uniquely identify the interaction they are about to perform.
Construct a State with the correct format
@statechannels/nitro-protocol
exposes a State
type as a container for all the fields that are required:
Notice that the outcome field must conform to the Outcome
type, which we also imported from @statechannels/nitro-protocol
. The outcome is some data that specifies a redistribution of funds when the channel finalizes. Don't worry about this field just yet, we will revisit it later (we got away with an empty array, for now).
Fixed and Variable Parts
It is convenient to define some solidity structs, each containing a subset of the above data:
which contains information which cannot be changed for the lifetime of the channel (unless by unanimous consensus), and
which contains fields which are allowed to be changed by the current mover (see below). These structs are part of the contract API, and we can import helper functions to extract a javascript encoding of them:
validTransition
function
Conform to an on chain In ForceMove, every state has an associated 'mover' - the participant who had the unique ability to progress the channel at the point the state was created. The mover can be calculated from the turnNum
and the participants
as follows:
The implication of this formula is that participants take turns to update the state of the channel. Furthermore, there are strict rules about whether a state update is valid, based on the previous state that has been announced. Beyond conforming to the state format, there are certain relationships that must hold between the state in question, and the previously announced state.
The full rule set is (pseudo-code):
Application logic
Note the use of app.ValidTransition
. This function should be written by third party DApp developers. We provide a TrivialApp
contract which always returns true
, to aid in testing:
Setup states
If n
is the number of participants, then states with turn numbers 0
through n-1
(inclusive) are known as "pre fund setup" states. They signal each participant's intention to join the channel with a particular outcome specified.
Once a pre fund setup state with turn number n-1
is supported, it is safe for the channel to be funded.
States with turn numbers n
through 2n-1
(inclusive) are known as "post fund setup" states. They signal each participant's agreement that the channel is fully funded. Once a post fund setup state with turn number 2n-1
is supported, the state channel can begin execution in earnest (updating the appData
and outcome
).
Support a state in several different ways
Although you can check the validity of a state transition by providing the data above to an on-chain method, to cause any meaningful change in on-chain state (such as releasing funds), digitial signatures on those states are also required.
Nitro protocol uses the idea of supporting a state: in order for the chain to accept a channel state, s
, that channel state must be accompanied by a support proof: i.e. exactly n
signatures (where n = participants.length
). The simplest way to accomplish this is to provide a sequence of n
states terminating is state s
, where each state is signed by its mover and each consecutive pair of states form a valid transition.
There is also an optimization where a state can be supported by n
signatures on a sequence of m < n
states, provided again that each consecutive pair of those m
states form a valid transition and further provided each participant has provided a signature on a state later or equal in the sequence than the state for which they were the mover.
In the extreme, this allows a single state signed by all n
parties to be accepted by the chain.
note
In most cases where a support proof is required for some change of state of the chain, the entire proof is submitted with the blockchain transaction: no on-chain channel states are involved. The respond
method is an exception to this rule, and allows for the submission of only a single state in certain circumstances, with the support proof being implied by a combination of on-chain storage and submitted data.
tip
Nitro wallets need only store the "last" n
states, because they never need to submit more than n
states to the chain.
In the following diagram, A is participant 0, B is participant 1 and C is participant 2. The states are shown by circles and numbered 0, 1, and 2. We are considering whether state with turnNum = 2
is supported by various sets of signatures on the states in the sequence.
The yellow boxes show who signed what: in the first example, everyone signed their own state. This is acceptable:
Alternatively, A could sign a later state in the sequence:
or A, B and C could all sign the final state in the sequence:
The following signatures would not be acceptable:
This is because C signed a state earlier in the sequence than the one she is the mover for.
tip
Note that there is no need to submit a state if it is both not signed by anybody and is not preceeded by any signed states.
We provide a helper function to sign a State
:
it returns an object of the SignedState
type:
which we can make use of in the rest of the tutorial.
Alternatively you may use signStates(states, wallets, whoSignedWhat)
,
which accepts an array of States
, an array of ethers.js Wallets
and a whoSignedWhat
array of integers. The implicit definition of this last argument is as follows: For each participant, we are asserting that participant[i]
signed states[whoSignedWhat[i]]
:
info
Note that sigs
is an array of signatures for each participant, in participant order (e.g. [sig of participant[0], sig of participant[1], ...]
). The above call to signStates
returned an appropriate array because we passed in the wallets
in the correct, participant order.
validTransition
Shortcutting Notice that a state may be supported simply by having a full set of signatures (one from each participant). In such a case, there will be no validTransiiton
check made by the chain.
State updates can always be made via unanimous consensus.
This mechanism can be used to dynamically update the FixedPart.challengeDuration
and/or the FixedPart.appDefinition
.
The other members of FixedPart
cannot be changed, even by unanimous consensus, because that would imply a change in channelId
.