Coq model of reserve balance and proof of safety

The guiding idea is simple:
  • *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.
Below we define the two algorithms, followed by proofs. The consensus check is defined in consensusAcceptableTxs, the execution check is in execValidatedTx (called by execTx after validation).
**Delayed reserve-balance configuration.** Accounts can update their reserve balance by calling a stateful precompile; updates become effective only after K blocks. We model this via delayedReserveBalOfAddr, which returns the latest update at or before block n-K when processing block n. Consensus therefore does not need to inspect transactions to track reserve-balance updates, and execution records pending updates in extra state. Reverted transactions discard reserve-balance updates.
The main soundness theorem is fullBlockStep. Intuitively, it says that if any sequence of txs is accepted by consensus, execution of those txs cannot run into an error because of inability to pay gas fees. References to any Coq item are hyperlinked to its definition if the definition is in this file or in the Coq standard library.
consensusAcceptableTxs is defined using idealized arithmetic (Z, which is Coq's type of unbounded integers with no over/underflow in operations). To be sure that it can be implemented without over/underflow issues, at the end of this file, we define a variant that only uses U256 operations and prove that variant equivalent to the one using Z. The proof essentially boils down to showing that it can be implemented in a way that avoids any over/underflows during the U256 operations involved in the computation.


Preliminaries

This is the full EVM state that EVM execution takes as input and returns as output:
Definition StateOfAccounts : Type := EvmAddr AccountM.

Whether an address is currently delegated in the core state.
Definition addrDelegated (s: StateOfAccounts) (a : EvmAddr) : bool :=
  match delegatedTo (s a) with
  | []false
  | _::_true
  end.

Record EIP7702Authority := {
    del_from: option EvmAddr;
    
^ None means the signature was invalid
    del_to: EvmAddr;
    
^ address 0 means undelegate
  }.

Many of the EVM semantics definitions we use come from Yoichi's EVM semantics, developed several years ago. The definition of Transaction there lacks fields to support newer features like delegation. We extend the extra fields with a summary of reserve-balance configuration updates (if any) that occurred during execution; these updates are applied with a delayed effect after K blocks, so consensus does not need to inspect transactions to determine current reserve balances.
Record TxExtra :=
  {
    
    authorities: list EIP7702Authority;

    
The fields above should ultimately come from EVM semantics. The fields below are monad-specific.
    
    indexInBlock: N;
    
^ index of this transaction in the block. monaddb stores when (blockindex, txindex) an account was born
Reserve-balance updates are no longer summarized in TxExtra; they are emitted by the EVM core during execution and applied with a delayed effect.
  }.

for any decidable assertion/proposition P, asbool P is a boolean such that (asbool P = true) P, where means iff (if and only if)
Notation asbool := bool_decide.

Definition is_undelegation (a: EIP7702Authority) : bool :=
  asbool (del_to a = 0).

list of accounts requested to be delegated in transaction txe
Definition dels (txe: TxExtra) : list EvmAddr :=
  flat_map (fun amatch del_from a with
                     | Some addrif is_undelegation a then [] else [addr]
                     | None[]
                     end ) (authorities txe).

list of accounts requested to be undelegated in transaction txe
Definition undels (txe: TxExtra) : list EvmAddr :=
  flat_map (fun amatch del_from a with
                     | Some addrif is_undelegation a then [addr] else []
                     | None[]
                     end ) (authorities txe).

#[global] Opaque dels undels.

Definition Transaction: Type := block.transaction × TxExtra.
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.
Definition TxWithHdr : Type := Transaction × BlockHeader.

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.1.1)) × (w256_to_N (block.tr_gas_limit t.1.1))).


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
Definition maxStorageFee (t: TxWithHdr) : N.

Section K.
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.
Variable K: N.
Hypothesis K_pos : (0 < K)%N.

Next, we have some simple wrappers for brevity.
sender of a transaction:
Definition sender (t: TxWithHdr): EvmAddr := tsender t.1.1.

balance of an account in a given state:
Definition balanceOfAc (s: StateOfAccounts) (a: EvmAddr) : N := balance (s a).

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 kif (asbool (k=updKey)) then f (oldmap updKey) else oldmap k.

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 oldold &: _balance %= upd).

value transfer field of a of a transaction
Definition value (t: TxWithHdr): N := w256_to_N (block.tr_value t.1.1).

