State: the transaction lifecycle¶
Journal checkpoints, rollback, per-transaction reset, and transaction-end merge into the block layer.
function k_journal_checkpoint¶
Appends a frame marker to the state journal. The suspended frame stores its refund counter separately.
function k_journal_checkpoint() -> unit = state_journal_checkpoint()Appends a JournalFrameCheckpointed entry.
val state_journal_checkpoint = impure { c: "state_journal_checkpoint" } : unit -> unitfunction k_set_header¶
Installs the block header.
function k_set_header(h : BlockHeader) -> unit = k_header = hThe executing payload's header.
register k_header : BlockHeader =
struct {
number = 0,
timestamp = 0,
extra_data = EMPTY_STATELESS_INPUT_SLICE,
gas_limit = 0,
gas_used = 0,
prev_randao = ZERO_WORD,
base_fee = ZERO_WORD,
blob_gas_used = 0,
excess_blob_gas = 0,
state_root = ZERO_HASH,
receipts_root = ZERO_HASH,
logs_bloom = stateless_input_slice(0, 256),
fee_recipient = ZERO_ADDRESS,
parent_hash = ZERO_HASH,
parent_beacon_block_root = ZERO_HASH,
slot_number = 0,
}The execution-payload header fields the model reads and validates
(YP §4.4). Scalar wire bounds come from the consensus/Amsterdam SSZ
ExecutionPayload schema. gas_used <= gas_limit and the active
blob-schedule rules are execution-protocol constraints checked when the
payload is admitted. extra_data retains the schema's
ByteList[MAX_EXTRA_DATA_BYTES] bound while staying source-backed; it is
RLP-encoded whole for the header hash and never inspected. The fixed
logs_bloom commitment likewise stays source-backed until a semantic
consumer explicitly decodes it.
struct BlockHeader = {
number : block_number,
timestamp : block_timestamp,
gas_limit : block_gas_limit,
gas_used : block_gas,
prev_randao : word,
base_fee : word,
/* EIP-4844: a multiple of GAS_PER_BLOB within the active schedule. */
blob_gas_used : blob_gas_used,
/* EIP-4844 uint64 on the wire; narrowed to the documented reachable-chain
invariant at the authenticated input boundary. */
excess_blob_gas : excess_blob_gas,
state_root : hash,
receipts_root : hash,
logs_bloom : LogsBloomRef,
fee_recipient : address,
parent_hash : hash,
parent_beacon_block_root : hash,
/* uint64 (EIP-7843 and the Amsterdam stateless SSZ schema). */
slot_number : slot_number,
extra_data : StatelessInputSliceAtMost(extra_data_length_bound),
}function k_set_tx¶
Installs the per-transaction environment.
The per-transaction environment (ORIGIN, GASPRICE,
BLOBHASH).
register k_tx : TxEnv =
struct { origin = ZERO_ADDRESS, gas_price = ZERO_WORD, blob_hashes = EMPTY_BLOB_HASHES } :
TxEnvFields(blob_schedule_inactive_count)A transaction environment packing its fork-selected blob-count limit existentially.
type TxEnv = {
'blob_limit,
transaction_blob_limit_value('blob_limit).
TxEnvFields('blob_limit)
}function k_tx_reset¶
Resets every per-transaction store and the state journal.
function k_tx_reset() -> unit = {
/* Storage reset consumes the per-account transaction worklists owned by
the account table, so it must precede the account reset. */
storage_tx_reset();
acct_tx_reset();
warm_reset(k_current_transaction_epoch);
transient_reset();
logs_tx_reset();
state_journal_reset()
}Empties the transaction-layer account overlay (per-transaction reset).
val acct_tx_reset = impure { c: "acct_tx_reset" } : unit -> unitStarts a new transaction-local view in the block-lifetime log store.
val logs_tx_reset = impure { c: "logs_tx_reset" } : unit -> unitEmpties the transaction-local state journal.
val state_journal_reset = impure { c: "state_journal_reset" } : unit -> unitEmpties the transaction-layer storage overlay (per-transaction reset).
val storage_tx_reset = impure { c: "storage_tx_reset" } : unit -> unitClears all transient storage (EIP-1153); part of the per-transaction reset.
val transient_reset = impure { c: "transient_storage_reset" } : unit -> unitClears both warm sets (per-transaction reset; EIP-2929 warmth is transaction-scoped).
val warm_reset = impure { c: "warm_reset" } : block_access_index -> unitThe current execution epoch: zero for pre-execution effects, transaction index plus one during transaction execution, and transaction count plus one for post-execution effects. EIP-2929 warmth and EIP-7928 BAL changes share this transaction-scoped identity.
register k_current_transaction_epoch : block_access_index = 0type TransactionMergeSemantics¶
The two correlated lifecycle choices selected once from the active fork. Passing this descriptor into the merge keeps both the Sail implementation and optimized host implementation from independently re-dispatching on the fork or observing impossible feature combinations.
struct TransactionMergeSemantics = {
delete_only_created : bool,
preserve_selfdestruct_balance : bool,
}function transaction_merge_semantics¶
Selects the complete transaction-end lifecycle semantics for one fork.
function transaction_merge_semantics(fork : Fork) -> TransactionMergeSemantics =
if fork >= Amsterdam then {
struct { delete_only_created = true, preserve_selfdestruct_balance = true }
} else if fork >= Cancun then {
struct { delete_only_created = true, preserve_selfdestruct_balance = false }
} else {
struct { delete_only_created = false, preserve_selfdestruct_balance = false }
}EIP-7954 code/initcode size bump (65536/131072).
let Amsterdam : int(amsterdam_fork_value) = sizeof(amsterdam_fork_value)EIP-1153/4844; precompiles 0x01-0x0a.
let Cancun : int(first_blob_fork_value) = sizeof(first_blob_fork_value)Every supported protocol and schema fork, in activation order. This is the
sole fork identity in the model: the decoded schema byte selects a
ProtocolProfile, which stores one of these values. The bounded semantic
type prevents values outside the supported fork sequence, while each named
constant retains its precise singleton type for dependent profile typing.
type Fork = range(0, 16)The two correlated lifecycle choices selected once from the active fork. Passing this descriptor into the merge keeps both the Sail implementation and optimized host implementation from independently re-dispatching on the fork or observing impossible feature combinations.
struct TransactionMergeSemantics = {
delete_only_created : bool,
preserve_selfdestruct_balance : bool,
}function account_deleted_at_tx_end¶
Whether a selfdestructed account is cleared at transaction end: always
before Cancun; only if created in the same transaction from Cancun on
(EIP-6780). Amsterdam preserves any balance left by a self-beneficiary
SELFDESTRUCT (EIP-8246).
function account_deleted_at_tx_end(semantics : TransactionMergeSemantics, acc : Account) -> bool =
acc.selfdestructed & (not_bool(semantics.delete_only_created) | acc.created)val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))An account plus its lifecycle flags: existence, EIP-161 storage clearing, same-transaction creation (EIP-6780), and selfdestruction.
struct Account = {
info : AccountInfo,
present : bool,
storage_cleared: bool,
created : bool,
selfdestructed : bool,
}The two correlated lifecycle choices selected once from the active fork. Passing this descriptor into the merge keeps both the Sail implementation and optimized host implementation from independently re-dispatching on the fork or observing impossible feature combinations.
struct TransactionMergeSemantics = {
delete_only_created : bool,
preserve_selfdestruct_balance : bool,
}function k_tx_merge¶
The transaction-end merge: drains the transaction overlays into the block layer, applying the fork-specific selfdestruct clearing rule, storage-clear generations, and recording nonce/balance/code/storage changes for the EIP-7928 block access list. Lifecycle flags reset as rows merge.
function k_tx_merge() -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let semantics = transaction_merge_semantics(profile.fork);
var more : bool = true;
/* Each pop advances a cursor over a protocol-sized host table. */
while more termination_measure(2 ^ 64) do {
let popped_account = acct_tx_pop();
match popped_account {
AcctTxPopRow(e) => {
var curr : Account = e.value.curr;
let deleted = account_deleted_at_tx_end(semantics, curr);
if deleted then {
let cleared_account =
if semantics.preserve_selfdestruct_balance
then account_clear_preserving_balance(curr)
else account_delete(curr);
curr = cleared_account;
/* The account's active transaction generation belongs to
the deleted incarnation. Retire it before the storage
drain so its writes cannot be reinserted after the
block-layer clear. */
storage_tx_clear(e.addr)
};
let original_storage_retained = not_bool(e.value.orig.storage_cleared);
if deleted | (curr.storage_cleared & original_storage_retained) then {
storage_block_clear(e.addr)
};
if curr.info.nonce != e.value.orig.info.nonce then {
bal_nonce_change(k_current_transaction_epoch, e.addr, curr.info.nonce)
};
if curr.info.balance != e.value.orig.info.balance then {
bal_balance_change(k_current_transaction_epoch, e.addr, curr.info.balance)
};
if curr.info.code_hash != e.value.orig.info.code_hash then {
bal_code_change(k_current_transaction_epoch, e.addr, curr.info.code_hash)
};
curr = { curr with created = false, selfdestructed = false };
let changed = account_changed(curr, e.value.orig);
if changed then {
acct_block_write(struct { addr = e.addr, value = struct { curr = curr, orig = e.value.orig } })
}
},
AcctTxPopExhausted(_) => more = false,
}
};
more = true;
/* Each pop advances a cursor over a protocol-sized host table. */
while more termination_measure(2 ^ 64) do {
let popped_storage = storage_tx_pop();
match popped_storage {
StorageTxPopRow(e) => {
let account = acct_block_get(e.key.addr);
if account.found then {
let acc = account.account;
if acc.present & e.value.curr != e.value.orig then {
bal_storage_change(k_current_transaction_epoch, e.key.addr, e.key.slot, e.value.curr);
storage_block_put(e)
}
}
},
StorageTxPopExhausted(_) => more = false,
}
};
storage_tx_reset();
acct_tx_reset()
}Whether an account differs from its original in any trie-observable way.
function account_changed(c : Account, o : Account) -> bool =
account_info_changed(c.info, o.info) | c.present != o.present | c.storage_cleared != o.storage_clearedClears nonce, code, and storage while preserving a nonzero balance. Amsterdam applies this form to an account created and selfdestructed in the same transaction (EIP-8246).
function account_clear_preserving_balance(acc : Account) -> Account = {
let cleared_storage = account_clear_storage(acc);
account_set_info(cleared_storage, { acc.info with nonce = 0, code_hash = KECCAK_EMPTY })
}The deleted form of an account: empty tuple, non-existent, storage cleared.
function account_delete(acc : Account) -> Account =
{
acc with
info = { EMPTY_ACCOUNT_INFO with storage_root = acc.info.storage_root },
present = false,
storage_cleared = true,
}Whether a selfdestructed account is cleared at transaction end: always
before Cancun; only if created in the same transaction from Cancun on
(EIP-6780). Amsterdam preserves any balance left by a self-beneficiary
SELFDESTRUCT (EIP-8246).
function account_deleted_at_tx_end(semantics : TransactionMergeSemantics, acc : Account) -> bool =
acc.selfdestructed & (not_bool(semantics.delete_only_created) | acc.created)The block-layer account row for an address. found is false when no
earlier transaction in the block has loaded it.
val acct_block_get = impure { c: "acct_block_get" } : address -> AccountRowWrites a merged current/original account pair into the block layer (transaction-end merge).
val acct_block_write = impure { c: "acct_block_write" } : AcctEntry -> unitReturns the next transaction-layer account row in insertion order, or reports exhaustion. Merge order is not protocol-visible.
val acct_tx_pop = impure { c: "acct_tx_pop" } : unit -> AcctTxPopResultEmpties the transaction-layer account overlay (per-transaction reset).
val acct_tx_reset = impure { c: "acct_tx_reset" } : unit -> unitRecords a post-transaction balance at the supplied EIP-7928 position.
val bal_balance_change = impure { c: "bal_note_balance_change" } : (block_access_index, address, word) -> unitRecords a post-transaction code hash at the supplied EIP-7928 position.
val bal_code_change = impure { c: "bal_note_code_change" } : (block_access_index, address, hash) -> unitRecords a post-transaction nonce at the supplied EIP-7928 position.
val bal_nonce_change = impure { c: "bal_note_nonce_change" } : (block_access_index, address, account_nonce) -> unitRecords a post-transaction storage value for (address, slot) at
the supplied EIP-7928 position.
val bal_storage_change = impure { c: "bal_note_storage_change" } : (block_access_index, address, word, word) -> unitval not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))We have special support for raising values to the power of two. Any Sail expression 2 ^ x will be compiled to this builtin.
val pow2 = pure {lean: "_lean_pow2i", _: "pow2"}: forall ('n : Int). int('n) -> int(2 ^ 'n)Drops every block-layer storage row of an address (account deletion or a fresh storage generation).
val storage_block_clear = impure { c: "storage_block_clear" } : address -> unitWrites a merged row into the block-layer overlay (transaction-end merge of a changed slot).
val storage_block_put = impure { c: "storage_block_put" } : StorageEntry -> unitDrops every transaction-layer storage row of an address — the storage side of an account collapsing to empty or clearing its storage.
val storage_tx_clear = impure { c: "storage_tx_clear" } : address -> unitRemoves and returns one row of the transaction-layer overlay, or reports exhaustion; the drain loop of the transaction-end merge.
val storage_tx_pop = impure { c: "storage_tx_pop" } : unit -> StorageTxPopResultEmpties the transaction-layer storage overlay (per-transaction reset).
val storage_tx_reset = impure { c: "storage_tx_reset" } : unit -> unitSelects the complete transaction-end lifecycle semantics for one fork.
function transaction_merge_semantics(fork : Fork) -> TransactionMergeSemantics =
if fork >= Amsterdam then {
struct { delete_only_created = true, preserve_selfdestruct_balance = true }
} else if fork >= Cancun then {
struct { delete_only_created = true, preserve_selfdestruct_balance = false }
} else {
struct { delete_only_created = false, preserve_selfdestruct_balance = false }
}The current execution epoch: zero for pre-execution effects, transaction index plus one during transaction execution, and transaction count plus one for post-execution effects. EIP-2929 warmth and EIP-7928 BAL changes share this transaction-scoped identity.
register k_current_transaction_epoch : block_access_index = 0The active protocol policy and all gas limits derived from the executing header, selected together while decoding the stateless input.
register k_execution_profile : ExecutionProfile = DEFAULT_EXECUTION_PROFILEAn account plus its lifecycle flags: existence, EIP-161 storage clearing, same-transaction creation (EIP-6780), and selfdestruction.
struct Account = {
info : AccountInfo,
present : bool,
storage_cleared: bool,
created : bool,
selfdestructed : bool,
}One transaction-layer account row, or exhaustion of the destructive transaction-end drain.
union AcctTxPopResult = {
/* the next drained transaction-layer account row */
AcctTxPopRow : AcctEntry,
/* the drain has yielded every row */
AcctTxPopExhausted : unit,
}One transaction-layer storage row, or exhaustion of the destructive transaction-end drain.
union StorageTxPopResult = {
/* the next drained transaction-layer storage row */
StorageTxPopRow : StorageEntry,
/* the drain has yielded every row */
StorageTxPopExhausted : unit,
}function k_journal_revert¶
Replays the state journal backwards to its innermost open frame boundary.
function k_journal_revert() -> unit = state_journal_revert()Replays entries backwards to the innermost open checkpoint, then removes its marker.
val state_journal_revert = impure { c: "state_journal_revert" } : unit -> unitfunction k_journal_commit¶
Records a successful child frame without discarding its reversible entries.
function k_journal_commit() -> unit = state_journal_commit()Appends a JournalFrameCommitted entry for the innermost open checkpoint.
Its mutations remain in the journal and are therefore revertible by a
parent.
val state_journal_commit = impure { c: "state_journal_commit" } : unit -> unit