Pure functions vs. in wallet code

Hi all,
I have some questions relating to off-chain interactions and validity checks. I’m not sure if I missed some things and therefore have a wrong view - please feel free to correct me.

A participant in a channel has two basic situations
a) receiving a state from a predecessor
b) proposing a new state (doing the move)

Aspect 1: keeping track of validity of off-chain transitions
When in an off-chain progressing situation, assuming the last supported state relates to a valid transition from the on-chain state, the participant in situation a) would be required to check if all moves from the last supported state are valid transitions.

This is generally necessary, as a predecessor could make a mistake (or try an attack), providing an invalid transition. In off-chain mode, the participants could progress based on that (invalid) state, finally (after one or several ‘complete’ rounds), believe they have reached a finalizable state, which however is not accepted by the on-chain executed forceMove transition rules (and/or the thereof called transition rules of the dApp contract).

This is not the case for ledger channels, as there the transitions are individually checked on-chain an denied if invalid. However, in off-chain mode, every participant is required to ensure complete integrity of the transition sequence for the running ‘round’.
Is this view correct?

If so, this would require the participant to get all transitions from the last supported (and assumed to be valid) state to his move (and also the rest).
He would finally need to check if the resulting supported state of this round relates to a valid transition from the current on-chain state. If so, he can neglect the detailed transition chain (and all older supported states).

Practical approach 1) call the pure function validTransition of the forceMove contract for all the transitions (further discussed below)

Aspect 2: making a valid move in a complex dApp
Suppose a) is done, now b) means making a valid move in a complex dApp, which means determine the correct appData and possibly updated outcome.

Practical approach2)
We could have the dApp logic not only within the transition rules, but encoded as additional pure functions in the dApp contract, which allow to do a move and returning the updated appData and outcome.
This is different from validTransition, as there the user provides two such datasets. However in complex applications, calculating the dataset for a desired move might be complex. These extra functions however bloat the contract.

Discussion
Both, approaches 1) and 2) are not really off-chain in the sense that a connection to the chain is required to be able to call the pure functions.

Another approach would be to ‘mirror’ the on-chain contract logic on the participant side (e.g. within the wallet), such that the validity check and calculation of transitions can be executed locally by the participant himself.
This however renders the wallet complex and dApp specific.

A third approach would be to have a local VM in which a copy of the foceMove contract and the dApp contract could be executed.

Another aspect I don’t get so far: with counterfactual approach, we should be able to have participants sign a contract, only uploading it on chain in case of dispute.
How can the foreMove’s pure function validTransition, which depends on the dApps.valid transition be executed?

You see - I’m a bit confused…

Alex

So, a few statements to clear things up:

  1. In the case of a counterfactually instantiated contract whose address is determined via CREATE2, it is true that you need to deploy this contract before any adjudicator methods can be called. If it is not deployed, adjudicator methods will revert.

  2. We prefer the approach of having a local VM for validating state transitions. Armani Ferrante has created pure-evm for this purpose in Node and browser contexts.

  3. I also personally advocate for developers to write their applications with convenient pure functions which can be used to compute states that are valid transitions and writing their validTransition method to use these functions. Here is some example pseudo code:

contract Counter is ForceMoveApp, Utils {
  function validTransition(State prev, State next)
    pure returns (boolean)
  {
    State computed = Utils.compute(
      prev, next.actionApplied
    );
    return computed == next;
  }
}

contract Utils {
  function compute(State state, Action action)
    pure returns (State)
  {
    if (action.type == ActionTypes.ADD) {
      return addOne(state);
    }
    if (action.type == ActionTypes.SUBTRACT) {
      return subtractOne(state);
    }
    revert('Bad Action');
  }
  function addOne(State state) {
    return State(state.counter + 1);
  }
  function subtractOne(State state) {
    return State(state.counter - 1);
  }
}

You can see above what is happening is that the validTransition function essentially dispatches the work to a redux / state + action style interface which then provides public facing methods for updating state based on some possible actions.

Personally I find this way of writing the smart contracts much cleaner and easier to reason about. This way, the frontend / dapp can simply take some supported state that it understands to be one he can provide a state with a higher turn number on, and compute the appData via a local execution of EVM code, for example by running addOne.