addresses delegated or undelegated by tx
Definition addrsDelUndelByTx (tx: TxWithHdr) : list EvmAddr := (dels tx.1.2 ++undels tx.1.2).

block number of a transaction
Definition txBlockNum (t: TxWithHdr) : N := number t.2.

A transaction may emit multiple reserve-balance updates (e.g., via repeated precompile calls). Each entry targets a specific address.
Definition ReserveBalUpdate : Type := list (EvmAddr × N).

To implement reserve balance checks, execution needs to maintain some extra state (beyond the core EVM state) for each account:
Record ExtraAcState :=
  {
    lastTxInBlockIndex : option N;
    
^ 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
    lastDelUndelInBlockIndex : option N;
    
^ last block index where this address was delegated or undelegated. In the implementation, we can just track the last 2K range.
    settledReserveBal: N;
    
^ last *settled* reserve balance value for this account (defaults to DefaultReserveBal).
    pendingReserveBal: option (N × N);
    
^ pending reserve balance update (if any), as (value, block).
The settled (non-delayed) configured reserve balance value.
Our modified execution function which does reserve balance checks will use the following type as input/output.
balance of an account in a given augmented state
Definition balanceOfAcA (s: AugmentedState) (ac: EvmAddr) := balanceOfAc s.1 ac.

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)
  | Nonefalse
  end.

returns true if sender tx sent a transaction within the last K blocks of txBlockNum 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.

Axiom keccak256_program : evm.program N.
Definition code_hash_of_program
           (pr: monad.EVMOpSem.evm.program)
  : Corelib.Numbers.BinNums.N :=
  keccak256_program pr.

Definition delegation_indicator_size : Z := 23%Z.
Parameter delegation_marker_prefix : evm.program bool.

Definition isDelegationMarker (p: evm.program) : bool :=
  (asbool (monad.EVMOpSem.evm.program_length p = delegation_indicator_size))
    && delegation_marker_prefix p.

is an account a smart contract?
Definition isAcSC (os: option AccountM) : bool:=
  match os with
   | Some s
      (asbool (code_hash_of_program (block.block_account_code (coreAc s)) 0%N)) && (negb (isDelegationMarker (block.block_account_code (coreAc s))))
   | Nonefalse
  end.

in state s, is the account at address addr a smart contract?
Definition isSC (s: StateOfAccounts) (addr: EvmAddr): bool:=
  isAcSC (Some (s addr)).


Consensus Check (algo 1)

Below, we build up the definition of the consensus check consensusAcceptableTxs The “emptying” gate:
candidateTx may “empty” the sender's balance iff all three are true about its sender:
  • 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.
This gate is what lets consensus handle fee-only budgets and still allows an EOA to empty their account under certain conditions. preIntermediatesState is the latest fully-executed state and intermediates is a list of all the transactions between preIntermediatesState and candidateTx.
Whether this is the first transaction from the sender within the last K blocks and without delegation/undelegation activity in that window. This is a slightly *weaker* condition than isAllowedToEmpty because it ignores delegation/undelegation activity that may appear in candidateTx itself. Hence isAllowedToEmpty implies senderNoRecentActivity, but not conversely (e.g., a tx that contains a self-authorization can make isAllowedToEmpty false while senderNoRecentActivity remains true). A precise characterization of this gap is given later in Lemma isAllowedToEmpty_false_senderNoRecentActivity.
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:
  • 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.
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
Definition EffReserveBals := EvmAddr Z.

delayed user reserve balance: latest update whose block index plus K is ≤ n.
Definition delayedReserveBal (es: ExtraAcState) (n: N) : N :=
  match pendingReserveBal es with
  | Some (v, blk)
      if asbool (blk + K n)
      then v
      else settledReserveBal es
  | NonesettledReserveBal es
  end.

delayed user reserve balance at address addr in the state map.
Definition delayedReserveBalOfAddr (s: ExtraAcStates) (addr : EvmAddr) (n: N) : N :=
  delayedReserveBal (s addr) n.


