mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
28 Commits
pr-21236
...
feat/bal-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cbcfe01a0 | ||
|
|
830cd5e355 | ||
|
|
f77d7d5983 | ||
|
|
a2237c534e | ||
|
|
1bd8fab887 | ||
|
|
22a68756c7 | ||
|
|
d99c0ffd62 | ||
|
|
ad476e2b5c | ||
|
|
6df249c1f1 | ||
|
|
5a076df09a | ||
|
|
f07629eac0 | ||
|
|
f643e93c35 | ||
|
|
653362a436 | ||
|
|
a02508600c | ||
|
|
937a7f226d | ||
|
|
a0df561117 | ||
|
|
be5a4ac7a6 | ||
|
|
0c854b6f14 | ||
|
|
28a31cd579 | ||
|
|
da12451c9c | ||
|
|
247ce3c4e9 | ||
|
|
bf43ebaa29 | ||
|
|
a0aac13f75 | ||
|
|
9f5cf847cc | ||
|
|
df1413167a | ||
|
|
3f50a36191 | ||
|
|
d750b4976d | ||
|
|
7a65d2595d |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
7
.github/workflows/check-alloy.yml
vendored
7
.github/workflows/check-alloy.yml
vendored
@@ -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
131
Cargo.lock
generated
@@ -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",
|
||||
|
||||
64
Cargo.toml
64
Cargo.toml
@@ -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" }
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
219
bin/reth-bench/src/bench/send_invalid_payload/invalidation.rs
Normal file
219
bin/reth-bench/src/bench/send_invalid_payload/invalidation.rs
Normal 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
|
||||
}
|
||||
}
|
||||
367
bin/reth-bench/src/bench/send_invalid_payload/mod.rs
Normal file
367
bin/reth-bench/src/bench/send_invalid_payload/mod.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
//!
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
34
crates/cli/commands/src/p2p/enode.rs
Normal file
34
crates/cli/commands/src/p2p/enode.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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`]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -62,6 +62,7 @@ where
|
||||
type ExecutionPayloadEnvelopeV3 = OpExecutionPayloadEnvelopeV3;
|
||||
type ExecutionPayloadEnvelopeV4 = OpExecutionPayloadEnvelopeV4;
|
||||
type ExecutionPayloadEnvelopeV5 = OpExecutionPayloadEnvelopeV4;
|
||||
type ExecutionPayloadEnvelopeV6 = OpExecutionPayloadEnvelopeV4;
|
||||
}
|
||||
|
||||
/// Validator for Optimism engine API.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
274
crates/rpc/rpc-engine-api/src/bal_cache.rs
Normal file
274
crates/rpc/rpc-engine-api/src/bal_cache.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
165
docs/vocs/docs/pages/cli/op-reth/p2p/enode.mdx
Normal file
165
docs/vocs/docs/pages/cli/op-reth/p2p/enode.mdx
Normal 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=]
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
165
docs/vocs/docs/pages/cli/reth/p2p/enode.mdx
Normal file
165
docs/vocs/docs/pages/cli/reth/p2p/enode.mdx
Normal 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=]
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user