NOTE: I glossed over some implementation details here such as the decoding and encoding of the State and Action structs, but I hope the general approach is clear. I may add an e2e tested example app at some point for this.

Hi Liam,
thanks for your answer.
I agree with your view on how the contract should be designed.
The glossed-over part is well covered in the nitro tutorial, so I should be able to get it all together.

Regarding counterfactual and create2: yes this is what I would try to achieve (have a virtual channel with counterfactual dApp contract, and keep as off-chain as possible).

I was using find and grep to if there is a code (e.g. in rps or xwallet), actually using create2.
I found
node_modules/ethers/utils/address.js:160:exports.getCreate2Address
but not where it is used (probably my fault)
The same for pure-evm;

So the question is:
Is there some code actually using counterdfactual dApp approach in an off-chian channel, possibly also using pure-evm?

Sorry for this kind of question; I’m doing my best to use find and browsing the code; but the topic is quite complex with lots of bits and pieces, which makes it hard to get the head around it all…

Alex

There isn’t presently an example of a 100% counterfactually instantiated application contract mostly because in practice there isn’t that much of an efficiency gain for simple applications. The efficiency really comes from making significant modifications to the application rules during the existance of the channel, which we don’t have a good example for yet. Usually you can write a sufficiently abstract application definition where the rules don’t need to change during the channels existence.

Let me be clear why it is not practically useful now for, say, Rock Paper Scissors. You only need a single on-chain contract on ethereum for all games of RPS. This means anyone can deploy RockPaperScissors.sol one time and all state channel users can reuse this contract. All that must be done is the appDefinition must be set to the deployed address.

So, you could avoid this step by pre-computing the address should it be deployed via CREATE2 but it’s just not that practically beneficial given that it only saves you a single on-chain transaction for all users, all time.

OK. So let’s say you want this small benefit anyway. How do you do it?

You need to know the following which CREATE2 is based on:

  • The address of the contract that would call create2
  • The initcode for the contract (bytecode + constructor arguments)
  • The salt that would be used

The best strategy then is to deploy some kind of factory contract that does one thing: it creates new contracts using create2 with a supplied initcode and salt. This way you know all three things. Something like this pseudocode (again note I gloss over some minor stuff like supplying a nonce and wei value):

contract FactoryWithCreate2 {
  function deploySomething(bytes initcode, uint256 salt) returns (address) {
    assembly {
      return create2(initcode, salt);
    }
  } 
}

With the existence of this contract on-chain, you know that any call to deploySomething with initcode and salt will have the address computed by the CREATE2 formula. So this means your appDefinition supplied to a channel could be this value, even if the contract didn’t exist on chain. However, as mentioned in my previous reply, adjudicator methods would revert until someone called deploySomething with the correct initcode and salt.

I also just noticed OpenZeppelin has some good tooling around this, see: Deploying Smart Contracts Using <code>CREATE2</code> - OpenZeppelin Docs

Hi Liam,
thank you for your answer; again, highly appreciated.

One aspect related to initial question still puzzles me, It’s about supported states and what needs to be stored by the agent in order to be able to force it later on on-chain.

The tutorial states (in my words)
supported state: has signatures of the n participants (or a valid chain of transitions, signed accordingly).
I will use {i} so signalize its a supported state and {i}* its on chain.

Suppose we have a 3 participant channel, with participants A,B,C and a supported state i on chain:
Now the participants can make their move (_A means A has signed):

{i}* ->(i+1)_A → (i+2)_B → (i+3)_C which gives us a supported state, written short as {j}

in short we have
{i}* ->{j}
and could proceed in the same way to end up in a later supported state
{i}* → {j} → {k}

1. Aspect’s questions
As far as I have understood , the participants could now keep the information related to most current supportet state {k}, and neglect {j}. Is this correct?

I.e.: can we have an on-chain transition {i}* → {k}* by just providing {k}?
Or do we need to keep all intermediary states?

As far as I have seen from the forceMove code, as long as its not related to challenges and finalized=True states, everything signed correctly is a validTransition IF app.validTransition is true.

This means that one needs to be careful with respect to app.validTransition to not produce situations where a valid chain of transitions leads to a supported state (e.g. {k}) which however ist not a valid direct transition from the latest on chain state {i}* - right?

