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
| [] ⇒ false
| _::_ ⇒ true
end.
Record EIP7702Authority := {
del_from: option EvmAddr;
match delegatedTo (s a) with
| [] ⇒ false
| _::_ ⇒ true
end.
Record EIP7702Authority := {
del_from: option EvmAddr;
^ None means the signature was invalid
^ 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.
The fields above should ultimately come from EVM semantics. The fields below are monad-specific.
^ index of this transaction in the block. monaddb stores when (blockindex, txindex) an account was born
^ 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).
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 a ⇒ match del_from a with
| Some addr ⇒ if is_undelegation a then [] else [addr]
| None ⇒ []
end ) (authorities txe).
flat_map (fun a ⇒ match del_from a with
| Some addr ⇒ if 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 a ⇒ match del_from a with
| Some addr ⇒ if is_undelegation a then [addr] else []
| None ⇒ []
end ) (authorities txe).
#[global] Opaque dels undels.
Definition Transaction: Type := block.transaction × TxExtra.
flat_map (fun a ⇒ match del_from a with
| Some addr ⇒ if 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.
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))).
((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
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.
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
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.
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.
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))))
| None ⇒ false
end.
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))))
| None ⇒ false
end.
in state s, is the account at address addr a smart contract?
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).
fun addr ⇒ (balanceOfAc s.1 addr `min` configuredReserveBalOfAddr s.2 addr).
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.
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 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.
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.
: 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
|}
).
Section EvmCore.
Abstract execution and revert
Record EvmExecResult := {
postState : StateOfAccounts;
receipt : TxResult;
changedAccounts : list EvmAddr;
Accounts that executed non-delegation code in their own context.
codeExecAccounts : list EvmAddr;
}.
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 := [] |}.
}.
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
- 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).
Definition finalBalSufficient (preTxState: AugmentedState) (postTxState : StateOfAccounts)
(t: TxWithHdr) (a: EvmAddr): bool :=
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 then true else asbool ((erb - maxTxFee t) ≤ balanceOfAc postTxState a)
else asbool (erb ≤ balanceOfAc postTxState a).
Definition allFinalBalSufficient (preTxState: AugmentedState) (postTxState : StateOfAccounts)
(changedAccounts: list EvmAddr) (t: TxWithHdr): bool:=
forallb (finalBalSufficient preTxState postTxState t) changedAccounts.
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 b ⇒ b - 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.
: AugmentedState × TxResult :=
match reserveBalUpdateOfTx t with
| Some n ⇒
((updateBalanceOfAc s.1 (sender t) (fun b ⇒ b - 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.
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.
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).
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
| None ⇒ None
| Some (si, r) ⇒
match execTxs si tls with
| Some (sf, rs) ⇒ Some (sf, r :: rs)
| None ⇒ None
end
end
end.
match ts with
| [] ⇒ Some (s, [])
| t::tls ⇒
match execTx s t with
| None ⇒ None
| Some (si, r) ⇒
match execTxs si tls with
| Some (sf, rs) ⇒ Some (sf, r :: rs)
| None ⇒ None
end
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 execCodeAccounts (s: StateOfAccounts) (tx: TxWithHdr) : list EvmAddr :=
match reserveBalUpdateOfTx tx with
| Some _ ⇒ []
| None ⇒ codeExecAccounts (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.
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 _ ⇒ []
| None ⇒ codeExecAccounts (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.
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 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 changedAccounts := changedAccounts r in
(∀ ac, ac ∉ changedAccounts → sf ac = s ac);
}.
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 changedAccounts := changedAccounts r 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).1 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 ReserveBal `min` (balanceOfAcA s (sender tx)) - maxTxFee tx ≤ (balanceOfAcA sf (sender tx)))
else
if (isSC sf.1 ac)
then True
else if (asbool (ac ∈ execCodeAccounts s.1 tx))
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).1 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 ReserveBal `min` (balanceOfAcA s (sender tx)) - maxTxFee tx ≤ (balanceOfAcA sf (sender tx)))
else
if (isSC sf.1 ac)
then True
else if (asbool (ac ∈ execCodeAccounts s.1 tx))
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).1 in
txBlockNum txnext - (K-1) ≤ txBlockNum txInterfirst ≤ txBlockNum txnext
→ isAllowedToEmpty s (txInterfirst :: rest) txnext = isAllowedToEmpty 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 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.
(∀ 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.
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 ∈ eoas → isSC s.1 ac = false)
→ txCannotCreateContractAtAddrs tx eoas
→ rbLe eoas (remainingEffReserveBalsL s rb1 (tx::inter) extension)
(remainingEffReserveBalsL ((execValidatedTx s tx).1) 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).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).
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
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).1) 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).1) 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 := revertTxState 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 × TxResult
maxStorageFee : TxWithHdr → N
execTxSenderBalCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
(maxTxFee tx ≤ balanceOfAc 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 (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1.1) (ac : EvmAddr),
ac ≠ sender tx → addrDelegated sf ac || isSC sf ac = false → (balanceOfAc s ac ≤ balanceOfAc sf ac)%N
evmExecTxCore : StateOfAccounts → TxWithHdr → (StateOfAccounts × TxResult) × list EvmAddr
changedAccountSetSound :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ let '((sf, sf_res), 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 (revertTxState 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 (revertTxState s tx) ac = balanceOfAc s ac
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 (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts × TxResult
maxStorageFee : TxWithHdr → N
execTxSenderBalCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
(maxTxFee tx ≤ balanceOfAc 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 (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1.1) (ac : EvmAddr),
ac ≠ sender tx → addrDelegated sf ac || isSC sf ac = false → (balanceOfAc s ac ≤ balanceOfAc sf ac)%N
evmExecTxCore : StateOfAccounts → TxWithHdr → (StateOfAccounts × TxResult) × list EvmAddr
changedAccountSetSound :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ let '((sf, sf_res), 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 (revertTxState 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 (revertTxState 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 [].
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 finalBalSufficient_if_balance_not_decreased:
∀ preTxState postTxState t a,
balanceOfAc preTxState.1 a ≤ balanceOfAc postTxState a
→ finalBalSufficient preTxState postTxState t a = true.
End EvmCore.
End K.
∀ preTxState postTxState t a,
balanceOfAc preTxState.1 a ≤ balanceOfAc postTxState a
→ finalBalSufficient preTxState postTxState t a = true.
End EvmCore.
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 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 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 Some prevErb ⊖ (maxTxFeeF candidateTx)
end.
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)
| 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