Blockchain basic introduction part 1

Introduction to Ethereum blockchain.

Detail visualize/ simplify and explanation from Yellow paper. Thank you for the amazing illustration from Ethereum EVM Illustrated.

Quicknote: After The Merge → Ethereum will have 2 service working together. Execution Layer and consensus layer. Yellow paper is define most the the Execution layer part.

Ethereum Blockchain.

“Ethereum, taken as a whole, can be viewed as a transaction-based state machine” - Ethereum Yellow Paper

It begins with the first state - “genesis state” and transition state by state and become the current Ethereum chains.

The transactions = the VALID link between 2 states and include a lot of information.

Ethereum state transition overview

We can present this formally by:

$$ \sigma_{t+1} = \Upsilon(\sigma_t,T) $$

Where:

  • $\Upsilon$ is the Ethereum state transition function. It represents the rules, the computing
  • $\sigma$ is the state. $\sigma_t$ the Ethereum state at the specific time $t$
  • $T$ is the transaction. E.g: An instruction from a user or a smart contract

Together this simple formula represent all the Ethereum power. Which $\Upsilon$ allows components to carry out computation, $\sigma$ allows components to store arbitrary state between transactions.

State transition formula illustration

Block($B$) is the collection of transactions - a packages of data.

The chaining function($\prod$) is using a cryptographic hash - as a mean of reference transactions together with the previous block and an identifier for the final state.

$$ \sigma_{t+1} = \prod(\sigma_t, B) $$
$$ B = (...,(T_0, T_1, ...),...) $$
$$ \prod(\sigma, B) = \Upsilon(\Upsilon(\sigma, T_0), T_1)\ldots $$

From the viewpoint of the states Ethereum can be seen as a state chain.

Ethereum as a state chain

From the viewpoint of the implementation Ethereum can also be seen as a chain of blocks, so it is BLOCKCHAIN.

Ethereum as a chain of blocks

From the viewpoint of the ledger, Ethereum can also be seen as a stack of transactions

Value:

From $\Upsilon$ and $\sigma$ we can compute anything, but to limit and calculate the right amount of computational power within the network → there is a need for a common currency to evaluate the power.

Ethereum define an intrinsic currency - Ether - ETH along with other sub-denominations of Ether. Conversion: 1 Ether = $10^{18}$ Wei

Multiplier Name
$10^0$ Wei
$10^9$ Gwei
$10^{12}$ Szabo
$10^{15}$ Finney
$10^{18}$ Ether

Since the system is decentralised → anyone can create a new block from older existing block, and when there was disagreement between the part from root (genesis block) to the leaf (newest block), a fork is created → a new branch with a different rule.

A fork can be created on purpose by a developer when their disagree with the current state of the chain. Like the ETH classic and current ETH chains, or can be forked by developer to include and update new features.

The newest fork and what is blog is currently describe is the shanghai fork - start from block 17034870.

Furthermore we can read more at Ethereum Roadmap.

Occasionally actors do not agree on a protocol change, and a permanent fork occurs. In order to distinguish between diverged blockchains, chain ID is introduced and the main Ethereum network will have chain id $\beta = 1$.

Block, State and Transaction:

World State:

Definition: the world state is a mapping from 160-bit addresses to account states. It is referenced through Ethereum’s trie structure rather than stored as one flat mapping on-chain.

A TRIE stores the global account state, and contract accounts may also have their own storage tries.

World state trie illustration

TRIE is an immutable data structure, so previous states can be referenced efficiently by their root hash.

Account state $\sigma[a]$:

Account state structure

Nonce: A scalar value equal to the number of transactions sent by an EOA or the number of contract creations performed by a contract. For account $a$ in state $\sigma$, denote it by $\sigma[a]_n$.

Balance: A scalar value equal to the number of wei held by the account. Denote it by $\sigma[a]_b$.

StorageRoot: A 256-bit hash of the root node of the trie that encodes the account’s storage contents. Denote it by $\sigma[a]_s$.

CodeHash: The hash of the EVM bytecode associated with this account. Denote it by $\sigma[a]_c$. If some byte sequence $b$ is the account code, then $KEC(b) = \sigma[a]_c$.

Here is the implementation in Go Ethereum (geth):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Account represents an Ethereum account and its attached data.
// This type is used to specify accounts in the genesis block state, and
// is also useful for JSON encoding/decoding of accounts.
type Account struct {
	Code    []byte                      `json:"code,omitempty"`
	Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
	Balance *big.Int                    `json:"balance" gencodec:"required"`
	Nonce   uint64                      `json:"nonce,omitempty"`
} 
// StateAccount is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
type StateAccount struct {
	Nonce    uint64
	Balance  *uint256.Int
	Root     common.Hash // merkle root of the storage trie
	CodeHash []byte
} // Store on chain

In practice, we usually work with account mappings and code rather than manipulating trie roots directly, so we use the following equivalence:

