glow icon indicating copy to clipboard operation
glow copied to clipboard

[Design RFC] Support explicit timeouts

Open kwannoel opened this issue 3 years ago • 18 comments

In GitLab by @plotnick on Jul 4, 2021, 07:49

Explicit Timeouts

  • Thanks to Alex K and Ian for providing ideas, considerations and implementations.
  • On second pass I do have new thoughts on parts of the previous discussion.

Timelines

Module 5 ready by 2nd december, 2021 for Glow MOOC.

Design constraints

How interoperable does the currentBlock command have to be? This will change how we implement it.

Should we make it EVM specific first?

  • Easier to implement, just one-chain, probably go this route if we need this feature within a short timeframe.
  • This could mean we have to deprecate it, if other chains we support do not provide the necessary primitives -> wasted engineering effort.
  • User attrition? If we have to replace fundamental concepts / primitives with more restrictive or complex ones, we probably will encounter this.

Consider all chains we are currently involved with?

  • Plutus: current_block is implicitly checked, but we can provide a range of blocks as buffer. Uses Constraints.mustValidateIn when creating the transaction. This checks the transaction against current time, to see if constraint is satisfied. This happens just before the validation script gets run. If this is not satisfied, the transaction falls through, the sender does not have to pay fees.

    Given the following boolean expression used as part of a validation script (in particular the auction script):

    correctBidSlotRange :: Bool
    correctBidSlotRange = to (aDeadline auction) `contains` txInfoValidRange info 
    

    This allows us to have both deadlines and timeouts. The client will be responsible for providing a sufficient buffer when constructing the transaction.

    Why was this needed? Plutus wants to provide a guarantee that if validation passes in wallet, it will pass on-chain.

  • Nervos: Users can proactively send a proof that a block was in the past. They can't say what time exactly it is within the lock script. This means we can have something like:

    assert-current-time-later-than(t)
    

    Because we can say that "t", the deadline was in the past.

    This would mean that for timeouts, users would need to be incentivized to call timeout to terminate the contract.

    NOTE: It could potentially lead to race conditions as well, if someone does not trigger the call in time.

  • EVM: We can provide the current_block with NUMBER instruction. There is also TIMESTAMP, but I'm not too sure if we should use that, it makes more sense to stick to an abstraction that applies to other chains, i.e. blocknumber.

First iteration

require! currentBlock() < expectedBlock;

Interoperability

  • Probably unable to implement this on Nervos.
  • Possible in plutus see the above comments.
  • Seems straightforward to implement on the consensus (EVM specific), which support some way to query block number on chain:
    • Consensus runtime:
      • Supported in Ethereum via NUMBER instruction: https://ethervm.io/#43
      • When used in a if/require block it can have same behaviour on-chain as normal require blocks.
    • Consensus compilation:
      • No changes?
  • Current time can be spoofed?

    If someone writes the following contract: t here would be not what we expect.

    @publicly!(A) let t = current_time();
    ... <interaction boundary> ...
    require! t < expirationTime;
    

    Noel: I can't think of a contract where such a pattern would be used. Is it overly contrived?

    How can we work around this?

    Are there other scenarios we did not consider?

Design

  • Intuitive way to represent timeouts.
  • This might be misleading / ambiguous: To a user, currentBlock might refer to the point at which they run their side of the interaction. On-chain, currentBlock refers to when the transaction gets into a block / confirmed.

    Allowing users to write things like could be dangerous, if they don't understand the above distinction well:

    require! currentBlock() == expectedBlock;
    

    Hence we probably want to only allow users to express the following for deadlines:

    require! currentBlock() < expectedBlock;
    

    Or:

    require! currentBlock() > expectedBlock;
    

    And we could go further to add the safeguard that both participant and on-chain refer to the same currentBlock(), treating the participant's notion of currentBlock as the source of truth. This would involve:

    • Before interpreting codeblock deepclone current state
      • Should be OK, we are already serializing no asymptotic difference.
      • Once we use persistent DS should be OK
    • We can then roll back^ once we fail tx.
    • If tx doesn't land in the block we want it to (or range of blocks), we need to retry.
  • Noel: Can we instead treat on-chain current-block as source of truth? If we make this explicit to our users, that when we talk about block number, we are talking about we are referring specifically to the block in which this transaction gets confirmed. This also means on participant runtime we have to watch for the time where tx gets confirmed.

    And mention that currentBlock refers to the block at which the transaction arrives on-chain?

Latest iteration

try A {
    // A does some stuff
} after (t) B {
    // B does some stuff ...
}

