glow
glow copied to clipboard
[Design RFC] Support explicit timeouts
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. UsesConstraints.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 alsoTIMESTAMP
, 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.
- Supported in Ethereum via
- Consensus compilation:
- No changes?
- Consensus runtime:
-
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.
- Before interpreting codeblock deepclone current state
-
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 aftert
.
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.
In GitLab by @plotnick on Jul 4, 2021, 07:49
assigned to @kwanzknoel
In GitLab by @kwanzknoel on Sep 2, 2021, 23:46
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 22:55
changed title from Support explicit timeouts to Support explicit timeouts{+ (Design RFC)+}
In GitLab by @kwanzknoel on Sep 8, 2021, 22:55
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 22:56
changed title from Support explicit timeouts{- (Design RFC)-} to {+[Design RFC] +}Support explicit timeouts
In GitLab by @kwanzknoel on Sep 8, 2021, 22:56
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 22:59
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 23:00
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 23:00
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 23:03
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 23:07
changed the description
In GitLab by @kwanzknoel on Sep 8, 2021, 23:22
Nervos can have deadlines, but they are constrainted to only be one block.
In GitLab by @kwanzknoel on Sep 8, 2021, 23:23
Recent block can also allow this.
In GitLab by @kwanzknoel on Sep 8, 2021, 23:34
t would be supplied as an interaction parameter.
In GitLab by @kwanzknoel on Sep 8, 2021, 23:34
Consider duration (of block numbers) instead? Relative to when the last transaction ended?
In GitLab by @kwanzknoel on Sep 8, 2021, 23:36
Can we use @A
?
Nope, @A
suggests that this is private to A.
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.
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, thenow
is past thedeadline
, depending on the case asserted by the transaction), - (b) that the interval duration from
now
tomaxtimestamp
is small enough to use themaxtimestamp
as the basis for a newdeadline
(unless thedeadline
stays the same, in which case we don't care), - (c) that the new
deadline
(if there is a new one) is indeed based onmaxtimestamp + 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.