initial effective reserve balances, before any proposed tx. For delayed URB, the initial value is a placeholder; the first time a sender appears, we recompute using the block number of that tx (via the senderNoRecentActivity and advanceEffReserveBals steps). For senders that never appear in the suffix, this placeholder is irrelevant.
Advance the effective-reserve map to a block b by clamping each entry to the delayed reserve balance effective at b.
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 used only for the isAllowedToEmpty check and to detect whether the sender already appeared in the last K blocks. So the Rust implementation can optimize it by instead just maintaining the necessary summaries (the set of senders and the set of addrs that are delegated or undelegated in those txs).
The file also contains a finite-width (U256) variant of the consensus checks; see Section U256Consensus near the end.
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, postStateAccountedSuffix is only used to track whether the sender already appeared in the suffix and for the isAllowedToEmpty check. An optimized implementation can maintain just the necessary summaries instead of the full list.

Algorithm 1 (consensus): acceptability of a suffix

postStateProposedTxs is **consensus-acceptable** if, after pessimistically removing the head-then-tail debits, *every sender that appears later still has a non-negative effective reserve*. This is exactly the safety condition proposers maintain as they build blocks up to K ahead.
When evaluating a new tx at block number N to add at the end of postStateProposedTxs, latestState must be the state after N-K block when proposing a new block. However, when the next pending (already proposed) block is executed, we need to derive that the remaining already proposed transactions are still valid on top of the more recent state: this is what the main soundness lemma fullBlockStep proves, in addition to proving that postStateProposedTxs will execute without running out of fees to pay.

Execution Check (algo 2)

The execution logic is also tweaked to ensure that a transaction cannot dip too much into reserves so as to not have enough fees for a transaction already included by consensus. The main complication is that some EOAs may be delegated and thus transactions sent to them can make arbitrary debits that are hard to statically estimate without actually executing the transaction. Thus, we have a dynamic check at the end of execution, to check if some EOA account (possibly other than the sender) was debited too much. First, some helpers for that.
isAllowedToEmptyExec is a trivial wrapper used in execution, where there are NO intermediate transactions between the current transaction and the last known fully executed state.
updateExtraState is the execution-time maintenance of the tiny history we need for emptiness checks and delayed reserve-config changes. It applies all reserve updates emitted during execution (possibly for multiple addresses) only on successful execution; updateExtraStateNoReserve is used on revert, and only promotes already-pending updates (it discards any new reserve updates from the reverted tx).
Definition promotePendingReserveBal (es: ExtraAcState) (n: N) : ExtraAcState :=
  match pendingReserveBal es with
  | Some (v, blk)
      if asbool (blk + K n)%N
      then {|
        lastTxInBlockIndex := lastTxInBlockIndex es;
        lastDelUndelInBlockIndex := lastDelUndelInBlockIndex es;
        settledReserveBal := v;
        pendingReserveBal := None;
      |}
      else es
  | Nonees
  end.

Definition applyReserveBalUpdate (es: ExtraAcState) (n: N) (newRb: N) : ExtraAcState :=
  match pendingReserveBal es with
  | Some (_v, blk)
      
if multiple updates are applied in a single block, the latest wins
      if asbool (blk = n)%N
      then {|
        lastTxInBlockIndex := lastTxInBlockIndex es;
        lastDelUndelInBlockIndex := lastDelUndelInBlockIndex es;
        settledReserveBal := settledReserveBal es;
        pendingReserveBal := Some (newRb, n)
      |}
      else es
  | None
      {|
        lastTxInBlockIndex := lastTxInBlockIndex es;
        lastDelUndelInBlockIndex := lastDelUndelInBlockIndex es;
        settledReserveBal := settledReserveBal es;
        pendingReserveBal := Some (newRb, n)
      |}
  end.

Definition applyReserveBalUpdatesForAddr (es: ExtraAcState) (n: N) (addr: EvmAddr)
           (updates: ReserveBalUpdate) : ExtraAcState :=
  fold_left (fun st '(a, newRb)
               if asbool (a = addr) then applyReserveBalUpdate st n newRb else st)
            updates es.

Definition updateExtraStateBase (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;
         settledReserveBal := settledReserveBal oldes;
         pendingReserveBal := pendingReserveBal oldes
       |}
    ).

Definition updateExtraState (a: ExtraAcStates) (tx: TxWithHdr) (rbUpdate: ReserveBalUpdate) : ExtraAcStates :=
  (fun addr
     let oldes := (updateExtraStateBase a tx) addr in
     let oldes' := promotePendingReserveBal oldes (txBlockNum tx) in
     applyReserveBalUpdatesForAddr oldes' (txBlockNum tx) addr rbUpdate
  ).