$$ \operatorname{TRIE}(L_I^*(\sigma[a]_s)) \equiv \sigma[a]_s $$
$$ L_I((k,v)) \equiv (KEC(k), RLP(v)) $$

where

$$ k \in \mathbb{B}_{32}, \qquad v \in \mathbb{N} $$

About the codeHash field: if $\sigma[a]_c = KEC(())$, then the account has no deployed code, so it is a simple account rather than a contract account.

We define world-state collapse function $L_s$:

$$ L_s(\sigma) \equiv \lbrace p(a) : \sigma[a] \neq \varnothing \rbrace $$
$$ p(a) \equiv (KEC(a), RLP((\sigma[a]_n, \sigma[a]_b, \sigma[a]_s, \sigma[a]_c))) $$

$L_s$ is used together with the TRIE function to derive the identity, or root hash, of the world state.

$$ \forall a:\ \sigma[a] = \varnothing \vee (a \in \mathbb{B}_{20} \wedge v(\sigma[a])) $$
$$ v(x) = x_n \in \mathbb{N}_{256} \wedge x_b \in \mathbb{N}_{256} \wedge x_s \in \mathbb{B}_{32} \wedge x_c \in \mathbb{B}_{32} $$

An EMPTY account has no code, nonce 0, and balance 0.

$$ EMPTY(\sigma,a) \equiv \sigma[a]_c = KEC(()) \wedge \sigma[a]_n = 0 \wedge \sigma[a]_b = 0 $$

A DEAD account is either absent from the world state or empty.

$$ DEAD(\sigma,a)\equiv \sigma[a] = \varnothing \vee EMPTY(\sigma,a) $$

EOAs vs Contract Accounts

EOA Contract Account
Nonce Yes Yes
Balance Yes Yes
StorageRoot Empty trie root Storage trie root
CodeHash Empty code hash by default; may be affected by EIP-7702 delegation semantics Hash of deployed contract bytecode

Note: EIP-7702 allows EOAs to temporarily delegate execution semantics in a way that makes them behave more like programmable accounts.

Transaction:

Definition: a single cryptographically-signed instruction constructed by an actor externally to the scope of Ethereum. There are three transactions type and 2 subtypes of transactions: message call and creation of new accounts with code (contract creation)

Fields: Meaning Symbol Transaction Type Boundaries
type Transaction type $T_x$ 0,1,2 ${0,1,2}$
nonce Scalar - number of transaction by the sender. $T_n$ 0,1,2 $\mathbb{N}_{256}$
to 160-bit address. Message call: recipient. Contract creation: empty value $T_t$ 0,1,2 $\mathbb{B}$ or $\mathbb{B}_{20}$
value Number of Wei. $T_v$ 0,1,2 $\mathbb{N}_{256}$
r,s Signature → determine the sender. $T_r$ and $T_s$ 0,1,2 $\mathbb{N}_{256}$
accessList List of access entries to warm up. Access entry: $E=(E_a,E_s)$. Touple of account address ,storage keys. $T_A$ 1,2
chainId Chain ID . Must be equal to the network chain ID $\beta$ $T_c$ 1,2 $\beta$
yParity Signature Y parity $T_y$ 1,2 ${0,1}$
w Combine chainId, yParity. $T_w=27+T_y$ or $T_w = 2*\beta+35+T_y$ $T_w$ 0 $\mathbb{N}_{256}$
maxFeePerGas Scalar = maximum number of Wei to be paid per unit of gas for all computation costs. $T_m$ 2 $\mathbb{N}_{256}$
maxPriorityFeePerGas Scalar = the maximum number of Wei to be paid to the recipient as an incentive to include the transaction $T_f$ 2 $\mathbb{N}_{256}$
gasPrice Scalar = number of Wei pay per unit of gas $T_p$ 0,1 $\mathbb{N}_{256}$
init Deployment Code $T_i$ 0,1,2. Contract creation transaction
data Calldata $T_d$ 0,1,2. Message call transaction

We assume all components are interpreted by the RLP as integer values, with the exception of the access list $T_A$ and the arbitrary length byte arrays $T_i$ and $T_d$.

Example of Transaction on the Test net:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[
  {
    "hash": "0x7f1ae3e9953390a3a079b735d17685c7b52ead2ce343fd48ae0b00f48834f53d",
    "transactionType": "CREATE",
    "contractName": "X",
    "contractAddress": "0x001c0b8f9b25aa4bcdf5abcde5f34cf2a6d697c4",
    "function": null,
    "arguments": [
      "0x6d178640DCe98B57F95695264319A43C88A08c17"
    ],
    "transaction": {
      "from": "0x025c1db463adf52d59d2f95c4be24c4475d190fc",
      "gas": "0x6616a",
      "value": "0x0",
      "input": "0x608060....(deployment + runtime bytecode) ... 08c17",
      "nonce": "0xfb",
      "chainId": "0xaa36a7"
    },
    "additionalContracts": [],
    "isFixedGasLimit": false
  },
  {
    "hash": "0x09d37c99ed770825d195c5b98bbfd6863fefe40cbee00cc3511f22485ea77215",
    "transactionType": "CALL",
    "contractName": "X",
    "contractAddress": "0x001c0b8f9b25aa4bcdf5abcde5f34cf2a6d697c4",
    "function": "Exploit()",
    "arguments": [],
    "transaction": {
      "from": "0x025c1db463adf52d59d2f95c4be24c4475d190fc",
      "to": "0x001c0b8f9b25aa4bcdf5abcde5f34cf2a6d697c4",
      "gas": "0x1898f",
      "value": "0x71afd498d0000",
      "input": "0xb44ee64a",
      "nonce": "0xfc",
      "chainId": "0xaa36a7"
    },
    "additionalContracts": [],
    "isFixedGasLimit": false
  }
]