Otherwise the participants would need to keep a long sequence of transitions in long running of-chain channels (or on-chain checkpoint frequently).

Allowing for not storing all transition, bu only the newest supported could mean that we need 2 alternative branches of validTransition checks in the app code.

IF transition from one state to the next (in the sense of usual mover transitions, so to say atomic transition): apply the game logic to check if e.g. the change in outcome is OK

ELSE IF we have a transition from one supported state {} to another, later (larger turn number)
return true if the second state is really a supported state. Do not check game logic, as we only have a resulting outcome and no detailed individual moves. But this is ok, as every participant signed that state.

Or am I wrong on all of this?

Sorry for the many weird questions…

Alex

P.S.: I decided to move the 2nd aspect to an extra topic, to keep things a bit more manageable

Yes, in this case you could use checkpoint, but I’m not sure what the motivation would be if all three parties are co-operating.

It is the responsibility of the wallet of each participant to ensure it only signs states which it identifies as being a part of a valid transition chain. Usually each participant would simply sign a single state for its turn and decide to sign it based on it being a valid transition from the previously supported state. You’re right that a wallet should not usually sign things which do not have valid transitions, unless it has some special reason to do so.

The general rule of thumb with state channels is that no matter what the chain says, if everyone agrees on something then they can overrule any past history and ignore anything on-chain.

Yes, in case they are cooperating, they could just continue. My question was regarding the situation they cooperated from {i}* → {j} → {k}, but then one participant defects.
In this case another participant might want to bring {k} on chain (->{k}*) in order to be able to de-fund (without the need to first bring {j} on chain).

As far as I have understood the code, this requires

  1. all necessary signatures for k (any participant has them)
  2. the transition (on-chain) from latest known on chain state {i}* to what we try to bring on-chain needs to be a valid transition (otherwise the adjudicator would deny)

Regarding 2., the forceMove validTransition check would return True for the case above.
However, the app.validTransition could (if not done correct) return False.

OK, so following this mindset, the app.validTransition could/should be coded such, that even if e.g. the outcome update from state a->b does not fulfill the game logic, but b has all participants signatures, it should return True.

One could have than additional functions which allow any participant to check (before signing) if a proposed transition is valid with respect to the game logic.

The adjudicator’s general purpose validTransition function does this check. Basically, if everyone agrees on some {k} where the outcome is in a state that is impossible for the app logic to arrive at, it can still be deemed OK by the adjudicator since everyone signed off on it.

Sorry, but I don’t see this.
The adjudicator in NitroAdjudicator.sol call _requireValidTransition, which is in ForceMove.sol line 492.
In there, however I find:

function _requireValidTransition( ... require( ForceMoveApp(appDefinition).validTransition( ab[0], ab[1], turnNumB, nParticipants )

If I now have a look in the game logic of e.g. RPS.sol, I see that e.g. a transition from start can only progress to RoundProposed.

So if the two RPS players had 1 round on-chain {1}* , followed by 10 rounds off-chain → {11} and the last transition was from reveal to start.
Now e.g. player B goes offline. A wants to de-fund. The transition from {1}*->{11} is (in my opinion) rated as invalid by the rps.validTransition.

How would A defund without having to provide all atomic transitions to {11}?

The keyword was “everyone” in what I just mentioned. See here:

If everyone has signed something with turnNum k then _requireValidTransition is not run.

In this example, since it is a two player game, if {10} was signed by B then A can challenge B on-chain by calling forceMove with {10} and {11}. If, {11} was isFinal then he would call conclude.

Thanks @liam @tom and @george for your patience and great support, I think now I got it.
Sometimes one is too blind to see the obvious things. I had read [1] but did overlook the respective part in the contracts.

So in my words:
We have forceMove (or e.g. checkpoint) checking the ‘support proof’ (i.e. a sufficient combination of signed states as discussed [1]) right at the beginning via
supportedStateHash = _requireStateSupportedBy

We therefore need to provide an appropriate sequence of states & signatures, and the transitions are checked for validity within this sequence and not in relation to what has been on-chain so far (the latter was my misinterpretation and lead to all the fuzz, sorry for that)

=> the individual participats only need to keep the input for the most recent support proof and can neglect all the rest

Sorry again for the fuzz!

Alex

1 Like