Definition updateExtraStateNoReserve (a: ExtraAcStates) (tx: TxWithHdr) : ExtraAcStates :=
  (fun addr
     let oldes := updateExtraStateBase a tx addr in
     promotePendingReserveBal oldes (txBlockNum tx)).


Section EvmCore.

Abstract execution and revert

We postulate a single-step EVM core (evmExecTxCore) that returns the new state, a transaction result (receipt), and the set of changed accounts; and the revert step for failed checks. This keeps the reserve logic orthogonal to the (much larger) EVM semantics. The list (list EvmAddr) returned by evmExecTxCore contains all the changed accounts.

Algorithm 2 (execution): execute a transaction

Next, we define execValidateTx, which assumes that t has already been validated to ensure that the sender has enough balance to cover maxTxFee. It uses a helper allFinalBalSufficient, which we define first.
Execution, as defined by execValidateTx, proceeds as follows:
  • 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). Reserve-balance updates are applied only on success.
allFinalBalSufficient captures the per-account reserve-balance postcondition for a transaction.
Execute a validated tx and return the post-state plus the EVM receipt.
Definition execValidatedTx (s: AugmentedState) (t: TxWithHdr)
  : AugmentedState × TxResult :=
  let '(((postTxState, txr), changedAccounts), rbUpdate) := evmExecTxCore (s.1) t in
  if (allFinalBalSufficient s postTxState changedAccounts t)
  then ((postTxState, updateExtraState s.2 t rbUpdate), txr)
  else
    let (revState, revRes) := revertTx s.1 t in
    ((revState, updateExtraStateNoReserve s.2 t), revRes).

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.
  • 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.
Execution check of tx validity: The implementation checks nonces as well, but here we are only concerned with tx fees.
Top-level execution wrapper that fails fast when validation fails. On success, it returns the post-state and receipt for t. 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 × TxResult) :=
  if (negb (validateTx (s.1) t))
  then None
  else Some (execValidatedTx s t).

execute a list of transactions one by one, returning the final state and receipts. 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 × list TxResult) :=
  match ts with
  | []Some (s, [])
  | t::tls
      match execTx s t with
      | NoneNone
      | Some (si, r)
        match execTxs si tls with
        | Some (sf, rs)Some (sf, r :: rs)
        | NoneNone
        end
      end
  end.

Main correctness theorem

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).1 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.

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:
^ execution cannot abort (indicated by returning None) due to balance being insufficient to even pay fees
     | Some (si, _)
        
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.

Proof

core execution assumptions

To prove the theorem fullBlockStep, we needed to make the following assumptions about how the core EVM execution updates balances and delegated-ness. The names of these assumptions are fairly descriptive.

Class EVMAssupmtions :=
  {
    
balanceOfRevertSender: s tx,
  maxTxFee tx balanceOfAc s (sender tx)
   balanceOfAc (revertTxState s tx) (sender tx)
     = balanceOfAc s (sender tx) - maxTxFee tx;

balanceOfRevertOther: s tx ac,
  ac (sender tx)
   balanceOfAc (revertTxState s tx) ac
     = balanceOfAc s ac;


revertTxDelegationUpdCore: tx s,
  let sf := (revertTxState s tx) in
  ( ac, addrDelegated sf ac =
                (addrDelegated s ac && asbool (ac (undels tx.1.2)))
                || asbool (ac (dels tx.1.2)));

execTxDelegationUpdCore: tx s,
  let sf := (evmExecTxCoreState s tx) in
  ( ac, addrDelegated sf ac =
                (addrDelegated s ac && asbool (ac (undels tx.1.2)))
                || asbool (ac (dels tx.1.2)));

execTxSenderBalCore: tx s,
  maxTxFee tx balanceOfAc s (sender tx)
  let sf := (evmExecTxCoreState s tx) 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.
execTxCannotDebitNonDelegatedNonContractAccountsCore: tx s,
  let sf := (evmExecTxCoreState s tx) in
   ac, ac sender tx
               (addrDelegated sf ac || isSC sf ac) = false
                  balanceOfAc s ac balanceOfAc sf ac;


changedAccountSetSound: tx s,
  let '(((sf, sf_res), changedAccounts), _) := (evmExecTxCore s tx) in
  ( ac, ac changedAccounts sf ac = s ac);
}.

