Coq model of reserve balance and proof of safety
- *Consensus* computes a **worst-case, fee-only budget** (the “effective
reserve”) for the yet-to-be-executed suffix and only proposes blocks when
that budget stays non-negative for every **sender** that appears later.
- *Execution* runs the actual EVM step and enforces that **no account dips below its protected reserve slice** (with a carefully fenced “emptying” exception). If a transaction would violate that, it is reverted in place.
Whether an address is currently delegated in the core state.
Definition addrDelegated (s: StateOfAccounts) (a : EvmAddr) : bool :=
match delegatedTo (s a) with
| None ⇒ false
| Some _ ⇒ true
end.
match delegatedTo (s a) with
| None ⇒ false
| Some _ ⇒ true
end.
Many of the EVM semantics definitions we use come from Yoichi's EVM semantics, developed several years ago. The definition of block.transaction there lacks fields to support newer features like delegation. Also, the last field is to support user-configurable reserve balances in Monad. There is a new transaction type which can update the configured reserve balance of the sender. Such transactions do nothing else.
The fields above should ultimately come from EVM semantics and not here. The fields below are monad-specific.
^ updates the reserve balance of the sender if Some. In that case, the transaction does nothing else, e.g., no smart contract invocation or transfer. It must be None for regular transactions (which do not reconfigure the reserve balance). option N in Coq can be thought of as std::optional<N> in C++. None in Coq corresponds to std::nullopt in C++.
Transactions to uodate reserve balance will be implemented as a call to a special precompile:
A stateful precompile at address 0xRESERVES maintains a mapping from account addresses to their configured
reserve balance values. The precompile exposes an update(uint256) function that allows accounts to set
their reserve balance. Any internal call (from a smart contract) to the update function of 0xRESERVES causes
the transaction to revert. Only EOA accounts may call the precompile’s update function directly.
A transaction t is a reconfiguring transaction if it calls the update(uint256) function of the 0xRESERVES
precompile (inspectable by the to and calldata field). A reconfiguring transaction is required to have value
field set as 0.
Behavior of the precompile: When a reconfiguring transaction is executed, the precompile only updates
the reserve balance mapping for t.sender to the specified value. The precompile performs no other operations:
no balance transfers, no state changes beyond updating the single mapping entry. As a result, a reconfiguring
transaction never reverts. This simplicity is assumed for proving the correctness of the reserve balance
mechanism.
}.
The type A × B in Coq can be thought of as std::pair<A,B>. The projections .1 and .2 can be used to obtain the first and second components of the pair, respectively.
Our **fee upper bound** is intentionally pessimistic: the consensus rule
reasons about gas_limit × gas_price, not about *actual* gas used, which can be hard to efficiently estimate.
This excludes storage fees, which are are separately handled below because storage fees are only accounted by consensus for transactions that are allowed to empty.
The proofs in this file never unfold the definition of maxTxFee, so nothing will break if this definition is changed.
Definition maxTxFee (t: TxWithHdr) : N :=
((w256_to_N (block.tr_gas_price t.2)) × (w256_to_N (block.tr_gas_limit t.2))).
((w256_to_N (block.tr_gas_price t.2)) × (w256_to_N (block.tr_gas_limit t.2))).
max storage fee. the exact definition does not matter for our proofs, so we keep it undefined, so our proofs would have to work for all possible definitions. one can choose the body as 0 to effectively disable storage fees
for any decidable assertion/proposition P, asbool P is a boolean such that (asbool P = true) ↔ P, where
↔ means iff (if and only if)
Parameterization by the lookahead window K:
Consensus can be ahead of execution by at most K. The n+Kth block must have the state root hash after executing block n. The next two variables are parameters for the rest of the proofs.
Next, we have some simple wrappers for brevity.
sender of a transaction:
value transfer field of a of a transaction
addresses delegated or undelegated by tx
block number of a transaction
returns None if this tx is not a reserve-balance-reconfig tx. Else it returns Some newRb, where newRb will become the new reserve balance threshold of the sender if/when t is executed.
NEW: We assume there is some mechanism by which a user can specify that they intend a transaction to be emptying. We leave it undefined, so our proofs work for any possible way to define it.
But note that the definition can only inspect the transaction, e.g. it has no access to history or the current state of accounts. An ideal way to define it would be to have a boolean field in the transaction header. The user needs to understand that if they chose to set this field (thus request to be allowed to empty), consensus will exclude their transactions for the next K blocks.
A suboptimal way may be to define it as whether the sender has requested themself to be undelegated in this transaction, but this may have unintended effects if the user did not really mean this tx to be emptying, as consensus will exclude transactions from this sender for the next K blocks
To implement reserve balance checks, execution needs to maintain some extra state (beyond the core EVM state) for each account:
^ last block index where this account sent a transaction. In the implementation, we can just track the last 2K range, e.g. this can be None if the last tx was more than 2K block before. we do not need to store this information in the db as it can be easily computed
^ last block index where this address was delegated or undelegated. In the implementation, we can just track the last 2K range.
^ the current configured reserve balance of the account. will either be DefaultReserveBal or something else if the account sent a transaction where reserveBalUpdate is not None
NEW: ^ record the last block index where a transaction with an explicit emptying request was sent
Our modified execution function which does reserve balance checks will use the following type as input/output.
just an abstract helper used in the next two definitions, which choose different instantiations for the proj argument
Definition indexWithinK (proj: ExtraAcState → option N) (state : ExtraAcStates) (tx: TxWithHdr) : bool :=
let startIndex := txBlockNum tx - (K-1) in
match proj (state (sender tx)) with
| Some index ⇒
asbool (startIndex ≤ index ≤ txBlockNum tx)
| None ⇒ false
end.
let startIndex := txBlockNum tx - (K-1) in
match proj (state (sender tx)) with
| Some index ⇒
asbool (startIndex ≤ index ≤ txBlockNum tx)
| None ⇒ false
end.
Definition existsTxWithinK (state : AugmentedState) (tx: TxWithHdr) : bool :=
indexWithinK lastTxInBlockIndex (state.2) tx.
indexWithinK lastTxInBlockIndex (state.2) tx.
returns true if the account sender tx was delegated/undelegated within the last K blocks of txBlockNum tx
Definition existsDelUndelTxWithinK (state : AugmentedState) (tx: TxWithHdr) : bool :=
indexWithinK lastDelUndelInBlockIndex (state.2) tx.
Definition existsEmptyReqWithinK (state : AugmentedState) (tx: TxWithHdr) : bool :=
indexWithinK lastEmptyReqBlockId (state.2) tx.
indexWithinK lastDelUndelInBlockIndex (state.2) tx.
Definition existsEmptyReqWithinK (state : AugmentedState) (tx: TxWithHdr) : bool :=
indexWithinK lastEmptyReqBlockId (state.2) tx.
is the account a smart contract: must have code that is not a delegation marker
update the value of the key updKey in oldMap to f (oldMap updKey) (f applied to the old value of the key updKey in the map)
Definition updateKey {T} `{c: EqDecision T} {V} (oldmap: T → V) (updKey: T) (f: V → V) : T → V :=
fun k ⇒ if (asbool (k=updKey)) then f (oldmap updKey) else oldmap k.
Lemma updateKeyLkp3 {T} `{c: EqDecision T} {V} (m: T → V) (a b: T) (f: V → V) :
(updateKey m a f) b = if (asbool (b=a)) then (f (m a)) else m b.
fun k ⇒ if (asbool (k=updKey)) then f (oldmap updKey) else oldmap k.
Lemma updateKeyLkp3 {T} `{c: EqDecision T} {V} (m: T → V) (a b: T) (f: V → V) :
(updateKey m a f) b = if (asbool (b=a)) then (f (m a)) else m b.
Consensus Check (algo 1)
Below, we build up the definition of the consensus check consensusAcceptableTxs The “emptying” gate:- it is *not* currently delegated,
- it had no delegation change within the last K blocks (last K-1 blocks and the prev transactions in the current block),
- it did not send a transaction in the last K blocks.
Definition isAllowedToEmpty
(preIntermediatesState : AugmentedState) (intermediates: list TxWithHdr) (candidateTx: TxWithHdr) : bool :=
let consideredDelegated :=
(addrDelegated (preIntermediatesState.1) (sender candidateTx))
|| existsDelUndelTxWithinK preIntermediatesState candidateTx
|| asbool ((sender candidateTx) ∈ flat_map addrsDelUndelByTx (candidateTx::intermediates)) in
let existsSameSenderTxInWindow :=
(existsTxWithinK preIntermediatesState candidateTx)
|| asbool ((sender candidateTx) ∈ map sender intermediates) in
(negb consideredDelegated) && (negb existsSameSenderTxInWindow).
(preIntermediatesState : AugmentedState) (intermediates: list TxWithHdr) (candidateTx: TxWithHdr) : bool :=
let consideredDelegated :=
(addrDelegated (preIntermediatesState.1) (sender candidateTx))
|| existsDelUndelTxWithinK preIntermediatesState candidateTx
|| asbool ((sender candidateTx) ∈ flat_map addrsDelUndelByTx (candidateTx::intermediates)) in
let existsSameSenderTxInWindow :=
(existsTxWithinK preIntermediatesState candidateTx)
|| asbool ((sender candidateTx) ∈ map sender intermediates) in
(negb consideredDelegated) && (negb existsSameSenderTxInWindow).
The effective reserve map:
Consensus reasons about *how much of the protected (reserve) slice of the balance
can be consumed* by
the yet-to-run suffix. This is the “effective reserve” map:
Notice the type is Z: negative entries encode an *over-consumption* that
should make the proposal unacceptable.
Z is the type of mathematical numbers in Coq: unbounded, exact arithmetic (no over/under flows). At the end of this file, we implement in Coq the algorithm using just U256, while still avoiding over/underflows and thus prove equivalence
- It is initialized with min(balance, userConfiguredReserve).
- Every transaction removes *at most* its fee from the sender’s entry, except for the “emptying” hole, where the pessimistic removal accounts for value + fee in one shot.
some more wrappers:
Definition configuredReserveBalOfAddr (s: ExtraAcStates) (addr : EvmAddr) : N := configuredReserveBal (s addr).
balance of an account in a given state:
update balance of the account addr such that the new value is upd applied to the old value
Definition updateBalanceOfAc (s: StateOfAccounts) (addr: EvmAddr) (upd: N → N) : StateOfAccounts :=
updateKey s addr (fun old ⇒ old &: _balance %= upd).
updateKey s addr (fun old ⇒ old &: _balance %= upd).
balance of an account in a given augmented state
initial effective reserve balances, before any proposed tx
Definition initialEffReserveBals (s: AugmentedState) : EffReserveBals :=
fun addr ⇒ (balanceOfAc s.1 addr `min` configuredReserveBalOfAddr s.2 addr).
Definition emptyRequestors (txs: list TxWithHdr) : list EvmAddr :=
flat_map (fun tx ⇒ if reqAllowToEmpty tx then [sender tx] else []) txs.
Definition senderinForbiddenWindow preIntermediatesState intermediates candidateTx : bool :=
(existsEmptyReqWithinK preIntermediatesState candidateTx ||
asbool ((sender candidateTx) ∈ emptyRequestors intermediates)).
fun addr ⇒ (balanceOfAc s.1 addr `min` configuredReserveBalOfAddr s.2 addr).
Definition emptyRequestors (txs: list TxWithHdr) : list EvmAddr :=
flat_map (fun tx ⇒ if reqAllowToEmpty tx then [sender tx] else []) txs.
Definition senderinForbiddenWindow preIntermediatesState intermediates candidateTx : bool :=
(existsEmptyReqWithinK preIntermediatesState candidateTx ||
asbool ((sender candidateTx) ∈ emptyRequestors intermediates)).
Consensus’ decrement step:
The next defn is the algebraic heart of consensus check algorithm:
fold this function left-to-right over the entire sequence of proposed txs, and you get the remaining
worst-case protected reserve for every sender.
Formally, this function conservatively estimates the remaining effective reserve balance of the sender of candidateTx after executing candidateTx, assuming prevErb is the estimate of the reserve balance of the sender of candidateTx just before executing candidateTx. preIntermediatesState is the latest fully-executed state and intermediates is a list of all the transactions between preIntermediatesState and candidateTx.
In the definition of newBal (let binding), the subtraction is capped below at 0: the result is non-negative.
So, if sbal < maxTxFee candidateTx + value candidateTx but maxTxFee next ≤ sbal, this transaction (candidateTx) will be accepted but all
subsequent ones (with maxTxFee > 0) from the same sender will be rejected as the remaining effective reserve balance becomes 0.
This function represents the check that consensus needs to do when adding candidateTx at the end of the already built/proposed valid sequence of txs intermediates. candidateTx will be accepted iff the returned value is non-negative.
The intermediates list is only used for the isAllowedToEmpty and senderinForbiddenWindow checks:
so the Rust implementation can optimize it by instead just maintaining the 3 pieces of info isAllowedToEmpty needs from intermediates: the set of senders, the set of addrs that are delegated or undelegated, and the set of emptying requestors in those intermediates.
NEW:
compare with the old version
- in the None case, the top-level branching on senderinForbiddenWindow is new. this is to exclude transactions from senders that recently explicitly requested emptying transactions
- at the end, the branching on reqAllowToEmpty is new. the else case returns the value as before. in the then case we set the effective reserve balance as 0 or negative: transactions from this sender will be excluded anyway for the the next K blocks. Because a user can request a transaction to be emptying even if the account is delegated or sent transactions in the same block, we do not have a good way to estimate its previous balance. We do know its previous effective reserve balance, which we use to determine whether that is enough to pay fees: if not, we return a negative number. If the previous effective reserve balance is sufficient, we return 0: transactions from this sender will be excluded anyway for the the next K blocks.
Definition remainingEffReserveBalOfSender (preIntermediatesState : AugmentedState) (prevErb: Z) (intermediates: list TxWithHdr) (candidateTx: TxWithHdr)
: Z :=
let s := preIntermediatesState in
let senderAddr := sender candidateTx in
match reserveBalUpdateOfTx candidateTx with
| Some newRb ⇒
if isAllowedToEmpty s intermediates candidateTx
then (balanceOfAc s.1 senderAddr - maxTxFee candidateTx) `min` newRb
else (prevErb - maxTxFee candidateTx) `min` newRb
| None ⇒
regular tx, not one that sets reserve balance:
if senderinForbiddenWindow preIntermediatesState intermediates candidateTx
then (-1) `min` prevErb
else
if isAllowedToEmpty s intermediates candidateTx
then
let sbal := balanceOfAc s.1 senderAddr in
let newBal:= (sbal - maxTxFee candidateTx - value candidateTx - maxStorageFee candidateTx) `max` 0 in
if asbool (maxTxFee candidateTx ≤ sbal)
then newBal `min` (configuredReserveBalOfAddr s.2 senderAddr)
else -1
else
if reqAllowToEmpty candidateTx
then 0 `min` (prevErb - maxTxFee candidateTx)
else (prevErb - maxTxFee candidateTx)
end.
then (-1) `min` prevErb
else
if isAllowedToEmpty s intermediates candidateTx
then
let sbal := balanceOfAc s.1 senderAddr in
let newBal:= (sbal - maxTxFee candidateTx - value candidateTx - maxStorageFee candidateTx) `max` 0 in
if asbool (maxTxFee candidateTx ≤ sbal)
then newBal `min` (configuredReserveBalOfAddr s.2 senderAddr)
else -1
else
if reqAllowToEmpty candidateTx
then 0 `min` (prevErb - maxTxFee candidateTx)
else (prevErb - maxTxFee candidateTx)
end.
At the end of this file, we have defined remainingEffReserveBalOfSenderF which is analogous to remainingEffReserveBalOfSender but instead of Z arithmetic (idealized, unbounded), only uses U256 aritmetic. Instead of returning Z, it returns an option U256, where negative numbers are represented by None. In that version, candidateTx is accepted iff the result is not None. We proved that remainingEffReserveBalOfSenderF and remainingEffReserveBalOfSender are equivalent.
Next, we just "fold" the above function over the entire sequence of proposed transactions, starting from the first one after the fully executed state. Note how in the recursive call, we add the processed head to the list of intermediate transactions.
As with the function above, the intermediates list is only used for the isAllowedToEmpty and senderinForbiddenWindow checks: the Rust implementation can instead maintain aggregate stats, as described above.
Fixpoint remainingEffReserveBalsL (latestState : AugmentedState) (preRestERBs: EffReserveBals) (postStateAccountedSuffix rest: list TxWithHdr)
: EffReserveBals:=
match rest with
| [] ⇒ preRestERBs
| hrest::tlrest ⇒
let rem: Z :=
remainingEffReserveBalOfSender latestState (preRestERBs (sender hrest)) postStateAccountedSuffix hrest in
let erbs: EffReserveBals := updateKey preRestERBs (sender hrest) (fun _ ⇒ rem) in
remainingEffReserveBalsL latestState erbs (postStateAccountedSuffix++[hrest]) tlrest
end.
: EffReserveBals:=
match rest with
| [] ⇒ preRestERBs
| hrest::tlrest ⇒
let rem: Z :=
remainingEffReserveBalOfSender latestState (preRestERBs (sender hrest)) postStateAccountedSuffix hrest in
let erbs: EffReserveBals := updateKey preRestERBs (sender hrest) (fun _ ⇒ rem) in
remainingEffReserveBalsL latestState erbs (postStateAccountedSuffix++[hrest]) tlrest
end.
Algorithm 1 (consensus): acceptability of a suffix
Definition consensusAcceptableTxs (latestState : AugmentedState) (postStateProposedTxs: list TxWithHdr) : Prop :=
∀ addr, addr ∈ map sender postStateProposedTxs →
0≤ (remainingEffReserveBalsL latestState (initialEffReserveBals latestState) [] postStateProposedTxs) addr.
∀ addr, addr ∈ map sender postStateProposedTxs →
0≤ (remainingEffReserveBalsL latestState (initialEffReserveBals latestState) [] postStateProposedTxs) addr.
Execution Check (algo 2)
Definition isAllowedToEmptyExec
(state : AugmentedState) (tx: TxWithHdr) : bool :=
isAllowedToEmpty state [] tx.
updateExtraState is the execution-time maintenance of the tiny history we
need for emptiness checks and reserve-config changes.
Definition updateExtraState (a: ExtraAcStates) (tx: TxWithHdr) : ExtraAcStates :=
(fun addr ⇒
let oldes := a addr in
{|
lastTxInBlockIndex :=
if asbool (sender tx = addr)
then Some (txBlockNum tx)
else lastTxInBlockIndex oldes;
lastDelUndelInBlockIndex :=
if asbool (addr ∈ addrsDelUndelByTx tx)
then Some (txBlockNum tx)
else lastDelUndelInBlockIndex oldes;
configuredReserveBal:=
if asbool (sender tx = addr)
then
match reserveBalUpdateOfTx tx with
| Some newRb ⇒ newRb
| None ⇒ configuredReserveBal oldes
end
else configuredReserveBal oldes;
lastEmptyReqBlockId :=
if asbool (sender tx = addr) && reqAllowToEmpty tx
then Some (txBlockNum tx)
else lastEmptyReqBlockId oldes;
|}
).
Abstract execution and revert
Axiom evmExecTxCore : StateOfAccounts → TxWithHdr → StateOfAccounts × (list EvmAddr).
Axiom revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts.
Algorithm 2 (execution): execute a transaction
- Special “reserve update” tx: pay fee; set new configured reserve.
- Otherwise, run the core EVM step to obtain the *actual* post state.
- For *changed* accounts, allFinalBalSufficient checks that the account was not debited too much, which may endanger the fee solvency of the later transactions already accepted by consensus.
- If any check fails, revert the tx *and still* update the extra history (so K-window bookkeeping remains accurate).
- the only change is that isAllowedToEmptyExec s t was replaced with isAllowedToEmptyExec s t || reqAllowToEmpty t
Definition allFinalBalSufficient (preTxState: AugmentedState) (postTxState : StateOfAccounts) (changedAccounts: list EvmAddr) (t: TxWithHdr): bool:=
let finalBalSufficient (a: EvmAddr) :=
let ReserveBal := configuredReserveBalOfAddr preTxState.2 a in
let erb:N := ReserveBal `min` (balanceOfAc preTxState.1 a) in
if isSC postTxState a
then true
else
if asbool (sender t =a)
then if isAllowedToEmptyExec preTxState t || reqAllowToEmpty t then true else asbool ((erb - maxTxFee t) ≤ balanceOfAc postTxState a)
else asbool (erb ≤ balanceOfAc postTxState a) in
(forallb finalBalSufficient changedAccounts).
Definition execValidatedTx (s: AugmentedState) (t: TxWithHdr)
: AugmentedState :=
match reserveBalUpdateOfTx t with
| Some n ⇒ (updateBalanceOfAc s.1 (sender t) (fun b ⇒ b - maxTxFee t)
, updateExtraState s.2 t)
| None ⇒
let (postTxState, changedAccounts) := evmExecTxCore (s.1) t in
if (allFinalBalSufficient s postTxState changedAccounts t)
then (postTxState, updateExtraState s.2 t)
else (revertTx s.1 t, updateExtraState s.2 t)
end.
Note that because the isSC check is done on si (the result of running the EVM blackbox to execute t), not s (the pre-exec state), the following scenario is allowed.
Execution check of tx validity:
The implementation checks nonces as well, but here we are only concerned with tx fees.
- Before the tx, Alice sends money to some address addr2 (computed using keccak). Alice is EOA.
- Alice sends tx foo to a smart contract address addr.
- foo execution deploys code at addr2 and calls it, and the call empties addr2.
Definition validateTx (preTxState: StateOfAccounts) (tx: TxWithHdr): bool :=
asbool (maxTxFee tx ≤ balanceOfAc preTxState (sender tx)).
asbool (maxTxFee tx ≤ balanceOfAc preTxState (sender tx)).
Top-level execution wrapper that fails fast when validation fails.
None means the execution of the whole block containing t aborts, which is what the consensus/execution checks must guarantee to never happen.
Definition execTx (s: AugmentedState) (t: TxWithHdr): option (AugmentedState) :=
if (negb (validateTx (s.1) t))
then None
else Some (execValidatedTx s t).
if (negb (validateTx (s.1) t))
then None
else Some (execValidatedTx s t).
execute a list of transactions one by one. Note that if the execution of any tx returns None (balance insufficient to cover fees), the entire execution (of the whole list of txs) returns None.
Fixpoint execTxs (s: AugmentedState) (ts: list TxWithHdr): option AugmentedState :=
match ts with
| [] ⇒ Some s
| t::tls ⇒
match execTx s t with
| Some si ⇒ execTxs si tls
| None ⇒ None
end
end.
match ts with
| [] ⇒ Some s
| t::tls ⇒
match execTx s t with
| Some si ⇒ execTxs si tls
| None ⇒ None
end
end.
Our correctness property only holds when the set of proposed transactions is within K.
This helps capture that assumption.
Fixpoint blockNumsInRange (ltx: list TxWithHdr) : Prop :=
match ltx with
| [] ⇒ True
| htx::ttx ⇒
(∀ txext, txext ∈ ttx → txBlockNum txext - (K-1) ≤ txBlockNum htx ≤ txBlockNum txext)
∧ blockNumsInRange ttx
end.
Definition txCannotCreateContractAtAddrs tx (eoasWithPrivateKey: list EvmAddr) :=
∀ s, let sf := (execValidatedTx s tx) in
∀ addr, addr ∈ eoasWithPrivateKey → isSC s.1 addr = false → isSC sf.1 addr = false.
match ltx with
| [] ⇒ True
| htx::ttx ⇒
(∀ txext, txext ∈ ttx → txBlockNum txext - (K-1) ≤ txBlockNum htx ≤ txBlockNum txext)
∧ blockNumsInRange ttx
end.
Definition txCannotCreateContractAtAddrs tx (eoasWithPrivateKey: list EvmAddr) :=
∀ s, let sf := (execValidatedTx s tx) in
∀ addr, addr ∈ eoasWithPrivateKey → isSC s.1 addr = false → isSC sf.1 addr = false.
The lemma below is probably what one would come up first as the main correctness theorem.
blocks represents the transactions in the blocks proposed after latestState.
It says that consensus checks (consensusAcceptableTxs latestState blocks) implies
that the execution of all transactions blocks one by one, starting from the state latestState will succeed and not abort (None) due to preTx balance being less than maxTxFee.
Theorem fullBlockStep2 (latestState : AugmentedState) (blocks: list TxWithHdr) :
(∀ ac, ac ∈ (map sender blocks) → isSC latestState.1 ac = false)
→ (∀ txext, txext ∈ blocks → txCannotCreateContractAtAddrs txext (map sender blocks))
→ blockNumsInRange blocks
→ consensusAcceptableTxs latestState blocks
→ match execTxs latestState blocks with
| None ⇒ False
| Some si ⇒ True
end.
(∀ ac, ac ∈ (map sender blocks) → isSC latestState.1 ac = false)
→ (∀ txext, txext ∈ blocks → txCannotCreateContractAtAddrs txext (map sender blocks))
→ blockNumsInRange blocks
→ consensusAcceptableTxs latestState blocks
→ match execTxs latestState blocks with
| None ⇒ False
| Some si ⇒ True
end.
main correctness theorem
We will prove the above correctness theorem below, but the actual correctness theorem we need is slightly stronger. Suppose we split blocks in the theorem above into firstblock and restblocks such that blocks=firstblock++blocksrest and suppose these blocks together are all transactions from the K proposed blocks since the last consensus state. Now, consensus will wait for execution to catch up and compute the state after firstblock, say latestState'. After that, consensus should check the next block after blocksrest w.r.t latestState'. At that time, it needs to know that blocksrest is already valid w.r.t latestState', i.e. consensusAcceptableTxs latestState' blocksrest. This is precisely what the main theorem, shown next does:
Theorem fullBlockStep (latestState : AugmentedState) (firstblock: list TxWithHdr) (restblocks: list TxWithHdr) :
blockNumsInRange (firstblock++restblocks)
→ consensusAcceptableTxs latestState (firstblock++restblocks)
→ (∀ txext, txext ∈ (firstblock++restblocks) → txCannotCreateContractAtAddrs txext (map sender (firstblock++restblocks)))
→ (∀ ac, ac ∈ (map sender (firstblock++restblocks)) → isSC latestState.1 ac = false)
→ match execTxs latestState firstblock with
| None ⇒ False
blockNumsInRange (firstblock++restblocks)
→ consensusAcceptableTxs latestState (firstblock++restblocks)
→ (∀ txext, txext ∈ (firstblock++restblocks) → txCannotCreateContractAtAddrs txext (map sender (firstblock++restblocks)))
→ (∀ ac, ac ∈ (map sender (firstblock++restblocks)) → isSC latestState.1 ac = false)
→ match execTxs latestState firstblock with
| None ⇒ False
^ execution cannot abort (indicated by returning None) due to balance being insufficient to even pay fees
in this case, we have enough conditions to guarantee fee-solvency of restblocks, so that it can be extended and then this lemma reapplied:
consensusAcceptableTxs si restblocks
∧ blockNumsInRange restblocks
∧ (∀ ac, ac ∈ (map sender restblocks) → isSC si.1 ac = false)
∧ (∀ txext, txext ∈ (restblocks) → txCannotCreateContractAtAddrs txext (map sender (restblocks)))
end.
∧ blockNumsInRange restblocks
∧ (∀ ac, ac ∈ (map sender restblocks) → isSC si.1 ac = false)
∧ (∀ txext, txext ∈ (restblocks) → txCannotCreateContractAtAddrs txext (map sender (restblocks)))
end.
core execution assumptions
To prove the theorem fullBlockStep, we need to make some assumptions about how the core EVM execution updates balances and delegated-ness. The names of these axioms are fairly descriptive.Axiom balanceOfRevertSender: ∀ s tx,
maxTxFee tx ≤ balanceOfAc s (sender tx)
→ reserveBalUpdateOfTx tx = None
→ balanceOfAc (revertTx s tx) (sender tx)
= balanceOfAc s (sender tx) - maxTxFee tx.
Axiom balanceOfRevertOther: ∀ s tx ac,
reserveBalUpdateOfTx tx = None
→ ac ≠ (sender tx)
→ balanceOfAc (revertTx s tx) ac
= balanceOfAc s ac.
Axiom revertTxDelegationUpdCore: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let sf := (revertTx s tx) in
(∀ ac, addrDelegated sf ac =
(addrDelegated s ac && asbool (ac ∉ (undels tx.1.2)))
|| asbool (ac ∈ (dels tx.1.2))).
Axiom execTxDelegationUpdCore: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
(∀ ac, addrDelegated sf ac =
(addrDelegated s ac && asbool (ac ∉ (undels tx.1.2)))
|| asbool (ac ∈ (dels tx.1.2))).
Axiom execTxSenderBalCore: ∀ tx s,
maxTxFee tx ≤ balanceOfAc s (sender tx) →
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
addrDelegated sf (sender tx) = false
→ balanceOfAc s (sender tx) - ( maxTxFee tx + value tx + maxStorageFee tx) ≤ balanceOfAc sf (sender tx)
∨ balanceOfAc sf (sender tx) = balanceOfAc s (sender tx) - (maxTxFee tx).
One caveat in the assumption below is that it assumes that the account ac does not receive so much credit that it overflows 2^256. In practice, this should never happen, assuming the ETH supply is well below 2^256. Thus, we can assume that evmExecTxCore caps the balance at 2^256 should it overflow, instead of wrapping around, which may violate this assumption.
Axiom execTxCannotDebitNonDelegatedNonContractAccountsCore: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
∀ ac, ac ≠ sender tx
→ (addrDelegated sf ac || isSC sf ac) = false
→ balanceOfAc s ac ≤ balanceOfAc sf ac.
Axiom changedAccountSetSound: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let (sf, changedAccounts) := (evmExecTxCore s tx) in
(∀ ac, ac ∉ changedAccounts → sf ac = s ac).
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
∀ ac, ac ≠ sender tx
→ (addrDelegated sf ac || isSC sf ac) = false
→ balanceOfAc s ac ≤ balanceOfAc sf ac.
Axiom changedAccountSetSound: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let (sf, changedAccounts) := (evmExecTxCore s tx) in
(∀ ac, ac ∉ changedAccounts → sf ac = s ac).
This lemma combines the axioms above to build a
lower bound of the balance of any account after executing a transaction.
Lemma execBalLb ac s tx:
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
let sf := (execValidatedTx s tx) in
let ReserveBal := configuredReserveBalOfAddr s.2 ac in
if (asbool (ac=sender tx)) then
isSC sf.1 (sender tx) = false→
(if isAllowedToEmpty s [] tx
then balanceOfAcA s (sender tx) - ( maxTxFee tx + value tx + maxStorageFee tx) ≤ balanceOfAcA sf (sender tx)
∨ balanceOfAcA sf (sender tx) = balanceOfAcA s (sender tx) - (maxTxFee tx)
else
if reqAllowToEmpty tx
then True
else ReserveBal `min` (balanceOfAcA s (sender tx)) - maxTxFee tx ≤ (balanceOfAcA sf (sender tx)))
else
if (isSC sf.1 ac)
then True
else (if addrDelegated (sf.1) ac then ReserveBal `min` (balanceOfAcA s ac) else balanceOfAcA s ac)
≤ (balanceOfAcA sf ac).
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
let sf := (execValidatedTx s tx) in
let ReserveBal := configuredReserveBalOfAddr s.2 ac in
if (asbool (ac=sender tx)) then
isSC sf.1 (sender tx) = false→
(if isAllowedToEmpty s [] tx
then balanceOfAcA s (sender tx) - ( maxTxFee tx + value tx + maxStorageFee tx) ≤ balanceOfAcA sf (sender tx)
∨ balanceOfAcA sf (sender tx) = balanceOfAcA s (sender tx) - (maxTxFee tx)
else
if reqAllowToEmpty tx
then True
else ReserveBal `min` (balanceOfAcA s (sender tx)) - maxTxFee tx ≤ (balanceOfAcA sf (sender tx)))
else
if (isSC sf.1 ac)
then True
else (if addrDelegated (sf.1) ac then ReserveBal `min` (balanceOfAcA s ac) else balanceOfAcA s ac)
≤ (balanceOfAcA sf ac).
isAllowedToEmpty lemmas
Lemma execPreservesIsAllowedToEmpty s txInterfirst rest txnext:
let sf := execValidatedTx s txInterfirst in
txBlockNum txnext - (K-1) ≤ txBlockNum txInterfirst ≤ txBlockNum txnext
→ isAllowedToEmpty s (txInterfirst :: rest) txnext = isAllowedToEmpty sf rest txnext.
Lemma delgUndelgUpdTxG txlast s addr:
lastEmptyReqBlockId (((execValidatedTx s txlast)).2 addr) =
if (asbool (sender txlast = addr)) && reqAllowToEmpty txlast
then Some (txBlockNum txlast)
else lastEmptyReqBlockId (s.2 addr).
Lemma execPreservesSenderIsForbidden s txInterfirst rest txnext:
let sf := execValidatedTx s txInterfirst in
txBlockNum txnext - (K-1) ≤ txBlockNum txInterfirst ≤ txBlockNum txnext
→ senderinForbiddenWindow s (txInterfirst::rest) txnext = senderinForbiddenWindow sf rest txnext.
Definition rbLe (eoas: list EvmAddr) (rb1 rb2: EffReserveBals) :=
∀ addr, addr ∈ eoas → rb1 addr ≤ rb2 addr.
lemmas about remainingEffReserveBalOfSender
Lemma mono s rb1 rb2 inter tx:
rb1 ≤ rb2
→ (remainingEffReserveBalOfSender s rb1 inter tx) ≤ (remainingEffReserveBalOfSender s rb2 inter tx).
In the proof of the main correctness theorem, we use a slightly different
variant of monotonicity, where in the RHS of ≤, remainingEffReserveBalOfSender starts
from the final state after executing tx from s and as a result, tx
is dropped from the list of intermediate transactions between the state
and the candidate transaction txc.
The proof follows from the definition of remainingEffReserveBalOfSender
and the execution lemmas above.
Lemma mono2 tx txc extension s (eoas: list EvmAddr) rb1 rb2 inter:
(∀ ac : EvmAddr,
ac ∈ sender tx :: sender txc :: map sender extension
→ isSC (execValidatedTx s tx).1 ac = false)
→ (rb1 ≤ rb2)
→ txBlockNum txc - (K-1) ≤ txBlockNum tx ≤ txBlockNum txc
→ ∀ addr : EvmAddr,
addr ∈ eoas
→ remainingEffReserveBalOfSender s rb1 (tx :: inter) txc
≤ remainingEffReserveBalOfSender (execValidatedTx s tx) rb2 inter txc.
(∀ ac : EvmAddr,
ac ∈ sender tx :: sender txc :: map sender extension
→ isSC (execValidatedTx s tx).1 ac = false)
→ (rb1 ≤ rb2)
→ txBlockNum txc - (K-1) ≤ txBlockNum tx ≤ txBlockNum txc
→ ∀ addr : EvmAddr,
addr ∈ eoas
→ remainingEffReserveBalOfSender s rb1 (tx :: inter) txc
≤ remainingEffReserveBalOfSender (execValidatedTx s tx) rb2 inter txc.
lifts mono2 from remainingEffReserveBalOfSender to remainingEffReserveBalsL:
proof follows a straightforward induction on the list extension,
using mono2 to fulfil the obligations of the induction hypothesis.
Lemma monoL2 eoas s rb1 rb2 inter extension tx:
(map sender extension) ⊆ eoas
→ rbLe eoas rb1 rb2
→ (∀ txext, txext ∈ extension → txBlockNum txext - (K-1) ≤ txBlockNum tx ≤ txBlockNum txext)
→ (∀ ac : EvmAddr, ac ∈ map sender (tx :: extension) → isSC (execValidatedTx s tx).1 ac = false)
→ rbLe eoas (remainingEffReserveBalsL s rb1 (tx::inter) extension)
(remainingEffReserveBalsL (execValidatedTx s tx) rb2 inter extension).
This lemma captures a key property of remainingEffReserveBalOfSender: it underapproximates
the resultant effective balance after execution of the transaction.
This is the heart of the proof of the top-level correctness theorem fullBlockStep.
Proof follows by unfolding definitions and case analysis. uses mono2, execBalLb, and many other lemmas above
Lemma exec1 tx extension s :
let remf := remainingEffReserveBalOfSender s (initialEffReserveBals s (sender tx)) [] tx in
let remRbsf := updateKey (initialEffReserveBals s) (sender tx) (fun _ ⇒ remf) in
let sf := (execValidatedTx s tx) in
maxTxFee tx ≤ balanceOfAc s.1 (sender tx)
→ (∀ ac : EvmAddr, ac ∈ sender tx :: map sender extension → isSC sf.1 ac = false)
→ (∀ addr : EvmAddr,
addr ∈ sender tx :: map sender extension
→ remRbsf addr
≤ initialEffReserveBals sf addr).
Definition remainingEffReserveBals s irb inter txc:=
let rem := remainingEffReserveBalOfSender s (irb (sender txc)) inter txc in
updateKey irb (sender txc) (fun _ ⇒ rem).
Lemma decreasingRemTxSender s irb proc tx txc:
let rem := remainingEffReserveBalOfSender s (irb (sender txc)) (tx :: proc) txc in
let remRbs := updateKey irb (sender txc) (fun _ ⇒ rem) in
remRbs (sender tx) ≤ irb (sender tx).
let remf := remainingEffReserveBalOfSender s (initialEffReserveBals s (sender tx)) [] tx in
let remRbsf := updateKey (initialEffReserveBals s) (sender tx) (fun _ ⇒ remf) in
let sf := (execValidatedTx s tx) in
maxTxFee tx ≤ balanceOfAc s.1 (sender tx)
→ (∀ ac : EvmAddr, ac ∈ sender tx :: map sender extension → isSC sf.1 ac = false)
→ (∀ addr : EvmAddr,
addr ∈ sender tx :: map sender extension
→ remRbsf addr
≤ initialEffReserveBals sf addr).
Definition remainingEffReserveBals s irb inter txc:=
let rem := remainingEffReserveBalOfSender s (irb (sender txc)) inter txc in
updateKey irb (sender txc) (fun _ ⇒ rem).
Lemma decreasingRemTxSender s irb proc tx txc:
let rem := remainingEffReserveBalOfSender s (irb (sender txc)) (tx :: proc) txc in
let remRbs := updateKey irb (sender txc) (fun _ ⇒ rem) in
remRbs (sender tx) ≤ irb (sender tx).
lifts the previous lemma from remainingEffReserveBalOfSender to remainingEffReserveBalsL. induction on nextL
Lemma decreasingRemL s irb proc (nextL: list TxWithHdr) tx:
(remainingEffReserveBalsL s irb (tx::proc) nextL) (sender tx) ≤ (irb (sender tx)).
(remainingEffReserveBalsL s irb (tx::proc) nextL) (sender tx) ≤ (irb (sender tx)).
Here is a component of the main theorem: it says that
the consensus checks guarantee that execution of the first transaction
will pass validation during execution, i.e. the balance would be sufficient to cover
maxTxFee.
This doesn't say anything about whether the execution of the later transactions (extension) will also pass the check. That is where the next lemma comes in handy.
Lemma execValidate tx extension s:
consensusAcceptableTxs s (tx::extension)
→ validateTx s.1 tx = true.
consensusAcceptableTxs s (tx::extension)
→ validateTx s.1 tx = true.
This lemma says that you can execute the first tx in the proposed extension and the consensus checks would
still hold on the resultant state for the remaining transactions in the proposal.
This follows from exec1 and monoL2
Lemma execPreservesConsensusChecks tx extension s:
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
(∀ txext, txext ∈ extension → txBlockNum txext - (K-1) ≤ txBlockNum tx ≤ txBlockNum txext) → (∀ txext, txext ∈ tx::extension → txCannotCreateContractAtAddrs txext (map sender (tx::extension)))
→ (∀ ac, ac ∈ (map sender (tx::extension)) → isSC s.1 ac = false)
→ consensusAcceptableTxs s (tx::extension)
→ consensusAcceptableTxs (execValidatedTx s tx) extension.
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
(∀ txext, txext ∈ extension → txBlockNum txext - (K-1) ≤ txBlockNum tx ≤ txBlockNum txext) → (∀ txext, txext ∈ tx::extension → txCannotCreateContractAtAddrs txext (map sender (tx::extension)))
→ (∀ ac, ac ∈ (map sender (tx::extension)) → isSC s.1 ac = false)
→ consensusAcceptableTxs s (tx::extension)
→ consensusAcceptableTxs (execValidatedTx s tx) extension.
The above 2 lemmas are used to yield the following:
Lemma inductiveStep (latestState : AugmentedState) (tx: TxWithHdr) (extension: list TxWithHdr) :
maxTxFee tx ≤ balanceOfAc latestState.1 (sender tx)
→ (∀ txext, txext ∈ extension → txBlockNum txext - (K-1) ≤ txBlockNum tx ≤ txBlockNum txext)
→ (∀ txext, txext ∈ tx::extension → txCannotCreateContractAtAddrs txext (map sender (tx::extension)))
→ (∀ ac, ac ∈ (map sender (tx::extension)) → isSC latestState.1 ac = false)
→ consensusAcceptableTxs latestState (tx::extension)
→ match execTx latestState tx with
| None ⇒ False
| Some si ⇒
consensusAcceptableTxs si extension
end.
Fixpoint blockNumsInRange2 (ltx: list TxWithHdr) : Prop :=
match ltx with
| [] ⇒ True
| htx::ttx ⇒
(∀ txext, txext ∈ ttx → txBlockNum txext ≤ txBlockNum htx + (K-1) ∧ txBlockNum htx ≤ txBlockNum txext)
∧ blockNumsInRange2 ttx
end.
Lemma bnequiv ltx: blockNumsInRange2 ltx → blockNumsInRange ltx .
Lemma bnequiv2 ltx: blockNumsInRange ltx → blockNumsInRange2 ltx .
Lemma txCannotCreateContractAtAddrsMono tx l1 l2:
l1 ⊆ l2
→ txCannotCreateContractAtAddrs tx l2
→ txCannotCreateContractAtAddrs tx l1.
Lemma txCannotCreateContractAtAddrsTrimHead tx h l:
txCannotCreateContractAtAddrs tx (h::l)
→ txCannotCreateContractAtAddrs tx l.
maxTxFee tx ≤ balanceOfAc latestState.1 (sender tx)
→ (∀ txext, txext ∈ extension → txBlockNum txext - (K-1) ≤ txBlockNum tx ≤ txBlockNum txext)
→ (∀ txext, txext ∈ tx::extension → txCannotCreateContractAtAddrs txext (map sender (tx::extension)))
→ (∀ ac, ac ∈ (map sender (tx::extension)) → isSC latestState.1 ac = false)
→ consensusAcceptableTxs latestState (tx::extension)
→ match execTx latestState tx with
| None ⇒ False
| Some si ⇒
consensusAcceptableTxs si extension
end.
Fixpoint blockNumsInRange2 (ltx: list TxWithHdr) : Prop :=
match ltx with
| [] ⇒ True
| htx::ttx ⇒
(∀ txext, txext ∈ ttx → txBlockNum txext ≤ txBlockNum htx + (K-1) ∧ txBlockNum htx ≤ txBlockNum txext)
∧ blockNumsInRange2 ttx
end.
Lemma bnequiv ltx: blockNumsInRange2 ltx → blockNumsInRange ltx .
Lemma bnequiv2 ltx: blockNumsInRange ltx → blockNumsInRange2 ltx .
Lemma txCannotCreateContractAtAddrsMono tx l1 l2:
l1 ⊆ l2
→ txCannotCreateContractAtAddrs tx l2
→ txCannotCreateContractAtAddrs tx l1.
Lemma txCannotCreateContractAtAddrsTrimHead tx h l:
txCannotCreateContractAtAddrs tx (h::l)
→ txCannotCreateContractAtAddrs tx l.
Proof of main theorem:
Straightforward induction on firstblock, with inductiveStep used in the inductive step.Lemma fullBlockStep (latestState : AugmentedState) (firstblock restblocks: list TxWithHdr) :
blockNumsInRange (firstblock++restblocks)
→ consensusAcceptableTxs latestState (firstblock++restblocks)
→ (∀ txext, txext ∈ (firstblock++restblocks) → txCannotCreateContractAtAddrs txext (map sender (firstblock++restblocks)))
→ (∀ ac, ac ∈ (map sender (firstblock++restblocks)) → isSC latestState.1 ac = false)
→ match execTxs latestState firstblock with
| None ⇒ False
| Some si ⇒
consensusAcceptableTxs si restblocks
∧ blockNumsInRange restblocks
∧ (∀ ac, ac ∈ (map sender restblocks) → isSC si.1 ac = false)
∧ (∀ txext, txext ∈ (restblocks) → txCannotCreateContractAtAddrs txext (map sender (restblocks)))
end.
Print Assumptions fullBlockStep.
All assumptions of the proof:
Section Variables:
K
: N
Axioms:
revertTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := revertTx s tx) (ac : EvmAddr),
addrDelegated sf ac = addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts
maxStorageFee : TxWithHdr → N
execTxSenderBalCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ let sf := (evmExecTxCore s tx).1 in
addrDelegated sf (sender tx) = false
→ (balanceOfAc s (sender tx) - (maxTxFee tx + value tx + maxStorageFee tx) ≤ balanceOfAc sf (sender tx))%N
∨ balanceOfAc sf (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
execTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
addrDelegated sf ac = addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
ac ≠ sender tx → addrDelegated sf ac || isSC sf ac = false → (balanceOfAc s ac ≤ balanceOfAc sf ac)%N
evmExecTxCore : StateOfAccounts → TxWithHdr → StateOfAccounts × list EvmAddr
changedAccountSetSound :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ let (sf, changedAccounts) := evmExecTxCore s tx in ∀ ac : EvmAddr, ac ∉ changedAccounts → sf ac = s ac
balanceOfRevertSender :
∀ (s : StateOfAccounts) (tx : TxWithHdr),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ balanceOfAc (revertTx s tx) (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
balanceOfRevertOther :
∀ (s : StateOfAccounts) (tx : TxWithHdr) (ac : EvmAddr),
reserveBalUpdateOfTx tx = None → ac ≠ sender tx → balanceOfAc (revertTx s tx) ac = balanceOfAc s ac
Section Variables:
K
: N
Axioms:
revertTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := revertTx s tx) (ac : EvmAddr),
addrDelegated sf ac = addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts
maxStorageFee : TxWithHdr → N
execTxSenderBalCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ let sf := (evmExecTxCore s tx).1 in
addrDelegated sf (sender tx) = false
→ (balanceOfAc s (sender tx) - (maxTxFee tx + value tx + maxStorageFee tx) ≤ balanceOfAc sf (sender tx))%N
∨ balanceOfAc sf (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
execTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
addrDelegated sf ac = addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
ac ≠ sender tx → addrDelegated sf ac || isSC sf ac = false → (balanceOfAc s ac ≤ balanceOfAc sf ac)%N
evmExecTxCore : StateOfAccounts → TxWithHdr → StateOfAccounts × list EvmAddr
changedAccountSetSound :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ let (sf, changedAccounts) := evmExecTxCore s tx in ∀ ac : EvmAddr, ac ∉ changedAccounts → sf ac = s ac
balanceOfRevertSender :
∀ (s : StateOfAccounts) (tx : TxWithHdr),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ balanceOfAc (revertTx s tx) (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
balanceOfRevertOther :
∀ (s : StateOfAccounts) (tx : TxWithHdr) (ac : EvmAddr),
reserveBalUpdateOfTx tx = None → ac ≠ sender tx → balanceOfAc (revertTx s tx) ac = balanceOfAc s ac
Corollary fullBlockStep2 (latestState : AugmentedState) (blocks: list TxWithHdr) :
(∀ ac, ac ∈ (map sender (blocks)) → isSC latestState.1 ac = false)
→ (∀ txext, txext ∈ (blocks) → txCannotCreateContractAtAddrs txext (map sender (blocks)))
→ blockNumsInRange (blocks)
→ consensusAcceptableTxs latestState (blocks)
→ match execTxs latestState blocks with
| None ⇒ False
| Some si ⇒ True
end.
Lemma acceptableNil lastConsensedState:
consensusAcceptableTxs lastConsensedState [].
End K.
Consensus checks using finite-width arithmetic (U256).
Definition u256z_equiv (o: option U256) (z: Z) : Prop :=
(z < 2^256) ∧
match o with
| None ⇒ (z < 0)
| Some u ⇒ z = u256_to_Z u
end.
(z < 2^256) ∧
match o with
| None ⇒ (z < 0)
| Some u ⇒ z = u256_to_Z u
end.
Below, we define variants of previous definitions where Z is substituted in by U256.
The suffix F stands for finite width numbers.
Helper projections relying on the invariant that balances already fit in 256 bits.
Definition balanceOfAcF (s : AugmentedState) (addr : EvmAddr) : U256 :=
u256_of_N (balanceOfAc s.1 addr).
Definition configuredReserveBalF (s : AugmentedState) (addr : EvmAddr) : U256 :=
u256_of_N (configuredReserveBalOfAddr s.2 addr).
Definition valueF (t: TxWithHdr) : U256 := u256_of_N (value t).
Definition maxTxFeeF (t: TxWithHdr) : U256 := u256_of_N (maxTxFee t).
Definition maxStorageFeeF (t: TxWithHdr) : U256 := u256_of_N (maxStorageFee t).
Definition reserveBalUpdateOfTxF (t: TxWithHdr) : option U256 := option_map u256_of_N (reserveBalUpdateOfTx t).
u256_of_N (balanceOfAc s.1 addr).
Definition configuredReserveBalF (s : AugmentedState) (addr : EvmAddr) : U256 :=
u256_of_N (configuredReserveBalOfAddr s.2 addr).
Definition valueF (t: TxWithHdr) : U256 := u256_of_N (value t).
Definition maxTxFeeF (t: TxWithHdr) : U256 := u256_of_N (maxTxFee t).
Definition maxStorageFeeF (t: TxWithHdr) : U256 := u256_of_N (maxStorageFee t).
Definition reserveBalUpdateOfTxF (t: TxWithHdr) : option U256 := option_map u256_of_N (reserveBalUpdateOfTx t).
The definition of remainingEffReserveBalOfSenderF will look almost exactly like remainingEffReserveBalOfSender except that it uses variants of arithmetic operations (like -, `min`, `max`) that work on option U256 instead of Z. So, we define those operations on option 256 and prove that they respect the equivalence u256z_equiv.
The first one is the min operation:
Definition u256_opt_min (lhs rhs : option U256) : option U256 :=
match lhs, rhs with
| Some l, Some r ⇒ Some (u256_min l r)
| _, _ ⇒ None
match lhs, rhs with
| Some l, Some r ⇒ Some (u256_min l r)
| _, _ ⇒ None
^ the min of 2 negative numbers is always a negative number
end.
We easily prove that our min operation on option U256 respects the
u256z_equiv relation: on inputs that are equivalent,
the outputs of u256_opt_min and Z.min are equivalent
Lemma u256z_equiv_mino (x y : option U256) (zx zy : Z) :
u256z_equiv x zx →
u256z_equiv y zy →
u256z_equiv (u256_opt_min x y) (Z.min zx zy).
u256z_equiv x zx →
u256z_equiv y zy →
u256z_equiv (u256_opt_min x y) (Z.min zx zy).
max operation on option U256
Definition u256_opt_max (lhs rhs : option U256) : option U256 :=
match lhs, rhs with
| Some l, Some r ⇒
Some (u256_wrap ((u256_to_N l) `max` (u256_to_N r)))
| Some l, None ⇒ Some l
| None, Some r ⇒ Some r
| None, None ⇒ None
end.
Lemma u256z_equiv_maxo (x y : option U256) (zx zy : Z) :
u256z_equiv x zx →
u256z_equiv y zy →
u256z_equiv (u256_opt_max x y) (Z.max zx zy).
match lhs, rhs with
| Some l, Some r ⇒
Some (u256_wrap ((u256_to_N l) `max` (u256_to_N r)))
| Some l, None ⇒ Some l
| None, Some r ⇒ Some r
| None, None ⇒ None
end.
Lemma u256z_equiv_maxo (x y : option U256) (zx zy : Z) :
u256z_equiv x zx →
u256z_equiv y zy →
u256z_equiv (u256_opt_max x y) (Z.max zx zy).
Subtraction is tricky. if a and b are negative numbers, the result may be negative or positive. But as we represent both a and b by None, we do not have enough information to determine that. Fortunately, in remainingEffReserveBalOfSender, the RHS of subtraction was always known to be non-negative. So it suffices to just define for that case:
Definition u256_opt_sub (lhs : option U256) (rhs: U256) : option U256 :=
match lhs with
| Some l ⇒
if (asbool (u256_to_N l < u256_to_N rhs)) then None else Some (u256_sub l rhs)
| None ⇒ None
match lhs with
| Some l ⇒
if (asbool (u256_to_N l < u256_to_N rhs)) then None else Some (u256_sub l rhs)
| None ⇒ None
^ subtracting a non-negative number from a negative number is always negative
end.
Lemma u256z_equiv_sub (x : option U256) (y : U256) (zx zy : Z) :
u256z_equiv x zx →
u256z_equiv (Some y) zy →
u256z_equiv (u256_opt_sub x y) (zx - zy).
Lemma u256z_equiv_sub (x : option U256) (y : U256) (zx zy : Z) :
u256z_equiv x zx →
u256z_equiv (Some y) zy →
u256z_equiv (u256_opt_sub x y) (zx - zy).
We define some notations to make our definition of remainingEffReserveBalOfSenderF look similar
Notation "x ⊖ y" := (u256_opt_sub x y) (at level 50, left associativity) : u256opt_scope.
Notation "x `mino` y" := (u256_opt_min x y) (at level 35, y at next level) : u256opt_scope.
Notation "x `maxo` y" := (u256_opt_max x y) (at level 35, y at next level) : u256opt_scope.
Notation "0f" := (Some u256_zero) : u256opt_scope.
Notation "x `mino` y" := (u256_opt_min x y) (at level 35, y at next level) : u256opt_scope.
Notation "x `maxo` y" := (u256_opt_max x y) (at level 35, y at next level) : u256opt_scope.
Notation "0f" := (Some u256_zero) : u256opt_scope.
Finally, below is the U256 version of remainingEffReserveBalOfSender
Definition remainingEffReserveBalOfSenderF
(preIntermediatesState : AugmentedState)
(prevErb : U256)
(intermediates : list TxWithHdr)
(candidateTx : TxWithHdr) : option U256 :=
let s := preIntermediatesState in
let senderAddr := sender candidateTx in
let senderBal := Some (balanceOfAcF preIntermediatesState senderAddr) in
let configuredRb := Some (configuredReserveBalF preIntermediatesState senderAddr) in
match reserveBalUpdateOfTxF candidateTx with
| Some newRb ⇒
if isAllowedToEmpty K preIntermediatesState intermediates candidateTx
then (senderBal ⊖ maxTxFeeF candidateTx) `mino` (Some newRb)
else (Some prevErb ⊖ maxTxFeeF candidateTx) `mino` (Some newRb)
| None ⇒
if senderinForbiddenWindow K preIntermediatesState intermediates candidateTx
then None `mino` Some prevErb
else
if isAllowedToEmpty K preIntermediatesState intermediates candidateTx
then
let newBal := (((senderBal ⊖ (maxTxFeeF candidateTx)) ⊖ (valueF candidateTx)) ⊖ (maxStorageFeeF candidateTx)) `maxo` 0f in
(preIntermediatesState : AugmentedState)
(prevErb : U256)
(intermediates : list TxWithHdr)
(candidateTx : TxWithHdr) : option U256 :=
let s := preIntermediatesState in
let senderAddr := sender candidateTx in
let senderBal := Some (balanceOfAcF preIntermediatesState senderAddr) in
let configuredRb := Some (configuredReserveBalF preIntermediatesState senderAddr) in
match reserveBalUpdateOfTxF candidateTx with
| Some newRb ⇒
if isAllowedToEmpty K preIntermediatesState intermediates candidateTx
then (senderBal ⊖ maxTxFeeF candidateTx) `mino` (Some newRb)
else (Some prevErb ⊖ maxTxFeeF candidateTx) `mino` (Some newRb)
| None ⇒
if senderinForbiddenWindow K preIntermediatesState intermediates candidateTx
then None `mino` Some prevErb
else
if isAllowedToEmpty K preIntermediatesState intermediates candidateTx
then
let newBal := (((senderBal ⊖ (maxTxFeeF candidateTx)) ⊖ (valueF candidateTx)) ⊖ (maxStorageFeeF candidateTx)) `maxo` 0f in
^ pay attention to the bracketing. If we instead first add up maxTxFeeF candidateTx, valueF candidateTx, and maxStorageFeeF candidateTx, and only then subtract from senderBal, the result of addition may overflow, although only a malicious person would probably send such a transaction
if asbool (maxTxFee candidateTx ≤ balanceOfAc (preIntermediatesState.1) senderAddr)
then newBal `mino` configuredRb
else None
else
if reqAllowToEmpty candidateTx
then 0f `mino` (Some prevErb ⊖ (maxTxFeeF candidateTx))
else Some prevErb ⊖ (maxTxFeeF candidateTx)
end.
then newBal `mino` configuredRb
else None
else
if reqAllowToEmpty candidateTx
then 0f `mino` (Some prevErb ⊖ (maxTxFeeF candidateTx))
else Some prevErb ⊖ (maxTxFeeF candidateTx)
end.
Next we prove equivalence: for that, we need to assume that the balances, configured reserve balance thresholds of all accounts, value, maxTxFee, etc. are within bounds of U256. These are trivial invariants because the EVM datatypes already enforce that
Definition stateWithinBounds (s : AugmentedState) : Prop :=
∀ addr,
((balanceOfAc s.1 addr) < 2^256) ∧
((configuredReserveBalOfAddr s.2 addr) < 2^256).
Definition txReserveUpdateWithinBounds (tx : TxWithHdr) : Prop :=
maxTxFee tx < 2^256 ∧
value tx < 2^256 ∧
maxStorageFee tx < 2^256 ∧
match reserveBalUpdateOfTx tx with
| Some rb ⇒ (rb < 2^256)
| None ⇒ True
end.
∀ addr,
((balanceOfAc s.1 addr) < 2^256) ∧
((configuredReserveBalOfAddr s.2 addr) < 2^256).
Definition txReserveUpdateWithinBounds (tx : TxWithHdr) : Prop :=
maxTxFee tx < 2^256 ∧
value tx < 2^256 ∧
maxStorageFee tx < 2^256 ∧
match reserveBalUpdateOfTx tx with
| Some rb ⇒ (rb < 2^256)
| None ⇒ True
end.
we can now use the above 2 definitions to state the equivalence theorem. The proof is mainly just unfolding definitions and using an arithmetic solver that understands Z.modulo
Lemma remainingEffReserveBalOfSender_equivF
(preIntermediatesState : AugmentedState)
(prevErbF : U256)
(intermediates : list TxWithHdr)
(candidateTx : TxWithHdr) :
stateWithinBounds preIntermediatesState →
txReserveUpdateWithinBounds candidateTx →
u256_val prevErbF < 2^256 →
u256z_equiv
(remainingEffReserveBalOfSenderF preIntermediatesState prevErbF intermediates candidateTx)
(remainingEffReserveBalOfSender K preIntermediatesState (u256_to_N prevErbF) intermediates candidateTx).
End U256Consensus.
(preIntermediatesState : AugmentedState)
(prevErbF : U256)
(intermediates : list TxWithHdr)
(candidateTx : TxWithHdr) :
stateWithinBounds preIntermediatesState →
txReserveUpdateWithinBounds candidateTx →
u256_val prevErbF < 2^256 →
u256z_equiv
(remainingEffReserveBalOfSenderF preIntermediatesState prevErbF intermediates candidateTx)
(remainingEffReserveBalOfSender K preIntermediatesState (u256_to_N prevErbF) intermediates candidateTx).
End U256Consensus.
This page has been generated by coqdoc