Validating Default Outcomes

Most generally, the outcome of a channel is some data that says what should happen outside the channel when it is closed. The most minimal, specific example would be an array of balances for the participants of that channel. There are a large number of steps in between (for example we at FunFair take part of the channel balance and pay to (channel-specific) third parties before the remaining funds are distributed).

In an ideal world, State Channels end by mutual consent; this means that the outcome of a channel can be “clean” - for example at the end of a game where there are no funds at stake.

Regardless of your approach to finalisability of a channel however, the core tenet of a Dispute(/Challenge) is that it can be made at any state transition, whether or not the State Machine(/app) thinks this is a good idea or not.

Consequently we need the concept of a default outcome for each state as the channel progresses; where you start from in the case of a dispute before any penalties are applied.

Add to this thought the concept of validating outcomes. YMMV depending on what the outcome actually specifies, but in the simple case of a list of balances it would seem self evident that the balances should always add up to the channel balance. You could apply rules where this isn’t necessarily the case (and I know Tom has some, although I don’t really understand what they are). However these are still rules.

I posit that the validity of the default outcome should be verified as part of each state transition. If a state machine “invents” funds during the course of a channel, then it’s going to cause problems when the participants try and close it. We do this religiously in our code.

This then asks the question as to where this validation should take place. The State Machine knows the individual participant balances but may not (and often doesn’t need to) know the channel balance. The State Channel(/Force Move) code might know the channel balance, but it might not if it’s being hooked into a separate ledger channel or a separate funding contract.

Finally, participants need to validate that state transitions from their counterparties are valid progressions of the state according to the protocol. I would suggest that this would ideally be able to be acheived in a single call() function. As I’ve been refactoring my code, this call has moved further and further out so it now sits on the top level. It’s in the code that knows the channel funding and associated rules. It calls the code that validates that the state transition is actually appropriate for this channel (ID’s, nonces, signatures etc), which in turn calls the State Machine to process the state transition logic. This returns a default outcome, which in turn is returned to the top level where it can be validated against the channel funding rules.

Does this make sense? I’ve spent that last 15 years writing code that processes virtual currency, and have a lifetime of paranoia about accidentally inventing or destroying it!


Some thoughts…

Outcome format. The format of an outcome is the encoding structure of the bytes data that represents the outcome. For example, the most basic format might be uint256[2] which encodes two integers.

Outcome interpreter. The interpreter of an outcome is a function interpret(bytes outcome) returns (KnownTransactionDataStruct) which converts some bytes outcome into a known data structure that encodes all of the necessary information for making an on-chain transaction (i.e., an “effect”). For example, interpret([1, 2]) may return something like [ { to: "alice", amt: 1 }, { to: "bob", amt: 2 }] which is what you need to know to send ETH on-chain.

In the most basic case, which is what I described in my example above, it’s quite easy to make a new function validate(bytes outcome) that would ensure that the sum of the outcome values sum(uint256[2] outcome) equates to some value. Then, you could add to the state format a new field called “ChannelBandwidth” that defines that sum, and validate would need to match this value.

Complications & Other Approaches

  1. Non-standard outcome types: It gets more complicated when you want to have more exotic types of outcome encodings. For example, in a game of Tic Tac Toe instead of encoding uint256[2] as the outcome type you might prefer enum { X_WIN, O_WIN, DRAW } as it is simpler logic for describing the application.

  2. Adapters: Another way of handling this is that you could define an “adapter” application which “wraps” the actual state machine of the application you are playing. For example, BalancesAdapterApp would be an app which converts enum { X_WIN, O_WIN, DRAW } to uint256[2] based on the aforementioned “ChannelBandwidth” value. Included in its validTransition rule would be the equality check.

Ta! Thoughts:

Is currentOutcome strictly necessary in the state format if it can be deterministically generated from appData?

I see where you’re going with the other two points. While that keeps generality (good), things could get pretty complicated with even a simple state machine that has multiple rounds. I don’t think I could encode the outcome of simplest of our games in a format like that.

Having said that a) we encode more detailed results in the appData structure for clients to interpret, and b) if you had sufficiently efficient virtual/sub-channels so that you could open a new channel off-chain for each game round then it could be an interesting approach. But it does mean that you have “app”-specific code wrapped around both sides of the actual channel code. If we want to go down that route, maybe it’s worth trying to strip out the “Force Move” stuff so that rather than it being at the very core of the process, it’s more on the side. Patrick was suggesting something like this a while ago. I thought it was an interesting idea but I didn’t really get much time to think about it…

I’ll start by defining a few terms, to make the post easier to follow:

  • allocation: an array of values that specify the amount allocated to each participant
  • allocation sum: the sum of the values in the allocation
  • channel balance: the amount held for the channel on-chain
  • payout: an array of values that specify the amount that each participant ultimately receives
  • payout sum: the sum of the values in the payout

My overall view is that, although most apps will want to enforce that the allocation sum remains the same throughout, we shouldn’t do this at the framework level.

The first thing to note that the thing the quantity of importance is the payout, and not the allocation. The payout is the quantity that the chain needs to calculate, and it’s also the quantity that the participants care about. At the framework level, we need to ensure that the payout sum never exceeds the channel balance.

The payout has to be some function of both the allocation and the channel balance. We can’t just take it to be the allocation itself. Why? Because we (the framework developers) can’t guarantee that a final state can be reached by a sequence of valid transitions from the initial state, as the participants are always free to collude to produce a state that doesn’t. Even if the transition rules state that the allocation sum must remain constant, we can’t avoid checks on the channel balance.

In ForceMove, we calculate the payouts by taking the allocation to be in priority order. For example, if allocation = { a: 5, b: 7 } and channel_balance = 9, then payout = { a: 5, b: 4} (payout first to a, then the remainder to b). One thing this allows is channel top-ups: in the previous example it is ‘safe’ for b to deposit (up to) an additional 3 coins into the channel, because by doing so, they increase their payout by the same amount.

As I mentioned above, most apps will want to enforce that the allocation sum remains constant throughout. These rules still protect participants against each other, even though they don’t protect the framework from the participants.