Withdrawal (new in Shapella upgrade)

A withdrawal $W$ → a tuple of data describing a consensus layer validator’s withdrawal of some amount of its staked Ether. A withdrawal is created and validated in the consensus layer → push to the execution layer to proceed.

A withdrawal composes:

Field Meaning Symbol Boundaries
globalindex Increase from 0 - unique identifier for the withdrawal $W_g$ $\mathbb{N}_{64}$
validatorIndex index of the consensus layer validator $W_v$ $\mathbb{B}_{20}$
recipient 20-bytes address receive Ether from the withdrawal $W_r$ $\mathbb{N}_{64}$
amount Scalar (≠0) = amount of Ether demoninated in Gwei($10^9$ Wei) $W_a$ $\mathbb{N}_{64}$

Withdrawal serialisation is defined as:

$$ L_W(W) \equiv (W_i, W_v, W_a, W_V) $$

Quick note: after Ethereum merge, changing from PoW to PoS. Ethereum now have 2 different layer: the consensus layer and the execution layer. See Execution and Consensus Layers.

Block

The block is the collection of information (block header) $H$ + Information of the transactions $T$ + $U$ (deprecated) + $W$ ← collection of validator’s withdrawal push from the consensus layer.

A Block $B$ formally define as:

$$ B \equiv (B_H, B_T, B_U, B_W) $$

A block header $B_H$ include:

Field Meaning Symbol Boundaries
parentHash $H_p =KEC(ParentBlockHeader)$ $H_p$ $\mathbb{B}_{32}$
ommersHash 256-bit hash field. Deprecated now = $KEC(RLP(()))$ $H_o$ $\mathbb{B}_{32}$
beneficiary 160-bit address to which priority fees from this block is transferred $H_c$ $\mathbb{B}_{20}$
stateRoot 256-bit hash of the root node of the state TRIE after all transactions exec and withdrawals - (it all about the WORLD STATES and accounts states) $H_r$ $\mathbb{B}_{32}$
transactionsRoot 256-bit hash root node of the TRIE populated with each transactions in the transactions list (all about the TRANSACTION) $H_t$ $\mathbb{B}_{32}$
receiptsRoot 256-bit hash root note of the TRIE populated with the receipts of each transaction in the transactions list $H_e$ $\mathbb{B}_{32}$
logsBloom The Bloom filter composed from indexable information - log entries from receipts of transactions $H_b$ $\mathbb{B}_{256}$
difficulty Scalar - deprecated - now equal 0 $H_d$ $\mathbb{N}$
number Scalar = number of ancestor blocks. Genesis block has number = 0 $H_i$ $\mathbb{N}$
gasLimit Scalar = limit of gas expenditure / block $H_l$ $\mathbb{N}$
gasUsed Scalar = total gas used in transactions for the block $H_g$ $\mathbb{N}$
timestamp Scalar = unix time at this block’s inception $H_s$ $\mathbb{N}_{256}$
extraData Byte array contain data relevant to the block must be less than 32 bytes $H_x$ $\mathbb{B}$
prevRandao lastest RANDAO mix of the post beacon state $H_a$ $\mathbb{B}_{32}$
nonce 64 bit value deprecated . Current value = 0x0...0 $H_n$ $\mathbb{B}_{8}$
baseFeePerGas Scalar = base amount of Wei burned for each unit of gas consumed $H_f$ $\mathbb{N}$
withdrawalsRoot 256-bit hash root node of the TRIE populated with each withdrawal operations $H_w$ $\mathbb{B}_{32}$