Note: each section of the try...catch above has to only be executed either by A or B. They should also be executed within one transaction (why?)

  • does not allow our users to maliciously / mistakenly spoof current_time().

  • This allows us to be explicit about which blocks get executed

  • Semantics of the above are like so: It matches on A, B and executes the respective blocks. For A, it simply executes the block as per usual. For B, it can only execute if current_block is after t.

What are the next steps?

  • Alex Knauth is sketching the EPP for the above, as part of the RPS interaction.

    #lang glow
    
    let winner = (handA : Nat, handB : Nat) : Nat => {
        (handA + (4 - handB)) % 3 };
    
    @interaction([A, B])
    let rockPaperScissors = (wagerAmount) => {
        @A let handA = input(Nat, "First player, pick your hand: 0 (Rock), 1 (Paper), 2 (Scissors)");
        @A require! (handA < 3);
        @A let salt = randomUInt256();
        @verifiably!(A) let commitment = digest(salt, handA);
        publish! A -> commitment; deposit! A -> wagerAmount;
    
        @B let handB = input(Nat, "Second player, pick your hand: 0 (Rock), 1 (Paper), 2 (Scissors)");
        publish! B -> handB; deposit! B -> wagerAmount;
        require! (handB < 3);
    
        try A {
            publish! A -> salt, handA;
            require! (handA < 3);
            verify! commitment;
            // outcome: 0 (B_Wins), 1 (Draw), 2 (A_Wins)
            let outcome = winner(handA, handB);
    
            switch (outcome) {
            // A_Wins
            | 2 => withdraw! A <- 2*wagerAmount
            // B_Wins
            | 0 => withdraw! B <- 2*wagerAmount
            // Draw
            | 1 => withdraw! A <- wagerAmount; withdraw! B <- wagerAmount };
    
            outcome
        } after (t) B {
            withdraw! B <- 2 * wagerAmount
        }
    };
    
  • Need input from @fahree @plotnick regarding design decisions and constraints.

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @plotnick on Jul 4, 2021, 07:49

assigned to @kwanzknoel

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 2, 2021, 23:46

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 22:55

changed title from Support explicit timeouts to Support explicit timeouts{+ (Design RFC)+}

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 22:55

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 22:56

changed title from Support explicit timeouts{- (Design RFC)-} to {+[Design RFC] +}Support explicit timeouts

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 22:56

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 22:59

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:00

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:00

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:03

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:07

changed the description

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:22

Nervos can have deadlines, but they are constrainted to only be one block.

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:23

Recent block can also allow this.

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:34

t would be supplied as an interaction parameter.

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:34

Consider duration (of block numbers) instead? Relative to when the last transaction ended?

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:36

Can we use @A? Nope, @A suggests that this is private to A.

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @kwanzknoel on Sep 8, 2021, 23:40

Q: assert_time_later_than(t)? Does not allow us to switch blocks based on timeouts.

kwannoel avatar Sep 22 '21 05:09 kwannoel

In GitLab by @fahree on Sep 9, 2021, 07:05

At a lower level, for Plutus and similar UTXO blockchains (Nervos?), the client would issue transactions that assert "valid in interval from now to maxtimestamp", where maxtimestamp is

  • (1) far enough in the future that we have solid chances of being included in the blockchain, yet
  • (2) less than the required deadline if any,
  • (3) not so far that the contract will refuse it (see below). (Also too long means next participant might then stall the interaction for a longer time than we want)

Then, the contract logic would use the now and maxtimestamp to check

  • (a) that said maxtimestamp is definitely within the allowed deadline (or, conversely, the now is past the deadline, depending on the case asserted by the transaction),
  • (b) that the interval duration from now to maxtimestamp is small enough to use the maxtimestamp as the basis for a new deadline (unless the deadline stays the same, in which case we don't care),
  • (c) that the new deadline (if there is a new one) is indeed based on maxtimestamp + deadline_duration

The client may have to watch the transaction, and issue a new one with new now/maxtimestamp interval if the previous one didn't make it. (In general, we'll have to watch transactions, anyway, to make sure they go through, in case gas fees increase past a level that make the previous attempt unlikely to succeed.)

At a higher level, in the Glow language, we should try to only expose deadlines without direct access to now. If access is needed for some contracts (e.g. to compute interests), then we should document just how imprecise the clock is on each supported network, in between the network's own notion of time and the need for intervals on some of them.

When using AVOUM, the miners could cooperate to take time into account when malleating transactions.

kwannoel avatar Sep 22 '21 05:09 kwannoel