Compare commits

..

28 Commits

Author SHA1 Message Date
Matthias Seitz
6cbcfe01a0 Merge branch 'main' into feat/bal-cache 2026-01-23 16:09:45 +01:00
Gigi
830cd5e355 chore: update snmalloc upstream repository link (#21360) 2026-01-23 14:57:46 +00:00
Georgios Konstantopoulos
f77d7d5983 feat(reth-bench): support human-readable gas format in generate-big-block (#21361) 2026-01-23 14:24:34 +00:00
Georgios Konstantopoulos
a2237c534e feat(p2p): add reth p2p enode command (#21357)
Co-authored-by: Amp <amp@ampcode.com>
2026-01-23 13:23:44 +00:00
Arsenii Kulikov
1bd8fab887 feat(txpool): add Block associated type to TransactionValidator trait (#21359) 2026-01-23 13:16:05 +00:00
Matthias Seitz
22a68756c7 fix(tree): evict changeset cache even when finalized block is unset (#21354) 2026-01-23 11:26:57 +00:00
Hwangjae Lee
d99c0ffd62 chore(etc): update ethereum-metrics-exporter GitHub URL (#21348)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-23 10:59:53 +00:00
Georgios Konstantopoulos
ad476e2b5c chore: add yongkangc as codeowner for crates/storage/provider (#21349) 2026-01-23 07:18:18 +00:00
Matthias Seitz
6df249c1f1 feat(engine): stub Amsterdam engine API endpoints (newPayloadV5, getPayloadV6, BALs) (#21344)
Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
2026-01-22 20:48:11 +00:00
Arsenii Kulikov
5a076df09a feat: allow setting custom debug block provider (#21345)
Co-authored-by: Karl <yh975593284@gmail.com>
2026-01-22 20:40:26 +00:00
YK
f07629eac0 perf: avoid creating RocksDB transactions for legacy MDBX-only nodes (#21325) 2026-01-22 20:30:52 +00:00
Dan Cline
f643e93c35 feat(reth-bench): send-invalid-payload command (#21335) 2026-01-22 19:42:19 +00:00
Matthias Seitz
653362a436 ci: align check-alloy workflow with main clippy job (#21329) 2026-01-22 20:48:53 +01:00
Seola Oh
a02508600c chore(txpool): explicitly deref RwLockReadGuard in PartialEq impl (#21336) 2026-01-22 19:35:00 +00:00
Georgios Konstantopoulos
937a7f226d fix(rpc): use Default for SimulateError to prepare for alloy breaking change (#21319)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-22 19:14:58 +00:00
joshieDo
a0df561117 fix(rocksdb): periodic batch commits in stages to prevent OOM (#21334)
Co-authored-by: Amp <amp@ampcode.com>
2026-01-22 19:04:56 +00:00
Arsenii Kulikov
be5a4ac7a6 feat: bump alloy and alloy-evm (#21337) 2026-01-22 18:43:24 +00:00
Georgios Konstantopoulos
0c854b6f14 fix(net): limit pending pool imports for broadcast transactions (#21254)
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-01-22 18:32:07 +00:00
Georgios Konstantopoulos
28a31cd579 fix: use unwrap_or_else for lazy evaluation of StorageSettings::legacy (#21332) 2026-01-22 17:02:15 +00:00
Brian Picciano
da12451c9c chore(trie): Cleanup unused trie changesets code (#21323) 2026-01-22 16:57:46 +00:00
Georgios Konstantopoulos
247ce3c4e9 feat(storage): warn storage settings diff at startup (#21320)
Co-authored-by: YK <chiayongkang@hotmail.com>
2026-01-22 16:40:10 +00:00
iPLAY888
bf43ebaa29 fix(cli): handle invalid hex in db list --search (#21315) 2026-01-22 16:18:36 +00:00
Matthias Seitz
a0aac13f75 fix: make len() private to satisfy clippy 2026-01-21 12:36:03 +01:00
Matthias Seitz
9f5cf847cc refactor: simplify BalCache to use HashMap + BTreeMap
Replace LRU-based cache with simpler design:
- Use HashMap<BlockHash, Bytes> for O(1) hash lookups
- Use BTreeMap<BlockNumber, BlockHash> as source of truth for eviction
- Evict oldest (lowest) block numbers when at capacity
- Handle reorgs by removing old hash when block number is replaced

This is simpler, more predictable, and removes schnellru dependency.
2026-01-21 12:24:32 +01:00
Matthias Seitz
df1413167a perf: clone only BAL bytes instead of entire payload
Extract num_hash and BAL before calling new_payload to avoid
cloning the entire ExecutionData payload.
2026-01-21 12:00:38 +01:00
Matthias Seitz
3f50a36191 fix: stop get_by_range at first missing block
Ensures caller knows returned BALs correspond to contiguous blocks
[start, start + len)
2026-01-21 11:49:29 +01:00
Matthias Seitz
d750b4976d fix: address clippy warnings
- Collapse nested if statements using let-chains
- Add backticks around BTreeMap in doc comment
2026-01-21 11:46:37 +01:00
Matthias Seitz
7a65d2595d feat(engine-api): add BAL cache for EIP-7928
Introduces an in-memory LRU cache for Block Access Lists (BALs) in the
Engine API. BALs are cached when payloads are validated as VALID via
newPayload.

- Add BalCache with internal Arc for cheap cloning
- Store BALs keyed by block hash with block number index for range queries
- Implement engine_getBALsByHashV1 and engine_getBALsByRangeV1
- Add metrics for cache inserts/hits/misses

Per EIP-7928, the EL should retain BALs for the weak subjectivity period
(~3533 epochs). This initial implementation uses a configurable LRU cache
(default 1024 entries) as a starting point.
2026-01-21 11:35:11 +01:00
91 changed files with 2816 additions and 2823 deletions

2
.github/CODEOWNERS vendored
View File

@@ -37,7 +37,7 @@ crates/storage/db/ @joshieDo
crates/storage/errors/ @joshieDo
crates/storage/libmdbx-rs/ @shekhirin
crates/storage/nippy-jar/ @joshieDo @shekhirin
crates/storage/provider/ @joshieDo @shekhirin
crates/storage/provider/ @joshieDo @shekhirin @yongkangc
crates/storage/storage-api/ @joshieDo
crates/tasks/ @mattsse
crates/tokio-util/ @mattsse

View File

@@ -60,7 +60,6 @@ jobs:
tail -50 Cargo.toml
- name: Check workspace
run: cargo check --workspace --all-features
- name: Check Optimism
run: cargo check -p reth-optimism-node --all-features
run: cargo clippy --workspace --lib --examples --tests --benches --all-features --locked
env:
RUSTFLAGS: -D warnings

131
Cargo.lock generated
View File

@@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "alloy-consensus"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3a590d13de3944675987394715f37537b50b856e3b23a0e66e97d963edbf38"
checksum = "ed1958f0294ecc05ebe7b3c9a8662a3e221c2523b7f2bcd94c7a651efbd510bf"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -149,9 +149,9 @@ dependencies = [
[[package]]
name = "alloy-consensus-any"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f28f769d5ea999f0d8a105e434f483456a15b4e1fcb08edbbbe1650a497ff6d"
checksum = "f752e99497ddc39e22d547d7dfe516af10c979405a034ed90e69b914b7dddeae"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -164,9 +164,9 @@ dependencies = [
[[package]]
name = "alloy-contract"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990fa65cd132a99d3c3795a82b9f93ec82b81c7de3bab0bf26ca5c73286f7186"
checksum = "f2140796bc79150b1b7375daeab99750f0ff5e27b1f8b0aa81ccde229c7f02a2"
dependencies = [
"alloy-consensus",
"alloy-dyn-abi",
@@ -255,19 +255,21 @@ checksum = "6adac476434bf024279164dcdca299309f0c7d1e3557024eb7a83f8d9d01c6b5"
dependencies = [
"alloy-primitives",
"alloy-rlp",
"arbitrary",
"borsh",
"serde",
]
[[package]]
name = "alloy-eips"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09535cbc646b0e0c6fcc12b7597eaed12cf86dff4c4fba9507a61e71b94f30eb"
checksum = "813a67f87e56b38554d18b182616ee5006e8e2bf9df96a0df8bf29dff1d52e3f"
dependencies = [
"alloy-eip2124",
"alloy-eip2930",
"alloy-eip7702",
"alloy-eip7928",
"alloy-primitives",
"alloy-rlp",
"alloy-serde",
@@ -287,9 +289,9 @@ dependencies = [
[[package]]
name = "alloy-evm"
version = "0.26.3"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96827207397445a919a8adc49289b53cc74e48e460411740bba31cec2fc307d"
checksum = "1582933a9fc27c0953220eb4f18f6492ff577822e9a8d848890ff59f6b4f5beb"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -309,9 +311,9 @@ dependencies = [
[[package]]
name = "alloy-genesis"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1005520ccf89fa3d755e46c1d992a9e795466c2e7921be2145ef1f749c5727de"
checksum = "05864eef929c4d28895ae4b4d8ac9c6753c4df66e873b9c8fafc8089b59c1502"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -350,9 +352,9 @@ dependencies = [
[[package]]
name = "alloy-json-rpc"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b626409c98ba43aaaa558361bca21440c88fd30df7542c7484b9c7a1489cdb"
checksum = "d2dd146b3de349a6ffaa4e4e319ab3a90371fb159fb0bddeb1c7bbe8b1792eff"
dependencies = [
"alloy-primitives",
"alloy-sol-types",
@@ -365,9 +367,9 @@ dependencies = [
[[package]]
name = "alloy-network"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89924fdcfeee0e0fa42b1f10af42f92802b5d16be614a70897382565663bf7cf"
checksum = "8c12278ffbb8872dfba3b2f17d8ea5e8503c2df5155d9bc5ee342794bde505c3"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
@@ -391,9 +393,9 @@ dependencies = [
[[package]]
name = "alloy-network-primitives"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0dbe56ff50065713ff8635d8712a0895db3ad7f209db9793ad8fcb6b1734aa"
checksum = "833037c04917bc2031541a60e8249e4ab5500e24c637c1c62e95e963a655d66f"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -404,9 +406,9 @@ dependencies = [
[[package]]
name = "alloy-op-evm"
version = "0.26.3"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54dc5c46a92fc7267055a174d30efb34e2599a0047102a4d38a025ae521435ba"
checksum = "6f19214adae08ea95600c3ede76bcbf0c40b36a263534a8f441a4c732f60e868"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -467,9 +469,9 @@ dependencies = [
[[package]]
name = "alloy-provider"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b56f7a77513308a21a2ba0e9d57785a9d9d2d609e77f4e71a78a1192b83ff2d"
checksum = "eafa840b0afe01c889a3012bb2fde770a544f74eab2e2870303eb0a5fb869c48"
dependencies = [
"alloy-chains",
"alloy-consensus",
@@ -512,9 +514,9 @@ dependencies = [
[[package]]
name = "alloy-pubsub"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94813abbd7baa30c700ea02e7f92319dbcb03bff77aeea92a3a9af7ba19c5c70"
checksum = "57b3a3b3e4efc9f4d30e3326b6bd6811231d16ef94837e18a802b44ca55119e6"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -556,9 +558,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-client"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff01723afc25ec4c5b04de399155bef7b6a96dfde2475492b1b7b4e7a4f46445"
checksum = "12768ae6303ec764905a8a7cd472aea9072f9f9c980d18151e26913da8ae0123"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -582,9 +584,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91bf006bb06b7d812591b6ac33395cb92f46c6a65cda11ee30b348338214f0f"
checksum = "0622d8bcac2f16727590aa33f4c3f05ea98130e7e4b4924bce8be85da5ad0dae"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-engine",
@@ -595,9 +597,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-admin"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b934c3bcdc6617563b45deb36a40881c8230b94d0546ea739dff7edb3aa2f6fd"
checksum = "c38c5ac70457ecc74e87fe1a5a19f936419224ded0eb0636241452412ca92733"
dependencies = [
"alloy-genesis",
"alloy-primitives",
@@ -607,9 +609,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-anvil"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e82145856df8abb1fefabef58cdec0f7d9abf337d4abd50c1ed7e581634acdd"
checksum = "ae8eb0e5d6c48941b61ab76fabab4af66f7d88309a98aa14ad3dec7911c1eba3"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -619,9 +621,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-any"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212ca1c1dab27f531d3858f8b1a2d6bfb2da664be0c1083971078eb7b71abe4b"
checksum = "a1cf5a093e437dfd62df48e480f24e1a3807632358aad6816d7a52875f1c04aa"
dependencies = [
"alloy-consensus-any",
"alloy-rpc-types-eth",
@@ -630,9 +632,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-beacon"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d92a9b4b268fac505ef7fb1dac9bb129d4fd7de7753f22a5b6e9f666f7f7de6"
checksum = "e07949e912479ef3b848e1cf8db54b534bdd7bc58e6c23f28ea9488960990c8c"
dependencies = [
"alloy-eips",
"alloy-primitives",
@@ -650,9 +652,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-debug"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab1ebed118b701c497e6541d2d11dfa6f3c6ae31a3c52999daa802fcdcc16b7"
checksum = "925ff0f48c2169c050f0ae7a82769bdf3f45723d6742ebb6a5efb4ed2f491b26"
dependencies = [
"alloy-primitives",
"derive_more",
@@ -662,9 +664,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-engine"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232f00fcbcd3ee3b9399b96223a8fc884d17742a70a44f9d7cef275f93e6e872"
checksum = "336ef381c7409f23c69f6e79bddc1917b6e832cff23e7a5cf84b9381d53582e6"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -683,9 +685,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-eth"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5715d0bf7efbd360873518bd9f6595762136b5327a9b759a8c42ccd9b5e44945"
checksum = "28e97603095020543a019ab133e0e3dc38cd0819f19f19bdd70c642404a54751"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
@@ -705,9 +707,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-mev"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7b61941d2add2ee64646612d3eda92cbbde8e6c933489760b6222c8898c79be"
checksum = "2805153975e25d38e37ee100880e642d5b24e421ed3014a7d2dae1d9be77562e"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -720,9 +722,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-trace"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9763cc931a28682bd4b9a68af90057b0fbe80e2538a82251afd69d7ae00bbebf"
checksum = "f1aec4e1c66505d067933ea1a949a4fb60a19c4cfc2f109aa65873ea99e62ea8"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -734,9 +736,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-txpool"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "359a8caaa98cb49eed62d03f5bc511dd6dd5dee292238e8627a6e5690156df0f"
checksum = "25b73c1d6e4f1737a20d246dad5a0abd6c1b76ec4c3d153684ef8c6f1b6bb4f4"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -746,9 +748,9 @@ dependencies = [
[[package]]
name = "alloy-serde"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ed8531cae8d21ee1c6571d0995f8c9f0652a6ef6452fde369283edea6ab7138"
checksum = "946a0d413dbb5cd9adba0de5f8a1a34d5b77deda9b69c1d7feed8fc875a1aa26"
dependencies = [
"alloy-primitives",
"arbitrary",
@@ -758,9 +760,9 @@ dependencies = [
[[package]]
name = "alloy-signer"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb10ccd49d0248df51063fce6b716f68a315dd912d55b32178c883fd48b4021d"
checksum = "2f7481dc8316768f042495eaf305d450c32defbc9bce09d8bf28afcd956895bb"
dependencies = [
"alloy-primitives",
"async-trait",
@@ -773,9 +775,9 @@ dependencies = [
[[package]]
name = "alloy-signer-local"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4d992d44e6c414ece580294abbadb50e74cfd4eaa69787350a4dfd4b20eaa1b"
checksum = "1259dac1f534a4c66c1d65237c89915d0010a2a91d6c3b0bada24dc5ee0fb917"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -862,9 +864,9 @@ dependencies = [
[[package]]
name = "alloy-transport"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f50a9516736d22dd834cc2240e5bf264f338667cc1d9e514b55ec5a78b987ca"
checksum = "78f169b85eb9334871db986e7eaf59c58a03d86a30cc68b846573d47ed0656bb"
dependencies = [
"alloy-json-rpc",
"auto_impl",
@@ -885,9 +887,9 @@ dependencies = [
[[package]]
name = "alloy-transport-http"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a18b541a6197cf9a084481498a766fdf32fefda0c35ea6096df7d511025e9f1"
checksum = "019821102e70603e2c141954418255bec539ef64ac4117f8e84fb493769acf73"
dependencies = [
"alloy-json-rpc",
"alloy-transport",
@@ -900,9 +902,9 @@ dependencies = [
[[package]]
name = "alloy-transport-ipc"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8075911680ebc537578cacf9453464fd394822a0f68614884a9c63f9fbaf5e89"
checksum = "e574ca2f490fb5961d2cdd78188897392c46615cd88b35c202d34bbc31571a81"
dependencies = [
"alloy-json-rpc",
"alloy-pubsub",
@@ -920,9 +922,9 @@ dependencies = [
[[package]]
name = "alloy-transport-ws"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "921d37a57e2975e5215f7dd0f28873ed5407c7af630d4831a4b5c737de4b0b8b"
checksum = "b92dea6996269769f74ae56475570e3586910661e037b7b52d50c9641f76c68f"
dependencies = [
"alloy-pubsub",
"alloy-transport",
@@ -957,9 +959,9 @@ dependencies = [
[[package]]
name = "alloy-tx-macros"
version = "1.4.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2289a842d02fe63f8c466db964168bb2c7a9fdfb7b24816dbb17d45520575fb"
checksum = "45ceac797eb8a56bdf5ab1fab353072c17d472eab87645ca847afe720db3246d"
dependencies = [
"darling 0.21.3",
"proc-macro2",
@@ -10626,6 +10628,7 @@ dependencies = [
"jsonrpsee-core",
"jsonrpsee-types",
"metrics",
"parking_lot",
"reth-chainspec",
"reth-engine-primitives",
"reth-ethereum-engine-primitives",

View File

@@ -485,10 +485,10 @@ revm-inspectors = "0.34.0"
# eth
alloy-chains = { version = "0.2.5", default-features = false }
alloy-dyn-abi = "1.4.3"
alloy-dyn-abi = "1.5.2"
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.3.0", default-features = false }
alloy-evm = { version = "0.26.3", default-features = false }
alloy-evm = { version = "0.27.0", default-features = false }
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] }
alloy-sol-macro = "1.5.0"
@@ -497,36 +497,36 @@ alloy-trie = { version = "0.9.1", default-features = false }
alloy-hardforks = "0.4.5"
alloy-consensus = { version = "1.4.3", default-features = false }
alloy-contract = { version = "1.4.3", default-features = false }
alloy-eips = { version = "1.4.3", default-features = false }
alloy-genesis = { version = "1.4.3", default-features = false }
alloy-json-rpc = { version = "1.4.3", default-features = false }
alloy-network = { version = "1.4.3", default-features = false }
alloy-network-primitives = { version = "1.4.3", default-features = false }
alloy-provider = { version = "1.4.3", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.4.3", default-features = false }
alloy-rpc-client = { version = "1.4.3", default-features = false }
alloy-rpc-types = { version = "1.4.3", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.4.3", default-features = false }
alloy-rpc-types-anvil = { version = "1.4.3", default-features = false }
alloy-rpc-types-beacon = { version = "1.4.3", default-features = false }
alloy-rpc-types-debug = { version = "1.4.3", default-features = false }
alloy-rpc-types-engine = { version = "1.4.3", default-features = false }
alloy-rpc-types-eth = { version = "1.4.3", default-features = false }
alloy-rpc-types-mev = { version = "1.4.3", default-features = false }
alloy-rpc-types-trace = { version = "1.4.3", default-features = false }
alloy-rpc-types-txpool = { version = "1.4.3", default-features = false }
alloy-serde = { version = "1.4.3", default-features = false }
alloy-signer = { version = "1.4.3", default-features = false }
alloy-signer-local = { version = "1.4.3", default-features = false }
alloy-transport = { version = "1.4.3" }
alloy-transport-http = { version = "1.4.3", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.4.3", default-features = false }
alloy-transport-ws = { version = "1.4.3", default-features = false }
alloy-consensus = { version = "1.5.2", default-features = false }
alloy-contract = { version = "1.5.2", default-features = false }
alloy-eips = { version = "1.5.2", default-features = false }
alloy-genesis = { version = "1.5.2", default-features = false }
alloy-json-rpc = { version = "1.5.2", default-features = false }
alloy-network = { version = "1.5.2", default-features = false }
alloy-network-primitives = { version = "1.5.2", default-features = false }
alloy-provider = { version = "1.5.2", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.5.2", default-features = false }
alloy-rpc-client = { version = "1.5.2", default-features = false }
alloy-rpc-types = { version = "1.5.2", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.5.2", default-features = false }
alloy-rpc-types-anvil = { version = "1.5.2", default-features = false }
alloy-rpc-types-beacon = { version = "1.5.2", default-features = false }
alloy-rpc-types-debug = { version = "1.5.2", default-features = false }
alloy-rpc-types-engine = { version = "1.5.2", default-features = false }
alloy-rpc-types-eth = { version = "1.5.2", default-features = false }
alloy-rpc-types-mev = { version = "1.5.2", default-features = false }
alloy-rpc-types-trace = { version = "1.5.2", default-features = false }
alloy-rpc-types-txpool = { version = "1.5.2", default-features = false }
alloy-serde = { version = "1.5.2", default-features = false }
alloy-signer = { version = "1.5.2", default-features = false }
alloy-signer-local = { version = "1.5.2", default-features = false }
alloy-transport = { version = "1.5.2" }
alloy-transport-http = { version = "1.5.2", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.5.2", default-features = false }
alloy-transport-ws = { version = "1.5.2", default-features = false }
# op
alloy-op-evm = { version = "0.26.3", default-features = false }
alloy-op-evm = { version = "0.27.0", default-features = false }
alloy-op-hardforks = "0.4.4"
op-alloy-rpc-types = { version = "0.23.1", default-features = false }
op-alloy-rpc-types-engine = { version = "0.23.1", default-features = false }
@@ -790,8 +790,8 @@ ipnet = "2.11"
# jsonrpsee-http-client = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# jsonrpsee-types = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "a69f0b45a6b0286e16072cb8399e02ce6ceca353" }
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "a69f0b45a6b0286e16072cb8399e02ce6ceca353" }
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "df124c0" }
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "df124c0" }
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "3020ea8" }

View File

@@ -32,7 +32,7 @@ alloy-eips.workspace = true
alloy-json-rpc.workspace = true
alloy-consensus.workspace = true
alloy-network.workspace = true
alloy-primitives.workspace = true
alloy-primitives = { workspace = true, features = ["rand"] }
alloy-provider = { workspace = true, features = ["engine-api", "pubsub", "reqwest-rustls-tls"], default-features = false }
alloy-pubsub.workspace = true
alloy-rpc-client = { workspace = true, features = ["pubsub"] }

View File

@@ -3,7 +3,7 @@
use crate::{
authenticated_transport::AuthenticatedTransportConnect,
bench::{
helpers::{build_payload, prepare_payload_request, rpc_block_to_header},
helpers::{build_payload, parse_gas_limit, prepare_payload_request, rpc_block_to_header},
output::GasRampPayloadFile,
},
valid_payload::{call_forkchoice_updated, call_new_payload, payload_to_new_payload},
@@ -22,29 +22,6 @@ use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIM
use std::{path::PathBuf, time::Instant};
use tracing::info;
/// Parses a gas limit value with optional suffix: K for thousand, M for million, G for billion.
///
/// Examples: "30000000", "30M", "1G", "2G"
fn parse_gas_limit(s: &str) -> eyre::Result<u64> {
let s = s.trim();
if s.is_empty() {
return Err(eyre::eyre!("empty value"));
}
let (num_str, multiplier) = if let Some(prefix) = s.strip_suffix(['G', 'g']) {
(prefix, 1_000_000_000u64)
} else if let Some(prefix) = s.strip_suffix(['M', 'm']) {
(prefix, 1_000_000u64)
} else if let Some(prefix) = s.strip_suffix(['K', 'k']) {
(prefix, 1_000u64)
} else {
(s, 1u64)
};
let base: u64 = num_str.trim().parse()?;
base.checked_mul(multiplier).ok_or_else(|| eyre::eyre!("value overflow"))
}
/// `reth benchmark gas-limit-ramp` command.
#[derive(Debug, Parser)]
pub struct Command {
@@ -237,50 +214,3 @@ const fn should_stop(mode: RampMode, blocks_processed: u64, current_gas_limit: u
RampMode::TargetGasLimit(target) => current_gas_limit >= target,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_gas_limit_plain_number() {
assert_eq!(parse_gas_limit("30000000").unwrap(), 30_000_000);
assert_eq!(parse_gas_limit("1").unwrap(), 1);
assert_eq!(parse_gas_limit("0").unwrap(), 0);
}
#[test]
fn test_parse_gas_limit_k_suffix() {
assert_eq!(parse_gas_limit("1K").unwrap(), 1_000);
assert_eq!(parse_gas_limit("30k").unwrap(), 30_000);
assert_eq!(parse_gas_limit("100K").unwrap(), 100_000);
}
#[test]
fn test_parse_gas_limit_m_suffix() {
assert_eq!(parse_gas_limit("1M").unwrap(), 1_000_000);
assert_eq!(parse_gas_limit("30m").unwrap(), 30_000_000);
assert_eq!(parse_gas_limit("100M").unwrap(), 100_000_000);
}
#[test]
fn test_parse_gas_limit_g_suffix() {
assert_eq!(parse_gas_limit("1G").unwrap(), 1_000_000_000);
assert_eq!(parse_gas_limit("2g").unwrap(), 2_000_000_000);
assert_eq!(parse_gas_limit("10G").unwrap(), 10_000_000_000);
}
#[test]
fn test_parse_gas_limit_with_whitespace() {
assert_eq!(parse_gas_limit(" 1G ").unwrap(), 1_000_000_000);
assert_eq!(parse_gas_limit("2 M").unwrap(), 2_000_000);
}
#[test]
fn test_parse_gas_limit_errors() {
assert!(parse_gas_limit("").is_err());
assert!(parse_gas_limit("abc").is_err());
assert!(parse_gas_limit("G").is_err());
assert!(parse_gas_limit("-1G").is_err());
}
}

View File

@@ -3,7 +3,9 @@
//! This command fetches transactions from existing blocks and packs them into a single
//! large block using the `testing_buildBlockV1` RPC endpoint.
use crate::authenticated_transport::AuthenticatedTransportConnect;
use crate::{
authenticated_transport::AuthenticatedTransportConnect, bench::helpers::parse_gas_limit,
};
use alloy_eips::{BlockNumberOrTag, Typed2718};
use alloy_primitives::{Bytes, B256};
use alloy_provider::{ext::EngineApi, network::AnyNetwork, Provider, RootProvider};
@@ -202,7 +204,9 @@ pub struct Command {
jwt_secret: std::path::PathBuf,
/// Target gas to pack into the block.
#[arg(long, value_name = "TARGET_GAS", default_value = "30000000")]
/// Accepts short notation: K for thousand, M for million, G for billion (e.g., 1G = 1
/// billion).
#[arg(long, value_name = "TARGET_GAS", default_value = "30000000", value_parser = parse_gas_limit)]
target_gas: u64,
/// Starting block number to fetch transactions from.

View File

@@ -1,6 +1,29 @@
//! Common helpers for reth-bench commands.
use crate::valid_payload::call_forkchoice_updated;
/// Parses a gas limit value with optional suffix: K for thousand, M for million, G for billion.
///
/// Examples: "30000000", "30M", "1G", "2G"
pub(crate) fn parse_gas_limit(s: &str) -> eyre::Result<u64> {
let s = s.trim();
if s.is_empty() {
return Err(eyre::eyre!("empty value"));
}
let (num_str, multiplier) = if let Some(prefix) = s.strip_suffix(['G', 'g']) {
(prefix, 1_000_000_000u64)
} else if let Some(prefix) = s.strip_suffix(['M', 'm']) {
(prefix, 1_000_000u64)
} else if let Some(prefix) = s.strip_suffix(['K', 'k']) {
(prefix, 1_000u64)
} else {
(s, 1u64)
};
let base: u64 = num_str.trim().parse()?;
base.checked_mul(multiplier).ok_or_else(|| eyre::eyre!("value overflow"))
}
use alloy_consensus::Header;
use alloy_eips::eip4844::kzg_to_versioned_hash;
use alloy_primitives::{Address, B256};
@@ -194,3 +217,50 @@ pub(crate) async fn get_payload_with_sidecar(
_ => panic!("This tool does not support getPayload versions past v5"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_gas_limit_plain_number() {
assert_eq!(parse_gas_limit("30000000").unwrap(), 30_000_000);
assert_eq!(parse_gas_limit("1").unwrap(), 1);
assert_eq!(parse_gas_limit("0").unwrap(), 0);
}
#[test]
fn test_parse_gas_limit_k_suffix() {
assert_eq!(parse_gas_limit("1K").unwrap(), 1_000);
assert_eq!(parse_gas_limit("30k").unwrap(), 30_000);
assert_eq!(parse_gas_limit("100K").unwrap(), 100_000);
}
#[test]
fn test_parse_gas_limit_m_suffix() {
assert_eq!(parse_gas_limit("1M").unwrap(), 1_000_000);
assert_eq!(parse_gas_limit("30m").unwrap(), 30_000_000);
assert_eq!(parse_gas_limit("100M").unwrap(), 100_000_000);
}
#[test]
fn test_parse_gas_limit_g_suffix() {
assert_eq!(parse_gas_limit("1G").unwrap(), 1_000_000_000);
assert_eq!(parse_gas_limit("2g").unwrap(), 2_000_000_000);
assert_eq!(parse_gas_limit("10G").unwrap(), 10_000_000_000);
}
#[test]
fn test_parse_gas_limit_with_whitespace() {
assert_eq!(parse_gas_limit(" 1G ").unwrap(), 1_000_000_000);
assert_eq!(parse_gas_limit("2 M").unwrap(), 2_000_000);
}
#[test]
fn test_parse_gas_limit_errors() {
assert!(parse_gas_limit("").is_err());
assert!(parse_gas_limit("abc").is_err());
assert!(parse_gas_limit("G").is_err());
assert!(parse_gas_limit("-1G").is_err());
}
}

View File

@@ -16,6 +16,7 @@ mod new_payload_fcu;
mod new_payload_only;
mod output;
mod replay_payloads;
mod send_invalid_payload;
mod send_payload;
/// `reth bench` command
@@ -74,6 +75,18 @@ pub enum Subcommands {
/// `reth-bench replay-payloads --payload-dir ./payloads --engine-rpc-url
/// http://localhost:8551 --jwt-secret ~/.local/share/reth/mainnet/jwt.hex`
ReplayPayloads(replay_payloads::Command),
/// Generate and send an invalid `engine_newPayload` request for testing.
///
/// Takes a valid block and modifies fields to make it invalid, allowing you to test
/// Engine API rejection behavior. Block hash is recalculated after modifications
/// unless `--invalid-block-hash` or `--skip-hash-recalc` is used.
///
/// Example:
///
/// `cast block latest --full --json | reth-bench send-invalid-payload --rpc-url localhost:5000
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex) --invalid-state-root`
SendInvalidPayload(Box<send_invalid_payload::Command>),
}
impl BenchmarkCommand {
@@ -89,6 +102,7 @@ impl BenchmarkCommand {
Subcommands::SendPayload(command) => command.execute(ctx).await,
Subcommands::GenerateBigBlock(command) => command.execute(ctx).await,
Subcommands::ReplayPayloads(command) => command.execute(ctx).await,
Subcommands::SendInvalidPayload(command) => (*command).execute(ctx).await,
}
}

View File

@@ -0,0 +1,219 @@
use alloy_eips::eip4895::Withdrawal;
use alloy_primitives::{Address, Bloom, Bytes, B256, U256};
use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3};
/// Configuration for invalidating payload fields
#[derive(Debug, Default)]
pub(super) struct InvalidationConfig {
// Explicit value overrides (Option<T>)
pub(super) parent_hash: Option<B256>,
pub(super) fee_recipient: Option<Address>,
pub(super) state_root: Option<B256>,
pub(super) receipts_root: Option<B256>,
pub(super) logs_bloom: Option<Bloom>,
pub(super) prev_randao: Option<B256>,
pub(super) block_number: Option<u64>,
pub(super) gas_limit: Option<u64>,
pub(super) gas_used: Option<u64>,
pub(super) timestamp: Option<u64>,
pub(super) extra_data: Option<Bytes>,
pub(super) base_fee_per_gas: Option<u64>,
pub(super) block_hash: Option<B256>,
pub(super) blob_gas_used: Option<u64>,
pub(super) excess_blob_gas: Option<u64>,
// Auto-invalidation flags
pub(super) invalidate_parent_hash: bool,
pub(super) invalidate_state_root: bool,
pub(super) invalidate_receipts_root: bool,
pub(super) invalidate_gas_used: bool,
pub(super) invalidate_block_number: bool,
pub(super) invalidate_timestamp: bool,
pub(super) invalidate_base_fee: bool,
pub(super) invalidate_transactions: bool,
pub(super) invalidate_block_hash: bool,
pub(super) invalidate_withdrawals: bool,
pub(super) invalidate_blob_gas_used: bool,
pub(super) invalidate_excess_blob_gas: bool,
}
impl InvalidationConfig {
/// Returns true if `block_hash` is being explicitly set or auto-invalidated.
/// When true, the caller should skip recalculating the block hash since it will be overwritten.
pub(super) const fn should_skip_hash_recalc(&self) -> bool {
self.block_hash.is_some() || self.invalidate_block_hash
}
/// Applies invalidations to a V1 payload, returns list of what was changed.
pub(super) fn apply_to_payload_v1(&self, payload: &mut ExecutionPayloadV1) -> Vec<String> {
let mut changes = Vec::new();
// Explicit value overrides
if let Some(parent_hash) = self.parent_hash {
payload.parent_hash = parent_hash;
changes.push(format!("parent_hash = {parent_hash}"));
}
if let Some(fee_recipient) = self.fee_recipient {
payload.fee_recipient = fee_recipient;
changes.push(format!("fee_recipient = {fee_recipient}"));
}
if let Some(state_root) = self.state_root {
payload.state_root = state_root;
changes.push(format!("state_root = {state_root}"));
}
if let Some(receipts_root) = self.receipts_root {
payload.receipts_root = receipts_root;
changes.push(format!("receipts_root = {receipts_root}"));
}
if let Some(logs_bloom) = self.logs_bloom {
payload.logs_bloom = logs_bloom;
changes.push("logs_bloom = <custom>".to_string());
}
if let Some(prev_randao) = self.prev_randao {
payload.prev_randao = prev_randao;
changes.push(format!("prev_randao = {prev_randao}"));
}
if let Some(block_number) = self.block_number {
payload.block_number = block_number;
changes.push(format!("block_number = {block_number}"));
}
if let Some(gas_limit) = self.gas_limit {
payload.gas_limit = gas_limit;
changes.push(format!("gas_limit = {gas_limit}"));
}
if let Some(gas_used) = self.gas_used {
payload.gas_used = gas_used;
changes.push(format!("gas_used = {gas_used}"));
}
if let Some(timestamp) = self.timestamp {
payload.timestamp = timestamp;
changes.push(format!("timestamp = {timestamp}"));
}
if let Some(ref extra_data) = self.extra_data {
payload.extra_data = extra_data.clone();
changes.push(format!("extra_data = {} bytes", extra_data.len()));
}
if let Some(base_fee_per_gas) = self.base_fee_per_gas {
payload.base_fee_per_gas = U256::from_limbs([base_fee_per_gas, 0, 0, 0]);
changes.push(format!("base_fee_per_gas = {base_fee_per_gas}"));
}
if let Some(block_hash) = self.block_hash {
payload.block_hash = block_hash;
changes.push(format!("block_hash = {block_hash}"));
}
// Auto-invalidation flags
if self.invalidate_parent_hash {
let random_hash = B256::random();
payload.parent_hash = random_hash;
changes.push(format!("parent_hash = {random_hash} (auto-invalidated: random)"));
}
if self.invalidate_state_root {
payload.state_root = B256::ZERO;
changes.push("state_root = ZERO (auto-invalidated: empty trie root)".to_string());
}
if self.invalidate_receipts_root {
payload.receipts_root = B256::ZERO;
changes.push("receipts_root = ZERO (auto-invalidated)".to_string());
}
if self.invalidate_gas_used {
let invalid_gas = payload.gas_limit + 1;
payload.gas_used = invalid_gas;
changes.push(format!("gas_used = {invalid_gas} (auto-invalidated: exceeds gas_limit)"));
}
if self.invalidate_block_number {
let invalid_number = payload.block_number + 999;
payload.block_number = invalid_number;
changes.push(format!("block_number = {invalid_number} (auto-invalidated: huge gap)"));
}
if self.invalidate_timestamp {
payload.timestamp = 0;
changes.push("timestamp = 0 (auto-invalidated: impossibly old)".to_string());
}
if self.invalidate_base_fee {
payload.base_fee_per_gas = U256::ZERO;
changes
.push("base_fee_per_gas = 0 (auto-invalidated: invalid post-London)".to_string());
}
if self.invalidate_transactions {
let invalid_tx = Bytes::from_static(&[0xff, 0xff, 0xff]);
payload.transactions.insert(0, invalid_tx);
changes.push("transactions = prepended invalid RLP (auto-invalidated)".to_string());
}
if self.invalidate_block_hash {
let random_hash = B256::random();
payload.block_hash = random_hash;
changes.push(format!("block_hash = {random_hash} (auto-invalidated: random)"));
}
changes
}
/// Applies invalidations to a V2 payload, returns list of what was changed.
pub(super) fn apply_to_payload_v2(&self, payload: &mut ExecutionPayloadV2) -> Vec<String> {
let mut changes = self.apply_to_payload_v1(&mut payload.payload_inner);
// Handle withdrawals invalidation (V2+)
if self.invalidate_withdrawals {
let fake_withdrawal = Withdrawal {
index: u64::MAX,
validator_index: u64::MAX,
address: Address::ZERO,
amount: u64::MAX,
};
payload.withdrawals.push(fake_withdrawal);
changes.push("withdrawals = added fake withdrawal (auto-invalidated)".to_string());
}
changes
}
/// Applies invalidations to a V3 payload, returns list of what was changed.
pub(super) fn apply_to_payload_v3(&self, payload: &mut ExecutionPayloadV3) -> Vec<String> {
let mut changes = self.apply_to_payload_v2(&mut payload.payload_inner);
// Explicit overrides for V3 fields
if let Some(blob_gas_used) = self.blob_gas_used {
payload.blob_gas_used = blob_gas_used;
changes.push(format!("blob_gas_used = {blob_gas_used}"));
}
if let Some(excess_blob_gas) = self.excess_blob_gas {
payload.excess_blob_gas = excess_blob_gas;
changes.push(format!("excess_blob_gas = {excess_blob_gas}"));
}
// Auto-invalidation for V3 fields
if self.invalidate_blob_gas_used {
payload.blob_gas_used = u64::MAX;
changes.push("blob_gas_used = MAX (auto-invalidated)".to_string());
}
if self.invalidate_excess_blob_gas {
payload.excess_blob_gas = u64::MAX;
changes.push("excess_blob_gas = MAX (auto-invalidated)".to_string());
}
changes
}
}

View File

@@ -0,0 +1,367 @@
//! Command for sending invalid payloads to test Engine API rejection.
mod invalidation;
use invalidation::InvalidationConfig;
use alloy_primitives::{Address, B256};
use alloy_provider::network::AnyRpcBlock;
use alloy_rpc_types_engine::ExecutionPayload;
use clap::Parser;
use eyre::{OptionExt, Result};
use op_alloy_consensus::OpTxEnvelope;
use reth_cli_runner::CliContext;
use std::io::{BufReader, Read, Write};
/// Command for generating and sending an invalid `engine_newPayload` request.
///
/// Takes a valid block and modifies fields to make it invalid for testing
/// Engine API rejection behavior. Block hash is recalculated after modifications
/// unless `--invalidate-block-hash` or `--skip-hash-recalc` is used.
#[derive(Debug, Parser)]
pub struct Command {
// ==================== Input Options ====================
/// Path to the JSON file containing the block. If not specified, stdin will be used.
#[arg(short, long, help_heading = "Input Options")]
path: Option<String>,
/// The engine RPC URL to use.
#[arg(
short,
long,
help_heading = "Input Options",
required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
required_unless_present("mode")
)]
rpc_url: Option<String>,
/// The JWT secret to use. Can be either a path to a file containing the secret or the secret
/// itself.
#[arg(short, long, help_heading = "Input Options")]
jwt_secret: Option<String>,
/// The newPayload version to use (3 or 4).
#[arg(long, default_value_t = 3, help_heading = "Input Options")]
new_payload_version: u8,
/// The output mode to use.
#[arg(long, value_enum, default_value = "execute", help_heading = "Input Options")]
mode: Mode,
// ==================== Explicit Value Overrides ====================
/// Override the parent hash with a specific value.
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
parent_hash: Option<B256>,
/// Override the fee recipient (coinbase) with a specific address.
#[arg(long, value_name = "ADDR", help_heading = "Explicit Value Overrides")]
fee_recipient: Option<Address>,
/// Override the state root with a specific value.
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
state_root: Option<B256>,
/// Override the receipts root with a specific value.
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
receipts_root: Option<B256>,
/// Override the block number with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
block_number: Option<u64>,
/// Override the gas limit with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
gas_limit: Option<u64>,
/// Override the gas used with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
gas_used: Option<u64>,
/// Override the timestamp with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
timestamp: Option<u64>,
/// Override the base fee per gas with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
base_fee_per_gas: Option<u64>,
/// Override the block hash with a specific value (skips hash recalculation).
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
block_hash: Option<B256>,
/// Override the blob gas used with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
blob_gas_used: Option<u64>,
/// Override the excess blob gas with a specific value.
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
excess_blob_gas: Option<u64>,
/// Override the parent beacon block root with a specific value.
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
parent_beacon_block_root: Option<B256>,
/// Override the requests hash with a specific value (EIP-7685).
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
requests_hash: Option<B256>,
// ==================== Auto-Invalidation Flags ====================
/// Invalidate the parent hash by setting it to a random value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_parent_hash: bool,
/// Invalidate the state root by setting it to a random value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_state_root: bool,
/// Invalidate the receipts root by setting it to a random value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_receipts_root: bool,
/// Invalidate the gas used by setting it to an incorrect value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_gas_used: bool,
/// Invalidate the block number by setting it to an incorrect value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_block_number: bool,
/// Invalidate the timestamp by setting it to an incorrect value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_timestamp: bool,
/// Invalidate the base fee by setting it to an incorrect value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_base_fee: bool,
/// Invalidate the transactions by modifying them.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_transactions: bool,
/// Invalidate the block hash by not recalculating it after modifications.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_block_hash: bool,
/// Invalidate the withdrawals by modifying them.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_withdrawals: bool,
/// Invalidate the blob gas used by setting it to an incorrect value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_blob_gas_used: bool,
/// Invalidate the excess blob gas by setting it to an incorrect value.
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_excess_blob_gas: bool,
/// Invalidate the requests hash by setting it to a random value (EIP-7685).
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
invalidate_requests_hash: bool,
// ==================== Meta Flags ====================
/// Skip block hash recalculation after modifications.
#[arg(long, default_value_t = false, help_heading = "Meta Flags")]
skip_hash_recalc: bool,
/// Print what would be done without actually sending the payload.
#[arg(long, default_value_t = false, help_heading = "Meta Flags")]
dry_run: bool,
}
#[derive(Debug, Clone, clap::ValueEnum)]
enum Mode {
/// Execute the `cast` command. This works with blocks of any size, because it pipes the
/// payload into the `cast` command.
Execute,
/// Print the `cast` command. Caution: this may not work with large blocks because of the
/// command length limit.
Cast,
/// Print the JSON payload. Can be piped into `cast` command if the block is small enough.
Json,
}
impl Command {
/// Read input from either a file or stdin
fn read_input(&self) -> Result<String> {
Ok(match &self.path {
Some(path) => reth_fs_util::read_to_string(path)?,
None => String::from_utf8(
BufReader::new(std::io::stdin()).bytes().collect::<Result<Vec<_>, _>>()?,
)?,
})
}
/// Load JWT secret from either a file or use the provided string directly
fn load_jwt_secret(&self) -> Result<Option<String>> {
match &self.jwt_secret {
Some(secret) => match std::fs::read_to_string(secret) {
Ok(contents) => Ok(Some(contents.trim().to_string())),
Err(_) => Ok(Some(secret.clone())),
},
None => Ok(None),
}
}
/// Build `InvalidationConfig` from command flags
const fn build_invalidation_config(&self) -> InvalidationConfig {
InvalidationConfig {
parent_hash: self.parent_hash,
fee_recipient: self.fee_recipient,
state_root: self.state_root,
receipts_root: self.receipts_root,
logs_bloom: None,
prev_randao: None,
block_number: self.block_number,
gas_limit: self.gas_limit,
gas_used: self.gas_used,
timestamp: self.timestamp,
extra_data: None,
base_fee_per_gas: self.base_fee_per_gas,
block_hash: self.block_hash,
blob_gas_used: self.blob_gas_used,
excess_blob_gas: self.excess_blob_gas,
invalidate_parent_hash: self.invalidate_parent_hash,
invalidate_state_root: self.invalidate_state_root,
invalidate_receipts_root: self.invalidate_receipts_root,
invalidate_gas_used: self.invalidate_gas_used,
invalidate_block_number: self.invalidate_block_number,
invalidate_timestamp: self.invalidate_timestamp,
invalidate_base_fee: self.invalidate_base_fee,
invalidate_transactions: self.invalidate_transactions,
invalidate_block_hash: self.invalidate_block_hash,
invalidate_withdrawals: self.invalidate_withdrawals,
invalidate_blob_gas_used: self.invalidate_blob_gas_used,
invalidate_excess_blob_gas: self.invalidate_excess_blob_gas,
}
}
/// Execute the command
pub async fn execute(self, _ctx: CliContext) -> Result<()> {
let block_json = self.read_input()?;
let jwt_secret = self.load_jwt_secret()?;
let block = serde_json::from_str::<AnyRpcBlock>(&block_json)?
.into_inner()
.map_header(|header| header.map(|h| h.into_header_with_defaults()))
.try_map_transactions(|tx| tx.try_into_either::<OpTxEnvelope>())?
.into_consensus();
let config = self.build_invalidation_config();
let parent_beacon_block_root =
self.parent_beacon_block_root.or(block.header.parent_beacon_block_root);
let blob_versioned_hashes =
block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
let use_v4 = block.header.requests_hash.is_some();
let requests_hash = self.requests_hash.or(block.header.requests_hash);
let mut execution_payload = ExecutionPayload::from_block_slow(&block).0;
let changes = match &mut execution_payload {
ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
};
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
if !skip_recalc {
let new_hash = match execution_payload.clone().into_block_raw() {
Ok(block) => block.header.hash_slow(),
Err(e) => {
eprintln!(
"Warning: Could not recalculate block hash: {e}. Using original hash."
);
match &execution_payload {
ExecutionPayload::V1(p) => p.block_hash,
ExecutionPayload::V2(p) => p.payload_inner.block_hash,
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
}
}
};
match &mut execution_payload {
ExecutionPayload::V1(p) => p.block_hash = new_hash,
ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
}
}
if self.dry_run {
println!("=== Dry Run ===");
println!("Changes that would be applied:");
for change in &changes {
println!(" - {}", change);
}
if changes.is_empty() {
println!(" (no changes)");
}
if skip_recalc {
println!(" - Block hash recalculation: SKIPPED");
} else {
println!(" - Block hash recalculation: PERFORMED");
}
println!("\nResulting payload JSON:");
let json = serde_json::to_string_pretty(&execution_payload)?;
println!("{}", json);
return Ok(());
}
let json_request = if use_v4 {
serde_json::to_string(&(
execution_payload,
blob_versioned_hashes,
parent_beacon_block_root,
requests_hash.unwrap_or_default(),
))?
} else {
serde_json::to_string(&(
execution_payload,
blob_versioned_hashes,
parent_beacon_block_root,
))?
};
match self.mode {
Mode::Execute => {
let mut command = std::process::Command::new("cast");
let method = if use_v4 { "engine_newPayloadV4" } else { "engine_newPayloadV3" };
command.arg("rpc").arg(method).arg("--raw");
if let Some(rpc_url) = self.rpc_url {
command.arg("--rpc-url").arg(rpc_url);
}
if let Some(secret) = &jwt_secret {
command.arg("--jwt-secret").arg(secret);
}
let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
process
.stdin
.take()
.ok_or_eyre("stdin not available")?
.write_all(json_request.as_bytes())?;
process.wait()?;
}
Mode::Cast => {
let mut cmd = format!(
"cast rpc engine_newPayloadV{} --raw '{}'",
self.new_payload_version, json_request
);
if let Some(rpc_url) = self.rpc_url {
cmd += &format!(" --rpc-url {rpc_url}");
}
if let Some(secret) = &jwt_secret {
cmd += &format!(" --jwt-secret {secret}");
}
println!("{cmd}");
}
Mode::Json => {
println!("{json_request}");
}
}
Ok(())
}
}

View File

@@ -25,8 +25,8 @@
//! - `jemalloc-unprefixed`: Uses unprefixed jemalloc symbols.
//! - `tracy-allocator`: Enables [Tracy](https://github.com/wolfpld/tracy) profiler allocator
//! integration for memory profiling.
//! - `snmalloc`: Uses [snmalloc](https://github.com/snmalloc/snmalloc) as the global allocator. Use
//! `--no-default-features` when enabling this, as jemalloc takes precedence.
//! - `snmalloc`: Uses [snmalloc](https://github.com/microsoft/snmalloc) as the global allocator.
//! Use `--no-default-features` when enabling this, as jemalloc takes precedence.
//! - `snmalloc-native`: Uses snmalloc with native CPU optimizations. Use `--no-default-features`
//! when enabling this.
//!

View File

@@ -61,19 +61,21 @@ impl Command {
}
/// Generate [`ListFilter`] from command.
pub fn list_filter(&self) -> ListFilter {
let search = self
.search
.as_ref()
.map(|search| {
pub fn list_filter(&self) -> eyre::Result<ListFilter> {
let search = match self.search.as_deref() {
Some(search) => {
if let Some(search) = search.strip_prefix("0x") {
return hex::decode(search).unwrap()
hex::decode(search).wrap_err(
"Invalid hex content after 0x prefix in --search (expected valid hex like 0xdeadbeef).",
)?
} else {
search.as_bytes().to_vec()
}
search.as_bytes().to_vec()
})
.unwrap_or_default();
}
None => Vec::new(),
};
ListFilter {
Ok(ListFilter {
skip: self.skip,
len: self.len,
search,
@@ -82,7 +84,7 @@ impl Command {
min_value_size: self.min_value_size,
reverse: self.reverse,
only_count: self.count,
}
})
}
}
@@ -115,7 +117,7 @@ impl<N: NodeTypes> TableViewer<()> for ListTableViewer<'_, N> {
}
let list_filter = self.args.list_filter();
let list_filter = self.args.list_filter()?;
if self.args.json || self.args.count {
let (list, count) = self.tool.list::<T>(&list_filter)?;

View File

@@ -0,0 +1,34 @@
//! Enode identifier command
use clap::Parser;
use reth_cli_util::get_secret_key;
use reth_network_peers::NodeRecord;
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf,
};
/// Print the enode identifier for a given secret key.
#[derive(Parser, Debug)]
pub struct Command {
/// Path to the secret key file for discovery.
pub discovery_secret: PathBuf,
/// Optional IP address to include in the enode URL.
///
/// If not provided, defaults to 0.0.0.0.
#[arg(long)]
pub ip: Option<IpAddr>,
}
impl Command {
/// Execute the enode command.
pub fn execute(self) -> eyre::Result<()> {
let sk = get_secret_key(&self.discovery_secret)?;
let ip = self.ip.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
let addr = SocketAddr::new(ip, 30303);
let enr = NodeRecord::from_secret_key(addr, &sk);
println!("{enr}");
Ok(())
}
}

View File

@@ -18,6 +18,7 @@ use reth_node_core::{
};
pub mod bootnode;
pub mod enode;
pub mod rlpx;
/// `reth p2p` command
@@ -85,6 +86,9 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
Subcommands::Bootnode(command) => {
command.execute().await?;
}
Subcommands::Enode(command) => {
command.execute()?;
}
}
Ok(())
@@ -99,6 +103,7 @@ impl<C: ChainSpecParser> Command<C> {
Subcommands::Body { args, .. } => Some(&args.chain),
Subcommands::Rlpx(_) => None,
Subcommands::Bootnode(_) => None,
Subcommands::Enode(_) => None,
}
}
}
@@ -126,6 +131,8 @@ pub enum Subcommands<C: ChainSpecParser> {
Rlpx(rlpx::Command),
/// Bootnode command
Bootnode(bootnode::Command),
/// Print enode identifier
Enode(enode::Command),
}
#[derive(Debug, Clone, Parser)]
@@ -225,4 +232,16 @@ mod tests {
let _args: Command<EthereumChainSpecParser> =
Command::parse_from(["reth", "body", "--chain", "mainnet", "1000"]);
}
#[test]
fn parse_enode_cmd() {
let _args: Command<EthereumChainSpecParser> =
Command::parse_from(["reth", "enode", "/tmp/secret"]);
}
#[test]
fn parse_enode_cmd_with_ip() {
let _args: Command<EthereumChainSpecParser> =
Command::parse_from(["reth", "enode", "/tmp/secret", "--ip", "192.168.1.1"]);
}
}

View File

@@ -62,7 +62,8 @@ pub trait EngineTypes:
+ TryInto<Self::ExecutionPayloadEnvelopeV2>
+ TryInto<Self::ExecutionPayloadEnvelopeV3>
+ TryInto<Self::ExecutionPayloadEnvelopeV4>
+ TryInto<Self::ExecutionPayloadEnvelopeV5>,
+ TryInto<Self::ExecutionPayloadEnvelopeV5>
+ TryInto<Self::ExecutionPayloadEnvelopeV6>,
> + DeserializeOwned
+ Serialize
{
@@ -106,6 +107,14 @@ pub trait EngineTypes:
+ Send
+ Sync
+ 'static;
/// Execution Payload V6 envelope type.
type ExecutionPayloadEnvelopeV6: DeserializeOwned
+ Serialize
+ Clone
+ Unpin
+ Send
+ Sync
+ 'static;
}
/// Type that validates the payloads processed by the engine API.

View File

@@ -85,6 +85,12 @@ pub mod state;
/// backfill this gap.
pub(crate) const MIN_BLOCKS_FOR_PIPELINE_RUN: u64 = EPOCH_SLOTS;
/// The minimum number of blocks to retain in the changeset cache after eviction.
///
/// This ensures that recent trie changesets are kept in memory for potential reorgs,
/// even when the finalized block is not set (e.g., on L2s like Optimism).
const CHANGESET_CACHE_RETENTION_BLOCKS: u64 = 64;
/// A builder for creating state providers that can be used across threads.
#[derive(Clone, Debug)]
pub struct StateProviderBuilder<N: NodePrimitives, P> {
@@ -1378,19 +1384,27 @@ where
debug!(target: "engine::tree", ?last_persisted_block_hash, ?last_persisted_block_number, elapsed=?start_time.elapsed(), "Finished persisting, calling finish");
self.persistence_state.finish(last_persisted_block_hash, last_persisted_block_number);
// Evict trie changesets for blocks below the finalized block, but keep at least 64 blocks
if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() {
let min_threshold = last_persisted_block_number.saturating_sub(64);
let eviction_threshold = finalized.number.min(min_threshold);
debug!(
target: "engine::tree",
last_persisted = last_persisted_block_number,
finalized_number = finalized.number,
eviction_threshold,
"Evicting changesets below threshold"
);
self.changeset_cache.evict(eviction_threshold);
}
// Evict trie changesets for blocks below the eviction threshold.
// Keep at least CHANGESET_CACHE_RETENTION_BLOCKS from the persisted tip, and also respect
// the finalized block if set.
let min_threshold =
last_persisted_block_number.saturating_sub(CHANGESET_CACHE_RETENTION_BLOCKS);
let eviction_threshold =
if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() {
// Use the minimum of finalized block and retention threshold to be conservative
finalized.number.min(min_threshold)
} else {
// When finalized is not set (e.g., on L2s), use the retention threshold
min_threshold
};
debug!(
target: "engine::tree",
last_persisted = last_persisted_block_number,
finalized_number = ?self.canonical_in_memory_state.get_finalized_num_hash().map(|f| f.number),
eviction_threshold,
"Evicting changesets below threshold"
);
self.changeset_cache.evict(eviction_threshold);
self.on_new_persisted_block()?;
Ok(())

View File

@@ -15,7 +15,7 @@ use crate::tree::{
};
use alloy_eip7928::BlockAccessList;
use alloy_eips::eip1898::BlockWithParent;
use alloy_evm::{block::StateChangeSource, ToTxEnv};
use alloy_evm::block::StateChangeSource;
use alloy_primitives::B256;
use crossbeam_channel::Sender as CrossbeamSender;
use executor::WorkloadExecutor;
@@ -25,6 +25,7 @@ use parking_lot::RwLock;
use prewarm::PrewarmMetrics;
use rayon::prelude::*;
use reth_evm::{
block::ExecutableTxParts,
execute::{ExecutableTxFor, WithTxEnv},
ConfigureEvm, EvmEnvFor, ExecutableTxIterator, ExecutableTxTuple, OnStateHook, SpecFor,
TxEnvFor,
@@ -101,7 +102,7 @@ pub const SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY: usize = 1_000_000;
/// Type alias for [`PayloadHandle`] returned by payload processor spawn methods.
type IteratorPayloadHandle<Evm, I, N> = PayloadHandle<
WithTxEnv<TxEnvFor<Evm>, <I as ExecutableTxTuple>::Tx>,
WithTxEnv<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>,
<I as ExecutableTxTuple>::Error,
<N as NodePrimitives>::Receipt,
>;
@@ -369,8 +370,8 @@ where
&self,
transactions: I,
) -> (
mpsc::Receiver<WithTxEnv<TxEnvFor<Evm>, I::Tx>>,
mpsc::Receiver<Result<WithTxEnv<TxEnvFor<Evm>, I::Tx>, I::Error>>,
mpsc::Receiver<WithTxEnv<TxEnvFor<Evm>, I::Recovered>>,
mpsc::Receiver<Result<WithTxEnv<TxEnvFor<Evm>, I::Recovered>, I::Error>>,
usize,
) {
let (transactions, convert) = transactions.into();
@@ -385,7 +386,10 @@ where
self.executor.spawn_blocking(move || {
transactions.enumerate().for_each_with(ooo_tx, |ooo_tx, (idx, tx)| {
let tx = convert(tx);
let tx = tx.map(|tx| WithTxEnv { tx_env: tx.to_tx_env(), tx: Arc::new(tx) });
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
// Only send Ok(_) variants to prewarming task.
if let Ok(tx) = &tx {
let _ = prewarm_tx.send(tx.clone());

View File

@@ -29,7 +29,7 @@ use alloy_evm::Database;
use alloy_primitives::{keccak256, map::B256Set, B256};
use crossbeam_channel::Sender as CrossbeamSender;
use metrics::{Counter, Gauge, Histogram};
use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, SpecFor};
use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, RecoveredTx, SpecFor};
use reth_metrics::Metrics;
use reth_primitives_traits::NodePrimitives;
use reth_provider::{
@@ -609,7 +609,8 @@ where
break
}
let res = match evm.transact(&tx) {
let (tx_env, tx) = tx.into_parts();
let res = match evm.transact(tx_env) {
Ok(res) => res,
Err(err) => {
trace!(

View File

@@ -17,10 +17,11 @@ pub use payload::{payload_id, BlobSidecars, EthBuiltPayload, EthPayloadBuilderAt
mod error;
pub use error::*;
use alloy_rpc_types_engine::{ExecutionData, ExecutionPayload, ExecutionPayloadEnvelopeV5};
use alloy_rpc_types_engine::{ExecutionData, ExecutionPayload};
pub use alloy_rpc_types_engine::{
ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadEnvelopeV4,
ExecutionPayloadV1, PayloadAttributes as EthPayloadAttributes,
ExecutionPayloadEnvelopeV5, ExecutionPayloadEnvelopeV6, ExecutionPayloadV1,
PayloadAttributes as EthPayloadAttributes,
};
use reth_engine_primitives::EngineTypes;
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
@@ -66,13 +67,15 @@ where
+ TryInto<ExecutionPayloadEnvelopeV2>
+ TryInto<ExecutionPayloadEnvelopeV3>
+ TryInto<ExecutionPayloadEnvelopeV4>
+ TryInto<ExecutionPayloadEnvelopeV5>,
+ TryInto<ExecutionPayloadEnvelopeV5>
+ TryInto<ExecutionPayloadEnvelopeV6>,
{
type ExecutionPayloadEnvelopeV1 = ExecutionPayloadV1;
type ExecutionPayloadEnvelopeV2 = ExecutionPayloadEnvelopeV2;
type ExecutionPayloadEnvelopeV3 = ExecutionPayloadEnvelopeV3;
type ExecutionPayloadEnvelopeV4 = ExecutionPayloadEnvelopeV4;
type ExecutionPayloadEnvelopeV5 = ExecutionPayloadEnvelopeV5;
type ExecutionPayloadEnvelopeV6 = ExecutionPayloadEnvelopeV6;
}
/// A default payload type for [`EthEngineTypes`]

View File

@@ -11,8 +11,8 @@ use alloy_primitives::{Address, B256, U256};
use alloy_rlp::Encodable;
use alloy_rpc_types_engine::{
BlobsBundleV1, BlobsBundleV2, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3,
ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadFieldV2,
ExecutionPayloadV1, ExecutionPayloadV3, PayloadAttributes, PayloadId,
ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadEnvelopeV6,
ExecutionPayloadFieldV2, ExecutionPayloadV1, ExecutionPayloadV3, PayloadAttributes, PayloadId,
};
use core::convert::Infallible;
use reth_ethereum_primitives::EthPrimitives;
@@ -160,6 +160,13 @@ impl EthBuiltPayload {
execution_requests: requests.unwrap_or_default(),
})
}
/// Try converting built payload into [`ExecutionPayloadEnvelopeV6`].
///
/// Note: Amsterdam fork is not yet implemented, so this conversion is not yet supported.
pub fn try_into_v6(self) -> Result<ExecutionPayloadEnvelopeV6, BuiltPayloadConversionError> {
unimplemented!("ExecutionPayloadEnvelopeV6 not yet supported")
}
}
impl<N: NodePrimitives> BuiltPayload for EthBuiltPayload<N> {
@@ -227,6 +234,14 @@ impl TryFrom<EthBuiltPayload> for ExecutionPayloadEnvelopeV5 {
}
}
impl TryFrom<EthBuiltPayload> for ExecutionPayloadEnvelopeV6 {
type Error = BuiltPayloadConversionError;
fn try_from(value: EthBuiltPayload) -> Result<Self, Self::Error> {
value.try_into_v6()
}
}
/// An enum representing blob transaction sidecars belonging to [`EthBuiltPayload`].
#[derive(Clone, Default, Debug)]
pub enum BlobSidecars {

View File

@@ -1,3 +1,4 @@
use alloy_consensus::TxType;
use alloy_evm::eth::receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx};
use reth_ethereum_primitives::{Receipt, TransactionSigned};
use reth_evm::Evm;
@@ -12,13 +13,10 @@ impl ReceiptBuilder for RethReceiptBuilder {
type Transaction = TransactionSigned;
type Receipt = Receipt;
fn build_receipt<E: Evm>(
&self,
ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>,
) -> Self::Receipt {
let ReceiptBuilderCtx { tx, result, cumulative_gas_used, .. } = ctx;
fn build_receipt<E: Evm>(&self, ctx: ReceiptBuilderCtx<'_, TxType, E>) -> Self::Receipt {
let ReceiptBuilderCtx { tx_type, result, cumulative_gas_used, .. } = ctx;
Receipt {
tx_type: tx.tx_type(),
tx_type,
// Success flag was added in `EIP-658: Embedding transaction status code in
// receipts`.
success: result.is_success(),

View File

@@ -1,6 +1,6 @@
use crate::EthEvmConfig;
use alloc::{boxed::Box, sync::Arc, vec, vec::Vec};
use alloy_consensus::Header;
use alloy_consensus::{Header, TxType};
use alloy_eips::eip7685::Requests;
use alloy_evm::precompiles::PrecompilesMap;
use alloy_primitives::Bytes;
@@ -11,14 +11,14 @@ use reth_evm::{
block::{
BlockExecutionError, BlockExecutor, BlockExecutorFactory, BlockExecutorFor, ExecutableTx,
},
eth::{EthBlockExecutionCtx, EthEvmContext},
eth::{EthBlockExecutionCtx, EthEvmContext, EthTxResult},
ConfigureEngineEvm, ConfigureEvm, Database, EthEvm, EthEvmFactory, Evm, EvmEnvFor, EvmFactory,
ExecutableTxIterator, ExecutionCtxFor,
ExecutableTxIterator, ExecutionCtxFor, RecoveredTx,
};
use reth_execution_types::{BlockExecutionResult, ExecutionOutcome};
use reth_primitives_traits::{BlockTy, SealedBlock, SealedHeader};
use revm::{
context::result::{ExecutionResult, Output, ResultAndState, SuccessReason},
context::result::{ExecutionResult, HaltReason, Output, ResultAndState, SuccessReason},
database::State,
Inspector,
};
@@ -90,6 +90,7 @@ impl<'a, DB: Database, I: Inspector<EthEvmContext<&'a mut State<DB>>>> BlockExec
type Evm = EthEvm<&'a mut State<DB>, I, PrecompilesMap>;
type Transaction = TransactionSigned;
type Receipt = Receipt;
type Result = EthTxResult<HaltReason, TxType>;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
Ok(())
@@ -101,25 +102,25 @@ impl<'a, DB: Database, I: Inspector<EthEvmContext<&'a mut State<DB>>>> BlockExec
fn execute_transaction_without_commit(
&mut self,
_tx: impl ExecutableTx<Self>,
) -> Result<ResultAndState<<Self::Evm as Evm>::HaltReason>, BlockExecutionError> {
Ok(ResultAndState::new(
ExecutionResult::Success {
reason: SuccessReason::Return,
gas_used: 0,
gas_refunded: 0,
logs: vec![],
output: Output::Call(Bytes::from(vec![])),
},
Default::default(),
))
tx: impl ExecutableTx<Self>,
) -> Result<Self::Result, BlockExecutionError> {
Ok(EthTxResult {
result: ResultAndState::new(
ExecutionResult::Success {
reason: SuccessReason::Return,
gas_used: 0,
gas_refunded: 0,
logs: vec![],
output: Output::Call(Bytes::from(vec![])),
},
Default::default(),
),
tx_type: tx.into_parts().1.tx().tx_type(),
blob_gas_used: 0,
})
}
fn commit_transaction(
&mut self,
_output: ResultAndState<<Self::Evm as Evm>::HaltReason>,
_tx: impl ExecutableTx<Self>,
) -> Result<u64, BlockExecutionError> {
fn commit_transaction(&mut self, _output: Self::Result) -> Result<u64, BlockExecutionError> {
Ok(0)
}

View File

@@ -18,7 +18,7 @@ use reth_evm::{
};
use reth_network::{primitives::BasicNetworkPrimitives, NetworkHandle, PeersInfo};
use reth_node_api::{
AddOnsContext, FullNodeComponents, HeaderTy, NodeAddOns, NodePrimitives,
AddOnsContext, BlockTy, FullNodeComponents, HeaderTy, NodeAddOns, NodePrimitives,
PayloadAttributesBuilder, PrimitivesTy, TxTy,
};
use reth_node_builder::{
@@ -53,8 +53,8 @@ use reth_rpc_eth_types::{error::FromEvmError, EthApiError};
use reth_rpc_server_types::RethRpcModule;
use reth_tracing::tracing::{debug, info};
use reth_transaction_pool::{
blobstore::DiskFileBlobStore, EthTransactionPool, PoolPooledTx, PoolTransaction,
TransactionPool, TransactionValidationTaskExecutor,
blobstore::DiskFileBlobStore, EthPooledTransaction, EthTransactionPool, PoolPooledTx,
PoolTransaction, TransactionPool, TransactionValidationTaskExecutor,
};
use revm::context::TxEnv;
use std::{marker::PhantomData, sync::Arc, time::SystemTime};
@@ -464,7 +464,8 @@ where
>,
Node: FullNodeTypes<Types = Types>,
{
type Pool = EthTransactionPool<Node::Provider, DiskFileBlobStore>;
type Pool =
EthTransactionPool<Node::Provider, DiskFileBlobStore, EthPooledTransaction, BlockTy<Types>>;
async fn build_pool(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Pool> {
let pool_config = ctx.pool_config();

View File

@@ -1,5 +1,7 @@
use crate::{execute::ExecutableTxFor, ConfigureEvm, EvmEnvFor, ExecutionCtxFor};
use crate::{execute::ExecutableTxFor, ConfigureEvm, EvmEnvFor, ExecutionCtxFor, TxEnvFor};
use alloy_evm::{block::ExecutableTxParts, RecoveredTx};
use rayon::prelude::*;
use reth_primitives_traits::TxTy;
/// [`ConfigureEvm`] extension providing methods for executing payloads.
pub trait ConfigureEngineEvm<ExecutionData>: ConfigureEvm {
@@ -61,11 +63,16 @@ where
/// Iterator over executable transactions.
pub trait ExecutableTxIterator<Evm: ConfigureEvm>:
ExecutableTxTuple<Tx: ExecutableTxFor<Evm>>
ExecutableTxTuple<Tx: ExecutableTxFor<Evm, Recovered = Self::Recovered>>
{
/// HACK: for some reason, this duplicated AT is the only way to enforce the inner Recovered:
/// Send + Sync bound. Effectively alias for `Self::Tx::Recovered`.
type Recovered: RecoveredTx<TxTy<Evm::Primitives>> + Send + Sync;
}
impl<T, Evm: ConfigureEvm> ExecutableTxIterator<Evm> for T where
T: ExecutableTxTuple<Tx: ExecutableTxFor<Evm>>
impl<T, Evm: ConfigureEvm> ExecutableTxIterator<Evm> for T
where
T: ExecutableTxTuple<Tx: ExecutableTxFor<Evm, Recovered: Send + Sync>>,
{
type Recovered = <T::Tx as ExecutableTxParts<TxEnvFor<Evm>, TxTy<Evm::Primitives>>>::Recovered;
}

View File

@@ -6,7 +6,7 @@ use alloy_consensus::{BlockHeader, Header};
use alloy_eips::eip2718::WithEncoded;
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory};
use alloy_evm::{
block::{CommitChanges, ExecutableTx},
block::{CommitChanges, ExecutableTxParts},
Evm, EvmEnv, EvmFactory, RecoveredTx, ToTxEnv,
};
use alloy_primitives::{Address, B256};
@@ -401,49 +401,31 @@ where
/// Conversions for executable transactions.
pub trait ExecutorTx<Executor: BlockExecutor> {
/// Converts the transaction into [`ExecutableTx`].
fn as_executable(&self) -> impl ExecutableTx<Executor>;
/// Converts the transaction into [`Recovered`].
fn into_recovered(self) -> Recovered<Executor::Transaction>;
/// Converts the transaction into a tuple of [`TxEnvFor`] and [`Recovered`].
fn into_parts(self) -> (<Executor::Evm as Evm>::Tx, Recovered<Executor::Transaction>);
}
impl<Executor: BlockExecutor> ExecutorTx<Executor>
for WithEncoded<Recovered<Executor::Transaction>>
{
fn as_executable(&self) -> impl ExecutableTx<Executor> {
self
}
fn into_recovered(self) -> Recovered<Executor::Transaction> {
self.1
fn into_parts(self) -> (<Executor::Evm as Evm>::Tx, Recovered<Executor::Transaction>) {
(self.to_tx_env(), self.1)
}
}
impl<Executor: BlockExecutor> ExecutorTx<Executor> for Recovered<Executor::Transaction> {
fn as_executable(&self) -> impl ExecutableTx<Executor> {
self
}
fn into_recovered(self) -> Self {
self
fn into_parts(self) -> (<Executor::Evm as Evm>::Tx, Self) {
(self.to_tx_env(), self)
}
}
impl<T, Executor> ExecutorTx<Executor>
for WithTxEnv<<<Executor as BlockExecutor>::Evm as Evm>::Tx, T>
impl<Executor> ExecutorTx<Executor>
for WithTxEnv<<Executor::Evm as Evm>::Tx, Recovered<Executor::Transaction>>
where
T: ExecutorTx<Executor> + Clone,
Executor: BlockExecutor,
<<Executor as BlockExecutor>::Evm as Evm>::Tx: Clone,
Self: RecoveredTx<Executor::Transaction>,
Executor: BlockExecutor<Transaction: Clone>,
{
fn as_executable(&self) -> impl ExecutableTx<Executor> {
self
}
fn into_recovered(self) -> Recovered<Executor::Transaction> {
Arc::unwrap_or_clone(self.tx).into_recovered()
fn into_parts(self) -> (<Executor::Evm as Evm>::Tx, Recovered<Executor::Transaction>) {
(self.tx_env, Arc::unwrap_or_clone(self.tx))
}
}
@@ -479,10 +461,11 @@ where
&ExecutionResult<<<Self::Executor as BlockExecutor>::Evm as Evm>::HaltReason>,
) -> CommitChanges,
) -> Result<Option<u64>, BlockExecutionError> {
let (tx_env, tx) = tx.into_parts();
if let Some(gas_used) =
self.executor.execute_transaction_with_commit_condition(tx.as_executable(), f)?
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
{
self.transactions.push(tx.into_recovered());
self.transactions.push(tx);
Ok(Some(gas_used))
} else {
Ok(None)
@@ -609,20 +592,20 @@ where
}
}
/// A helper trait marking a 'static type that can be converted into an [`ExecutableTx`] for block
/// executor.
/// A helper trait marking a 'static type that can be converted into an [`ExecutableTxParts`] for
/// block executor.
pub trait ExecutableTxFor<Evm: ConfigureEvm>:
ToTxEnv<TxEnvFor<Evm>> + RecoveredTx<TxTy<Evm::Primitives>>
ExecutableTxParts<TxEnvFor<Evm>, TxTy<Evm::Primitives>> + RecoveredTx<TxTy<Evm::Primitives>>
{
}
impl<T, Evm: ConfigureEvm> ExecutableTxFor<Evm> for T where
T: ToTxEnv<TxEnvFor<Evm>> + RecoveredTx<TxTy<Evm::Primitives>>
T: ExecutableTxParts<TxEnvFor<Evm>, TxTy<Evm::Primitives>> + RecoveredTx<TxTy<Evm::Primitives>>
{
}
/// A container for a transaction and a transaction environment.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct WithTxEnv<TxEnv, T> {
/// The transaction environment for EVM.
pub tx_env: TxEnv,
@@ -630,6 +613,12 @@ pub struct WithTxEnv<TxEnv, T> {
pub tx: Arc<T>,
}
impl<TxEnv: Clone, T> Clone for WithTxEnv<TxEnv, T> {
fn clone(&self) -> Self {
Self { tx_env: self.tx_env.clone(), tx: self.tx.clone() }
}
}
impl<TxEnv, Tx, T: RecoveredTx<Tx>> RecoveredTx<Tx> for WithTxEnv<TxEnv, T> {
fn tx(&self) -> &Tx {
self.tx.tx()
@@ -640,9 +629,11 @@ impl<TxEnv, Tx, T: RecoveredTx<Tx>> RecoveredTx<Tx> for WithTxEnv<TxEnv, T> {
}
}
impl<TxEnv: Clone, T> ToTxEnv<TxEnv> for WithTxEnv<TxEnv, T> {
fn to_tx_env(&self) -> TxEnv {
self.tx_env.clone()
impl<TxEnv, T: RecoveredTx<Tx>, Tx> ExecutableTxParts<TxEnv, Tx> for WithTxEnv<TxEnv, T> {
type Recovered = Arc<T>;
fn into_parts(self) -> (TxEnv, Self::Recovered) {
(self.tx_env, self.tx)
}
}

View File

@@ -131,6 +131,8 @@ pub struct TransactionsManagerMetrics {
/// capacity. Note, this is not a limit to the number of inflight requests, but a health
/// measure.
pub(crate) capacity_pending_pool_imports: Counter,
/// Total number of transactions ignored because pending pool imports are at capacity.
pub(crate) skipped_transactions_pending_pool_imports_at_capacity: Counter,
/// The time it took to prepare transactions for import. This is mostly sender recovery.
pub(crate) pool_import_prepare_duration: Histogram,

View File

@@ -429,11 +429,22 @@ impl<Pool: TransactionPool, N: NetworkPrimitives> TransactionsManager<Pool, N> {
/// Returns `true` if [`TransactionsManager`] has capacity to request pending hashes. Returns
/// `false` if [`TransactionsManager`] is operating close to full capacity.
fn has_capacity_for_fetching_pending_hashes(&self) -> bool {
self.pending_pool_imports_info
.has_capacity(self.pending_pool_imports_info.max_pending_pool_imports) &&
self.has_capacity_for_pending_pool_imports() &&
self.transaction_fetcher.has_capacity_for_fetching_pending_hashes()
}
/// Returns `true` if [`TransactionsManager`] has capacity for more pending pool imports.
fn has_capacity_for_pending_pool_imports(&self) -> bool {
self.remaining_pool_import_capacity() > 0
}
/// Returns the remaining capacity for pending pool imports.
fn remaining_pool_import_capacity(&self) -> usize {
self.pending_pool_imports_info.max_pending_pool_imports.saturating_sub(
self.pending_pool_imports_info.pending_pool_imports.load(Ordering::Relaxed),
)
}
fn report_peer_bad_transactions(&self, peer_id: PeerId) {
self.report_peer(peer_id, ReputationChangeKind::BadTransactions);
self.metrics.reported_bad_transactions.increment(1);
@@ -1285,6 +1296,7 @@ where
trace!(target: "net::tx", peer_id=format!("{peer_id:#}"), policy=?self.config.ingress_policy, "Ignoring full transactions from peer blocked by ingress policy");
return;
}
// ensure we didn't receive any blob transactions as these are disallowed to be
// broadcasted in full
@@ -1335,7 +1347,13 @@ where
return
}
// Early return if we don't have capacity for any imports
if !self.has_capacity_for_pending_pool_imports() {
return
}
let Some(peer) = self.peers.get_mut(&peer_id) else { return };
let client_version = peer.client_version.clone();
let mut transactions = transactions.0;
let start = Instant::now();
@@ -1378,7 +1396,7 @@ where
trace!(target: "net::tx",
peer_id=format!("{peer_id:#}"),
hash=%tx.tx_hash(),
client_version=%peer.client_version,
%client_version,
"received a known bad transaction from peer"
);
has_bad_transactions = true;
@@ -1387,6 +1405,18 @@ where
true
});
// Truncate to remaining capacity before recovery to avoid wasting CPU on transactions
// that won't be imported anyway.
let capacity = self.remaining_pool_import_capacity();
if transactions.len() > capacity {
let skipped = transactions.len() - capacity;
transactions.truncate(capacity);
self.metrics
.skipped_transactions_pending_pool_imports_at_capacity
.increment(skipped as u64);
trace!(target: "net::tx", skipped, capacity, "Truncated transactions batch to capacity");
}
let txs_len = transactions.len();
let new_txs = transactions
@@ -1397,7 +1427,7 @@ where
trace!(target: "net::tx",
peer_id=format!("{peer_id:#}"),
hash=%badtx.tx_hash(),
client_version=%peer.client_version,
client_version=%client_version,
"failed ecrecovery for transaction"
);
None
@@ -1448,7 +1478,7 @@ where
self.metrics
.occurrences_of_transaction_already_seen_by_peer
.increment(num_already_seen_by_peer);
trace!(target: "net::tx", num_txs=%num_already_seen_by_peer, ?peer_id, client=?peer.client_version, "Peer sent already seen transactions");
trace!(target: "net::tx", num_txs=%num_already_seen_by_peer, ?peer_id, client=%client_version, "Peer sent already seen transactions");
}
if has_bad_transactions {

View File

@@ -4,7 +4,7 @@ use crate::{BuilderContext, FullNodeTypes};
use alloy_primitives::Address;
use reth_chain_state::CanonStateSubscriptions;
use reth_chainspec::EthereumHardforks;
use reth_node_api::{NodeTypes, TxTy};
use reth_node_api::{BlockTy, NodeTypes, TxTy};
use reth_transaction_pool::{
blobstore::DiskFileBlobStore, BlobStore, CoinbaseTipOrdering, PoolConfig, PoolTransaction,
SubPoolLimit, TransactionPool, TransactionValidationTaskExecutor, TransactionValidator,
@@ -129,7 +129,7 @@ impl<'a, Node: FullNodeTypes, V> TxPoolBuilder<'a, Node, V> {
impl<'a, Node, V> TxPoolBuilder<'a, Node, TransactionValidationTaskExecutor<V>>
where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec: EthereumHardforks>>,
V: TransactionValidator + 'static,
V: TransactionValidator<Block = BlockTy<Node::Types>> + 'static,
V::Transaction:
PoolTransaction<Consensus = TxTy<Node::Types>> + reth_transaction_pool::EthPoolTransaction,
{
@@ -248,7 +248,7 @@ fn spawn_pool_maintenance_task<Node, Pool>(
) -> eyre::Result<()>
where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec: EthereumHardforks>>,
Pool: reth_transaction_pool::TransactionPoolExt + Clone + 'static,
Pool: reth_transaction_pool::TransactionPoolExt<Block = BlockTy<Node::Types>> + Clone + 'static,
Pool::Transaction: PoolTransaction<Consensus = TxTy<Node::Types>>,
{
let chain_events = ctx.provider().canonical_state_stream();
@@ -280,7 +280,7 @@ pub fn spawn_maintenance_tasks<Node, Pool>(
) -> eyre::Result<()>
where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec: EthereumHardforks>>,
Pool: reth_transaction_pool::TransactionPoolExt + Clone + 'static,
Pool: reth_transaction_pool::TransactionPoolExt<Block = BlockTy<Node::Types>> + Clone + 'static,
Pool::Transaction: PoolTransaction<Consensus = TxTy<Node::Types>>,
{
spawn_local_backup_task(ctx, pool.clone())?;

View File

@@ -4,10 +4,13 @@ use alloy_consensus::transaction::Either;
use alloy_provider::network::AnyNetwork;
use jsonrpsee::core::{DeserializeOwned, Serialize};
use reth_chainspec::EthChainSpec;
use reth_consensus_debug_client::{DebugConsensusClient, EtherscanBlockProvider, RpcBlockProvider};
use reth_consensus_debug_client::{
BlockProvider, DebugConsensusClient, EtherscanBlockProvider, RpcBlockProvider,
};
use reth_engine_local::LocalMiner;
use reth_node_api::{
BlockTy, FullNodeComponents, HeaderTy, PayloadAttrTy, PayloadAttributesBuilder, PayloadTypes,
BlockTy, FullNodeComponents, FullNodeTypes, HeaderTy, PayloadAttrTy, PayloadAttributesBuilder,
PayloadTypes,
};
use std::{
future::{Future, IntoFuture},
@@ -109,9 +112,16 @@ impl<L> DebugNodeLauncher<L> {
}
}
/// Type alias for the default debug block provider. We use etherscan provider to satisfy the
/// bounds.
pub type DefaultDebugBlockProvider<N> = EtherscanBlockProvider<
<<N as FullNodeTypes>::Types as DebugNode<N>>::RpcBlock,
BlockTy<<N as FullNodeTypes>::Types>,
>;
/// Future for the [`DebugNodeLauncher`].
#[expect(missing_debug_implementations, clippy::type_complexity)]
pub struct DebugNodeLauncherFuture<L, Target, N>
pub struct DebugNodeLauncherFuture<L, Target, N, B = DefaultDebugBlockProvider<N>>
where
N: FullNodeComponents<Types: DebugNode<N>>,
{
@@ -121,14 +131,17 @@ where
Option<Box<dyn PayloadAttributesBuilder<PayloadAttrTy<N::Types>, HeaderTy<N::Types>>>>,
map_attributes:
Option<Box<dyn Fn(PayloadAttrTy<N::Types>) -> PayloadAttrTy<N::Types> + Send + Sync>>,
debug_block_provider: Option<B>,
}
impl<L, Target, N, AddOns> DebugNodeLauncherFuture<L, Target, N>
impl<L, Target, N, AddOns, B> DebugNodeLauncherFuture<L, Target, N, B>
where
N: FullNodeComponents<Types: DebugNode<N>>,
AddOns: RethRpcAddOns<N>,
L: LaunchNode<Target, Node = NodeHandle<N, AddOns>>,
B: BlockProvider<Block = BlockTy<N::Types>> + Clone,
{
/// Sets a custom payload attributes builder for local mining in dev mode.
pub fn with_payload_attributes_builder(
self,
builder: impl PayloadAttributesBuilder<PayloadAttrTy<N::Types>, HeaderTy<N::Types>>,
@@ -138,9 +151,11 @@ where
target: self.target,
local_payload_attributes_builder: Some(Box::new(builder)),
map_attributes: None,
debug_block_provider: self.debug_block_provider,
}
}
/// Sets a function to map payload attributes before building.
pub fn map_debug_payload_attributes(
self,
f: impl Fn(PayloadAttrTy<N::Types>) -> PayloadAttrTy<N::Types> + Send + Sync + 'static,
@@ -150,16 +165,58 @@ where
target: self.target,
local_payload_attributes_builder: None,
map_attributes: Some(Box::new(f)),
debug_block_provider: self.debug_block_provider,
}
}
/// Sets a custom block provider for the debug consensus client.
///
/// When set, this provider will be used instead of creating an `EtherscanBlockProvider`
/// or `RpcBlockProvider` from CLI arguments.
pub fn with_debug_block_provider<B2>(
self,
provider: B2,
) -> DebugNodeLauncherFuture<L, Target, N, B2>
where
B2: BlockProvider<Block = BlockTy<N::Types>> + Clone,
{
DebugNodeLauncherFuture {
inner: self.inner,
target: self.target,
local_payload_attributes_builder: self.local_payload_attributes_builder,
map_attributes: self.map_attributes,
debug_block_provider: Some(provider),
}
}
async fn launch_node(self) -> eyre::Result<NodeHandle<N, AddOns>> {
let Self { inner, target, local_payload_attributes_builder, map_attributes } = self;
let Self {
inner,
target,
local_payload_attributes_builder,
map_attributes,
debug_block_provider,
} = self;
let handle = inner.launch_node(target).await?;
let config = &handle.node.config;
if let Some(url) = config.debug.rpc_consensus_url.clone() {
if let Some(provider) = debug_block_provider {
info!(target: "reth::cli", "Using custom debug block provider");
let rpc_consensus_client = DebugConsensusClient::new(
handle.node.add_ons_handle.beacon_engine_handle.clone(),
Arc::new(provider),
);
handle
.node
.task_executor
.spawn_critical("custom debug block provider consensus client", async move {
rpc_consensus_client.run().await
});
} else if let Some(url) = config.debug.rpc_consensus_url.clone() {
info!(target: "reth::cli", "Using RPC consensus client: {}", url);
let block_provider =
@@ -180,14 +237,11 @@ where
handle.node.task_executor.spawn_critical("rpc-ws consensus client", async move {
rpc_consensus_client.run().await
});
}
if let Some(maybe_custom_etherscan_url) = config.debug.etherscan.clone() {
} else if let Some(maybe_custom_etherscan_url) = config.debug.etherscan.clone() {
info!(target: "reth::cli", "Using etherscan as consensus client");
let chain = config.chain.chain();
let etherscan_url = maybe_custom_etherscan_url.map(Ok).unwrap_or_else(|| {
// If URL isn't provided, use default Etherscan URL for the chain if it is known
chain
.etherscan_urls()
.map(|urls| urls.0.to_string())
@@ -252,12 +306,13 @@ where
}
}
impl<L, Target, N, AddOns> IntoFuture for DebugNodeLauncherFuture<L, Target, N>
impl<L, Target, N, AddOns, B> IntoFuture for DebugNodeLauncherFuture<L, Target, N, B>
where
Target: Send + 'static,
N: FullNodeComponents<Types: DebugNode<N>>,
AddOns: RethRpcAddOns<N> + 'static,
L: LaunchNode<Target, Node = NodeHandle<N, AddOns>> + 'static,
B: BlockProvider<Block = BlockTy<N::Types>> + Clone + 'static,
{
type Output = eyre::Result<NodeHandle<N, AddOns>>;
type IntoFuture = Pin<Box<dyn Future<Output = eyre::Result<NodeHandle<N, AddOns>>> + Send>>;
@@ -273,6 +328,7 @@ where
N: FullNodeComponents<Types: DebugNode<N>>,
AddOns: RethRpcAddOns<N> + 'static,
L: LaunchNode<Target, Node = NodeHandle<N, AddOns>> + 'static,
DefaultDebugBlockProvider<N>: BlockProvider<Block = BlockTy<N::Types>> + Clone,
{
type Node = NodeHandle<N, AddOns>;
type Future = DebugNodeLauncherFuture<L, Target, N>;
@@ -283,6 +339,7 @@ where
target,
local_payload_attributes_builder: None,
map_attributes: None,
debug_block_provider: None,
}
}
}

View File

@@ -31,7 +31,7 @@ pub use builder::{add_ons::AddOns, *};
mod launch;
pub use launch::{
debug::{DebugNode, DebugNodeLauncher},
debug::{DebugNode, DebugNodeLauncher, DebugNodeLauncherFuture, DefaultDebugBlockProvider},
engine::EngineNodeLauncher,
*,
};

View File

@@ -17,9 +17,9 @@ impl OpReceiptBuilder for OpRethReceiptBuilder {
fn build_receipt<'a, E: Evm>(
&self,
ctx: ReceiptBuilderCtx<'a, OpTransactionSigned, E>,
) -> Result<Self::Receipt, ReceiptBuilderCtx<'a, OpTransactionSigned, E>> {
match ctx.tx.tx_type() {
ctx: ReceiptBuilderCtx<'a, OpTxType, E>,
) -> Result<Self::Receipt, ReceiptBuilderCtx<'a, OpTxType, E>> {
match ctx.tx_type {
OpTxType::Deposit => Err(ctx),
ty => {
let receipt = Receipt {

View File

@@ -62,6 +62,7 @@ where
type ExecutionPayloadEnvelopeV3 = OpExecutionPayloadEnvelopeV3;
type ExecutionPayloadEnvelopeV4 = OpExecutionPayloadEnvelopeV4;
type ExecutionPayloadEnvelopeV5 = OpExecutionPayloadEnvelopeV4;
type ExecutionPayloadEnvelopeV6 = OpExecutionPayloadEnvelopeV4;
}
/// Validator for Optimism engine API.

View File

@@ -16,7 +16,7 @@ use reth_network::{
PeersInfo,
};
use reth_node_api::{
AddOnsContext, BuildNextEnv, EngineTypes, FullNodeComponents, HeaderTy, NodeAddOns,
AddOnsContext, BlockTy, BuildNextEnv, EngineTypes, FullNodeComponents, HeaderTy, NodeAddOns,
NodePrimitives, PayloadAttributesBuilder, PayloadTypes, PrimitivesTy, TxTy,
};
use reth_node_builder::{
@@ -962,7 +962,7 @@ where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec: OpHardforks>>,
T: EthPoolTransaction<Consensus = TxTy<Node::Types>> + OpPooledTx,
{
type Pool = OpTransactionPool<Node::Provider, DiskFileBlobStore, T>;
type Pool = OpTransactionPool<Node::Provider, DiskFileBlobStore, T, BlockTy<Node::Types>>;
async fn build_pool(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Pool> {
let Self { pool_config_overrides, .. } = self;

View File

@@ -9,6 +9,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
mod validator;
use op_alloy_consensus::OpBlock;
pub use validator::{OpL1BlockInfo, OpTransactionValidator};
pub mod conditional;
@@ -24,8 +25,8 @@ pub mod estimated_da_size;
use reth_transaction_pool::{CoinbaseTipOrdering, Pool, TransactionValidationTaskExecutor};
/// Type alias for default optimism transaction pool
pub type OpTransactionPool<Client, S, T = OpPooledTransaction> = Pool<
TransactionValidationTaskExecutor<OpTransactionValidator<Client, T>>,
pub type OpTransactionPool<Client, S, T = OpPooledTransaction, B = OpBlock> = Pool<
TransactionValidationTaskExecutor<OpTransactionValidator<Client, T, B>>,
CoinbaseTipOrdering<T>,
S,
>;

View File

@@ -325,10 +325,11 @@ mod tests {
#[tokio::test]
async fn validate_optimism_transaction() {
let client = MockEthProvider::default().with_chain_spec(OP_MAINNET.clone());
let validator = EthTransactionValidatorBuilder::new(client)
.no_shanghai()
.no_cancun()
.build(InMemoryBlobStore::default());
let validator =
EthTransactionValidatorBuilder::new(client)
.no_shanghai()
.no_cancun()
.build::<_, _, reth_optimism_primitives::OpBlock>(InMemoryBlobStore::default());
let validator = OpTransactionValidator::new(validator);
let origin = TransactionOrigin::External;

View File

@@ -1,5 +1,6 @@
use crate::{supervisor::SupervisorClient, InvalidCrossTx, OpPooledTx};
use alloy_consensus::{BlockHeader, Transaction};
use op_alloy_consensus::OpBlock;
use op_revm::L1BlockInfo;
use parking_lot::RwLock;
use reth_chainspec::ChainSpecProvider;
@@ -39,9 +40,9 @@ impl OpL1BlockInfo {
/// Validator for Optimism transactions.
#[derive(Debug, Clone)]
pub struct OpTransactionValidator<Client, Tx> {
pub struct OpTransactionValidator<Client, Tx, B = OpBlock> {
/// The type that performs the actual validation.
inner: Arc<EthTransactionValidator<Client, Tx>>,
inner: Arc<EthTransactionValidator<Client, Tx, B>>,
/// Additional block info required for validation.
block_info: Arc<OpL1BlockInfo>,
/// If true, ensure that the transaction's sender has enough balance to cover the L1 gas fee
@@ -54,7 +55,7 @@ pub struct OpTransactionValidator<Client, Tx> {
fork_tracker: Arc<OpForkTracker>,
}
impl<Client, Tx> OpTransactionValidator<Client, Tx> {
impl<Client, Tx, B: Block> OpTransactionValidator<Client, Tx, B> {
/// Returns the configured chain spec
pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
where
@@ -86,14 +87,15 @@ impl<Client, Tx> OpTransactionValidator<Client, Tx> {
}
}
impl<Client, Tx> OpTransactionValidator<Client, Tx>
impl<Client, Tx, B> OpTransactionValidator<Client, Tx, B>
where
Client:
ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt + Sync,
Tx: EthPoolTransaction + OpPooledTx,
B: Block,
{
/// Create a new [`OpTransactionValidator`].
pub fn new(inner: EthTransactionValidator<Client, Tx>) -> Self {
pub fn new(inner: EthTransactionValidator<Client, Tx, B>) -> Self {
let this = Self::with_block_info(inner, OpL1BlockInfo::default());
if let Ok(Some(block)) =
this.inner.client().block_by_number_or_tag(alloy_eips::BlockNumberOrTag::Latest)
@@ -112,7 +114,7 @@ where
/// Create a new [`OpTransactionValidator`] with the given [`OpL1BlockInfo`].
pub fn with_block_info(
inner: EthTransactionValidator<Client, Tx>,
inner: EthTransactionValidator<Client, Tx, B>,
block_info: OpL1BlockInfo,
) -> Self {
Self {
@@ -288,13 +290,15 @@ where
}
}
impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
impl<Client, Tx, B> TransactionValidator for OpTransactionValidator<Client, Tx, B>
where
Client:
ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt + Sync,
Tx: EthPoolTransaction + OpPooledTx,
B: Block,
{
type Transaction = Tx;
type Block = B;
async fn validate_transaction(
&self,
@@ -325,10 +329,7 @@ where
.await
}
fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
where
B: Block,
{
fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
self.inner.on_new_head_block(new_tip_block);
self.update_l1_block_info(
new_tip_block.header(),

View File

@@ -1,119 +0,0 @@
use crate::{
db_ext::DbTxPruneExt,
segments::{PruneInput, Segment},
PrunerError,
};
use alloy_primitives::B256;
use reth_db_api::{models::BlockNumberHashedAddress, table::Value, tables, transaction::DbTxMut};
use reth_primitives_traits::NodePrimitives;
use reth_provider::{
errors::provider::ProviderResult, BlockReader, ChainStateBlockReader, DBProvider,
NodePrimitivesProvider, PruneCheckpointWriter, TransactionsProvider,
};
use reth_prune_types::{
PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint,
};
use reth_stages_types::StageId;
use tracing::{instrument, trace};
#[derive(Debug)]
pub struct MerkleChangeSets {
mode: PruneMode,
}
impl MerkleChangeSets {
pub const fn new(mode: PruneMode) -> Self {
Self { mode }
}
}
impl<Provider> Segment<Provider> for MerkleChangeSets
where
Provider: DBProvider<Tx: DbTxMut>
+ PruneCheckpointWriter
+ TransactionsProvider
+ BlockReader
+ ChainStateBlockReader
+ NodePrimitivesProvider<Primitives: NodePrimitives<Receipt: Value>>,
{
fn segment(&self) -> PruneSegment {
PruneSegment::MerkleChangeSets
}
fn mode(&self) -> Option<PruneMode> {
Some(self.mode)
}
fn purpose(&self) -> PrunePurpose {
PrunePurpose::User
}
fn required_stage(&self) -> Option<StageId> {
Some(StageId::MerkleChangeSets)
}
#[instrument(level = "trace", target = "pruner", skip(self, provider), ret)]
fn prune(&self, provider: &Provider, input: PruneInput) -> Result<SegmentOutput, PrunerError> {
let Some(block_range) = input.get_next_block_range() else {
trace!(target: "pruner", "No change sets to prune");
return Ok(SegmentOutput::done())
};
let block_range_end = *block_range.end();
let mut limiter = input.limiter;
// Create range for StoragesTrieChangeSets which uses BlockNumberHashedAddress as key
let storage_range_start: BlockNumberHashedAddress =
(*block_range.start(), B256::ZERO).into();
let storage_range_end: BlockNumberHashedAddress =
(*block_range.end() + 1, B256::ZERO).into();
let storage_range = storage_range_start..storage_range_end;
let mut last_storages_pruned_block = None;
let (storages_pruned, done) =
provider.tx_ref().prune_dupsort_table_with_range::<tables::StoragesTrieChangeSets>(
storage_range,
&mut limiter,
|(BlockNumberHashedAddress((block_number, _)), _)| {
last_storages_pruned_block = Some(block_number);
},
)?;
trace!(target: "pruner", %storages_pruned, %done, "Pruned storages change sets");
let mut last_accounts_pruned_block = block_range_end;
let last_storages_pruned_block = last_storages_pruned_block
// If there's more storage changesets to prune, set the checkpoint block number to
// previous, so we could finish pruning its storage changesets on the next run.
.map(|block_number| if done { block_number } else { block_number.saturating_sub(1) })
.unwrap_or(block_range_end);
let (accounts_pruned, done) =
provider.tx_ref().prune_dupsort_table_with_range::<tables::AccountsTrieChangeSets>(
block_range,
&mut limiter,
|row| last_accounts_pruned_block = row.0,
)?;
trace!(target: "pruner", %accounts_pruned, %done, "Pruned accounts change sets");
let progress = limiter.progress(done);
Ok(SegmentOutput {
progress,
pruned: accounts_pruned + storages_pruned,
checkpoint: Some(SegmentOutputCheckpoint {
block_number: Some(last_storages_pruned_block.min(last_accounts_pruned_block)),
tx_number: None,
}),
})
}
fn save_checkpoint(
&self,
provider: &Provider,
checkpoint: PruneCheckpoint,
) -> ProviderResult<()> {
provider.save_prune_checkpoint(PruneSegment::MerkleChangeSets, checkpoint)
}
}

View File

@@ -12,7 +12,8 @@ use alloy_json_rpc::RpcObject;
use alloy_primitives::{Address, BlockHash, Bytes, B256, U256, U64};
use alloy_rpc_types_engine::{
ClientVersionV1, ExecutionPayloadBodiesV1, ExecutionPayloadInputV2, ExecutionPayloadV1,
ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus,
ExecutionPayloadV3, ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId,
PayloadStatus,
};
use alloy_rpc_types_eth::{
state::StateOverride, BlockOverrides, EIP1186AccountProofResponse, Filter, Log, SyncStatus,
@@ -73,6 +74,18 @@ pub trait EngineApi<Engine: EngineTypes> {
execution_requests: RequestsOrHash,
) -> RpcResult<PayloadStatus>;
/// Post Amsterdam payload handler
///
/// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_newpayloadv5>
#[method(name = "newPayloadV5")]
async fn new_payload_v5(
&self,
payload: ExecutionPayloadV4,
versioned_hashes: Vec<B256>,
parent_beacon_block_root: B256,
execution_requests: RequestsOrHash,
) -> RpcResult<PayloadStatus>;
/// See also <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/paris.md#engine_forkchoiceupdatedv1>
///
/// Caution: This should not accept the `withdrawals` field in the payload attributes.
@@ -178,6 +191,19 @@ pub trait EngineApi<Engine: EngineTypes> {
payload_id: PayloadId,
) -> RpcResult<Engine::ExecutionPayloadEnvelopeV5>;
/// Post Amsterdam payload handler.
///
/// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_getpayloadv6>
///
/// Returns the most recent version of the payload that is available in the corresponding
/// payload build process at the time of receiving this call. Note:
/// > Provider software MAY stop the corresponding build process after serving this call.
#[method(name = "getPayloadV6")]
async fn get_payload_v6(
&self,
payload_id: PayloadId,
) -> RpcResult<Engine::ExecutionPayloadEnvelopeV6>;
/// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#engine_getpayloadbodiesbyhashv1>
#[method(name = "getPayloadBodiesByHashV1")]
async fn get_payload_bodies_by_hash_v1(

View File

@@ -41,6 +41,7 @@ metrics.workspace = true
async-trait.workspace = true
jsonrpsee-core.workspace = true
jsonrpsee-types.workspace = true
parking_lot.workspace = true
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true

View File

@@ -0,0 +1,274 @@
//! Block Access List (BAL) cache for EIP-7928.
//!
//! This module provides an in-memory cache for storing Block Access Lists received via
//! the Engine API. BALs are stored for valid payloads and can be retrieved via
//! `engine_getBALsByHashV1` and `engine_getBALsByRangeV1`.
//!
//! According to EIP-7928, the EL MUST retain BALs for at least the duration of the
//! weak subjectivity period (~3533 epochs) to support synchronization with re-execution.
//! This initial implementation uses a simple in-memory cache with configurable capacity.
use alloy_primitives::{BlockHash, BlockNumber, Bytes};
use parking_lot::RwLock;
use reth_metrics::{
metrics::{Counter, Gauge},
Metrics,
};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
/// Default capacity for the BAL cache.
///
/// This is a conservative default - production deployments should configure based on
/// weak subjectivity period requirements (~3533 epochs ≈ 113,000 blocks).
const DEFAULT_BAL_CACHE_CAPACITY: u32 = 1024;
/// In-memory cache for Block Access Lists (BALs).
///
/// Provides O(1) lookups by block hash and O(log n) range queries by block number.
/// Evicts the oldest (lowest) block numbers when capacity is exceeded.
///
/// This type is cheaply cloneable as it wraps an `Arc` internally.
#[derive(Debug, Clone)]
pub struct BalCache {
inner: Arc<BalCacheInner>,
}
#[derive(Debug)]
struct BalCacheInner {
/// Maximum number of entries to store.
capacity: u32,
/// Mapping from block hash to BAL bytes.
entries: RwLock<HashMap<BlockHash, Bytes>>,
/// Index mapping block number to block hash for range queries.
/// Uses `BTreeMap` for efficient range iteration and eviction of oldest blocks.
block_index: RwLock<BTreeMap<BlockNumber, BlockHash>>,
/// Cache metrics.
metrics: BalCacheMetrics,
}
impl BalCache {
/// Creates a new BAL cache with the default capacity.
pub fn new() -> Self {
Self::with_capacity(DEFAULT_BAL_CACHE_CAPACITY)
}
/// Creates a new BAL cache with the specified capacity.
pub fn with_capacity(capacity: u32) -> Self {
Self {
inner: Arc::new(BalCacheInner {
capacity,
entries: RwLock::new(HashMap::new()),
block_index: RwLock::new(BTreeMap::new()),
metrics: BalCacheMetrics::default(),
}),
}
}
/// Inserts a BAL into the cache.
///
/// If a different hash already exists for this block number (reorg), the old entry
/// is removed first. If the cache is at capacity, the oldest block number is evicted.
pub fn insert(&self, block_hash: BlockHash, block_number: BlockNumber, bal: Bytes) {
let mut entries = self.inner.entries.write();
let mut block_index = self.inner.block_index.write();
// If this block number already has a different hash, remove the old entry
if let Some(old_hash) = block_index.get(&block_number) &&
*old_hash != block_hash
{
entries.remove(old_hash);
}
// Evict oldest block if at capacity and this is a new entry
if !entries.contains_key(&block_hash) &&
entries.len() as u32 >= self.inner.capacity &&
let Some((&oldest_num, &oldest_hash)) = block_index.first_key_value()
{
entries.remove(&oldest_hash);
block_index.remove(&oldest_num);
}
entries.insert(block_hash, bal);
block_index.insert(block_number, block_hash);
self.inner.metrics.inserts.increment(1);
self.inner.metrics.count.set(entries.len() as f64);
}
/// Retrieves BALs for the given block hashes.
///
/// Returns a vector with the same length as `block_hashes`, where each element
/// is `Some(bal)` if found or `None` if not in cache.
pub fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> Vec<Option<Bytes>> {
let entries = self.inner.entries.read();
block_hashes
.iter()
.map(|hash| {
let result = entries.get(hash).cloned();
if result.is_some() {
self.inner.metrics.hits.increment(1);
} else {
self.inner.metrics.misses.increment(1);
}
result
})
.collect()
}
/// Retrieves BALs for a range of blocks starting at `start` for `count` blocks.
///
/// Returns a vector of contiguous BALs in block number order, stopping at the first
/// missing block. This ensures the caller knows the returned BALs correspond to
/// blocks `[start, start + len)`.
pub fn get_by_range(&self, start: BlockNumber, count: u64) -> Vec<Bytes> {
let entries = self.inner.entries.read();
let block_index = self.inner.block_index.read();
let mut result = Vec::new();
for block_num in start..start.saturating_add(count) {
let Some(hash) = block_index.get(&block_num) else {
break;
};
let Some(bal) = entries.get(hash) else {
break;
};
result.push(bal.clone());
}
result
}
/// Returns the number of entries in the cache.
#[cfg(test)]
fn len(&self) -> usize {
self.inner.entries.read().len()
}
}
impl Default for BalCache {
fn default() -> Self {
Self::new()
}
}
/// Metrics for the BAL cache.
#[derive(Metrics)]
#[metrics(scope = "engine.bal_cache")]
struct BalCacheMetrics {
/// The total number of BALs in the cache.
count: Gauge,
/// The number of cache inserts.
inserts: Counter,
/// The number of cache hits.
hits: Counter,
/// The number of cache misses.
misses: Counter,
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::B256;
#[test]
fn test_insert_and_get_by_hash() {
let cache = BalCache::with_capacity(10);
let hash1 = B256::random();
let hash2 = B256::random();
let bal1 = Bytes::from_static(b"bal1");
let bal2 = Bytes::from_static(b"bal2");
cache.insert(hash1, 1, bal1.clone());
cache.insert(hash2, 2, bal2.clone());
let results = cache.get_by_hashes(&[hash1, hash2, B256::random()]);
assert_eq!(results.len(), 3);
assert_eq!(results[0], Some(bal1));
assert_eq!(results[1], Some(bal2));
assert_eq!(results[2], None);
}
#[test]
fn test_get_by_range() {
let cache = BalCache::with_capacity(10);
for i in 1..=5 {
let hash = B256::random();
let bal = Bytes::from(format!("bal{i}").into_bytes());
cache.insert(hash, i, bal);
}
let results = cache.get_by_range(2, 3);
assert_eq!(results.len(), 3);
}
#[test]
fn test_get_by_range_stops_at_gap() {
let cache = BalCache::with_capacity(10);
// Insert blocks 1, 2, 4, 5 (missing block 3)
for i in [1, 2, 4, 5] {
let hash = B256::random();
let bal = Bytes::from(format!("bal{i}").into_bytes());
cache.insert(hash, i, bal);
}
// Requesting range starting at 1 should stop at the gap (block 3)
let results = cache.get_by_range(1, 5);
assert_eq!(results.len(), 2); // Only blocks 1 and 2
// Requesting range starting at 4 should return 4 and 5
let results = cache.get_by_range(4, 3);
assert_eq!(results.len(), 2);
}
#[test]
fn test_eviction_oldest_first() {
let cache = BalCache::with_capacity(3);
// Insert blocks 10, 20, 30
for i in [10, 20, 30] {
let hash = B256::random();
cache.insert(hash, i, Bytes::from_static(b"bal"));
}
assert_eq!(cache.len(), 3);
// Insert block 40, should evict block 10 (oldest/lowest)
let hash40 = B256::random();
cache.insert(hash40, 40, Bytes::from_static(b"bal40"));
assert_eq!(cache.len(), 3);
// Block 10 should be gone, block 20 should still be there
let results = cache.get_by_range(10, 1);
assert_eq!(results.len(), 0);
let results = cache.get_by_range(20, 1);
assert_eq!(results.len(), 1);
}
#[test]
fn test_reorg_replaces_hash() {
let cache = BalCache::with_capacity(10);
let hash1 = B256::random();
let hash2 = B256::random();
let bal1 = Bytes::from_static(b"bal1");
let bal2 = Bytes::from_static(b"bal2");
// Insert block 100 with hash1
cache.insert(hash1, 100, bal1.clone());
assert_eq!(cache.get_by_hashes(&[hash1])[0], Some(bal1));
// Reorg: insert block 100 with hash2
cache.insert(hash2, 100, bal2.clone());
// hash1 should be gone, hash2 should be there
assert_eq!(cache.get_by_hashes(&[hash1])[0], None);
assert_eq!(cache.get_by_hashes(&[hash2])[0], Some(bal2));
assert_eq!(cache.len(), 1);
}
}

View File

@@ -1,18 +1,20 @@
use crate::{
capabilities::EngineCapabilities, metrics::EngineApiMetrics, EngineApiError, EngineApiResult,
bal_cache::BalCache, capabilities::EngineCapabilities, metrics::EngineApiMetrics,
EngineApiError, EngineApiResult,
};
use alloy_eips::{
eip1898::BlockHashOrNumber,
eip4844::{BlobAndProofV1, BlobAndProofV2},
eip4895::Withdrawals,
eip7685::RequestsOrHash,
BlockNumHash,
};
use alloy_primitives::{BlockHash, BlockNumber, B256, U64};
use alloy_primitives::{BlockHash, BlockNumber, Bytes, B256, U64};
use alloy_rpc_types_engine::{
CancunPayloadFields, ClientVersionV1, ExecutionData, ExecutionPayloadBodiesV1,
ExecutionPayloadBodyV1, ExecutionPayloadInputV2, ExecutionPayloadSidecar, ExecutionPayloadV1,
ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus,
PraguePayloadFields,
ExecutionPayloadV3, ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId,
PayloadStatus, PraguePayloadFields,
};
use async_trait::async_trait;
use jsonrpsee_core::{server::RpcModule, RpcResult};
@@ -21,7 +23,7 @@ use reth_engine_primitives::{ConsensusEngineHandle, EngineApiValidator, EngineTy
use reth_network_api::NetworkInfo;
use reth_payload_builder::PayloadStore;
use reth_payload_primitives::{
validate_payload_timestamp, EngineApiMessageVersion, MessageValidationKind,
validate_payload_timestamp, EngineApiMessageVersion, ExecutionPayload, MessageValidationKind,
PayloadOrAttributes, PayloadTypes,
};
use reth_primitives_traits::{Block, BlockBody};
@@ -96,6 +98,38 @@ where
validator: Validator,
accept_execution_requests_hash: bool,
network: impl NetworkInfo + 'static,
) -> Self {
Self::with_bal_cache(
provider,
chain_spec,
beacon_consensus,
payload_store,
tx_pool,
task_spawner,
client,
capabilities,
validator,
accept_execution_requests_hash,
network,
BalCache::new(),
)
}
/// Create new instance of [`EngineApi`] with a custom BAL cache.
#[expect(clippy::too_many_arguments)]
pub fn with_bal_cache(
provider: Provider,
chain_spec: Arc<ChainSpec>,
beacon_consensus: ConsensusEngineHandle<PayloadT>,
payload_store: PayloadStore<PayloadT>,
tx_pool: Pool,
task_spawner: Box<dyn TaskSpawner>,
client: ClientVersionV1,
capabilities: EngineCapabilities,
validator: Validator,
accept_execution_requests_hash: bool,
network: impl NetworkInfo + 'static,
bal_cache: BalCache,
) -> Self {
let is_syncing = Arc::new(move || network.is_syncing());
let inner = Arc::new(EngineApiInner {
@@ -111,10 +145,25 @@ where
validator,
accept_execution_requests_hash,
is_syncing,
bal_cache,
});
Self { inner }
}
/// Returns a reference to the BAL cache.
pub fn bal_cache(&self) -> &BalCache {
&self.inner.bal_cache
}
/// Caches the BAL if the status is valid.
fn maybe_cache_bal(&self, num_hash: BlockNumHash, bal: Option<Bytes>, status: &PayloadStatus) {
if status.is_valid() &&
let Some(bal) = bal
{
self.inner.bal_cache.insert(num_hash.hash, num_hash.number, bal);
}
}
/// Fetches the client version.
pub fn get_client_version_v1(
&self,
@@ -149,7 +198,11 @@ where
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V1, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metered version of `new_payload_v1`.
@@ -177,7 +230,12 @@ where
self.inner
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V2, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metered version of `new_payload_v2`.
@@ -206,7 +264,11 @@ where
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V3, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metrics version of `new_payload_v3`
@@ -236,7 +298,11 @@ where
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V4, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metrics version of `new_payload_v4`
@@ -881,6 +947,22 @@ where
res
}
/// Retrieves BALs for the given block hashes from the cache.
///
/// Returns the RLP-encoded BALs for blocks found in the cache.
/// Missing blocks are returned as empty bytes.
pub fn get_bals_by_hash(&self, block_hashes: Vec<BlockHash>) -> Vec<alloy_primitives::Bytes> {
let results = self.inner.bal_cache.get_by_hashes(&block_hashes);
results.into_iter().map(|opt| opt.unwrap_or_default()).collect()
}
/// Retrieves BALs for a range of blocks from the cache.
///
/// Returns the RLP-encoded BALs for blocks in the range `[start, start + count)`.
pub fn get_bals_by_range(&self, start: u64, count: u64) -> Vec<alloy_primitives::Bytes> {
self.inner.bal_cache.get_by_range(start, count)
}
}
// This is the concrete ethereum engine API implementation.
@@ -963,6 +1045,24 @@ where
Ok(self.new_payload_v4_metered(payload).await?)
}
/// Handler for `engine_newPayloadV5`
///
/// Post Amsterdam payload handler. Currently returns unsupported fork error.
///
/// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_newpayloadv5>
async fn new_payload_v5(
&self,
_payload: ExecutionPayloadV4,
_versioned_hashes: Vec<B256>,
_parent_beacon_block_root: B256,
_execution_requests: RequestsOrHash,
) -> RpcResult<PayloadStatus> {
trace!(target: "rpc::engine", "Serving engine_newPayloadV5");
Err(EngineApiError::EngineObjectValidationError(
reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
))?
}
/// Handler for `engine_forkchoiceUpdatedV1`
/// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_forkchoiceupdatedv1>
///
@@ -1086,6 +1186,21 @@ where
Ok(self.get_payload_v5_metered(payload_id).await?)
}
/// Handler for `engine_getPayloadV6`
///
/// Post Amsterdam payload handler. Currently returns unsupported fork error.
///
/// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#engine_getpayloadv6>
async fn get_payload_v6(
&self,
_payload_id: PayloadId,
) -> RpcResult<EngineT::ExecutionPayloadEnvelopeV6> {
trace!(target: "rpc::engine", "Serving engine_getPayloadV6");
Err(EngineApiError::EngineObjectValidationError(
reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
))?
}
/// Handler for `engine_getPayloadBodiesByHashV1`
/// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#engine_getpayloadbodiesbyhashv1>
async fn get_payload_bodies_by_hash_v1(
@@ -1172,12 +1287,10 @@ where
/// See also <https://eips.ethereum.org/EIPS/eip-7928>
async fn get_bals_by_hash_v1(
&self,
_block_hashes: Vec<BlockHash>,
block_hashes: Vec<BlockHash>,
) -> RpcResult<Vec<alloy_primitives::Bytes>> {
trace!(target: "rpc::engine", "Serving engine_getBALsByHashV1");
Err(EngineApiError::EngineObjectValidationError(
reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
))?
Ok(self.get_bals_by_hash(block_hashes))
}
/// Handler for `engine_getBALsByRangeV1`
@@ -1185,13 +1298,11 @@ where
/// See also <https://eips.ethereum.org/EIPS/eip-7928>
async fn get_bals_by_range_v1(
&self,
_start: U64,
_count: U64,
start: U64,
count: U64,
) -> RpcResult<Vec<alloy_primitives::Bytes>> {
trace!(target: "rpc::engine", "Serving engine_getBALsByRangeV1");
Err(EngineApiError::EngineObjectValidationError(
reth_payload_primitives::EngineObjectValidationError::UnsupportedFork,
))?
Ok(self.get_bals_by_range(start.to(), count.to()))
}
}
@@ -1251,6 +1362,8 @@ struct EngineApiInner<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSp
accept_execution_requests_hash: bool,
/// Returns `true` if the node is currently syncing.
is_syncing: Arc<dyn Fn() -> bool + Send + Sync>,
/// Cache for Block Access Lists (BALs) per EIP-7928.
bal_cache: BalCache,
}
#[cfg(test)]

View File

@@ -12,6 +12,10 @@
/// The Engine API implementation.
mod engine_api;
/// Block Access List (BAL) cache for EIP-7928.
mod bal_cache;
pub use bal_cache::BalCache;
/// Engine API capabilities.
pub mod capabilities;
pub use capabilities::EngineCapabilities;

View File

@@ -258,11 +258,13 @@ where
let call = match result {
ExecutionResult::Halt { reason, gas_used } => {
let error = Err::from_evm_halt(reason, tx.gas_limit());
#[allow(clippy::needless_update)]
SimCallResult {
return_data: Bytes::new(),
error: Some(SimulateError {
message: error.to_string(),
code: error.into().code(),
..SimulateError::invalid_params()
}),
gas_used,
logs: Vec::new(),
@@ -271,11 +273,13 @@ where
}
ExecutionResult::Revert { output, gas_used } => {
let error = Err::from_revert(output.clone());
#[allow(clippy::needless_update)]
SimCallResult {
return_data: output,
error: Some(SimulateError {
message: error.to_string(),
code: error.into().code(),
..SimulateError::invalid_params()
}),
gas_used,
status: false,

View File

@@ -136,7 +136,7 @@ where
info!(target: "sync::stages::index_account_history::exec", "Loading indices into database");
provider.with_rocksdb_batch(|rocksdb_batch| {
provider.with_rocksdb_batch_auto_commit(|rocksdb_batch| {
let mut writer = EitherWriter::new_accounts_history(provider, rocksdb_batch)?;
load_account_history(collector, first_sync, &mut writer)
.map_err(|e| reth_provider::ProviderError::other(Box::new(e)))?;
@@ -517,95 +517,6 @@ mod tests {
assert!(table.is_empty());
}
/// Tests exact shard boundary: exactly k * `NUM_OF_INDICES_IN_SHARD` entries.
/// Verifies the final shard correctly uses `u64::MAX` as sentinel key when
/// the entry count is an exact multiple of shard size.
#[tokio::test]
async fn insert_index_exact_shard_boundary() {
let db = TestStageDB::default();
db.commit(|tx| {
for block in 0..NUM_OF_INDICES_IN_SHARD as u64 {
tx.put::<tables::BlockBodyIndices>(
block,
StoredBlockBodyIndices { tx_count: 1, ..Default::default() },
)?;
tx.put::<tables::AccountChangeSets>(block, acc())?;
}
Ok(())
})
.unwrap();
run(&db, (NUM_OF_INDICES_IN_SHARD - 1) as u64, None);
let table = cast(db.table::<tables::AccountsHistory>().unwrap());
let expected_blocks: Vec<u64> = (0..NUM_OF_INDICES_IN_SHARD as u64).collect();
assert_eq!(table.len(), 1, "Should have exactly one shard");
assert_eq!(
table,
BTreeMap::from([(shard(u64::MAX), expected_blocks)]),
"Final shard key should be u64::MAX"
);
unwind(&db, (NUM_OF_INDICES_IN_SHARD - 1) as u64, 0);
let table = cast(db.table::<tables::AccountsHistory>().unwrap());
assert_eq!(table, BTreeMap::from([(shard(u64::MAX), vec![0])]));
}
/// Tests incremental merge overflow: existing full shard gets converted
/// from `u64::MAX` sentinel to actual highest block, and new entries
/// create a new final shard with `u64::MAX`.
#[tokio::test]
async fn insert_index_incremental_merge_overflow() {
let db = TestStageDB::default();
let first_shard_blocks: Vec<u64> = (0..NUM_OF_INDICES_IN_SHARD as u64).collect();
db.commit(|tx| {
for block in 0..(NUM_OF_INDICES_IN_SHARD + 5) as u64 {
tx.put::<tables::BlockBodyIndices>(
block,
StoredBlockBodyIndices { tx_count: 1, ..Default::default() },
)?;
tx.put::<tables::AccountChangeSets>(block, acc())?;
}
tx.put::<tables::AccountsHistory>(shard(u64::MAX), list(&first_shard_blocks))?;
Ok(())
})
.unwrap();
let last_block = (NUM_OF_INDICES_IN_SHARD + 4) as u64;
run(&db, last_block, Some((NUM_OF_INDICES_IN_SHARD - 1) as u64));
let table = cast(db.table::<tables::AccountsHistory>().unwrap());
assert_eq!(table.len(), 2, "Should have two shards after overflow");
let new_shard_blocks: Vec<u64> =
(NUM_OF_INDICES_IN_SHARD as u64..(NUM_OF_INDICES_IN_SHARD + 5) as u64).collect();
assert_eq!(
table.get(&shard((NUM_OF_INDICES_IN_SHARD - 1) as u64)),
Some(&first_shard_blocks),
"First shard should have highest_block = last entry"
);
assert_eq!(
table.get(&shard(u64::MAX)),
Some(&new_shard_blocks),
"New final shard should have u64::MAX key"
);
unwind(&db, last_block, (NUM_OF_INDICES_IN_SHARD - 1) as u64);
let table = cast(db.table::<tables::AccountsHistory>().unwrap());
assert_eq!(
table,
BTreeMap::from([(shard(u64::MAX), first_shard_blocks)]),
"After unwind, should revert to single shard with u64::MAX"
);
}
stage_test_suite_ext!(IndexAccountHistoryTestRunner, index_account_history);
struct IndexAccountHistoryTestRunner {

View File

@@ -140,7 +140,7 @@ where
info!(target: "sync::stages::index_storage_history::exec", "Loading indices into database");
provider.with_rocksdb_batch(|rocksdb_batch| {
provider.with_rocksdb_batch_auto_commit(|rocksdb_batch| {
let mut writer = EitherWriter::new_storages_history(provider, rocksdb_batch)?;
load_storage_history(collector, first_sync, &mut writer)
.map_err(|e| reth_provider::ProviderError::other(Box::new(e)))?;
@@ -537,101 +537,6 @@ mod tests {
assert!(table.is_empty());
}
/// Tests exact shard boundary: exactly k * `NUM_OF_INDICES_IN_SHARD` entries.
/// Verifies the final shard correctly uses `u64::MAX` as sentinel key when
/// the entry count is an exact multiple of shard size.
#[tokio::test]
async fn insert_index_exact_shard_boundary() {
let db = TestStageDB::default();
db.commit(|tx| {
for block in 0..NUM_OF_INDICES_IN_SHARD as u64 {
tx.put::<tables::BlockBodyIndices>(
block,
StoredBlockBodyIndices { tx_count: 1, ..Default::default() },
)?;
tx.put::<tables::StorageChangeSets>(
block_number_address(block),
storage(STORAGE_KEY),
)?;
}
Ok(())
})
.unwrap();
run(&db, (NUM_OF_INDICES_IN_SHARD - 1) as u64, None);
let table = cast(db.table::<tables::StoragesHistory>().unwrap());
let expected_blocks: Vec<u64> = (0..NUM_OF_INDICES_IN_SHARD as u64).collect();
assert_eq!(table.len(), 1, "Should have exactly one shard");
assert_eq!(
table,
BTreeMap::from([(shard(u64::MAX), expected_blocks)]),
"Final shard key should be u64::MAX"
);
unwind(&db, (NUM_OF_INDICES_IN_SHARD - 1) as u64, 0);
let table = cast(db.table::<tables::StoragesHistory>().unwrap());
assert_eq!(table, BTreeMap::from([(shard(u64::MAX), vec![0])]));
}
/// Tests incremental merge overflow: existing full shard gets converted
/// from `u64::MAX` sentinel to actual highest block, and new entries
/// create a new final shard with `u64::MAX`.
#[tokio::test]
async fn insert_index_incremental_merge_overflow() {
let db = TestStageDB::default();
let first_shard_blocks: Vec<u64> = (0..NUM_OF_INDICES_IN_SHARD as u64).collect();
db.commit(|tx| {
for block in 0..(NUM_OF_INDICES_IN_SHARD + 5) as u64 {
tx.put::<tables::BlockBodyIndices>(
block,
StoredBlockBodyIndices { tx_count: 1, ..Default::default() },
)?;
tx.put::<tables::StorageChangeSets>(
block_number_address(block),
storage(STORAGE_KEY),
)?;
}
tx.put::<tables::StoragesHistory>(shard(u64::MAX), list(&first_shard_blocks))?;
Ok(())
})
.unwrap();
let last_block = (NUM_OF_INDICES_IN_SHARD + 4) as u64;
run(&db, last_block, Some((NUM_OF_INDICES_IN_SHARD - 1) as u64));
let table = cast(db.table::<tables::StoragesHistory>().unwrap());
assert_eq!(table.len(), 2, "Should have two shards after overflow");
let new_shard_blocks: Vec<u64> =
(NUM_OF_INDICES_IN_SHARD as u64..(NUM_OF_INDICES_IN_SHARD + 5) as u64).collect();
assert_eq!(
table.get(&shard((NUM_OF_INDICES_IN_SHARD - 1) as u64)),
Some(&first_shard_blocks),
"First shard should have highest_block = last entry"
);
assert_eq!(
table.get(&shard(u64::MAX)),
Some(&new_shard_blocks),
"New final shard should have u64::MAX key"
);
unwind(&db, last_block, (NUM_OF_INDICES_IN_SHARD - 1) as u64);
let table = cast(db.table::<tables::StoragesHistory>().unwrap());
assert_eq!(
table,
BTreeMap::from([(shard(u64::MAX), first_shard_blocks)]),
"After unwind, should revert to single shard with u64::MAX"
);
}
stage_test_suite_ext!(IndexStorageHistoryTestRunner, index_storage_history);
struct IndexStorageHistoryTestRunner {

View File

@@ -1,449 +0,0 @@
use crate::stages::merkle::INVALID_STATE_ROOT_ERROR_MESSAGE;
use alloy_consensus::BlockHeader;
use alloy_primitives::BlockNumber;
use reth_consensus::ConsensusError;
use reth_primitives_traits::{GotExpected, SealedHeader};
use reth_provider::{
BlockNumReader, ChainStateBlockReader, ChangeSetReader, DBProvider, HeaderProvider,
ProviderError, PruneCheckpointReader, PruneCheckpointWriter, StageCheckpointReader,
StageCheckpointWriter, StorageChangeSetReader, TrieWriter,
};
use reth_prune_types::{
PruneCheckpoint, PruneMode, PruneSegment, MERKLE_CHANGESETS_RETENTION_BLOCKS,
};
use reth_stages_api::{
BlockErrorKind, ExecInput, ExecOutput, Stage, StageCheckpoint, StageError, StageId,
UnwindInput, UnwindOutput,
};
use reth_trie::{
updates::TrieUpdates, HashedPostStateSorted, KeccakKeyHasher, StateRoot, TrieInputSorted,
};
use reth_trie_db::{DatabaseHashedPostState, DatabaseStateRoot};
use std::{ops::Range, sync::Arc};
use tracing::{debug, error};
/// The `MerkleChangeSets` stage.
///
/// This stage processes and maintains trie changesets from the finalized block to the latest block.
#[derive(Debug, Clone)]
pub struct MerkleChangeSets {
/// The number of blocks to retain changesets for, used as a fallback when the finalized block
/// is not found. Defaults to [`MERKLE_CHANGESETS_RETENTION_BLOCKS`] (2 epochs in beacon
/// chain).
retention_blocks: u64,
}
impl MerkleChangeSets {
/// Creates a new `MerkleChangeSets` stage with the default retention blocks.
pub const fn new() -> Self {
Self { retention_blocks: MERKLE_CHANGESETS_RETENTION_BLOCKS }
}
/// Creates a new `MerkleChangeSets` stage with a custom finalized block height.
pub const fn with_retention_blocks(retention_blocks: u64) -> Self {
Self { retention_blocks }
}
/// Returns the range of blocks which are already computed. Will return an empty range if none
/// have been computed.
fn computed_range<Provider>(
provider: &Provider,
checkpoint: Option<StageCheckpoint>,
) -> Result<Range<BlockNumber>, StageError>
where
Provider: PruneCheckpointReader,
{
let to = checkpoint.map(|chk| chk.block_number).unwrap_or_default();
// Get the prune checkpoint for MerkleChangeSets to use as the lower bound. If there's no
// prune checkpoint or if the pruned block number is None, return empty range
let Some(from) = provider
.get_prune_checkpoint(PruneSegment::MerkleChangeSets)?
.and_then(|chk| chk.block_number)
// prune checkpoint indicates the last block pruned, so the block after is the start of
// the computed data
.map(|block_number| block_number + 1)
else {
return Ok(0..0)
};
Ok(from..to + 1)
}
/// Determines the target range for changeset computation based on the checkpoint and provider
/// state.
///
/// Returns the target range (exclusive end) to compute changesets for.
fn determine_target_range<Provider>(
&self,
provider: &Provider,
) -> Result<Range<BlockNumber>, StageError>
where
Provider: StageCheckpointReader + ChainStateBlockReader,
{
// Get merkle checkpoint which represents our target end block
let merkle_checkpoint = provider
.get_stage_checkpoint(StageId::MerkleExecute)?
.map(|checkpoint| checkpoint.block_number)
.unwrap_or(0);
let target_end = merkle_checkpoint + 1; // exclusive
// Calculate the target range based on the finalized block and the target block.
// We maintain changesets from the finalized block to the latest block.
let finalized_block = provider.last_finalized_block_number()?;
// Calculate the fallback start position based on retention blocks
let retention_based_start = merkle_checkpoint.saturating_sub(self.retention_blocks);
// If the finalized block was way in the past then we don't want to generate changesets for
// all of those past blocks; we only care about the recent history.
//
// Use maximum of finalized_block and retention_based_start if finalized_block exists,
// otherwise just use retention_based_start.
let mut target_start = finalized_block
.map(|finalized| finalized.saturating_add(1).max(retention_based_start))
.unwrap_or(retention_based_start);
// We cannot revert the genesis block; target_start must be >0
target_start = target_start.max(1);
Ok(target_start..target_end)
}
/// Calculates the trie updates given a [`TrieInputSorted`], asserting that the resulting state
/// root matches the expected one for the block.
fn calculate_block_trie_updates<Provider: DBProvider + HeaderProvider>(
provider: &Provider,
block_number: BlockNumber,
input: TrieInputSorted,
) -> Result<TrieUpdates, StageError> {
let (root, trie_updates) =
StateRoot::overlay_root_from_nodes_with_updates(provider.tx_ref(), input).map_err(
|e| {
error!(
target: "sync::stages::merkle_changesets",
%e,
?block_number,
"Incremental state root failed! {INVALID_STATE_ROOT_ERROR_MESSAGE}");
StageError::Fatal(Box::new(e))
},
)?;
let block = provider
.header_by_number(block_number)?
.ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))?;
let (got, expected) = (root, block.state_root());
if got != expected {
// Only seal the header when we need it for the error
let header = SealedHeader::seal_slow(block);
error!(
target: "sync::stages::merkle_changesets",
?block_number,
?got,
?expected,
"Failed to verify block state root! {INVALID_STATE_ROOT_ERROR_MESSAGE}",
);
return Err(StageError::Block {
error: BlockErrorKind::Validation(ConsensusError::BodyStateRootDiff(
GotExpected { got, expected }.into(),
)),
block: Box::new(header.block_with_parent()),
})
}
Ok(trie_updates)
}
fn populate_range<Provider>(
provider: &Provider,
target_range: Range<BlockNumber>,
) -> Result<(), StageError>
where
Provider: StageCheckpointReader
+ TrieWriter
+ DBProvider
+ HeaderProvider
+ ChainStateBlockReader
+ BlockNumReader
+ ChangeSetReader
+ StorageChangeSetReader,
{
let target_start = target_range.start;
let target_end = target_range.end;
debug!(
target: "sync::stages::merkle_changesets",
?target_range,
"Starting trie changeset computation",
);
// We need to distinguish a cumulative revert and a per-block revert. A cumulative revert
// reverts changes starting at db tip all the way to a block. A per-block revert only
// reverts a block's changes.
//
// We need to calculate the cumulative HashedPostState reverts for every block in the
// target range. The cumulative HashedPostState revert for block N can be calculated as:
//
//
// ```
// // where `extend` overwrites any shared keys
// cumulative_state_revert(N) = cumulative_state_revert(N + 1).extend(get_block_state_revert(N))
// ```
//
// We need per-block reverts to calculate the prefix set for each individual block. By
// using the per-block reverts to calculate cumulative reverts on-the-fly we can save a
// bunch of memory.
debug!(
target: "sync::stages::merkle_changesets",
?target_range,
"Computing per-block state reverts",
);
let range_len = target_end - target_start;
let mut per_block_state_reverts = Vec::with_capacity(range_len as usize);
for block_number in target_range.clone() {
per_block_state_reverts.push(HashedPostStateSorted::from_reverts::<KeccakKeyHasher>(
provider,
block_number..=block_number,
)?);
}
// Helper to retrieve state revert data for a specific block from the pre-computed array
let get_block_state_revert = |block_number: BlockNumber| -> &HashedPostStateSorted {
let index = (block_number - target_start) as usize;
&per_block_state_reverts[index]
};
// Helper to accumulate state reverts from a given block to the target end
let compute_cumulative_state_revert = |block_number: BlockNumber| -> HashedPostStateSorted {
let mut cumulative_revert = HashedPostStateSorted::default();
for n in (block_number..target_end).rev() {
cumulative_revert.extend_ref_and_sort(get_block_state_revert(n))
}
cumulative_revert
};
// To calculate the changeset for a block, we first need the TrieUpdates which are
// generated as a result of processing the block. To get these we need:
// 1) The TrieUpdates which revert the db's trie to _prior_ to the block
// 2) The HashedPostStateSorted to revert the db's state to _after_ the block
//
// To get (1) for `target_start` we need to do a big state root calculation which takes
// into account all changes between that block and db tip. For each block after the
// `target_start` we can update (1) using the TrieUpdates which were output by the previous
// block, only targeting the state changes of that block.
debug!(
target: "sync::stages::merkle_changesets",
?target_start,
"Computing trie state at starting block",
);
let initial_state = compute_cumulative_state_revert(target_start);
let initial_prefix_sets = initial_state.construct_prefix_sets();
let initial_input =
TrieInputSorted::new(Arc::default(), Arc::new(initial_state), initial_prefix_sets);
// target_start will be >= 1, see `determine_target_range`.
let mut nodes = Arc::new(
Self::calculate_block_trie_updates(provider, target_start - 1, initial_input)?
.into_sorted(),
);
for block_number in target_range {
debug!(
target: "sync::stages::merkle_changesets",
?block_number,
"Computing trie updates for block",
);
// Revert the state so that this block has been just processed, meaning we take the
// cumulative revert of the subsequent block.
let state = Arc::new(compute_cumulative_state_revert(block_number + 1));
// Construct prefix sets from only this block's `HashedPostStateSorted`, because we only
// care about trie updates which occurred as a result of this block being processed.
let prefix_sets = get_block_state_revert(block_number).construct_prefix_sets();
let input = TrieInputSorted::new(Arc::clone(&nodes), state, prefix_sets);
// Calculate the trie updates for this block, then apply those updates to the reverts.
// We calculate the overlay which will be passed into the next step using the trie
// reverts prior to them being updated.
let this_trie_updates =
Self::calculate_block_trie_updates(provider, block_number, input)?.into_sorted();
let trie_overlay = Arc::clone(&nodes);
let mut nodes_mut = Arc::unwrap_or_clone(nodes);
nodes_mut.extend_ref_and_sort(&this_trie_updates);
nodes = Arc::new(nodes_mut);
// Write the changesets to the DB using the trie updates produced by the block, and the
// trie reverts as the overlay.
debug!(
target: "sync::stages::merkle_changesets",
?block_number,
"Writing trie changesets for block",
);
provider.write_trie_changesets(
block_number,
&this_trie_updates,
Some(&trie_overlay),
)?;
}
Ok(())
}
}
impl Default for MerkleChangeSets {
fn default() -> Self {
Self::new()
}
}
impl<Provider> Stage<Provider> for MerkleChangeSets
where
Provider: StageCheckpointReader
+ TrieWriter
+ DBProvider
+ HeaderProvider
+ ChainStateBlockReader
+ StageCheckpointWriter
+ PruneCheckpointReader
+ PruneCheckpointWriter
+ ChangeSetReader
+ StorageChangeSetReader
+ BlockNumReader,
{
fn id(&self) -> StageId {
StageId::MerkleChangeSets
}
fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result<ExecOutput, StageError> {
// Get merkle checkpoint and assert that the target is the same.
let merkle_checkpoint = provider
.get_stage_checkpoint(StageId::MerkleExecute)?
.map(|checkpoint| checkpoint.block_number)
.unwrap_or(0);
if input.target.is_none_or(|target| merkle_checkpoint != target) {
return Err(StageError::Fatal(eyre::eyre!("Cannot sync stage to block {:?} when MerkleExecute is at block {merkle_checkpoint:?}", input.target).into()))
}
let mut target_range = self.determine_target_range(provider)?;
// Get the previously computed range. This will be updated to reflect the populating of the
// target range.
let mut computed_range = Self::computed_range(provider, input.checkpoint)?;
debug!(
target: "sync::stages::merkle_changesets",
?computed_range,
?target_range,
"Got computed and target ranges",
);
// We want the target range to not include any data already computed previously, if
// possible, so we start the target range from the end of the computed range if that is
// greater.
//
// ------------------------------> Block #
// |------computed-----|
// |-----target-----|
// |--actual--|
//
// However, if the target start is less than the previously computed start, we don't want to
// do this, as it would leave a gap of data at `target_range.start..=computed_range.start`.
//
// ------------------------------> Block #
// |---computed---|
// |-------target-------|
// |-------actual-------|
//
if target_range.start >= computed_range.start {
target_range.start = target_range.start.max(computed_range.end);
}
// If target range is empty (target_start >= target_end), stage is already successfully
// executed.
if target_range.start >= target_range.end {
return Ok(ExecOutput::done(StageCheckpoint::new(target_range.end.saturating_sub(1))));
}
// If our target range is a continuation of the already computed range then we can keep the
// already computed data.
if target_range.start == computed_range.end {
// Clear from target_start onwards to ensure no stale data exists
provider.clear_trie_changesets_from(target_range.start)?;
computed_range.end = target_range.end;
} else {
// If our target range is not a continuation of the already computed range then we
// simply clear the computed data, to make sure there's no gaps or conflicts.
provider.clear_trie_changesets()?;
computed_range = target_range.clone();
}
// Populate the target range with changesets
Self::populate_range(provider, target_range)?;
// Update the prune checkpoint to reflect that all data before `computed_range.start`
// is not available.
provider.save_prune_checkpoint(
PruneSegment::MerkleChangeSets,
PruneCheckpoint {
block_number: Some(computed_range.start.saturating_sub(1)),
tx_number: None,
prune_mode: PruneMode::Before(computed_range.start),
},
)?;
// `computed_range.end` is exclusive.
let checkpoint = StageCheckpoint::new(computed_range.end.saturating_sub(1));
Ok(ExecOutput::done(checkpoint))
}
fn unwind(
&mut self,
provider: &Provider,
input: UnwindInput,
) -> Result<UnwindOutput, StageError> {
// Unwinding is trivial; just clear everything after the target block.
provider.clear_trie_changesets_from(input.unwind_to + 1)?;
let mut computed_range = Self::computed_range(provider, Some(input.checkpoint))?;
computed_range.end = input.unwind_to + 1;
if computed_range.start > computed_range.end {
computed_range.start = computed_range.end;
}
// If we've unwound so far that there are no longer enough trie changesets available then
// simply clear them and the checkpoints, so that on next pipeline startup they will be
// regenerated.
//
// We don't do this check if the target block is not greater than the retention threshold
// (which happens near genesis), as in that case would could still have all possible
// changesets even if the total count doesn't meet the threshold.
debug!(
target: "sync::stages::merkle_changesets",
?computed_range,
retention_blocks=?self.retention_blocks,
"Checking if computed range is over retention threshold",
);
if input.unwind_to > self.retention_blocks &&
computed_range.end - computed_range.start < self.retention_blocks
{
debug!(
target: "sync::stages::merkle_changesets",
?computed_range,
retention_blocks=?self.retention_blocks,
"Clearing checkpoints completely",
);
provider.clear_trie_changesets()?;
provider
.save_stage_checkpoint(StageId::MerkleChangeSets, StageCheckpoint::default())?;
return Ok(UnwindOutput { checkpoint: StageCheckpoint::default() })
}
// `computed_range.end` is exclusive
let checkpoint = StageCheckpoint::new(computed_range.end.saturating_sub(1));
Ok(UnwindOutput { checkpoint })
}
}

View File

@@ -1,5 +1,6 @@
/// The bodies stage.
mod bodies;
mod era;
/// The execution stage that generates state diff.
mod execution;
/// The finish stage
@@ -36,9 +37,7 @@ pub use prune::*;
pub use sender_recovery::*;
pub use tx_lookup::*;
mod era;
mod utils;
use utils::*;
#[cfg(test)]

View File

@@ -158,15 +158,13 @@ where
let append_only =
provider.count_entries::<tables::TransactionHashNumbers>()?.is_zero();
// Create RocksDB batch if feature is enabled
// Auto-commits on threshold; consistency check heals any crash.
#[cfg(all(unix, feature = "rocksdb"))]
let rocksdb = provider.rocksdb_provider();
#[cfg(all(unix, feature = "rocksdb"))]
let rocksdb_batch = rocksdb.batch();
let rocksdb_batch = rocksdb.batch_with_auto_commit();
#[cfg(not(all(unix, feature = "rocksdb")))]
let rocksdb_batch = ();
// Create writer that routes to either MDBX or RocksDB based on settings
let mut writer =
EitherWriter::new_transaction_hash_numbers(provider, rocksdb_batch)?;
@@ -217,15 +215,12 @@ where
) -> Result<UnwindOutput, StageError> {
let (range, unwind_to, _) = input.unwind_block_range_with_threshold(self.chunk_size);
// Create RocksDB batch if feature is enabled
#[cfg(all(unix, feature = "rocksdb"))]
let rocksdb = provider.rocksdb_provider();
#[cfg(all(unix, feature = "rocksdb"))]
let rocksdb_batch = rocksdb.batch();
#[cfg(not(all(unix, feature = "rocksdb")))]
let rocksdb_batch = ();
// Create writer that routes to either MDBX or RocksDB based on settings
let mut writer = EitherWriter::new_transaction_hash_numbers(provider, rocksdb_batch)?;
let static_file_provider = provider.static_file_provider();

View File

@@ -1,13 +1,13 @@
//! Utils for `stages`.
use alloy_primitives::{Address, BlockNumber, TxNumber};
use alloy_primitives::{Address, BlockNumber, TxNumber, B256};
use reth_config::config::EtlConfig;
use reth_db_api::{
cursor::{DbCursorRO, DbCursorRW},
models::{
sharded_key::NUM_OF_INDICES_IN_SHARD, storage_sharded_key::StorageShardedKey,
AccountBeforeTx, AddressStorageKey, BlockNumberAddress, ShardedHistoryKey, ShardedKey,
AccountBeforeTx, AddressStorageKey, BlockNumberAddress, ShardedKey,
},
table::{Decode, Decompress, Key, Table},
table::{Decode, Decompress, Table},
transaction::DbTx,
BlockNumberList,
};
@@ -15,7 +15,7 @@ use reth_etl::Collector;
use reth_primitives_traits::NodePrimitives;
use reth_provider::{
providers::StaticFileProvider, to_range, BlockReader, DBProvider, EitherWriter, ProviderError,
ProviderResult, StaticFileProviderFactory,
StaticFileProviderFactory,
};
use reth_stages_api::StageError;
use reth_static_file_types::StaticFileSegment;
@@ -23,169 +23,6 @@ use reth_storage_api::{ChangeSetReader, StorageChangeSetReader};
use std::{collections::HashMap, hash::Hash, ops::RangeBounds};
use tracing::info;
/// Trait for writing sharded history indices to the database.
pub(crate) trait HistoryShardWriter {
/// The full sharded key type for the table.
type TableKey: Key + ShardedHistoryKey;
/// Gets the last shard for a prefix (for incremental sync merging).
fn get_last_shard(
&mut self,
prefix: <Self::TableKey as ShardedHistoryKey>::Prefix,
) -> ProviderResult<Option<BlockNumberList>>;
/// Writes a shard to the database (append or upsert based on flag).
fn write_shard(
&mut self,
key: Self::TableKey,
value: &BlockNumberList,
append: bool,
) -> ProviderResult<()>;
}
/// Loads sharded history indices from a collector into the database.
///
/// ## Why sharding?
/// History indices track "which blocks modified this address/storage slot". A popular contract
/// may have millions of changes, too large for a single DB value. We split into shards of
/// `NUM_OF_INDICES_IN_SHARD` (2000) block numbers each.
///
/// ## Key structure
/// Each shard is keyed by `(prefix, highest_block_in_shard)`. Example for an address:
/// - `(0xABC..., 5000)` → blocks 3001-5000
/// - `(0xABC..., u64::MAX)` → blocks 5001-6234 (final shard)
///
/// The `u64::MAX` sentinel on the last shard enables `seek_exact(prefix, u64::MAX)` to find
/// it for incremental sync merging.
///
/// When `append_only=true`, collector must yield keys in ascending order (MDBX requirement).
fn load_sharded_history<H: HistoryShardWriter>(
collector: &mut Collector<H::TableKey, BlockNumberList>,
append_only: bool,
writer: &mut H,
) -> Result<(), StageError> {
type Prefix<H> = <<H as HistoryShardWriter>::TableKey as ShardedHistoryKey>::Prefix;
// Option needed to distinguish "no prefix yet" from "processing Address::ZERO"
let mut current_prefix: Option<Prefix<H>> = None;
// Buffer for block numbers; sized for ~2 shards to minimize reallocations
let mut current_list = Vec::<u64>::with_capacity(NUM_OF_INDICES_IN_SHARD * 2);
// Progress reporting setup
let total_entries = collector.len();
let interval = (total_entries / 10).max(1);
for (index, element) in collector.iter()?.enumerate() {
let (k, v) = element?;
let sharded_key = H::TableKey::decode_owned(k)?;
let new_list = BlockNumberList::decompress_owned(v)?;
if index > 0 && index.is_multiple_of(interval) && total_entries > 10 {
info!(target: "sync::stages::index_history", progress = %format!("{:.2}%", (index as f64 / total_entries as f64) * 100.0), "Writing indices");
}
let prefix = sharded_key.prefix();
// When prefix changes, flush previous prefix's shards and start fresh
if current_prefix != Some(prefix) {
// Flush remaining shards for the previous prefix (uses u64::MAX for final shard)
if let Some(prev_prefix) = current_prefix {
flush_shards::<H>(prev_prefix, &mut current_list, append_only, writer)?;
}
current_prefix = Some(prefix);
current_list.clear();
// On incremental sync, merge with existing last shard (stored with u64::MAX key)
if !append_only && let Some(last_shard) = writer.get_last_shard(prefix)? {
current_list.extend(last_shard.iter());
}
}
// Accumulate new block numbers
current_list.extend(new_list.iter());
// Flush complete shards while keeping one buffered for continued accumulation
flush_shards_partial::<H>(prefix, &mut current_list, append_only, writer)?;
}
// Flush final prefix's remaining shard
if let Some(prefix) = current_prefix {
flush_shards::<H>(prefix, &mut current_list, append_only, writer)?;
}
Ok(())
}
/// Flushes complete shards, keeping at least one shard buffered for continued accumulation.
///
/// We buffer one shard because `flush_shards` uses `u64::MAX` as the final shard's key.
/// If we flushed everything here, we'd write `u64::MAX` keys that get overwritten later.
fn flush_shards_partial<H: HistoryShardWriter>(
prefix: <H::TableKey as ShardedHistoryKey>::Prefix,
list: &mut Vec<u64>,
append_only: bool,
writer: &mut H,
) -> Result<(), StageError> {
// Not enough to fill a shard yet
if list.len() <= NUM_OF_INDICES_IN_SHARD {
return Ok(());
}
let num_full_shards = list.len() / NUM_OF_INDICES_IN_SHARD;
// Keep one shard buffered: if exact multiple, keep last full shard for u64::MAX key later
let shards_to_flush = if list.len().is_multiple_of(NUM_OF_INDICES_IN_SHARD) {
num_full_shards - 1
} else {
num_full_shards
};
if shards_to_flush == 0 {
return Ok(());
}
let flush_len = shards_to_flush * NUM_OF_INDICES_IN_SHARD;
debug_assert!(flush_len <= list.len(), "flush_len exceeds list length");
// Write complete shards with their actual highest block number as key
for chunk in list[..flush_len].chunks(NUM_OF_INDICES_IN_SHARD) {
let highest = *chunk.last().expect("chunk is non-empty");
let key = H::TableKey::new_sharded(prefix, highest);
let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
writer.write_shard(key, &value, append_only)?;
}
// Shift remaining elements to front (avoids allocation vs split_off)
list.copy_within(flush_len.., 0);
list.truncate(list.len() - flush_len);
Ok(())
}
/// Flushes all remaining shards. Uses `u64::MAX` for the final shard's key to enable
/// incremental sync lookups via `seek_exact(prefix, u64::MAX)`.
fn flush_shards<H: HistoryShardWriter>(
prefix: <H::TableKey as ShardedHistoryKey>::Prefix,
list: &mut Vec<u64>,
append_only: bool,
writer: &mut H,
) -> Result<(), StageError> {
if list.is_empty() {
return Ok(());
}
let num_chunks = list.len().div_ceil(NUM_OF_INDICES_IN_SHARD);
for (i, chunk) in list.chunks(NUM_OF_INDICES_IN_SHARD).enumerate() {
let is_last = i == num_chunks - 1;
let highest = if is_last { u64::MAX } else { *chunk.last().expect("chunk is non-empty") };
let key = H::TableKey::new_sharded(prefix, highest);
let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
writer.write_shard(key, &value, append_only)?;
}
list.clear();
Ok(())
}
/// Number of blocks before pushing indices from cache to [`Collector`]
const DEFAULT_CACHE_THRESHOLD: u64 = 100_000;
@@ -393,40 +230,17 @@ where
Ok(collector)
}
/// Adapter for writing account history shards via `EitherWriter`.
struct AccountHistoryShardWriter<'a, 'tx, CURSOR, N> {
writer: &'a mut EitherWriter<'tx, CURSOR, N>,
}
impl<CURSOR, N: NodePrimitives> HistoryShardWriter for AccountHistoryShardWriter<'_, '_, CURSOR, N>
where
CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
+ DbCursorRO<reth_db_api::tables::AccountsHistory>,
{
type TableKey = ShardedKey<Address>;
fn get_last_shard(
&mut self,
prefix: <Self::TableKey as ShardedHistoryKey>::Prefix,
) -> ProviderResult<Option<BlockNumberList>> {
self.writer.get_last_account_history_shard(prefix)
}
fn write_shard(
&mut self,
key: Self::TableKey,
value: &BlockNumberList,
append: bool,
) -> ProviderResult<()> {
if append {
self.writer.append_account_history(key, value)
} else {
self.writer.upsert_account_history(key, value)
}
}
}
/// Loads account history indices from the collector into the database.
/// Loads account history indices into the database via `EitherWriter`.
///
/// Works with [`EitherWriter`] to support both MDBX and `RocksDB` backends.
///
/// ## Process
/// Iterates over elements, grouping indices by their address. It flushes indices to disk
/// when reaching a shard's max length (`NUM_OF_INDICES_IN_SHARD`) or when the address changes,
/// ensuring the last previous address shard is stored.
///
/// Uses `Option<Address>` instead of `Address::default()` as the sentinel to avoid
/// incorrectly treating `Address::ZERO` as "no previous address".
pub(crate) fn load_account_history<N, CURSOR>(
mut collector: Collector<ShardedKey<Address>, BlockNumberList>,
append_only: bool,
@@ -437,8 +251,155 @@ where
CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
+ DbCursorRO<reth_db_api::tables::AccountsHistory>,
{
let mut adapter = AccountHistoryShardWriter { writer };
load_sharded_history(&mut collector, append_only, &mut adapter)
let mut current_address: Option<Address> = None;
// Accumulator for block numbers where the current address changed.
let mut current_list = Vec::<u64>::new();
let total_entries = collector.len();
let interval = (total_entries / 10).max(1);
for (index, element) in collector.iter()?.enumerate() {
let (k, v) = element?;
let sharded_key = ShardedKey::<Address>::decode_owned(k)?;
let new_list = BlockNumberList::decompress_owned(v)?;
if index > 0 && index.is_multiple_of(interval) && total_entries > 10 {
info!(target: "sync::stages::index_history", progress = %format!("{:.2}%", (index as f64 / total_entries as f64) * 100.0), "Writing indices");
}
let address = sharded_key.key;
// When address changes, flush the previous address's shards and start fresh.
if current_address != Some(address) {
// Flush all remaining shards for the previous address (uses u64::MAX for last shard).
if let Some(prev_addr) = current_address {
flush_account_history_shards(prev_addr, &mut current_list, append_only, writer)?;
}
current_address = Some(address);
current_list.clear();
// On incremental sync, merge with the existing last shard from the database.
// The last shard is stored with key (address, u64::MAX) so we can find it.
if !append_only &&
let Some(last_shard) = writer.get_last_account_history_shard(address)?
{
current_list.extend(last_shard.iter());
}
}
// Append new block numbers to the accumulator.
current_list.extend(new_list.iter());
// Flush complete shards, keeping the last (partial) shard buffered.
flush_account_history_shards_partial(address, &mut current_list, append_only, writer)?;
}
// Flush the final address's remaining shard.
if let Some(addr) = current_address {
flush_account_history_shards(addr, &mut current_list, append_only, writer)?;
}
Ok(())
}
/// Flushes complete shards for account history, keeping the trailing partial shard buffered.
///
/// Only flushes when we have more than one shard's worth of data, keeping the last
/// (possibly partial) shard for continued accumulation. This avoids writing a shard
/// that may need to be updated when more indices arrive.
fn flush_account_history_shards_partial<N, CURSOR>(
address: Address,
list: &mut Vec<u64>,
append_only: bool,
writer: &mut EitherWriter<'_, CURSOR, N>,
) -> Result<(), StageError>
where
N: NodePrimitives,
CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
+ DbCursorRO<reth_db_api::tables::AccountsHistory>,
{
// Nothing to flush if we haven't filled a complete shard yet.
if list.len() <= NUM_OF_INDICES_IN_SHARD {
return Ok(());
}
let num_full_shards = list.len() / NUM_OF_INDICES_IN_SHARD;
// Always keep at least one shard buffered for continued accumulation.
// If len is exact multiple of shard size, keep the last full shard.
let shards_to_flush = if list.len().is_multiple_of(NUM_OF_INDICES_IN_SHARD) {
num_full_shards - 1
} else {
num_full_shards
};
if shards_to_flush == 0 {
return Ok(());
}
// Split: flush the first N shards, keep the remainder buffered.
let flush_len = shards_to_flush * NUM_OF_INDICES_IN_SHARD;
let remainder = list.split_off(flush_len);
// Write each complete shard with its highest block number as the key.
for chunk in list.chunks(NUM_OF_INDICES_IN_SHARD) {
let highest = *chunk.last().expect("chunk is non-empty");
let key = ShardedKey::new(address, highest);
let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
if append_only {
writer.append_account_history(key, &value)?;
} else {
writer.upsert_account_history(key, &value)?;
}
}
// Keep the remaining indices for the next iteration.
*list = remainder;
Ok(())
}
/// Flushes all remaining shards for account history, using `u64::MAX` for the last shard.
///
/// The `u64::MAX` key for the final shard is an invariant that allows `seek_exact(address,
/// u64::MAX)` to find the last shard during incremental sync for merging with new indices.
fn flush_account_history_shards<N, CURSOR>(
address: Address,
list: &mut Vec<u64>,
append_only: bool,
writer: &mut EitherWriter<'_, CURSOR, N>,
) -> Result<(), StageError>
where
N: NodePrimitives,
CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
+ DbCursorRO<reth_db_api::tables::AccountsHistory>,
{
if list.is_empty() {
return Ok(());
}
let num_chunks = list.len().div_ceil(NUM_OF_INDICES_IN_SHARD);
for (i, chunk) in list.chunks(NUM_OF_INDICES_IN_SHARD).enumerate() {
let is_last = i == num_chunks - 1;
// Use u64::MAX for the final shard's key. This invariant allows incremental sync
// to find the last shard via seek_exact(address, u64::MAX) for merging.
let highest = if is_last { u64::MAX } else { *chunk.last().expect("chunk is non-empty") };
let key = ShardedKey::new(address, highest);
let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
if append_only {
writer.append_account_history(key, &value)?;
} else {
writer.upsert_account_history(key, &value)?;
}
}
list.clear();
Ok(())
}
/// Called when database is ahead of static files. Attempts to find the first block we are missing
@@ -477,40 +438,17 @@ where
})
}
/// Adapter for writing storage history shards via `EitherWriter`.
struct StorageHistoryShardWriter<'a, 'tx, CURSOR, N> {
writer: &'a mut EitherWriter<'tx, CURSOR, N>,
}
impl<CURSOR, N: NodePrimitives> HistoryShardWriter for StorageHistoryShardWriter<'_, '_, CURSOR, N>
where
CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
+ DbCursorRO<reth_db_api::tables::StoragesHistory>,
{
type TableKey = StorageShardedKey;
fn get_last_shard(
&mut self,
prefix: <Self::TableKey as ShardedHistoryKey>::Prefix,
) -> ProviderResult<Option<BlockNumberList>> {
self.writer.get_last_storage_history_shard(prefix.0, prefix.1)
}
fn write_shard(
&mut self,
key: Self::TableKey,
value: &BlockNumberList,
append: bool,
) -> ProviderResult<()> {
if append {
self.writer.append_storage_history(key, value)
} else {
self.writer.upsert_storage_history(key, value)
}
}
}
/// Loads storage history indices from the collector into the database.
/// Loads storage history indices into the database via `EitherWriter`.
///
/// Works with [`EitherWriter`] to support both MDBX and `RocksDB` backends.
///
/// ## Process
/// Iterates over elements, grouping indices by their (address, `storage_key`) pairs. It flushes
/// indices to disk when reaching a shard's max length (`NUM_OF_INDICES_IN_SHARD`) or when the
/// (address, `storage_key`) pair changes, ensuring the last previous shard is stored.
///
/// Uses `Option<(Address, B256)>` instead of default values as the sentinel to avoid
/// incorrectly treating `(Address::ZERO, B256::ZERO)` as "no previous key".
pub(crate) fn load_storage_history<N, CURSOR>(
mut collector: Collector<StorageShardedKey, BlockNumberList>,
append_only: bool,
@@ -521,6 +459,169 @@ where
CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
+ DbCursorRO<reth_db_api::tables::StoragesHistory>,
{
let mut adapter = StorageHistoryShardWriter { writer };
load_sharded_history(&mut collector, append_only, &mut adapter)
let mut current_key: Option<(Address, B256)> = None;
// Accumulator for block numbers where the current (address, storage_key) changed.
let mut current_list = Vec::<u64>::new();
let total_entries = collector.len();
let interval = (total_entries / 10).max(1);
for (index, element) in collector.iter()?.enumerate() {
let (k, v) = element?;
let sharded_key = StorageShardedKey::decode_owned(k)?;
let new_list = BlockNumberList::decompress_owned(v)?;
if index > 0 && index.is_multiple_of(interval) && total_entries > 10 {
info!(target: "sync::stages::index_history", progress = %format!("{:.2}%", (index as f64 / total_entries as f64) * 100.0), "Writing indices");
}
let partial_key = (sharded_key.address, sharded_key.sharded_key.key);
// When (address, storage_key) changes, flush the previous key's shards and start fresh.
if current_key != Some(partial_key) {
// Flush all remaining shards for the previous key (uses u64::MAX for last shard).
if let Some((prev_addr, prev_storage_key)) = current_key {
flush_storage_history_shards(
prev_addr,
prev_storage_key,
&mut current_list,
append_only,
writer,
)?;
}
current_key = Some(partial_key);
current_list.clear();
// On incremental sync, merge with the existing last shard from the database.
// The last shard is stored with key (address, storage_key, u64::MAX) so we can find it.
if !append_only &&
let Some(last_shard) =
writer.get_last_storage_history_shard(partial_key.0, partial_key.1)?
{
current_list.extend(last_shard.iter());
}
}
// Append new block numbers to the accumulator.
current_list.extend(new_list.iter());
// Flush complete shards, keeping the last (partial) shard buffered.
flush_storage_history_shards_partial(
partial_key.0,
partial_key.1,
&mut current_list,
append_only,
writer,
)?;
}
// Flush the final key's remaining shard.
if let Some((addr, storage_key)) = current_key {
flush_storage_history_shards(addr, storage_key, &mut current_list, append_only, writer)?;
}
Ok(())
}
/// Flushes complete shards for storage history, keeping the trailing partial shard buffered.
///
/// Only flushes when we have more than one shard's worth of data, keeping the last
/// (possibly partial) shard for continued accumulation. This avoids writing a shard
/// that may need to be updated when more indices arrive.
fn flush_storage_history_shards_partial<N, CURSOR>(
address: Address,
storage_key: B256,
list: &mut Vec<u64>,
append_only: bool,
writer: &mut EitherWriter<'_, CURSOR, N>,
) -> Result<(), StageError>
where
N: NodePrimitives,
CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
+ DbCursorRO<reth_db_api::tables::StoragesHistory>,
{
// Nothing to flush if we haven't filled a complete shard yet.
if list.len() <= NUM_OF_INDICES_IN_SHARD {
return Ok(());
}
let num_full_shards = list.len() / NUM_OF_INDICES_IN_SHARD;
// Always keep at least one shard buffered for continued accumulation.
// If len is exact multiple of shard size, keep the last full shard.
let shards_to_flush = if list.len().is_multiple_of(NUM_OF_INDICES_IN_SHARD) {
num_full_shards - 1
} else {
num_full_shards
};
if shards_to_flush == 0 {
return Ok(());
}
// Split: flush the first N shards, keep the remainder buffered.
let flush_len = shards_to_flush * NUM_OF_INDICES_IN_SHARD;
let remainder = list.split_off(flush_len);
// Write each complete shard with its highest block number as the key.
for chunk in list.chunks(NUM_OF_INDICES_IN_SHARD) {
let highest = *chunk.last().expect("chunk is non-empty");
let key = StorageShardedKey::new(address, storage_key, highest);
let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
if append_only {
writer.append_storage_history(key, &value)?;
} else {
writer.upsert_storage_history(key, &value)?;
}
}
// Keep the remaining indices for the next iteration.
*list = remainder;
Ok(())
}
/// Flushes all remaining shards for storage history, using `u64::MAX` for the last shard.
///
/// The `u64::MAX` key for the final shard is an invariant that allows
/// `seek_exact(address, storage_key, u64::MAX)` to find the last shard during incremental
/// sync for merging with new indices.
fn flush_storage_history_shards<N, CURSOR>(
address: Address,
storage_key: B256,
list: &mut Vec<u64>,
append_only: bool,
writer: &mut EitherWriter<'_, CURSOR, N>,
) -> Result<(), StageError>
where
N: NodePrimitives,
CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
+ DbCursorRO<reth_db_api::tables::StoragesHistory>,
{
if list.is_empty() {
return Ok(());
}
let num_chunks = list.len().div_ceil(NUM_OF_INDICES_IN_SHARD);
for (i, chunk) in list.chunks(NUM_OF_INDICES_IN_SHARD).enumerate() {
let is_last = i == num_chunks - 1;
// Use u64::MAX for the final shard's key. This invariant allows incremental sync
// to find the last shard via seek_exact(address, storage_key, u64::MAX) for merging.
let highest = if is_last { u64::MAX } else { *chunk.last().expect("chunk is non-empty") };
let key = StorageShardedKey::new(address, storage_key, highest);
let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
if append_only {
writer.append_storage_history(key, &value)?;
} else {
writer.upsert_storage_history(key, &value)?;
}
}
list.clear();
Ok(())
}

View File

@@ -12,6 +12,10 @@ pub enum StageId {
note = "Static Files are generated outside of the pipeline and do not require a separate stage"
)]
StaticFile,
#[deprecated(
note = "MerkleChangeSets stage has been removed; kept for DB checkpoint compatibility"
)]
MerkleChangeSets,
Era,
Headers,
Bodies,
@@ -75,6 +79,8 @@ impl StageId {
match self {
#[expect(deprecated)]
Self::StaticFile => "StaticFile",
#[expect(deprecated)]
Self::MerkleChangeSets => "MerkleChangeSets",
Self::Era => "Era",
Self::Headers => "Headers",
Self::Bodies => "Bodies",

View File

@@ -5,7 +5,7 @@ use crate::{
table::{Decode, Encode},
DatabaseError,
};
use alloy_primitives::{Address, BlockNumber, StorageKey, B256};
use alloy_primitives::{Address, BlockNumber, StorageKey};
use serde::{Deserialize, Serialize};
use std::ops::{Bound, Range, RangeBounds, RangeInclusive};
@@ -108,43 +108,6 @@ impl<R: RangeBounds<BlockNumber>> From<R> for BlockNumberAddressRange {
}
}
/// [`BlockNumber`] concatenated with [`B256`] (hashed address).
///
/// Since it's used as a key, it isn't compressed when encoding it.
#[derive(
Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd, Hash,
)]
pub struct BlockNumberHashedAddress(pub (BlockNumber, B256));
impl From<(BlockNumber, B256)> for BlockNumberHashedAddress {
fn from(tpl: (BlockNumber, B256)) -> Self {
Self(tpl)
}
}
impl Encode for BlockNumberHashedAddress {
type Encoded = [u8; 40];
fn encode(self) -> Self::Encoded {
let block_number = self.0 .0;
let hashed_address = self.0 .1;
let mut buf = [0u8; 40];
buf[..8].copy_from_slice(&block_number.to_be_bytes());
buf[8..].copy_from_slice(hashed_address.as_slice());
buf
}
}
impl Decode for BlockNumberHashedAddress {
fn decode(value: &[u8]) -> Result<Self, DatabaseError> {
let num = u64::from_be_bytes(value[..8].try_into().map_err(|_| DatabaseError::Decode)?);
let hash = B256::from_slice(&value[8..]);
Ok(Self((num, hash)))
}
}
/// [`Address`] concatenated with [`StorageKey`]. Used by `reth_etl` and history stages.
///
/// Since it's used as a key, it isn't compressed when encoding it.
@@ -176,11 +139,7 @@ impl Decode for AddressStorageKey {
}
}
impl_fixed_arbitrary!(
(BlockNumberAddress, 28),
(BlockNumberHashedAddress, 40),
(AddressStorageKey, 52)
);
impl_fixed_arbitrary!((BlockNumberAddress, 28), (AddressStorageKey, 52));
#[cfg(test)]
mod tests {
@@ -213,31 +172,6 @@ mod tests {
assert_eq!(bytes, Encode::encode(key));
}
#[test]
fn test_block_number_hashed_address() {
let num = 1u64;
let hash = B256::from_slice(&[0xba; 32]);
let key = BlockNumberHashedAddress((num, hash));
let mut bytes = [0u8; 40];
bytes[..8].copy_from_slice(&num.to_be_bytes());
bytes[8..].copy_from_slice(hash.as_slice());
let encoded = Encode::encode(key);
assert_eq!(encoded, bytes);
let decoded: BlockNumberHashedAddress = Decode::decode(&encoded).unwrap();
assert_eq!(decoded, key);
}
#[test]
fn test_block_number_hashed_address_rand() {
let mut bytes = [0u8; 40];
rng().fill(bytes.as_mut_slice());
let key = BlockNumberHashedAddress::arbitrary(&mut Unstructured::new(&bytes)).unwrap();
assert_eq!(bytes, Encode::encode(key));
}
#[test]
fn test_address_storage_key() {
let storage_key = StorageKey::random();

View File

@@ -12,9 +12,7 @@ use reth_ethereum_primitives::{Receipt, TransactionSigned, TxType};
use reth_primitives_traits::{Account, Bytecode, StorageEntry};
use reth_prune_types::{PruneCheckpoint, PruneSegment};
use reth_stages_types::StageCheckpoint;
use reth_trie_common::{
StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, TrieChangeSetsEntry, *,
};
use reth_trie_common::{StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, *};
use serde::{Deserialize, Serialize};
pub mod accounts;
@@ -32,7 +30,7 @@ pub use reth_db_models::{
AccountBeforeTx, ClientVersion, StaticFileBlockWithdrawals, StorageBeforeTx,
StoredBlockBodyIndices, StoredBlockWithdrawals,
};
pub use sharded_key::{ShardedHistoryKey, ShardedKey};
pub use sharded_key::ShardedKey;
/// Macro that implements [`Encode`] and [`Decode`] for uint types.
macro_rules! impl_uints {
@@ -220,7 +218,6 @@ impl_compression_for_compact!(
TxType,
StorageEntry,
BranchNodeCompact,
TrieChangeSetsEntry,
StoredNibbles,
StoredNibblesSubKey,
StorageTrieEntry,

View File

@@ -10,21 +10,6 @@ use std::hash::Hash;
/// Number of indices in one shard.
pub const NUM_OF_INDICES_IN_SHARD: usize = 2_000;
/// Trait for sharded history keys that can be constructed from a prefix + block number.
///
/// This abstracts over the key construction and prefix extraction for sharded history tables
/// like `AccountsHistory` and `StoragesHistory`.
pub trait ShardedHistoryKey: Sized {
/// The prefix type (e.g., `Address` for accounts, `(Address, B256)` for storage).
type Prefix: Copy + Eq;
/// Creates a new sharded key from prefix and highest block number.
fn new_sharded(prefix: Self::Prefix, highest_block_number: u64) -> Self;
/// Extracts the prefix from this key.
fn prefix(&self) -> Self::Prefix;
}
/// Size of `BlockNumber` in bytes (u64 = 8 bytes).
const BLOCK_NUMBER_SIZE: usize = std::mem::size_of::<BlockNumber>();
@@ -92,20 +77,6 @@ impl Decode for ShardedKey<Address> {
}
}
impl ShardedHistoryKey for ShardedKey<Address> {
type Prefix = Address;
#[inline]
fn new_sharded(prefix: Self::Prefix, highest_block_number: u64) -> Self {
Self::new(prefix, highest_block_number)
}
#[inline]
fn prefix(&self) -> Self::Prefix {
self.key
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -139,14 +110,4 @@ mod tests {
let decoded = ShardedKey::<Address>::decode(&encoded).unwrap();
assert_eq!(decoded.highest_block_number, u64::MAX);
}
#[test]
fn sharded_history_key_trait_roundtrip() {
let addr = address!("0102030405060708091011121314151617181920");
let block_num = 0x123456789ABCDEFu64;
let key = ShardedKey::<Address>::new_sharded(addr, block_num);
assert_eq!(key.prefix(), addr);
assert_eq!(key.highest_block_number, block_num);
}
}

View File

@@ -7,7 +7,7 @@ use alloy_primitives::{Address, BlockNumber, B256};
use derive_more::AsRef;
use serde::{Deserialize, Serialize};
use super::{ShardedHistoryKey, ShardedKey};
use super::ShardedKey;
/// Number of indices in one shard.
pub const NUM_OF_INDICES_IN_SHARD: usize = 2_000;
@@ -91,20 +91,6 @@ impl Decode for StorageShardedKey {
}
}
impl ShardedHistoryKey for StorageShardedKey {
type Prefix = (Address, B256);
#[inline]
fn new_sharded(prefix: Self::Prefix, highest_block_number: u64) -> Self {
Self::new(prefix.0, prefix.1, highest_block_number)
}
#[inline]
fn prefix(&self) -> Self::Prefix {
(self.address, self.sharded_key.key)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -144,15 +130,4 @@ mod tests {
let decoded = StorageShardedKey::decode(&encoded).unwrap();
assert_eq!(decoded.sharded_key.highest_block_number, u64::MAX);
}
#[test]
fn sharded_history_key_trait_roundtrip() {
let addr = address!("0102030405060708091011121314151617181920");
let storage_key = b256!("0001020304050607080910111213141516171819202122232425262728293031");
let block_num = 0x123456789ABCDEFu64;
let key = StorageShardedKey::new_sharded((addr, storage_key), block_num);
assert_eq!(key.prefix(), (addr, storage_key));
assert_eq!(key.sharded_key.highest_block_number, block_num);
}
}

View File

@@ -21,8 +21,8 @@ use crate::{
accounts::BlockNumberAddress,
blocks::{HeaderHash, StoredBlockOmmers},
storage_sharded_key::StorageShardedKey,
AccountBeforeTx, BlockNumberHashedAddress, ClientVersion, CompactU256, IntegerList,
ShardedKey, StoredBlockBodyIndices, StoredBlockWithdrawals,
AccountBeforeTx, ClientVersion, CompactU256, IntegerList, ShardedKey,
StoredBlockBodyIndices, StoredBlockWithdrawals,
},
table::{Decode, DupSort, Encode, Table, TableInfo},
};
@@ -32,9 +32,7 @@ use reth_ethereum_primitives::{Receipt, TransactionSigned};
use reth_primitives_traits::{Account, Bytecode, StorageEntry};
use reth_prune_types::{PruneCheckpoint, PruneSegment};
use reth_stages_types::StageCheckpoint;
use reth_trie_common::{
BranchNodeCompact, StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, TrieChangeSetsEntry,
};
use reth_trie_common::{BranchNodeCompact, StorageTrieEntry, StoredNibbles, StoredNibblesSubKey};
use serde::{Deserialize, Serialize};
use std::fmt;
@@ -492,20 +490,6 @@ tables! {
type SubKey = StoredNibblesSubKey;
}
/// Stores the state of a node in the accounts trie prior to a particular block being executed.
table AccountsTrieChangeSets {
type Key = BlockNumber;
type Value = TrieChangeSetsEntry;
type SubKey = StoredNibblesSubKey;
}
/// Stores the state of a node in a storage trie prior to a particular block being executed.
table StoragesTrieChangeSets {
type Key = BlockNumberHashedAddress;
type Value = TrieChangeSetsEntry;
type SubKey = StoredNibblesSubKey;
}
/// Stores the transaction sender for each canonical transaction.
/// It is needed to speed up execution stage and allows fetching signer without doing
/// transaction signed recovery

View File

@@ -15,9 +15,10 @@ use reth_primitives_traits::{
use reth_provider::{
errors::provider::ProviderResult, providers::StaticFileWriter, BlockHashReader, BlockNumReader,
BundleStateInit, ChainSpecProvider, DBProvider, DatabaseProviderFactory, ExecutionOutcome,
HashingWriter, HeaderProvider, HistoryWriter, MetadataWriter, OriginalValuesKnown,
ProviderError, RevertsInit, StageCheckpointReader, StageCheckpointWriter, StateWriteConfig,
StateWriter, StaticFileProviderFactory, StorageSettings, StorageSettingsCache, TrieWriter,
HashingWriter, HeaderProvider, HistoryWriter, MetadataProvider, MetadataWriter,
OriginalValuesKnown, ProviderError, RevertsInit, StageCheckpointReader, StageCheckpointWriter,
StateWriteConfig, StateWriter, StaticFileProviderFactory, StorageSettings,
StorageSettingsCache, TrieWriter,
};
use reth_stages_types::{StageCheckpoint, StageId};
use reth_static_file_types::StaticFileSegment;
@@ -28,7 +29,7 @@ use reth_trie::{
use reth_trie_db::DatabaseStateRoot;
use serde::{Deserialize, Serialize};
use std::io::BufRead;
use tracing::{debug, error, info, trace};
use tracing::{debug, error, info, trace, warn};
/// Default soft limit for number of bytes to read from state dump file, before inserting into
/// database.
@@ -90,7 +91,8 @@ where
+ StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>
+ ChainSpecProvider
+ StageCheckpointReader
+ BlockHashReader
+ BlockNumReader
+ MetadataProvider
+ StorageSettingsCache,
PF::ProviderRW: StaticFileProviderFactory<Primitives = PF::Primitives>
+ StageCheckpointWriter
@@ -124,7 +126,8 @@ where
+ StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>
+ ChainSpecProvider
+ StageCheckpointReader
+ BlockHashReader
+ BlockNumReader
+ MetadataProvider
+ StorageSettingsCache,
PF::ProviderRW: StaticFileProviderFactory<Primitives = PF::Primitives>
+ StageCheckpointWriter
@@ -159,6 +162,16 @@ where
return Err(InitStorageError::UninitializedDatabase)
}
let stored = factory.storage_settings()?.unwrap_or_else(StorageSettings::legacy);
if stored != genesis_storage_settings {
warn!(
target: "reth::storage",
?stored,
requested = ?genesis_storage_settings,
"Storage settings mismatch detected"
);
}
debug!("Genesis already written, skipping.");
return Ok(hash)
}
@@ -897,4 +910,30 @@ mod tests {
)],
);
}
#[test]
fn warn_storage_settings_mismatch() {
let factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
init_genesis_with_settings(&factory, StorageSettings::legacy()).unwrap();
// Request different settings - should warn but succeed
let result = init_genesis_with_settings(
&factory,
StorageSettings::legacy().with_receipts_in_static_files(true),
);
// Should succeed (warning is logged, not an error)
assert!(result.is_ok());
}
#[test]
fn allow_same_storage_settings() {
let factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
let settings = StorageSettings::legacy().with_receipts_in_static_files(true);
init_genesis_with_settings(&factory, settings).unwrap();
let result = init_genesis_with_settings(&factory, settings);
assert!(result.is_ok());
}
}

View File

@@ -560,6 +560,35 @@ impl DatabaseEnv {
Ok(handles)
}
/// Drops an orphaned table by name.
///
/// This is used to clean up tables that are no longer defined in the schema but may still
/// exist on disk from previous versions.
///
/// Returns `Ok(true)` if the table existed and was dropped, `Ok(false)` if the table was not
/// found.
///
/// # Safety
/// This permanently deletes the table and all its data. Only use for tables that are
/// confirmed to be obsolete.
pub fn drop_orphan_table(&self, name: &str) -> Result<bool, DatabaseError> {
let tx = self.inner.begin_rw_txn().map_err(|e| DatabaseError::InitTx(e.into()))?;
match tx.open_db(Some(name)) {
Ok(db) => {
// SAFETY: We just opened the db handle and will commit immediately after dropping.
// No other cursors or handles exist for this table.
unsafe {
tx.drop_db(db.dbi()).map_err(|e| DatabaseError::Delete(e.into()))?;
}
tx.commit().map_err(|e| DatabaseError::Commit(e.into()))?;
Ok(true)
}
Err(reth_libmdbx::Error::NotFound) => Ok(false),
Err(e) => Err(DatabaseError::Open(e.into())),
}
}
/// Records version that accesses the database with write privileges.
pub fn record_client_version(&self, version: ClientVersion) -> Result<(), DatabaseError> {
if version.is_empty() {
@@ -646,6 +675,46 @@ mod tests {
create_test_db(DatabaseEnvKind::RW);
}
#[test]
fn db_drop_orphan_table() {
let path = tempfile::TempDir::new().expect(ERROR_TEMPDIR).keep();
let db = create_test_db_with_path(DatabaseEnvKind::RW, &path);
// Create an orphan table by manually creating it
let orphan_table_name = "OrphanTestTable";
{
let tx = db.inner.begin_rw_txn().expect(ERROR_INIT_TX);
tx.create_db(Some(orphan_table_name), DatabaseFlags::empty())
.expect("Failed to create orphan table");
tx.commit().expect(ERROR_COMMIT);
}
// Verify the table exists by opening it
{
let tx = db.inner.begin_ro_txn().expect(ERROR_INIT_TX);
assert!(tx.open_db(Some(orphan_table_name)).is_ok(), "Orphan table should exist");
}
// Drop the orphan table
let result = db.drop_orphan_table(orphan_table_name);
assert!(result.is_ok(), "drop_orphan_table should succeed");
assert!(result.unwrap(), "drop_orphan_table should return true for existing table");
// Verify the table no longer exists
{
let tx = db.inner.begin_ro_txn().expect(ERROR_INIT_TX);
assert!(
tx.open_db(Some(orphan_table_name)).is_err(),
"Orphan table should no longer exist"
);
}
// Dropping a non-existent table should return Ok(false)
let result = db.drop_orphan_table("NonExistentTable");
assert!(result.is_ok(), "drop_orphan_table should succeed for non-existent table");
assert!(!result.unwrap(), "drop_orphan_table should return false for non-existent table");
}
#[test]
fn db_manual_put_get() {
let env = create_test_db(DatabaseEnvKind::RW);

View File

@@ -2,11 +2,16 @@
use crate::{is_database_empty, TableSet, Tables};
use eyre::Context;
use reth_tracing::tracing::info;
use std::path::Path;
pub use crate::implementation::mdbx::*;
pub use reth_libmdbx::*;
/// Tables that have been removed from the schema but may still exist on disk from previous
/// versions. These will be dropped during database initialization.
const ORPHAN_TABLES: &[&str] = &["AccountsTrieChangeSets", "StoragesTrieChangeSets"];
/// Creates a new database at the specified path if it doesn't exist. Does NOT create tables. Check
/// [`init_db`].
pub fn create_db<P: AsRef<Path>>(path: P, args: DatabaseArguments) -> eyre::Result<DatabaseEnv> {
@@ -44,9 +49,30 @@ pub fn init_db_for<P: AsRef<Path>, TS: TableSet>(
let mut db = create_db(path, args)?;
db.create_and_track_tables_for::<TS>()?;
db.record_client_version(client_version)?;
drop_orphan_tables(&db);
Ok(db)
}
/// Drops orphaned tables that are no longer part of the schema.
fn drop_orphan_tables(db: &DatabaseEnv) {
for table_name in ORPHAN_TABLES {
match db.drop_orphan_table(table_name) {
Ok(true) => {
info!(target: "reth::db", table = %table_name, "Dropped orphaned database table");
}
Ok(false) => {}
Err(e) => {
reth_tracing::tracing::warn!(
target: "reth::db",
table = %table_name,
%e,
"Failed to drop orphaned database table"
);
}
}
}
}
/// Opens up an existing database. Read only mode. It doesn't create it or create tables if missing.
pub fn open_db_read_only(
path: impl AsRef<Path>,

View File

@@ -83,14 +83,17 @@ pub type RawRocksDBBatch = ();
/// Helper type for `RocksDB` transaction reference argument in reader constructors.
///
/// When `rocksdb` feature is enabled, this is a reference to a `RocksDB` transaction.
/// Otherwise, it's `()` (unit type) to allow the same API without feature gates.
/// When `rocksdb` feature is enabled, this is an optional reference to a `RocksDB` transaction.
/// The `Option` allows callers to skip transaction creation when `RocksDB` isn't needed
/// (e.g., on legacy MDBX-only nodes).
/// When `rocksdb` feature is disabled, it's `()` (unit type) to allow the same API without
/// feature gates.
#[cfg(all(unix, feature = "rocksdb"))]
pub type RocksTxRefArg<'a> = &'a crate::providers::rocksdb::RocksTx<'a>;
pub type RocksTxRefArg<'a> = Option<&'a crate::providers::rocksdb::RocksTx<'a>>;
/// Helper type for `RocksDB` transaction reference argument in reader constructors.
///
/// When `rocksdb` feature is enabled, this is a reference to a `RocksDB` transaction.
/// Otherwise, it's `()` (unit type) to allow the same API without feature gates.
/// When `rocksdb` feature is disabled, it's `()` (unit type) to allow the same API without
/// feature gates.
#[cfg(not(all(unix, feature = "rocksdb")))]
pub type RocksTxRefArg<'a> = ();
@@ -762,7 +765,9 @@ impl<'a> EitherReader<'a, (), ()> {
{
#[cfg(all(unix, feature = "rocksdb"))]
if provider.cached_storage_settings().storages_history_in_rocksdb {
return Ok(EitherReader::RocksDB(_rocksdb_tx));
return Ok(EitherReader::RocksDB(
_rocksdb_tx.expect("storages_history_in_rocksdb requires rocksdb tx"),
));
}
Ok(EitherReader::Database(
@@ -782,7 +787,9 @@ impl<'a> EitherReader<'a, (), ()> {
{
#[cfg(all(unix, feature = "rocksdb"))]
if provider.cached_storage_settings().transaction_hash_numbers_in_rocksdb {
return Ok(EitherReader::RocksDB(_rocksdb_tx));
return Ok(EitherReader::RocksDB(
_rocksdb_tx.expect("transaction_hash_numbers_in_rocksdb requires rocksdb tx"),
));
}
Ok(EitherReader::Database(
@@ -802,7 +809,9 @@ impl<'a> EitherReader<'a, (), ()> {
{
#[cfg(all(unix, feature = "rocksdb"))]
if provider.cached_storage_settings().account_history_in_rocksdb {
return Ok(EitherReader::RocksDB(_rocksdb_tx));
return Ok(EitherReader::RocksDB(
_rocksdb_tx.expect("account_history_in_rocksdb requires rocksdb tx"),
));
}
Ok(EitherReader::Database(
@@ -1814,4 +1823,20 @@ mod rocksdb_tests {
"Data should be visible after provider.commit()"
);
}
/// Test that `EitherReader::new_accounts_history` panics when settings require
/// `RocksDB` but no tx is provided (`None`). This is an invariant violation that
/// indicates a bug - `with_rocksdb_tx` should always provide a tx when needed.
#[test]
#[should_panic(expected = "account_history_in_rocksdb requires rocksdb tx")]
fn test_settings_mismatch_panics() {
let factory = create_test_provider_factory();
factory.set_storage_settings_cache(
StorageSettings::legacy().with_account_history_in_rocksdb(true),
);
let provider = factory.database_provider_ro().unwrap();
let _ = EitherReader::<(), ()>::new_accounts_history(&provider, None);
}
}

View File

@@ -40,8 +40,7 @@ use reth_db_api::{
database::Database,
models::{
sharded_key, storage_sharded_key::StorageShardedKey, AccountBeforeTx, BlockNumberAddress,
BlockNumberHashedAddress, ShardedKey, StorageBeforeTx, StorageSettings,
StoredBlockBodyIndices,
ShardedKey, StorageBeforeTx, StorageSettings, StoredBlockBodyIndices,
},
table::Table,
tables,
@@ -65,12 +64,10 @@ use reth_storage_api::{
};
use reth_storage_errors::provider::{ProviderResult, StaticFileWriterError};
use reth_trie::{
changesets::storage_trie_wiped_changeset_iter,
trie_cursor::{InMemoryTrieCursor, TrieCursor, TrieCursorIter, TrieStorageCursor},
updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted},
HashedPostStateSorted, StoredNibbles, StoredNibblesSubKey, TrieChangeSetsEntry,
HashedPostStateSorted, StoredNibbles,
};
use reth_trie_db::{ChangesetCache, DatabaseAccountTrieCursor, DatabaseStorageTrieCursor};
use reth_trie_db::{ChangesetCache, DatabaseStorageTrieCursor};
use revm_database::states::{
PlainStateReverts, PlainStorageChangeset, PlainStorageRevert, StateChangeset,
};
@@ -78,7 +75,7 @@ use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt::Debug,
ops::{Deref, DerefMut, Range, RangeBounds, RangeFrom, RangeInclusive},
ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive},
sync::Arc,
thread,
time::Instant,
@@ -792,9 +789,6 @@ impl<TX: DbTx + DbTxMut + 'static, N: NodeTypesForProvider> DatabaseProvider<TX,
let trie_revert = self.changeset_cache.get_or_compute_range(self, from..=db_tip_block)?;
self.write_trie_updates_sorted(&trie_revert)?;
// Clear trie changesets which have been unwound.
self.clear_trie_changesets_from(from)?;
Ok(())
}
@@ -2783,90 +2777,6 @@ impl<TX: DbTxMut + DbTx + 'static, N: NodeTypes> TrieWriter for DatabaseProvider
Ok(num_entries)
}
/// Records the current values of all trie nodes which will be updated using the `TrieUpdates`
/// into the trie changesets tables.
///
/// The intended usage of this method is to call it _prior_ to calling `write_trie_updates` with
/// the same `TrieUpdates`.
///
/// Returns the number of keys written.
#[instrument(level = "debug", target = "providers::db", skip_all)]
fn write_trie_changesets(
&self,
block_number: BlockNumber,
trie_updates: &TrieUpdatesSorted,
updates_overlay: Option<&TrieUpdatesSorted>,
) -> ProviderResult<usize> {
let mut num_entries = 0;
let mut changeset_cursor =
self.tx_ref().cursor_dup_write::<tables::AccountsTrieChangeSets>()?;
let curr_values_cursor = self.tx_ref().cursor_read::<tables::AccountsTrie>()?;
// Wrap the cursor in DatabaseAccountTrieCursor
let mut db_account_cursor = DatabaseAccountTrieCursor::new(curr_values_cursor);
// Create empty TrieUpdatesSorted for when updates_overlay is None
let empty_updates = TrieUpdatesSorted::default();
let overlay = updates_overlay.unwrap_or(&empty_updates);
// Wrap the cursor in InMemoryTrieCursor with the overlay
let mut in_memory_account_cursor =
InMemoryTrieCursor::new_account(&mut db_account_cursor, overlay);
for (path, _) in trie_updates.account_nodes_ref() {
num_entries += 1;
let node = in_memory_account_cursor.seek_exact(*path)?.map(|(_, node)| node);
changeset_cursor.append_dup(
block_number,
TrieChangeSetsEntry { nibbles: StoredNibblesSubKey(*path), node },
)?;
}
let mut storage_updates = trie_updates.storage_tries_ref().iter().collect::<Vec<_>>();
storage_updates.sort_unstable_by(|a, b| a.0.cmp(b.0));
num_entries += self.write_storage_trie_changesets(
block_number,
storage_updates.into_iter(),
updates_overlay,
)?;
Ok(num_entries)
}
fn clear_trie_changesets(&self) -> ProviderResult<()> {
let tx = self.tx_ref();
tx.clear::<tables::AccountsTrieChangeSets>()?;
tx.clear::<tables::StoragesTrieChangeSets>()?;
Ok(())
}
fn clear_trie_changesets_from(&self, from: BlockNumber) -> ProviderResult<()> {
let tx = self.tx_ref();
{
let range = from..;
let mut cursor = tx.cursor_dup_write::<tables::AccountsTrieChangeSets>()?;
let mut walker = cursor.walk_range(range)?;
while walker.next().transpose()?.is_some() {
walker.delete_current()?;
}
}
{
let range: RangeFrom<BlockNumberHashedAddress> = (from, B256::ZERO).into()..;
let mut cursor = tx.cursor_dup_write::<tables::StoragesTrieChangeSets>()?;
let mut walker = cursor.walk_range(range)?;
while walker.next().transpose()?.is_some() {
walker.delete_current()?;
}
}
Ok(())
}
}
impl<TX: DbTxMut + DbTx + 'static, N: NodeTypes> StorageTrieWriter for DatabaseProvider<TX, N> {
@@ -2893,75 +2803,6 @@ impl<TX: DbTxMut + DbTx + 'static, N: NodeTypes> StorageTrieWriter for DatabaseP
Ok(num_entries)
}
/// Records the current values of all trie nodes which will be updated using the
/// `StorageTrieUpdates` into the storage trie changesets table.
///
/// The intended usage of this method is to call it _prior_ to calling
/// `write_storage_trie_updates` with the same set of `StorageTrieUpdates`.
///
/// Returns the number of keys written.
fn write_storage_trie_changesets<'a>(
&self,
block_number: BlockNumber,
storage_tries: impl Iterator<Item = (&'a B256, &'a StorageTrieUpdatesSorted)>,
updates_overlay: Option<&TrieUpdatesSorted>,
) -> ProviderResult<usize> {
let mut num_written = 0;
let mut changeset_cursor =
self.tx_ref().cursor_dup_write::<tables::StoragesTrieChangeSets>()?;
let curr_values_cursor = self.tx_ref().cursor_dup_read::<tables::StoragesTrie>()?;
// Wrap the cursor in DatabaseStorageTrieCursor
let mut db_storage_cursor = DatabaseStorageTrieCursor::new(
curr_values_cursor,
B256::default(), // Will be set per iteration
);
// Create empty TrieUpdatesSorted for when updates_overlay is None
let empty_updates = TrieUpdatesSorted::default();
for (hashed_address, storage_trie_updates) in storage_tries {
let changeset_key = BlockNumberHashedAddress((block_number, *hashed_address));
// Update the hashed address for the cursor
db_storage_cursor.set_hashed_address(*hashed_address);
// Get the overlay updates, or use empty updates
let overlay = updates_overlay.unwrap_or(&empty_updates);
// Wrap the cursor in InMemoryTrieCursor with the overlay
let mut in_memory_storage_cursor =
InMemoryTrieCursor::new_storage(&mut db_storage_cursor, overlay, *hashed_address);
let changed_paths = storage_trie_updates.storage_nodes.iter().map(|e| e.0);
if storage_trie_updates.is_deleted() {
let all_nodes = TrieCursorIter::new(&mut in_memory_storage_cursor);
for wiped in storage_trie_wiped_changeset_iter(changed_paths, all_nodes)? {
let (path, node) = wiped?;
num_written += 1;
changeset_cursor.append_dup(
changeset_key,
TrieChangeSetsEntry { nibbles: StoredNibblesSubKey(path), node },
)?;
}
} else {
for path in changed_paths {
let node = in_memory_storage_cursor.seek_exact(path)?.map(|(_, node)| node);
num_written += 1;
changeset_cursor.append_dup(
changeset_key,
TrieChangeSetsEntry { nibbles: StoredNibblesSubKey(path), node },
)?;
}
}
}
Ok(num_written)
}
}
impl<TX: DbTxMut + DbTx + 'static, N: NodeTypes> HashingWriter for DatabaseProvider<TX, N> {
@@ -3732,7 +3573,7 @@ mod tests {
use alloy_primitives::map::B256Map;
use reth_ethereum_primitives::Receipt;
use reth_testing_utils::generators::{self, random_block, BlockParams};
use reth_trie::Nibbles;
use reth_trie::{Nibbles, StoredNibblesSubKey};
#[test]
fn test_receipts_by_block_range_empty_range() {
@@ -3976,781 +3817,6 @@ mod tests {
assert_eq!(range_result, individual_results);
}
#[test]
fn test_write_trie_changesets() {
use reth_db_api::models::BlockNumberHashedAddress;
use reth_trie::{BranchNodeCompact, StorageTrieEntry};
let factory = create_test_provider_factory();
let provider_rw = factory.provider_rw().unwrap();
let block_number = 1u64;
// Create some test nibbles and nodes
let account_nibbles1 = Nibbles::from_nibbles([0x1, 0x2, 0x3, 0x4]);
let account_nibbles2 = Nibbles::from_nibbles([0x5, 0x6, 0x7, 0x8]);
let node1 = BranchNodeCompact::new(
0b1111_1111_1111_1111, // state_mask
0b0000_0000_0000_0000, // tree_mask
0b0000_0000_0000_0000, // hash_mask
vec![], // hashes
None, // root hash
);
// Pre-populate AccountsTrie with a node that will be updated (for account_nibbles1)
{
let mut cursor = provider_rw.tx_ref().cursor_write::<tables::AccountsTrie>().unwrap();
cursor.insert(StoredNibbles(account_nibbles1), &node1).unwrap();
}
// Create account trie updates: one Some (update) and one None (removal)
let account_nodes = vec![
(account_nibbles1, Some(node1.clone())), // This will update existing node
(account_nibbles2, None), // This will be a removal (no existing node)
];
// Create storage trie updates
let storage_address1 = B256::from([1u8; 32]); // Normal storage trie
let storage_address2 = B256::from([2u8; 32]); // Wiped storage trie
let storage_nibbles1 = Nibbles::from_nibbles([0xa, 0xb]);
let storage_nibbles2 = Nibbles::from_nibbles([0xc, 0xd]);
let storage_nibbles3 = Nibbles::from_nibbles([0xe, 0xf]);
let storage_node1 = BranchNodeCompact::new(
0b1111_0000_0000_0000,
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
let storage_node2 = BranchNodeCompact::new(
0b0000_1111_0000_0000,
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
// Create an old version of storage_node1 to prepopulate
let storage_node1_old = BranchNodeCompact::new(
0b1010_0000_0000_0000, // Different mask to show it's an old value
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
// Pre-populate StoragesTrie for normal storage (storage_address1)
{
let mut cursor =
provider_rw.tx_ref().cursor_dup_write::<tables::StoragesTrie>().unwrap();
// Add node that will be updated (storage_nibbles1) with old value
let entry = StorageTrieEntry {
nibbles: StoredNibblesSubKey(storage_nibbles1),
node: storage_node1_old.clone(),
};
cursor.upsert(storage_address1, &entry).unwrap();
}
// Pre-populate StoragesTrie for wiped storage (storage_address2)
{
let mut cursor =
provider_rw.tx_ref().cursor_dup_write::<tables::StoragesTrie>().unwrap();
// Add node that will be updated (storage_nibbles1)
let entry1 = StorageTrieEntry {
nibbles: StoredNibblesSubKey(storage_nibbles1),
node: storage_node1.clone(),
};
cursor.upsert(storage_address2, &entry1).unwrap();
// Add node that won't be updated but exists (storage_nibbles3)
let entry3 = StorageTrieEntry {
nibbles: StoredNibblesSubKey(storage_nibbles3),
node: storage_node2.clone(),
};
cursor.upsert(storage_address2, &entry3).unwrap();
}
// Normal storage trie: one Some (update) and one None (new)
let storage_trie1 = StorageTrieUpdatesSorted {
is_deleted: false,
storage_nodes: vec![
(storage_nibbles1, Some(storage_node1.clone())), // This will update existing node
(storage_nibbles2, None), // This is a new node
],
};
// Wiped storage trie
let storage_trie2 = StorageTrieUpdatesSorted {
is_deleted: true,
storage_nodes: vec![
(storage_nibbles1, Some(storage_node1.clone())), // Updated node already in db
(storage_nibbles2, Some(storage_node2.clone())), /* Updated node not in db
* storage_nibbles3 is in db
* but not updated */
],
};
let mut storage_tries = B256Map::default();
storage_tries.insert(storage_address1, storage_trie1);
storage_tries.insert(storage_address2, storage_trie2);
let trie_updates = TrieUpdatesSorted::new(account_nodes, storage_tries);
// Write the changesets
let num_written =
provider_rw.write_trie_changesets(block_number, &trie_updates, None).unwrap();
// Verify number of entries written
// Account changesets: 2 (one update, one removal)
// Storage changesets:
// - Normal storage: 2 (one update, one removal)
// - Wiped storage: 3 (two updated, one existing not updated)
// Total: 2 + 2 + 3 = 7
assert_eq!(num_written, 7);
// Verify account changesets were written correctly
{
let mut cursor =
provider_rw.tx_ref().cursor_dup_read::<tables::AccountsTrieChangeSets>().unwrap();
// Get all entries for this block to see what was written
let all_entries = cursor
.walk_dup(Some(block_number), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
// Assert the full value of all_entries in a single assert_eq
assert_eq!(
all_entries,
vec![
(
block_number,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(account_nibbles1),
node: Some(node1),
}
),
(
block_number,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(account_nibbles2),
node: None,
}
),
]
);
}
// Verify storage changesets were written correctly
{
let mut cursor =
provider_rw.tx_ref().cursor_dup_read::<tables::StoragesTrieChangeSets>().unwrap();
// Check normal storage trie changesets
let key1 = BlockNumberHashedAddress((block_number, storage_address1));
let entries1 =
cursor.walk_dup(Some(key1), None).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(
entries1,
vec![
(
key1,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles1),
node: Some(storage_node1_old), // Old value that was prepopulated
}
),
(
key1,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles2),
node: None, // New node, no previous value
}
),
]
);
// Check wiped storage trie changesets
let key2 = BlockNumberHashedAddress((block_number, storage_address2));
let entries2 =
cursor.walk_dup(Some(key2), None).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(
entries2,
vec![
(
key2,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles1),
node: Some(storage_node1), // Was in db, so has old value
}
),
(
key2,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles2),
node: None, // Was not in db
}
),
(
key2,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles3),
node: Some(storage_node2), // Existing node in wiped storage
}
),
]
);
}
provider_rw.commit().unwrap();
}
#[test]
fn test_write_trie_changesets_with_overlay() {
use reth_db_api::models::BlockNumberHashedAddress;
use reth_trie::BranchNodeCompact;
let factory = create_test_provider_factory();
let provider_rw = factory.provider_rw().unwrap();
let block_number = 1u64;
// Create some test nibbles and nodes
let account_nibbles1 = Nibbles::from_nibbles([0x1, 0x2, 0x3, 0x4]);
let account_nibbles2 = Nibbles::from_nibbles([0x5, 0x6, 0x7, 0x8]);
let node1 = BranchNodeCompact::new(
0b1111_1111_1111_1111, // state_mask
0b0000_0000_0000_0000, // tree_mask
0b0000_0000_0000_0000, // hash_mask
vec![], // hashes
None, // root hash
);
// NOTE: Unlike the previous test, we're NOT pre-populating the database
// All node values will come from the overlay
// Create the overlay with existing values that would normally be in the DB
let node1_old = BranchNodeCompact::new(
0b1010_1010_1010_1010, // Different mask to show it's the overlay "existing" value
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
// Create overlay account nodes
let overlay_account_nodes = vec![
(account_nibbles1, Some(node1_old.clone())), // This simulates existing node in overlay
];
// Create account trie updates: one Some (update) and one None (removal)
let account_nodes = vec![
(account_nibbles1, Some(node1)), // This will update overlay node
(account_nibbles2, None), // This will be a removal (no existing node)
];
// Create storage trie updates
let storage_address1 = B256::from([1u8; 32]); // Normal storage trie
let storage_address2 = B256::from([2u8; 32]); // Wiped storage trie
let storage_nibbles1 = Nibbles::from_nibbles([0xa, 0xb]);
let storage_nibbles2 = Nibbles::from_nibbles([0xc, 0xd]);
let storage_nibbles3 = Nibbles::from_nibbles([0xe, 0xf]);
let storage_node1 = BranchNodeCompact::new(
0b1111_0000_0000_0000,
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
let storage_node2 = BranchNodeCompact::new(
0b0000_1111_0000_0000,
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
// Create old versions for overlay
let storage_node1_old = BranchNodeCompact::new(
0b1010_0000_0000_0000, // Different mask to show it's an old value
0b0000_0000_0000_0000,
0b0000_0000_0000_0000,
vec![],
None,
);
// Create overlay storage nodes
let mut overlay_storage_tries = B256Map::default();
// Overlay for normal storage (storage_address1)
let overlay_storage_trie1 = StorageTrieUpdatesSorted {
is_deleted: false,
storage_nodes: vec![
(storage_nibbles1, Some(storage_node1_old.clone())), /* Simulates existing in
* overlay */
],
};
// Overlay for wiped storage (storage_address2)
let overlay_storage_trie2 = StorageTrieUpdatesSorted {
is_deleted: false,
storage_nodes: vec![
(storage_nibbles1, Some(storage_node1.clone())), // Existing in overlay
(storage_nibbles3, Some(storage_node2.clone())), // Also existing in overlay
],
};
overlay_storage_tries.insert(storage_address1, overlay_storage_trie1);
overlay_storage_tries.insert(storage_address2, overlay_storage_trie2);
let overlay = TrieUpdatesSorted::new(overlay_account_nodes, overlay_storage_tries);
// Normal storage trie: one Some (update) and one None (new)
let storage_trie1 = StorageTrieUpdatesSorted {
is_deleted: false,
storage_nodes: vec![
(storage_nibbles1, Some(storage_node1.clone())), // This will update overlay node
(storage_nibbles2, None), // This is a new node
],
};
// Wiped storage trie
let storage_trie2 = StorageTrieUpdatesSorted {
is_deleted: true,
storage_nodes: vec![
(storage_nibbles1, Some(storage_node1.clone())), // Updated node from overlay
(storage_nibbles2, Some(storage_node2.clone())), /* Updated node not in overlay
* storage_nibbles3 is in
* overlay
* but not updated */
],
};
let mut storage_tries = B256Map::default();
storage_tries.insert(storage_address1, storage_trie1);
storage_tries.insert(storage_address2, storage_trie2);
let trie_updates = TrieUpdatesSorted::new(account_nodes, storage_tries);
// Write the changesets WITH OVERLAY
let num_written =
provider_rw.write_trie_changesets(block_number, &trie_updates, Some(&overlay)).unwrap();
// Verify number of entries written
// Account changesets: 2 (one update from overlay, one removal)
// Storage changesets:
// - Normal storage: 2 (one update from overlay, one new)
// - Wiped storage: 3 (two updated, one existing from overlay not updated)
// Total: 2 + 2 + 3 = 7
assert_eq!(num_written, 7);
// Verify account changesets were written correctly
{
let mut cursor =
provider_rw.tx_ref().cursor_dup_read::<tables::AccountsTrieChangeSets>().unwrap();
// Get all entries for this block to see what was written
let all_entries = cursor
.walk_dup(Some(block_number), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
// Assert the full value of all_entries in a single assert_eq
assert_eq!(
all_entries,
vec![
(
block_number,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(account_nibbles1),
node: Some(node1_old), // Value from overlay, not DB
}
),
(
block_number,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(account_nibbles2),
node: None,
}
),
]
);
}
// Verify storage changesets were written correctly
{
let mut cursor =
provider_rw.tx_ref().cursor_dup_read::<tables::StoragesTrieChangeSets>().unwrap();
// Check normal storage trie changesets
let key1 = BlockNumberHashedAddress((block_number, storage_address1));
let entries1 =
cursor.walk_dup(Some(key1), None).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(
entries1,
vec![
(
key1,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles1),
node: Some(storage_node1_old), // Old value from overlay
}
),
(
key1,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles2),
node: None, // New node, no previous value
}
),
]
);
// Check wiped storage trie changesets
let key2 = BlockNumberHashedAddress((block_number, storage_address2));
let entries2 =
cursor.walk_dup(Some(key2), None).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(
entries2,
vec![
(
key2,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles1),
node: Some(storage_node1), // Value from overlay
}
),
(
key2,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles2),
node: None, // Was not in overlay
}
),
(
key2,
TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey(storage_nibbles3),
node: Some(storage_node2), /* Existing node from overlay in wiped
* storage */
}
),
]
);
}
provider_rw.commit().unwrap();
}
#[test]
fn test_clear_trie_changesets_from() {
use alloy_primitives::hex_literal::hex;
use reth_db_api::models::BlockNumberHashedAddress;
use reth_trie::{BranchNodeCompact, StoredNibblesSubKey, TrieChangeSetsEntry};
let factory = create_test_provider_factory();
// Create some test data for different block numbers
let block1 = 100u64;
let block2 = 101u64;
let block3 = 102u64;
let block4 = 103u64;
let block5 = 104u64;
// Create test addresses for storage changesets
let storage_address1 =
B256::from(hex!("1111111111111111111111111111111111111111111111111111111111111111"));
let storage_address2 =
B256::from(hex!("2222222222222222222222222222222222222222222222222222222222222222"));
// Create test nibbles
let nibbles1 = StoredNibblesSubKey(Nibbles::from_nibbles([0x1, 0x2, 0x3]));
let nibbles2 = StoredNibblesSubKey(Nibbles::from_nibbles([0x4, 0x5, 0x6]));
let nibbles3 = StoredNibblesSubKey(Nibbles::from_nibbles([0x7, 0x8, 0x9]));
// Create test nodes
let node1 = BranchNodeCompact::new(
0b1111_1111_1111_1111,
0b1111_1111_1111_1111,
0b0000_0000_0000_0001,
vec![B256::from(hex!(
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
))],
None,
);
let node2 = BranchNodeCompact::new(
0b1111_1111_1111_1110,
0b1111_1111_1111_1110,
0b0000_0000_0000_0010,
vec![B256::from(hex!(
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
))],
Some(B256::from(hex!(
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
))),
);
// Populate AccountsTrieChangeSets with data across multiple blocks
{
let provider_rw = factory.provider_rw().unwrap();
let mut cursor =
provider_rw.tx_ref().cursor_dup_write::<tables::AccountsTrieChangeSets>().unwrap();
// Block 100: 2 entries (will be kept - before start block)
cursor
.upsert(
block1,
&TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node1.clone()) },
)
.unwrap();
cursor
.upsert(block1, &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: None })
.unwrap();
// Block 101: 3 entries with duplicates (will be deleted - from this block onwards)
cursor
.upsert(
block2,
&TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node2.clone()) },
)
.unwrap();
cursor
.upsert(
block2,
&TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node1.clone()) },
)
.unwrap(); // duplicate key
cursor
.upsert(block2, &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: None })
.unwrap();
// Block 102: 2 entries (will be deleted - after start block)
cursor
.upsert(
block3,
&TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node1.clone()) },
)
.unwrap();
cursor
.upsert(
block3,
&TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: Some(node2.clone()) },
)
.unwrap();
// Block 103: 1 entry (will be deleted - after start block)
cursor
.upsert(block4, &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: None })
.unwrap();
// Block 104: 2 entries (will be deleted - after start block)
cursor
.upsert(
block5,
&TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node2.clone()) },
)
.unwrap();
cursor
.upsert(block5, &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: None })
.unwrap();
provider_rw.commit().unwrap();
}
// Populate StoragesTrieChangeSets with data across multiple blocks
{
let provider_rw = factory.provider_rw().unwrap();
let mut cursor =
provider_rw.tx_ref().cursor_dup_write::<tables::StoragesTrieChangeSets>().unwrap();
// Block 100, address1: 2 entries (will be kept - before start block)
let key1_block1 = BlockNumberHashedAddress((block1, storage_address1));
cursor
.upsert(
key1_block1,
&TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node1.clone()) },
)
.unwrap();
cursor
.upsert(key1_block1, &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: None })
.unwrap();
// Block 101, address1: 3 entries with duplicates (will be deleted - from this block
// onwards)
let key1_block2 = BlockNumberHashedAddress((block2, storage_address1));
cursor
.upsert(
key1_block2,
&TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node2.clone()) },
)
.unwrap();
cursor
.upsert(key1_block2, &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: None })
.unwrap(); // duplicate key
cursor
.upsert(
key1_block2,
&TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node1.clone()) },
)
.unwrap();
// Block 102, address2: 2 entries (will be deleted - after start block)
let key2_block3 = BlockNumberHashedAddress((block3, storage_address2));
cursor
.upsert(
key2_block3,
&TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node2.clone()) },
)
.unwrap();
cursor
.upsert(key2_block3, &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: None })
.unwrap();
// Block 103, address1: 2 entries with duplicate (will be deleted - after start block)
let key1_block4 = BlockNumberHashedAddress((block4, storage_address1));
cursor
.upsert(
key1_block4,
&TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: Some(node1) },
)
.unwrap();
cursor
.upsert(
key1_block4,
&TrieChangeSetsEntry { nibbles: nibbles3, node: Some(node2.clone()) },
)
.unwrap(); // duplicate key
// Block 104, address2: 2 entries (will be deleted - after start block)
let key2_block5 = BlockNumberHashedAddress((block5, storage_address2));
cursor
.upsert(key2_block5, &TrieChangeSetsEntry { nibbles: nibbles1, node: None })
.unwrap();
cursor
.upsert(key2_block5, &TrieChangeSetsEntry { nibbles: nibbles2, node: Some(node2) })
.unwrap();
provider_rw.commit().unwrap();
}
// Clear all changesets from block 101 onwards
{
let provider_rw = factory.provider_rw().unwrap();
provider_rw.clear_trie_changesets_from(block2).unwrap();
provider_rw.commit().unwrap();
}
// Verify AccountsTrieChangeSets after clearing
{
let provider = factory.provider().unwrap();
let mut cursor =
provider.tx_ref().cursor_dup_read::<tables::AccountsTrieChangeSets>().unwrap();
// Block 100 should still exist (before range)
let block1_entries = cursor
.walk_dup(Some(block1), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(block1_entries.len(), 2, "Block 100 entries should be preserved");
assert_eq!(block1_entries[0].0, block1);
assert_eq!(block1_entries[1].0, block1);
// Blocks 101-104 should be deleted
let block2_entries = cursor
.walk_dup(Some(block2), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block2_entries.is_empty(), "Block 101 entries should be deleted");
let block3_entries = cursor
.walk_dup(Some(block3), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block3_entries.is_empty(), "Block 102 entries should be deleted");
let block4_entries = cursor
.walk_dup(Some(block4), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block4_entries.is_empty(), "Block 103 entries should be deleted");
// Block 104 should also be deleted
let block5_entries = cursor
.walk_dup(Some(block5), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block5_entries.is_empty(), "Block 104 entries should be deleted");
}
// Verify StoragesTrieChangeSets after clearing
{
let provider = factory.provider().unwrap();
let mut cursor =
provider.tx_ref().cursor_dup_read::<tables::StoragesTrieChangeSets>().unwrap();
// Block 100 entries should still exist (before range)
let key1_block1 = BlockNumberHashedAddress((block1, storage_address1));
let block1_entries = cursor
.walk_dup(Some(key1_block1), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(block1_entries.len(), 2, "Block 100 storage entries should be preserved");
// Blocks 101-104 entries should be deleted
let key1_block2 = BlockNumberHashedAddress((block2, storage_address1));
let block2_entries = cursor
.walk_dup(Some(key1_block2), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block2_entries.is_empty(), "Block 101 storage entries should be deleted");
let key2_block3 = BlockNumberHashedAddress((block3, storage_address2));
let block3_entries = cursor
.walk_dup(Some(key2_block3), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block3_entries.is_empty(), "Block 102 storage entries should be deleted");
let key1_block4 = BlockNumberHashedAddress((block4, storage_address1));
let block4_entries = cursor
.walk_dup(Some(key1_block4), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block4_entries.is_empty(), "Block 103 storage entries should be deleted");
// Block 104 entries should also be deleted
let key2_block5 = BlockNumberHashedAddress((block5, storage_address2));
let block5_entries = cursor
.walk_dup(Some(key2_block5), None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(block5_entries.is_empty(), "Block 104 storage entries should be deleted");
}
}
#[test]
fn test_write_trie_updates_sorted() {
use reth_trie::{

View File

@@ -98,6 +98,13 @@ const DEFAULT_BYTES_PER_SYNC: u64 = 1_048_576;
/// reducing the first few reallocations without over-allocating.
const DEFAULT_COMPRESS_BUF_CAPACITY: usize = 4096;
/// Default auto-commit threshold for batch writes (4 GiB).
///
/// When a batch exceeds this size, it is automatically committed to prevent OOM
/// during large bulk writes. The consistency check on startup heals any crash
/// that occurs between auto-commits.
const DEFAULT_AUTO_COMMIT_THRESHOLD: usize = 4 * 1024 * 1024 * 1024;
/// Builder for [`RocksDBProvider`].
pub struct RocksDBBuilder {
path: PathBuf,
@@ -629,6 +636,21 @@ impl RocksDBProvider {
provider: self,
inner: WriteBatchWithTransaction::<true>::default(),
buf: Vec::with_capacity(DEFAULT_COMPRESS_BUF_CAPACITY),
auto_commit_threshold: None,
}
}
/// Creates a new batch with auto-commit enabled.
///
/// When the batch size exceeds the threshold (4 GiB), the batch is automatically
/// committed and reset. This prevents OOM during large bulk writes while maintaining
/// crash-safety via the consistency check on startup.
pub fn batch_with_auto_commit(&self) -> RocksDBBatch<'_> {
RocksDBBatch {
provider: self,
inner: WriteBatchWithTransaction::<true>::default(),
buf: Vec::with_capacity(DEFAULT_COMPRESS_BUF_CAPACITY),
auto_commit_threshold: Some(DEFAULT_AUTO_COMMIT_THRESHOLD),
}
}
@@ -1137,11 +1159,16 @@ impl RocksDBProvider {
/// Unlike [`RocksTx`], this does NOT support read-your-writes. Use for write-only flows
/// where you don't need to read back uncommitted data within the same operation
/// (e.g., history index writes).
///
/// When `auto_commit_threshold` is set, the batch will automatically commit and reset
/// when the batch size exceeds the threshold. This prevents OOM during large bulk writes.
#[must_use = "batch must be committed"]
pub struct RocksDBBatch<'a> {
provider: &'a RocksDBProvider,
inner: WriteBatchWithTransaction<true>,
buf: Vec<u8>,
/// If set, batch auto-commits when size exceeds this threshold (in bytes).
auto_commit_threshold: Option<usize>,
}
impl fmt::Debug for RocksDBBatch<'_> {
@@ -1160,12 +1187,16 @@ impl fmt::Debug for RocksDBBatch<'_> {
impl<'a> RocksDBBatch<'a> {
/// Puts a value into the batch.
///
/// If auto-commit is enabled and the batch exceeds the threshold, commits and resets.
pub fn put<T: Table>(&mut self, key: T::Key, value: &T::Value) -> ProviderResult<()> {
let encoded_key = key.encode();
self.put_encoded::<T>(&encoded_key, value)
}
/// Puts a value into the batch using pre-encoded key.
///
/// If auto-commit is enabled and the batch exceeds the threshold, commits and resets.
pub fn put_encoded<T: Table>(
&mut self,
key: &<T::Key as Encode>::Encoded,
@@ -1173,12 +1204,43 @@ impl<'a> RocksDBBatch<'a> {
) -> ProviderResult<()> {
let value_bytes = compress_to_buf_or_ref!(self.buf, value).unwrap_or(&self.buf);
self.inner.put_cf(self.provider.get_cf_handle::<T>()?, key, value_bytes);
self.maybe_auto_commit()?;
Ok(())
}
/// Deletes a value from the batch.
///
/// If auto-commit is enabled and the batch exceeds the threshold, commits and resets.
pub fn delete<T: Table>(&mut self, key: T::Key) -> ProviderResult<()> {
self.inner.delete_cf(self.provider.get_cf_handle::<T>()?, key.encode().as_ref());
self.maybe_auto_commit()?;
Ok(())
}
/// Commits and resets the batch if it exceeds the auto-commit threshold.
///
/// This is called after each `put` or `delete` operation to prevent unbounded memory growth.
/// Returns immediately if auto-commit is disabled or threshold not reached.
fn maybe_auto_commit(&mut self) -> ProviderResult<()> {
if let Some(threshold) = self.auto_commit_threshold &&
self.inner.size_in_bytes() >= threshold
{
tracing::debug!(
target: "providers::rocksdb",
batch_size = self.inner.size_in_bytes(),
threshold,
"Auto-committing RocksDB batch"
);
let old_batch = std::mem::take(&mut self.inner);
self.provider.0.db_rw().write_opt(old_batch, &WriteOptions::default()).map_err(
|e| {
ProviderError::Database(DatabaseError::Commit(DatabaseErrorInfo {
message: e.to_string().into(),
code: -1,
}))
},
)?;
}
Ok(())
}
@@ -1208,6 +1270,11 @@ impl<'a> RocksDBBatch<'a> {
self.inner.is_empty()
}
/// Returns the size of the batch in bytes.
pub fn size_in_bytes(&self) -> usize {
self.inner.size_in_bytes()
}
/// Returns a reference to the underlying `RocksDB` provider.
pub const fn provider(&self) -> &RocksDBProvider {
self.provider
@@ -2767,4 +2834,40 @@ mod tests {
assert_eq!(shards[1].0.highest_block_number, u64::MAX);
assert_eq!(shards[1].1.iter().collect::<Vec<_>>(), (51..=75).collect::<Vec<_>>());
}
#[test]
fn test_batch_auto_commit_on_threshold() {
let temp_dir = TempDir::new().unwrap();
let provider =
RocksDBBuilder::new(temp_dir.path()).with_table::<TestTable>().build().unwrap();
// Create batch with tiny threshold (1KB) to force auto-commits
let mut batch = RocksDBBatch {
provider: &provider,
inner: WriteBatchWithTransaction::<true>::default(),
buf: Vec::new(),
auto_commit_threshold: Some(1024), // 1KB
};
// Write entries until we exceed threshold multiple times
// Each entry is ~20 bytes, so 100 entries = ~2KB = 2 auto-commits
for i in 0..100u64 {
let value = format!("value_{i:04}").into_bytes();
batch.put::<TestTable>(i, &value).unwrap();
}
// Data should already be visible (auto-committed) even before final commit
// At least some entries should be readable
let first_visible = provider.get::<TestTable>(0).unwrap();
assert!(first_visible.is_some(), "Auto-committed data should be visible");
// Final commit for remaining batch
batch.commit().unwrap();
// All entries should now be visible
for i in 0..100u64 {
let value = format!("value_{i:04}").into_bytes();
assert_eq!(provider.get::<TestTable>(i).unwrap(), Some(value));
}
}
}

View File

@@ -2,6 +2,7 @@ use crate::{
either_writer::{RawRocksDBBatch, RocksBatchArg, RocksTxRefArg},
providers::RocksDBProvider,
};
use reth_storage_api::StorageSettingsCache;
use reth_storage_errors::provider::ProviderResult;
/// `RocksDB` provider factory.
@@ -21,15 +22,21 @@ pub trait RocksDBProviderFactory {
/// Executes a closure with a `RocksDB` transaction for reading.
///
/// This helper encapsulates all the cfg-gated `RocksDB` transaction handling for reads.
/// On legacy MDBX-only nodes (where `any_in_rocksdb()` is false), this skips creating
/// the `RocksDB` transaction entirely, avoiding unnecessary overhead.
fn with_rocksdb_tx<F, R>(&self, f: F) -> ProviderResult<R>
where
Self: StorageSettingsCache,
F: FnOnce(RocksTxRefArg<'_>) -> ProviderResult<R>,
{
#[cfg(all(unix, feature = "rocksdb"))]
{
let rocksdb = self.rocksdb_provider();
let tx = rocksdb.tx();
f(&tx)
if self.cached_storage_settings().any_in_rocksdb() {
let rocksdb = self.rocksdb_provider();
let tx = rocksdb.tx();
return f(Some(&tx));
}
f(None)
}
#[cfg(not(all(unix, feature = "rocksdb")))]
f(())
@@ -58,4 +65,126 @@ pub trait RocksDBProviderFactory {
Ok(result)
}
}
/// Executes a closure with a `RocksDB` batch that auto-commits on threshold.
///
/// Unlike [`Self::with_rocksdb_batch`], this uses a batch that automatically commits
/// when it exceeds the size threshold, preventing OOM during large bulk writes.
/// The consistency check on startup heals any crash between auto-commits.
fn with_rocksdb_batch_auto_commit<F, R>(&self, f: F) -> ProviderResult<R>
where
F: FnOnce(RocksBatchArg<'_>) -> ProviderResult<(R, Option<RawRocksDBBatch>)>,
{
#[cfg(all(unix, feature = "rocksdb"))]
{
let rocksdb = self.rocksdb_provider();
let batch = rocksdb.batch_with_auto_commit();
let (result, raw_batch) = f(batch)?;
if let Some(b) = raw_batch {
self.set_pending_rocksdb_batch(b);
}
Ok(result)
}
#[cfg(not(all(unix, feature = "rocksdb")))]
{
let (result, _) = f(())?;
Ok(result)
}
}
}
#[cfg(all(test, unix, feature = "rocksdb"))]
mod tests {
use super::*;
use reth_db_api::models::StorageSettings;
use std::sync::atomic::{AtomicUsize, Ordering};
/// Mock `RocksDB` provider that tracks `tx()` calls.
struct MockRocksDBProvider {
tx_call_count: AtomicUsize,
}
impl MockRocksDBProvider {
const fn new() -> Self {
Self { tx_call_count: AtomicUsize::new(0) }
}
fn tx_call_count(&self) -> usize {
self.tx_call_count.load(Ordering::SeqCst)
}
fn increment_tx_count(&self) {
self.tx_call_count.fetch_add(1, Ordering::SeqCst);
}
}
/// Test provider that implements [`RocksDBProviderFactory`] + [`StorageSettingsCache`].
struct TestProvider {
settings: StorageSettings,
mock_rocksdb: MockRocksDBProvider,
temp_dir: tempfile::TempDir,
}
impl TestProvider {
fn new(settings: StorageSettings) -> Self {
Self {
settings,
mock_rocksdb: MockRocksDBProvider::new(),
temp_dir: tempfile::TempDir::new().unwrap(),
}
}
fn tx_call_count(&self) -> usize {
self.mock_rocksdb.tx_call_count()
}
}
impl StorageSettingsCache for TestProvider {
fn cached_storage_settings(&self) -> StorageSettings {
self.settings
}
fn set_storage_settings_cache(&self, _settings: StorageSettings) {}
}
impl RocksDBProviderFactory for TestProvider {
fn rocksdb_provider(&self) -> RocksDBProvider {
self.mock_rocksdb.increment_tx_count();
RocksDBProvider::new(self.temp_dir.path()).unwrap()
}
fn set_pending_rocksdb_batch(&self, _batch: rocksdb::WriteBatchWithTransaction<true>) {}
}
#[test]
fn test_legacy_settings_skip_rocksdb_tx_creation() {
let provider = TestProvider::new(StorageSettings::legacy());
let result = provider.with_rocksdb_tx(|tx| {
assert!(tx.is_none(), "legacy settings should pass None tx");
Ok(42)
});
assert_eq!(result.unwrap(), 42);
assert_eq!(provider.tx_call_count(), 0, "should not create RocksDB tx for legacy settings");
}
#[test]
fn test_rocksdb_settings_create_tx() {
let settings =
StorageSettings { account_history_in_rocksdb: true, ..StorageSettings::legacy() };
let provider = TestProvider::new(settings);
let result = provider.with_rocksdb_tx(|tx| {
assert!(tx.is_some(), "rocksdb settings should pass Some tx");
Ok(42)
});
assert_eq!(result.unwrap(), 42);
assert_eq!(
provider.tx_call_count(),
1,
"should create RocksDB tx when any_in_rocksdb is true"
);
}
}

View File

@@ -1,5 +1,5 @@
use alloc::vec::Vec;
use alloy_primitives::{Address, BlockNumber, Bytes, B256};
use alloy_primitives::{Address, Bytes, B256};
use reth_storage_errors::provider::ProviderResult;
use reth_trie_common::{
updates::{StorageTrieUpdatesSorted, TrieUpdates, TrieUpdatesSorted},
@@ -103,32 +103,6 @@ pub trait TrieWriter: Send {
///
/// Returns the number of entries modified.
fn write_trie_updates_sorted(&self, trie_updates: &TrieUpdatesSorted) -> ProviderResult<usize>;
/// Records the current values of all trie nodes which will be updated using the [`TrieUpdates`]
/// into the trie changesets tables.
///
/// The intended usage of this method is to call it _prior_ to calling `write_trie_updates` with
/// the same [`TrieUpdates`].
///
/// The `updates_overlay` parameter allows providing additional in-memory trie updates that
/// should be considered when looking up current node values. When provided, these overlay
/// updates are applied on top of the database state, allowing the method to see a view that
/// includes both committed database values and pending in-memory changes. This is useful
/// when writing changesets for updates that depend on previous uncommitted trie changes.
///
/// Returns the number of keys written.
fn write_trie_changesets(
&self,
block_number: BlockNumber,
trie_updates: &TrieUpdatesSorted,
updates_overlay: Option<&TrieUpdatesSorted>,
) -> ProviderResult<usize>;
/// Clears contents of trie changesets completely
fn clear_trie_changesets(&self) -> ProviderResult<()>;
/// Clears contents of trie changesets starting from the given block number (inclusive) onwards.
fn clear_trie_changesets_from(&self, from: BlockNumber) -> ProviderResult<()>;
}
/// Storage Trie Writer
@@ -143,25 +117,4 @@ pub trait StorageTrieWriter: Send {
&self,
storage_tries: impl Iterator<Item = (&'a B256, &'a StorageTrieUpdatesSorted)>,
) -> ProviderResult<usize>;
/// Records the current values of all trie nodes which will be updated using the
/// [`StorageTrieUpdatesSorted`] into the storage trie changesets table.
///
/// The intended usage of this method is to call it _prior_ to calling
/// `write_storage_trie_updates` with the same set of [`StorageTrieUpdatesSorted`].
///
/// The `updates_overlay` parameter allows providing additional in-memory trie updates that
/// should be considered when looking up current node values. When provided, these overlay
/// updates are applied on top of the database state for each storage trie, allowing the
/// method to see a view that includes both committed database values and pending in-memory
/// changes. This is useful when writing changesets for storage updates that depend on
/// previous uncommitted trie changes.
///
/// Returns the number of keys written.
fn write_storage_trie_changesets<'a>(
&self,
block_number: BlockNumber,
storage_tries: impl Iterator<Item = (&'a B256, &'a StorageTrieUpdatesSorted)>,
updates_overlay: Option<&TrieUpdatesSorted>,
) -> ProviderResult<usize>;
}

View File

@@ -56,7 +56,7 @@ struct InMemoryBlobStoreInner {
impl PartialEq for InMemoryBlobStoreInner {
fn eq(&self, other: &Self) -> bool {
self.store.read().eq(&other.store.read())
self.store.read().eq(&*other.store.read())
}
}

View File

@@ -303,7 +303,7 @@ use aquamarine as _;
use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
use reth_eth_wire_types::HandleMempoolData;
use reth_execution_types::ChangedAccount;
use reth_primitives_traits::{Block, Recovered};
use reth_primitives_traits::Recovered;
use reth_storage_api::StateProviderFactory;
use std::{collections::HashSet, sync::Arc};
use tokio::sync::mpsc::Receiver;
@@ -328,8 +328,13 @@ mod traits;
pub mod test_utils;
/// Type alias for default ethereum transaction pool
pub type EthTransactionPool<Client, S, T = EthPooledTransaction> = Pool<
TransactionValidationTaskExecutor<EthTransactionValidator<Client, T>>,
pub type EthTransactionPool<
Client,
S,
T = EthPooledTransaction,
B = reth_ethereum_primitives::Block,
> = Pool<
TransactionValidationTaskExecutor<EthTransactionValidator<Client, T, B>>,
CoinbaseTipOrdering<T>,
S,
>;
@@ -776,16 +781,15 @@ where
T: TransactionOrdering<Transaction = <V as TransactionValidator>::Transaction>,
S: BlobStore,
{
type Block = V::Block;
#[instrument(skip(self), target = "txpool")]
fn set_block_info(&self, info: BlockInfo) {
trace!(target: "txpool", "updating pool block info");
self.pool.set_block_info(info)
}
fn on_canonical_state_change<B>(&self, update: CanonicalStateUpdate<'_, B>)
where
B: Block,
{
fn on_canonical_state_change(&self, update: CanonicalStateUpdate<'_, Self::Block>) {
self.pool.on_canonical_state_change(update);
}

View File

@@ -107,7 +107,8 @@ where
+ ChainSpecProvider<ChainSpec: EthChainSpec<Header = N::BlockHeader> + EthereumHardforks>
+ Clone
+ 'static,
P: TransactionPoolExt<Transaction: PoolTransaction<Consensus = N::SignedTx>> + 'static,
P: TransactionPoolExt<Transaction: PoolTransaction<Consensus = N::SignedTx>, Block = N::Block>
+ 'static,
St: Stream<Item = CanonStateNotification<N>> + Send + Unpin + 'static,
Tasks: TaskSpawner + Clone + 'static,
{
@@ -133,7 +134,8 @@ pub async fn maintain_transaction_pool<N, Client, P, St, Tasks>(
+ ChainSpecProvider<ChainSpec: EthChainSpec<Header = N::BlockHeader> + EthereumHardforks>
+ Clone
+ 'static,
P: TransactionPoolExt<Transaction: PoolTransaction<Consensus = N::SignedTx>> + 'static,
P: TransactionPoolExt<Transaction: PoolTransaction<Consensus = N::SignedTx>, Block = N::Block>
+ 'static,
St: Stream<Item = CanonStateNotification<N>> + Send + Unpin + 'static,
Tasks: TaskSpawner + Clone + 'static,
{
@@ -855,7 +857,8 @@ mod tests {
use super::*;
use crate::{
blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
CoinbaseTipOrdering, EthPooledTransaction, Pool, TransactionOrigin,
CoinbaseTipOrdering, EthPooledTransaction, EthTransactionValidator, Pool,
TransactionOrigin,
};
use alloy_eips::eip2718::Decodable2718;
use alloy_primitives::{hex, U256};
@@ -889,7 +892,8 @@ mod tests {
let sender = hex!("1f9090aaE28b8a3dCeaDf281B0F12828e676c326").into();
provider.add_account(sender, ExtendedAccount::new(42, U256::MAX));
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider).build(blob_store.clone());
let validator: EthTransactionValidator<_, _, reth_ethereum_primitives::Block> =
EthTransactionValidatorBuilder::new(provider).build(blob_store.clone());
let txpool = Pool::new(
validator,

View File

@@ -373,6 +373,7 @@ pub struct MockTransactionValidator<T> {
impl<T: EthPoolTransaction> TransactionValidator for MockTransactionValidator<T> {
type Transaction = T;
type Block = reth_ethereum_primitives::Block;
async fn validate_transaction(
&self,

View File

@@ -114,7 +114,6 @@ pub use events::{FullTransactionEvent, NewTransactionEvent, TransactionEvent};
pub use listener::{AllTransactionsEvents, TransactionEvents, TransactionListenerKind};
pub use parked::{BasefeeOrd, ParkedOrd, ParkedPool, QueuedOrd};
pub use pending::PendingPool;
use reth_primitives_traits::Block;
mod best;
pub use best::BestTransactions;
@@ -504,10 +503,7 @@ where
}
/// Updates the entire pool after a new block was executed.
pub fn on_canonical_state_change<B>(&self, update: CanonicalStateUpdate<'_, B>)
where
B: Block,
{
pub fn on_canonical_state_change(&self, update: CanonicalStateUpdate<'_, V::Block>) {
trace!(target: "txpool", ?update, "updating pool on canonical state change");
let block_info = update.block_info();

View File

@@ -4,6 +4,7 @@ use crate::{
validate::ValidTransaction, EthPooledTransaction, PoolTransaction, TransactionOrigin,
TransactionValidationOutcome, TransactionValidator,
};
use reth_ethereum_primitives::Block;
/// A transaction validator that determines all transactions to be valid.
#[derive(Debug)]
@@ -33,6 +34,7 @@ where
T: PoolTransaction,
{
type Transaction = T;
type Block = Block;
async fn validate_transaction(
&self,

View File

@@ -667,6 +667,9 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
/// Extension for [`TransactionPool`] trait that allows to set the current block info.
#[auto_impl::auto_impl(&, Arc)]
pub trait TransactionPoolExt: TransactionPool {
/// The block type used for chain tip updates.
type Block: Block;
/// Sets the current block info for the pool.
fn set_block_info(&self, info: BlockInfo);
@@ -685,9 +688,7 @@ pub trait TransactionPoolExt: TransactionPool {
/// sidecar must not be removed from the blob store. Only after a blob transaction is
/// finalized, its sidecar is removed from the blob store. This ensures that in case of a reorg,
/// the sidecar is still available.
fn on_canonical_state_change<B>(&self, update: CanonicalStateUpdate<'_, B>)
where
B: Block;
fn on_canonical_state_change(&self, update: CanonicalStateUpdate<'_, Self::Block>);
/// Updates the accounts in the pool
fn update_accounts(&self, accounts: Vec<ChangedAccount>);

View File

@@ -28,7 +28,7 @@ use alloy_eips::{
use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
use reth_primitives_traits::{
constants::MAX_TX_GAS_LIMIT_OSAKA, transaction::error::InvalidTransactionError, Account, Block,
GotExpected, SealedBlock,
GotExpected,
};
use reth_storage_api::{AccountInfoReader, BytecodeReader, StateProviderFactory};
use reth_tasks::TaskSpawner;
@@ -58,7 +58,7 @@ use tokio::sync::Mutex;
///
/// And adheres to the configured [`LocalTransactionConfig`].
#[derive(Debug)]
pub struct EthTransactionValidator<Client, T> {
pub struct EthTransactionValidator<Client, T, B = reth_ethereum_primitives::Block> {
/// This type fetches account info from the db
client: Client,
/// Blobstore used for fetching re-injected blob transactions.
@@ -90,14 +90,14 @@ pub struct EthTransactionValidator<Client, T> {
/// Disable balance checks during transaction validation
disable_balance_check: bool,
/// Marker for the transaction type
_marker: PhantomData<T>,
_marker: PhantomData<(T, B)>,
/// Metrics for tsx pool validation
validation_metrics: TxPoolValidationMetrics,
/// Bitmap of custom transaction types that are allowed.
other_tx_types: U256,
}
impl<Client, Tx> EthTransactionValidator<Client, Tx> {
impl<Client, Tx, B: Block> EthTransactionValidator<Client, Tx, B> {
/// Returns the configured chain spec
pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
where
@@ -176,7 +176,7 @@ impl<Client, Tx> EthTransactionValidator<Client, Tx> {
}
}
impl<Client, Tx> EthTransactionValidator<Client, Tx>
impl<Client, Tx, B: Block> EthTransactionValidator<Client, Tx, B>
where
Client: ChainSpecProvider<ChainSpec: EthereumHardforks> + StateProviderFactory,
Tx: EthPoolTransaction,
@@ -799,12 +799,14 @@ where
}
}
impl<Client, Tx> TransactionValidator for EthTransactionValidator<Client, Tx>
impl<Client, Tx, B> TransactionValidator for EthTransactionValidator<Client, Tx, B>
where
Client: ChainSpecProvider<ChainSpec: EthereumHardforks> + StateProviderFactory,
Tx: EthPoolTransaction,
B: Block,
{
type Transaction = Tx;
type Block = B;
async fn validate_transaction(
&self,
@@ -829,11 +831,8 @@ where
self.validate_batch_with_origin(origin, transactions)
}
fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
where
B: Block,
{
self.on_new_head_block(new_tip_block.header())
fn on_new_head_block(&self, new_tip_block: &reth_primitives_traits::SealedBlock<Self::Block>) {
Self::on_new_head_block(self, new_tip_block.header())
}
}
@@ -1105,9 +1104,10 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
}
/// Builds a the [`EthTransactionValidator`] without spawning validator tasks.
pub fn build<Tx, S>(self, blob_store: S) -> EthTransactionValidator<Client, Tx>
pub fn build<Tx, S, B>(self, blob_store: S) -> EthTransactionValidator<Client, Tx, B>
where
S: BlobStore,
B: Block,
{
let Self {
client,
@@ -1170,17 +1170,18 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
/// The validator will spawn `additional_tasks` additional tasks for validation.
///
/// By default this will spawn 1 additional task.
pub fn build_with_tasks<Tx, T, S>(
pub fn build_with_tasks<Tx, T, S, B>(
self,
tasks: T,
blob_store: S,
) -> TransactionValidationTaskExecutor<EthTransactionValidator<Client, Tx>>
) -> TransactionValidationTaskExecutor<EthTransactionValidator<Client, Tx, B>>
where
T: TaskSpawner,
S: BlobStore,
B: Block,
{
let additional_tasks = self.additional_tasks;
let validator = self.build(blob_store);
let validator = self.build::<Tx, S, B>(blob_store);
let (tx, task) = ValidationTask::new();
@@ -1341,7 +1342,8 @@ mod tests {
ExtendedAccount::new(transaction.nonce(), U256::MAX),
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider).build(blob_store.clone());
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider).build(blob_store.clone());
let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
@@ -1368,9 +1370,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.set_block_gas_limit(1_000_000) // tx gas limit is 1_015_288
.build(blob_store.clone());
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.set_block_gas_limit(1_000_000) // tx gas limit is 1_015_288
.build(blob_store.clone());
let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
@@ -1401,9 +1404,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.set_tx_fee_cap(100) // 100 wei cap
.build(blob_store.clone());
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.set_tx_fee_cap(100) // 100 wei cap
.build(blob_store.clone());
let outcome = validator.validate_one(TransactionOrigin::Local, transaction.clone());
assert!(outcome.is_invalid());
@@ -1438,9 +1442,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.set_tx_fee_cap(0) // no cap
.build(blob_store);
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.set_tx_fee_cap(0) // no cap
.build(blob_store);
let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
assert!(outcome.is_valid());
@@ -1456,9 +1461,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.set_tx_fee_cap(2e18 as u128) // 2 ETH cap
.build(blob_store);
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.set_tx_fee_cap(2e18 as u128) // 2 ETH cap
.build(blob_store);
let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
assert!(outcome.is_valid());
@@ -1474,9 +1480,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.with_max_tx_gas_limit(Some(500_000)) // Set limit lower than transaction gas limit (1_015_288)
.build(blob_store.clone());
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.with_max_tx_gas_limit(Some(500_000)) // Set limit lower than transaction gas limit (1_015_288)
.build(blob_store.clone());
let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
assert!(outcome.is_invalid());
@@ -1506,9 +1513,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.with_max_tx_gas_limit(None) // disabled
.build(blob_store);
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.with_max_tx_gas_limit(None) // disabled
.build(blob_store);
let outcome = validator.validate_one(TransactionOrigin::External, transaction);
assert!(outcome.is_valid());
@@ -1524,9 +1532,10 @@ mod tests {
);
let blob_store = InMemoryBlobStore::default();
let validator = EthTransactionValidatorBuilder::new(provider)
.with_max_tx_gas_limit(Some(2_000_000)) // Set limit higher than transaction gas limit (1_015_288)
.build(blob_store);
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.with_max_tx_gas_limit(Some(2_000_000)) // Set limit higher than transaction gas limit (1_015_288)
.build(blob_store);
let outcome = validator.validate_one(TransactionOrigin::External, transaction);
assert!(outcome.is_valid());
@@ -1727,8 +1736,9 @@ mod tests {
);
// Validate with balance check enabled
let validator = EthTransactionValidatorBuilder::new(provider.clone())
.build(InMemoryBlobStore::default());
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider.clone())
.build(InMemoryBlobStore::default());
let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
let expected_cost = *transaction.cost();
@@ -1743,9 +1753,10 @@ mod tests {
}
// Validate with balance check disabled
let validator = EthTransactionValidatorBuilder::new(provider)
.disable_balance_check() // This should allow the transaction through despite zero balance
.build(InMemoryBlobStore::default());
let validator: EthTransactionValidator<_, _> =
EthTransactionValidatorBuilder::new(provider)
.disable_balance_check()
.build(InMemoryBlobStore::default());
let outcome = validator.validate_one(TransactionOrigin::External, transaction);
assert!(outcome.is_valid()); // Should be valid because balance check is disabled

View File

@@ -9,7 +9,7 @@ use crate::{
use alloy_eips::{eip7594::BlobTransactionSidecarVariant, eip7702::SignedAuthorization};
use alloy_primitives::{Address, TxHash, B256, U256};
use futures_util::future::Either;
use reth_primitives_traits::{Recovered, SealedBlock};
use reth_primitives_traits::{Block, Recovered, SealedBlock};
use std::{fmt, fmt::Debug, future::Future, time::Instant};
mod constants;
@@ -24,7 +24,6 @@ pub use task::{TransactionValidationTaskExecutor, ValidationTask};
pub use constants::{
DEFAULT_MAX_TX_INPUT_BYTES, MAX_CODE_BYTE_SIZE, MAX_INIT_CODE_BYTE_SIZE, TX_SLOT_BYTE_SIZE,
};
use reth_primitives_traits::Block;
/// A Result type returned after checking a transaction's validity.
#[derive(Debug)]
@@ -174,6 +173,9 @@ pub trait TransactionValidator: Debug + Send + Sync {
/// The transaction type to validate.
type Transaction: PoolTransaction;
/// The block type used for new head block notifications.
type Block: Block;
/// Validates the transaction and returns a [`TransactionValidationOutcome`] describing the
/// validity of the given transaction.
///
@@ -236,19 +238,16 @@ pub trait TransactionValidator: Debug + Send + Sync {
/// Invoked when the head block changes.
///
/// This can be used to update fork specific values (timestamp).
fn on_new_head_block<B>(&self, _new_tip_block: &SealedBlock<B>)
where
B: Block,
{
}
fn on_new_head_block(&self, _new_tip_block: &SealedBlock<Self::Block>) {}
}
impl<A, B> TransactionValidator for Either<A, B>
where
A: TransactionValidator,
B: TransactionValidator<Transaction = A::Transaction>,
B: TransactionValidator<Transaction = A::Transaction, Block = A::Block>,
{
type Transaction = A::Transaction;
type Block = A::Block;
async fn validate_transaction(
&self,
@@ -282,10 +281,7 @@ where
}
}
fn on_new_head_block<Bl>(&self, new_tip_block: &SealedBlock<Bl>)
where
Bl: Block,
{
fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
match self {
Self::Left(v) => v.on_new_head_block(new_tip_block),
Self::Right(v) => v.on_new_head_block(new_tip_block),

View File

@@ -8,7 +8,7 @@ use crate::{
TransactionValidator,
};
use futures_util::{lock::Mutex, StreamExt};
use reth_primitives_traits::{Block, SealedBlock};
use reth_primitives_traits::SealedBlock;
use reth_tasks::TaskSpawner;
use std::{future::Future, pin::Pin, sync::Arc};
use tokio::{
@@ -171,7 +171,7 @@ impl<Client, Tx> TransactionValidationTaskExecutor<EthTransactionValidator<Clien
{
EthTransactionValidatorBuilder::new(client)
.with_additional_tasks(num_additional_tasks)
.build_with_tasks::<Tx, T, S>(tasks, blob_store)
.build_with_tasks(tasks, blob_store)
}
}
@@ -197,6 +197,7 @@ where
V: TransactionValidator + 'static,
{
type Transaction = <V as TransactionValidator>::Transaction;
type Block = V::Block;
async fn validate_transaction(
&self,
@@ -284,10 +285,7 @@ where
self.validate_transactions(transactions.into_iter().map(|tx| (origin, tx)).collect()).await
}
fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
where
B: Block,
{
fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
self.validator.on_new_head_block(new_tip_block)
}
}
@@ -307,6 +305,7 @@ mod tests {
impl TransactionValidator for NoopValidator {
type Transaction = MockTransaction;
type Block = reth_ethereum_primitives::Block;
async fn validate_transaction(
&self,

View File

@@ -40,7 +40,7 @@ mod nibbles;
pub use nibbles::{Nibbles, StoredNibbles, StoredNibblesSubKey};
mod storage;
pub use storage::{StorageTrieEntry, TrieChangeSetsEntry};
pub use storage::StorageTrieEntry;
mod subnode;
pub use subnode::StoredSubNode;

View File

@@ -42,181 +42,3 @@ impl reth_codecs::Compact for StorageTrieEntry {
(this, buf)
}
}
/// Trie changeset entry representing the state of a trie node before a block.
///
/// `nibbles` is the subkey when used as a value in the changeset tables.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
pub struct TrieChangeSetsEntry {
/// The nibbles of the intermediate node
pub nibbles: StoredNibblesSubKey,
/// Node value prior to the block being processed, None indicating it didn't exist.
pub node: Option<BranchNodeCompact>,
}
impl ValueWithSubKey for TrieChangeSetsEntry {
type SubKey = StoredNibblesSubKey;
fn get_subkey(&self) -> Self::SubKey {
self.nibbles.clone()
}
}
#[cfg(any(test, feature = "reth-codec"))]
impl reth_codecs::Compact for TrieChangeSetsEntry {
fn to_compact<B>(&self, buf: &mut B) -> usize
where
B: bytes::BufMut + AsMut<[u8]>,
{
let nibbles_len = self.nibbles.to_compact(buf);
let node_len = self.node.as_ref().map(|node| node.to_compact(buf)).unwrap_or(0);
nibbles_len + node_len
}
fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
if len == 0 {
// Return an empty entry without trying to parse anything
return (
Self { nibbles: StoredNibblesSubKey::from(super::Nibbles::default()), node: None },
buf,
)
}
let (nibbles, buf) = StoredNibblesSubKey::from_compact(buf, 65);
if len <= 65 {
return (Self { nibbles, node: None }, buf)
}
let (node, buf) = BranchNodeCompact::from_compact(buf, len - 65);
(Self { nibbles, node: Some(node) }, buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::BytesMut;
use reth_codecs::Compact;
#[test]
fn test_trie_changesets_entry_full_empty() {
// Test a fully empty entry (empty nibbles, None node)
let entry = TrieChangeSetsEntry { nibbles: StoredNibblesSubKey::from(vec![]), node: None };
let mut buf = BytesMut::new();
let len = entry.to_compact(&mut buf);
// Empty nibbles takes 65 bytes (64 for padding + 1 for length)
// None node adds 0 bytes
assert_eq!(len, 65);
assert_eq!(buf.len(), 65);
// Deserialize and verify
let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len);
assert_eq!(decoded.nibbles.0.to_vec(), Vec::<u8>::new());
assert_eq!(decoded.node, None);
assert_eq!(remaining.len(), 0);
}
#[test]
fn test_trie_changesets_entry_none_node() {
// Test non-empty nibbles with None node
let nibbles_data = vec![0x01, 0x02, 0x03, 0x04];
let entry = TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey::from(nibbles_data.clone()),
node: None,
};
let mut buf = BytesMut::new();
let len = entry.to_compact(&mut buf);
// Nibbles takes 65 bytes regardless of content
assert_eq!(len, 65);
// Deserialize and verify
let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len);
assert_eq!(decoded.nibbles.0.to_vec(), nibbles_data);
assert_eq!(decoded.node, None);
assert_eq!(remaining.len(), 0);
}
#[test]
fn test_trie_changesets_entry_empty_path_with_node() {
// Test empty path with Some node
// Using the same signature as in the codebase: (state_mask, hash_mask, tree_mask, hashes,
// value)
let test_node = BranchNodeCompact::new(
0b1111_1111_1111_1111, // state_mask: all children present
0b1111_1111_1111_1111, // hash_mask: all have hashes
0b0000_0000_0000_0000, // tree_mask: no embedded trees
vec![], // hashes
None, // value
);
let entry = TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey::from(vec![]),
node: Some(test_node.clone()),
};
let mut buf = BytesMut::new();
let len = entry.to_compact(&mut buf);
// Calculate expected length
let mut temp_buf = BytesMut::new();
let node_len = test_node.to_compact(&mut temp_buf);
assert_eq!(len, 65 + node_len);
// Deserialize and verify
let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len);
assert_eq!(decoded.nibbles.0.to_vec(), Vec::<u8>::new());
assert_eq!(decoded.node, Some(test_node));
assert_eq!(remaining.len(), 0);
}
#[test]
fn test_trie_changesets_entry_normal() {
// Test normal case: non-empty path with Some node
let nibbles_data = vec![0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f];
// Using the same signature as in the codebase
let test_node = BranchNodeCompact::new(
0b0000_0000_1111_0000, // state_mask: some children present
0b0000_0000_0011_0000, // hash_mask: some have hashes
0b0000_0000_0000_0000, // tree_mask: no embedded trees
vec![], // hashes (empty for this test)
None, // value
);
let entry = TrieChangeSetsEntry {
nibbles: StoredNibblesSubKey::from(nibbles_data.clone()),
node: Some(test_node.clone()),
};
let mut buf = BytesMut::new();
let len = entry.to_compact(&mut buf);
// Verify serialization length
let mut temp_buf = BytesMut::new();
let node_len = test_node.to_compact(&mut temp_buf);
assert_eq!(len, 65 + node_len);
// Deserialize and verify
let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len);
assert_eq!(decoded.nibbles.0.to_vec(), nibbles_data);
assert_eq!(decoded.node, Some(test_node));
assert_eq!(remaining.len(), 0);
}
#[test]
fn test_trie_changesets_entry_from_compact_zero_len() {
// Test from_compact with zero length
let buf = vec![0x01, 0x02, 0x03];
let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, 0);
// Should return empty nibbles and None node
assert_eq!(decoded.nibbles.0.to_vec(), Vec::<u8>::new());
assert_eq!(decoded.node, None);
assert_eq!(remaining, &buf[..]); // Buffer should be unchanged
}
}

View File

@@ -58,8 +58,6 @@ There are many tables within the node, all used to store different types of data
- HashedStorages
- AccountsTrie
- StoragesTrie
- AccountsTrieChangeSets
- StoragesTrieChangeSets
- TransactionSenders
- StageCheckpoints
- StageCheckpointProgresses

View File

@@ -12,7 +12,6 @@ The `stages` lib plays a central role in syncing the node, maintaining state, up
- AccountHashingStage
- StorageHashingStage
- MerkleStage (execute)
- MerkleChangeSets
- TransactionLookupStage
- IndexStorageHistoryStage
- IndexAccountHistoryStage
@@ -114,12 +113,6 @@ The `StorageHashingStage` is responsible for computing hashes of contract storag
<br>
## MerkleChangeSets
The `MerkleChangeSets` stage consolidates and finalizes Merkle-related change sets after the `MerkleStage` execution mode has run, ensuring consistent trie updates and checkpoints.
<br>
## TransactionLookupStage
The `TransactionLookupStage` builds and maintains transaction lookup indices. These indices enable efficient querying of transactions by hash or block position. This stage is crucial for RPC functionality, allowing users to quickly retrieve transaction information without scanning the entire blockchain.

View File

@@ -55,6 +55,7 @@
- [`reth p2p rlpx`](./reth/p2p/rlpx.mdx)
- [`reth p2p rlpx ping`](./reth/p2p/rlpx/ping.mdx)
- [`reth p2p bootnode`](./reth/p2p/bootnode.mdx)
- [`reth p2p enode`](./reth/p2p/enode.mdx)
- [`reth config`](./reth/config.mdx)
- [`reth prune`](./reth/prune.mdx)
- [`reth re-execute`](./reth/re-execute.mdx)
@@ -113,6 +114,7 @@
- [`op-reth p2p rlpx`](./op-reth/p2p/rlpx.mdx)
- [`op-reth p2p rlpx ping`](./op-reth/p2p/rlpx/ping.mdx)
- [`op-reth p2p bootnode`](./op-reth/p2p/bootnode.mdx)
- [`op-reth p2p enode`](./op-reth/p2p/enode.mdx)
- [`op-reth config`](./op-reth/config.mdx)
- [`op-reth prune`](./op-reth/prune.mdx)
- [`op-reth re-execute`](./op-reth/re-execute.mdx)

View File

@@ -13,6 +13,7 @@ Commands:
body Download block body
rlpx RLPx commands
bootnode Bootnode command
enode Print enode identifier
help Print this message or the help of the given subcommand(s)
Options:

View File

@@ -0,0 +1,165 @@
# op-reth p2p enode
Print enode identifier
```bash
$ op-reth p2p enode --help
```
```txt
Usage: op-reth p2p enode [OPTIONS] <DISCOVERY_SECRET>
Arguments:
<DISCOVERY_SECRET>
Path to the secret key file for discovery
Options:
--ip <IP>
Optional IP address to include in the enode URL.
If not provided, defaults to 0.0.0.0.
-h, --help
Print help (see a summary with '-h')
Logging:
--log.stdout.format <FORMAT>
The format to use for logs written to stdout
Possible values:
- json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging
- log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications
- terminal: Represents terminal-friendly formatting for logs
[default: terminal]
--log.stdout.filter <FILTER>
The filter to use for logs written to stdout
[default: ]
--log.file.format <FORMAT>
The format to use for logs written to the log file
Possible values:
- json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging
- log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications
- terminal: Represents terminal-friendly formatting for logs
[default: terminal]
--log.file.filter <FILTER>
The filter to use for logs written to the log file
[default: debug]
--log.file.directory <PATH>
The path to put log files in
[default: <CACHE_DIR>/logs]
--log.file.name <NAME>
The prefix name of the log files
[default: reth.log]
--log.file.max-size <SIZE>
The maximum size (in MB) of one log file
[default: 200]
--log.file.max-files <COUNT>
The maximum amount of log files that will be stored. If set to 0, background file logging is disabled
[default: 5]
--log.journald
Write logs to journald
--log.journald.filter <FILTER>
The filter to use for logs written to journald
[default: error]
--color <COLOR>
Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting
Possible values:
- always: Colors on
- auto: Auto-detect
- never: Colors off
[default: always]
--logs-otlp[=<URL>]
Enable `Opentelemetry` logs export to an OTLP endpoint.
If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/logs` - gRPC: `http://localhost:4317`
Example: --logs-otlp=http://collector:4318/v1/logs
[env: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=]
--logs-otlp.filter <FILTER>
Set a filter directive for the OTLP logs exporter. This controls the verbosity of logs sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable.
Example: --logs-otlp.filter=info,reth=debug
Defaults to INFO if not specified.
[default: info]
Display:
-v, --verbosity...
Set the minimum log level.
-v Errors
-vv Warnings
-vvv Info
-vvvv Debug
-vvvvv Traces (warning: very verbose!)
-q, --quiet
Silence all log output
Tracing:
--tracing-otlp[=<URL>]
Enable `Opentelemetry` tracing export to an OTLP endpoint.
If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317`
Example: --tracing-otlp=http://collector:4318/v1/traces
[env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=]
--tracing-otlp-protocol <PROTOCOL>
OTLP transport protocol to use for exporting traces and logs.
- `http`: expects endpoint path to end with `/v1/traces` or `/v1/logs` - `grpc`: expects endpoint without a path
Defaults to HTTP if not specified.
Possible values:
- http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path
- grpc: gRPC transport, port 4317
[env: OTEL_EXPORTER_OTLP_PROTOCOL=]
[default: http]
--tracing-otlp.filter <FILTER>
Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable.
Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off
Defaults to TRACE if not specified.
[default: debug]
--tracing-otlp.sample-ratio <RATIO>
Trace sampling ratio to control the percentage of traces to export.
Valid range: 0.0 to 1.0 - 1.0, default: Sample all traces - 0.01: Sample 1% of traces - 0.0: Disable sampling
Example: --tracing-otlp.sample-ratio=0.0.
[env: OTEL_TRACES_SAMPLER_ARG=]
```

View File

@@ -13,6 +13,7 @@ Commands:
body Download block body
rlpx RLPx commands
bootnode Bootnode command
enode Print enode identifier
help Print this message or the help of the given subcommand(s)
Options:

View File

@@ -0,0 +1,165 @@
# reth p2p enode
Print enode identifier
```bash
$ reth p2p enode --help
```
```txt
Usage: reth p2p enode [OPTIONS] <DISCOVERY_SECRET>
Arguments:
<DISCOVERY_SECRET>
Path to the secret key file for discovery
Options:
--ip <IP>
Optional IP address to include in the enode URL.
If not provided, defaults to 0.0.0.0.
-h, --help
Print help (see a summary with '-h')
Logging:
--log.stdout.format <FORMAT>
The format to use for logs written to stdout
Possible values:
- json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging
- log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications
- terminal: Represents terminal-friendly formatting for logs
[default: terminal]
--log.stdout.filter <FILTER>
The filter to use for logs written to stdout
[default: ]
--log.file.format <FORMAT>
The format to use for logs written to the log file
Possible values:
- json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging
- log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications
- terminal: Represents terminal-friendly formatting for logs
[default: terminal]
--log.file.filter <FILTER>
The filter to use for logs written to the log file
[default: debug]
--log.file.directory <PATH>
The path to put log files in
[default: <CACHE_DIR>/logs]
--log.file.name <NAME>
The prefix name of the log files
[default: reth.log]
--log.file.max-size <SIZE>
The maximum size (in MB) of one log file
[default: 200]
--log.file.max-files <COUNT>
The maximum amount of log files that will be stored. If set to 0, background file logging is disabled
[default: 5]
--log.journald
Write logs to journald
--log.journald.filter <FILTER>
The filter to use for logs written to journald
[default: error]
--color <COLOR>
Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting
Possible values:
- always: Colors on
- auto: Auto-detect
- never: Colors off
[default: always]
--logs-otlp[=<URL>]
Enable `Opentelemetry` logs export to an OTLP endpoint.
If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/logs` - gRPC: `http://localhost:4317`
Example: --logs-otlp=http://collector:4318/v1/logs
[env: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=]
--logs-otlp.filter <FILTER>
Set a filter directive for the OTLP logs exporter. This controls the verbosity of logs sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable.
Example: --logs-otlp.filter=info,reth=debug
Defaults to INFO if not specified.
[default: info]
Display:
-v, --verbosity...
Set the minimum log level.
-v Errors
-vv Warnings
-vvv Info
-vvvv Debug
-vvvvv Traces (warning: very verbose!)
-q, --quiet
Silence all log output
Tracing:
--tracing-otlp[=<URL>]
Enable `Opentelemetry` tracing export to an OTLP endpoint.
If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317`
Example: --tracing-otlp=http://collector:4318/v1/traces
[env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=]
--tracing-otlp-protocol <PROTOCOL>
OTLP transport protocol to use for exporting traces and logs.
- `http`: expects endpoint path to end with `/v1/traces` or `/v1/logs` - `grpc`: expects endpoint without a path
Defaults to HTTP if not specified.
Possible values:
- http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path
- grpc: gRPC transport, port 4317
[env: OTEL_EXPORTER_OTLP_PROTOCOL=]
[default: http]
--tracing-otlp.filter <FILTER>
Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable.
Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off
Defaults to TRACE if not specified.
[default: debug]
--tracing-otlp.sample-ratio <RATIO>
Trace sampling ratio to control the percentage of traces to export.
Valid range: 0.0 to 1.0 - 1.0, default: Sample all traces - 0.01: Sample 1% of traces - 0.0: Disable sampling
Example: --tracing-otlp.sample-ratio=0.0.
[env: OTEL_TRACES_SAMPLER_ARG=]
```

View File

@@ -434,11 +434,6 @@ storage_history = { distance = 100_000 } # Prune all historical storage states b
# Bodies History pruning configuration
bodies_history = { distance = 100_000 } # Prune all historical block bodies before the block `head-100000`
# Merkle Changesets pruning configuration
# Controls pruning of AccountsTrieChangeSets and StoragesTrieChangeSets.
# Default: { distance = 128 } - keeps the last 128 blocks of merkle changesets
merkle_changesets = { distance = 128 }
```
We can also prune receipts more granular, using the logs filtering:

View File

@@ -254,6 +254,10 @@ export const opRethCliSidebar: SidebarItem = {
{
text: "op-reth p2p bootnode",
link: "/cli/op-reth/p2p/bootnode"
},
{
text: "op-reth p2p enode",
link: "/cli/op-reth/p2p/enode"
}
]
},

View File

@@ -262,6 +262,10 @@ export const rethCliSidebar: SidebarItem = {
{
text: "reth p2p bootnode",
link: "/cli/reth/p2p/bootnode"
},
{
text: "reth p2p enode",
link: "/cli/reth/p2p/enode"
}
]
},

View File

@@ -21,7 +21,7 @@
}
]
},
"description": "Companion dashboard for https://github.com/samcm/ethereum-metrics-exporter",
"description": "Companion dashboard for https://github.com/ethpandaops/ethereum-metrics-exporter",
"editable": true,
"fiscalYearStartMonth": 0,
"gnetId": 16277,

View File

@@ -6,9 +6,9 @@
use alloy_eips::eip4895::Withdrawal;
use alloy_evm::{
block::{BlockExecutorFactory, BlockExecutorFor, ExecutableTx},
eth::{EthBlockExecutionCtx, EthBlockExecutor},
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthTxResult},
precompiles::PrecompilesMap,
revm::context::{result::ResultAndState, Block as _},
revm::context::Block as _,
EthEvm, EthEvmFactory,
};
use alloy_sol_macro::sol;
@@ -39,7 +39,7 @@ use reth_ethereum::{
primitives::{Header, SealedBlock, SealedHeader},
provider::BlockExecutionResult,
rpc::types::engine::ExecutionData,
Block, EthPrimitives, Receipt, TransactionSigned,
Block, EthPrimitives, Receipt, TransactionSigned, TxType,
};
use std::{fmt::Display, sync::Arc};
@@ -196,6 +196,7 @@ where
type Transaction = TransactionSigned;
type Receipt = Receipt;
type Evm = E;
type Result = EthTxResult<E::HaltReason, TxType>;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
self.inner.apply_pre_execution_changes()
@@ -208,16 +209,12 @@ where
fn execute_transaction_without_commit(
&mut self,
tx: impl ExecutableTx<Self>,
) -> Result<ResultAndState<<Self::Evm as Evm>::HaltReason>, BlockExecutionError> {
) -> Result<Self::Result, BlockExecutionError> {
self.inner.execute_transaction_without_commit(tx)
}
fn commit_transaction(
&mut self,
output: ResultAndState<<Self::Evm as Evm>::HaltReason>,
tx: impl ExecutableTx<Self>,
) -> Result<u64, BlockExecutionError> {
self.inner.commit_transaction(output, tx)
fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
self.inner.commit_transaction(output)
}
fn finish(mut self) -> Result<(Self::Evm, BlockExecutionResult<Receipt>), BlockExecutionError> {

View File

@@ -23,8 +23,8 @@ use alloy_primitives::{Address, B256};
use alloy_rpc_types::{
engine::{
ExecutionData, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3,
ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadV1,
PayloadAttributes as EthPayloadAttributes, PayloadId,
ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadEnvelopeV6,
ExecutionPayloadV1, PayloadAttributes as EthPayloadAttributes, PayloadId,
},
Withdrawal,
};
@@ -169,6 +169,7 @@ impl EngineTypes for CustomEngineTypes {
type ExecutionPayloadEnvelopeV3 = ExecutionPayloadEnvelopeV3;
type ExecutionPayloadEnvelopeV4 = ExecutionPayloadEnvelopeV4;
type ExecutionPayloadEnvelopeV5 = ExecutionPayloadEnvelopeV5;
type ExecutionPayloadEnvelopeV6 = ExecutionPayloadEnvelopeV6;
}
/// Custom engine validator

View File

@@ -6,14 +6,14 @@ use reth_ethereum::{
chainspec::ChainSpec,
cli::interface::Cli,
node::{
api::{FullNodeTypes, NodeTypes},
api::{BlockTy, FullNodeTypes, NodeTypes},
builder::{components::PoolBuilder, BuilderContext},
node::EthereumAddOns,
EthereumNode,
},
pool::{
blobstore::InMemoryBlobStore, EthTransactionPool, PoolConfig,
TransactionValidationTaskExecutor,
blobstore::InMemoryBlobStore, CoinbaseTipOrdering, EthPooledTransaction,
EthTransactionPool, Pool, PoolConfig, TransactionValidationTaskExecutor,
},
provider::CanonStateSubscriptions,
EthPrimitives,
@@ -53,7 +53,12 @@ impl<Node> PoolBuilder<Node> for CustomPoolBuilder
where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec = ChainSpec, Primitives = EthPrimitives>>,
{
type Pool = EthTransactionPool<Node::Provider, InMemoryBlobStore>;
type Pool = EthTransactionPool<
Node::Provider,
InMemoryBlobStore,
EthPooledTransaction,
BlockTy<Node::Types>,
>;
async fn build_pool(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Pool> {
let data_dir = ctx.config().datadir();
@@ -62,10 +67,13 @@ where
.with_head_timestamp(ctx.head().timestamp)
.kzg_settings(ctx.kzg_settings()?)
.with_additional_tasks(ctx.config().txpool.additional_validation_tasks)
.build_with_tasks(ctx.task_executor().clone(), blob_store.clone());
.build_with_tasks::<_, _, _, BlockTy<Node::Types>>(
ctx.task_executor().clone(),
blob_store.clone(),
);
let transaction_pool =
reth_ethereum::pool::Pool::eth_pool(validator, blob_store, self.pool_config);
Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, self.pool_config);
info!(target: "reth::cli", "Transaction pool initialized");
let transactions_path = data_dir.txpool_transactions();

View File

@@ -12,12 +12,12 @@ use alloy_evm::{
BlockExecutorFor, ExecutableTx, OnStateHook,
},
precompiles::PrecompilesMap,
Database, Evm,
Database, Evm, RecoveredTx,
};
use alloy_op_evm::{OpBlockExecutionCtx, OpBlockExecutor};
use alloy_op_evm::{block::OpTxResult, OpBlockExecutionCtx, OpBlockExecutor};
use reth_ethereum::evm::primitives::InspectorFor;
use reth_op::{chainspec::OpChainSpec, node::OpRethReceiptBuilder, OpReceipt};
use revm::{context::result::ResultAndState, database::State};
use reth_op::{chainspec::OpChainSpec, node::OpRethReceiptBuilder, OpReceipt, OpTxType};
use revm::database::State;
use std::sync::Arc;
pub struct CustomBlockExecutor<Evm> {
@@ -32,6 +32,7 @@ where
type Transaction = CustomTransaction;
type Receipt = OpReceipt;
type Evm = E;
type Result = OpTxResult<E::HaltReason, OpTxType>;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
self.inner.apply_pre_execution_changes()
@@ -44,7 +45,8 @@ where
fn execute_transaction_without_commit(
&mut self,
tx: impl ExecutableTx<Self>,
) -> Result<ResultAndState<<Self::Evm as Evm>::HaltReason>, BlockExecutionError> {
) -> Result<Self::Result, BlockExecutionError> {
let tx = tx.into_parts().1;
match tx.tx() {
CustomTransaction::Op(op_tx) => self
.inner
@@ -53,17 +55,8 @@ where
}
}
fn commit_transaction(
&mut self,
output: ResultAndState<<Self::Evm as Evm>::HaltReason>,
tx: impl ExecutableTx<Self>,
) -> Result<u64, BlockExecutionError> {
match tx.tx() {
CustomTransaction::Op(op_tx) => {
self.inner.commit_transaction(output, Recovered::new_unchecked(op_tx, *tx.signer()))
}
CustomTransaction::Payment(..) => todo!(),
}
fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
self.inner.commit_transaction(output)
}
fn finish(self) -> Result<(Self::Evm, BlockExecutionResult<OpReceipt>), BlockExecutionError> {