Go struct for the block header:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Header represents a block header in the Ethereum blockchain.
type Header struct {
	ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
	UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
	Coinbase    common.Address `json:"miner"`
	Root        common.Hash    `json:"stateRoot"        gencodec:"required"`
	TxHash      common.Hash    `json:"transactionsRoot" gencodec:"required"`
	ReceiptHash common.Hash    `json:"receiptsRoot"     gencodec:"required"`
	Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`
	Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`
	Number      *big.Int       `json:"number"           gencodec:"required"`
	GasLimit    uint64         `json:"gasLimit"         gencodec:"required"`
	GasUsed     uint64         `json:"gasUsed"          gencodec:"required"`
	Time        uint64         `json:"timestamp"        gencodec:"required"`
	Extra       []byte         `json:"extraData"        gencodec:"required"`
	MixDigest   common.Hash    `json:"mixHash"`
	Nonce       BlockNonce     `json:"nonce"`

	// BaseFee was added by EIP-1559 and is ignored in legacy headers.
	BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`

	// WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.
	WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`

	// BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.
	BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"`

	// ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.
	ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"`

	// ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers.
	ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`

	// RequestsHash was added by EIP-7685 and is ignored in legacy headers.
	RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"`

	// SlotNumber was added by EIP-7843 and is ignored in legacy headers.
	SlotNumber *uint64 `json:"slotNumber" rlp:"optional"`
}

Note: RANDAO = pseudorandom value gen by validators on the Ethereum consensus layer. See Ethereum Consensus Specs.

Transaction Receipt:

Because there may be useful information to form a zk proof, index and search. The receipt is encoded as $B_R[i]$ for the $i$ transaction (i-index in the array) → placed in an index-keyed TRIE → root as $H_e$.

We can view it in Geth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Receipt represents the results of a transaction.
type Receipt struct {
	// Consensus fields: These fields are defined by the Yellow Paper
	Type              uint8  `json:"type,omitempty"`
	PostState         []byte `json:"root"`
	Status            uint64 `json:"status"`
	CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
	Bloom             Bloom  `json:"logsBloom"         gencodec:"required"`
	Logs              []*Log `json:"logs"              gencodec:"required"`

	// Implementation fields: These fields are added by geth when processing a transaction.
	TxHash            common.Hash    `json:"transactionHash" gencodec:"required"`
	ContractAddress   common.Address `json:"contractAddress"`
	GasUsed           uint64         `json:"gasUsed" gencodec:"required"`
	EffectiveGasPrice *big.Int       `json:"effectiveGasPrice"` // required, but tag omitted for backwards compatibility
	BlobGasUsed       uint64         `json:"blobGasUsed,omitempty"`
	BlobGasPrice      *big.Int       `json:"blobGasPrice,omitempty"`

	// Inclusion information: These fields provide information about the inclusion of the
	// transaction corresponding to this receipt.
	BlockHash        common.Hash `json:"blockHash,omitempty"`
	BlockNumber      *big.Int    `json:"blockNumber,omitempty"`
	TransactionIndex uint        `json:"transactionIndex"`
}

Transaction Receipt $R$ have form:

$$ R \equiv (R_x, R_z, R_u, R_b, R_l) $$
$$ L_R(R) \equiv (R_z, R_u, R_b, R_l) $$

Where:

  • $R_x$ = Type of transaction.
  • $R_z$ = Status code. 0x00 for revert. 0x01 for succeed.
  • $R_u$ = Cumulative gas used in the block immediately after the specific transaction finishes executing.
  • $R_l$ = Set of logs created through execution.
  • $R_b$ = Bloom filter.

Details:

$R_x$ equal to the type of the corresponding transaction. $L_R$ → transform into RLP-serialized byte array. $R_z$ must be non negative number

$$ \begin{aligned} R_z &\in \mathbb{N} \\ R_u &\in \mathbb{N} \quad \wedge \quad R_b \in \mathbb{B}_{256} \end{aligned} $$
1
2
3
4
5
6
7
const (
	LegacyTxType     = 0x00
	AccessListTxType = 0x01
	DynamicFeeTxType = 0x02
	BlobTxType       = 0x03
	SetCodeTxType    = 0x04
)

$R_l$ is the series of log entries. $(O_0,O_1,….)$.

Log entries $O=tuple(O_a,(O_{t0},O_{t1},…),O_d)$

  • $O_a$ = logger address $O_a \in ,mathbb{B}_{20}$
  • $O_t$ = log topics (can be null) $\forall x \in O_t : x \in \mathbb{B}_{32}$
  • $O_d$ = number of bytes of data. $O_d \in \mathbb{B}$.

$R_b$ is the Bloom filter which is a 256-byte hash value. Detail in Appendix.

The receipts for the above transactions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  "receipts": [
    {
      "status": "0x1",
      "cumulativeGasUsed": "0x78ac72",
      "logs": [],
      "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "type": "0x2",
      "transactionHash": "0x7f1ae3e9953390a3a079b735d17685c7b52ead2ce343fd48ae0b00f48834f53d",
      "transactionIndex": "0x51",
      "blockHash": "0x20eb35c61787030cb79f608745ce23e421331d1520c7b7c04421d530cf039ca7",
      "blockNumber": "0x9b777c",
      "gasUsed": "0x4e879",
      "effectiveGasPrice": "0x3d09a0bf",
      "from": "0x025c1db463adf52d59d2f95c4be24c4475d190fc",
      "to": null,
      "contractAddress": "0x001c0b8f9b25aa4bcdf5abcde5f34cf2a6d697c4"
    },
    {
      "status": "0x1",
      "cumulativeGasUsed": "0x79b98c",
      "logs": [],
      "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "type": "0x2",
      "transactionHash": "0x09d37c99ed770825d195c5b98bbfd6863fefe40cbee00cc3511f22485ea77215",
      "transactionIndex": "0x52",
      "blockHash": "0x20eb35c61787030cb79f608745ce23e421331d1520c7b7c04421d530cf039ca7",
      "blockNumber": "0x9b777c",
      "gasUsed": "0x10d1a",
      "effectiveGasPrice": "0x3d09a0bf",
      "from": "0x025c1db463adf52d59d2f95c4be24c4475d190fc",
      "to": "0x001c0b8f9b25aa4bcdf5abcde5f34cf2a6d697c4",
      "contractAddress": null
    }
  ]

