roller: add utilities for handling low-gas batches

This adds support for handling cases where the send-batch thread failed,
mainly among them, a thread crash. One of the events that causes this
behavior is a ver low gas price for this L1 transaction.

Here we add support for manually bumping the price for such transaction,
and for increasing the default fallback gas-price, together with discarding
any malformed batch from the sending queue.
This commit is contained in:
yosoyubik 2021-12-10 11:36:45 +01:00
parent c467d5aa67
commit f6ff4f108b
4 changed files with 157 additions and 53 deletions

View File

@ -31,7 +31,7 @@
:: ::
|% |%
+$ app-state +$ app-state
$: %3 $: %4
:: pending: the next l2 txs to be sent :: pending: the next l2 txs to be sent
:: sending: l2 txs awaiting l2 confirmation, ordered by nonce :: sending: l2 txs awaiting l2 confirmation, ordered by nonce
:: finding: sig+raw-tx hash reverse lookup for txs in sending map :: finding: sig+raw-tx hash reverse lookup for txs in sending map
@ -68,6 +68,7 @@
:: chain-id: mainnet, ropsten, local (https://chainid.network/) :: chain-id: mainnet, ropsten, local (https://chainid.network/)
:: resend-time: time to resend a batch with higher gas prie :: resend-time: time to resend a batch with higher gas prie
:: update-rate: frequency to update the roller's predicted state :: update-rate: frequency to update the roller's predicted state
:: fallback-gas-price: default batch gas price
:: ::
pk=@ pk=@
slice=@dr slice=@dr
@ -79,15 +80,11 @@
chain-id=@ chain-id=@
resend-time=@dr resend-time=@dr
update-rate=@dr update-rate=@dr
fallback-gas-price=@ud
== ==
:: ::
+$ action +$ action
$% :: submit: request to add a l2 tx to the pending queue $% :: submit: request to add a l2 tx to the pending queue
::
:: the address needs to be added so pending txs show up
:: in the tx history, but because users can send the wrong
:: address, in +apply-tx:predicted-state, we replace the
:: given address, with the one used when the message was signed
:: ::
[%submit force=? =address:naive sig=@ tx=part-tx] [%submit force=? =address:naive sig=@ tx=part-tx]
:: cancel: cancels a pending transaction :: cancel: cancels a pending transaction
@ -106,10 +103,12 @@
:: assign: assign an allowance to a ship for submitting l2 txs :: assign: assign an allowance to a ship for submitting l2 txs
:: ::
[%assign =ship quota=(unit @ud)] [%assign =ship quota=(unit @ud)]
:: refuel: bumps the next-gas-price of a sending tx
::
[%refuel nonce=@ address=(unit address:ethereum) gas=@ud]
== ==
:: ::
+$ card card:agent:gall +$ card card:agent:gall
::
++ lverb & ++ lverb &
-- --
:: Helpers :: Helpers
@ -231,13 +230,14 @@
:: ::
++ on-init ++ on-init
^- (quip card _this) ^- (quip card _this)
=: frequency ~h1 =: frequency ~h1
quota 25 quota 25
slice ~d7 slice ~d7
resend-time ~m5 resend-time ~m5
update-rate ~m5 update-rate ~m5
contract naive:local-contracts:azimuth contract naive:local-contracts:azimuth
chain-id chain-id:local-contracts:azimuth chain-id chain-id:local-contracts:azimuth
fallback-gas-price 10.000.000.000
== ==
=^ card-1 next-batch (set-roller:timer frequency now.bowl) =^ card-1 next-batch (set-roller:timer frequency now.bowl)
=^ card-2 next-slice (set-quota:timer slice now.bowl) =^ card-2 next-slice (set-quota:timer slice now.bowl)
@ -281,7 +281,7 @@
resend-time update-rate resend-time update-rate
== ==
=? old-state ?=(%2 -.old-state) =? old-state ?=(%2 -.old-state)
^- app-state ^- state-3
=, old-state =, old-state
=| allowances=(map ship (unit @ud)) =| allowances=(map ship (unit @ud))
=/ next-slice=time (mul +((div now.bowl slice)) slice) =/ next-slice=time (mul +((div now.bowl slice)) slice)
@ -293,10 +293,22 @@
frequency endpoint contract chain-id frequency endpoint contract chain-id
resend-time update-rate resend-time update-rate
== ==
?> ?=(%3 -.old-state) =? old-state ?=(%3 -.old-state)
^- app-state
=, old-state
=/ fallback-gas-price=@ud 10.000.000.000
:* %4
pending sending finding history
ship-quota allowances
next-nonce next-batch next-slice
pre own spo pk slice quota derive
frequency endpoint contract chain-id
resend-time update-rate fallback-gas-price
==
?> ?=(%4 -.old-state)
[cards this(state old-state)] [cards this(state old-state)]
:: ::
++ app-states $%(state-0 state-1 state-2 app-state) ++ app-states $%(state-0 state-1 state-2 state-3 app-state)
++ state-0 ++ state-0
$: %0 $: %0
pending=(list pend-tx) pending=(list pend-tx)
@ -361,6 +373,31 @@
resend-time=@dr resend-time=@dr
update-rate=@dr update-rate=@dr
== ==
++ state-3
$: %3
pending=(list pend-tx)
sending=(tree [l1-tx-pointer send-tx])
finding=(map keccak ?(%confirmed %failed [=time l1-tx-pointer]))
history=(map address:ethereum (tree hist-tx))
ship-quota=(map ship @ud)
allowances=(map ship (unit @ud))
next-nonce=(unit @ud)
next-batch=time
next-slice=time
pre=^state:naive
own=owners
spo=sponsors
pk=@
slice=@dr
quota=@ud
derive=?
frequency=@dr
endpoint=(unit @t)
contract=@ux
chain-id=@
resend-time=@dr
update-rate=@dr
==
-- --
:: ::
++ on-poke ++ on-poke
@ -561,7 +598,7 @@
=+ !<([=term =tang] q.cage.sign) =+ !<([=term =tang] q.cage.sign)
%- (slog leaf+"{(trip dap.bowl)} failed" leaf+<term> tang) %- (slog leaf+"{(trip dap.bowl)} failed" leaf+<term> tang)
=^ cards state =^ cards state
(on-batch-result:do address nonce %.n^[%error 'thread failed']) (on-batch-result:do address nonce %.n^[%crash 'thread failed'])
[cards this] [cards this]
:: ::
%thread-done %thread-done
@ -801,6 +838,7 @@
%commit ?>(local on-timer) %commit ?>(local on-timer)
%config ?>(local (on-config +.action)) %config ?>(local (on-config +.action))
%assign ?>(local `state(allowances (~(put by allowances) +.action))) %assign ?>(local `state(allowances (~(put by allowances) +.action)))
%refuel ?>(local (refuel-tx +.action))
%cancel (cancel-tx +.action) %cancel (cancel-tx +.action)
:: ::
%submit %submit
@ -839,6 +877,7 @@
^- (quip card _state) ^- (quip card _state)
?- -.config ?- -.config
%frequency [~ state(frequency frequency.config)] %frequency [~ state(frequency frequency.config)]
%fallback [~ state(fallback-gas-price gas.config)]
%resend-time [~ state(resend-time time.config)] %resend-time [~ state(resend-time time.config)]
%update-rate [~ state(update-rate rate.config)] %update-rate [~ state(update-rate rate.config)]
%slice [~ state(slice slice.config)] %slice [~ state(slice slice.config)]
@ -911,6 +950,21 @@
u.time u.time
[ship %cancelled keccak l2-tx] [ship %cancelled keccak l2-tx]
== ==
:: +refuel-tx: bumps the gas price for a sending tx
::
++ refuel-tx
|= [nonce=@ud address=(unit address:ethereum) gas=@ud]
^- (quip card _state)
=/ batch=[address:ethereum @ud]
:_ nonce
?^(address u.address (get-address pk.state))
=. sending
?~ send-tx=(get:ors:dice sending batch)
sending
%^ put:ors:dice sending
batch
u.send-tx(next-gas-price gas)
`state
:: +take-tx: accept submitted l2 tx into the :pending list :: +take-tx: accept submitted l2 tx into the :pending list
:: ::
++ take-tx ++ take-tx
@ -961,7 +1015,7 @@
:: when the thread that's sending the previous batch :: when the thread that's sending the previous batch
:: has come back and confirms that it was sent to L1 :: has come back and confirms that it was sent to L1
:: ::
?: out-of-sync ?: pending-batch
:: this would postpone sending the batch for a whole "frequency" :: this would postpone sending the batch for a whole "frequency"
:: TODO: set up a timer to retry this in ~mX ? :: TODO: set up a timer to retry this in ~mX ?
:: ::
@ -1022,9 +1076,22 @@
:: ship has been whitelisted ("?~ u.allow" means no quota restrictions) :: ship has been whitelisted ("?~ u.allow" means no quota restrictions)
:: ::
?~(u.allow | (gte u.quota u.u.allow)) ?~(u.allow | (gte u.quota u.u.allow))
:: +out-of-sync: checks if the previous nonce has been sent :: +pending-batch: checks if the previous nonce has been sent
:: ::
++ out-of-sync :: If %.y, the roller has been trying to send a batch for a whole frequency.
::
:: The cause of not sending the previous batch can happen because
:: of thread failure (see line 1251) or because the private key loaded onto
:: the roller was used for outside of the roller (i.e. for other than signing
:: L2 batches) right after the send-batch thread started.
::
:: After reaching this state, any subsequents attempts have failed (L: 1251)
:: (prior to updating the sending nonce if we hit the on-out-of-sync case)
:: which would possibly require a manual intervention (e.g. changing the
:: ethereum node URL, adding funds to the roller's address, manually bumping
:: the fall-back-gas-price or refueling the current batch with higher gas)
::
++ pending-batch
^- ? ^- ?
?~ newest-batch=(ram:ors:dice sending) | ?~ newest-batch=(ram:ors:dice sending) |
!=(sent.val.u.newest-batch &) !=(sent.val.u.newest-batch &)
@ -1032,8 +1099,10 @@
:: ::
++ on-out-of-sync ++ on-out-of-sync
|= [nonce=@ud failed-nonce=@ud] |= [nonce=@ud failed-nonce=@ud]
^- (quip card _state)
~& > %begin-on-out-of-sync
=/ =address:ethereum (get-address pk) =/ =address:ethereum (get-address pk)
:: we only care about nonces >= than the one that failed :: we only consider nonces >= than the one that failed
:: ::
=/ failed-sending=(list [l1-tx-pointer send-tx]) =/ failed-sending=(list [l1-tx-pointer send-tx])
%- tap:ors:dice %- tap:ors:dice
@ -1056,7 +1125,7 @@
=* txs txs.q =* txs txs.q
:: TODO: this shouldn't be needed :: TODO: this shouldn't be needed
?: (lth nonce.p failed-nonce) ?: (lth nonce.p failed-nonce)
~& ["weird case" nonce+nonce.p] ~& >>> [%on-out-of-sync nonce+nonce.p failed+failed-nonce]
[new-nonce sending finding history] [new-nonce sending finding history]
:+ +(new-nonce) :+ +(new-nonce)
fix-sending fix-sending
@ -1098,6 +1167,7 @@
history sih history sih
next-nonce `+(nonce) next-nonce `+(nonce)
== ==
~& > %end-on-out-of-sync
[(send-roll address nonce) state] [(send-roll address nonce) state]
:: +send-roll: start thread to submit roll from :sending to l1 :: +send-roll: start thread to submit roll from :sending to l1
:: ::
@ -1125,6 +1195,7 @@
chain-id chain-id
pk pk
nonce nonce
fallback-gas-price
:: ::
=< [next-gas-price txs] =< [next-gas-price txs]
(got:ors:dice sending [address nonce]) (got:ors:dice sending [address nonce])
@ -1143,38 +1214,54 @@
?. (has:ors:dice sending [address nonce]) ?. (has:ors:dice sending [address nonce])
~? lverb [dap.bowl %done-sending [address nonce]] ~? lverb [dap.bowl %done-sending [address nonce]]
`state `state
?: ?=([%| %not-sent %batch-parse-error] result)
:: if we tried to send a malformed batch, remove it from the queue
::
~& >>> [dap.bowl %removing-malformed-batch]
=^ * sending
(del:ors:dice sending [address nonce])
`state
=/ =send-tx (got:ors:dice sending [address nonce]) =/ =send-tx (got:ors:dice sending [address nonce])
=? sending ?=(%& -.result) =? sending ?| ?=(%& -.result)
?=([%| %crash *] result)
==
%^ put:ors:dice sending %^ put:ors:dice sending
[address nonce] [address nonce]
:: update gas price for this tx in state :: update gas price for this tx in state
:: and set it as sent to L1
:: ::
send-tx(next-gas-price p.result, sent &) ?: ?=(%& -.result)
send-tx(next-gas-price p.result, sent &)
:: if the thread crashed, we don't know the gas used,
:: so we udpate it manually, same as the thread would do
::
%_ send-tx
next-gas-price
?: =(0 next-gas-price.send-tx)
fallback-gas-price
(add next-gas-price.send-tx 5.000.000.000)
==
:_ state :_ state
?: ?| ?=(%& -.result) ?: ?& !sent.send-tx
:: a general error shouldn't innitiate ?=([%| %not-sent %behind-nonce] result)
:: the out-of-sync nonce thread ==
:: :: start out-of-sync flow if our L1 nonce is behind
?=([%| %error *] result) :: and this transaction hasn't been sent out yet
:: this accounts for a resend with higher gas
:: for a previous nonce, so we shouldn't start
:: the out-of-sync nonce thread
::
?& sent.send-tx
?=([%| %not-sent *] result)
== ==
:_ ~
:: resend the l1 tx in five minutes
:: ::
%+ wait:b:sys ~& > [dap.bowl %start-refresh-nonce-thread]
/resend/(scot %ux address)/(scot %ud nonce) (get-nonce pk.state /refresh-nonce/(scot %ud nonce))
(add resend-time now.bowl) :: resend the l1 tx in five minutes if:
:: TODO: this only accounts for the case where the nonce is out of sync,
:: reaching this because of lower funds needs to be addressed manually
:: ::
?> ?=(%not-sent -.p.result) :: - the thread succeeds and returns the next gas price
(get-nonce pk.state /refresh-nonce/(scot %ud nonce)) :: - the thread failed because:
:: - the roll's eth addres doesn't have enough funds to pay
:: - the thread crashes
:: - the sending L1 nonce is ahead of the expected one
:: - a general ethereum error
::
:_ ~
%+ wait:b:sys
/resend/(scot %ux address)/(scot %ud nonce)
(add resend-time now.bowl)
:: +on-naive-diff: process l2 tx confirmations :: +on-naive-diff: process l2 tx confirmations
:: ::
++ on-naive-diff ++ on-naive-diff

View File

@ -0,0 +1,5 @@
:: Bumps the gas price for a sending transaction
::
:- %say
|= [* [nonce=@ gas=@ud address=(unit @ux) ~] ~]
[%roller-action %refuel nonce address gas]

View File

@ -12,6 +12,7 @@
:: ::
+$ config +$ config
$% [%frequency frequency=@dr] $% [%frequency frequency=@dr]
[%fallback gas=@ud]
[%setkey pk=@] [%setkey pk=@]
[%endpoint endpoint=@t =net] [%endpoint endpoint=@t =net]
[%resend-time time=@dr] [%resend-time time=@dr]
@ -98,6 +99,7 @@
pk=@ pk=@
:: ::
nonce=@ud nonce=@ud
fallback-gas-price=@ud
next-gas-price=@ud next-gas-price=@ud
txs=(list raw-tx:naive) txs=(list raw-tx:naive)
== ==

View File

@ -9,7 +9,6 @@
=/ m (strand:strandio ,vase) =/ m (strand:strandio ,vase)
|^ |^
^- form:m ^- form:m
:: =* not-sent (pure:m !>(%.n^next-gas-price))
:: ::
=/ =address:ethereum (address-from-prv:key:ethereum pk) =/ =address:ethereum (address-from-prv:key:ethereum pk)
;< expected-nonce=@ud bind:m ;< expected-nonce=@ud bind:m
@ -17,8 +16,18 @@
:: if chain expects a different nonce, don't send this transaction :: if chain expects a different nonce, don't send this transaction
:: ::
?. =(nonce expected-nonce) ?. =(nonce expected-nonce)
~& [%unexpected-nonce nonce expected+expected-nonce] ~& >>> [%unexpected-nonce nonce expected+expected-nonce]
(pure:m !>(%.n^[%not-sent %unexpected-nonce])) %- pure:m
!> ^- [%.n @tas @t]
:+ %.n
%not-sent
?: (lth expected-nonce nonce)
:: if ahead, it will use the same next-gas-price when resending
::
%ahead-nonce
:: if behind, start out-of-sync flow
::
%behind-nonce
:: if a gas-price of 0 was specified, fetch the recommended one :: if a gas-price of 0 was specified, fetch the recommended one
:: ::
;< use-gas-price=@ud bind:m ;< use-gas-price=@ud bind:m
@ -72,7 +81,8 @@
:: log batch tx-hash to getTransactionReceipt(tx-hash) :: log batch tx-hash to getTransactionReceipt(tx-hash)
:: ::
~? &(?=(%result -.response) ?=(%s -.res.response)) ~? &(?=(%result -.response) ?=(%s -.res.response))
^-([nonce=@ud batch-hash=@t] nonce^(so:dejs:format res.response)) ^- [nonce=@ud batch-hash=@t gas=@ud]
nonce^(so:dejs:format res.response)^use-gas-price
%- pure:m %- pure:m
!> ^- (each @ud [term @t]) !> ^- (each @ud [term @t])
:: TODO: capture if the tx fails (e.g. Runtime Error: revert) :: TODO: capture if the tx fails (e.g. Runtime Error: revert)
@ -102,7 +112,7 @@
take-maybe-response:strandio take-maybe-response:strandio
=* fallback =* fallback
~& >> %fallback-gas-price ~& >> %fallback-gas-price
(pure:m 10.000.000.000) (pure:m fallback-gas-price)
?. ?& ?=([~ %finished *] rep) ?. ?& ?=([~ %finished *] rep)
?=(^ full-file.u.rep) ?=(^ full-file.u.rep)
== ==