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).
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. 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.
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
    
    reserveBalUpdate: option N
    
^ 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.
  }.

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.

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

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.

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.
Definition reserveBalUpdateOfTx (t: TxWithHdr) : option N :=
  reserveBalUpdate t.1.2.

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.
    configuredReserveBal: N;
    
^ 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
  }.


Definition ExtraAcStates := (EvmAddr ExtraAcState).

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) (txBlockNum: N) (addr: EvmAddr) : bool :=
  let startIndex := txBlockNum - (K-1) in
  match proj (state addr) with
  | Some index
      asbool (startIndex index txBlockNum)
  | 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) (txBlockNum: N) (addr: EvmAddr) : bool :=
  indexWithinK lastDelUndelInBlockIndex (state.2) txBlockNum addr.

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

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.


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

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

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

balance of an account in a given augmented state
Definition balanceOfAcA (s: AugmentedState) (ac: EvmAddr) := balanceOfAc s.1 ac.

initial effective reserve balances, before any proposed tx
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 check: so the Rust implementation can optimize it by instead just maintaining the 2 pieces of info isAllowedToEmpty needs from intermediates: the set of senders, the set of addrs that are delegated or undelegated in those txs.
regular tx, not one that sets reserve balance:
      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 (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 check: so the Rust implementation can optimize it by instead just maintaining the 2 pieces of info isAllowedToEmpty needs from intermediates: the set of senders, the set of addrs that are delegated or undelegated in those txs.
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.

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.
The per-account exemption used by execution is isAllowedToEmptyG with an empty intermediates list.
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 newRbnewRb
             | NoneconfiguredReserveBal oldes
             end
           else configuredReserveBal oldes
       |}
    ).


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.

Record EvmExecResult := {
  postState : StateOfAccounts;
  receipt : TxResult;
  changedAccounts : list EvmAddr;
  codeExecAccounts : list EvmAddr;
  
^ accounts that executed code, possibly as delegated EOAs
}.

Variable evmExecTxCore : StateOfAccounts TxWithHdr EvmExecResult.
Variable revertTx : StateOfAccounts TxWithHdr (StateOfAccounts × TxResult).

Definition revertTxState (s: StateOfAccounts) (t: TxWithHdr) : StateOfAccounts :=
  (revertTx s t).1.

Definition emptyTxResult : TxResult :=
  {| gas_used := 0; gas_refund := 0; logs := [] |}.

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:
  • 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).
this is a slight generalization of isAllowedToEmpty where we can talk about an arbitrary address rather than the sender of the transaction
allFinalBalSufficient captures the per-account reserve-balance postcondition for a transaction. In this variant, the “emptying” exemption is checked per account using isAllowedToEmptyG (not just for the sender).
A sender-only variant of the execution check, matching the earlier design: the emptying exemption is only applied to the sender.
Execute a validated tx and return the post-state plus the EVM receipt.
Definition execValidatedTx (s: AugmentedState) (t: TxWithHdr)
  : AugmentedState × TxResult :=
  match reserveBalUpdateOfTx t with
  | Some n
      ((updateBalanceOfAc s.1 (sender t) (fun bb - maxTxFee t),
        updateExtraState s.2 t), emptyTxResult)
  | None
      let r := evmExecTxCore (s.1) t in
      let postTxState := postState r in
      let txr := receipt r in
      let changedAccounts := changedAccounts r in
      if (allFinalBalSufficient s postTxState changedAccounts t)
      then ((postTxState, updateExtraState s.2 t), txr)
      else
        let (revState, revRes) := revertTx s.1 t in
        ((revState, updateExtraState s.2 t), revRes)
  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.
  • 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 execCodeAccounts (s: StateOfAccounts) (tx: TxWithHdr) : list EvmAddr :=
  match reserveBalUpdateOfTx tx with
  | Some _[]
  | NonecodeExecAccounts (evmExecTxCore s tx)
  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 addr execCodeAccounts s.1 tx.

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)
   reserveBalUpdateOfTx tx = None
   balanceOfAc (revertTxState s tx) (sender tx)
     = balanceOfAc s (sender tx) - maxTxFee tx;

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