Check valid of a block:

Must satisfied these constraints:

  • Ommers field - $B_U$ must be empty array + Block Header -$B_H$ must be consistent with Transaction - $B_T$ and Withdrawal - $B_W$
  • State Root - $H_r$ must match the resultant state after executing all transactions → all withdrawals in order from the based state $\sigma$
  • $H_t,H_e,H_b,H_w$ - Transaction Root, receiptsRoot, logsBloom,withdrawalsRoot must be correctly derived from the transactions, transactions receipts, resulting logs, withdrawals respectively.

In formal definition:

$$ B_U \equiv () $$
$$ H_r \equiv TRIE(L_s(\prod(\sigma, B))) $$
$$ H_t \equiv TRIE(\{\, \forall i < \lVert B_T \rVert,\ i \in \mathbb{N} : p_T(i, B_T[i]) \,\}) $$
$$ H_e \equiv TRIE(\{\, \forall i < \lVert B_R \rVert,\ i \in \mathbb{N} : p_R(i, B_R[i]) \,\}) $$
$$ H_w \equiv TRIE(\{\, \forall i < \lVert B_W \rVert,\ i \in \mathbb{N} : p_W(i, B_W[i]) \,\}) $$
$$ H_b \equiv \bigvee_{r \in B_R}(r_b) $$

$p_T,p_R,p_W$ each will be the pairwise RLP transformations but with special case for EIP-2718.

Block header Validity:

Define $P(B_H)$ = Parent block of $B$:

$$ P(H) \equiv B' : KEC(RLP(B'_H)) = H_p $$

Block number $H_i$ will be

$$ H_i \equiv P(H)_{H_i} + 1 $$

Starting with London release introduce baseFeePerGas $H_f$. $H_f$ = amount of wei burned per unit of gas consumed while executing transactions within the block. The function to calculate is define as $F(H)$.

  • If current block = first block of the London fork → base = 1000000000.
  • If the parent gas limit $P(H)_{H_g}$ = the target gas ($\tau$) → the base fee unchanged.
  • If $P(H)_{H_g}$ > ($\tau$) → the base fee increase by $v$.
  • If $P(H)_{H_g}$ < ($\tau$) → the base fee decrease by $v$.

Where $\tau$ and $v$ define:

$$ \tau \equiv \left\lfloor \frac{P(H)_{H_l}}{2} \right\rfloor $$
  • If increase:
$$ v \equiv \max\left(1,\left\lfloor \frac{P(H)_{H_f}\bigl(P(H)_{H_g} - \tau\bigr)}{\tau \times 8} \right\rfloor\right) $$
  • If decrease:
$$ v \equiv \left\lfloor \frac{P(H)_{H_f}\bigl(\tau - P(H)_{H_g}\bigr)}{\tau \times 8} \right\rfloor $$

The concrete implementation is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// CalcBaseFee calculates the basefee of the header.
func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int {
	// If the current block is the first EIP-1559 block, return the InitialBaseFee.
	if !config.IsLondon(parent.Number) { 
		return new(big.Int).SetUint64(params.InitialBaseFee)
	}

	parentGasTarget := parent.GasLimit / config.ElasticityMultiplier() //( /2 )
	// If the parent gasUsed is the same as the target, the baseFee remains unchanged.
	if parent.GasUsed == parentGasTarget {
		return new(big.Int).Set(parent.BaseFee)
	}

	var (
		num   = new(big.Int)
		denom = new(big.Int)
	)

	if parent.GasUsed > parentGasTarget {
		// If the parent block used more gas than its target, the baseFee should increase.
		// max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator)
		num.SetUint64(parent.GasUsed - parentGasTarget)
		num.Mul(num, parent.BaseFee)
		num.Div(num, denom.SetUint64(parentGasTarget))
		num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator())) // DIV by 8
		if num.Cmp(common.Big1) < 0 {
			return num.Add(parent.BaseFee, common.Big1)
		}
		return num.Add(parent.BaseFee, num)
	} else {
		// Otherwise if the parent block used less gas than its target, the baseFee should decrease.
		// max(0, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator)
		num.SetUint64(parentGasTarget - parent.GasUsed)
		num.Mul(num, parent.BaseFee)
		num.Div(num, denom.SetUint64(parentGasTarget))
		num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator()))

		baseFee := num.Sub(parent.BaseFee, num)
		if baseFee.Cmp(common.Big0) < 0 {
			baseFee = common.Big0
		}
		return baseFee
	}
}

