Transaction RLP signing preimages¶
The signature semantics of transactions, between the crypto core
(secp recovery and the curve constants) and the envelope decoder:
the signing preimage hashes, the per-envelope v-range rule, and sender
authentication — binding the witnessed public key to a transaction's
signature, called from transaction validity.
function legacy_sig_chain_id¶
The chain id encoded in a legacy signature with v >= 35
(EIP-155).
function legacy_sig_chain_id(v : word) -> word = {
let adjusted_v = word_sub(v, 35);
word_div(adjusted_v, 2)
}The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)let LEGACY_SIGNATURE_SUFFIX_LENGTH¶
let LEGACY_SIGNATURE_SUFFIX_LENGTH : int(2) = 2let PUBLIC_KEY_BODY_LENGTH¶
let PUBLIC_KEY_BODY_LENGTH : int(64) = DOUBLE_WORD_BYTE_LENGTHlet DOUBLE_WORD_BYTE_LENGTH : int(64) = 64function tx_signing_hash¶
The transaction signing-preimage hash. content_src spans the RLP of
the pre-signature fields in the witness and is copied once into the final
contiguous preimage; legacy EIP-155 transactions append
(chain_id, 0, 0), typed transactions prepend the type byte as a
domain separator (EIP-2718).
function tx_signing_hash(t : TxType, content_src : StatelessInputSlice, v : word) -> hash = {
let tb : byte = tx_envelope_type(t);
let eip155 = (tb == 0x00) & word_ule(35, v);
let chain_id =
if eip155 then legacy_sig_chain_id(v) else ZERO_WORD;
let suffix_len : range(0, 35) =
if eip155 then {
let chain_id_length = rlp_uint_word_size(chain_id);
let suffix_length = LEGACY_SIGNATURE_SUFFIX_LENGTH;
chain_id_length + suffix_length
} else {
0
};
let content_length : transaction_byte_length =
if content_src.len <= sizeof(transaction_length_bound) then content_src.len else fatal_error(RlpDecode);
let suffix_length = suffix_len;
let content_len : range(0, transaction_length_bound + 35) = content_length + suffix_length;
let prefix_len = rlp_length_prefix_len(content_len);
let type_len : range(0, 1) =
if tb == 0x00 then 0 else 1;
let preimage_len : range(0, transaction_length_bound + 69) = type_len + prefix_len + content_len;
let encoder = rlp_encoder_begin(preimage_len);
if tb != 0x00 then {
scratch_push_byte(tb)
};
rlp_write_list_prefix(content_len);
scratch_push_slice(content_src);
if eip155 then {
rlp_write_uint_word(chain_id);
scratch_push_byte(0x80);
scratch_push_byte(0x80)
};
let preimage = rlp_encoder_finish(encoder);
let signing_hash = keccak256(preimage);
rlp_encoder_rewind(encoder);
signing_hash
}function fatal_error(_reason) = exit(())The chain id encoded in a legacy signature with v >= 35
(EIP-155).
function legacy_sig_chain_id(v : word) -> word = {
let adjusted_v = word_sub(v, 35);
word_div(adjusted_v, 2)
}function rlp_encoder_begin(expected_len) =
struct { start = scratch_reserve(expected_len), expected_len = expected_len }Finishes an exact-size construction and retains its encoded bytes.
function rlp_encoder_finish(encoder : RlpEncoder) -> ScratchSlice = {
let encoded = scratch_finish(encoder.start);
assert(encoded.len == encoder.expected_len, "RLP encoder length");
encoded
}Releases every byte appended by an encoder after its result is consumed.
function rlp_encoder_rewind(encoder : RlpEncoder) -> unit =
scratch_rewind(encoder.start)function rlp_length_prefix_len(len) =
if len <= RLP_SHORT_LENGTH_LIMIT then {
1
} else {
let length_word = rlp_length_word(len);
1 + rlp_minimal_word_len(length_word)
}function rlp_uint_word_size(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
if (len == 1) & (first[7] == bitzero) then {
1
} else {
1 + len
}
}function rlp_write_list_prefix(content_len) = {
if content_len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(content_len);
let prefix = add_bits(0xc0, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(content_len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xf7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}function rlp_write_uint_word(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
rlp_write_string_prefix(len, first);
let word = u256(w);
scratch_push_word_be(word, len)
}Appends one byte without constructing a Sail list.
function scratch_push_byte(data : byte) -> unit = {
let arena = scratch_arena;
scratch_arena = host_scratch_store_byte(arena.len, data)
}The canonical EIP-2718 wire discriminant. Encoding is total over the closed transaction-type algebra; decoding handles the remaining byte values explicitly at its validation boundary.
function tx_envelope_type(t : TxType) -> byte = match t {
LegacyTx => 0x00,
AccessListTx => 0x01,
FeeMarketTx => 0x02,
BlobTx => 0x03,
SetCodeTx => 0x04,
}function word_ule(a, b) = {
let greater = word_ult(b, a);
not_bool(greater)
}let LEGACY_SIGNATURE_SUFFIX_LENGTH : int(2) = 2let ZERO_WORD : word = word_from_bits(0x0000000000000000000000000000000000000000000000000000000000000000)The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}The EIP-2718 envelope type: the single transaction discriminant. Its closed semantic descriptor below derives fork and feature requirements; transactions do not store redundant boolean flags.
enum TxType = {
/* type 0: pre-EIP-2718 RLP list */
LegacyTx,
/* type 1: EIP-2930 access list */
AccessListTx,
/* type 2: EIP-1559 fee market */
FeeMarketTx,
/* type 3: EIP-4844 blob */
BlobTx,
/* type 4: EIP-7702 set code */
SetCodeTx,
}An 8-bit byte.
type byte = bits(8)The common digest type used by trie, code, and block hashes.
type hash = b256A byte length contained by one SSZ transaction envelope.
type transaction_byte_length = range(0, transaction_length_bound)Maximum byte length of one encoded transaction envelope. Provenance:
Amsterdam MAX_BYTES_PER_TRANSACTION in
SszExecutionPayload.transactions.
type transaction_length_bound : Int = 2 ^ 30The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function auth_signing_hash¶
The EIP-7702 authorization signing hash:
keccak256(0x05 || rlp([chain_id, address, nonce])).
function auth_signing_hash(chain_id : word, addr : address, nonce : account_nonce) -> hash = {
let chain_id_length = rlp_uint_word_size(chain_id);
let address_length = rlp_addr_size();
let nonce_length = rlp_uint_size(nonce);
let content_len : range(0, 87) = chain_id_length + address_length + nonce_length;
let preimage_len : range(0, 121) = 1 + rlp_list_size(content_len);
let encoder = rlp_encoder_begin(preimage_len);
scratch_push_byte(0x05);
rlp_write_list_prefix(content_len);
rlp_write_uint_word(chain_id);
rlp_write_addr(addr);
rlp_write_uint(nonce);
let encoded = rlp_encoder_finish(encoder);
let signing_hash = keccak256(encoded);
rlp_encoder_rewind(encoder);
signing_hash
}function rlp_addr_size() -> int(21) = RLP_ENCODED_ADDRESS_LENGTHfunction rlp_encoder_begin(expected_len) =
struct { start = scratch_reserve(expected_len), expected_len = expected_len }Finishes an exact-size construction and retains its encoded bytes.
function rlp_encoder_finish(encoder : RlpEncoder) -> ScratchSlice = {
let encoded = scratch_finish(encoder.start);
assert(encoded.len == encoder.expected_len, "RLP encoder length");
encoded
}Releases every byte appended by an encoder after its result is consumed.
function rlp_encoder_rewind(encoder : RlpEncoder) -> unit =
scratch_rewind(encoder.start)function rlp_list_size(content_len) = {
rlp_length_prefix_len(content_len) + content_len
}function rlp_uint_word_size(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
if (len == 1) & (first[7] == bitzero) then {
1
} else {
1 + len
}
}Appends one Ethereum address as an RLP byte string.
function rlp_write_addr(a : address) -> unit = {
rlp_write_string_prefix(ADDRESS_BYTE_LENGTH, a[0]);
scratch_push_address(a)
}function rlp_write_list_prefix(content_len) = {
if content_len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(content_len);
let prefix = add_bits(0xc0, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(content_len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xf7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}function rlp_write_uint_word(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
rlp_write_string_prefix(len, first);
let word = u256(w);
scratch_push_word_be(word, len)
}Appends one byte without constructing a Sail list.
function scratch_push_byte(data : byte) -> unit = {
let arena = scratch_arena;
scratch_arena = host_scratch_store_byte(arena.len, data)
}An account transaction-count nonce (EIP-2681).
type account_nonce = range(0, account_nonce_bound)A 20-byte account address (YP §4.1), in canonical protocol byte order.
type address = vector(20, inc, byte)The common digest type used by trie, code, and block hashes.
type hash = b256The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)