Due to the turn-based behaviour of a ForceMove state channel, the current wallet design must behave differently, depending on the order of the participants
property of the state channel.
Example
Suppose Alice and Bob want to open a channel C1
.
- They choose the ordering [Alice, Bob], and exchange pre-fund setups
- Now, they must decide whether to fund the channel with a direct deposit, indirectly through a ledger channel between [Alice, Bob], or virtually through an intermediary Henry the hub.
- Currently, Alice’s wallet is responsible for behaving like a “coordinator” for the funding process.
- Concurrently
a. Alice’s wallet asks Alice for her choice. Say she chooses virtual. Alice’s wallet tells Bob’s wallet that she chose virtual.
b. Bob’s wallet asks Bob for his choice. Say he chooses indirect. - Depending on whether a happens first, or b happens first, Bob’s wallet goes into either
a. Wait for Bob to choose.
b. Wait for Alice to tell Bob her choice, and then respond with “Reject – I wanted indirect”
Likewise, after 3a, Alice’s wallet goes into:- wait for Bob to respond with his choice, and then try to find a common hub
This works for two-party channels, and is manageable for three-party channels. That’s enough to virtually fund C
through a single hub. However, it’s very procedural, and doesn’t scale well: to fund C
through two intermediaries requires three 2-party guarantor channels, three 2-party ledger channels, two 3-party virtually funded channels, and one 4-party virtually-funded channel. Continuing with the above technique seems unmanageable. Furthermore, since each participant must wait for all prior participants, funding C
would take an unacceptable amount of wall time due to lag in communication.
Observations
While moving in-turn is important for safety in a general ForceMove channel, it’s not required in some specific rules
Example
Take the ConsensusApp, whose rules are currently here.
For those not familiar, the ConsensusApp simply changes the outcome of the channel once everyone has voted on that channel.
I’ve made a toy model of it here – watch the “STATES” tab as you click on the diagram. This implementation doesn’t take into account the fact that only participant turnNum % numParticipants
is allowed to take an action.
Turn order – filling the gaps
In Alice and Bob’s channel C
, even states are Alice’s turn, and odd states are Bob’s turn. Suppose they both have s5, s6, where
s6 = {
turnNumber: 6,
currentOutcome: '0xabc',
proposedOutcome: '0xabc',
votesRemaining: 0
}
Suppose Alice wants to propose a change to the outcome to 0x123
. Currently, she’d have to ask Bob’s wallet to move, and then propose 0x123
:
s8 = {
turnNumber 8,
currentOutcome: '0xabc',
proposedOutcome: '0x123',
votesRemaining: 1
}
Then, she’d wait for Bob to vote in s9
, which updates the outcome.
Until at least one of them has both s8
and s9
, the channel can only finalize in outcome 0xabc
, which they both agree to. In a world where they both agree to switching the outcome to 0x123
, they’re ok with either outcome until they both have s8
and s9
. At that point, 0xabc
is now a stale outcome, and they can forget about it.
However, it’s actually safe for Alice to simply sign
s8 = {
turnNumber 8,
currentOutcome: '0x123',
proposedOutcome: '0x123',
votesRemaining: 0
}
Note that s8
isn’t supported. Bob currently has sole control over using s8
in a support proof. He can do so in the following ways:
- Sign
s7 = { turnNumber: 7, currentOutcome: '0xabc', proposedOutcome: '0x123', votesRemaining: 1}
- Sign
s8
itself - Sign
s9 = { turnNumber: 9, currentOutcome: '0x123', proposedOutcome: '0x123', votesRemaining: 0}
- Sign
s9 = { turnNumber: 9, currentOutcome: '0xabc', proposedOutcome: '0x456', votesRemaining: 1}
In other words, Bob can only choose whether to update the outcome to 0x123
, or keep it at 0xabc
. He has no more or no less power than if they sign states in order.
Implications
The wallet standard can be designed around Alice’s ability to safely broadcast a state out of order, and have Alice and Bob’s wallets make exactly the same choices:
- If Alice wants to change the outcome, she can choose the most efficient path that updates it, sign her state in that path, and send it out.
- If Bob agrees, he can fill in the gaps (if needed), and the outcome has changed.
Valid transitions
Another observation is that it’s actually safe for Alice to simply sign
s7 = {
turnNumber: 7,
currentOutcome: `0123`,
proposedOutcome: `0x123`,
votesRemaining: 0
}
Note that s6 -> s7
is not a valid transition.
This is a bit subtle. Bob can either:
- Sign
s7
. This makes it supported - Sign a valid transition from
s6
s7_valid = {
turnNumber: 7,
currentOutcome: '0xabc',
proposedOutcome: '0x456',
votesRemaining: 1
}
- Sign a different state that’s not a valid transition from
s6
s7_invalid = {
turnNumber: 7,
currentOutcome: '0xabc',
proposedOutcome: EverythingGoesToBob,
votesRemaining: 1
}
In 1, Bob could sign s7
and send it to Alice, but then call forceMove([s6, s7_valid])
. However, since it’s Alice’s turn next, she can create a valid transition by signing
s8 = {
turnNumber: 7,
currentOutcome: `0123`,
proposedOutcome: EverythingGoesToAlice,
votesRemaining: 1
}
She can then call checkpoint([s7, s8])
.
2 is no different from Bob rejecting Alice’s proposal in the current wallet.
3 is always an option for a malicious wallet, but s7_invalid
will never have support without Alice’s vote, either on s7_invalid
, or a valid previous or next state.
Implications
Two correct wallets only ever need to sign states where votesRemaining: 0
.
The safety in this case seems to rely on there being just two participants. Is there a neat way to take advantage of a similar principle in an n
-party consensus channel? One way is for the participants to only ever sign states where turnNum % numParticipants == 0
. That way, once all participants sign such a state, it takes priority over any valid transitions from any previous states, removing Bob’s s7_valid
“attack”.