lemmas about execution



This lemma combines the axioms above to build a lower bound of the balance of any account after executing a transaction.

isAllowedToEmpty lemmas

Recall isAllowedToEmpty s (txInterfirst :: rest) txnext determines whether the transaction txnext is allowed to empty its balance, in the setting where s is the last available state and (txInterfirst :: rest) are the transactions between s and txnext.
This lemma proves that to be equivalent to executing txInterfirst at state s and considering the result as the latest available state and thereby removing txInterfirst from intermediates.

lemmas about remainingEffReserveBalOfSender

remainingEffReserveBalOfSender is monotone. proof is straightforward by analyzing cases and using Coq's arith automation lia.
Monotonicity helpers for Z.min and Z.max.
Lemma zmax_mono_l a b c : (a b)%Z (a `max` c) (b `max` c).

Lemma zmin_mono_l a b c : (a b)%Z (a `min` c) (b `min` c).

Lemma zmin_mono_r a b c : (a b)%Z (c `min` a) (c `min` b).

Lemma zle_min a b c : (a b)%Z (a c)%Z (a b `min` c)%Z.

Lemma nmin_le_l a b : N.min a b a.

Lemma nmin_le_r a b : N.min a b b.

Lemma nmin_mono_l a b c : (a b)%N N.min a c N.min b c.

Lemma nle_min a b c : (a b)%N (a c)%N (a N.min b c)%N.

Lemma Z_of_N_min a b :
  Z.of_N (N.min a b) = Z.min (Z.of_N a) (Z.of_N b).

Lemma Z_of_N_sub_le a b :
  (Z.of_N a - Z.of_N b Z.of_N (a - b))%Z.

Lemma nsub_le a b : (a - b a)%N.

Lemma nsub_le_mono_l a b c : (a b)%N (a - c b - c)%N.

Lemma mono s rb1 rb2 inter tx:
  rb1 rb2
   (remainingEffReserveBalOfSender s rb1 inter tx) (remainingEffReserveBalOfSender s rb2 inter tx).

remainingEffReserveBalOfSender never exceeds the sender's balance, assuming prevErb doesn't.
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.1) ac = false)
   (rb1 rb2)
   (rb2 balanceOfAc (execValidatedTx s tx).1.1 (sender txc))
   txBlockNum txc - (K-1) txBlockNum tx txBlockNum txc
   addr : EvmAddr,
      addr eoas
       remainingEffReserveBalOfSender s rb1 (tx :: inter) txc
           remainingEffReserveBalOfSender ((execValidatedTx s tx).1) 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.
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 irb0 := advanceEffReserveBals s (initialEffReserveBals s) (txBlockNum tx) in
  let remf := remainingEffReserveBalOfSender s (irb0 (sender tx)) [] tx in
  let remRbsf := updateKey irb0 (sender tx) (fun _remf) in
  let sf := (execValidatedTx s tx).1 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 irb' := advanceEffReserveBals s irb (txBlockNum txc) in
  let rem := remainingEffReserveBalOfSender s (irb' (sender txc)) inter txc in
  updateKey irb' (sender txc) (fun _rem).

Lemma decreasingRemTxSender s irb proc tx txc:
  let remRbs := remainingEffReserveBals s irb (tx :: proc) txc in
  remRbs (sender tx) irb (sender tx).

lifts the previous lemma from remainingEffReserveBalOfSender to remainingEffReserveBalsL. induction on nextL
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.
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
The above 2 lemmas are used to yield the following:

Proof of main theorem:

Straightforward induction on firstblock, with inductiveStep used in the inductive step.
If an account's balance did not decrease, the reserve-balance check cannot fail for that account. finalBalSufficient preTxState postTxState t a is the condition that is checked at the end of a transaction t for every changed account a. If the check returns false for any changed account, the transaction is reverted: see execValidatedTx above

Consensus checks using finite-width arithmetic (U256).

Below we define remainingEffReserveBalOfSenderF, which is equivalent to remainingEffReserveBalOfSender, but only uses finite-width arithmetic (U256), instead of the unbounded/exact Z, where one does not need to worry about over/underflows.
The main idea is to represent a Z by option U256 where None represents all the negative numbers. All negative values of remaining effective reserve balance are useless anyway. 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.

Section U256Consensus.
  Context (K: N).

  Definition u256_to_Z (u: U256): Z := Z.of_N (u256_to_N u).

below, we define precisely what it means for an o:option U256 is equivalent to a z:Z
  Definition u256z_equiv (o: option U256) (z: Z) : Prop :=
    (z < 2^256)
      match o with
    | None ⇒ (z < 0)
    | Some uz = 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.
  Definition EffReserveBalsF := EvmAddr U256.

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 delayedReserveBalF (s : AugmentedState) (addr : EvmAddr) (n: N) : U256 :=
    u256_of_N (delayedReserveBalOfAddr K s.2 addr n).

  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).

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 rSome (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).

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, NoneSome l
    | None, Some rSome r
    | None, NoneNone
    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)
    | NoneNone
    
