Coq model of reserve balance and proof of safety
- No need to maintain history of previous 2K transactions. Any *currently* undelegated sender is allowed to empty, even if it sent many other transactions in the same block or was delegated earlier in the same block. There can be many emptying transactions.
- The execution check is *strictly* more liberal, so transactions are less likely to revert. The only execution change is the new (more permissive) definition of isAllowedToEmpty.
- The definitions and proofs are much shorter (< 1/2 the size in Coq).
- Consensus checks are more liberal in some ways and less liberal in others. They are more liberal because several emptying transactions in the same block are allowed, as long as the balance lower-bound estimates allow it. They are less liberal in the sense that they may reject some sequences that the previous design allowed, but those transactions would have reverted during execution anyway unless there was an intervening credit. A concrete example:
Alice: balance = 100, reserve = 5, undelegated
tx1: sent by Alice, fee 6, value 1 (balance ≈ 93 after tx1)
tx2: some tx from Bob that does not change any account's delegation status
tx3: sent by Alice, fee 2, value 91 (needs ≈93 total, fully consuming the budget)
tx4: sent by Alice, fee 1, value 0
Design Overview:
- **Consensus** does a very shallow execution of transactions to only compute 3 pieces of info for each account (the implementation can be sparse):
- whether the account is delegated after the tx
- the current configured reserve balance
- a lower bound on the balance of the account. a tx is accepted if its sender's pre-tx lowerbound is enough to cover the max fee.
- **Execution**: exactly the same as before, except for the simpler (and more liberal) definition of isAllowedToEmpty: execute the core EVM step and then check each changed delegated/non-code account to ensure it did not dip too far into the reserve; otherwise revert.
Examples illustrating the difference
Example 2:
Alice: bal 100, reserve 5, undelegated
tx1: alice, fee 6, value 10
tx2: alice, fee 6, value 10
- consensus: the new design accepts this sequence of 2 txs but the old one rejects tx2 because the estimate of remaining reserve balance after tx1 is less than the fee of 6. Because Alice remains undelegated throughout, its balance lower bound can be more precisely computed by tracking the value transfers, so the new design of consensus checks knows that tx2 has enough balance to cover the fee.
- execution: the new design will not revert tx2 because it is allowed to empty because it is undelegated.
Example 3:
Alice 100, undelegated, reservebal =5
tx1: sender Alice, delegates Alice, value 10, fee 1
tx2: Bob sends to Alice, and a delegated smart-contract execution debits her by 20
tx3: sender Alice, undelegates Alice, value 0, fee 1
tx4: sender Alice, value 10, fee 1
- consensus: both new and old designs will accept. In the new proposal, consensus maintains a lower bound on every account's balance and ensures that the lower bound is enough to cover the fees. Once an account is delegated, its lower bound drops to the min of the previous lower bound and configured reserve balance. After tx4, in the new design, the balance lower bound estimate becomes negative for Alice and thus no more transactions from Alice will be accepted for the next K blocks.
- execution: only the old design will unnecessarily revert tx4. In the new proposal, tx4 will be considered emptying because it is not delegated and will NOT revert.
Main artifacts:
- Consensus check: consensusAcceptableTxs.
- Execution step (with its check): execValidatedTx (called by execTx).
- Main “safety” theorem: fullBlockStep, says that if a sequence of txs is accepted by consensus, the execution of that sequence cannot run into an error due to inability to pay the fee.
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.
match delegatedTo (s a) with
| [] ⇒ false
| _ ⇒ true
end.
Many of the EVM semantics definitions we use come from Yoichi's EVM semantics, developed several years ago. The definition of block.transaction there lacks fields to support newer features like delegation. Also, the last field is to support user-configurable reserve balances in Monad. There is a new transaction type which can update the configured reserve balance of the sender. Such transactions do nothing else.
The fields above should ultimately come from EVM semantics and not here. The fields below are monad-specific.
^ updates the reserve balance of the sender if Some. In that case, the transaction does nothing else, e.g., no smart contract invocation or transfer. It must be None for regular transactions (which do not reconfigure the reserve balance). option N in Coq can be thought of as std::optional<N> in C++. None in Coq corresponds to std::nullopt in C++.
Transactions to uodate reserve balance will be implemented as a call to a special precompile:
A stateful precompile at address 0xRESERVES maintains a mapping from account addresses to their configured
reserve balance values. The precompile exposes an update(uint256) function that allows accounts to set
their reserve balance. Any internal call (from a smart contract) to the update function of 0xRESERVES causes
the transaction to revert. Only EOA accounts may call the precompile’s update function directly.
A transaction t is a reconfiguring transaction if it calls the update(uint256) function of the 0xRESERVES
precompile (inspectable by the to and calldata field). A reconfiguring transaction is required to have value
field set as 0.
Behavior of the precompile: When a reconfiguring transaction is executed, the precompile only updates
the reserve balance mapping for t.sender to the specified value. The precompile performs no other operations:
no balance transfers, no state changes beyond updating the single mapping entry. As a result, a reconfiguring
transaction never reverts. This simplicity is assumed for proving the correctness of the reserve balance
mechanism.
A transaction that reconfigures a user's reserve balance cannot also change any delegation status.
In the implementation, the data-structure design probably will not allow both.
Here, we simply ignore the reserve-balance update of a transaction if the transaction delegates or undelegates.
Definition reserveBalUpdate (t: TxExtra) : option N :=
if isEmpty (dels t ++ undels t) then reserveBalUpdate' t else None.
if isEmpty (dels t ++ undels t) then reserveBalUpdate' t else None.
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.
Definition maxTxFee (t: TxWithHdr) : N :=
((w256_to_N (block.tr_gas_price t.2)) × (w256_to_N (block.tr_gas_limit t.2))).
((w256_to_N (block.tr_gas_price t.2)) × (w256_to_N (block.tr_gas_limit t.2))).
The proofs in this file never unfold the definition of maxTxFee, so nothing will break if this definition is changed.
for any decidable assertion/proposition P, asbool P is a boolean such that (asbool P = true) ↔ P, where
↔ means iff (if and only if)
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
Does tx delegate/undelegate addr?
Definition txDelUndelAddr (addr: EvmAddr) (tx: TxWithHdr) : bool :=
asbool (addr ∈ addrsDelUndelByTx tx).
asbool (addr ∈ addrsDelUndelByTx 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.
The extra (non-EVM) state needed to be maintained by execution to implement reserve balance checks. Note that this had many more (history) fields in the old design
^ current configured reserve balance of the account (defaults to the global reserve if it never reconfigured).
We thread the EVM state with this metadata. This “augmented state” is the
substrate both consensus and execution read from and update.
Whether an address currently has code. excludes cases where the code is just a delegation marker
update the value of the key updKey in oldMap to f (oldMap updKey) (f applied to the old value of the key updKey in the map)
Definition updateKey {T} `{c: EqDecision T} {V} (oldmap: T → V) (updKey: T) (f: V → V) : T → V :=
fun k ⇒ if (asbool (k=updKey)) then f (oldmap updKey) else oldmap k.
Lemma updateKeyLkp3 {T} `{c: EqDecision T} {V} (m: T → V) (a b: T) (f: V → V) :
(updateKey m a f) b = if (asbool (b=a)) then (f (m a)) else m b.
fun k ⇒ if (asbool (k=updKey)) then f (oldmap updKey) else oldmap k.
Lemma updateKeyLkp3 {T} `{c: EqDecision T} {V} (m: T → V) (a b: T) (f: V → V) :
(updateKey m a f) b = if (asbool (b=a)) then (f (m a)) else m b.
Consensus (Algorithm 1)
Pessimistic **lower bound** on the account’s balance:
the account's configured reserve balance:
is the account delegated? :
Shallow execution results of all accounts. It is a pair containing:
- a bool indicating whether the sequence of proposed txs is fee-solvent so far, and
- each account's shallow state after shallow execution.
some more wrappers:
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 shallow execution result, before executing any proposed transactions. s will typically be the last fully executed/finalized state. If an account is not delegated, its balance lower bound is the entire balance in s. Otherwise, we take the minimum of its balance and the configured reserve balance of the account. The fee-solvency bool (first element of the pair) is true because we are starting with an empty proposal.
Definition initialShallowExecResults (s: AugmentedState) : ShallowExecResults :=
(true,
fun addr ⇒
let crb := configuredReserveBalOfAddr s.2 addr in
let del := addrDelegated s.1 addr in
let bal := balanceOfAc s.1 addr in
{|
balanceLb := if del then bal `min` crb else bal;
configuredRB := crb;
delegated := del;
|}).
(true,
fun addr ⇒
let crb := configuredReserveBalOfAddr s.2 addr in
let del := addrDelegated s.1 addr in
let bal := balanceOfAc s.1 addr in
{|
balanceLb := if del then bal `min` crb else bal;
configuredRB := crb;
delegated := del;
|}).
the new delegation status of an account after executing tx
Definition delegatedAfterTx (prevDelegated: bool) (tx: TxWithHdr) (addr: EvmAddr) : bool :=
(prevDelegated && asbool (addr ∉ undels tx.1.2))
|| asbool (addr ∈ dels tx.1.2).
(prevDelegated && asbool (addr ∉ undels tx.1.2))
|| asbool (addr ∈ dels tx.1.2).
Shallow execution of a transaction
- it is monotone (proven in the lemma mono below)
- if the account is delegated, the balance lower bound drops to the min of the previous lower bound and the configured reserve balance
- updates the shallow account state (ShallowExecRes) of not just the sender but also every account that got delegated in candTx
- the fee-solvency bool is true iff the previous result itself was solvent and the previous lower bound on balance was greater than or equal to maxTxFee candTx
Definition shallowExecTx (preRes: ShallowExecResults) (candTx: TxWithHdr)
: ShallowExecResults :=
let feeSolvent : bool :=
let previouslySolvent := preRes.1 in
previouslySolvent && asbool (maxTxFee candTx ≤ balanceLb (preRes.2 (sender candTx))) in
let shallowRes (addr: EvmAddr) :=
let prev := preRes.2 addr in
let newCrb :=
if asbool (sender candTx ≠ addr)
then configuredRB prev
else
match reserveBalUpdateOfTx candTx with
| Some newRb ⇒ newRb
| None ⇒ configuredRB prev
end in
let newDelegated := delegatedAfterTx (delegated prev) candTx addr in
let isNotSender := asbool (sender candTx ≠ addr) in
let startingRb := balanceLb prev `min` newCrb in
{|
balanceLb :=
if newDelegated
then (if isNotSender then startingRb else startingRb - maxTxFee candTx)
else if isNotSender
then balanceLb prev
else balanceLb prev - maxTxFee candTx - value candTx;
configuredRB := newCrb ;
delegated := newDelegated;
|} in
(feeSolvent, shallowRes)
.
: ShallowExecResults :=
let feeSolvent : bool :=
let previouslySolvent := preRes.1 in
previouslySolvent && asbool (maxTxFee candTx ≤ balanceLb (preRes.2 (sender candTx))) in
let shallowRes (addr: EvmAddr) :=
let prev := preRes.2 addr in
let newCrb :=
if asbool (sender candTx ≠ addr)
then configuredRB prev
else
match reserveBalUpdateOfTx candTx with
| Some newRb ⇒ newRb
| None ⇒ configuredRB prev
end in
let newDelegated := delegatedAfterTx (delegated prev) candTx addr in
let isNotSender := asbool (sender candTx ≠ addr) in
let startingRb := balanceLb prev `min` newCrb in
{|
balanceLb :=
if newDelegated
then (if isNotSender then startingRb else startingRb - maxTxFee candTx)
else if isNotSender
then balanceLb prev
else balanceLb prev - maxTxFee candTx - value candTx;
configuredRB := newCrb ;
delegated := newDelegated;
|} in
(feeSolvent, shallowRes)
.
We can fold the function above over the entire list of proposed transactions, chaining the results.
Fixpoint shallowExecTxL (preProposalRes: ShallowExecResults) (proposal: list TxWithHdr)
: ShallowExecResults:=
match proposal with
| [] ⇒ preProposalRes
| htx::tltx ⇒
shallowExecTxL (shallowExecTx preProposalRes htx) tltx
end.
: ShallowExecResults:=
match proposal with
| [] ⇒ preProposalRes
| htx::tltx ⇒
shallowExecTxL (shallowExecTx preProposalRes htx) tltx
end.
Consensus acceptability.
A suffix proposedTxs is **acceptable** if the fee-solvency bool is true in the final result.
Definition consensusAcceptableTxs (latestState : AugmentedState) (proposedTxs: list TxWithHdr) : Prop :=
(shallowExecTxL
(initialShallowExecResults latestState) proposedTxs).1 = true.
(shallowExecTxL
(initialShallowExecResults latestState) proposedTxs).1 = true.
Execution Check (algo 2)
Definition senderDelegatedAfterTx (s: StateOfAccounts) (tx: TxWithHdr) :=
delegatedAfterTx (addrDelegated s (sender tx)) tx (sender tx).
delegatedAfterTx (addrDelegated s (sender tx)) tx (sender tx).
Notice that we did not need any “isAllowedToEmpty” concept in defining consensus checks.
The execution check is the same as in the old design, except that the definition of “is allowed to empty” is radically simplified: it does not need any history of previous transactions and is equivalent to the sender being not delegated *after* executing the transaction.
Definition isAllowedToEmpty
(s : AugmentedState) (t: TxWithHdr) : bool :=
negb (senderDelegatedAfterTx s.1 t).
(s : AugmentedState) (t: TxWithHdr) : bool :=
negb (senderDelegatedAfterTx s.1 t).
Helper to update the extra metadata (just the configured reserve balance of each account).
Definition updateExtraState (a: ExtraAcStates) (tx: TxWithHdr) : ExtraAcStates :=
(fun addr ⇒
let oldes := a addr in
{|
configuredReserveBal:=
if asbool (sender tx = addr)
then
match reserveBalUpdateOfTx tx with
| Some newRb ⇒ newRb
| None ⇒ configuredReserveBal oldes
end
else configuredReserveBal oldes
|}
).
(fun addr ⇒
let oldes := a addr in
{|
configuredReserveBal:=
if asbool (sender tx = addr)
then
match reserveBalUpdateOfTx tx with
| Some newRb ⇒ newRb
| None ⇒ configuredReserveBal oldes
end
else configuredReserveBal oldes
|}
).
Abstract execution and revert
Axiom evmExecTxCore : StateOfAccounts → TxWithHdr → StateOfAccounts × (list EvmAddr) .
Axiom revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts.
Axiom revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts.
Algorithm 2 (execution): execute a transaction
- Special “reserve update” tx: pay fee; set new configured reserve.
- Otherwise, run the core EVM step to obtain the *actual* post state.
- For *changed* accounts, allFinalBalSufficient checks that the account was not debited too much, which may endanger the fee solvency of the later transactions already accepted by consensus. Note that the checks are trivial for accounts that have code or are not delegated after executing t.
- If any check fails, revert the tx.
Definition allFinalBalSufficient (preTxState: AugmentedState) (postTxState : StateOfAccounts) (changedAccounts: list EvmAddr) (t: TxWithHdr): bool:=
let finalBalSufficient (a: EvmAddr) :=
let ReserveBal := configuredReserveBalOfAddr preTxState.2 a in
let erb:N := ReserveBal `min` (balanceOfAc preTxState.1 a) in
if isSC postTxState a
then true
else
if asbool (sender t = a)
then if isAllowedToEmpty preTxState t
then true
else asbool ((erb - maxTxFee t) ≤ balanceOfAc postTxState a)
else asbool (erb ≤ balanceOfAc postTxState a) in
(forallb finalBalSufficient changedAccounts).
Definition execValidatedTx (s: AugmentedState) (t: TxWithHdr)
: AugmentedState :=
match reserveBalUpdateOfTx t with
| Some n ⇒ (updateBalanceOfAc s.1 (sender t) (fun b ⇒ b - maxTxFee t)
, updateExtraState s.2 t)
| None ⇒
let (postTxState, changedAccounts) := evmExecTxCore (fst s) t in
if (allFinalBalSufficient s postTxState changedAccounts t)
then (postTxState, updateExtraState s.2 t)
else (revertTx s.1 t, updateExtraState s.2 t)
end.
Note that because the isSC check is done on si (the result of running the EVM blackbox to execute t), not s (the pre-exec state), the following scenario is allowed.
Execution check of tx validity:
The implementation checks nonces as well, but here we are only concerned with tx fees.
- Before the tx, Alice sends money to some address addr2 (computed using keccak). Alice is EOA.
- Alice sends tx foo to a smart contract address addr.
- foo execution deploys code at addr2 and calls it, and the call empties addr2.
Definition validateTx (preTxState: StateOfAccounts) (tx: TxWithHdr): bool :=
asbool (maxTxFee tx ≤ balanceOfAc preTxState (sender tx)).
asbool (maxTxFee tx ≤ balanceOfAc preTxState (sender tx)).
Top-level execution wrapper that fails fast when validation fails.
None means the execution of the whole block containing t aborts, which is what the consensus/execution checks must guarantee to never happen.
Definition execTx (s: AugmentedState) (t: TxWithHdr): option (AugmentedState) :=
if (negb (validateTx (fst s) t)) then None
else Some (execValidatedTx s t).
if (negb (validateTx (fst s) t)) then None
else Some (execValidatedTx s t).
execute a list of transactions one by one. Note that if the execution of any tx returns None (balance insufficient to cover fees), the entire execution (of the whole list of txs) returns None.
Fixpoint execTxs (s: AugmentedState) (ts: list TxWithHdr): option AugmentedState :=
match ts with
| [] ⇒ Some s
| t::tls ⇒
match execTx s t with
| Some si ⇒ execTxs si tls
| None ⇒ None
end
end.
match ts with
| [] ⇒ Some s
| t::tls ⇒
match execTx s t with
| Some si ⇒ execTxs si tls
| None ⇒ None
end
end.
Cryptographic difficulty assumption:
Definition txCannotCreateContractAtAddrs tx (eoasWithPrivateKey: list EvmAddr) :=
∀ s, let sf := (execValidatedTx s tx) in
∀ addr, addr ∈ eoasWithPrivateKey → isSC s.1 addr = false → isSC sf.1 addr = false.
∀ s, let sf := (execValidatedTx s tx) in
∀ addr, addr ∈ eoasWithPrivateKey → isSC s.1 addr = false → isSC sf.1 addr = false.
The lemma below is probably what one would come up first as the main correctness theorem.
blocks represents the transactions in the blocks proposed after latestState.
It says that consensus checks (consensusAcceptableTxs latestState blocks) implies
that the execution of all transactions blocks one by one, starting from the state latestState will succeed and not abort (None) due to preTx balance being less than maxTxFee.
Theorem fullBlockStep2 (latestState : AugmentedState) (blocks: list TxWithHdr) :
(∀ ac, ac ∈ (map sender blocks) → isSC latestState.1 ac = false)
→ (∀ txext, txext ∈ blocks → txCannotCreateContractAtAddrs txext (map sender blocks))
→ 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))
→ 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) :
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
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 because a head tx lacks enough fee balance.
^ Moreover, the tail remains fee‑solvent (and keeps the hygiene side
conditions), enabling induction over blocks.
consensusAcceptableTxs si restblocks
∧ (∀ ac, ac ∈ (map sender restblocks) → isSC si.1 ac = false)
∧ (∀ txext, txext ∈ (restblocks) → txCannotCreateContractAtAddrs txext (map sender (restblocks)))
end.
∧ (∀ ac, ac ∈ (map sender restblocks) → isSC si.1 ac = false)
∧ (∀ txext, txext ∈ (restblocks) → txCannotCreateContractAtAddrs txext (map sender (restblocks)))
end.
Core execution assumptions
To prove the theorem fullBlockStep, we need to make some assumptions about how the core EVM execution updates balances and delegated-ness. The names of these axioms are fairly descriptive.Axiom balanceOfRevertSender: ∀ s tx,
maxTxFee tx ≤ balanceOfAc s (sender tx)
→ reserveBalUpdateOfTx tx = None
→ balanceOfAc (revertTx s tx) (sender tx)
= balanceOfAc s (sender tx) - maxTxFee tx.
Axiom balanceOfRevertOther: ∀ s tx ac,
reserveBalUpdateOfTx tx = None
→ ac ≠ (sender tx)
→ balanceOfAc (revertTx s tx) ac
= balanceOfAc s ac.
Axiom revertTxDelegationUpdCore: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let sf := (revertTx s tx) in
(∀ ac, addrDelegated sf ac =
(addrDelegated s ac && asbool (ac ∉ (undels tx.1.2)))
|| asbool (ac ∈ (dels tx.1.2))).
Axiom execTxDelegationUpdCore: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
(∀ ac, addrDelegated sf ac =
(addrDelegated s ac && asbool (ac ∉ (undels tx.1.2)))
|| asbool (ac ∈ (dels tx.1.2))).
Axiom execTxSenderBalCore: ∀ tx s,
maxTxFee tx ≤ balanceOfAc s (sender tx) →
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
senderDelegatedAfterTx s tx = false
→ balanceOfAc sf (sender tx) = balanceOfAc s (sender tx) - ( maxTxFee tx + value tx)
∨ balanceOfAc sf (sender tx) = balanceOfAc s (sender tx) - (maxTxFee tx).
One caveat in the assumption below is that it assumes that the account ac does not receive so much credit that it overflows 2^256. In practice, this should never happen, assuming the ETH supply is well below 2^256. Thus, we can assume that evmExecTxCore caps the balance at 2^256 should it overflow, instead of wrapping around, which may violate this assumption.
Axiom execTxCannotDebitNonDelegatedNonContractAccountsCore: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
∀ ac, ac ≠ sender tx
→ (addrDelegated sf ac || isSC sf ac) = false
→ balanceOfAc s ac ≤ balanceOfAc sf ac.
Axiom changedAccountSetSound: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let (sf, changedAccounts) := (evmExecTxCore s tx) in
(∀ ac, ac ∉ changedAccounts → sf ac = s ac).
reserveBalUpdateOfTx tx = None →
let sf := (evmExecTxCore s tx).1 in
∀ ac, ac ≠ sender tx
→ (addrDelegated sf ac || isSC sf ac) = false
→ balanceOfAc s ac ≤ balanceOfAc sf ac.
Axiom changedAccountSetSound: ∀ tx s,
reserveBalUpdateOfTx tx = None →
let (sf, changedAccounts) := (evmExecTxCore s tx) in
(∀ ac, ac ∉ changedAccounts → sf ac = s ac).
This lemma combines many of the execution lemmas above to build a
lower bound of the balance of any account after executing a transaction.
Lemma execBalLb ac s tx:
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
let sf := (execValidatedTx s tx) in
let ReserveBal := configuredReserveBalOfAddr s.2 ac in
if (asbool (ac=sender tx)) then
isSC sf.1 (sender tx) = false→
(if isAllowedToEmpty s tx
then balanceOfAcA sf (sender tx) = balanceOfAcA s (sender tx) - ( maxTxFee tx + value 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 addrDelegated (fst sf) ac then ReserveBal `min` (balanceOfAcA s ac) else balanceOfAcA s ac)
≤ (balanceOfAcA sf ac).
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
let sf := (execValidatedTx s tx) in
let ReserveBal := configuredReserveBalOfAddr s.2 ac in
if (asbool (ac=sender tx)) then
isSC sf.1 (sender tx) = false→
(if isAllowedToEmpty s tx
then balanceOfAcA sf (sender tx) = balanceOfAcA s (sender tx) - ( maxTxFee tx + value 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 addrDelegated (fst sf) ac then ReserveBal `min` (balanceOfAcA s ac) else balanceOfAcA s ac)
≤ (balanceOfAcA sf ac).
Order and monotonicity for shallowExecTx
Definition sresLe (r1 r2: ShallowExecRes) :=
delegated r1 = delegated r2
∧ balanceLb r1 ≤ balanceLb r2
∧ configuredRB r1 = configuredRB r2.
delegated r1 = delegated r2
∧ balanceLb r1 ≤ balanceLb r2
∧ configuredRB r1 = configuredRB r2.
Now we lift the above pointwise order to all accounts to define a “≤” relation on full results (of all accounts). Recall that the results are a pair where the first component is the fee-solvency boolean and the second component is the ShallowExecRes of each account. We only enforce the ≼ relation for the accounts that are in a given set of EOAs. eoas will later be instantiated to the list of all senders in the proposed sequence of transactions.
Definition resLe (eoas: list EvmAddr) (rb1 rb2: ShallowExecResults) :=
(rb1.1 = true → rb2.1= true) ∧
∀ addr, addr ∈ eoas → (rb1.2 addr) ≼ (rb2.2 addr).
(rb1.1 = true → rb2.1= true) ∧
∀ addr, addr ∈ eoas → (rb1.2 addr) ≼ (rb2.2 addr).
Reasoning principles for the shallow execution
- **Monotonicity.** If you start from a pointwise larger shallow map, one
shallow step (and therefore the whole fold) keeps you pointwise larger.
This is the heart of the “execute head, refine the budget, and the tail
still passes” argument.
- **Under‑approximation.** A single shallow step *under‑approximates* the effect of the real execution step on the quantities we guard in the execution check. This bridges Algorithm 1 (consensus) and 2 (execution).
One‑step monotonicity for the shallow execution of a transaction. Proof by unfolding definitions, case analysis, and arithmetic reasoning
Lemma mono (eoas: list EvmAddr) (rb1 rb2: ShallowExecResults) (tx: TxWithHdr) :
sender tx ∈ eoas
→ resLe eoas rb1 rb2
→ resLe eoas (shallowExecTx rb1 tx) (shallowExecTx rb2 tx).
sender tx ∈ eoas
→ resLe eoas rb1 rb2
→ resLe eoas (shallowExecTx rb1 tx) (shallowExecTx rb2 tx).
monotonicity of shallow executing a list of transactions. proof by induction on extension, using mono.
Lemma monoL (eoas: list EvmAddr) (rb1 rb2: ShallowExecResults) (extension: list TxWithHdr):
map sender extension ⊆ eoas
→ resLe eoas rb1 rb2
→ resLe eoas (shallowExecTxL rb1 extension)
(shallowExecTxL rb2 extension).
map sender extension ⊆ eoas
→ resLe eoas rb1 rb2
→ resLe eoas (shallowExecTxL rb1 extension)
(shallowExecTxL rb2 extension).
Under‑approximation for one head step.**
Lemma exec1 (tx: TxWithHdr) (extension: list TxWithHdr) (s: AugmentedState) :
let sf := (execValidatedTx s tx) in
maxTxFee tx ≤ balanceOfAc s.1 (sender tx)
→ (∀ ac : EvmAddr, ac ∈ sender tx :: map sender extension → isSC sf.1 ac = false)
→ resLe (map sender (tx::extension))
(shallowExecTx (initialShallowExecResults s) tx)
(initialShallowExecResults sf).
let sf := (execValidatedTx s tx) in
maxTxFee tx ≤ balanceOfAc s.1 (sender tx)
→ (∀ ac : EvmAddr, ac ∈ sender tx :: map sender extension → isSC sf.1 ac = false)
→ resLe (map sender (tx::extension))
(shallowExecTx (initialShallowExecResults s) tx)
(initialShallowExecResults sf).
From one step to many
Lemma decreasingRemTxSender (irb: ShallowExecResults) (txc: TxWithHdr):
(shallowExecTx irb txc).1 = true
→ irb.1 = true.
Lemma decreasingRemL irb (nextL: list TxWithHdr):
(shallowExecTxL irb nextL).1 = true
→ irb.1 = true.
(shallowExecTx irb txc).1 = true
→ irb.1 = true.
Lemma decreasingRemL irb (nextL: list TxWithHdr):
(shallowExecTxL irb nextL).1 = true
→ irb.1 = true.
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 monoL
Lemma execPreservesConsensusChecks tx extension s:
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
(∀ txext, txext ∈ tx::extension → txCannotCreateContractAtAddrs txext (map sender (tx::extension)))
→ (∀ ac, ac ∈ (map sender (tx::extension)) → isSC s.1 ac = false)
→ consensusAcceptableTxs s (tx::extension)
→ consensusAcceptableTxs (execValidatedTx s tx) extension.
maxTxFee tx ≤ balanceOfAc s.1 (sender tx) →
(∀ txext, txext ∈ tx::extension → txCannotCreateContractAtAddrs txext (map sender (tx::extension)))
→ (∀ ac, ac ∈ (map sender (tx::extension)) → isSC s.1 ac = false)
→ consensusAcceptableTxs s (tx::extension)
→ consensusAcceptableTxs (execValidatedTx s tx) extension.
The two lemmas above combine to yield the following:
Lemma inductiveStep (latestState : AugmentedState) (tx: TxWithHdr) (extension: list TxWithHdr) :
maxTxFee tx ≤ balanceOfAc latestState.1 (sender tx)
→ (∀ 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.
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 ∈ 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.
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) :
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
∧ (∀ ac, ac ∈ (map sender restblocks) → isSC si.1 ac = false)
∧ (∀ txext, txext ∈ (restblocks) → txCannotCreateContractAtAddrs txext (map sender (restblocks)))
end.
Print Assumptions fullBlockStep.
All assumptions of the proof:
Section Variables:
K
: N
Axioms:
revertTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := revertTx s tx) (ac : EvmAddr),
addrDelegated sf ac =
addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts
execTxSenderBalCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ let sf := (evmExecTxCore s tx).1 in
senderDelegatedAfterTx s tx = false
→ balanceOfAc sf (sender tx) = (balanceOfAc s (sender tx) - (maxTxFee tx + value tx))%N
∨ balanceOfAc sf (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
execTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
addrDelegated sf ac =
addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
ac ≠ sender tx
→ addrDelegated sf ac || isSC sf ac = false
→ (balanceOfAc s ac ≤ balanceOfAc sf ac)%N
evmExecTxCore : StateOfAccounts → TxWithHdr → StateOfAccounts × list EvmAddr
changedAccountSetSound :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ let (sf, changedAccounts) := evmExecTxCore s tx in
∀ ac : EvmAddr, ac ∉ changedAccounts → sf ac = s ac
balanceOfRevertSender :
∀ (s : StateOfAccounts) (tx : TxWithHdr),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ balanceOfAc (revertTx s tx) (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
balanceOfRevertOther :
∀ (s : StateOfAccounts) (tx : TxWithHdr) (ac : EvmAddr),
reserveBalUpdateOfTx tx = None
→ ac ≠ sender tx → balanceOfAc (revertTx s tx) ac = balanceOfAc s ac
Section Variables:
K
: N
Axioms:
revertTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := revertTx s tx) (ac : EvmAddr),
addrDelegated sf ac =
addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
revertTx : StateOfAccounts → TxWithHdr → StateOfAccounts
execTxSenderBalCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ let sf := (evmExecTxCore s tx).1 in
senderDelegatedAfterTx s tx = false
→ balanceOfAc sf (sender tx) = (balanceOfAc s (sender tx) - (maxTxFee tx + value tx))%N
∨ balanceOfAc sf (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
execTxDelegationUpdCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
addrDelegated sf ac =
addrDelegated s ac && asbool (ac ∉ undels tx.1.2) || asbool (ac ∈ dels tx.1.2)
execTxCannotDebitNonDelegatedNonContractAccountsCore :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ ∀ (sf := (evmExecTxCore s tx).1) (ac : EvmAddr),
ac ≠ sender tx
→ addrDelegated sf ac || isSC sf ac = false
→ (balanceOfAc s ac ≤ balanceOfAc sf ac)%N
evmExecTxCore : StateOfAccounts → TxWithHdr → StateOfAccounts × list EvmAddr
changedAccountSetSound :
∀ (tx : TxWithHdr) (s : StateOfAccounts),
reserveBalUpdateOfTx tx = None
→ let (sf, changedAccounts) := evmExecTxCore s tx in
∀ ac : EvmAddr, ac ∉ changedAccounts → sf ac = s ac
balanceOfRevertSender :
∀ (s : StateOfAccounts) (tx : TxWithHdr),
(maxTxFee tx ≤ balanceOfAc s (sender tx))%N
→ reserveBalUpdateOfTx tx = None
→ balanceOfAc (revertTx s tx) (sender tx) = (balanceOfAc s (sender tx) - maxTxFee tx)%N
balanceOfRevertOther :
∀ (s : StateOfAccounts) (tx : TxWithHdr) (ac : EvmAddr),
reserveBalUpdateOfTx tx = None
→ ac ≠ sender tx → balanceOfAc (revertTx s tx) ac = balanceOfAc s ac
Corollary fullBlockStep2 (latestState : AugmentedState) (blocks: list TxWithHdr) :
(∀ ac, ac ∈ (map sender (blocks)) → isSC latestState.1 ac = false)
→ (∀ txext, txext ∈ (blocks) → txCannotCreateContractAtAddrs txext (map sender (blocks)))
→ consensusAcceptableTxs latestState (blocks)
→ match execTxs latestState blocks with
| None ⇒ False
| Some si ⇒ True
end.
Lemma acceptableNil lastConsensedState:
consensusAcceptableTxs lastConsensedState [].
This page has been generated by coqdoc