The canonical gas limit $H_l$ is defined to follow these constraints:

$$ P(H)_{H_{l'}} - \left\lfloor \frac{P(H)_{H_{l'}}}{1024} \right\rfloor < H_l < P(H)_{H_{l'}} + \left\lfloor \frac{P(H)_{H_{l'}}}{1024} \right\rfloor $$
$$ H_l \geq 5000 $$

Where:

$$ P(H)_{H_{l'}} \equiv \begin{cases} P(H)_{H_l}\,\rho, & \text{if } H_i = F_{\text{London}} \\ P(H)_{H_l}, & \text{if } H_i > F_{\text{London}} \end{cases} $$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// CalcGasLimit computes the gas limit of the next block after parent. It aims
// to keep the baseline gas close to the provided target, and increase it towards
// the target if the baseline gas is lower.
func CalcGasLimit(parentGasLimit, desiredLimit uint64) uint64 {
	delta := parentGasLimit/params.GasLimitBoundDivisor - 1
	limit := parentGasLimit
	if desiredLimit < params.MinGasLimit {
		desiredLimit = params.MinGasLimit
	}
	// If we're outside our allowed gas range, we try to hone towards them
	if limit < desiredLimit {
		limit = parentGasLimit + delta
		if limit > desiredLimit {
			limit = desiredLimit
		}
		return limit
	}
	if limit > desiredLimit {
		limit = parentGasLimit - delta
		if limit < desiredLimit {
			limit = desiredLimit
		}
	}
	return limit
}

$H_s$ is the timestamp of block $H$ and must fulfill:

$$ H_s > P(H)_{H_s} $$

prevRando must be determined using information from the Beacon Chain (Consensus Layer). We define as PREVRANDAO().

Validity Function:

After define all this - How we define the validity function $V(H)$ → we can easily see that it must follow all the constraint of all function above.

Formally define:

$$ \begin{aligned} V(H) \equiv \quad & H_g \le H_l \quad \wedge \\ & H_l < P(H)_{H_{l'}} + \left\lfloor \frac{P(H)_{H_{l'}}}{1024} \right\rfloor \quad \wedge \\ & H_l > P(H)_{H_{l'}} - \left\lfloor \frac{P(H)_{H_{l'}}}{1024} \right\rfloor \quad \wedge \\ & H_l \geqslant 5000 \quad \wedge \\ & H_s > P(H)_{H_s} \quad \wedge \\ & H_i = P(H)_{H_i} + 1 \quad \wedge \\ & \|H_x\| \le 32 \quad \wedge \\ & H_f = F(H) \quad \wedge \\ & H_o = \mathrm{KEC}(\mathrm{RLP}(())) \quad \wedge \\ & H_d = 0 \quad \wedge \\ & H_n = \mathtt{0x0000000000000000} \quad \wedge \\ & H_a = \mathrm{PREVRANDAO}() \end{aligned} $$

Example for a block on main Ethereum network:

Main Net
baseFeePerGas        148677693
difficulty           0
extraData            0x546974616e2028746974616e6275696c6465722e78797a29
gasLimit             60000000
gasUsed              11473415
hash                 0xa8b200fb93f6b36a21e636ae6c33e043e7aeec7179f89c0d1d855508eab7962f
logsBloom            0x0c20c24bc9220d94372c004bc4700092210410010c01ec08c28910c824261c19002c1540a108284700125f418a2345a01e50316c1c016c230ae923102b20a2069802b900108a122a183e0d8c0145a2300840633694f3c92882180404a0a404a09483000a1a202a0015448c1801a6a99b9261482062829600a7c01690681b23000c208e184ca1e50901417100040e134245038e854300180e46b18744241c04324622142902b02059665311e818c85428008242840609c0008f68a2024ea838404a5cc8ae40825c103001434220e618c505be62675c2290700885140a430fb30382343c2100d0212080e210c8098092012d1fb0504d90006a040a2282b6003101
miner                0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97
mixHash              0x9c7e169d9ff3e26a1ef455cffe13929ff6bf9f227c4b34a0766d8fe8333fd38c
nonce                0x0000000000000000
number               24747612
parentHash           0xd4d99eca1069bf6282b5fe58de1f77e336c75ebf88fb2ef01dad4ceaad9d412b
parentBeaconRoot     0x6e646d848224b6cce1b3b84b3e6b7b3e3a87fcfa4e3f019b1d2bf589df088215
transactionsRoot     0x15c4eee221a497d9da0bb24ff391c5812b9886f95adf86813a719f40837ee969
receiptsRoot         0xc2cc226bc26433034643217303118667416c2509b3ff5e292c0c1f26bb912e99
sha3Uncles           0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
size                 61503
stateRoot            0x5b4abbb7977b06f950b4a0cc6061713af7be6eb3bcdc7a3592c70f2e49c856fb
timestamp            1774598999 (Fri, 27 Mar 2026 08:09:59 +0000)
withdrawalsRoot      0x587c1fa4c5c79ed7b852a2d1b1245e2b34b8a5d8016e139c308b862806838639
totalDifficulty      0
blobGasUsed          1048576
excessBlobGas        184989393
requestsHash         0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
transactions:        [
	0x518337cfa421a43fd32bffbdb9050b20a86ce026a351d49e1b1fea45df1316d8
	... truncated
	0xceaf8ed0ea814dda246f6117c5fcda5a30e7ecf58fc843ccaeaf038f4998b41a
	0xdb8e1be01d704a5452fda2f0a1d3e87f2ae16138c8b79e1a307d0690eb541f7a
	0x8849e9c2f64b2943354421e2704149b3d2add962f621de21ac9409a78a7e71f7
]