^ 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).

We define some notations to make our definition of remainingEffReserveBalOfSenderF look similar
the line below tells Coq to parse x y as u256_opt_sub x y
  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.


Finally, below is the U256 version of remainingEffReserveBalOfSender
^ 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
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)
      ((settledReserveBalOfAddr s.2 addr) < 2^256)
      match pendingReserveBalOfAddr s.2 addr with
      | Some (rb, _)rb < 2^256
      | NoneTrue
      end.

  Definition txReserveUpdateWithinBounds (tx : TxWithHdr) : Prop :=
    maxTxFee tx < 2^256
    value tx < 2^256
    maxStorageFee tx < 2^256.

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

RBTests: small “test case” lemmas by unfolding and rewriting

Section RBTests.
  Context (K: N) (K_pos: (0 < K)%N).

  Variable s0 : AugmentedState.
  Variables A B : EvmAddr.
  Variable tx1 : TxWithHdr.
  Let n1 : N := txBlockNum tx1.
  Let rb10 : N := 10.

Example setup: A is delegated and its settled reserve is 0 in the pre‑state. A can recover by having another EOA B call into A’s delegated code to set the reserve to 10, which yields a reserve‑balance update for A during execution.
  Hypothesis Hdeleg : addrDelegated s0.1 A = true.
  Hypothesis Hsettled0 : settledReserveBal (s0.2 A) = 0.
  Hypothesis Hpend0 : pendingReserveBal (s0.2 A) = None.
  Hypothesis Hsender1 : sender tx1 = B.

  Let rbUpdate1 : ReserveBalUpdate := [(A, rb10)].
  Let es1 : ExtraAcStates := @updateExtraState K s0.2 tx1 rbUpdate1.

In the pre‑state, A’s delayed reserve is 0.
  Lemma RBTest_pre_rb0 :
    delayedReserveBalOfAddr K s0.2 A n1 = 0.

After the sponsor update, A’s delayed reserve becomes 10 at n1+K.
Consensus acceptability for the sponsor tx (sender = B).
Putting the pieces together: although A’s reserve is 0 in the pre‑state, a sponsor tx from B can be accepted by consensus and raise A’s delayed reserve back to 10 after K blocks.
Execution success for a concrete two‑tx suffix: just unfold.
  Variables evmExecTxCore :
    StateOfAccounts TxWithHdr ((StateOfAccounts × TxResult) × (list EvmAddr)) × ReserveBalUpdate.
  Variables revertTx : StateOfAccounts TxWithHdr (StateOfAccounts × TxResult).
  Variables post1 : StateOfAccounts.
  Variable changed1 : list EvmAddr.
  Variable rbUpdate1' : ReserveBalUpdate.
  Variable r1 : TxResult.
  Hypothesis Hcore1 :
    evmExecTxCore s0.1 tx1 = (((post1, r1), changed1), rbUpdate1').
  Hypothesis Hsufficient1 :
    @allFinalBalSufficient K s0 post1 changed1 tx1 = true.

  Lemma RBTest_validateTx :
    validateTx s0.1 tx1 = true.

  Lemma RBTest_execTx_no_revert :
    @execTx K evmExecTxCore revertTx s0 tx1 =
      Some ((post1, @updateExtraState K s0.2 tx1 rbUpdate1'), r1).

  Lemma RBTest_execTxs_one :
    @execTxs K evmExecTxCore revertTx s0 [tx1] =
      Some ((post1, @updateExtraState K s0.2 tx1 rbUpdate1'), [r1]).
End RBTests.

This page has been generated by coqdoc