revertTxDelegationUpdCore: tx s,
  reserveBalUpdateOfTx tx = None
  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,
  reserveBalUpdateOfTx tx = None
  let sf := (postState (evmExecTxCore 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)
  reserveBalUpdateOfTx tx = None
  let sf := (postState (evmExecTxCore 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,
  reserveBalUpdateOfTx tx = None
  let r := evmExecTxCore s tx in
  let sf := postState r in
  let execAccounts := codeExecAccounts r in
   ac, ac sender tx
               (addrDelegated sf ac || isSC sf ac || asbool (ac execAccounts)) = false
                  balanceOfAc s ac balanceOfAc sf ac;


changedAccountSetSound: tx s,
  reserveBalUpdateOfTx tx = None
  let r := (evmExecTxCore s tx) in
  let sf := postState r in
  let sf_res := receipt r in
  let changedAccounts := changedAccounts r 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.
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 s (eoas: list EvmAddr) rb1 rb2 inter:
  ( ac : EvmAddr, ac eoas isSC s.1 ac = false)
   txCannotCreateContractAtAddrs tx eoas
   (rb1 rb2)
   txBlockNum txc - (K-1) txBlockNum tx txBlockNum txc
   sender txc eoas
   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 remf := remainingEffReserveBalOfSender s (initialEffReserveBals s (sender tx)) [] tx in
  let remRbsf := updateKey (initialEffReserveBals s) (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)
   ( ac : EvmAddr, ac sender tx :: map sender extension ac execCodeAccounts s.1 tx)
      ( 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
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.
All assumptions of the proof:
Section Variables:
K
: N
Axioms:
revertTxDelegationUpdCore :
  ∀ (tx : TxWithHdr) (s : StateOfAccounts),
    reserveBalUpdateOfTx tx = None
    → ∀ (sf := revertTxState s tx) (ac : EvmAddr),
        addrDelegated sf ac = addrDelegated s ac && asbool (acundels tx.1.2) || asbool (acdels tx.1.2)
revertTx : StateOfAccountsTxWithHdrStateOfAccounts × TxResult
maxStorageFee : TxWithHdrN
execTxSenderBalCore :
  ∀ (tx : TxWithHdr) (s : StateOfAccounts),
    (maxTxFee txbalanceOfAc s (sender tx))%N
    → reserveBalUpdateOfTx tx = None
      → let sf := (evmExecTxCore s tx).1.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.1) (ac : EvmAddr),
        addrDelegated sf ac = addrDelegated s ac && asbool (acundels tx.1.2) || asbool (acdels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
  ∀ (tx : TxWithHdr) (s : StateOfAccounts),
    reserveBalUpdateOfTx tx = None
    → ∀ (sf := (evmExecTxCore s tx).1.1) (ac : EvmAddr),
        acsender txaddrDelegated sf ac || isSC sf ac = false → (balanceOfAc s acbalanceOfAc sf ac)%N
evmExecTxCore : StateOfAccountsTxWithHdr → (StateOfAccounts × TxResult) × list EvmAddr
changedAccountSetSound :
  ∀ (tx : TxWithHdr) (s : StateOfAccounts),
    reserveBalUpdateOfTx tx = None
    → let '((sf, sf_res), changedAccounts) := evmExecTxCore s tx inac : EvmAddr, acchangedAccountssf ac = s ac
balanceOfRevertSender :
  ∀ (s : StateOfAccounts) (tx : TxWithHdr),
    (maxTxFee txbalanceOfAc s (sender tx))%N
    → reserveBalUpdateOfTx tx = None
      → balanceOfAc (revertTxState s tx) (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
balanceOfRevertOther :
  ∀ (s : StateOfAccounts) (tx : TxWithHdr) (ac : EvmAddr),
    reserveBalUpdateOfTx tx = Noneacsender txbalanceOfAc (revertTxState s tx) ac = balanceOfAc s ac
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

Lemma: execution check equivalence for non‑sender EOAs

finalBalSufficient is the per‑account execution check used by the design in this file: it exempts an account a if isAllowedToEmptyG preTxState [] t a holds, even when a is not the sender. This is the “more permissive” change discussed in the thread (needed for the CREATE+SELFDESTRUCT‑in‑constructor case).
finalBalSufficientOld is the earlier, sender‑only version: the emptying exemption is only applied when a = sender t.
For any account addr, unless it ran code as a non-delegated account, the checks are equivalent: return the same boolean result, as proved in the lemma below:

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 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 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
          if asbool (maxTxFee candidateTx balanceOfAc (preIntermediatesState.1) senderAddr)
          then newBal `mino` configuredRb
          else None
        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)
    | NoneTrue
    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

This page has been generated by coqdoc