Note: for backward compatibility: mixHash $H_m$ is prevRandao $H_a$.

Transision to Proof of Stake remove the miner. But in the block miner in here is the feeRecipient and collect this:

  • The Base Fee is burned and disappears completely. The miner address gets none
  • The Priority Fee (Tip) paid by users to get their transactions included faster goes directly to this miner address.
  • MEV (Maximal Extractable Value) payouts also go to this address. If a searcher or builder rearranged transactions to make a profit (like an arbitrage trade), they pay a cut to the validator, which lands here.

Appendix:

Recursive Length Prefix:

Recursive Length Prefix (RLP) serialization is used extensively in Ethereum. RLP standardizes the transfer of data between nodes in a space-efficient format. It is a serialization method for encoding arbitrarily structured binary data.

RLP have these rule:

  • Positive integer → convert to shortest byte array (big-endian interpretation = integer) → encode as a string
  • Single byte value in 0x00 - 0x7f → remain the same.
  • String is 0-55 bytes long → RLP encoding = ( 0x80 + len(string) ).to_string()+ string → value of the first bytes [0x80,0xb7]
  • String > 55 bytes → RLP encoding = (0xb7 + len(to_binary(string))).to_string() + len(string) + string
  • String > $2^{64}$ → not encoded.
  • List: if total length of RLP(list_ele) ≤ 55 bytes → RLP=(0xc0 + len(RLP(list_ele)) + RLP(list_ele)
  • List: RLP(list_ele) > 55 bytes → RLP = (0xc0 + len(to_binary(RLP(list_ele)))+ len(RLP(list_ele)) + RLP(list_ele)

RLP Encode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def rlp_encode(input):
    if isinstance(input,str):
        if len(input) == 1 and ord(input) < 0x80:
            return input
        return encode_length(len(input), 0x80) + input
    elif isinstance(input, list):
        output = ''
        for item in input:
            output += rlp_encode(item)
        return encode_length(len(output), 0xc0) + output
def encode_length(L, offset):
    if L < 56:
         return chr(L + offset)
    elif L < 256**8:
         BL = to_binary(L)
         return chr(len(BL) + offset + 55) + BL
    raise Exception("input too long")
def to_binary(x):
    if x == 0:
        return ''
    return to_binary(int(x / 256)) + chr(x % 256)

RLP decode:

Reverse engineer the encode. Furthermore, see Ethereum RLP Definition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def rlp_decode(input):
    if len(input) == 0:
        return
    output = ''
    (offset, dataLen, type) = decode_length(input)
    if type is str:
        output = instantiate_str(substr(input, offset, dataLen))
    elif type is list:
        output = instantiate_list(substr(input, offset, dataLen))
    output += rlp_decode(substr(input, offset + dataLen))
    return output
def decode_length(input):
    length = len(input)
    if length == 0:
        raise Exception("input is null")
    prefix = ord(input[0])
    if prefix <= 0x7f:
        return (0, 1, str)
    elif prefix <= 0xb7 and length > prefix - 0x80:
        strLen = prefix - 0x80
        return (1, strLen, str)
    elif prefix <= 0xbf and length > prefix - 0xb7 and length > prefix - 0xb7 + to_integer(substr(input, 1, prefix - 0xb7)):
        lenOfStrLen = prefix - 0xb7
        strLen = to_integer(substr(input, 1, lenOfStrLen))
        return (1 + lenOfStrLen, strLen, str)
    elif prefix <= 0xf7 and length > prefix - 0xc0:
        listLen = prefix - 0xc0;
        return (1, listLen, list)
    elif prefix <= 0xff and length > prefix - 0xf7 and length > prefix - 0xf7 + to_integer(substr(input, 1, prefix - 0xf7)):
        lenOfListLen = prefix - 0xf7
        listLen = to_integer(substr(input, 1, lenOfListLen))
        return (1 + lenOfListLen, listLen, list)
    raise Exception("input does not conform to RLP encoding form")
def to_integer(b):
    length = len(b)
    if length == 0:
        raise Exception("input is null")
    elif length == 1:
        return ord(b[0])
    return ord(substr(b, -1)) + to_integer(substr(b, 0, -1)) * 256

Calculate Address:

How to derive public key from private key:

For Ethereum the curve is secp256k1. We denote the following: sk=private key, pk = public key

G is the base point for the Curve.

$$ \begin{aligned} 0 &\le sk \le \text{order} \\ pk &= sk \cdot G \end{aligned} $$

The Address of Wallet will be: keccak256(pk)[-40:] ← take the last 40 bytes.

The CheckSum - Encoding Rule can be find here: EIP-55

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def checksum_encode(addr): # Takes a 20-byte binary address as input
    hex_addr = addr.hex()
    checksummed_buffer = ""

    # Treat the hex address as ascii/utf-8 for keccak256 hashing
    hashed_address = eth_utils.keccak(text=hex_addr).hex()

    # Iterate over each character in the hex address
    for nibble_index, character in enumerate(hex_addr):

        if character in "0123456789":
            # We can't upper-case the decimal digits
            checksummed_buffer += character
        elif character in "abcdef":
            # Check if the corresponding hex digit (nibble) in the hash is 8 or higher
            hashed_address_nibble = int(hashed_address[nibble_index], 16)
            if hashed_address_nibble > 7:
                checksummed_buffer += character.upper()
            else:
                checksummed_buffer += character
        else:
            raise eth_utils.ValidationError(
                f"Unrecognized hex character {character!r} at position {nibble_index}"
            )

    return "0x" + checksummed_buffer

Transaction structure overview

Bloom filter:

Bloom filter function $M$ → compress log into 256-byte hash

Formal definition in Ethereum:

$$ M(O) \equiv \bigvee_{x \in \{O_a\} \cup O_t}(M_{3:2048}(x)) $$
$$ M_{3:2048}(x),\ x \in B,\ \equiv y,\ \text{where } y \in \mathbb{B}_{256} $$
$$ y = (0,0,\ldots,0) \quad \text{except that} $$
$$ \forall i \in \{0,2,4\}: \ B_{2047 - m(x,i)}(y) = 1 $$
$$ m(x,i) \equiv KEC(x)[i,i+1] \bmod 2048 $$

Detail in GETH code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// at /core/types/bloom9.go:42
const (
	// BloomByteLength represents the number of bytes used in a header log bloom.
	BloomByteLength = 256
	// BloomBitLength represents the number of bits used in a header log bloom.
	BloomBitLength = 8 * BloomByteLength
)
// Bloom represents a 2048 bit bloom filter.
type Bloom [BloomByteLength]byte

// CreateBloom creates a bloom filter out of the give Receipt (+Logs)
func CreateBloom(receipt *Receipt) Bloom {
	var (
		bin Bloom
		buf [6]byte
	)
	for _, log := range receipt.Logs { // THIS is the M(0) loop to OR 
		bin.AddWithBuffer(log.Address.Bytes(), &buf)
		for _, b := range log.Topics {
			bin.AddWithBuffer(b[:], &buf)
		}
	}
	return bin
}

// add is internal version of Add, which takes a scratch buffer for reuse (needs to be at least 6 bytes)

func (b *Bloom) AddWithBuffer(d []byte, buf *[6]byte) { 
	i1, v1, i2, v2, i3, v3 := bloomValues(d, buf)
	b[i1] |= v1
	b[i2] |= v2
	b[i3] |= v3
}

// bloomValues returns the bytes (index-value pairs) to set for the given data
// M3:20489(x)
//  It calculates the keccak256 hash of the given address or topic, and then calculates which three bits of bloom need to be set to 1 based on the first six bytes of the hash.
func bloomValues(data []byte, hashbuf *[6]byte) (uint, byte, uint, byte, uint, byte) {
	sha := hasherPool.Get().(crypto.KeccakState)
	sha.Reset()
	sha.Write(data)
	sha.Read(hashbuf[:])
	hasherPool.Put(sha)
	// The actual bits to flip
	v1 := byte(1 << (hashbuf[1] & 0x7))
	v2 := byte(1 << (hashbuf[3] & 0x7))
	v3 := byte(1 << (hashbuf[5] & 0x7))
	// The indices for the bytes to OR in
	i1 := BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[0:])&0x7ff)>>3) - 1
	i2 := BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[2:])&0x7ff)>>3) - 1
	i3 := BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[4:])&0x7ff)>>3) - 1
	return i1, v1, i2, v2, i3, v3
}

References

  1. Ethereum Yellow Paper
  2. Ethereum EVM Illustrated
  3. Ethereum Roadmap
  4. Execution and Consensus Layers
  5. Ethereum Consensus Specs
  6. Ethereum RLP Definition
  7. EIP-55
updatedupdated2026-03-292026-03-29