mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
510 Commits
performanc
...
wt/batch-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7840143e6d | ||
|
|
11d9f38077 | ||
|
|
226ce14ca1 | ||
|
|
a6e1dea2d7 | ||
|
|
71ed68e944 | ||
|
|
adecbd7814 | ||
|
|
26a37f3c00 | ||
|
|
0bfa7fa5fa | ||
|
|
18bec10a0b | ||
|
|
1e33821e19 | ||
|
|
da92733be8 | ||
|
|
c41c8e6cae | ||
|
|
1ccc174e7b | ||
|
|
f1459fcf91 | ||
|
|
94235d64a8 | ||
|
|
7fe60017cf | ||
|
|
f9ec2fafa0 | ||
|
|
768a687189 | ||
|
|
b87cde5479 | ||
|
|
ab685579f0 | ||
|
|
c7faafd183 | ||
|
|
935a2cc056 | ||
|
|
507cf58db0 | ||
|
|
6cfd369d17 | ||
|
|
934f462d01 | ||
|
|
d4f28b02ff | ||
|
|
963bfeeeed | ||
|
|
adbe6d9da0 | ||
|
|
6d19c0ed8e | ||
|
|
4baf2baec4 | ||
|
|
0b5f79e8c9 | ||
|
|
afe164baca | ||
|
|
31fdbe914c | ||
|
|
6870747246 | ||
|
|
0ad8c772e1 | ||
|
|
5440d0d89a | ||
|
|
0eea4d76e9 | ||
|
|
8a1702cd74 | ||
|
|
7feb56d5f6 | ||
|
|
0aa922c4e8 | ||
|
|
ccff9a08f0 | ||
|
|
eb788cc7cf | ||
|
|
fb05a0654f | ||
|
|
d5a36dcc00 | ||
|
|
ffbef9e3cd | ||
|
|
820c112e8e | ||
|
|
9285f7eafc | ||
|
|
9a4c6d8a11 | ||
|
|
963c26550a | ||
|
|
3648483512 | ||
|
|
ab418642b4 | ||
|
|
decb56fae1 | ||
|
|
ee1ec8f9f0 | ||
|
|
d7bf87da52 | ||
|
|
dd0c6d279f | ||
|
|
c137ed836f | ||
|
|
a543752f7d | ||
|
|
b814893221 | ||
|
|
fcef82261d | ||
|
|
d3846d98a9 | ||
|
|
1f536cce65 | ||
|
|
0ddaf1b26c | ||
|
|
830cd5e355 | ||
|
|
f77d7d5983 | ||
|
|
a2237c534e | ||
|
|
1bd8fab887 | ||
|
|
22a68756c7 | ||
|
|
d99c0ffd62 | ||
|
|
ad476e2b5c | ||
|
|
6df249c1f1 | ||
|
|
5a076df09a | ||
|
|
f07629eac0 | ||
|
|
f643e93c35 | ||
|
|
653362a436 | ||
|
|
a02508600c | ||
|
|
937a7f226d | ||
|
|
a0df561117 | ||
|
|
be5a4ac7a6 | ||
|
|
0c854b6f14 | ||
|
|
28a31cd579 | ||
|
|
da12451c9c | ||
|
|
247ce3c4e9 | ||
|
|
bf43ebaa29 | ||
|
|
a01ecce73f | ||
|
|
3e55c6ca6e | ||
|
|
2ac7d719f3 | ||
|
|
965705ff88 | ||
|
|
ebe2ca1366 | ||
|
|
cc242f83fd | ||
|
|
12cf3d685b | ||
|
|
ad5b533ad1 | ||
|
|
118f15f345 | ||
|
|
97481f69e5 | ||
|
|
f692ac7d1e | ||
|
|
4b1c341ced | ||
|
|
865f8f8951 | ||
|
|
492fc20fd1 | ||
|
|
ad9886abb8 | ||
|
|
5c3e45cd6b | ||
|
|
68fdba32d2 | ||
|
|
8f6a0a2992 | ||
|
|
ec9c7f8d3e | ||
|
|
dbdaf068f0 | ||
|
|
055bf63ee9 | ||
|
|
2305c3ebeb | ||
|
|
eb55c3c3da | ||
|
|
72e1467ba3 | ||
|
|
74edce0089 | ||
|
|
8c645d5762 | ||
|
|
b7d2ee2566 | ||
|
|
7609deddda | ||
|
|
ec50fd40b3 | ||
|
|
624ddc5779 | ||
|
|
dd72cfe23e | ||
|
|
ff8ac97e33 | ||
|
|
0974485863 | ||
|
|
274394e777 | ||
|
|
1954c91a60 | ||
|
|
9cf82c8403 | ||
|
|
f85fcba872 | ||
|
|
ebaa4bda3a | ||
|
|
04d4c9a02f | ||
|
|
3065a328f9 | ||
|
|
43a84f1231 | ||
|
|
5a5c21cc1b | ||
|
|
8a8a9126d6 | ||
|
|
6f73c2447d | ||
|
|
2cae438642 | ||
|
|
37b5db0d47 | ||
|
|
238433e146 | ||
|
|
660964a0f5 | ||
|
|
22b465dd64 | ||
|
|
3ff575b877 | ||
|
|
d12752dc8a | ||
|
|
869b5d0851 | ||
|
|
78de3d8f61 | ||
|
|
bc79cc44c9 | ||
|
|
ff8f434dcd | ||
|
|
9662dc5271 | ||
|
|
3ba37082dc | ||
|
|
7934294988 | ||
|
|
7371bd3f29 | ||
|
|
80980b8e4d | ||
|
|
2e2cd67663 | ||
|
|
4f009728e2 | ||
|
|
39d5ae73e8 | ||
|
|
5ef200eaad | ||
|
|
d002dacc13 | ||
|
|
bb39cba504 | ||
|
|
bd144a4c42 | ||
|
|
a0845bab18 | ||
|
|
346cc0da71 | ||
|
|
ea3d4663ae | ||
|
|
3667d3b5aa | ||
|
|
7cfb19c98e | ||
|
|
5a38871489 | ||
|
|
c825c8c187 | ||
|
|
8f37cd08fc | ||
|
|
c9dad4765d | ||
|
|
1d55abeef3 | ||
|
|
f7460e219c | ||
|
|
0c66315f20 | ||
|
|
6a2010e595 | ||
|
|
c2435ff6f8 | ||
|
|
52ec8e9491 | ||
|
|
a901d80ee6 | ||
|
|
915164078f | ||
|
|
be3234d848 | ||
|
|
f624372334 | ||
|
|
40bc9d3860 | ||
|
|
1ea574417f | ||
|
|
27e055f790 | ||
|
|
d5dc0b27eb | ||
|
|
c11c13000f | ||
|
|
6bf43ab24a | ||
|
|
574bde0d6f | ||
|
|
79b8ffb828 | ||
|
|
c617d25c36 | ||
|
|
b96a30821f | ||
|
|
012fbf5110 | ||
|
|
d7a5d1f872 | ||
|
|
3a39251f79 | ||
|
|
f6dbf2d82d | ||
|
|
13707faf1a | ||
|
|
6e6415690c | ||
|
|
b81e373d78 | ||
|
|
a164654145 | ||
|
|
905bb95f8b | ||
|
|
13c32625bc | ||
|
|
1be9fab5bf | ||
|
|
80eb0d0fb6 | ||
|
|
5e178f6ac6 | ||
|
|
b4b64096c8 | ||
|
|
e313de818b | ||
|
|
86c414081a | ||
|
|
a74cb9cbc3 | ||
|
|
e25411c32b | ||
|
|
ec3323bba0 | ||
|
|
26cd132631 | ||
|
|
079f59c2be | ||
|
|
e9b079ad62 | ||
|
|
b1f107b171 | ||
|
|
7d0e7e72de | ||
|
|
f012b3391e | ||
|
|
d225fc1d7f | ||
|
|
d469b7f1d0 | ||
|
|
9bcd3712c8 | ||
|
|
b25f32a977 | ||
|
|
905de96944 | ||
|
|
27fbd9a7de | ||
|
|
26a99ac5a3 | ||
|
|
1265a89c21 | ||
|
|
b9ff5941eb | ||
|
|
a75a0a5db7 | ||
|
|
0a4bac77d0 | ||
|
|
1fbd5a95f8 | ||
|
|
1bc07fad8e | ||
|
|
8cb506c4d3 | ||
|
|
15f16a5a2e | ||
|
|
5cf1d2a0b0 | ||
|
|
59fb25d892 | ||
|
|
665a0a8553 | ||
|
|
54735ce0f4 | ||
|
|
a73e73adef | ||
|
|
4f3bd3eac1 | ||
|
|
ae41823be6 | ||
|
|
1fa71f893c | ||
|
|
c6b17848dd | ||
|
|
a5dd7d0106 | ||
|
|
61354e6c21 | ||
|
|
2444533a04 | ||
|
|
8fa01eb62e | ||
|
|
c5e00e4aeb | ||
|
|
98a35cc870 | ||
|
|
46d670eca5 | ||
|
|
25906b7b3e | ||
|
|
1b3d815cb8 | ||
|
|
23f3f8e820 | ||
|
|
2663942b50 | ||
|
|
369c629b9b | ||
|
|
6fec4603cf | ||
|
|
515fd597f3 | ||
|
|
126a7c9570 | ||
|
|
8aeee5018e | ||
|
|
210309ca76 | ||
|
|
551918b0d8 | ||
|
|
89677e1bd9 | ||
|
|
0e2b3afa3f | ||
|
|
5d551eab29 | ||
|
|
12c4c04f7d | ||
|
|
392f8e6e13 | ||
|
|
1a94d1f091 | ||
|
|
97ae89c7f0 | ||
|
|
a4921119e4 | ||
|
|
0f3d3695f5 | ||
|
|
54355dfc78 | ||
|
|
44a6035fa3 | ||
|
|
746baed2b1 | ||
|
|
e86c5fba53 | ||
|
|
485fa3448d | ||
|
|
0db3813941 | ||
|
|
52c2ae3362 | ||
|
|
b1d75f2771 | ||
|
|
ef80ee1687 | ||
|
|
8dacfb3d9c | ||
|
|
425a021e3b | ||
|
|
08c0d30ea7 | ||
|
|
84e970e4c9 | ||
|
|
020f20db42 | ||
|
|
f53929e0c8 | ||
|
|
4a8fbe15e3 | ||
|
|
a59e9832e6 | ||
|
|
07beb76cf7 | ||
|
|
3ddf0bd729 | ||
|
|
c3d92ddfc2 | ||
|
|
c0628dfbff | ||
|
|
a2aa1f18df | ||
|
|
d489f80f6b | ||
|
|
bf272c9432 | ||
|
|
ebb54d0dcc | ||
|
|
1d7367c389 | ||
|
|
824ae12d75 | ||
|
|
2db281e51d | ||
|
|
8367ba473e | ||
|
|
f2abad5f5c | ||
|
|
4673d77c03 | ||
|
|
33bcd60348 | ||
|
|
8a9b5d90f4 | ||
|
|
c26cfa3dcb | ||
|
|
13e59651f1 | ||
|
|
0f4995d1ea | ||
|
|
cff7e8be53 | ||
|
|
5433d7a4ac | ||
|
|
1866db4d50 | ||
|
|
c9b92550b6 | ||
|
|
8e81ebfc1f | ||
|
|
1363205b5d | ||
|
|
ed201cae0e | ||
|
|
a5b10f11ce | ||
|
|
a06644944f | ||
|
|
8eecad3d1d | ||
|
|
412f39e223 | ||
|
|
13106233e4 | ||
|
|
e63fef0e79 | ||
|
|
eed34254f5 | ||
|
|
b38d37a1e1 | ||
|
|
7efaf4ca97 | ||
|
|
ef708792a9 | ||
|
|
bcd74d021b | ||
|
|
0f0a181fe2 | ||
|
|
9678d6c76d | ||
|
|
7ceca70353 | ||
|
|
4412a501eb | ||
|
|
3ca5cf49b6 | ||
|
|
1d4603769f | ||
|
|
9bba8c7a98 | ||
|
|
6f0ef914b9 | ||
|
|
d756e8310a | ||
|
|
74a7ba581c | ||
|
|
a8980bf7c1 | ||
|
|
050d9f440f | ||
|
|
df33a8200f | ||
|
|
d3dab613fc | ||
|
|
1b31739adf | ||
|
|
6280abedd0 | ||
|
|
4c064a4d20 | ||
|
|
8d19a36492 | ||
|
|
78f2685ee9 | ||
|
|
fee7e997ff | ||
|
|
5fa1b99bb6 | ||
|
|
d52b337127 | ||
|
|
342a795ebe | ||
|
|
485eb2e8d5 | ||
|
|
63842264f3 | ||
|
|
e1d984035f | ||
|
|
d5fd0c04fc | ||
|
|
8c5ff4b2fd | ||
|
|
0ad5574115 | ||
|
|
485f5b36ce | ||
|
|
d488a7d130 | ||
|
|
7bc3c95f05 | ||
|
|
a64ac7c1c7 | ||
|
|
9773e6233d | ||
|
|
1fd7a88e2e | ||
|
|
dea27a55a8 | ||
|
|
5f8d7ddd21 | ||
|
|
44452359b9 | ||
|
|
c1ef67df70 | ||
|
|
0c6688d056 | ||
|
|
0b71c21986 | ||
|
|
4d1c2c4939 | ||
|
|
39b2dc8f4f | ||
|
|
e9e940919a | ||
|
|
b6f95866cc | ||
|
|
fa05d19f1b | ||
|
|
981d1da41a | ||
|
|
5ded234131 | ||
|
|
cfeaedd389 | ||
|
|
7779d484a3 | ||
|
|
790a73cd2a | ||
|
|
39e2c5167a | ||
|
|
0f1bec0ad1 | ||
|
|
17c1365368 | ||
|
|
a7841919d9 | ||
|
|
0dbbb3ff37 | ||
|
|
96ff33120e | ||
|
|
f920ffd5f9 | ||
|
|
da1d7e542f | ||
|
|
186208fef9 | ||
|
|
5265079654 | ||
|
|
9ca5cffaee | ||
|
|
b51ce5c155 | ||
|
|
8e9e595799 | ||
|
|
b77898c00d | ||
|
|
58b0125784 | ||
|
|
e8cc91ebc2 | ||
|
|
59486a64d4 | ||
|
|
b1263d4651 | ||
|
|
a79432ffc6 | ||
|
|
480029a678 | ||
|
|
66f3453b3c | ||
|
|
3d4efdb271 | ||
|
|
5ac9184ba6 | ||
|
|
0e6efdb91c | ||
|
|
986e07f21a | ||
|
|
5307da4794 | ||
|
|
0c69e294c3 | ||
|
|
dc931f5669 | ||
|
|
9cfe5c7363 | ||
|
|
454b060d5a | ||
|
|
0808bd67c2 | ||
|
|
3b4bc77532 | ||
|
|
4eaa5c7d46 | ||
|
|
34c6b8d81c | ||
|
|
f79fdf3564 | ||
|
|
16f75bb0c3 | ||
|
|
5053322711 | ||
|
|
d72105b47c | ||
|
|
0f585f892e | ||
|
|
f7c77e72a7 | ||
|
|
fc248e3323 | ||
|
|
d564d9ba36 | ||
|
|
b7883953c4 | ||
|
|
b40b7dc210 | ||
|
|
65b5a149be | ||
|
|
05ed753e58 | ||
|
|
624bfa1f49 | ||
|
|
d9c6f745c6 | ||
|
|
240dc8602b | ||
|
|
489da4a38b | ||
|
|
05b3a8668c | ||
|
|
cb1de1ac19 | ||
|
|
751a985ea7 | ||
|
|
a92cbb5e8b | ||
|
|
e595b58c28 | ||
|
|
a852084b43 | ||
|
|
5260532992 | ||
|
|
ca6853edd6 | ||
|
|
8ae7a1c8d1 | ||
|
|
150fd62bab | ||
|
|
5fce0fea5e | ||
|
|
0b90a613e0 | ||
|
|
4fb453bb39 | ||
|
|
97f6db61aa | ||
|
|
8e975f940c | ||
|
|
3ec1ca58e0 | ||
|
|
ad37490e7d | ||
|
|
334d9f2a76 | ||
|
|
6627c19071 | ||
|
|
0b6361afa5 | ||
|
|
cf457689a6 | ||
|
|
6c49e5a89d | ||
|
|
b79c58d835 | ||
|
|
9f2aea0494 | ||
|
|
ff2081dcf0 | ||
|
|
66db0839a0 | ||
|
|
f8b927c6cd | ||
|
|
8374646e49 | ||
|
|
353c2a7f70 | ||
|
|
21934d9946 | ||
|
|
538de9e456 | ||
|
|
b9d14d4a54 | ||
|
|
529aa83777 | ||
|
|
da10201b88 | ||
|
|
eec76a3faf | ||
|
|
5e4a219182 | ||
|
|
ccb897f9a0 | ||
|
|
f9d872e9cb | ||
|
|
642bbea2a8 | ||
|
|
1c4233d1b4 | ||
|
|
eeb2d55f44 | ||
|
|
96c77fd8b2 | ||
|
|
ed7a5696b7 | ||
|
|
5a3cffa3e9 | ||
|
|
535d97f39e | ||
|
|
f3aea8dac0 | ||
|
|
807fac0409 | ||
|
|
7b2fbdcd51 | ||
|
|
3b8acd4b07 | ||
|
|
62abfdaeb5 | ||
|
|
256a9fdb79 | ||
|
|
4d9aff99bf | ||
|
|
28bb2891bb | ||
|
|
1d8f265744 | ||
|
|
c754caf8c7 | ||
|
|
e1b0046329 | ||
|
|
ddfe177578 | ||
|
|
178558c6d7 | ||
|
|
f4d3a9701f | ||
|
|
42e41a9370 | ||
|
|
a66dcce834 | ||
|
|
21d835cf2b | ||
|
|
29438631be | ||
|
|
0eb4e0ce29 | ||
|
|
9147f9aafe | ||
|
|
13b111e058 | ||
|
|
25c247b14c | ||
|
|
72bea44d8c | ||
|
|
63b9d5fe57 | ||
|
|
30162c535e | ||
|
|
cd8fec3273 | ||
|
|
1e38c7fea8 | ||
|
|
4dfaf238c9 | ||
|
|
4cf36dda54 | ||
|
|
41ce3d3bbf | ||
|
|
429d13772e | ||
|
|
0cbf89193d | ||
|
|
0c3c42bffe | ||
|
|
cdbbd08677 | ||
|
|
4adb1fa5ac | ||
|
|
b3a792ad1e | ||
|
|
98a7095c7a | ||
|
|
701e5ec455 | ||
|
|
8e00e81af4 | ||
|
|
453514c48f | ||
|
|
432ac7afa1 | ||
|
|
c7fca9f2b4 | ||
|
|
715ca5b980 | ||
|
|
9ae62aad26 | ||
|
|
c65df40526 | ||
|
|
d8acc1e4cf | ||
|
|
852aad8126 | ||
|
|
61c072ad20 | ||
|
|
6a5b985113 | ||
|
|
1adc6aec00 | ||
|
|
5edc16ad85 | ||
|
|
f54a8a1ef5 | ||
|
|
c681851ec8 | ||
|
|
d964fcbcde | ||
|
|
e79691aae7 |
@@ -12,7 +12,7 @@ workflows:
|
||||
# Check that `A` activates the features of `B`.
|
||||
"propagate-feature",
|
||||
# These are the features to check:
|
||||
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,js-tracer,portable,keccak-cache-global",
|
||||
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,tracy,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,otlp-logs,js-tracer,portable,keccak-cache-global",
|
||||
# Do not try to add a new section to `[features]` of `A` only because `B` exposes that feature. There are edge-cases where this is still needed, but we can add them manually.
|
||||
"--left-side-feature-missing=ignore",
|
||||
# Ignore the case that `A` it outside of the workspace. Otherwise it will report errors in external dependencies that we have no influence on.
|
||||
|
||||
43
.github/CODEOWNERS
vendored
43
.github/CODEOWNERS
vendored
@@ -1,45 +1,52 @@
|
||||
* @gakonst
|
||||
crates/blockchain-tree-api/ @rakita @mattsse @Rjected
|
||||
crates/blockchain-tree/ @rakita @mattsse @Rjected
|
||||
crates/chain-state/ @fgimenez @mattsse
|
||||
crates/chainspec/ @Rjected @joshieDo @mattsse
|
||||
crates/cli/ @mattsse
|
||||
crates/config/ @shekhirin @mattsse @Rjected
|
||||
crates/consensus/ @mattsse @Rjected
|
||||
crates/e2e-test-utils/ @mattsse @Rjected @klkvr @fgimenez
|
||||
crates/engine/ @mattsse @Rjected @fgimenez @mediocregopher @yongkangc
|
||||
crates/era/ @mattsse @RomanHodulak
|
||||
crates/engine/ @mattsse @Rjected @mediocregopher @yongkangc
|
||||
crates/era/ @mattsse
|
||||
crates/era-downloader/ @mattsse
|
||||
crates/era-utils/ @mattsse
|
||||
crates/errors/ @mattsse
|
||||
crates/ethereum-forks/ @mattsse @Rjected
|
||||
crates/ethereum/ @mattsse @Rjected
|
||||
crates/etl/ @joshieDo @shekhirin
|
||||
crates/evm/ @rakita @mattsse @Rjected
|
||||
crates/evm/ @mattsse @Rjected @klkvr
|
||||
crates/exex/ @shekhirin
|
||||
crates/fs-util/ @mattsse
|
||||
crates/metrics/ @mattsse @Rjected
|
||||
crates/net/ @mattsse @Rjected
|
||||
crates/net/downloaders/ @Rjected
|
||||
crates/node/ @mattsse @Rjected @klkvr
|
||||
crates/optimism/ @mattsse @Rjected @fgimenez
|
||||
crates/optimism/ @mattsse @Rjected
|
||||
crates/payload/ @mattsse @Rjected
|
||||
crates/primitives-traits/ @Rjected @RomanHodulak @mattsse @klkvr
|
||||
crates/primitives-traits/ @Rjected @mattsse @klkvr
|
||||
crates/primitives/ @Rjected @mattsse @klkvr
|
||||
crates/prune/ @shekhirin @joshieDo
|
||||
crates/ress @shekhirin @Rjected
|
||||
crates/revm/ @mattsse @rakita
|
||||
crates/rpc/ @mattsse @Rjected @RomanHodulak
|
||||
crates/ress/ @shekhirin @Rjected
|
||||
crates/revm/ @mattsse
|
||||
crates/rpc/ @mattsse @Rjected
|
||||
crates/stages/ @shekhirin @mediocregopher
|
||||
crates/static-file/ @joshieDo @shekhirin
|
||||
crates/stateless/ @mattsse
|
||||
crates/storage/codecs/ @joshieDo
|
||||
crates/storage/db-api/ @joshieDo @rakita
|
||||
crates/storage/db-api/ @joshieDo
|
||||
crates/storage/db-common/ @Rjected
|
||||
crates/storage/db/ @joshieDo @rakita
|
||||
crates/storage/errors/ @rakita
|
||||
crates/storage/libmdbx-rs/ @rakita @shekhirin
|
||||
crates/storage/db/ @joshieDo
|
||||
crates/storage/errors/ @joshieDo
|
||||
crates/storage/libmdbx-rs/ @shekhirin
|
||||
crates/storage/nippy-jar/ @joshieDo @shekhirin
|
||||
crates/storage/provider/ @rakita @joshieDo @shekhirin
|
||||
crates/storage/provider/ @joshieDo @shekhirin @yongkangc
|
||||
crates/storage/storage-api/ @joshieDo
|
||||
crates/tasks/ @mattsse
|
||||
crates/tokio-util/ @fgimenez
|
||||
crates/tokio-util/ @mattsse
|
||||
crates/tracing/ @mattsse @shekhirin
|
||||
crates/tracing-otlp/ @mattsse @Rjected
|
||||
crates/transaction-pool/ @mattsse @yongkangc
|
||||
crates/trie/ @Rjected @shekhirin @mediocregopher
|
||||
crates/trie/ @Rjected @shekhirin @mediocregopher @yongkangc
|
||||
bin/reth/ @mattsse @shekhirin @Rjected
|
||||
bin/reth-bench/ @mattsse @Rjected @shekhirin @yongkangc
|
||||
bin/reth-bench-compare/ @mediocregopher @shekhirin @yongkangc
|
||||
etc/ @Rjected @shekhirin
|
||||
.github/ @gakonst @DaniPopes
|
||||
|
||||
14
.github/scripts/codspeed-build.sh
vendored
14
.github/scripts/codspeed-build.sh
vendored
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# TODO: Benchmarks run WAY too slow due to excessive amount of iterations.
|
||||
|
||||
cmd=(cargo codspeed build --profile profiling)
|
||||
crates=(
|
||||
-p reth-primitives
|
||||
-p reth-trie
|
||||
-p reth-trie-common
|
||||
-p reth-trie-sparse
|
||||
)
|
||||
|
||||
"${cmd[@]}" --features test-utils "${crates[@]}"
|
||||
14
.github/workflows/bench.yml
vendored
14
.github/workflows/bench.yml
vendored
@@ -17,6 +17,16 @@ name: bench
|
||||
jobs:
|
||||
codspeed:
|
||||
runs-on: depot-ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
partition: [1, 2]
|
||||
total_partitions: [2]
|
||||
include:
|
||||
- partition: 1
|
||||
crates: "-p reth-primitives -p reth-trie-common -p reth-trie-sparse"
|
||||
- partition: 2
|
||||
crates: "-p reth-trie"
|
||||
name: codspeed (${{ matrix.partition }}/${{ matrix.total_partitions }})
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -32,10 +42,10 @@ jobs:
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
- name: Build the benchmark target(s)
|
||||
run: ./.github/scripts/codspeed-build.sh
|
||||
run: cargo codspeed build --profile profiling --features test-utils ${{ matrix.crates }}
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
run: cargo codspeed run --workspace
|
||||
run: cargo codspeed run ${{ matrix.crates }}
|
||||
mode: instrumentation
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
65
.github/workflows/check-alloy.yml
vendored
Normal file
65
.github/workflows/check-alloy.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# Checks reth compilation against alloy branches to detect breaking changes.
|
||||
# Run on-demand via workflow_dispatch.
|
||||
|
||||
name: Check Alloy Breaking Changes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
alloy_branch:
|
||||
description: 'Branch/rev for alloy-rs/alloy (leave empty to skip)'
|
||||
required: false
|
||||
type: string
|
||||
alloy_evm_branch:
|
||||
description: 'Branch/rev for alloy-rs/evm (alloy-evm, alloy-op-evm) (leave empty to skip)'
|
||||
required: false
|
||||
type: string
|
||||
op_alloy_branch:
|
||||
description: 'Branch/rev for alloy-rs/op-alloy (leave empty to skip)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check compilation with patched alloy
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Apply alloy patches
|
||||
run: |
|
||||
ARGS=""
|
||||
if [ -n "${{ inputs.alloy_branch }}" ]; then
|
||||
ARGS="$ARGS --alloy ${{ inputs.alloy_branch }}"
|
||||
fi
|
||||
if [ -n "${{ inputs.alloy_evm_branch }}" ]; then
|
||||
ARGS="$ARGS --evm ${{ inputs.alloy_evm_branch }}"
|
||||
fi
|
||||
if [ -n "${{ inputs.op_alloy_branch }}" ]; then
|
||||
ARGS="$ARGS --op ${{ inputs.op_alloy_branch }}"
|
||||
fi
|
||||
|
||||
if [ -z "$ARGS" ]; then
|
||||
echo "No branches specified, nothing to patch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./scripts/patch-alloy.sh $ARGS
|
||||
|
||||
echo "=== Final patch section ==="
|
||||
tail -50 Cargo.toml
|
||||
|
||||
- name: Check workspace
|
||||
run: cargo clippy --workspace --lib --examples --tests --benches --all-features --locked
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
2
.github/workflows/dependencies.yml
vendored
2
.github/workflows/dependencies.yml
vendored
@@ -15,6 +15,6 @@ permissions:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
uses: ithacaxyz/ci/.github/workflows/cargo-update-pr.yml@main
|
||||
uses: tempoxyz/ci/.github/workflows/cargo-update-pr.yml@main
|
||||
secrets:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
54
.github/workflows/docker-git.yml
vendored
54
.github/workflows/docker-git.yml
vendored
@@ -1,54 +0,0 @@
|
||||
# Publishes the Docker image, only to be used with `workflow_dispatch`. The
|
||||
# images from this workflow will be tagged with the git sha of the branch used
|
||||
# and will NOT tag it as `latest`.
|
||||
|
||||
name: docker-git
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
REPO_NAME: ${{ github.repository_owner }}/reth
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/reth
|
||||
OP_DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and push
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: 'Build and push the git-sha-tagged reth image'
|
||||
command: 'make PROFILE=maxperf GIT_SHA=$GIT_SHA docker-build-push-git-sha'
|
||||
- name: 'Build and push the git-sha-tagged op-reth image'
|
||||
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME GIT_SHA=$GIT_SHA PROFILE=maxperf op-docker-build-push-git-sha'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
61
.github/workflows/docker-nightly.yml
vendored
61
.github/workflows/docker-nightly.yml
vendored
@@ -1,61 +0,0 @@
|
||||
# Publishes the nightly Docker image.
|
||||
|
||||
name: docker-nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
env:
|
||||
REPO_NAME: ${{ github.repository_owner }}/reth
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/reth
|
||||
OP_DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and push
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: 'Build and push the nightly reth image'
|
||||
command: 'make PROFILE=maxperf docker-build-push-nightly'
|
||||
- name: 'Build and push the nightly profiling reth image'
|
||||
command: 'make PROFILE=profiling docker-build-push-nightly-profiling'
|
||||
- name: 'Build and push the nightly op-reth image'
|
||||
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push-nightly'
|
||||
- name: 'Build and push the nightly profiling op-reth image'
|
||||
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=profiling op-docker-build-push-nightly-profiling'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Remove bloatware
|
||||
uses: laverdet/remove-bloatware@v1.0.0
|
||||
with:
|
||||
docker: true
|
||||
lang: rust
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
155
.github/workflows/docker.yml
vendored
155
.github/workflows/docker.yml
vendored
@@ -1,4 +1,9 @@
|
||||
# Publishes the Docker image.
|
||||
# Publishes Docker images.
|
||||
#
|
||||
# Triggers:
|
||||
# - Push tag v*: builds release (RC or latest)
|
||||
# - Schedule: builds nightly + profiling
|
||||
# - Manual: builds git-sha or nightly
|
||||
|
||||
name: docker
|
||||
|
||||
@@ -6,84 +11,94 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/reth
|
||||
OP_IMAGE_NAME: ${{ github.repository_owner }}/op-reth
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/reth
|
||||
OP_DOCKER_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/op-reth
|
||||
DOCKER_USERNAME: ${{ github.actor }}
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_type:
|
||||
description: "Build type"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- git-sha
|
||||
- nightly
|
||||
default: git-sha
|
||||
dry_run:
|
||||
description: "Skip pushing images (dry run)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-rc:
|
||||
if: contains(github.ref, '-rc')
|
||||
name: build and push as release candidate
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: "Build and push reth image"
|
||||
command: "make IMAGE_NAME=$IMAGE_NAME DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME PROFILE=maxperf docker-build-push"
|
||||
- name: "Build and push op-reth image"
|
||||
command: "make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
|
||||
build:
|
||||
if: ${{ !contains(github.ref, '-rc') }}
|
||||
name: build and push as latest
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: "Build and push reth image"
|
||||
command: "make IMAGE_NAME=$IMAGE_NAME DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME PROFILE=maxperf docker-build-push-latest"
|
||||
- name: "Build and push op-reth image"
|
||||
command: "make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push-latest"
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get git info for vergen
|
||||
id: git
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: Log in to Docker
|
||||
echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "describe=$(git describe --always --tags)" >> "$GITHUB_OUTPUT"
|
||||
echo "dirty=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Determine build parameters
|
||||
id: params
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username ${DOCKER_USERNAME} --password-stdin
|
||||
- name: Set up Docker builder
|
||||
run: |
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
|
||||
docker buildx create --use --name cross-builder
|
||||
- name: Build and push ${{ matrix.build.name }}
|
||||
run: ${{ matrix.build.command }}
|
||||
REGISTRY="ghcr.io/${{ github.repository_owner }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "targets=ethereum optimism" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Add 'latest' tag for non-RC releases
|
||||
if [[ ! "$VERSION" =~ -rc ]]; then
|
||||
echo "ethereum_tags=${REGISTRY}/reth:${VERSION},${REGISTRY}/reth:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "optimism_tags=${REGISTRY}/op-reth:${VERSION},${REGISTRY}/op-reth:latest" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ethereum_tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "optimism_tags=${REGISTRY}/op-reth:${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]] || [[ "${{ inputs.build_type }}" == "nightly" ]]; then
|
||||
echo "targets=nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
|
||||
echo "optimism_tags=${REGISTRY}/op-reth:nightly" >> "$GITHUB_OUTPUT"
|
||||
|
||||
else
|
||||
# git-sha build
|
||||
echo "targets=ethereum optimism" >> "$GITHUB_OUTPUT"
|
||||
echo "ethereum_tags=${REGISTRY}/reth:${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "optimism_tags=${REGISTRY}/op-reth:${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build and push images
|
||||
uses: depot/bake-action@v1
|
||||
env:
|
||||
VERGEN_GIT_SHA: ${{ steps.git.outputs.sha }}
|
||||
VERGEN_GIT_DESCRIBE: ${{ steps.git.outputs.describe }}
|
||||
VERGEN_GIT_DIRTY: ${{ steps.git.outputs.dirty }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
files: docker-bake.hcl
|
||||
targets: ${{ steps.params.outputs.targets }}
|
||||
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
|
||||
set: |
|
||||
ethereum.tags=${{ steps.params.outputs.ethereum_tags }}
|
||||
optimism.tags=${{ steps.params.outputs.optimism_tags }}
|
||||
|
||||
28
.github/workflows/hive.yml
vendored
28
.github/workflows/hive.yml
vendored
@@ -15,16 +15,27 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-reth:
|
||||
prepare-reth-stable:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
image_tag: ghcr.io/paradigmxyz/reth:latest
|
||||
binary_name: reth
|
||||
cargo_features: "asm-keccak"
|
||||
artifact_name: "reth-stable"
|
||||
|
||||
prepare-reth-edge:
|
||||
uses: ./.github/workflows/prepare-reth.yml
|
||||
with:
|
||||
image_tag: ghcr.io/paradigmxyz/reth:latest
|
||||
binary_name: reth
|
||||
cargo_features: "asm-keccak edge"
|
||||
artifact_name: "reth-edge"
|
||||
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
runs-on:
|
||||
group: Reth
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout hive tests
|
||||
@@ -76,6 +87,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
storage: [stable, edge]
|
||||
# ethereum/rpc to be deprecated:
|
||||
# https://github.com/ethereum/hive/pull/1117
|
||||
scenario:
|
||||
@@ -85,7 +97,7 @@ jobs:
|
||||
- sim: devp2p
|
||||
limit: discv4
|
||||
# started failing after https://github.com/ethereum/go-ethereum/pull/31843, no
|
||||
# action on our side, remove from here when we get unxpected passes on these tests
|
||||
# action on our side, remove from here when we get unexpected passes on these tests
|
||||
# - sim: devp2p
|
||||
# limit: eth
|
||||
# include:
|
||||
@@ -175,10 +187,12 @@ jobs:
|
||||
- sim: ethereum/eels/consume-rlp
|
||||
limit: .*tests/paris.*
|
||||
needs:
|
||||
- prepare-reth
|
||||
- prepare-reth-stable
|
||||
- prepare-reth-edge
|
||||
- prepare-hive
|
||||
name: run ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
name: ${{ matrix.storage }} / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
runs-on:
|
||||
group: Reth
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
@@ -195,7 +209,7 @@ jobs:
|
||||
- name: Download reth image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: artifacts
|
||||
name: reth-${{ matrix.storage }}
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker images
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -277,7 +277,7 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: taiki-e/cache-cargo-install-action@v2
|
||||
- uses: taiki-e/cache-cargo-install-action@v3
|
||||
with:
|
||||
tool: zepter
|
||||
- name: Eagerly pull dependencies
|
||||
@@ -285,7 +285,7 @@ jobs:
|
||||
- run: zepter run check
|
||||
|
||||
deny:
|
||||
uses: ithacaxyz/ci/.github/workflows/deny.yml@main
|
||||
uses: tempoxyz/ci/.github/workflows/deny.yml@main
|
||||
|
||||
lint-success:
|
||||
name: lint success
|
||||
|
||||
7
.github/workflows/prepare-reth.yml
vendored
7
.github/workflows/prepare-reth.yml
vendored
@@ -21,6 +21,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
description: "Optional cargo package path"
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: "artifacts"
|
||||
description: "Name for the uploaded artifact"
|
||||
|
||||
jobs:
|
||||
prepare-reth:
|
||||
@@ -52,5 +57,5 @@ jobs:
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: artifacts
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ./artifacts
|
||||
|
||||
27
.github/workflows/unit.yml
vendored
27
.github/workflows/unit.yml
vendored
@@ -19,29 +19,22 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test / ${{ matrix.type }} (${{ matrix.partition }}/${{ matrix.total_partitions }})
|
||||
name: test / ${{ matrix.type }} / ${{ matrix.storage }}
|
||||
runs-on: depot-ubuntu-latest-4
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
EDGE_FEATURES: ${{ matrix.storage == 'edge' && 'edge' || '' }}
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ethereum, optimism]
|
||||
storage: [stable, edge]
|
||||
include:
|
||||
- type: ethereum
|
||||
args: --features "asm-keccak ethereum" --locked
|
||||
partition: 1
|
||||
total_partitions: 2
|
||||
- type: ethereum
|
||||
args: --features "asm-keccak ethereum" --locked
|
||||
partition: 2
|
||||
total_partitions: 2
|
||||
features: asm-keccak ethereum
|
||||
exclude_args: ""
|
||||
- type: optimism
|
||||
args: --features "asm-keccak" --locked --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
|
||||
partition: 1
|
||||
total_partitions: 2
|
||||
- type: optimism
|
||||
args: --features "asm-keccak" --locked --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
|
||||
partition: 2
|
||||
total_partitions: 2
|
||||
features: asm-keccak
|
||||
exclude_args: --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -59,9 +52,9 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo nextest run \
|
||||
${{ matrix.args }} --workspace \
|
||||
--features "${{ matrix.features }} $EDGE_FEATURES" --locked \
|
||||
${{ matrix.exclude_args }} --workspace \
|
||||
--exclude ef-tests --no-tests=warn \
|
||||
--partition hash:${{ matrix.partition }}/2 \
|
||||
-E "!kind(test) and not binary(e2e_testsuite)"
|
||||
|
||||
state:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,9 @@ target/
|
||||
# Generated by Intellij-based IDEs.
|
||||
.idea
|
||||
|
||||
# ck-search metadata
|
||||
.ck
|
||||
|
||||
# Generated by MacOS
|
||||
.DS_Store
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Reth is a high-performance Ethereum execution client written in Rust, focusing o
|
||||
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
|
||||
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
|
||||
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
|
||||
9 **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
|
||||
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
@@ -249,7 +249,7 @@ Write comments that remain valuable after the PR is merged. Future readers won't
|
||||
unsafe impl GlobalAlloc for LimitedAllocator { ... }
|
||||
|
||||
// Binary search requires sorted input. Panics on unsorted slices.
|
||||
fn find_index(items: &[Item], target: &Item) -> Option
|
||||
fn find_index(items: &[Item], target: &Item) -> Option<usize>
|
||||
|
||||
// Timeout set to 5s to match EVM block processing limits
|
||||
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
@@ -51,9 +51,7 @@ elsewhere.
|
||||
<!-- - **Asking in the support Telegram:** The [Foundry Support Telegram][support-tg] is a fast and easy way to ask questions. -->
|
||||
<!-- - **Opening a discussion:** This repository comes with a discussions board where you can also ask for help. Click the "Discussions" tab at the top. -->
|
||||
|
||||
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help by *
|
||||
*opening a discussion**. This repository comes with a discussions board where you can also ask for help. Click the "
|
||||
Discussions" tab at the top.
|
||||
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help by **opening a discussion**. This repository comes with a discussions board where you can also ask for help. Click the "Discussions" tab at the top.
|
||||
|
||||
As Reth is still in heavy development, the documentation can be a bit scattered. The [Reth Docs][reth-docs] is our
|
||||
current best-effort attempt at keeping up-to-date information.
|
||||
|
||||
2038
Cargo.lock
generated
2038
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
112
Cargo.toml
112
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace.package]
|
||||
version = "1.9.3"
|
||||
version = "1.10.2"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -473,22 +473,22 @@ reth-ress-protocol = { path = "crates/ress/protocol" }
|
||||
reth-ress-provider = { path = "crates/ress/provider" }
|
||||
|
||||
# revm
|
||||
revm = { version = "33.1.0", default-features = false }
|
||||
revm-bytecode = { version = "7.1.1", default-features = false }
|
||||
revm-database = { version = "9.0.5", default-features = false }
|
||||
revm-state = { version = "8.1.1", default-features = false }
|
||||
revm-primitives = { version = "21.0.2", default-features = false }
|
||||
revm-interpreter = { version = "31.1.0", default-features = false }
|
||||
revm-database-interface = { version = "8.0.5", default-features = false }
|
||||
op-revm = { version = "14.1.0", default-features = false }
|
||||
revm-inspectors = "0.33.2"
|
||||
revm = { version = "34.0.0", default-features = false }
|
||||
revm-bytecode = { version = "8.0.0", default-features = false }
|
||||
revm-database = { version = "10.0.0", default-features = false }
|
||||
revm-state = { version = "9.0.0", default-features = false }
|
||||
revm-primitives = { version = "22.0.0", default-features = false }
|
||||
revm-interpreter = { version = "32.0.0", default-features = false }
|
||||
revm-database-interface = { version = "9.0.0", default-features = false }
|
||||
op-revm = { version = "15.0.0", default-features = false }
|
||||
revm-inspectors = "0.34.1"
|
||||
|
||||
# eth
|
||||
alloy-chains = { version = "0.2.5", default-features = false }
|
||||
alloy-dyn-abi = "1.4.1"
|
||||
alloy-dyn-abi = "1.5.2"
|
||||
alloy-eip2124 = { version = "0.2.0", default-features = false }
|
||||
alloy-eip7928 = { version = "0.1.0" }
|
||||
alloy-evm = { version = "0.25.1", default-features = false }
|
||||
alloy-eip7928 = { version = "0.3.0", 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.1.3", default-features = false }
|
||||
alloy-contract = { version = "1.1.3", default-features = false }
|
||||
alloy-eips = { version = "1.1.3", default-features = false }
|
||||
alloy-genesis = { version = "1.1.3", default-features = false }
|
||||
alloy-json-rpc = { version = "1.1.3", default-features = false }
|
||||
alloy-network = { version = "1.1.3", default-features = false }
|
||||
alloy-network-primitives = { version = "1.1.3", default-features = false }
|
||||
alloy-provider = { version = "1.1.3", features = ["reqwest", "debug-api"], default-features = false }
|
||||
alloy-pubsub = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-client = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types = { version = "1.1.3", features = ["eth"], default-features = false }
|
||||
alloy-rpc-types-admin = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-anvil = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-beacon = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-debug = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-engine = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-mev = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-trace = { version = "1.1.3", default-features = false }
|
||||
alloy-rpc-types-txpool = { version = "1.1.3", default-features = false }
|
||||
alloy-serde = { version = "1.1.3", default-features = false }
|
||||
alloy-signer = { version = "1.1.3", default-features = false }
|
||||
alloy-signer-local = { version = "1.1.3", default-features = false }
|
||||
alloy-transport = { version = "1.1.3" }
|
||||
alloy-transport-http = { version = "1.1.3", features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-transport-ipc = { version = "1.1.3", default-features = false }
|
||||
alloy-transport-ws = { version = "1.1.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.25.0", 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 }
|
||||
@@ -548,12 +548,14 @@ bytes = { version = "1.5", default-features = false }
|
||||
brotli = "8"
|
||||
cfg-if = "1.0"
|
||||
clap = "4"
|
||||
color-eyre = "0.6"
|
||||
dashmap = "6.0"
|
||||
derive_more = { version = "2", default-features = false, features = ["full"] }
|
||||
dirs-next = "2.0.0"
|
||||
dyn-clone = "1.0.17"
|
||||
eyre = "0.6"
|
||||
fdlimit = "0.3.0"
|
||||
fixed-map = { version = "0.9", default-features = false }
|
||||
humantime = "2.1"
|
||||
humantime-serde = "1.1"
|
||||
itertools = { version = "0.14", default-features = false }
|
||||
@@ -586,7 +588,8 @@ tracing-appender = "0.2"
|
||||
url = { version = "2.3", default-features = false }
|
||||
zstd = "0.13"
|
||||
byteorder = "1"
|
||||
mini-moka = "0.10"
|
||||
fixed-cache = { version = "0.1.7", features = ["stats"] }
|
||||
moka = "0.12"
|
||||
tar-no-std = { version = "0.3.2", default-features = false }
|
||||
miniz_oxide = { version = "0.8.4", default-features = false }
|
||||
chrono = "0.4.41"
|
||||
@@ -594,9 +597,9 @@ chrono = "0.4.41"
|
||||
# metrics
|
||||
metrics = "0.24.0"
|
||||
metrics-derive = "0.1"
|
||||
metrics-exporter-prometheus = { version = "0.16.0", default-features = false }
|
||||
metrics-exporter-prometheus = { version = "0.18.0", default-features = false }
|
||||
metrics-process = "2.1.0"
|
||||
metrics-util = { default-features = false, version = "0.19.0" }
|
||||
metrics-util = { default-features = false, version = "0.20.0" }
|
||||
|
||||
# proc-macros
|
||||
proc-macro2 = "1.0"
|
||||
@@ -662,6 +665,7 @@ opentelemetry_sdk = "0.31"
|
||||
opentelemetry = "0.31"
|
||||
opentelemetry-otlp = "0.31"
|
||||
opentelemetry-semantic-conventions = "0.31"
|
||||
opentelemetry-appender-tracing = "0.31"
|
||||
tracing-opentelemetry = "0.32"
|
||||
|
||||
# misc-testing
|
||||
@@ -682,6 +686,7 @@ ethereum_ssz = "0.9.0"
|
||||
ethereum_ssz_derive = "0.9.0"
|
||||
|
||||
# allocators
|
||||
jemalloc_pprof = { version = "0.8", default-features = false }
|
||||
tikv-jemalloc-ctl = "0.6"
|
||||
tikv-jemallocator = "0.6"
|
||||
tracy-client = "0.18.0"
|
||||
@@ -692,7 +697,7 @@ ahash = "0.8"
|
||||
anyhow = "1.0"
|
||||
bindgen = { version = "0.71", default-features = false }
|
||||
block-padding = "0.3.2"
|
||||
cc = "=1.2.15"
|
||||
cc = "1.2.15"
|
||||
cipher = "0.4.3"
|
||||
comfy-table = "7.0"
|
||||
concat-kdf = "0.1.0"
|
||||
@@ -729,18 +734,20 @@ socket2 = { version = "0.5", default-features = false }
|
||||
sysinfo = { version = "0.33", default-features = false }
|
||||
tracing-journald = "0.3"
|
||||
tracing-logfmt = "0.3.3"
|
||||
tracing-samply = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false }
|
||||
tracing-tracy = "0.11"
|
||||
triehash = "0.8"
|
||||
typenum = "1.15.0"
|
||||
vergen = "9.0.4"
|
||||
vergen = "9.1.0"
|
||||
visibility = "0.1.1"
|
||||
walkdir = "2.3.3"
|
||||
vergen-git2 = "1.0.5"
|
||||
vergen-git2 = "9.1.0"
|
||||
|
||||
# networking
|
||||
ipnet = "2.11"
|
||||
|
||||
# [patch.crates-io]
|
||||
[patch.crates-io]
|
||||
# alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
# alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
|
||||
@@ -783,5 +790,10 @@ 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" }
|
||||
|
||||
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
|
||||
|
||||
@@ -18,7 +18,7 @@ FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
|
||||
# Build profile, release by default
|
||||
ARG BUILD_PROFILE=release
|
||||
ARG BUILD_PROFILE=maxperf
|
||||
ENV BUILD_PROFILE=$BUILD_PROFILE
|
||||
|
||||
# Extra Cargo flags
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# This image is meant to enable cross-architecture builds.
|
||||
# It assumes the reth binary has already been compiled for `$TARGETPLATFORM` and is
|
||||
# locatable in `./dist/bin/$TARGETARCH`
|
||||
FROM --platform=$TARGETPLATFORM ubuntu:22.04
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
# Filled by docker buildx
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY ./dist/bin/$TARGETARCH/reth /usr/local/bin/reth
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546
|
||||
ENTRYPOINT ["/usr/local/bin/reth"]
|
||||
99
Dockerfile.depot
Normal file
99
Dockerfile.depot
Normal file
@@ -0,0 +1,99 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Unified Dockerfile for reth and op-reth, optimized for Depot builds
|
||||
# Usage:
|
||||
# reth: --build-arg BINARY=reth
|
||||
# op-reth: --build-arg BINARY=op-reth --build-arg MANIFEST_PATH=crates/optimism/bin
|
||||
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
RUN apt-get update && apt-get install -y libclang-dev pkg-config
|
||||
|
||||
# Install sccache for compilation caching
|
||||
RUN cargo install sccache --locked
|
||||
ENV RUSTC_WRAPPER=sccache
|
||||
ENV SCCACHE_DIR=/sccache
|
||||
ENV SCCACHE_WEBDAV_ENDPOINT=https://cache.depot.dev
|
||||
|
||||
# Builds a cargo-chef plan
|
||||
FROM chef AS planner
|
||||
COPY --exclude=.git . .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
|
||||
# Binary to build (reth or op-reth)
|
||||
ARG BINARY=reth
|
||||
|
||||
# Manifest path for the binary
|
||||
ARG MANIFEST_PATH=bin/reth
|
||||
|
||||
# Build profile, release by default
|
||||
ARG BUILD_PROFILE=release
|
||||
ENV BUILD_PROFILE=$BUILD_PROFILE
|
||||
|
||||
# Extra Cargo flags
|
||||
ARG RUSTFLAGS=""
|
||||
ENV RUSTFLAGS="$RUSTFLAGS"
|
||||
|
||||
# Extra Cargo features
|
||||
ARG FEATURES=""
|
||||
ENV FEATURES=$FEATURES
|
||||
|
||||
# Git info for vergen (since .git is excluded from Docker context)
|
||||
ARG VERGEN_GIT_SHA=""
|
||||
ARG VERGEN_GIT_DESCRIBE=""
|
||||
ARG VERGEN_GIT_DIRTY="false"
|
||||
ENV VERGEN_GIT_SHA=$VERGEN_GIT_SHA
|
||||
ENV VERGEN_GIT_DESCRIBE=$VERGEN_GIT_DESCRIBE
|
||||
ENV VERGEN_GIT_DIRTY=$VERGEN_GIT_DIRTY
|
||||
|
||||
# Build dependencies
|
||||
RUN --mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \
|
||||
--mount=type=cache,target=/usr/local/cargo/git,sharing=shared \
|
||||
--mount=type=cache,target=$SCCACHE_DIR,sharing=shared \
|
||||
cargo chef cook --profile $BUILD_PROFILE --features "$FEATURES" --locked --recipe-path recipe.json --manifest-path $MANIFEST_PATH/Cargo.toml
|
||||
|
||||
# Build application
|
||||
COPY --exclude=.git . .
|
||||
RUN --mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \
|
||||
--mount=type=cache,target=/usr/local/cargo/git,sharing=shared \
|
||||
--mount=type=cache,target=$SCCACHE_DIR,sharing=shared \
|
||||
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml
|
||||
|
||||
RUN sccache --show-stats || true
|
||||
|
||||
# Copy binary to a known location (ARG not resolved in COPY)
|
||||
# Note: Custom profiles like maxperf/profiling output to target/<profile>/, not target/release/
|
||||
RUN cp /app/target/$BUILD_PROFILE/$BINARY /app/binary || \
|
||||
cp /app/target/release/$BINARY /app/binary
|
||||
|
||||
FROM ubuntu:24.04 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Binary name for entrypoint
|
||||
ARG BINARY=reth
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy binary from build stage and create canonical symlink for entrypoint
|
||||
COPY --from=builder /app/binary /usr/local/bin/
|
||||
RUN mv /usr/local/bin/binary /usr/local/bin/$BINARY && \
|
||||
ln -s /usr/local/bin/$BINARY /usr/local/bin/reth-binary && \
|
||||
chmod +x /usr/local/bin/$BINARY
|
||||
|
||||
# Copy licenses
|
||||
COPY LICENSE-* ./
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546
|
||||
ENTRYPOINT ["/usr/local/bin/reth-binary"]
|
||||
@@ -14,7 +14,7 @@ RUN cargo chef prepare --recipe-path recipe.json
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
|
||||
ARG BUILD_PROFILE=release
|
||||
ARG BUILD_PROFILE=maxperf
|
||||
ENV BUILD_PROFILE=$BUILD_PROFILE
|
||||
|
||||
ARG RUSTFLAGS=""
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# This image is meant to enable cross-architecture builds.
|
||||
# It assumes the reth binary has already been compiled for `$TARGETPLATFORM` and is
|
||||
# locatable in `./dist/bin/$TARGETARCH`
|
||||
FROM --platform=$TARGETPLATFORM ubuntu:22.04
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/paradigmxyz/reth
|
||||
LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
|
||||
# Filled by docker buildx
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY ./dist/bin/$TARGETARCH/op-reth /usr/local/bin/op-reth
|
||||
|
||||
EXPOSE 30303 30303/udp 9001 8545 8546
|
||||
ENTRYPOINT ["/usr/local/bin/op-reth"]
|
||||
@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022-2025 Reth Contributors
|
||||
Copyright 2022-2026 Reth Contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022-2025 Reth Contributors
|
||||
Copyright (c) 2022-2026 Reth Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
124
Makefile
124
Makefile
@@ -35,9 +35,6 @@ EEST_TESTS_TAG := v4.5.0
|
||||
EEST_TESTS_URL := https://github.com/ethereum/execution-spec-tests/releases/download/$(EEST_TESTS_TAG)/fixtures_stable.tar.gz
|
||||
EEST_TESTS_DIR := ./testing/ef-tests/execution-spec-tests
|
||||
|
||||
# The docker image name
|
||||
DOCKER_IMAGE_NAME ?= ghcr.io/paradigmxyz/reth
|
||||
|
||||
##@ Help
|
||||
|
||||
.PHONY: help
|
||||
@@ -242,127 +239,6 @@ install-reth-bench: ## Build and install the reth binary under `$(CARGO_HOME)/bi
|
||||
--features "$(FEATURES)" \
|
||||
--profile "$(PROFILE)"
|
||||
|
||||
##@ Docker
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: docker-build-push
|
||||
docker-build-push: ## Build and push a cross-arch Docker image tagged with the latest git tag.
|
||||
$(call docker_build_push,$(GIT_TAG),$(GIT_TAG))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: docker-build-push-git-sha
|
||||
docker-build-push-git-sha: ## Build and push a cross-arch Docker image tagged with the latest git sha.
|
||||
$(call docker_build_push,$(GIT_SHA),$(GIT_SHA))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: docker-build-push-latest
|
||||
docker-build-push-latest: ## Build and push a cross-arch Docker image tagged with the latest git tag and `latest`.
|
||||
$(call docker_build_push,$(GIT_TAG),latest)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: docker-build-push-nightly
|
||||
docker-build-push-nightly: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
|
||||
$(call docker_build_push,nightly,nightly)
|
||||
|
||||
# Create a cross-arch Docker image with the given tags and push it
|
||||
define docker_build_push
|
||||
$(MAKE) build-x86_64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/amd64
|
||||
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/reth $(BIN_DIR)/amd64/reth
|
||||
|
||||
$(MAKE) build-aarch64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/arm64
|
||||
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/reth $(BIN_DIR)/arm64/reth
|
||||
|
||||
docker buildx build --file ./Dockerfile.cross . \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(1) \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(2) \
|
||||
--provenance=false \
|
||||
--push
|
||||
endef
|
||||
|
||||
##@ Optimism docker
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: op-docker-build-push
|
||||
op-docker-build-push: ## Build and push a cross-arch Docker image tagged with the latest git tag.
|
||||
$(call op_docker_build_push,$(GIT_TAG),$(GIT_TAG))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: op-docker-build-push-git-sha
|
||||
op-docker-build-push-git-sha: ## Build and push a cross-arch Docker image tagged with the latest git sha.
|
||||
$(call op_docker_build_push,$(GIT_SHA),$(GIT_SHA))
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --driver docker-container --name cross-builder`
|
||||
.PHONY: op-docker-build-push-latest
|
||||
op-docker-build-push-latest: ## Build and push a cross-arch Docker image tagged with the latest git tag and `latest`.
|
||||
$(call op_docker_build_push,$(GIT_TAG),latest)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: op-docker-build-push-nightly
|
||||
op-docker-build-push-nightly: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
|
||||
$(call op_docker_build_push,nightly,nightly)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: docker-build-push-nightly-profiling
|
||||
docker-build-push-nightly-profiling: ## Build and push cross-arch Docker image with profiling profile tagged with nightly-profiling.
|
||||
$(call docker_build_push,nightly-profiling,nightly-profiling)
|
||||
|
||||
# Note: This requires a buildx builder with emulation support. For example:
|
||||
#
|
||||
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
|
||||
# `docker buildx create --use --name cross-builder`
|
||||
.PHONY: op-docker-build-push-nightly-profiling
|
||||
op-docker-build-push-nightly-profiling: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
|
||||
$(call op_docker_build_push,nightly-profiling,nightly-profiling)
|
||||
|
||||
|
||||
# Create a cross-arch Docker image with the given tags and push it
|
||||
define op_docker_build_push
|
||||
$(MAKE) op-build-x86_64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/amd64
|
||||
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/op-reth $(BIN_DIR)/amd64/op-reth
|
||||
|
||||
$(MAKE) op-build-aarch64-unknown-linux-gnu
|
||||
mkdir -p $(BIN_DIR)/arm64
|
||||
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/op-reth $(BIN_DIR)/arm64/op-reth
|
||||
|
||||
docker buildx build --file ./DockerfileOp.cross . \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(1) \
|
||||
--tag $(DOCKER_IMAGE_NAME):$(2) \
|
||||
--provenance=false \
|
||||
--push
|
||||
endef
|
||||
|
||||
##@ Other
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
@@ -44,7 +44,7 @@ More historical context below:
|
||||
- We released 1.0 "production-ready" stable Reth in June 2024.
|
||||
- Reth completed an audit with [Sigma Prime](https://sigmaprime.io/), the developers of [Lighthouse](https://github.com/sigp/lighthouse), the Rust Consensus Layer implementation. Find it [here](./audit/sigma_prime_audit_v2.pdf).
|
||||
- Revm (the EVM used in Reth) underwent an audit with [Guido Vranken](https://x.com/guidovranken) (#1 [Ethereum Bug Bounty](https://ethereum.org/en/bug-bounty)). We will publish the results soon.
|
||||
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024,the last beta release.
|
||||
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024, the last beta release.
|
||||
- We released [beta](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) on Monday March 4, 2024, our first breaking change to the database model, providing faster query speed, smaller database footprint, and allowing "history" to be mounted on separate drives.
|
||||
- We shipped iterative improvements until the last alpha release on February 28, 2024, [0.1.0-alpha.21](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.21).
|
||||
- We [initially announced](https://www.paradigm.xyz/2023/06/reth-alpha) [0.1.0-alpha.1](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.1) on June 20, 2023.
|
||||
|
||||
@@ -25,7 +25,9 @@ reth-chainspec.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-provider = { workspace = true, features = ["reqwest-rustls-tls"], default-features = false }
|
||||
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
|
||||
alloy-rpc-types-eth.workspace = true
|
||||
alloy-transport-ws.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
|
||||
# CLI and argument parsing
|
||||
@@ -69,7 +71,11 @@ jemalloc = [
|
||||
"reth-node-core/jemalloc",
|
||||
]
|
||||
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
|
||||
tracy = [
|
||||
"reth-node-core/tracy",
|
||||
"reth-tracing/tracy",
|
||||
]
|
||||
|
||||
min-error-logs = [
|
||||
"tracing/release_max_level_error",
|
||||
|
||||
50
bin/reth-bench-compare/README.md
Normal file
50
bin/reth-bench-compare/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# reth-bench-compare
|
||||
|
||||
Compare reth performance between two git references.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
reth-bench-compare \
|
||||
--baseline-ref main \
|
||||
--feature-ref my-feature \
|
||||
--blocks 100 \
|
||||
--wait-for-persistence
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `--baseline-ref <REF>` | Git reference for baseline | - | Yes |
|
||||
| `--feature-ref <REF>` | Git reference to compare | - | Yes |
|
||||
| `--blocks <N>` | Number of blocks to benchmark | `100` | No |
|
||||
| `--chain <CHAIN>` | Chain to benchmark | `mainnet` | No |
|
||||
| `--datadir <PATH>` | Data directory path | OS-specific | No |
|
||||
| `--rpc-url <URL>` | RPC endpoint for block data | Chain default | No |
|
||||
| `--output-dir <PATH>` | Output directory | `./reth-bench-compare` | No |
|
||||
| `--wait-for-persistence` | Wait for block persistence | `false` | No |
|
||||
| `--persistence-threshold <N>` | Wait after every N+1 blocks | `2` | No |
|
||||
| `--wait-time <DURATION>` | Fixed delay (legacy) | - | No |
|
||||
| `--warmup-blocks <N>` | Cache warmup blocks | Same as `--blocks` | No |
|
||||
| `--draw` | Generate charts (needs Python/uv) | `false` | No |
|
||||
| `--profile` | Enable CPU profiling (needs samply) | `false` | No |
|
||||
| `-vvvv` | Debug logging | Info | No |
|
||||
| `--features <FEATURES>` | Rust features for both builds | `jemalloc,asm-keccak` | No |
|
||||
| `--rustflags <FLAGS>` | RUSTFLAGS for both builds | `-C target-cpu=native` | No |
|
||||
| `--baseline-features <FEATURES>` | Features for baseline only | Inherits `--features` | No |
|
||||
| `--feature-features <FEATURES>` | Features for feature only | Inherits `--features` | No |
|
||||
| `--baseline-rustflags <FLAGS>` | RUSTFLAGS for baseline only | Inherits `--rustflags` | No |
|
||||
| `--feature-rustflags <FLAGS>` | RUSTFLAGS for feature only | Inherits `--rustflags` | No |
|
||||
| `--baseline-args <ARGS>` | Extra args for baseline node | - | No |
|
||||
| `--feature-args <ARGS>` | Extra args for feature node | - | No |
|
||||
| `--metrics-port <PORT>` | Metrics endpoint port | `5005` | No |
|
||||
| `--sudo` | Run with elevated privileges | `false` | No |
|
||||
|
||||
## Output
|
||||
|
||||
Results in `./reth-bench-compare/results/<timestamp>/`:
|
||||
- `comparison_report.json` - Metrics comparison
|
||||
- `per_block_comparison.csv` - Per-block statistics
|
||||
- `baseline/` and `feature/` - Individual run results
|
||||
- `latency_comparison.png` - Chart (if `--draw` used)
|
||||
@@ -18,6 +18,8 @@ pub(crate) struct BenchmarkRunner {
|
||||
rpc_url: String,
|
||||
jwt_secret: String,
|
||||
wait_time: Option<String>,
|
||||
wait_for_persistence: bool,
|
||||
persistence_threshold: Option<u64>,
|
||||
warmup_blocks: u64,
|
||||
}
|
||||
|
||||
@@ -28,6 +30,8 @@ impl BenchmarkRunner {
|
||||
rpc_url: args.get_rpc_url(),
|
||||
jwt_secret: args.jwt_secret_path().to_string_lossy().to_string(),
|
||||
wait_time: args.wait_time.clone(),
|
||||
wait_for_persistence: args.wait_for_persistence,
|
||||
persistence_threshold: args.persistence_threshold,
|
||||
warmup_blocks: args.get_warmup_blocks(),
|
||||
}
|
||||
}
|
||||
@@ -96,13 +100,9 @@ impl BenchmarkRunner {
|
||||
&from_block.to_string(),
|
||||
"--to",
|
||||
&to_block.to_string(),
|
||||
"--wait-time=0ms", // Warmup should avoid persistence waits.
|
||||
]);
|
||||
|
||||
// Add wait-time argument if provided
|
||||
if let Some(ref wait_time) = self.wait_time {
|
||||
cmd.args(["--wait-time", wait_time]);
|
||||
}
|
||||
|
||||
cmd.env("RUST_LOG_STYLE", "never")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
@@ -186,9 +186,16 @@ impl BenchmarkRunner {
|
||||
&output_dir.to_string_lossy(),
|
||||
]);
|
||||
|
||||
// Add wait-time argument if provided
|
||||
// Configure wait mode: wait-time takes precedence over persistence-based flow
|
||||
if let Some(ref wait_time) = self.wait_time {
|
||||
cmd.args(["--wait-time", wait_time]);
|
||||
} else if self.wait_for_persistence {
|
||||
cmd.arg("--wait-for-persistence");
|
||||
|
||||
// Add persistence threshold if specified
|
||||
if let Some(threshold) = self.persistence_threshold {
|
||||
cmd.args(["--persistence-threshold", &threshold.to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.env("RUST_LOG_STYLE", "never")
|
||||
|
||||
@@ -114,10 +114,29 @@ pub(crate) struct Args {
|
||||
#[arg(long)]
|
||||
pub profile: bool,
|
||||
|
||||
/// Wait time between engine API calls (passed to reth-bench)
|
||||
#[arg(long, value_name = "DURATION")]
|
||||
/// Optional fixed delay between engine API calls (passed to reth-bench).
|
||||
///
|
||||
/// When set, reth-bench uses wait-time mode and disables persistence-based flow.
|
||||
/// This flag remains for compatibility with older scripts.
|
||||
#[arg(long, value_name = "DURATION", hide = true)]
|
||||
pub wait_time: Option<String>,
|
||||
|
||||
/// Wait for blocks to be persisted before sending the next batch (passed to reth-bench).
|
||||
///
|
||||
/// When enabled, waits for every Nth block to be persisted using the
|
||||
/// `reth_subscribePersistedBlock` subscription. This ensures the benchmark
|
||||
/// doesn't outpace persistence.
|
||||
#[arg(long)]
|
||||
pub wait_for_persistence: bool,
|
||||
|
||||
/// Engine persistence threshold (passed to reth-bench).
|
||||
///
|
||||
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
|
||||
/// matches the engine's default persistence threshold (2), so waits occur
|
||||
/// at blocks 3, 6, 9, etc.
|
||||
#[arg(long, value_name = "PERSISTENCE_THRESHOLD")]
|
||||
pub persistence_threshold: Option<u64>,
|
||||
|
||||
/// Number of blocks to run for cache warmup after clearing caches.
|
||||
/// If not specified, defaults to the same as --blocks
|
||||
#[arg(long, value_name = "N")]
|
||||
@@ -128,6 +147,11 @@ pub(crate) struct Args {
|
||||
#[arg(long)]
|
||||
pub no_clear_cache: bool,
|
||||
|
||||
/// Skip waiting for the node to sync before starting benchmarks.
|
||||
/// When enabled, assumes the node is already synced and skips the initial tip check.
|
||||
#[arg(long)]
|
||||
pub skip_wait_syncing: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub logs: LogArgs,
|
||||
|
||||
@@ -164,12 +188,42 @@ pub(crate) struct Args {
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
pub reth_args: Vec<String>,
|
||||
|
||||
/// Comma-separated list of features to enable during reth compilation
|
||||
/// Comma-separated list of features to enable during reth compilation (applied to both builds)
|
||||
///
|
||||
/// Example: `jemalloc,asm-keccak`
|
||||
#[arg(long, value_name = "FEATURES", default_value = "jemalloc,asm-keccak")]
|
||||
pub features: String,
|
||||
|
||||
/// Comma-separated list of features to enable only for baseline build (overrides --features)
|
||||
///
|
||||
/// Example: `--baseline-features jemalloc`
|
||||
#[arg(long, value_name = "FEATURES")]
|
||||
pub baseline_features: Option<String>,
|
||||
|
||||
/// Comma-separated list of features to enable only for feature build (overrides --features)
|
||||
///
|
||||
/// Example: `--feature-features jemalloc,asm-keccak`
|
||||
#[arg(long, value_name = "FEATURES")]
|
||||
pub feature_features: Option<String>,
|
||||
|
||||
/// RUSTFLAGS to use for both baseline and feature builds
|
||||
///
|
||||
/// Example: `--rustflags "-C target-cpu=native"`
|
||||
#[arg(long, value_name = "FLAGS", default_value = "-C target-cpu=native")]
|
||||
pub rustflags: String,
|
||||
|
||||
/// RUSTFLAGS to use only for baseline build (overrides --rustflags)
|
||||
///
|
||||
/// Example: `--baseline-rustflags "-C target-cpu=native -C lto"`
|
||||
#[arg(long, value_name = "FLAGS")]
|
||||
pub baseline_rustflags: Option<String>,
|
||||
|
||||
/// RUSTFLAGS to use only for feature build (overrides --rustflags)
|
||||
///
|
||||
/// Example: `--feature-rustflags "-C target-cpu=native -C lto"`
|
||||
#[arg(long, value_name = "FLAGS")]
|
||||
pub feature_rustflags: Option<String>,
|
||||
|
||||
/// Disable automatic --debug.startup-sync-state-idle flag for specific runs.
|
||||
/// Can be "baseline", "feature", or "all".
|
||||
/// By default, the flag is passed to warmup, baseline, and feature runs.
|
||||
@@ -328,7 +382,6 @@ pub(crate) async fn run_comparison(args: Args, _ctx: CliContext) -> Result<()> {
|
||||
git_manager.repo_root().to_string(),
|
||||
output_dir.clone(),
|
||||
git_manager.clone(),
|
||||
args.features.clone(),
|
||||
)?;
|
||||
// Initialize node manager
|
||||
let mut node_manager = NodeManager::new(&args);
|
||||
@@ -448,6 +501,18 @@ async fn run_compilation_phase(
|
||||
let ref_type = ref_types[i];
|
||||
let commit = &ref_commits[git_ref];
|
||||
|
||||
// Get per-build features and rustflags
|
||||
let features = match ref_type {
|
||||
"baseline" => args.baseline_features.as_ref().unwrap_or(&args.features),
|
||||
"feature" => args.feature_features.as_ref().unwrap_or(&args.features),
|
||||
_ => &args.features,
|
||||
};
|
||||
let rustflags = match ref_type {
|
||||
"baseline" => args.baseline_rustflags.as_ref().unwrap_or(&args.rustflags),
|
||||
"feature" => args.feature_rustflags.as_ref().unwrap_or(&args.rustflags),
|
||||
_ => &args.rustflags,
|
||||
};
|
||||
|
||||
info!(
|
||||
"Compiling {} binary for reference: {} (commit: {})",
|
||||
ref_type,
|
||||
@@ -459,7 +524,7 @@ async fn run_compilation_phase(
|
||||
git_manager.switch_ref(git_ref)?;
|
||||
|
||||
// Compile reth (with caching)
|
||||
compilation_manager.compile_reth(commit, is_optimism)?;
|
||||
compilation_manager.compile_reth(commit, is_optimism, features, rustflags)?;
|
||||
|
||||
info!("Completed compilation for {} reference", ref_type);
|
||||
}
|
||||
@@ -471,6 +536,7 @@ async fn run_compilation_phase(
|
||||
Ok((baseline_commit, feature_commit))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Run warmup phase to warm up caches before benchmarking
|
||||
async fn run_warmup_phase(
|
||||
git_manager: &GitManager,
|
||||
@@ -480,9 +546,15 @@ async fn run_warmup_phase(
|
||||
args: &Args,
|
||||
is_optimism: bool,
|
||||
baseline_commit: &str,
|
||||
starting_tip: u64,
|
||||
) -> Result<()> {
|
||||
info!("=== Running warmup phase ===");
|
||||
|
||||
// Unwind to starting block minus warmup blocks, so we end up back at starting_tip
|
||||
let warmup_blocks = args.get_warmup_blocks();
|
||||
let unwind_target = starting_tip.saturating_sub(warmup_blocks);
|
||||
node_manager.unwind_to_block(unwind_target).await?;
|
||||
|
||||
// Use baseline for warmup
|
||||
let warmup_ref = &args.baseline_ref;
|
||||
|
||||
@@ -511,12 +583,13 @@ async fn run_warmup_phase(
|
||||
node_manager.start_node(&binary_path, warmup_ref, "warmup", &additional_args).await?;
|
||||
|
||||
// Wait for node to be ready and get its current tip
|
||||
let current_tip = node_manager.wait_for_node_ready_and_get_tip().await?;
|
||||
let current_tip = if args.skip_wait_syncing {
|
||||
node_manager.wait_for_rpc_and_get_tip(&mut node_process).await?
|
||||
} else {
|
||||
node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?
|
||||
};
|
||||
info!("Warmup node is ready at tip: {}", current_tip);
|
||||
|
||||
// Store the tip we'll unwind back to
|
||||
let original_tip = current_tip;
|
||||
|
||||
// Clear filesystem caches before warmup run only (unless disabled)
|
||||
if args.no_clear_cache {
|
||||
info!("Skipping filesystem cache clearing (--no-clear-cache flag set)");
|
||||
@@ -527,12 +600,9 @@ async fn run_warmup_phase(
|
||||
// Run warmup to warm up caches
|
||||
benchmark_runner.run_warmup(current_tip).await?;
|
||||
|
||||
// Stop node before unwinding (node must be stopped to release database lock)
|
||||
// Stop node after warmup
|
||||
node_manager.stop_node(&mut node_process).await?;
|
||||
|
||||
// Unwind back to starting block after warmup
|
||||
node_manager.unwind_to_block(original_tip).await?;
|
||||
|
||||
info!("Warmup phase completed");
|
||||
Ok(())
|
||||
}
|
||||
@@ -554,6 +624,31 @@ async fn run_benchmark_workflow(
|
||||
let (baseline_commit, feature_commit) =
|
||||
run_compilation_phase(git_manager, compilation_manager, args, is_optimism).await?;
|
||||
|
||||
// Switch to baseline reference and get the starting tip
|
||||
git_manager.switch_ref(&args.baseline_ref)?;
|
||||
let binary_path =
|
||||
compilation_manager.get_cached_binary_path_for_commit(&baseline_commit, is_optimism);
|
||||
if !binary_path.exists() {
|
||||
return Err(eyre!(
|
||||
"Cached baseline binary not found at {:?}. Compilation phase should have created it.",
|
||||
binary_path
|
||||
));
|
||||
}
|
||||
|
||||
// Start node briefly to get the current tip, then stop it
|
||||
info!("=== Determining initial block height ===");
|
||||
let additional_args = args.build_additional_args("baseline", args.baseline_args.as_ref());
|
||||
let (mut node_process, _) = node_manager
|
||||
.start_node(&binary_path, &args.baseline_ref, "baseline", &additional_args)
|
||||
.await?;
|
||||
let starting_tip = if args.skip_wait_syncing {
|
||||
node_manager.wait_for_rpc_and_get_tip(&mut node_process).await?
|
||||
} else {
|
||||
node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?
|
||||
};
|
||||
info!("Node starting tip: {}", starting_tip);
|
||||
node_manager.stop_node(&mut node_process).await?;
|
||||
|
||||
// Run warmup phase before benchmarking (skip if warmup_blocks is 0)
|
||||
if args.get_warmup_blocks() > 0 {
|
||||
run_warmup_phase(
|
||||
@@ -564,6 +659,7 @@ async fn run_benchmark_workflow(
|
||||
args,
|
||||
is_optimism,
|
||||
&baseline_commit,
|
||||
starting_tip,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
@@ -579,6 +675,10 @@ async fn run_benchmark_workflow(
|
||||
let commit = commits[i];
|
||||
info!("=== Processing {} reference: {} ===", ref_type, git_ref);
|
||||
|
||||
// Unwind to starting block minus benchmark blocks, so we end up back at starting_tip
|
||||
let unwind_target = starting_tip.saturating_sub(args.blocks);
|
||||
node_manager.unwind_to_block(unwind_target).await?;
|
||||
|
||||
// Switch to target reference
|
||||
git_manager.switch_ref(git_ref)?;
|
||||
|
||||
@@ -612,17 +712,18 @@ async fn run_benchmark_workflow(
|
||||
node_manager.start_node(&binary_path, git_ref, ref_type, &additional_args).await?;
|
||||
|
||||
// Wait for node to be ready and get its current tip (wherever it is)
|
||||
let current_tip = node_manager.wait_for_node_ready_and_get_tip().await?;
|
||||
let current_tip = if args.skip_wait_syncing {
|
||||
node_manager.wait_for_rpc_and_get_tip(&mut node_process).await?
|
||||
} else {
|
||||
node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?
|
||||
};
|
||||
info!("Node is ready at tip: {}", current_tip);
|
||||
|
||||
// Store the tip we'll unwind back to
|
||||
let original_tip = current_tip;
|
||||
|
||||
// Calculate benchmark range
|
||||
// Note: reth-bench has an off-by-one error where it consumes the first block
|
||||
// of the range, so we add 1 to compensate and get exactly args.blocks blocks
|
||||
let from_block = original_tip;
|
||||
let to_block = original_tip + args.blocks;
|
||||
let from_block = current_tip;
|
||||
let to_block = current_tip + args.blocks;
|
||||
|
||||
// Run benchmark
|
||||
let output_dir = comparison_generator.get_ref_output_dir(ref_type);
|
||||
@@ -639,9 +740,6 @@ async fn run_benchmark_workflow(
|
||||
// Stop node
|
||||
node_manager.stop_node(&mut node_process).await?;
|
||||
|
||||
// Unwind back to original tip
|
||||
node_manager.unwind_to_block(original_tip).await?;
|
||||
|
||||
// Store results for comparison
|
||||
comparison_generator.add_ref_results(ref_type, &output_dir)?;
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ pub(crate) struct BenchmarkResults {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct CombinedLatencyRow {
|
||||
pub block_number: u64,
|
||||
pub transaction_count: u64,
|
||||
#[serde(default)]
|
||||
pub transaction_count: Option<u64>,
|
||||
pub gas_used: u64,
|
||||
pub new_payload_latency: u128,
|
||||
}
|
||||
@@ -48,7 +49,8 @@ pub(crate) struct CombinedLatencyRow {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct TotalGasRow {
|
||||
pub block_number: u64,
|
||||
pub transaction_count: u64,
|
||||
#[serde(default)]
|
||||
pub transaction_count: Option<u64>,
|
||||
pub gas_used: u64,
|
||||
pub time: u128,
|
||||
}
|
||||
@@ -97,6 +99,7 @@ pub(crate) struct RefInfo {
|
||||
/// Summary of the comparison between references.
|
||||
///
|
||||
/// Percent deltas are `(feature - baseline) / baseline * 100`:
|
||||
/// - `new_payload_latency_mean_change_percent`: percent changes of the per-block means.
|
||||
/// - `new_payload_latency_p50_change_percent` / p90 / p99: percent changes of the respective
|
||||
/// per-block percentiles.
|
||||
/// - `per_block_latency_change_mean_percent` / `per_block_latency_change_median_percent` are the
|
||||
@@ -114,6 +117,7 @@ pub(crate) struct ComparisonSummary {
|
||||
pub per_block_latency_change_median_percent: f64,
|
||||
pub per_block_latency_change_std_dev_percent: f64,
|
||||
pub new_payload_total_latency_change_percent: f64,
|
||||
pub new_payload_latency_mean_change_percent: f64,
|
||||
pub new_payload_latency_p50_change_percent: f64,
|
||||
pub new_payload_latency_p90_change_percent: f64,
|
||||
pub new_payload_latency_p99_change_percent: f64,
|
||||
@@ -125,7 +129,8 @@ pub(crate) struct ComparisonSummary {
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct BlockComparison {
|
||||
pub block_number: u64,
|
||||
pub transaction_count: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transaction_count: Option<u64>,
|
||||
pub gas_used: u64,
|
||||
pub baseline_new_payload_latency: u128,
|
||||
pub feature_new_payload_latency: u128,
|
||||
@@ -442,6 +447,10 @@ impl ComparisonGenerator {
|
||||
per_block_latency_change_median_percent,
|
||||
per_block_latency_change_std_dev_percent,
|
||||
new_payload_total_latency_change_percent,
|
||||
new_payload_latency_mean_change_percent: calc_percent_change(
|
||||
baseline.mean_new_payload_latency_ms,
|
||||
feature.mean_new_payload_latency_ms,
|
||||
),
|
||||
new_payload_latency_p50_change_percent: calc_percent_change(
|
||||
baseline.median_new_payload_latency_ms,
|
||||
feature.median_new_payload_latency_ms,
|
||||
@@ -572,6 +581,10 @@ impl ComparisonGenerator {
|
||||
" Total newPayload time change: {:+.2}%",
|
||||
summary.new_payload_total_latency_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency mean: {:+.2}%",
|
||||
summary.new_payload_latency_mean_change_percent
|
||||
);
|
||||
println!(
|
||||
" NewPayload Latency p50: {:+.2}%",
|
||||
summary.new_payload_latency_p50_change_percent
|
||||
|
||||
@@ -13,7 +13,6 @@ pub(crate) struct CompilationManager {
|
||||
repo_root: String,
|
||||
output_dir: PathBuf,
|
||||
git_manager: GitManager,
|
||||
features: String,
|
||||
}
|
||||
|
||||
impl CompilationManager {
|
||||
@@ -22,9 +21,8 @@ impl CompilationManager {
|
||||
repo_root: String,
|
||||
output_dir: PathBuf,
|
||||
git_manager: GitManager,
|
||||
features: String,
|
||||
) -> Result<Self> {
|
||||
Ok(Self { repo_root, output_dir, git_manager, features })
|
||||
Ok(Self { repo_root, output_dir, git_manager })
|
||||
}
|
||||
|
||||
/// Detect if the RPC endpoint is an Optimism chain
|
||||
@@ -68,7 +66,13 @@ impl CompilationManager {
|
||||
}
|
||||
|
||||
/// Compile reth using cargo build and cache the binary
|
||||
pub(crate) fn compile_reth(&self, commit: &str, is_optimism: bool) -> Result<()> {
|
||||
pub(crate) fn compile_reth(
|
||||
&self,
|
||||
commit: &str,
|
||||
is_optimism: bool,
|
||||
features: &str,
|
||||
rustflags: &str,
|
||||
) -> Result<()> {
|
||||
// Validate that current git commit matches the expected commit
|
||||
let current_commit = self.git_manager.get_current_commit()?;
|
||||
if current_commit != commit {
|
||||
@@ -100,9 +104,8 @@ impl CompilationManager {
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg("build").arg("--profile").arg("profiling");
|
||||
|
||||
// Add features
|
||||
cmd.arg("--features").arg(&self.features);
|
||||
info!("Using features: {}", self.features);
|
||||
cmd.arg("--features").arg(features);
|
||||
info!("Using features: {features}");
|
||||
|
||||
// Add bin-specific arguments for optimism
|
||||
if is_optimism {
|
||||
@@ -114,11 +117,11 @@ impl CompilationManager {
|
||||
|
||||
cmd.current_dir(&self.repo_root);
|
||||
|
||||
// Set RUSTFLAGS for native CPU optimization
|
||||
cmd.env("RUSTFLAGS", "-C target-cpu=native");
|
||||
// Set RUSTFLAGS
|
||||
cmd.env("RUSTFLAGS", rustflags);
|
||||
info!("Using RUSTFLAGS: {rustflags}");
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing cargo command: {:?}", cmd);
|
||||
info!("Compiling {binary_name} with {cmd:?}");
|
||||
|
||||
let output = cmd.output().wrap_err("Failed to execute cargo build command")?;
|
||||
|
||||
@@ -227,8 +230,7 @@ impl CompilationManager {
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(["install", "--locked", "samply"]);
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing cargo command: {:?}", cmd);
|
||||
info!("Installing samply with {cmd:?}");
|
||||
|
||||
let output = cmd.output().wrap_err("Failed to execute cargo install samply command")?;
|
||||
|
||||
@@ -303,8 +305,7 @@ impl CompilationManager {
|
||||
let mut cmd = Command::new("make");
|
||||
cmd.arg("install-reth-bench").current_dir(&self.repo_root);
|
||||
|
||||
// Debug log the command
|
||||
debug!("Executing make command: {:?}", cmd);
|
||||
info!("Compiling reth-bench with {cmd:?}");
|
||||
|
||||
let output = cmd.output().wrap_err("Failed to execute make install-reth-bench command")?;
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
use crate::cli::Args;
|
||||
use alloy_provider::{Provider, ProviderBuilder};
|
||||
use alloy_rpc_client::RpcClient;
|
||||
use alloy_rpc_types_eth::SyncStatus;
|
||||
use alloy_transport_ws::WsConnect;
|
||||
use eyre::{eyre, OptionExt, Result, WrapErr};
|
||||
#[cfg(unix)]
|
||||
use nix::sys::signal::{killpg, Signal};
|
||||
@@ -18,6 +20,9 @@ use tokio::{
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Default websocket RPC port used by reth
|
||||
const DEFAULT_WS_RPC_PORT: u16 = 8546;
|
||||
|
||||
/// Manages reth node lifecycle and operations
|
||||
pub(crate) struct NodeManager {
|
||||
datadir: Option<String>,
|
||||
@@ -152,9 +157,13 @@ impl NodeManager {
|
||||
metrics_arg,
|
||||
"--http".to_string(),
|
||||
"--http.api".to_string(),
|
||||
"eth".to_string(),
|
||||
"eth,reth".to_string(),
|
||||
"--ws".to_string(),
|
||||
"--ws.api".to_string(),
|
||||
"eth,reth".to_string(),
|
||||
"--disable-discovery".to_string(),
|
||||
"--trusted-only".to_string(),
|
||||
"--disable-tx-gossip".to_string(),
|
||||
]);
|
||||
|
||||
// Add tracing arguments if OTLP endpoint is configured
|
||||
@@ -211,6 +220,11 @@ impl NodeManager {
|
||||
cmd.arg("--");
|
||||
cmd.args(reth_args);
|
||||
|
||||
// Enable tracing-samply
|
||||
if supports_samply_flags(&reth_args[0]) {
|
||||
cmd.arg("--log.samply");
|
||||
}
|
||||
|
||||
// Set environment variable to disable log styling
|
||||
cmd.env("RUST_LOG_STYLE", "never");
|
||||
|
||||
@@ -354,8 +368,13 @@ impl NodeManager {
|
||||
Ok((child, reth_command))
|
||||
}
|
||||
|
||||
/// Wait for the node to be ready and return its current tip
|
||||
pub(crate) async fn wait_for_node_ready_and_get_tip(&self) -> Result<u64> {
|
||||
/// Wait for the node to be ready and return its current tip.
|
||||
///
|
||||
/// Fails early if the node process exits before becoming ready.
|
||||
pub(crate) async fn wait_for_node_ready_and_get_tip(
|
||||
&self,
|
||||
child: &mut tokio::process::Child,
|
||||
) -> Result<u64> {
|
||||
info!("Waiting for node to be ready and synced...");
|
||||
|
||||
let max_wait = Duration::from_secs(120); // 2 minutes to allow for sync
|
||||
@@ -366,8 +385,23 @@ impl NodeManager {
|
||||
let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
|
||||
let provider = ProviderBuilder::new().connect_http(url);
|
||||
|
||||
let start_time = tokio::time::Instant::now();
|
||||
let mut iteration = 0;
|
||||
|
||||
timeout(max_wait, async {
|
||||
loop {
|
||||
iteration += 1;
|
||||
debug!(
|
||||
"Readiness check iteration {} (elapsed: {:?})",
|
||||
iteration,
|
||||
start_time.elapsed()
|
||||
);
|
||||
|
||||
// Check if the node process has exited.
|
||||
if let Some(status) = child.try_wait()? {
|
||||
return Err(eyre!("Node process exited unexpectedly with {status}"));
|
||||
}
|
||||
|
||||
// First check if RPC is up and node is not syncing
|
||||
match provider.syncing().await {
|
||||
Ok(sync_result) => {
|
||||
@@ -376,24 +410,48 @@ impl NodeManager {
|
||||
debug!("Node is still syncing {sync_info:?}, waiting...");
|
||||
}
|
||||
_ => {
|
||||
debug!("HTTP RPC is up and node is not syncing, checking block number...");
|
||||
// Node is not syncing, now get the tip
|
||||
match provider.get_block_number().await {
|
||||
Ok(tip) => {
|
||||
info!("Node is ready and not syncing at block: {}", tip);
|
||||
return Ok(tip);
|
||||
debug!("HTTP RPC ready at block: {}, checking WebSocket...", tip);
|
||||
|
||||
// Verify WebSocket RPC is ready (public endpoint, no JWT required)
|
||||
let ws_url = format!("ws://localhost:{}", DEFAULT_WS_RPC_PORT);
|
||||
debug!("Attempting WebSocket connection to {} (public endpoint)", ws_url);
|
||||
let ws_connect = WsConnect::new(&ws_url);
|
||||
|
||||
match RpcClient::connect_pubsub(ws_connect).await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Node is ready (HTTP and WebSocket) at block: {} (took {:?}, {} iterations)",
|
||||
tip, start_time.elapsed(), iteration
|
||||
);
|
||||
return Ok(tip);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"HTTP RPC ready but WebSocket not ready yet (iteration {}): {:?}",
|
||||
iteration, e
|
||||
);
|
||||
debug!("WebSocket error details: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to get block number: {}", e);
|
||||
debug!("Failed to get block number (iteration {}): {:?}", iteration, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Node RPC not ready yet or failed to check sync status: {}", e);
|
||||
debug!("Node RPC not ready yet or failed to check sync status (iteration {}): {:?}", iteration, e);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Sleeping for {:?} before next check", check_interval);
|
||||
sleep(check_interval).await;
|
||||
}
|
||||
})
|
||||
@@ -401,9 +459,79 @@ impl NodeManager {
|
||||
.wrap_err("Timed out waiting for node to be ready and synced")?
|
||||
}
|
||||
|
||||
/// Wait for the node RPC to be ready and return its current tip, without waiting for sync.
|
||||
///
|
||||
/// This is faster than `wait_for_node_ready_and_get_tip` but may return a tip while
|
||||
/// the node is still syncing.
|
||||
pub(crate) async fn wait_for_rpc_and_get_tip(
|
||||
&self,
|
||||
child: &mut tokio::process::Child,
|
||||
) -> Result<u64> {
|
||||
info!("Waiting for node RPC to be ready (skipping sync wait)...");
|
||||
|
||||
let max_wait = Duration::from_secs(60);
|
||||
let check_interval = Duration::from_secs(2);
|
||||
let rpc_url = "http://localhost:8545";
|
||||
|
||||
let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
|
||||
let provider = ProviderBuilder::new().connect_http(url);
|
||||
|
||||
let start_time = tokio::time::Instant::now();
|
||||
let mut iteration = 0;
|
||||
|
||||
timeout(max_wait, async {
|
||||
loop {
|
||||
iteration += 1;
|
||||
debug!(
|
||||
"RPC readiness check iteration {} (elapsed: {:?})",
|
||||
iteration,
|
||||
start_time.elapsed()
|
||||
);
|
||||
|
||||
if let Some(status) = child.try_wait()? {
|
||||
return Err(eyre!("Node process exited unexpectedly with {status}"));
|
||||
}
|
||||
|
||||
match provider.get_block_number().await {
|
||||
Ok(tip) => {
|
||||
debug!("HTTP RPC ready at block: {}, checking WebSocket...", tip);
|
||||
|
||||
let ws_url = format!("ws://localhost:{}", DEFAULT_WS_RPC_PORT);
|
||||
let ws_connect = WsConnect::new(&ws_url);
|
||||
|
||||
match RpcClient::connect_pubsub(ws_connect).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Node RPC is ready at block: {} (took {:?}, {} iterations)",
|
||||
tip,
|
||||
start_time.elapsed(),
|
||||
iteration
|
||||
);
|
||||
return Ok(tip);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"HTTP RPC ready but WebSocket not ready yet (iteration {}): {:?}",
|
||||
iteration, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("RPC not ready yet (iteration {}): {:?}", iteration, e);
|
||||
}
|
||||
}
|
||||
|
||||
sleep(check_interval).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.wrap_err("Timed out waiting for node RPC to be ready")?
|
||||
}
|
||||
|
||||
/// Stop the reth node gracefully
|
||||
pub(crate) async fn stop_node(&self, child: &mut tokio::process::Child) -> Result<()> {
|
||||
let pid = child.id().expect("Child process ID should be available");
|
||||
let pid = child.id().ok_or_eyre("Child process ID should be available")?;
|
||||
|
||||
// Check if the process has already exited
|
||||
match child.try_wait() {
|
||||
@@ -552,3 +680,16 @@ impl NodeManager {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_samply_flags(bin: &str) -> bool {
|
||||
let mut cmd = std::process::Command::new(bin);
|
||||
// NOTE: The flag to check must come before --help.
|
||||
// We pass --help as a shortcut to not execute any command.
|
||||
cmd.args(["--log.samply", "--help"]);
|
||||
debug!(?cmd, "Checking samply flags support");
|
||||
let Ok(output) = cmd.output() else {
|
||||
return false;
|
||||
};
|
||||
debug!(?output, "Samply flags support check");
|
||||
output.status.success()
|
||||
}
|
||||
|
||||
@@ -16,20 +16,27 @@ workspace = true
|
||||
# reth
|
||||
reth-cli-runner.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-engine-primitives.workspace = true
|
||||
reth-ethereum-primitives.workspace = true
|
||||
reth-fs-util.workspace = true
|
||||
reth-node-api.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-rpc-api.workspace = true
|
||||
|
||||
reth-tracing.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-eips.workspace = true
|
||||
alloy-json-rpc.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
alloy-provider = { workspace = true, features = ["engine-api", "reqwest-rustls-tls"], default-features = false }
|
||||
alloy-consensus.workspace = true
|
||||
alloy-network.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
|
||||
alloy-rpc-types-engine.workspace = true
|
||||
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
|
||||
alloy-rpc-types-engine = { workspace = true, features = ["kzg"] }
|
||||
alloy-transport-http.workspace = true
|
||||
alloy-transport-ipc.workspace = true
|
||||
alloy-transport-ws.workspace = true
|
||||
@@ -50,6 +57,9 @@ tracing.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
# url parsing
|
||||
url.workspace = true
|
||||
|
||||
# async
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -58,6 +68,7 @@ tokio = { workspace = true, features = ["sync", "macros", "time", "rt-multi-thre
|
||||
# misc
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
eyre.workspace = true
|
||||
color-eyre.workspace = true
|
||||
thiserror.workspace = true
|
||||
humantime.workspace = true
|
||||
|
||||
@@ -79,7 +90,11 @@ jemalloc = [
|
||||
"reth-node-core/jemalloc",
|
||||
]
|
||||
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator"]
|
||||
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
|
||||
tracy = [
|
||||
"reth-node-core/tracy",
|
||||
"reth-tracing/tracy",
|
||||
]
|
||||
|
||||
min-error-logs = [
|
||||
"tracing/release_max_level_error",
|
||||
|
||||
@@ -31,6 +31,14 @@ Otherwise, running `make maxperf` at the root of the repo should be sufficient f
|
||||
`reth-bench` contains different commands to benchmark different patterns of engine API calls.
|
||||
The `reth-bench new-payload-fcu` command is the most representative of ethereum mainnet live sync, alternating between sending `engine_newPayload` calls and `engine_forkchoiceUpdated` calls.
|
||||
|
||||
The `new-payload-fcu` command supports two optional waiting modes that can be used together or independently:
|
||||
- `--wait-time <duration>`: Fixed sleep interval between blocks (e.g., `--wait-time 100ms`)
|
||||
- `--wait-for-persistence`: Waits for blocks to be persisted using the `reth_subscribePersistedBlock` subscription
|
||||
|
||||
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold (2). This can be customized with `--persistence-threshold <N>`.
|
||||
|
||||
By default, the WebSocket URL for persistence subscriptions is derived from `--engine-rpc-url` (converting to ws:// on port 8546). Use `--ws-rpc-url` to override this.
|
||||
|
||||
Below is an overview of how to run a benchmark:
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -163,7 +163,7 @@ impl AuthenticatedTransport {
|
||||
|
||||
// shift the iat forward by one second so there is some buffer time
|
||||
let mut shifted_claims = inner_and_claims.1;
|
||||
shifted_claims.iat -= 1;
|
||||
shifted_claims.iat -= 30;
|
||||
|
||||
// if the claims are out of date, reset the inner transport
|
||||
if !shifted_claims.is_within_time_window() {
|
||||
|
||||
@@ -103,14 +103,20 @@ impl BenchContext {
|
||||
(bench_args.from, bench_args.to)
|
||||
};
|
||||
|
||||
// If neither `--from` nor `--to` are provided, we will run the benchmark continuously,
|
||||
// If `--to` are not provided, we will run the benchmark continuously,
|
||||
// starting at the latest block.
|
||||
let mut benchmark_mode = BenchMode::new(from, to)?;
|
||||
let latest_block = block_provider
|
||||
.get_block_by_number(BlockNumberOrTag::Latest)
|
||||
.full()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block from RPC"))?;
|
||||
let mut benchmark_mode = BenchMode::new(from, to, latest_block.into_inner().number())?;
|
||||
|
||||
let first_block = match benchmark_mode {
|
||||
BenchMode::Continuous => {
|
||||
// fetch Latest block
|
||||
block_provider.get_block_by_number(BlockNumberOrTag::Latest).full().await?.unwrap()
|
||||
BenchMode::Continuous(start) => {
|
||||
block_provider.get_block_by_number(start.into()).full().await?.ok_or_else(|| {
|
||||
eyre::eyre!("Failed to fetch block {} from RPC for continuous mode", start)
|
||||
})?
|
||||
}
|
||||
BenchMode::Range(ref mut range) => {
|
||||
match range.next() {
|
||||
@@ -120,7 +126,9 @@ impl BenchContext {
|
||||
.get_block_by_number(block_number.into())
|
||||
.full()
|
||||
.await?
|
||||
.unwrap()
|
||||
.ok_or_else(|| {
|
||||
eyre::eyre!("Failed to fetch block {} from RPC", block_number)
|
||||
})?
|
||||
}
|
||||
None => {
|
||||
return Err(eyre::eyre!(
|
||||
|
||||
216
bin/reth-bench/src/bench/gas_limit_ramp.rs
Normal file
216
bin/reth-bench/src/bench/gas_limit_ramp.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
//! Benchmarks empty block processing by ramping the block gas limit.
|
||||
|
||||
use crate::{
|
||||
authenticated_transport::AuthenticatedTransportConnect,
|
||||
bench::{
|
||||
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},
|
||||
};
|
||||
use alloy_eips::BlockNumberOrTag;
|
||||
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::{ExecutionPayload, ForkchoiceState, JwtSecret};
|
||||
|
||||
use clap::Parser;
|
||||
use reqwest::Url;
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_ethereum_primitives::TransactionSigned;
|
||||
use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK};
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
use tracing::info;
|
||||
|
||||
/// `reth benchmark gas-limit-ramp` command.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// Number of blocks to generate. Mutually exclusive with --target-gas-limit.
|
||||
#[arg(long, value_name = "BLOCKS", conflicts_with = "target_gas_limit")]
|
||||
blocks: Option<u64>,
|
||||
|
||||
/// Target gas limit to ramp up to. The benchmark will generate blocks until the gas limit
|
||||
/// reaches or exceeds this value. Mutually exclusive with --blocks.
|
||||
/// Accepts short notation: K for thousand, M for million, G for billion (e.g., 2G = 2
|
||||
/// billion).
|
||||
#[arg(long, value_name = "TARGET_GAS_LIMIT", conflicts_with = "blocks", value_parser = parse_gas_limit)]
|
||||
target_gas_limit: Option<u64>,
|
||||
|
||||
/// The Engine API RPC URL.
|
||||
#[arg(long = "engine-rpc-url", value_name = "ENGINE_RPC_URL")]
|
||||
engine_rpc_url: String,
|
||||
|
||||
/// Path to the JWT secret for Engine API authentication.
|
||||
#[arg(long = "jwt-secret", value_name = "JWT_SECRET")]
|
||||
jwt_secret: PathBuf,
|
||||
|
||||
/// Output directory for benchmark results and generated payloads.
|
||||
#[arg(long, value_name = "OUTPUT")]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
/// Mode for determining when to stop ramping.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RampMode {
|
||||
/// Ramp for a fixed number of blocks.
|
||||
Blocks(u64),
|
||||
/// Ramp until reaching or exceeding target gas limit.
|
||||
TargetGasLimit(u64),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute `benchmark gas-limit-ramp` command.
|
||||
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
|
||||
let mode = match (self.blocks, self.target_gas_limit) {
|
||||
(Some(blocks), None) => {
|
||||
if blocks == 0 {
|
||||
return Err(eyre::eyre!("--blocks must be greater than 0"));
|
||||
}
|
||||
RampMode::Blocks(blocks)
|
||||
}
|
||||
(None, Some(target)) => {
|
||||
if target == 0 {
|
||||
return Err(eyre::eyre!("--target-gas-limit must be greater than 0"));
|
||||
}
|
||||
RampMode::TargetGasLimit(target)
|
||||
}
|
||||
_ => {
|
||||
return Err(eyre::eyre!(
|
||||
"Exactly one of --blocks or --target-gas-limit must be specified"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure output directory exists
|
||||
if self.output.is_file() {
|
||||
return Err(eyre::eyre!("Output path must be a directory"));
|
||||
}
|
||||
if !self.output.exists() {
|
||||
std::fs::create_dir_all(&self.output)?;
|
||||
info!("Created output directory: {:?}", self.output);
|
||||
}
|
||||
|
||||
// Set up authenticated provider (used for both Engine API and eth_ methods)
|
||||
let jwt = std::fs::read_to_string(&self.jwt_secret)?;
|
||||
let jwt = JwtSecret::from_hex(jwt)?;
|
||||
let auth_url = Url::parse(&self.engine_rpc_url)?;
|
||||
|
||||
info!("Connecting to Engine RPC at {}", auth_url);
|
||||
let auth_transport = AuthenticatedTransportConnect::new(auth_url, jwt);
|
||||
let client = ClientBuilder::default().connect_with(auth_transport).await?;
|
||||
let provider = RootProvider::<AnyNetwork>::new(client);
|
||||
|
||||
// Get chain spec - required for fork detection
|
||||
let chain_id = provider.get_chain_id().await?;
|
||||
let chain_spec = ChainSpec::from_chain_id(chain_id)
|
||||
.ok_or_else(|| eyre::eyre!("Unsupported chain id: {chain_id}"))?;
|
||||
|
||||
// Fetch the current head block as parent
|
||||
let parent_block = provider
|
||||
.get_block_by_number(BlockNumberOrTag::Latest)
|
||||
.full()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?;
|
||||
|
||||
let (mut parent_header, mut parent_hash) = rpc_block_to_header(parent_block);
|
||||
|
||||
let canonical_parent = parent_header.number;
|
||||
let start_block = canonical_parent + 1;
|
||||
|
||||
match mode {
|
||||
RampMode::Blocks(blocks) => {
|
||||
info!(
|
||||
canonical_parent,
|
||||
start_block,
|
||||
end_block = start_block + blocks - 1,
|
||||
"Starting gas limit ramp benchmark (block count mode)"
|
||||
);
|
||||
}
|
||||
RampMode::TargetGasLimit(target) => {
|
||||
info!(
|
||||
canonical_parent,
|
||||
start_block,
|
||||
current_gas_limit = parent_header.gas_limit,
|
||||
target_gas_limit = target,
|
||||
"Starting gas limit ramp benchmark (target gas limit mode)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut blocks_processed = 0u64;
|
||||
let total_benchmark_duration = Instant::now();
|
||||
|
||||
while !should_stop(mode, blocks_processed, parent_header.gas_limit) {
|
||||
let timestamp = parent_header.timestamp.saturating_add(1);
|
||||
|
||||
let request = prepare_payload_request(&chain_spec, timestamp, parent_hash);
|
||||
let new_payload_version = request.new_payload_version;
|
||||
|
||||
let (payload, sidecar) = build_payload(&provider, request).await?;
|
||||
|
||||
let mut block =
|
||||
payload.clone().try_into_block_with_sidecar::<TransactionSigned>(&sidecar)?;
|
||||
|
||||
let max_increase = max_gas_limit_increase(parent_header.gas_limit);
|
||||
let gas_limit =
|
||||
parent_header.gas_limit.saturating_add(max_increase).min(MAXIMUM_GAS_LIMIT_BLOCK);
|
||||
|
||||
block.header.gas_limit = gas_limit;
|
||||
|
||||
let block_hash = block.header.hash_slow();
|
||||
// Regenerate the payload from the modified block, but keep the original sidecar
|
||||
// which contains the actual execution requests data (not just the hash)
|
||||
let (payload, _) = ExecutionPayload::from_block_unchecked(block_hash, &block);
|
||||
let (version, params) = payload_to_new_payload(
|
||||
payload,
|
||||
sidecar,
|
||||
false,
|
||||
block.header.withdrawals_root,
|
||||
Some(new_payload_version),
|
||||
)?;
|
||||
|
||||
// Save payload to file with version info for replay
|
||||
let payload_path =
|
||||
self.output.join(format!("payload_block_{}.json", block.header.number));
|
||||
let file =
|
||||
GasRampPayloadFile { version: version as u8, block_hash, params: params.clone() };
|
||||
let payload_json = serde_json::to_string_pretty(&file)?;
|
||||
std::fs::write(&payload_path, &payload_json)?;
|
||||
info!(block_number = block.header.number, path = %payload_path.display(), "Saved payload");
|
||||
|
||||
call_new_payload(&provider, version, params).await?;
|
||||
|
||||
let forkchoice_state = ForkchoiceState {
|
||||
head_block_hash: block_hash,
|
||||
safe_block_hash: block_hash,
|
||||
finalized_block_hash: block_hash,
|
||||
};
|
||||
call_forkchoice_updated(&provider, version, forkchoice_state, None).await?;
|
||||
|
||||
parent_header = block.header;
|
||||
parent_hash = block_hash;
|
||||
blocks_processed += 1;
|
||||
}
|
||||
|
||||
let final_gas_limit = parent_header.gas_limit;
|
||||
info!(
|
||||
total_duration=?total_benchmark_duration.elapsed(),
|
||||
blocks_processed,
|
||||
final_gas_limit,
|
||||
"Benchmark complete"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const fn max_gas_limit_increase(parent_gas_limit: u64) -> u64 {
|
||||
(parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR).saturating_sub(1)
|
||||
}
|
||||
|
||||
const fn should_stop(mode: RampMode, blocks_processed: u64, current_gas_limit: u64) -> bool {
|
||||
match mode {
|
||||
RampMode::Blocks(target_blocks) => blocks_processed >= target_blocks,
|
||||
RampMode::TargetGasLimit(target) => current_gas_limit >= target,
|
||||
}
|
||||
}
|
||||
632
bin/reth-bench/src/bench/generate_big_block.rs
Normal file
632
bin/reth-bench/src/bench/generate_big_block.rs
Normal file
@@ -0,0 +1,632 @@
|
||||
//! Command for generating large blocks by packing transactions from real blocks.
|
||||
//!
|
||||
//! 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, bench::helpers::parse_gas_limit,
|
||||
};
|
||||
use alloy_eips::{BlockNumberOrTag, Typed2718};
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use alloy_provider::{ext::EngineApi, network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::{
|
||||
ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ForkchoiceState, JwtSecret,
|
||||
PayloadAttributes,
|
||||
};
|
||||
use alloy_transport::layers::RetryBackoffLayer;
|
||||
use clap::Parser;
|
||||
use eyre::Context;
|
||||
use reqwest::Url;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_rpc_api::TestingBuildBlockRequestV1;
|
||||
use std::future::Future;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// A single transaction with its gas used and raw encoded bytes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawTransaction {
|
||||
/// The actual gas used by the transaction (from receipt).
|
||||
pub gas_used: u64,
|
||||
/// The transaction type (e.g., 3 for EIP-4844 blob txs).
|
||||
pub tx_type: u8,
|
||||
/// The raw RLP-encoded transaction bytes.
|
||||
pub raw: Bytes,
|
||||
}
|
||||
|
||||
/// Abstraction over sources of transactions for big block generation.
|
||||
///
|
||||
/// Implementors provide transactions from different sources (RPC, database, files, etc.)
|
||||
pub trait TransactionSource {
|
||||
/// Fetch transactions from a specific block number.
|
||||
///
|
||||
/// Returns `Ok(None)` if the block doesn't exist.
|
||||
/// Returns `Ok(Some((transactions, gas_used)))` with the block's transactions and total gas.
|
||||
fn fetch_block_transactions(
|
||||
&self,
|
||||
block_number: u64,
|
||||
) -> impl Future<Output = eyre::Result<Option<(Vec<RawTransaction>, u64)>>> + Send;
|
||||
}
|
||||
|
||||
/// RPC-based transaction source that fetches from a remote node.
|
||||
#[derive(Debug)]
|
||||
pub struct RpcTransactionSource {
|
||||
provider: RootProvider<AnyNetwork>,
|
||||
}
|
||||
|
||||
impl RpcTransactionSource {
|
||||
/// Create a new RPC transaction source.
|
||||
pub const fn new(provider: RootProvider<AnyNetwork>) -> Self {
|
||||
Self { provider }
|
||||
}
|
||||
|
||||
/// Create from an RPC URL with retry backoff.
|
||||
pub fn from_url(rpc_url: &str) -> eyre::Result<Self> {
|
||||
let client = ClientBuilder::default()
|
||||
.layer(RetryBackoffLayer::new(10, 800, u64::MAX))
|
||||
.http(rpc_url.parse()?);
|
||||
let provider = RootProvider::<AnyNetwork>::new(client);
|
||||
Ok(Self { provider })
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionSource for RpcTransactionSource {
|
||||
async fn fetch_block_transactions(
|
||||
&self,
|
||||
block_number: u64,
|
||||
) -> eyre::Result<Option<(Vec<RawTransaction>, u64)>> {
|
||||
// Fetch block and receipts in parallel
|
||||
let (block, receipts) = tokio::try_join!(
|
||||
self.provider.get_block_by_number(block_number.into()).full(),
|
||||
self.provider.get_block_receipts(block_number.into())
|
||||
)?;
|
||||
|
||||
let Some(block) = block else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(receipts) = receipts else {
|
||||
return Err(eyre::eyre!("Receipts not found for block {}", block_number));
|
||||
};
|
||||
|
||||
let block_gas_used = block.header.gas_used;
|
||||
|
||||
// Convert cumulative gas from receipts to per-tx gas_used
|
||||
let mut prev_cumulative = 0u64;
|
||||
let transactions: Vec<RawTransaction> = block
|
||||
.transactions
|
||||
.txns()
|
||||
.zip(receipts.iter())
|
||||
.map(|(tx, receipt)| {
|
||||
let cumulative = receipt.inner.inner.inner.receipt.cumulative_gas_used;
|
||||
let gas_used = cumulative - prev_cumulative;
|
||||
prev_cumulative = cumulative;
|
||||
|
||||
let with_encoded = tx.inner.inner.clone().into_encoded();
|
||||
RawTransaction {
|
||||
gas_used,
|
||||
tx_type: tx.inner.ty(),
|
||||
raw: with_encoded.encoded_bytes().clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Some((transactions, block_gas_used)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects transactions from a source up to a target gas usage.
|
||||
#[derive(Debug)]
|
||||
pub struct TransactionCollector<S> {
|
||||
source: S,
|
||||
target_gas: u64,
|
||||
}
|
||||
|
||||
impl<S: TransactionSource> TransactionCollector<S> {
|
||||
/// Create a new transaction collector.
|
||||
pub const fn new(source: S, target_gas: u64) -> Self {
|
||||
Self { source, target_gas }
|
||||
}
|
||||
|
||||
/// Collect transactions starting from the given block number.
|
||||
///
|
||||
/// Skips blob transactions (type 3) and collects until target gas is reached.
|
||||
/// Returns the collected raw transaction bytes, total gas used, and the next block number.
|
||||
pub async fn collect(&self, start_block: u64) -> eyre::Result<(Vec<Bytes>, u64, u64)> {
|
||||
let mut transactions: Vec<Bytes> = Vec::new();
|
||||
let mut total_gas: u64 = 0;
|
||||
let mut current_block = start_block;
|
||||
|
||||
while total_gas < self.target_gas {
|
||||
let Some((block_txs, _)) = self.source.fetch_block_transactions(current_block).await?
|
||||
else {
|
||||
warn!(block = current_block, "Block not found, stopping");
|
||||
break;
|
||||
};
|
||||
|
||||
for tx in block_txs {
|
||||
// Skip blob transactions (EIP-4844, type 3)
|
||||
if tx.tx_type == 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if total_gas + tx.gas_used <= self.target_gas {
|
||||
transactions.push(tx.raw);
|
||||
total_gas += tx.gas_used;
|
||||
}
|
||||
|
||||
if total_gas >= self.target_gas {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
current_block += 1;
|
||||
|
||||
// Stop early if remaining gas is under 1M (close enough to target)
|
||||
let remaining_gas = self.target_gas.saturating_sub(total_gas);
|
||||
if remaining_gas < 1_000_000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
total_txs = transactions.len(),
|
||||
total_gas,
|
||||
next_block = current_block,
|
||||
"Finished collecting transactions"
|
||||
);
|
||||
|
||||
Ok((transactions, total_gas, current_block))
|
||||
}
|
||||
}
|
||||
|
||||
/// `reth bench generate-big-block` command
|
||||
///
|
||||
/// Generates a large block by fetching transactions from existing blocks and packing them
|
||||
/// into a single block using the `testing_buildBlockV1` RPC endpoint.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// The RPC URL to use for fetching blocks (can be an external archive node).
|
||||
#[arg(long, value_name = "RPC_URL")]
|
||||
rpc_url: String,
|
||||
|
||||
/// The engine RPC URL (with JWT authentication).
|
||||
#[arg(long, value_name = "ENGINE_RPC_URL", default_value = "http://localhost:8551")]
|
||||
engine_rpc_url: String,
|
||||
|
||||
/// The RPC URL for `testing_buildBlockV1` calls (same node as engine, regular RPC port).
|
||||
#[arg(long, value_name = "TESTING_RPC_URL", default_value = "http://localhost:8545")]
|
||||
testing_rpc_url: String,
|
||||
|
||||
/// Path to the JWT secret file for engine API authentication.
|
||||
#[arg(long, value_name = "JWT_SECRET")]
|
||||
jwt_secret: std::path::PathBuf,
|
||||
|
||||
/// Target gas to pack into the block.
|
||||
/// 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,
|
||||
|
||||
/// Block number to start fetching transactions from (required).
|
||||
///
|
||||
/// This must be the last canonical block BEFORE any gas limit ramping was performed.
|
||||
/// The command collects transactions from historical blocks starting at this number
|
||||
/// to pack into large blocks.
|
||||
///
|
||||
/// How to determine this value:
|
||||
/// - If starting from a fresh node (no gas limit ramp yet): use the current chain tip
|
||||
/// - If gas limit ramping has already been performed: use the block number that was the chain
|
||||
/// tip BEFORE ramping began (you must track this yourself)
|
||||
///
|
||||
/// Using a block after ramping started will cause transaction collection to fail
|
||||
/// because those blocks contain synthetic transactions that cannot be replayed.
|
||||
#[arg(long, value_name = "FROM_BLOCK")]
|
||||
from_block: u64,
|
||||
|
||||
/// Execute the payload (call newPayload + forkchoiceUpdated).
|
||||
/// If false, only builds the payload and prints it.
|
||||
#[arg(long, default_value = "false")]
|
||||
execute: bool,
|
||||
|
||||
/// Number of payloads to generate. Each payload uses the previous as parent.
|
||||
/// When count == 1, the payload is only generated and saved, not executed.
|
||||
/// When count > 1, each payload is executed before building the next.
|
||||
#[arg(long, default_value = "1")]
|
||||
count: u64,
|
||||
|
||||
/// Number of transaction batches to prefetch in background when count > 1.
|
||||
/// Higher values reduce latency but use more memory.
|
||||
#[arg(long, default_value = "4")]
|
||||
prefetch_buffer: usize,
|
||||
|
||||
/// Output directory for generated payloads. Each payload is saved as `payload_block_N.json`.
|
||||
#[arg(long, value_name = "OUTPUT_DIR")]
|
||||
output_dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
/// A built payload ready for execution.
|
||||
struct BuiltPayload {
|
||||
block_number: u64,
|
||||
envelope: ExecutionPayloadEnvelopeV4,
|
||||
block_hash: B256,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute the `generate-big-block` command
|
||||
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
|
||||
info!(target_gas = self.target_gas, count = self.count, "Generating big block(s)");
|
||||
|
||||
// Set up authenticated engine provider
|
||||
let jwt =
|
||||
std::fs::read_to_string(&self.jwt_secret).wrap_err("Failed to read JWT secret file")?;
|
||||
let jwt = JwtSecret::from_hex(jwt.trim())?;
|
||||
let auth_url = Url::parse(&self.engine_rpc_url)?;
|
||||
|
||||
info!("Connecting to Engine RPC at {}", auth_url);
|
||||
let auth_transport = AuthenticatedTransportConnect::new(auth_url.clone(), jwt);
|
||||
let auth_client = ClientBuilder::default().connect_with(auth_transport).await?;
|
||||
let auth_provider = RootProvider::<AnyNetwork>::new(auth_client);
|
||||
|
||||
// Set up testing RPC provider (for testing_buildBlockV1)
|
||||
info!("Connecting to Testing RPC at {}", self.testing_rpc_url);
|
||||
let testing_client = ClientBuilder::default()
|
||||
.layer(RetryBackoffLayer::new(10, 800, u64::MAX))
|
||||
.http(self.testing_rpc_url.parse()?);
|
||||
let testing_provider = RootProvider::<AnyNetwork>::new(testing_client);
|
||||
|
||||
// Get the parent block (latest canonical block)
|
||||
info!(endpoint = "engine", method = "eth_getBlockByNumber", block = "latest", "RPC call");
|
||||
let parent_block = auth_provider
|
||||
.get_block_by_number(BlockNumberOrTag::Latest)
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?;
|
||||
|
||||
let parent_hash = parent_block.header.hash;
|
||||
let parent_number = parent_block.header.number;
|
||||
let parent_timestamp = parent_block.header.timestamp;
|
||||
|
||||
info!(
|
||||
parent_hash = %parent_hash,
|
||||
parent_number = parent_number,
|
||||
"Using initial parent block"
|
||||
);
|
||||
|
||||
// Create output directory
|
||||
std::fs::create_dir_all(&self.output_dir).wrap_err_with(|| {
|
||||
format!("Failed to create output directory: {:?}", self.output_dir)
|
||||
})?;
|
||||
|
||||
let start_block = self.from_block;
|
||||
|
||||
// Use pipelined execution when generating multiple payloads
|
||||
if self.count > 1 {
|
||||
self.execute_pipelined(
|
||||
&auth_provider,
|
||||
&testing_provider,
|
||||
start_block,
|
||||
parent_hash,
|
||||
parent_timestamp,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// Single payload - collect transactions and build
|
||||
let tx_source = RpcTransactionSource::from_url(&self.rpc_url)?;
|
||||
let collector = TransactionCollector::new(tx_source, self.target_gas);
|
||||
let (transactions, _total_gas, _next_block) = collector.collect(start_block).await?;
|
||||
|
||||
if transactions.is_empty() {
|
||||
return Err(eyre::eyre!("No transactions collected"));
|
||||
}
|
||||
|
||||
self.execute_sequential(
|
||||
&auth_provider,
|
||||
&testing_provider,
|
||||
transactions,
|
||||
parent_hash,
|
||||
parent_timestamp,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
info!(count = self.count, output_dir = %self.output_dir.display(), "All payloads generated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sequential execution path for single payload or no-execute mode.
|
||||
async fn execute_sequential(
|
||||
&self,
|
||||
auth_provider: &RootProvider<AnyNetwork>,
|
||||
testing_provider: &RootProvider<AnyNetwork>,
|
||||
transactions: Vec<Bytes>,
|
||||
mut parent_hash: B256,
|
||||
mut parent_timestamp: u64,
|
||||
) -> eyre::Result<()> {
|
||||
for i in 0..self.count {
|
||||
info!(
|
||||
payload = i + 1,
|
||||
total = self.count,
|
||||
parent_hash = %parent_hash,
|
||||
parent_timestamp = parent_timestamp,
|
||||
"Building payload via testing_buildBlockV1"
|
||||
);
|
||||
|
||||
let built = self
|
||||
.build_payload(testing_provider, &transactions, i, parent_hash, parent_timestamp)
|
||||
.await?;
|
||||
|
||||
self.save_payload(&built)?;
|
||||
|
||||
if self.execute || self.count > 1 {
|
||||
info!(payload = i + 1, block_hash = %built.block_hash, "Executing payload (newPayload + FCU)");
|
||||
self.execute_payload_v4(auth_provider, built.envelope, parent_hash).await?;
|
||||
info!(payload = i + 1, "Payload executed successfully");
|
||||
}
|
||||
|
||||
parent_hash = built.block_hash;
|
||||
parent_timestamp = built.timestamp;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pipelined execution - fetches transactions and builds payloads in background.
|
||||
async fn execute_pipelined(
|
||||
&self,
|
||||
auth_provider: &RootProvider<AnyNetwork>,
|
||||
testing_provider: &RootProvider<AnyNetwork>,
|
||||
start_block: u64,
|
||||
initial_parent_hash: B256,
|
||||
initial_parent_timestamp: u64,
|
||||
) -> eyre::Result<()> {
|
||||
// Create channel for transaction batches (one batch per payload)
|
||||
let (tx_sender, mut tx_receiver) = mpsc::channel::<Vec<Bytes>>(self.prefetch_buffer);
|
||||
|
||||
// Spawn background task to continuously fetch transaction batches
|
||||
let rpc_url = self.rpc_url.clone();
|
||||
let target_gas = self.target_gas;
|
||||
let count = self.count;
|
||||
|
||||
let fetcher_handle = tokio::spawn(async move {
|
||||
let tx_source = match RpcTransactionSource::from_url(&rpc_url) {
|
||||
Ok(source) => source,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Failed to create transaction source");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let collector = TransactionCollector::new(tx_source, target_gas);
|
||||
let mut current_block = start_block;
|
||||
|
||||
for payload_idx in 0..count {
|
||||
match collector.collect(current_block).await {
|
||||
Ok((transactions, total_gas, next_block)) => {
|
||||
info!(
|
||||
payload = payload_idx + 1,
|
||||
tx_count = transactions.len(),
|
||||
total_gas,
|
||||
blocks = format!("{}..{}", current_block, next_block),
|
||||
"Fetched transactions"
|
||||
);
|
||||
current_block = next_block;
|
||||
|
||||
if tx_sender.send(transactions).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(payload = payload_idx + 1, error = %e, "Failed to fetch transactions");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut parent_hash = initial_parent_hash;
|
||||
let mut parent_timestamp = initial_parent_timestamp;
|
||||
let mut pending_build: Option<tokio::task::JoinHandle<eyre::Result<BuiltPayload>>> = None;
|
||||
|
||||
for i in 0..self.count {
|
||||
let is_last = i == self.count - 1;
|
||||
|
||||
// Get current payload (either from pending build or build now)
|
||||
let current_payload = if let Some(handle) = pending_build.take() {
|
||||
handle.await??
|
||||
} else {
|
||||
// First payload - wait for transactions and build synchronously
|
||||
let transactions = tx_receiver
|
||||
.recv()
|
||||
.await
|
||||
.ok_or_else(|| eyre::eyre!("Transaction fetcher stopped unexpectedly"))?;
|
||||
|
||||
if transactions.is_empty() {
|
||||
return Err(eyre::eyre!("No transactions collected for payload {}", i + 1));
|
||||
}
|
||||
|
||||
info!(
|
||||
payload = i + 1,
|
||||
total = self.count,
|
||||
parent_hash = %parent_hash,
|
||||
parent_timestamp = parent_timestamp,
|
||||
tx_count = transactions.len(),
|
||||
"Building payload via testing_buildBlockV1"
|
||||
);
|
||||
self.build_payload(
|
||||
testing_provider,
|
||||
&transactions,
|
||||
i,
|
||||
parent_hash,
|
||||
parent_timestamp,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
self.save_payload(¤t_payload)?;
|
||||
|
||||
let current_block_hash = current_payload.block_hash;
|
||||
let current_timestamp = current_payload.timestamp;
|
||||
|
||||
// Execute current payload first
|
||||
info!(payload = i + 1, block_hash = %current_block_hash, "Executing payload (newPayload + FCU)");
|
||||
self.execute_payload_v4(auth_provider, current_payload.envelope, parent_hash).await?;
|
||||
info!(payload = i + 1, "Payload executed successfully");
|
||||
|
||||
// Start building next payload in background (if not last) - AFTER execution
|
||||
if !is_last {
|
||||
// Get transactions for next payload (should already be fetched or fetching)
|
||||
let next_transactions = tx_receiver
|
||||
.recv()
|
||||
.await
|
||||
.ok_or_else(|| eyre::eyre!("Transaction fetcher stopped unexpectedly"))?;
|
||||
|
||||
if next_transactions.is_empty() {
|
||||
return Err(eyre::eyre!("No transactions collected for payload {}", i + 2));
|
||||
}
|
||||
|
||||
let testing_provider = testing_provider.clone();
|
||||
let next_index = i + 1;
|
||||
let total = self.count;
|
||||
|
||||
pending_build = Some(tokio::spawn(async move {
|
||||
info!(
|
||||
payload = next_index + 1,
|
||||
total = total,
|
||||
parent_hash = %current_block_hash,
|
||||
parent_timestamp = current_timestamp,
|
||||
tx_count = next_transactions.len(),
|
||||
"Building payload via testing_buildBlockV1"
|
||||
);
|
||||
|
||||
Self::build_payload_static(
|
||||
&testing_provider,
|
||||
&next_transactions,
|
||||
next_index,
|
||||
current_block_hash,
|
||||
current_timestamp,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
}
|
||||
|
||||
parent_hash = current_block_hash;
|
||||
parent_timestamp = current_timestamp;
|
||||
}
|
||||
|
||||
// Clean up the fetcher task
|
||||
drop(tx_receiver);
|
||||
let _ = fetcher_handle.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a single payload via `testing_buildBlockV1`.
|
||||
async fn build_payload(
|
||||
&self,
|
||||
testing_provider: &RootProvider<AnyNetwork>,
|
||||
transactions: &[Bytes],
|
||||
index: u64,
|
||||
parent_hash: B256,
|
||||
parent_timestamp: u64,
|
||||
) -> eyre::Result<BuiltPayload> {
|
||||
Self::build_payload_static(
|
||||
testing_provider,
|
||||
transactions,
|
||||
index,
|
||||
parent_hash,
|
||||
parent_timestamp,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Static version for use in spawned tasks.
|
||||
async fn build_payload_static(
|
||||
testing_provider: &RootProvider<AnyNetwork>,
|
||||
transactions: &[Bytes],
|
||||
index: u64,
|
||||
parent_hash: B256,
|
||||
parent_timestamp: u64,
|
||||
) -> eyre::Result<BuiltPayload> {
|
||||
let request = TestingBuildBlockRequestV1 {
|
||||
parent_block_hash: parent_hash,
|
||||
payload_attributes: PayloadAttributes {
|
||||
timestamp: parent_timestamp + 12,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: alloy_primitives::Address::ZERO,
|
||||
withdrawals: Some(vec![]),
|
||||
parent_beacon_block_root: Some(B256::ZERO),
|
||||
},
|
||||
transactions: transactions.to_vec(),
|
||||
extra_data: None,
|
||||
};
|
||||
|
||||
let total_tx_bytes: usize = transactions.iter().map(|tx| tx.len()).sum();
|
||||
info!(
|
||||
payload = index + 1,
|
||||
tx_count = transactions.len(),
|
||||
total_tx_bytes = total_tx_bytes,
|
||||
parent_hash = %parent_hash,
|
||||
"Sending to testing_buildBlockV1"
|
||||
);
|
||||
let envelope: ExecutionPayloadEnvelopeV5 =
|
||||
testing_provider.client().request("testing_buildBlockV1", [request]).await?;
|
||||
|
||||
let v4_envelope = envelope.try_into_v4()?;
|
||||
|
||||
let inner = &v4_envelope.envelope_inner.execution_payload.payload_inner.payload_inner;
|
||||
let block_hash = inner.block_hash;
|
||||
let block_number = inner.block_number;
|
||||
let timestamp = inner.timestamp;
|
||||
|
||||
Ok(BuiltPayload { block_number, envelope: v4_envelope, block_hash, timestamp })
|
||||
}
|
||||
|
||||
/// Save a payload to disk.
|
||||
fn save_payload(&self, payload: &BuiltPayload) -> eyre::Result<()> {
|
||||
let filename = format!("payload_block_{}.json", payload.block_number);
|
||||
let filepath = self.output_dir.join(&filename);
|
||||
let json = serde_json::to_string_pretty(&payload.envelope)?;
|
||||
std::fs::write(&filepath, &json)
|
||||
.wrap_err_with(|| format!("Failed to write payload to {:?}", filepath))?;
|
||||
info!(block_number = payload.block_number, block_hash = %payload.block_hash, path = %filepath.display(), "Payload saved");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_payload_v4(
|
||||
&self,
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
envelope: ExecutionPayloadEnvelopeV4,
|
||||
parent_hash: B256,
|
||||
) -> eyre::Result<()> {
|
||||
let block_hash =
|
||||
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.block_hash;
|
||||
|
||||
let status = provider
|
||||
.new_payload_v4(
|
||||
envelope.envelope_inner.execution_payload,
|
||||
vec![],
|
||||
B256::ZERO,
|
||||
envelope.execution_requests.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !status.is_valid() {
|
||||
return Err(eyre::eyre!("Payload rejected: {:?}", status));
|
||||
}
|
||||
|
||||
let fcu_state = ForkchoiceState {
|
||||
head_block_hash: block_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
|
||||
let fcu_result = provider.fork_choice_updated_v3(fcu_state, None).await?;
|
||||
|
||||
if !fcu_result.is_valid() {
|
||||
return Err(eyre::eyre!("FCU rejected: {:?}", fcu_result));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
266
bin/reth-bench/src/bench/helpers.rs
Normal file
266
bin/reth-bench/src/bench/helpers.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
//! 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};
|
||||
use alloy_provider::{ext::EngineApi, network::AnyNetwork, RootProvider};
|
||||
use alloy_rpc_types_engine::{
|
||||
CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ForkchoiceState,
|
||||
PayloadAttributes, PayloadId, PraguePayloadFields,
|
||||
};
|
||||
use eyre::OptionExt;
|
||||
use reth_chainspec::{ChainSpec, EthereumHardforks};
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use tracing::debug;
|
||||
|
||||
/// Prepared payload request data for triggering block building.
|
||||
pub(crate) struct PayloadRequest {
|
||||
/// The payload attributes for the new block.
|
||||
pub(crate) attributes: PayloadAttributes,
|
||||
/// The forkchoice state pointing to the parent block.
|
||||
pub(crate) forkchoice_state: ForkchoiceState,
|
||||
/// The engine API version for FCU calls.
|
||||
pub(crate) fcu_version: EngineApiMessageVersion,
|
||||
/// The getPayload version to use (1-5).
|
||||
pub(crate) get_payload_version: u8,
|
||||
/// The newPayload version to use.
|
||||
pub(crate) new_payload_version: EngineApiMessageVersion,
|
||||
}
|
||||
|
||||
/// Prepare payload attributes and forkchoice state for a new block.
|
||||
pub(crate) fn prepare_payload_request(
|
||||
chain_spec: &ChainSpec,
|
||||
timestamp: u64,
|
||||
parent_hash: B256,
|
||||
) -> PayloadRequest {
|
||||
let shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
|
||||
let cancun_active = chain_spec.is_cancun_active_at_timestamp(timestamp);
|
||||
let prague_active = chain_spec.is_prague_active_at_timestamp(timestamp);
|
||||
let osaka_active = chain_spec.is_osaka_active_at_timestamp(timestamp);
|
||||
|
||||
// FCU version: V3 for Cancun+Prague+Osaka, V2 for Shanghai, V1 otherwise
|
||||
let fcu_version = if cancun_active {
|
||||
EngineApiMessageVersion::V3
|
||||
} else if shanghai_active {
|
||||
EngineApiMessageVersion::V2
|
||||
} else {
|
||||
EngineApiMessageVersion::V1
|
||||
};
|
||||
|
||||
// getPayload version: 5 for Osaka, 4 for Prague, 3 for Cancun, 2 for Shanghai, 1 otherwise
|
||||
// newPayload version: 4 for Prague+Osaka (no V5), 3 for Cancun, 2 for Shanghai, 1 otherwise
|
||||
let (get_payload_version, new_payload_version) = if osaka_active {
|
||||
(5, EngineApiMessageVersion::V4) // Osaka uses getPayloadV5 but newPayloadV4
|
||||
} else if prague_active {
|
||||
(4, EngineApiMessageVersion::V4)
|
||||
} else if cancun_active {
|
||||
(3, EngineApiMessageVersion::V3)
|
||||
} else if shanghai_active {
|
||||
(2, EngineApiMessageVersion::V2)
|
||||
} else {
|
||||
(1, EngineApiMessageVersion::V1)
|
||||
};
|
||||
|
||||
PayloadRequest {
|
||||
attributes: PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao: B256::ZERO,
|
||||
suggested_fee_recipient: Address::ZERO,
|
||||
withdrawals: shanghai_active.then(Vec::new),
|
||||
parent_beacon_block_root: cancun_active.then_some(B256::ZERO),
|
||||
},
|
||||
forkchoice_state: ForkchoiceState {
|
||||
head_block_hash: parent_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
},
|
||||
fcu_version,
|
||||
get_payload_version,
|
||||
new_payload_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger payload building via FCU and retrieve the built payload.
|
||||
///
|
||||
/// This sends a forkchoiceUpdated with payload attributes to start building,
|
||||
/// then calls getPayload to retrieve the result.
|
||||
pub(crate) async fn build_payload(
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
request: PayloadRequest,
|
||||
) -> eyre::Result<(ExecutionPayload, ExecutionPayloadSidecar)> {
|
||||
let fcu_result = call_forkchoice_updated(
|
||||
provider,
|
||||
request.fcu_version,
|
||||
request.forkchoice_state,
|
||||
Some(request.attributes.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let payload_id =
|
||||
fcu_result.payload_id.ok_or_eyre("Payload builder did not return a payload id")?;
|
||||
|
||||
get_payload_with_sidecar(
|
||||
provider,
|
||||
request.get_payload_version,
|
||||
payload_id,
|
||||
request.attributes.parent_beacon_block_root,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Convert an RPC block to a consensus header and block hash.
|
||||
pub(crate) fn rpc_block_to_header(block: alloy_provider::network::AnyRpcBlock) -> (Header, B256) {
|
||||
let block_hash = block.header.hash;
|
||||
let header = block.header.inner.clone().into_header_with_defaults();
|
||||
(header, block_hash)
|
||||
}
|
||||
|
||||
/// Compute versioned hashes from KZG commitments.
|
||||
fn versioned_hashes_from_commitments(
|
||||
commitments: &[alloy_primitives::FixedBytes<48>],
|
||||
) -> Vec<B256> {
|
||||
commitments.iter().map(|c| kzg_to_versioned_hash(c.as_ref())).collect()
|
||||
}
|
||||
|
||||
/// Fetch an execution payload using the appropriate engine API version.
|
||||
pub(crate) async fn get_payload_with_sidecar(
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
version: u8,
|
||||
payload_id: PayloadId,
|
||||
parent_beacon_block_root: Option<B256>,
|
||||
) -> eyre::Result<(ExecutionPayload, ExecutionPayloadSidecar)> {
|
||||
debug!(get_payload_version = ?version, ?payload_id, "Sending getPayload");
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
let payload = provider.get_payload_v1(payload_id).await?;
|
||||
Ok((ExecutionPayload::V1(payload), ExecutionPayloadSidecar::none()))
|
||||
}
|
||||
2 => {
|
||||
let envelope = provider.get_payload_v2(payload_id).await?;
|
||||
let payload = match envelope.execution_payload {
|
||||
alloy_rpc_types_engine::ExecutionPayloadFieldV2::V1(p) => ExecutionPayload::V1(p),
|
||||
alloy_rpc_types_engine::ExecutionPayloadFieldV2::V2(p) => ExecutionPayload::V2(p),
|
||||
};
|
||||
Ok((payload, ExecutionPayloadSidecar::none()))
|
||||
}
|
||||
3 => {
|
||||
let envelope = provider.get_payload_v3(payload_id).await?;
|
||||
let versioned_hashes =
|
||||
versioned_hashes_from_commitments(&envelope.blobs_bundle.commitments);
|
||||
let cancun_fields = CancunPayloadFields {
|
||||
parent_beacon_block_root: parent_beacon_block_root
|
||||
.ok_or_eyre("parent_beacon_block_root required for V3")?,
|
||||
versioned_hashes,
|
||||
};
|
||||
Ok((
|
||||
ExecutionPayload::V3(envelope.execution_payload),
|
||||
ExecutionPayloadSidecar::v3(cancun_fields),
|
||||
))
|
||||
}
|
||||
4 => {
|
||||
let envelope = provider.get_payload_v4(payload_id).await?;
|
||||
let versioned_hashes = versioned_hashes_from_commitments(
|
||||
&envelope.envelope_inner.blobs_bundle.commitments,
|
||||
);
|
||||
let cancun_fields = CancunPayloadFields {
|
||||
parent_beacon_block_root: parent_beacon_block_root
|
||||
.ok_or_eyre("parent_beacon_block_root required for V4")?,
|
||||
versioned_hashes,
|
||||
};
|
||||
let prague_fields = PraguePayloadFields::new(envelope.execution_requests);
|
||||
Ok((
|
||||
ExecutionPayload::V3(envelope.envelope_inner.execution_payload),
|
||||
ExecutionPayloadSidecar::v4(cancun_fields, prague_fields),
|
||||
))
|
||||
}
|
||||
5 => {
|
||||
// V5 (Osaka) - use raw request since alloy doesn't have get_payload_v5 yet
|
||||
let envelope = provider.get_payload_v5(payload_id).await?;
|
||||
let versioned_hashes =
|
||||
versioned_hashes_from_commitments(&envelope.blobs_bundle.commitments);
|
||||
let cancun_fields = CancunPayloadFields {
|
||||
parent_beacon_block_root: parent_beacon_block_root
|
||||
.ok_or_eyre("parent_beacon_block_root required for V5")?,
|
||||
versioned_hashes,
|
||||
};
|
||||
let prague_fields = PraguePayloadFields::new(envelope.execution_requests);
|
||||
Ok((
|
||||
ExecutionPayload::V3(envelope.execution_payload),
|
||||
ExecutionPayloadSidecar::v4(cancun_fields, prague_fields),
|
||||
))
|
||||
}
|
||||
_ => 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());
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,17 @@ use reth_node_core::args::LogArgs;
|
||||
use reth_tracing::FileWorkerGuard;
|
||||
|
||||
mod context;
|
||||
mod gas_limit_ramp;
|
||||
mod generate_big_block;
|
||||
pub(crate) mod helpers;
|
||||
pub use generate_big_block::{
|
||||
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
|
||||
};
|
||||
mod new_payload_fcu;
|
||||
mod new_payload_only;
|
||||
mod output;
|
||||
mod replay_payloads;
|
||||
mod send_invalid_payload;
|
||||
mod send_payload;
|
||||
|
||||
/// `reth bench` command
|
||||
@@ -27,6 +35,9 @@ pub enum Subcommands {
|
||||
/// Benchmark which calls `newPayload`, then `forkchoiceUpdated`.
|
||||
NewPayloadFcu(new_payload_fcu::Command),
|
||||
|
||||
/// Benchmark which builds empty blocks with a ramped gas limit.
|
||||
GasLimitRamp(gas_limit_ramp::Command),
|
||||
|
||||
/// Benchmark which only calls subsequent `newPayload` calls.
|
||||
NewPayloadOnly(new_payload_only::Command),
|
||||
|
||||
@@ -41,6 +52,41 @@ pub enum Subcommands {
|
||||
/// `cast block latest --full --json | reth-bench send-payload --rpc-url localhost:5000
|
||||
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex)`
|
||||
SendPayload(send_payload::Command),
|
||||
|
||||
/// Generate a large block by packing transactions from existing blocks.
|
||||
///
|
||||
/// This command fetches transactions from real blocks and packs them into a single
|
||||
/// block using the `testing_buildBlockV1` RPC endpoint.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --engine-rpc-url
|
||||
/// http://localhost:8551 --jwt-secret ~/.local/share/reth/mainnet/jwt.hex --target-gas
|
||||
/// 30000000`
|
||||
GenerateBigBlock(generate_big_block::Command),
|
||||
|
||||
/// Replay pre-generated payloads from a directory.
|
||||
///
|
||||
/// This command reads payload files from a previous `generate-big-block` run and replays
|
||||
/// them in sequence using `newPayload` followed by `forkchoiceUpdated`.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// `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 {
|
||||
@@ -51,8 +97,12 @@ impl BenchmarkCommand {
|
||||
|
||||
match self.command {
|
||||
Subcommands::NewPayloadFcu(command) => command.execute(ctx).await,
|
||||
Subcommands::GasLimitRamp(command) => command.execute(ctx).await,
|
||||
Subcommands::NewPayloadOnly(command) => command.execute(ctx).await,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
//! Runs the `reth bench` command, calling first newPayload for each block, then calling
|
||||
//! forkchoiceUpdated.
|
||||
//!
|
||||
//! Supports configurable waiting behavior:
|
||||
//! - **`--wait-time`**: Fixed sleep interval between blocks.
|
||||
//! - **`--wait-for-persistence`**: Waits for every Nth block to be persisted using the
|
||||
//! `reth_subscribePersistedBlock` subscription, where N matches the engine's persistence
|
||||
//! threshold. This ensures the benchmark doesn't outpace persistence.
|
||||
//!
|
||||
//! Both options can be used together or independently.
|
||||
|
||||
use crate::{
|
||||
bench::{
|
||||
context::BenchContext,
|
||||
output::{
|
||||
CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow, COMBINED_OUTPUT_SUFFIX,
|
||||
GAS_OUTPUT_SUFFIX,
|
||||
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
|
||||
},
|
||||
},
|
||||
valid_payload::{block_to_new_payload, call_forkchoice_updated, call_new_payload},
|
||||
};
|
||||
use alloy_provider::Provider;
|
||||
use alloy_eips::BlockNumHash;
|
||||
use alloy_network::Ethereum;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
use alloy_pubsub::SubscriptionStream;
|
||||
use alloy_rpc_client::RpcClient;
|
||||
use alloy_rpc_types_engine::ForkchoiceState;
|
||||
use alloy_transport_ws::WsConnect;
|
||||
use clap::Parser;
|
||||
use csv::Writer;
|
||||
use eyre::{Context, OptionExt};
|
||||
use futures::StreamExt;
|
||||
use humantime::parse_duration;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
|
||||
use reth_node_core::args::BenchmarkArgs;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, info};
|
||||
use url::Url;
|
||||
|
||||
const PERSISTENCE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// `reth benchmark new-payload-fcu` command
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -30,8 +46,31 @@ pub struct Command {
|
||||
rpc_url: String,
|
||||
|
||||
/// How long to wait after a forkchoice update before sending the next payload.
|
||||
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, default_value = "250ms", verbatim_doc_comment)]
|
||||
wait_time: Duration,
|
||||
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, verbatim_doc_comment)]
|
||||
wait_time: Option<Duration>,
|
||||
|
||||
/// Wait for blocks to be persisted before sending the next batch.
|
||||
///
|
||||
/// When enabled, waits for every Nth block to be persisted using the
|
||||
/// `reth_subscribePersistedBlock` subscription. This ensures the benchmark
|
||||
/// doesn't outpace persistence.
|
||||
///
|
||||
/// The subscription uses the regular RPC websocket endpoint (no JWT required).
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
wait_for_persistence: bool,
|
||||
|
||||
/// Engine persistence threshold used for deciding when to wait for persistence.
|
||||
///
|
||||
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
|
||||
/// matches the engine's `DEFAULT_PERSISTENCE_THRESHOLD` (2), so waits occur
|
||||
/// at blocks 3, 6, 9, etc.
|
||||
#[arg(
|
||||
long = "persistence-threshold",
|
||||
value_name = "PERSISTENCE_THRESHOLD",
|
||||
default_value_t = DEFAULT_PERSISTENCE_THRESHOLD,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
persistence_threshold: u64,
|
||||
|
||||
/// The size of the block buffer (channel capacity) for prefetching blocks from the RPC
|
||||
/// endpoint.
|
||||
@@ -50,12 +89,39 @@ pub struct Command {
|
||||
impl Command {
|
||||
/// Execute `benchmark new-payload-fcu` command
|
||||
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
|
||||
// Log mode configuration
|
||||
if let Some(duration) = self.wait_time {
|
||||
info!("Using wait-time mode with {}ms delay between blocks", duration.as_millis());
|
||||
}
|
||||
if self.wait_for_persistence {
|
||||
info!(
|
||||
"Persistence waiting enabled (waits after every {} blocks to match engine gap > {} behavior)",
|
||||
self.persistence_threshold + 1,
|
||||
self.persistence_threshold
|
||||
);
|
||||
}
|
||||
|
||||
// Set up waiter based on configured options (duration takes precedence)
|
||||
let mut waiter = match (self.wait_time, self.wait_for_persistence) {
|
||||
(Some(duration), _) => Some(PersistenceWaiter::with_duration(duration)),
|
||||
(None, true) => {
|
||||
let sub = self.setup_persistence_subscription().await?;
|
||||
Some(PersistenceWaiter::with_subscription(
|
||||
sub,
|
||||
self.persistence_threshold,
|
||||
PERSISTENCE_CHECKPOINT_TIMEOUT,
|
||||
))
|
||||
}
|
||||
(None, false) => None,
|
||||
};
|
||||
|
||||
let BenchContext {
|
||||
benchmark_mode,
|
||||
block_provider,
|
||||
auth_provider,
|
||||
mut next_block,
|
||||
is_optimism,
|
||||
..
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let buffer_size = self.rpc_block_buffer_size;
|
||||
@@ -110,7 +176,6 @@ impl Command {
|
||||
}
|
||||
});
|
||||
|
||||
// put results in a summary vec so they can be printed at the end
|
||||
let mut results = Vec::new();
|
||||
let total_benchmark_duration = Instant::now();
|
||||
let mut total_wait_time = Duration::ZERO;
|
||||
@@ -121,14 +186,13 @@ impl Command {
|
||||
total_wait_time += wait_start.elapsed();
|
||||
result
|
||||
} {
|
||||
// just put gas used here
|
||||
let gas_used = block.header.gas_used;
|
||||
let gas_limit = block.header.gas_limit;
|
||||
let block_number = block.header.number;
|
||||
let transaction_count = block.transactions.len() as u64;
|
||||
|
||||
debug!(target: "reth-bench", ?block_number, "Sending payload",);
|
||||
debug!(target: "reth-bench", ?block_number, "Sending payload");
|
||||
|
||||
// construct fcu to call
|
||||
let forkchoice_state = ForkchoiceState {
|
||||
head_block_hash: head,
|
||||
safe_block_hash: safe,
|
||||
@@ -143,28 +207,26 @@ impl Command {
|
||||
|
||||
call_forkchoice_updated(&auth_provider, version, forkchoice_state, None).await?;
|
||||
|
||||
// calculate the total duration and the fcu latency, record
|
||||
let total_latency = start.elapsed();
|
||||
let fcu_latency = total_latency - new_payload_result.latency;
|
||||
let combined_result = CombinedResult {
|
||||
block_number,
|
||||
gas_limit,
|
||||
transaction_count,
|
||||
new_payload_result,
|
||||
fcu_latency,
|
||||
total_latency,
|
||||
};
|
||||
|
||||
// current duration since the start of the benchmark minus the time
|
||||
// waiting for blocks
|
||||
// Exclude time spent waiting on the block prefetch channel from the benchmark duration.
|
||||
// We want to measure engine throughput, not RPC fetch latency.
|
||||
let current_duration = total_benchmark_duration.elapsed() - total_wait_time;
|
||||
|
||||
// convert gas used to gigagas, then compute gigagas per second
|
||||
info!(%combined_result);
|
||||
|
||||
// wait before sending the next payload
|
||||
tokio::time::sleep(self.wait_time).await;
|
||||
if let Some(w) = &mut waiter {
|
||||
w.on_block(block_number).await?;
|
||||
}
|
||||
|
||||
// record the current result
|
||||
let gas_row =
|
||||
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
|
||||
results.push((gas_row, combined_result));
|
||||
@@ -175,34 +237,19 @@ impl Command {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let (gas_output_results, combined_results): (_, Vec<CombinedResult>) =
|
||||
// Drop waiter - we don't need to wait for final blocks to persist
|
||||
// since the benchmark goal is measuring Ggas/s of newPayload/FCU, not persistence.
|
||||
drop(waiter);
|
||||
|
||||
let (gas_output_results, combined_results): (Vec<TotalGasRow>, Vec<CombinedResult>) =
|
||||
results.into_iter().unzip();
|
||||
|
||||
// write the csv output to files
|
||||
if let Some(path) = self.benchmark.output {
|
||||
// first write the combined results to a file
|
||||
let output_path = path.join(COMBINED_OUTPUT_SUFFIX);
|
||||
info!("Writing engine api call latency output to file: {:?}", output_path);
|
||||
let mut writer = Writer::from_path(output_path)?;
|
||||
for result in combined_results {
|
||||
writer.serialize(result)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
// now write the gas output to a file
|
||||
let output_path = path.join(GAS_OUTPUT_SUFFIX);
|
||||
info!("Writing total gas output to file: {:?}", output_path);
|
||||
let mut writer = Writer::from_path(output_path)?;
|
||||
for row in &gas_output_results {
|
||||
writer.serialize(row)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
info!("Finished writing benchmark output files to {:?}.", path);
|
||||
if let Some(ref path) = self.benchmark.output {
|
||||
write_benchmark_results(path, &gas_output_results, combined_results)?;
|
||||
}
|
||||
|
||||
// accumulate the results and calculate the overall Ggas/s
|
||||
let gas_output = TotalGasOutput::new(gas_output_results)?;
|
||||
|
||||
info!(
|
||||
total_duration=?gas_output.total_duration,
|
||||
total_gas_used=?gas_output.total_gas_used,
|
||||
@@ -213,4 +260,278 @@ impl Command {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the websocket RPC URL used for the persistence subscription.
|
||||
///
|
||||
/// Preference:
|
||||
/// - If `--ws-rpc-url` is provided, use it directly.
|
||||
/// - Otherwise, derive a WS RPC URL from `--engine-rpc-url`.
|
||||
///
|
||||
/// The persistence subscription endpoint (`reth_subscribePersistedBlock`) is exposed on
|
||||
/// the regular RPC server (WS port, usually 8546), not on the engine API port (usually 8551).
|
||||
/// Since `BenchmarkArgs` only has the engine URL by default, we convert the scheme
|
||||
/// (http→ws, https→wss) and force the port to 8546.
|
||||
fn derive_ws_rpc_url(&self) -> eyre::Result<Url> {
|
||||
if let Some(ref ws_url) = self.benchmark.ws_rpc_url {
|
||||
let parsed: Url = ws_url
|
||||
.parse()
|
||||
.wrap_err_with(|| format!("Failed to parse WebSocket RPC URL: {ws_url}"))?;
|
||||
info!(target: "reth-bench", ws_url = %parsed, "Using provided WebSocket RPC URL");
|
||||
Ok(parsed)
|
||||
} else {
|
||||
let derived = engine_url_to_ws_url(&self.benchmark.engine_rpc_url)?;
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
engine_url = %self.benchmark.engine_rpc_url,
|
||||
%derived,
|
||||
"Derived WebSocket RPC URL from engine RPC URL"
|
||||
);
|
||||
Ok(derived)
|
||||
}
|
||||
}
|
||||
|
||||
/// Establishes a websocket connection and subscribes to `reth_subscribePersistedBlock`.
|
||||
async fn setup_persistence_subscription(&self) -> eyre::Result<PersistenceSubscription> {
|
||||
let ws_url = self.derive_ws_rpc_url()?;
|
||||
|
||||
info!("Connecting to WebSocket at {} for persistence subscription", ws_url);
|
||||
|
||||
let ws_connect = WsConnect::new(ws_url.to_string());
|
||||
let client = RpcClient::connect_pubsub(ws_connect)
|
||||
.await
|
||||
.wrap_err("Failed to connect to WebSocket RPC endpoint")?;
|
||||
let provider: RootProvider<Ethereum> = RootProvider::new(client);
|
||||
|
||||
let subscription = provider
|
||||
.subscribe_to::<BlockNumHash>("reth_subscribePersistedBlock")
|
||||
.await
|
||||
.wrap_err("Failed to subscribe to persistence notifications")?;
|
||||
|
||||
info!("Subscribed to persistence notifications");
|
||||
|
||||
Ok(PersistenceSubscription::new(provider, subscription.into_stream()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an engine API URL to the default RPC websocket URL.
|
||||
///
|
||||
/// Transformations:
|
||||
/// - `http` → `ws`
|
||||
/// - `https` → `wss`
|
||||
/// - `ws` / `wss` keep their scheme
|
||||
/// - Port is always set to `8546`, reth's default RPC websocket port.
|
||||
///
|
||||
/// This is used when we only know the engine API URL (typically `:8551`) but
|
||||
/// need to connect to the node's WS RPC endpoint for persistence events.
|
||||
fn engine_url_to_ws_url(engine_url: &str) -> eyre::Result<Url> {
|
||||
let url: Url = engine_url
|
||||
.parse()
|
||||
.wrap_err_with(|| format!("Failed to parse engine RPC URL: {engine_url}"))?;
|
||||
|
||||
let mut ws_url = url.clone();
|
||||
|
||||
match ws_url.scheme() {
|
||||
"http" => ws_url
|
||||
.set_scheme("ws")
|
||||
.map_err(|_| eyre::eyre!("Failed to set WS scheme for URL: {url}"))?,
|
||||
"https" => ws_url
|
||||
.set_scheme("wss")
|
||||
.map_err(|_| eyre::eyre!("Failed to set WSS scheme for URL: {url}"))?,
|
||||
"ws" | "wss" => {}
|
||||
scheme => {
|
||||
return Err(eyre::eyre!(
|
||||
"Unsupported URL scheme '{scheme}' for URL: {url}. Expected http, https, ws, or wss."
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
ws_url.set_port(Some(8546)).map_err(|_| eyre::eyre!("Failed to set port for URL: {url}"))?;
|
||||
|
||||
Ok(ws_url)
|
||||
}
|
||||
|
||||
/// Waits until the persistence subscription reports that `target` has been persisted.
|
||||
///
|
||||
/// Consumes subscription events until `last_persisted >= target`, or returns an error if:
|
||||
/// - the subscription stream ends unexpectedly, or
|
||||
/// - `timeout` elapses before `target` is observed.
|
||||
async fn wait_for_persistence(
|
||||
stream: &mut SubscriptionStream<BlockNumHash>,
|
||||
target: u64,
|
||||
last_persisted: &mut u64,
|
||||
timeout: Duration,
|
||||
) -> eyre::Result<()> {
|
||||
tokio::time::timeout(timeout, async {
|
||||
while *last_persisted < target {
|
||||
match stream.next().await {
|
||||
Some(persisted) => {
|
||||
*last_persisted = persisted.number;
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
persisted_block = ?last_persisted,
|
||||
"Received persistence notification"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
return Err(eyre::eyre!("Persistence subscription closed unexpectedly"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
eyre::eyre!(
|
||||
"Persistence timeout: target block {} not persisted within {:?}. Last persisted: {}",
|
||||
target,
|
||||
timeout,
|
||||
last_persisted
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
/// Wrapper that keeps both the subscription stream and the underlying provider alive.
|
||||
/// The provider must be kept alive for the subscription to continue receiving events.
|
||||
struct PersistenceSubscription {
|
||||
_provider: RootProvider<Ethereum>,
|
||||
stream: SubscriptionStream<BlockNumHash>,
|
||||
}
|
||||
|
||||
impl PersistenceSubscription {
|
||||
const fn new(
|
||||
provider: RootProvider<Ethereum>,
|
||||
stream: SubscriptionStream<BlockNumHash>,
|
||||
) -> Self {
|
||||
Self { _provider: provider, stream }
|
||||
}
|
||||
|
||||
const fn stream_mut(&mut self) -> &mut SubscriptionStream<BlockNumHash> {
|
||||
&mut self.stream
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates the block waiting logic.
|
||||
///
|
||||
/// Provides a simple `on_block()` interface that handles both:
|
||||
/// - Fixed duration waits (when `wait_time` is set)
|
||||
/// - Persistence-based waits (when `subscription` is set)
|
||||
///
|
||||
/// For persistence mode, waits after every `(threshold + 1)` blocks.
|
||||
struct PersistenceWaiter {
|
||||
wait_time: Option<Duration>,
|
||||
subscription: Option<PersistenceSubscription>,
|
||||
blocks_sent: u64,
|
||||
last_persisted: u64,
|
||||
threshold: u64,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl PersistenceWaiter {
|
||||
const fn with_duration(wait_time: Duration) -> Self {
|
||||
Self {
|
||||
wait_time: Some(wait_time),
|
||||
subscription: None,
|
||||
blocks_sent: 0,
|
||||
last_persisted: 0,
|
||||
threshold: 0,
|
||||
timeout: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
const fn with_subscription(
|
||||
subscription: PersistenceSubscription,
|
||||
threshold: u64,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
wait_time: None,
|
||||
subscription: Some(subscription),
|
||||
blocks_sent: 0,
|
||||
last_persisted: 0,
|
||||
threshold,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Called once per block. Waits based on the configured mode.
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
async fn on_block(&mut self, block_number: u64) -> eyre::Result<()> {
|
||||
if let Some(wait_time) = self.wait_time {
|
||||
tokio::time::sleep(wait_time).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(ref mut subscription) = self.subscription else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.blocks_sent += 1;
|
||||
|
||||
if self.blocks_sent % (self.threshold + 1) == 0 {
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
target_block = ?block_number,
|
||||
last_persisted = self.last_persisted,
|
||||
blocks_sent = self.blocks_sent,
|
||||
"Waiting for persistence"
|
||||
);
|
||||
|
||||
wait_for_persistence(
|
||||
subscription.stream_mut(),
|
||||
block_number,
|
||||
&mut self.last_persisted,
|
||||
self.timeout,
|
||||
)
|
||||
.await?;
|
||||
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
persisted = self.last_persisted,
|
||||
"Persistence caught up"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_engine_url_to_ws_url() {
|
||||
// http -> ws, always uses port 8546
|
||||
let result = engine_url_to_ws_url("http://localhost:8551").unwrap();
|
||||
assert_eq!(result.as_str(), "ws://localhost:8546/");
|
||||
|
||||
// https -> wss
|
||||
let result = engine_url_to_ws_url("https://localhost:8551").unwrap();
|
||||
assert_eq!(result.as_str(), "wss://localhost:8546/");
|
||||
|
||||
// Custom engine port still maps to 8546
|
||||
let result = engine_url_to_ws_url("http://localhost:9551").unwrap();
|
||||
assert_eq!(result.port(), Some(8546));
|
||||
|
||||
// Already ws passthrough
|
||||
let result = engine_url_to_ws_url("ws://localhost:8546").unwrap();
|
||||
assert_eq!(result.scheme(), "ws");
|
||||
|
||||
// Invalid inputs
|
||||
assert!(engine_url_to_ws_url("ftp://localhost:8551").is_err());
|
||||
assert!(engine_url_to_ws_url("not a valid url").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_waiter_with_duration() {
|
||||
let mut waiter = PersistenceWaiter::with_duration(Duration::from_millis(1));
|
||||
|
||||
let start = Instant::now();
|
||||
waiter.on_block(1).await.unwrap();
|
||||
waiter.on_block(2).await.unwrap();
|
||||
waiter.on_block(3).await.unwrap();
|
||||
|
||||
// Should have waited ~3ms total
|
||||
assert!(start.elapsed() >= Duration::from_millis(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ impl Command {
|
||||
auth_provider,
|
||||
mut next_block,
|
||||
is_optimism,
|
||||
..
|
||||
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
|
||||
|
||||
let buffer_size = self.rpc_block_buffer_size;
|
||||
@@ -96,11 +97,7 @@ impl Command {
|
||||
let transaction_count = block.transactions.len() as u64;
|
||||
let gas_used = block.header.gas_used;
|
||||
|
||||
debug!(
|
||||
target: "reth-bench",
|
||||
number=?block.header.number,
|
||||
"Sending payload to engine",
|
||||
);
|
||||
debug!(number=?block.header.number, "Sending payload to engine");
|
||||
|
||||
let (version, params) = block_to_new_payload(block, is_optimism)?;
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
//! Contains various benchmark output formats, either for logging or for
|
||||
//! serialization to / from files.
|
||||
|
||||
use alloy_primitives::B256;
|
||||
use csv::Writer;
|
||||
use eyre::OptionExt;
|
||||
use reth_primitives_traits::constants::GIGAGAS;
|
||||
use serde::{ser::SerializeStruct, Serialize};
|
||||
use std::time::Duration;
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
use std::{path::Path, time::Duration};
|
||||
use tracing::info;
|
||||
|
||||
/// This is the suffix for gas output csv files.
|
||||
pub(crate) const GAS_OUTPUT_SUFFIX: &str = "total_gas.csv";
|
||||
@@ -15,6 +18,17 @@ pub(crate) const COMBINED_OUTPUT_SUFFIX: &str = "combined_latency.csv";
|
||||
/// This is the suffix for new payload output csv files.
|
||||
pub(crate) const NEW_PAYLOAD_OUTPUT_SUFFIX: &str = "new_payload_latency.csv";
|
||||
|
||||
/// Serialized format for gas ramp payloads on disk.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct GasRampPayloadFile {
|
||||
/// Engine API version (1-5).
|
||||
pub(crate) version: u8,
|
||||
/// The block hash for FCU.
|
||||
pub(crate) block_hash: B256,
|
||||
/// The params to pass to newPayload.
|
||||
pub(crate) params: serde_json::Value,
|
||||
}
|
||||
|
||||
/// This represents the results of a single `newPayload` call in the benchmark, containing the gas
|
||||
/// used and the `newPayload` latency.
|
||||
#[derive(Debug)]
|
||||
@@ -67,6 +81,8 @@ impl Serialize for NewPayloadResult {
|
||||
pub(crate) struct CombinedResult {
|
||||
/// The block number of the block being processed.
|
||||
pub(crate) block_number: u64,
|
||||
/// The gas limit of the block.
|
||||
pub(crate) gas_limit: u64,
|
||||
/// The number of transactions in the block.
|
||||
pub(crate) transaction_count: u64,
|
||||
/// The `newPayload` result.
|
||||
@@ -88,7 +104,7 @@ impl std::fmt::Display for CombinedResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Payload {} processed at {:.4} Ggas/s, used {} total gas. Combined gas per second: {:.4} Ggas/s. fcu latency: {:?}, newPayload latency: {:?}",
|
||||
"Block {} processed at {:.4} Ggas/s, used {} total gas. Combined: {:.4} Ggas/s. fcu: {:?}, newPayload: {:?}",
|
||||
self.block_number,
|
||||
self.new_payload_result.gas_per_second() / GIGAGAS as f64,
|
||||
self.new_payload_result.gas_used,
|
||||
@@ -110,10 +126,11 @@ impl Serialize for CombinedResult {
|
||||
let fcu_latency = self.fcu_latency.as_micros();
|
||||
let new_payload_latency = self.new_payload_result.latency.as_micros();
|
||||
let total_latency = self.total_latency.as_micros();
|
||||
let mut state = serializer.serialize_struct("CombinedResult", 6)?;
|
||||
let mut state = serializer.serialize_struct("CombinedResult", 7)?;
|
||||
|
||||
// flatten the new payload result because this is meant for CSV writing
|
||||
state.serialize_field("block_number", &self.block_number)?;
|
||||
state.serialize_field("gas_limit", &self.gas_limit)?;
|
||||
state.serialize_field("transaction_count", &self.transaction_count)?;
|
||||
state.serialize_field("gas_used", &self.new_payload_result.gas_used)?;
|
||||
state.serialize_field("new_payload_latency", &new_payload_latency)?;
|
||||
@@ -167,6 +184,36 @@ impl TotalGasOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write benchmark results to CSV files.
|
||||
///
|
||||
/// Writes two files to the output directory:
|
||||
/// - `combined_latency.csv`: Per-block latency results
|
||||
/// - `total_gas.csv`: Per-block gas usage over time
|
||||
pub(crate) fn write_benchmark_results(
|
||||
output_dir: &Path,
|
||||
gas_results: &[TotalGasRow],
|
||||
combined_results: Vec<CombinedResult>,
|
||||
) -> eyre::Result<()> {
|
||||
let output_path = output_dir.join(COMBINED_OUTPUT_SUFFIX);
|
||||
info!("Writing engine api call latency output to file: {:?}", output_path);
|
||||
let mut writer = Writer::from_path(&output_path)?;
|
||||
for result in combined_results {
|
||||
writer.serialize(result)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
let output_path = output_dir.join(GAS_OUTPUT_SUFFIX);
|
||||
info!("Writing total gas output to file: {:?}", output_path);
|
||||
let mut writer = Writer::from_path(&output_path)?;
|
||||
for row in gas_results {
|
||||
writer.serialize(row)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
info!("Finished writing benchmark output files to {:?}.", output_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This serializes the `time` field of the [`TotalGasRow`] to microseconds.
|
||||
///
|
||||
/// This is essentially just for the csv writer, which would have headers
|
||||
|
||||
332
bin/reth-bench/src/bench/replay_payloads.rs
Normal file
332
bin/reth-bench/src/bench/replay_payloads.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
//! Command for replaying pre-generated payloads from disk.
|
||||
//!
|
||||
//! This command reads `ExecutionPayloadEnvelopeV4` files from a directory and replays them
|
||||
//! in sequence using `newPayload` followed by `forkchoiceUpdated`.
|
||||
|
||||
use crate::{
|
||||
authenticated_transport::AuthenticatedTransportConnect,
|
||||
bench::output::GasRampPayloadFile,
|
||||
valid_payload::{call_forkchoice_updated, call_new_payload},
|
||||
};
|
||||
use alloy_primitives::B256;
|
||||
use alloy_provider::{ext::EngineApi, network::AnyNetwork, Provider, RootProvider};
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_rpc_types_engine::{ExecutionPayloadEnvelopeV4, ForkchoiceState, JwtSecret};
|
||||
use clap::Parser;
|
||||
use eyre::Context;
|
||||
use reqwest::Url;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use std::path::PathBuf;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// `reth bench replay-payloads` command
|
||||
///
|
||||
/// Replays pre-generated payloads from a directory by calling `newPayload` followed by
|
||||
/// `forkchoiceUpdated` for each payload in sequence.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// The engine RPC URL (with JWT authentication).
|
||||
#[arg(long, value_name = "ENGINE_RPC_URL", default_value = "http://localhost:8551")]
|
||||
engine_rpc_url: String,
|
||||
|
||||
/// Path to the JWT secret file for engine API authentication.
|
||||
#[arg(long, value_name = "JWT_SECRET")]
|
||||
jwt_secret: PathBuf,
|
||||
|
||||
/// Directory containing payload files (`payload_block_N.json`).
|
||||
#[arg(long, value_name = "PAYLOAD_DIR")]
|
||||
payload_dir: PathBuf,
|
||||
|
||||
/// Optional limit on the number of payloads to replay.
|
||||
/// If not specified, replays all payloads in the directory.
|
||||
#[arg(long, value_name = "COUNT")]
|
||||
count: Option<usize>,
|
||||
|
||||
/// Skip the first N payloads.
|
||||
#[arg(long, value_name = "SKIP", default_value = "0")]
|
||||
skip: usize,
|
||||
|
||||
/// Optional directory containing gas ramp payloads to replay first.
|
||||
/// These are replayed before the main payloads to warm up the gas limit.
|
||||
#[arg(long, value_name = "GAS_RAMP_DIR")]
|
||||
gas_ramp_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// A loaded payload ready for execution.
|
||||
struct LoadedPayload {
|
||||
/// The index (from filename).
|
||||
index: u64,
|
||||
/// The payload envelope.
|
||||
envelope: ExecutionPayloadEnvelopeV4,
|
||||
/// The block hash.
|
||||
block_hash: B256,
|
||||
}
|
||||
|
||||
/// A gas ramp payload loaded from disk.
|
||||
struct GasRampPayload {
|
||||
/// Block number from filename.
|
||||
block_number: u64,
|
||||
/// Engine API version for newPayload.
|
||||
version: EngineApiMessageVersion,
|
||||
/// The file contents.
|
||||
file: GasRampPayloadFile,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute the `replay-payloads` command.
|
||||
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
|
||||
info!(payload_dir = %self.payload_dir.display(), "Replaying payloads");
|
||||
|
||||
// Set up authenticated engine provider
|
||||
let jwt =
|
||||
std::fs::read_to_string(&self.jwt_secret).wrap_err("Failed to read JWT secret file")?;
|
||||
let jwt = JwtSecret::from_hex(jwt.trim())?;
|
||||
let auth_url = Url::parse(&self.engine_rpc_url)?;
|
||||
|
||||
info!("Connecting to Engine RPC at {}", auth_url);
|
||||
let auth_transport = AuthenticatedTransportConnect::new(auth_url.clone(), jwt);
|
||||
let auth_client = ClientBuilder::default().connect_with(auth_transport).await?;
|
||||
let auth_provider = RootProvider::<AnyNetwork>::new(auth_client);
|
||||
|
||||
// Get parent block (latest canonical block) - we need this for the first FCU
|
||||
let parent_block = auth_provider
|
||||
.get_block_by_number(alloy_eips::BlockNumberOrTag::Latest)
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?;
|
||||
|
||||
let initial_parent_hash = parent_block.header.hash;
|
||||
let initial_parent_number = parent_block.header.number;
|
||||
|
||||
info!(
|
||||
parent_hash = %initial_parent_hash,
|
||||
parent_number = initial_parent_number,
|
||||
"Using initial parent block"
|
||||
);
|
||||
|
||||
// Load all payloads upfront to avoid I/O delays between phases
|
||||
let gas_ramp_payloads = if let Some(ref gas_ramp_dir) = self.gas_ramp_dir {
|
||||
let payloads = self.load_gas_ramp_payloads(gas_ramp_dir)?;
|
||||
if payloads.is_empty() {
|
||||
return Err(eyre::eyre!("No gas ramp payload files found in {:?}", gas_ramp_dir));
|
||||
}
|
||||
info!(count = payloads.len(), "Loaded gas ramp payloads from disk");
|
||||
payloads
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let payloads = self.load_payloads()?;
|
||||
if payloads.is_empty() {
|
||||
return Err(eyre::eyre!("No payload files found in {:?}", self.payload_dir));
|
||||
}
|
||||
info!(count = payloads.len(), "Loaded main payloads from disk");
|
||||
|
||||
let mut parent_hash = initial_parent_hash;
|
||||
|
||||
// Replay gas ramp payloads first
|
||||
for (i, payload) in gas_ramp_payloads.iter().enumerate() {
|
||||
info!(
|
||||
gas_ramp_payload = i + 1,
|
||||
total = gas_ramp_payloads.len(),
|
||||
block_number = payload.block_number,
|
||||
block_hash = %payload.file.block_hash,
|
||||
"Executing gas ramp payload (newPayload + FCU)"
|
||||
);
|
||||
|
||||
call_new_payload(&auth_provider, payload.version, payload.file.params.clone()).await?;
|
||||
|
||||
let fcu_state = ForkchoiceState {
|
||||
head_block_hash: payload.file.block_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
call_forkchoice_updated(&auth_provider, payload.version, fcu_state, None).await?;
|
||||
|
||||
info!(gas_ramp_payload = i + 1, "Gas ramp payload executed successfully");
|
||||
parent_hash = payload.file.block_hash;
|
||||
}
|
||||
|
||||
if !gas_ramp_payloads.is_empty() {
|
||||
info!(count = gas_ramp_payloads.len(), "All gas ramp payloads replayed");
|
||||
}
|
||||
|
||||
for (i, payload) in payloads.iter().enumerate() {
|
||||
info!(
|
||||
payload = i + 1,
|
||||
total = payloads.len(),
|
||||
index = payload.index,
|
||||
block_hash = %payload.block_hash,
|
||||
"Executing payload (newPayload + FCU)"
|
||||
);
|
||||
|
||||
self.execute_payload_v4(&auth_provider, &payload.envelope, parent_hash).await?;
|
||||
|
||||
info!(payload = i + 1, "Payload executed successfully");
|
||||
parent_hash = payload.block_hash;
|
||||
}
|
||||
|
||||
info!(count = payloads.len(), "All payloads replayed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load and parse all payload files from the directory.
|
||||
fn load_payloads(&self) -> eyre::Result<Vec<LoadedPayload>> {
|
||||
let mut payloads = Vec::new();
|
||||
|
||||
// Read directory entries
|
||||
let entries: Vec<_> = std::fs::read_dir(&self.payload_dir)
|
||||
.wrap_err_with(|| format!("Failed to read directory {:?}", self.payload_dir))?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.path().extension().and_then(|s| s.to_str()) == Some("json") &&
|
||||
e.file_name().to_string_lossy().starts_with("payload_block_")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Parse filenames to get indices and sort
|
||||
let mut indexed_paths: Vec<(u64, PathBuf)> = entries
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Extract index from "payload_NNN.json"
|
||||
let index_str = name_str.strip_prefix("payload_block_")?.strip_suffix(".json")?;
|
||||
let index: u64 = index_str.parse().ok()?;
|
||||
Some((index, e.path()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
indexed_paths.sort_by_key(|(idx, _)| *idx);
|
||||
|
||||
// Apply skip and count
|
||||
let indexed_paths: Vec<_> = indexed_paths.into_iter().skip(self.skip).collect();
|
||||
let indexed_paths: Vec<_> = match self.count {
|
||||
Some(count) => indexed_paths.into_iter().take(count).collect(),
|
||||
None => indexed_paths,
|
||||
};
|
||||
|
||||
// Load each payload
|
||||
for (index, path) in indexed_paths {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.wrap_err_with(|| format!("Failed to read {:?}", path))?;
|
||||
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
|
||||
let block_hash =
|
||||
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.block_hash;
|
||||
|
||||
info!(
|
||||
index = index,
|
||||
block_hash = %block_hash,
|
||||
path = %path.display(),
|
||||
"Loaded payload"
|
||||
);
|
||||
|
||||
payloads.push(LoadedPayload { index, envelope, block_hash });
|
||||
}
|
||||
|
||||
Ok(payloads)
|
||||
}
|
||||
|
||||
/// Load and parse gas ramp payload files from a directory.
|
||||
fn load_gas_ramp_payloads(&self, dir: &PathBuf) -> eyre::Result<Vec<GasRampPayload>> {
|
||||
let mut payloads = Vec::new();
|
||||
|
||||
let entries: Vec<_> = std::fs::read_dir(dir)
|
||||
.wrap_err_with(|| format!("Failed to read directory {:?}", dir))?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.path().extension().and_then(|s| s.to_str()) == Some("json") &&
|
||||
e.file_name().to_string_lossy().starts_with("payload_block_")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Parse filenames to get block numbers and sort
|
||||
let mut indexed_paths: Vec<(u64, PathBuf)> = entries
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Extract block number from "payload_block_NNN.json"
|
||||
let block_str = name_str.strip_prefix("payload_block_")?.strip_suffix(".json")?;
|
||||
let block_number: u64 = block_str.parse().ok()?;
|
||||
Some((block_number, e.path()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
indexed_paths.sort_by_key(|(num, _)| *num);
|
||||
|
||||
for (block_number, path) in indexed_paths {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.wrap_err_with(|| format!("Failed to read {:?}", path))?;
|
||||
let file: GasRampPayloadFile = serde_json::from_str(&content)
|
||||
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
|
||||
|
||||
let version = match file.version {
|
||||
1 => EngineApiMessageVersion::V1,
|
||||
2 => EngineApiMessageVersion::V2,
|
||||
3 => EngineApiMessageVersion::V3,
|
||||
4 => EngineApiMessageVersion::V4,
|
||||
5 => EngineApiMessageVersion::V5,
|
||||
v => return Err(eyre::eyre!("Invalid version {} in {:?}", v, path)),
|
||||
};
|
||||
|
||||
info!(
|
||||
block_number,
|
||||
block_hash = %file.block_hash,
|
||||
path = %path.display(),
|
||||
"Loaded gas ramp payload"
|
||||
);
|
||||
|
||||
payloads.push(GasRampPayload { block_number, version, file });
|
||||
}
|
||||
|
||||
Ok(payloads)
|
||||
}
|
||||
|
||||
async fn execute_payload_v4(
|
||||
&self,
|
||||
provider: &RootProvider<AnyNetwork>,
|
||||
envelope: &ExecutionPayloadEnvelopeV4,
|
||||
parent_hash: B256,
|
||||
) -> eyre::Result<()> {
|
||||
let block_hash =
|
||||
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.block_hash;
|
||||
|
||||
debug!(
|
||||
method = "engine_newPayloadV4",
|
||||
block_hash = %block_hash,
|
||||
"Sending newPayload"
|
||||
);
|
||||
|
||||
let status = provider
|
||||
.new_payload_v4(
|
||||
envelope.envelope_inner.execution_payload.clone(),
|
||||
vec![],
|
||||
B256::ZERO,
|
||||
envelope.execution_requests.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(?status, "newPayloadV4 response");
|
||||
|
||||
if !status.is_valid() {
|
||||
return Err(eyre::eyre!("Payload rejected: {:?}", status));
|
||||
}
|
||||
|
||||
let fcu_state = ForkchoiceState {
|
||||
head_block_hash: block_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
finalized_block_hash: parent_hash,
|
||||
};
|
||||
|
||||
debug!(method = "engine_forkchoiceUpdatedV3", ?fcu_state, "Sending forkchoiceUpdated");
|
||||
|
||||
let fcu_result = provider.fork_choice_updated_v3(fcu_state, None).await?;
|
||||
|
||||
info!(?fcu_result, "forkchoiceUpdatedV3 response");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,8 @@ use std::ops::RangeInclusive;
|
||||
/// Whether or not the benchmark should run as a continuous stream of payloads.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum BenchMode {
|
||||
// TODO: just include the start block in `Continuous`
|
||||
/// Run the benchmark as a continuous stream of payloads, until the benchmark is interrupted.
|
||||
Continuous,
|
||||
Continuous(u64),
|
||||
/// Run the benchmark for a specific range of blocks.
|
||||
Range(RangeInclusive<u64>),
|
||||
}
|
||||
@@ -16,18 +15,19 @@ impl BenchMode {
|
||||
/// Check if the block number is in the range
|
||||
pub fn contains(&self, block_number: u64) -> bool {
|
||||
match self {
|
||||
Self::Continuous => true,
|
||||
Self::Continuous(start) => block_number >= *start,
|
||||
Self::Range(range) => range.contains(&block_number),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`BenchMode`] from optional `from` and `to` fields.
|
||||
pub fn new(from: Option<u64>, to: Option<u64>) -> Result<Self, eyre::Error> {
|
||||
pub fn new(from: Option<u64>, to: Option<u64>, latest_block: u64) -> Result<Self, eyre::Error> {
|
||||
// If neither `--from` nor `--to` are provided, we will run the benchmark continuously,
|
||||
// starting at the latest block.
|
||||
match (from, to) {
|
||||
(Some(from), Some(to)) => Ok(Self::Range(from..=to)),
|
||||
(None, None) => Ok(Self::Continuous),
|
||||
(None, None) => Ok(Self::Continuous(latest_block)),
|
||||
(Some(start), None) => Ok(Self::Continuous(start)),
|
||||
_ => {
|
||||
// both or neither are allowed, everything else is ambiguous
|
||||
Err(eyre::eyre!("`from` and `to` must be provided together, or not at all."))
|
||||
|
||||
@@ -23,7 +23,7 @@ use bench::BenchmarkCommand;
|
||||
use clap::Parser;
|
||||
use reth_cli_runner::CliRunner;
|
||||
|
||||
fn main() {
|
||||
fn main() -> eyre::Result<()> {
|
||||
// Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided.
|
||||
if std::env::var_os("RUST_BACKTRACE").is_none() {
|
||||
unsafe {
|
||||
@@ -31,12 +31,11 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
// Run until either exit or sigint or sigterm
|
||||
let runner = CliRunner::try_default_runtime().unwrap();
|
||||
runner
|
||||
.run_command_until_exit(|ctx| {
|
||||
let command = BenchmarkCommand::parse();
|
||||
command.execute(ctx)
|
||||
})
|
||||
.unwrap();
|
||||
let runner = CliRunner::try_default_runtime()?;
|
||||
runner.run_command_until_exit(|ctx| BenchmarkCommand::parse().execute(ctx))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
//! before sending additional calls.
|
||||
|
||||
use alloy_eips::eip7685::Requests;
|
||||
use alloy_primitives::B256;
|
||||
use alloy_provider::{ext::EngineApi, network::AnyRpcBlock, Network, Provider};
|
||||
use alloy_rpc_types_engine::{
|
||||
ExecutionPayload, ExecutionPayloadInputV2, ForkchoiceState, ForkchoiceUpdated,
|
||||
PayloadAttributes, PayloadStatus,
|
||||
ExecutionPayload, ExecutionPayloadInputV2, ExecutionPayloadSidecar, ForkchoiceState,
|
||||
ForkchoiceUpdated, PayloadAttributes, PayloadStatus,
|
||||
};
|
||||
use alloy_transport::TransportResult;
|
||||
use op_alloy_rpc_types_engine::OpExecutionPayloadV4;
|
||||
use reth_node_api::EngineApiMessageVersion;
|
||||
use tracing::error;
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// An extension trait for providers that implement the engine API, to wait for a VALID response.
|
||||
#[async_trait::async_trait]
|
||||
@@ -52,6 +53,13 @@ where
|
||||
fork_choice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
debug!(
|
||||
method = "engine_forkchoiceUpdatedV1",
|
||||
?fork_choice_state,
|
||||
?payload_attributes,
|
||||
"Sending forkchoiceUpdated"
|
||||
);
|
||||
|
||||
let mut status =
|
||||
self.fork_choice_updated_v1(fork_choice_state, payload_attributes.clone()).await?;
|
||||
|
||||
@@ -82,6 +90,13 @@ where
|
||||
fork_choice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
debug!(
|
||||
method = "engine_forkchoiceUpdatedV2",
|
||||
?fork_choice_state,
|
||||
?payload_attributes,
|
||||
"Sending forkchoiceUpdated"
|
||||
);
|
||||
|
||||
let mut status =
|
||||
self.fork_choice_updated_v2(fork_choice_state, payload_attributes.clone()).await?;
|
||||
|
||||
@@ -112,6 +127,13 @@ where
|
||||
fork_choice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
debug!(
|
||||
method = "engine_forkchoiceUpdatedV3",
|
||||
?fork_choice_state,
|
||||
?payload_attributes,
|
||||
"Sending forkchoiceUpdated"
|
||||
);
|
||||
|
||||
let mut status =
|
||||
self.fork_choice_updated_v3(fork_choice_state, payload_attributes.clone()).await?;
|
||||
|
||||
@@ -148,33 +170,47 @@ pub(crate) fn block_to_new_payload(
|
||||
|
||||
// Convert to execution payload
|
||||
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
|
||||
payload_to_new_payload(payload, sidecar, is_optimism, block.withdrawals_root, None)
|
||||
}
|
||||
|
||||
pub(crate) fn payload_to_new_payload(
|
||||
payload: ExecutionPayload,
|
||||
sidecar: ExecutionPayloadSidecar,
|
||||
is_optimism: bool,
|
||||
withdrawals_root: Option<B256>,
|
||||
target_version: Option<EngineApiMessageVersion>,
|
||||
) -> eyre::Result<(EngineApiMessageVersion, serde_json::Value)> {
|
||||
let (version, params) = match payload {
|
||||
ExecutionPayload::V3(payload) => {
|
||||
let cancun = sidecar.cancun().unwrap();
|
||||
|
||||
if let Some(prague) = sidecar.prague() {
|
||||
// Use target version if provided (for Osaka), otherwise default to V4
|
||||
let version = target_version.unwrap_or(EngineApiMessageVersion::V4);
|
||||
|
||||
if is_optimism {
|
||||
let withdrawals_root = withdrawals_root.ok_or_else(|| {
|
||||
eyre::eyre!("Missing withdrawals root for Optimism payload")
|
||||
})?;
|
||||
(
|
||||
EngineApiMessageVersion::V4,
|
||||
version,
|
||||
serde_json::to_value((
|
||||
OpExecutionPayloadV4 {
|
||||
payload_inner: payload,
|
||||
withdrawals_root: block.withdrawals_root.unwrap(),
|
||||
},
|
||||
OpExecutionPayloadV4 { payload_inner: payload, withdrawals_root },
|
||||
cancun.versioned_hashes.clone(),
|
||||
cancun.parent_beacon_block_root,
|
||||
Requests::default(),
|
||||
))?,
|
||||
)
|
||||
} else {
|
||||
// Extract actual Requests from RequestsOrHash
|
||||
let requests = prague.requests.requests_hash();
|
||||
(
|
||||
EngineApiMessageVersion::V4,
|
||||
version,
|
||||
serde_json::to_value((
|
||||
payload,
|
||||
cancun.versioned_hashes.clone(),
|
||||
cancun.parent_beacon_block_root,
|
||||
prague.requests.requests_hash(),
|
||||
requests,
|
||||
))?,
|
||||
)
|
||||
}
|
||||
@@ -217,6 +253,8 @@ pub(crate) async fn call_new_payload<N: Network, P: Provider<N>>(
|
||||
) -> TransportResult<()> {
|
||||
let method = version.method_name();
|
||||
|
||||
debug!(method, "Sending newPayload");
|
||||
|
||||
let mut status: PayloadStatus = provider.client().request(method, ¶ms).await?;
|
||||
|
||||
while !status.is_valid() {
|
||||
@@ -237,12 +275,15 @@ pub(crate) async fn call_new_payload<N: Network, P: Provider<N>>(
|
||||
/// Calls the correct `engine_forkchoiceUpdated` method depending on the given
|
||||
/// `EngineApiMessageVersion`, using the provided forkchoice state and payload attributes for the
|
||||
/// actual engine api message call.
|
||||
///
|
||||
/// Note: For Prague (V4), we still use forkchoiceUpdatedV3 as there is no V4.
|
||||
pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
|
||||
provider: P,
|
||||
message_version: EngineApiMessageVersion,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> TransportResult<ForkchoiceUpdated> {
|
||||
// FCU V3 is used for both Cancun and Prague (there is no FCU V4)
|
||||
match message_version {
|
||||
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
|
||||
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
|
||||
|
||||
@@ -81,12 +81,16 @@ backon.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["jemalloc", "otlp", "reth-revm/portable", "js-tracer", "keccak-cache-global"]
|
||||
default = ["jemalloc", "otlp", "otlp-logs", "reth-revm/portable", "js-tracer", "keccak-cache-global", "asm-keccak"]
|
||||
|
||||
otlp = [
|
||||
"reth-ethereum-cli/otlp",
|
||||
"reth-node-core/otlp",
|
||||
]
|
||||
otlp-logs = [
|
||||
"reth-ethereum-cli/otlp-logs",
|
||||
"reth-node-core/otlp-logs",
|
||||
]
|
||||
js-tracer = [
|
||||
"reth-node-builder/js-tracer",
|
||||
"reth-node-ethereum/js-tracer",
|
||||
@@ -103,6 +107,7 @@ asm-keccak = [
|
||||
"reth-node-ethereum/asm-keccak",
|
||||
]
|
||||
keccak-cache-global = [
|
||||
"reth-node-core/keccak-cache-global",
|
||||
"reth-node-ethereum/keccak-cache-global",
|
||||
]
|
||||
jemalloc = [
|
||||
@@ -115,6 +120,11 @@ jemalloc-prof = [
|
||||
"reth-cli-util/jemalloc",
|
||||
"reth-cli-util/jemalloc-prof",
|
||||
"reth-ethereum-cli/jemalloc-prof",
|
||||
"reth-node-metrics/jemalloc-prof",
|
||||
]
|
||||
jemalloc-symbols = [
|
||||
"jemalloc-prof",
|
||||
"reth-ethereum-cli/jemalloc-symbols",
|
||||
]
|
||||
jemalloc-unprefixed = [
|
||||
"reth-cli-util/jemalloc-unprefixed",
|
||||
@@ -125,6 +135,11 @@ jemalloc-unprefixed = [
|
||||
tracy-allocator = [
|
||||
"reth-cli-util/tracy-allocator",
|
||||
"reth-ethereum-cli/tracy-allocator",
|
||||
"tracy",
|
||||
]
|
||||
tracy = [
|
||||
"reth-ethereum-cli/tracy",
|
||||
"reth-node-core/tracy",
|
||||
]
|
||||
|
||||
# Because jemalloc is default and preferred over snmalloc when both features are
|
||||
@@ -165,6 +180,8 @@ min-trace-logs = [
|
||||
"reth-node-core/min-trace-logs",
|
||||
]
|
||||
|
||||
edge = ["reth-ethereum-cli/edge", "reth-node-core/edge"]
|
||||
|
||||
[[bin]]
|
||||
name = "reth"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -2,22 +2,46 @@
|
||||
//!
|
||||
//! ## Feature Flags
|
||||
//!
|
||||
//! ### Default Features
|
||||
//!
|
||||
//! - `jemalloc`: Uses [jemallocator](https://github.com/tikv/jemallocator) as the global allocator.
|
||||
//! This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc)
|
||||
//! for more info.
|
||||
//! - `otlp`: Enables [OpenTelemetry](https://opentelemetry.io/) metrics export to a configured OTLP
|
||||
//! collector endpoint.
|
||||
//! - `js-tracer`: Enables the `JavaScript` tracer for the `debug_trace` endpoints, allowing custom
|
||||
//! `JavaScript`-based transaction tracing.
|
||||
//! - `keccak-cache-global`: Enables global caching for Keccak256 hashes to improve performance.
|
||||
//! - `asm-keccak`: Replaces the default, pure-Rust implementation of Keccak256 with one implemented
|
||||
//! in assembly; see [the `keccak-asm` crate](https://github.com/DaniPopes/keccak-asm) for more
|
||||
//! details and supported targets.
|
||||
//!
|
||||
//! ### Allocator Features
|
||||
//!
|
||||
//! - `jemalloc-prof`: Enables [jemallocator's](https://github.com/tikv/jemallocator) heap profiling
|
||||
//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof)
|
||||
//! documentation for usage details. This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc)
|
||||
//! for more info.
|
||||
//! - `asm-keccak`: replaces the default, pure-Rust implementation of Keccak256 with one implemented
|
||||
//! in assembly; see [the `keccak-asm` crate](https://github.com/DaniPopes/keccak-asm) for more
|
||||
//! details and supported targets
|
||||
//! documentation for usage details. This is **not recommended on Windows**.
|
||||
//! - `jemalloc-symbols`: Enables jemalloc symbols for profiling. Includes `jemalloc-prof`.
|
||||
//! - `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/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.
|
||||
//!
|
||||
//! ### Log Level Features
|
||||
//!
|
||||
//! - `min-error-logs`: Disables all logs below `error` level.
|
||||
//! - `min-warn-logs`: Disables all logs below `warn` level.
|
||||
//! - `min-info-logs`: Disables all logs below `info` level. This can speed up the node, since fewer
|
||||
//! calls to the logging component are made.
|
||||
//! - `min-debug-logs`: Disables all logs below `debug` level.
|
||||
//! - `min-trace-logs`: Disables all logs below `trace` level.
|
||||
//!
|
||||
//! ### Development Features
|
||||
//!
|
||||
//! - `dev`: Enables development mode features, including test vector generation commands.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
@@ -170,7 +194,7 @@ pub mod rpc {
|
||||
pub use reth_rpc::eth::*;
|
||||
}
|
||||
|
||||
/// Re-exported from `reth_rpc::rpc`.
|
||||
/// Re-exported from `reth_rpc_server_types::result`.
|
||||
pub mod result {
|
||||
pub use reth_rpc_server_types::result::*;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#[global_allocator]
|
||||
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
|
||||
|
||||
#[cfg(all(feature = "jemalloc-prof", unix))]
|
||||
#[unsafe(export_name = "_rjem_malloc_conf")]
|
||||
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
|
||||
|
||||
use clap::Parser;
|
||||
use reth::{args::RessArgs, cli::Cli, ress::install_ress_subprotocol};
|
||||
use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
|
||||
|
||||
@@ -41,6 +41,7 @@ derive_more.workspace = true
|
||||
metrics.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pin-project.workspace = true
|
||||
rayon = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
# optional deps for test-utils
|
||||
@@ -84,6 +85,7 @@ test-utils = [
|
||||
"reth-trie/test-utils",
|
||||
"reth-ethereum-primitives/test-utils",
|
||||
]
|
||||
rayon = ["dep:rayon"]
|
||||
|
||||
[[bench]]
|
||||
name = "canonical_hashes_range"
|
||||
|
||||
@@ -33,6 +33,7 @@ where
|
||||
) -> Self {
|
||||
let (finalized_block, _) = watch::channel(finalized);
|
||||
let (safe_block, _) = watch::channel(safe);
|
||||
let (persisted_block, _) = watch::channel(None);
|
||||
|
||||
Self {
|
||||
inner: Arc::new(ChainInfoInner {
|
||||
@@ -42,6 +43,7 @@ where
|
||||
canonical_head: RwLock::new(head),
|
||||
safe_block,
|
||||
finalized_block,
|
||||
persisted_block,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -97,6 +99,11 @@ where
|
||||
self.inner.finalized_block.borrow().as_ref().map(SealedHeader::num_hash)
|
||||
}
|
||||
|
||||
/// Returns the `BlockNumHash` of the persisted block.
|
||||
pub fn get_persisted_num_hash(&self) -> Option<BlockNumHash> {
|
||||
*self.inner.persisted_block.borrow()
|
||||
}
|
||||
|
||||
/// Sets the canonical head of the chain.
|
||||
pub fn set_canonical_head(&self, header: SealedHeader<N::BlockHeader>) {
|
||||
let number = header.number();
|
||||
@@ -130,6 +137,18 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets the persisted block of the chain.
|
||||
pub fn set_persisted(&self, num_hash: BlockNumHash) {
|
||||
self.inner.persisted_block.send_if_modified(|current| {
|
||||
if current.map(|b| b.hash) != Some(num_hash.hash) {
|
||||
let _ = current.replace(num_hash);
|
||||
return true
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
/// Subscribe to the finalized block.
|
||||
pub fn subscribe_finalized_block(
|
||||
&self,
|
||||
@@ -141,6 +160,11 @@ where
|
||||
pub fn subscribe_safe_block(&self) -> watch::Receiver<Option<SealedHeader<N::BlockHeader>>> {
|
||||
self.inner.safe_block.subscribe()
|
||||
}
|
||||
|
||||
/// Subscribe to the persisted block.
|
||||
pub fn subscribe_persisted_block(&self) -> watch::Receiver<Option<BlockNumHash>> {
|
||||
self.inner.persisted_block.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
/// Container type for all chain info fields
|
||||
@@ -159,11 +183,14 @@ struct ChainInfoInner<N: NodePrimitives = reth_ethereum_primitives::EthPrimitive
|
||||
safe_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
|
||||
/// The block that the beacon node considers finalized.
|
||||
finalized_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
|
||||
/// The last block that was persisted to disk.
|
||||
persisted_block: watch::Sender<Option<BlockNumHash>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::B256;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_testing_utils::{generators, generators::random_header};
|
||||
|
||||
@@ -338,4 +365,28 @@ mod tests {
|
||||
// Assert that the BlockNumHash returned matches the safe header
|
||||
assert_eq!(tracker.get_safe_num_hash(), Some(safe_header.num_hash()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_persisted() {
|
||||
let mut rng = generators::rng();
|
||||
let header = random_header(&mut rng, 10, None);
|
||||
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header, None, None);
|
||||
|
||||
// Initial state: persisted block should be None
|
||||
assert!(tracker.get_persisted_num_hash().is_none());
|
||||
|
||||
// Set a persisted block
|
||||
let num_hash1 = BlockNumHash::new(10, B256::random());
|
||||
tracker.set_persisted(num_hash1);
|
||||
assert_eq!(tracker.get_persisted_num_hash(), Some(num_hash1));
|
||||
|
||||
// Setting the same block again should not change anything
|
||||
tracker.set_persisted(num_hash1);
|
||||
assert_eq!(tracker.get_persisted_num_hash(), Some(num_hash1));
|
||||
|
||||
// Set a different block
|
||||
let num_hash2 = BlockNumHash::new(20, B256::random());
|
||||
tracker.set_persisted(num_hash2);
|
||||
assert_eq!(tracker.get_persisted_num_hash(), Some(num_hash2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,19 @@ pub struct ComputedTrieData {
|
||||
|
||||
/// Trie input bundled with its anchor hash.
|
||||
///
|
||||
/// This is used to store the trie input and anchor hash for a block together.
|
||||
/// The `trie_input` contains the **cumulative** overlay of all in-memory ancestor blocks,
|
||||
/// not just this block's changes. Child blocks reuse the parent's overlay in O(1) by
|
||||
/// cloning the Arc-wrapped data.
|
||||
///
|
||||
/// The `anchor_hash` is metadata indicating which persisted base state this overlay
|
||||
/// sits on top of. It is CRITICAL for overlay reuse decisions: an overlay built on top
|
||||
/// of Anchor A cannot be reused for a block anchored to Anchor B, as it would result
|
||||
/// in an incorrect state.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnchoredTrieInput {
|
||||
/// The persisted ancestor hash this trie input is anchored to.
|
||||
pub anchor_hash: B256,
|
||||
/// Trie input constructed from in-memory overlays.
|
||||
/// Cumulative trie input overlay from all in-memory ancestors.
|
||||
pub trie_input: Arc<TrieInputSorted>,
|
||||
}
|
||||
|
||||
@@ -62,7 +69,8 @@ static DEFERRED_TRIE_METRICS: LazyLock<DeferredTrieMetrics> =
|
||||
/// Internal state for deferred trie data.
|
||||
enum DeferredState {
|
||||
/// Data is not yet available; raw inputs stored for fallback computation.
|
||||
Pending(PendingInputs),
|
||||
/// Wrapped in `Option` to allow taking ownership during computation.
|
||||
Pending(Option<PendingInputs>),
|
||||
/// Data has been computed and is ready.
|
||||
Ready(ComputedTrieData),
|
||||
}
|
||||
@@ -112,12 +120,12 @@ impl DeferredTrieData {
|
||||
ancestors: Vec<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(DeferredState::Pending(PendingInputs {
|
||||
state: Arc::new(Mutex::new(DeferredState::Pending(Some(PendingInputs {
|
||||
hashed_state,
|
||||
trie_updates,
|
||||
anchor_hash,
|
||||
ancestors,
|
||||
}))),
|
||||
})))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +146,9 @@ impl DeferredTrieData {
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Sort the current block's hashed state and trie updates
|
||||
/// 2. Merge ancestor overlays (oldest -> newest, so later state takes precedence)
|
||||
/// 3. Extend the merged overlay with this block's sorted data
|
||||
/// 2. Reuse parent's cached overlay if available (O(1) - the common case)
|
||||
/// 3. Otherwise, rebuild overlay from ancestors (rare fallback)
|
||||
/// 4. Extend the overlay with this block's sorted data
|
||||
///
|
||||
/// Used by both the async background task and the synchronous fallback path.
|
||||
///
|
||||
@@ -147,49 +156,149 @@ impl DeferredTrieData {
|
||||
/// * `hashed_state` - Unsorted hashed post-state (account/storage changes) from execution
|
||||
/// * `trie_updates` - Unsorted trie node updates from state root computation
|
||||
/// * `anchor_hash` - The persisted ancestor hash this trie input is anchored to
|
||||
/// * `ancestors` - Deferred trie data from ancestor blocks for merging
|
||||
/// * `ancestors` - Deferred trie data from ancestor blocks for merging (oldest -> newest)
|
||||
pub fn sort_and_build_trie_input(
|
||||
hashed_state: &HashedPostState,
|
||||
trie_updates: &TrieUpdates,
|
||||
hashed_state: Arc<HashedPostState>,
|
||||
trie_updates: Arc<TrieUpdates>,
|
||||
anchor_hash: B256,
|
||||
ancestors: &[Self],
|
||||
) -> ComputedTrieData {
|
||||
// Sort the current block's hashed state and trie updates
|
||||
let sorted_hashed_state = Arc::new(hashed_state.clone_into_sorted());
|
||||
let sorted_trie_updates = Arc::new(trie_updates.clone().into_sorted());
|
||||
#[cfg(feature = "rayon")]
|
||||
let (sorted_hashed_state, sorted_trie_updates) = rayon::join(
|
||||
|| match Arc::try_unwrap(hashed_state) {
|
||||
Ok(state) => state.into_sorted(),
|
||||
Err(arc) => arc.clone_into_sorted(),
|
||||
},
|
||||
|| match Arc::try_unwrap(trie_updates) {
|
||||
Ok(updates) => updates.into_sorted(),
|
||||
Err(arc) => arc.clone_into_sorted(),
|
||||
},
|
||||
);
|
||||
|
||||
// Merge trie data from ancestors (oldest -> newest so later state takes precedence)
|
||||
let mut overlay = TrieInputSorted::default();
|
||||
for ancestor in ancestors {
|
||||
let ancestor_data = ancestor.wait_cloned();
|
||||
{
|
||||
let state_mut = Arc::make_mut(&mut overlay.state);
|
||||
state_mut.extend_ref(ancestor_data.hashed_state.as_ref());
|
||||
}
|
||||
{
|
||||
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
|
||||
nodes_mut.extend_ref(ancestor_data.trie_updates.as_ref());
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "rayon"))]
|
||||
let (sorted_hashed_state, sorted_trie_updates) = (
|
||||
match Arc::try_unwrap(hashed_state) {
|
||||
Ok(state) => state.into_sorted(),
|
||||
Err(arc) => arc.clone_into_sorted(),
|
||||
},
|
||||
match Arc::try_unwrap(trie_updates) {
|
||||
Ok(updates) => updates.into_sorted(),
|
||||
Err(arc) => arc.clone_into_sorted(),
|
||||
},
|
||||
);
|
||||
|
||||
// Extend overlay with current block's sorted data
|
||||
{
|
||||
let state_mut = Arc::make_mut(&mut overlay.state);
|
||||
state_mut.extend_ref(sorted_hashed_state.as_ref());
|
||||
}
|
||||
{
|
||||
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
|
||||
nodes_mut.extend_ref(sorted_trie_updates.as_ref());
|
||||
}
|
||||
// Reuse parent's overlay if available and anchors match.
|
||||
// We can only reuse the parent's overlay if it was built on top of the same
|
||||
// persisted anchor. If the anchor has changed (e.g., due to persistence),
|
||||
// the parent's overlay is relative to an old state and cannot be used.
|
||||
let overlay = if let Some(parent) = ancestors.last() {
|
||||
let parent_data = parent.wait_cloned();
|
||||
|
||||
match &parent_data.anchored_trie_input {
|
||||
// Case 1: Parent has cached overlay AND anchors match.
|
||||
Some(AnchoredTrieInput { anchor_hash: parent_anchor, trie_input })
|
||||
if *parent_anchor == anchor_hash =>
|
||||
{
|
||||
// O(1): Reuse parent's overlay, extend with current block's data.
|
||||
let mut overlay = TrieInputSorted::new(
|
||||
Arc::clone(&trie_input.nodes),
|
||||
Arc::clone(&trie_input.state),
|
||||
Default::default(), // prefix_sets are per-block, not cumulative
|
||||
);
|
||||
// Only trigger COW clone if there's actually data to add.
|
||||
#[cfg(feature = "rayon")]
|
||||
{
|
||||
rayon::join(
|
||||
|| {
|
||||
if !sorted_hashed_state.is_empty() {
|
||||
Arc::make_mut(&mut overlay.state)
|
||||
.extend_ref_and_sort(&sorted_hashed_state);
|
||||
}
|
||||
},
|
||||
|| {
|
||||
if !sorted_trie_updates.is_empty() {
|
||||
Arc::make_mut(&mut overlay.nodes)
|
||||
.extend_ref_and_sort(&sorted_trie_updates);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
#[cfg(not(feature = "rayon"))]
|
||||
{
|
||||
if !sorted_hashed_state.is_empty() {
|
||||
Arc::make_mut(&mut overlay.state)
|
||||
.extend_ref_and_sort(&sorted_hashed_state);
|
||||
}
|
||||
if !sorted_trie_updates.is_empty() {
|
||||
Arc::make_mut(&mut overlay.nodes)
|
||||
.extend_ref_and_sort(&sorted_trie_updates);
|
||||
}
|
||||
}
|
||||
overlay
|
||||
}
|
||||
// Case 2: Parent exists but anchor mismatch or no cached overlay.
|
||||
// We must rebuild from the ancestors list (which only contains unpersisted blocks).
|
||||
_ => Self::merge_ancestors_into_overlay(
|
||||
ancestors,
|
||||
&sorted_hashed_state,
|
||||
&sorted_trie_updates,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
// Case 3: No in-memory ancestors (first block after persisted anchor).
|
||||
// Build overlay with just this block's data.
|
||||
Self::merge_ancestors_into_overlay(&[], &sorted_hashed_state, &sorted_trie_updates)
|
||||
};
|
||||
|
||||
ComputedTrieData::with_trie_input(
|
||||
sorted_hashed_state,
|
||||
sorted_trie_updates,
|
||||
Arc::new(sorted_hashed_state),
|
||||
Arc::new(sorted_trie_updates),
|
||||
anchor_hash,
|
||||
Arc::new(overlay),
|
||||
)
|
||||
}
|
||||
|
||||
/// Merge all ancestors and current block's data into a single overlay.
|
||||
///
|
||||
/// This is a rare fallback path, only used when no ancestor has a cached
|
||||
/// `anchored_trie_input` (e.g., blocks created via alternative constructors).
|
||||
/// In normal operation, the parent always has a cached overlay and this
|
||||
/// function is never called.
|
||||
///
|
||||
/// Iterates ancestors oldest -> newest, then extends with current block's data,
|
||||
/// so later state takes precedence.
|
||||
fn merge_ancestors_into_overlay(
|
||||
ancestors: &[Self],
|
||||
sorted_hashed_state: &HashedPostStateSorted,
|
||||
sorted_trie_updates: &TrieUpdatesSorted,
|
||||
) -> TrieInputSorted {
|
||||
let mut overlay = TrieInputSorted::default();
|
||||
|
||||
let state_mut = Arc::make_mut(&mut overlay.state);
|
||||
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
|
||||
|
||||
for ancestor in ancestors {
|
||||
let ancestor_data = ancestor.wait_cloned();
|
||||
state_mut.extend_ref_and_sort(ancestor_data.hashed_state.as_ref());
|
||||
nodes_mut.extend_ref_and_sort(ancestor_data.trie_updates.as_ref());
|
||||
}
|
||||
|
||||
// Extend with current block's sorted data last (takes precedence)
|
||||
#[cfg(feature = "rayon")]
|
||||
rayon::join(
|
||||
|| state_mut.extend_ref_and_sort(sorted_hashed_state),
|
||||
|| nodes_mut.extend_ref_and_sort(sorted_trie_updates),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "rayon"))]
|
||||
{
|
||||
state_mut.extend_ref_and_sort(sorted_hashed_state);
|
||||
nodes_mut.extend_ref_and_sort(sorted_trie_updates);
|
||||
}
|
||||
|
||||
overlay
|
||||
}
|
||||
|
||||
/// Returns trie data, computing synchronously if the async task hasn't completed.
|
||||
///
|
||||
/// - If the async task has completed (`Ready`), returns the cached result.
|
||||
@@ -204,7 +313,7 @@ impl DeferredTrieData {
|
||||
#[instrument(level = "debug", target = "engine::tree::deferred_trie", skip_all)]
|
||||
pub fn wait_cloned(&self) -> ComputedTrieData {
|
||||
let mut state = self.state.lock();
|
||||
match &*state {
|
||||
match &mut *state {
|
||||
// If the deferred trie data is ready, return the cached result.
|
||||
DeferredState::Ready(bundle) => {
|
||||
DEFERRED_TRIE_METRICS.deferred_trie_async_ready.increment(1);
|
||||
@@ -212,15 +321,23 @@ impl DeferredTrieData {
|
||||
}
|
||||
// If the deferred trie data is pending, compute the trie data synchronously and return
|
||||
// the result. This is the fallback path if the async task hasn't completed.
|
||||
DeferredState::Pending(inputs) => {
|
||||
DeferredState::Pending(maybe_inputs) => {
|
||||
DEFERRED_TRIE_METRICS.deferred_trie_sync_fallback.increment(1);
|
||||
|
||||
let inputs = maybe_inputs.take().expect("inputs must be present in Pending state");
|
||||
|
||||
let computed = Self::sort_and_build_trie_input(
|
||||
&inputs.hashed_state,
|
||||
&inputs.trie_updates,
|
||||
inputs.hashed_state,
|
||||
inputs.trie_updates,
|
||||
inputs.anchor_hash,
|
||||
&inputs.ancestors,
|
||||
);
|
||||
*state = DeferredState::Ready(computed.clone());
|
||||
|
||||
// Release lock before inputs (and its ancestors) drop to avoid holding it
|
||||
// while their potential last Arc refs drop (which could trigger recursive locking)
|
||||
drop(state);
|
||||
|
||||
computed
|
||||
}
|
||||
}
|
||||
@@ -441,4 +558,365 @@ mod tests {
|
||||
let (_, account) = &overlay_state[0];
|
||||
assert_eq!(account.unwrap().nonce, 2);
|
||||
}
|
||||
|
||||
/// Helper to create a ready block with anchored trie input containing specific state.
|
||||
fn ready_block_with_state(
|
||||
anchor_hash: B256,
|
||||
accounts: Vec<(B256, Option<Account>)>,
|
||||
) -> DeferredTrieData {
|
||||
let hashed_state = Arc::new(HashedPostStateSorted::new(accounts, B256Map::default()));
|
||||
let trie_updates = Arc::default();
|
||||
let mut overlay = TrieInputSorted::default();
|
||||
Arc::make_mut(&mut overlay.state).extend_ref_and_sort(hashed_state.as_ref());
|
||||
|
||||
DeferredTrieData::ready(ComputedTrieData {
|
||||
hashed_state,
|
||||
trie_updates,
|
||||
anchored_trie_input: Some(AnchoredTrieInput {
|
||||
anchor_hash,
|
||||
trie_input: Arc::new(overlay),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Verifies that first block after anchor (no ancestors) creates empty base overlay.
|
||||
#[test]
|
||||
fn first_block_after_anchor_creates_empty_base() {
|
||||
let anchor = B256::with_last_byte(1);
|
||||
let key = B256::with_last_byte(42);
|
||||
let account = Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None };
|
||||
|
||||
// First block after anchor - no ancestors
|
||||
let first_block = DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default().with_accounts([(key, Some(account))])),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
vec![], // No ancestors
|
||||
);
|
||||
|
||||
let result = first_block.wait_cloned();
|
||||
|
||||
// Should have overlay with just this block's data
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.anchor_hash, anchor);
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
|
||||
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
|
||||
assert_eq!(*found_key, key);
|
||||
assert_eq!(found_account.unwrap().nonce, 1);
|
||||
}
|
||||
|
||||
/// Verifies that parent's overlay is reused regardless of anchor.
|
||||
#[test]
|
||||
fn reuses_parent_overlay() {
|
||||
let anchor = B256::with_last_byte(1);
|
||||
let key = B256::with_last_byte(42);
|
||||
let account = Account { nonce: 100, balance: U256::ZERO, bytecode_hash: None };
|
||||
|
||||
// Create parent with anchored trie input
|
||||
let parent = ready_block_with_state(anchor, vec![(key, Some(account))]);
|
||||
|
||||
// Create child - should reuse parent's overlay
|
||||
let child = DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default()),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
vec![parent],
|
||||
);
|
||||
|
||||
let result = child.wait_cloned();
|
||||
|
||||
// Verify parent's account is in the overlay
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.anchor_hash, anchor);
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
|
||||
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
|
||||
assert_eq!(*found_key, key);
|
||||
assert_eq!(found_account.unwrap().nonce, 100);
|
||||
}
|
||||
|
||||
/// Verifies that parent's overlay is NOT reused when anchor changes (after persist).
|
||||
/// The overlay data is dependent on the anchor, so it must be rebuilt from the
|
||||
/// remaining ancestors.
|
||||
#[test]
|
||||
fn rebuilds_overlay_when_anchor_changes() {
|
||||
let old_anchor = B256::with_last_byte(1);
|
||||
let new_anchor = B256::with_last_byte(2);
|
||||
let key = B256::with_last_byte(42);
|
||||
let account = Account { nonce: 50, balance: U256::ZERO, bytecode_hash: None };
|
||||
|
||||
// Create parent with OLD anchor
|
||||
let parent = ready_block_with_state(old_anchor, vec![(key, Some(account))]);
|
||||
|
||||
// Create child with NEW anchor (simulates after persist)
|
||||
// Should NOT reuse parent's overlay because anchor changed
|
||||
let child = DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default()),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
new_anchor,
|
||||
vec![parent],
|
||||
);
|
||||
|
||||
let result = child.wait_cloned();
|
||||
|
||||
// Verify result uses new anchor
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.anchor_hash, new_anchor);
|
||||
|
||||
// Crucially, since we provided `parent` in ancestors but it has a different anchor,
|
||||
// the code falls back to `merge_ancestors_into_overlay`.
|
||||
// `merge_ancestors_into_overlay` reads `parent.hashed_state` (which has the account).
|
||||
// So the account IS present, but it was obtained via REBUILD, not REUSE.
|
||||
// We can check `DEFERRED_TRIE_METRICS` if we want to be sure, but functionally:
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
|
||||
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
|
||||
assert_eq!(*found_key, key);
|
||||
assert_eq!(found_account.unwrap().nonce, 50);
|
||||
}
|
||||
|
||||
/// Verifies that parent without `anchored_trie_input` triggers rebuild path.
|
||||
#[test]
|
||||
fn rebuilds_when_parent_has_no_anchored_input() {
|
||||
let anchor = B256::with_last_byte(1);
|
||||
let key = B256::with_last_byte(42);
|
||||
let account = Account { nonce: 25, balance: U256::ZERO, bytecode_hash: None };
|
||||
|
||||
// Create parent WITHOUT anchored trie input (e.g., from without_trie_input constructor)
|
||||
let parent_state =
|
||||
HashedPostStateSorted::new(vec![(key, Some(account))], B256Map::default());
|
||||
let parent = DeferredTrieData::ready(ComputedTrieData {
|
||||
hashed_state: Arc::new(parent_state),
|
||||
trie_updates: Arc::default(),
|
||||
anchored_trie_input: None, // No anchored input
|
||||
});
|
||||
|
||||
// Create child - should rebuild from parent's hashed_state
|
||||
let child = DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default()),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
vec![parent],
|
||||
);
|
||||
|
||||
let result = child.wait_cloned();
|
||||
|
||||
// Verify overlay is built and contains parent's data
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.anchor_hash, anchor);
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
|
||||
}
|
||||
|
||||
/// Verifies that a chain of blocks with matching anchors builds correct cumulative overlay.
|
||||
#[test]
|
||||
fn chain_of_blocks_builds_cumulative_overlay() {
|
||||
let anchor = B256::with_last_byte(1);
|
||||
let key1 = B256::with_last_byte(1);
|
||||
let key2 = B256::with_last_byte(2);
|
||||
let key3 = B256::with_last_byte(3);
|
||||
|
||||
// Block 1: sets account at key1
|
||||
let block1 = ready_block_with_state(
|
||||
anchor,
|
||||
vec![(key1, Some(Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None }))],
|
||||
);
|
||||
|
||||
// Block 2: adds account at key2, ancestor is block1
|
||||
let block2_hashed = HashedPostState::default().with_accounts([(
|
||||
key2,
|
||||
Some(Account { nonce: 2, balance: U256::ZERO, bytecode_hash: None }),
|
||||
)]);
|
||||
let block2 = DeferredTrieData::pending(
|
||||
Arc::new(block2_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
vec![block1.clone()],
|
||||
);
|
||||
// Compute block2's trie data
|
||||
let block2_computed = block2.wait_cloned();
|
||||
let block2_ready = DeferredTrieData::ready(block2_computed);
|
||||
|
||||
// Block 3: adds account at key3, ancestor is block2 (which includes block1)
|
||||
let block3_hashed = HashedPostState::default().with_accounts([(
|
||||
key3,
|
||||
Some(Account { nonce: 3, balance: U256::ZERO, bytecode_hash: None }),
|
||||
)]);
|
||||
let block3 = DeferredTrieData::pending(
|
||||
Arc::new(block3_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
vec![block1, block2_ready],
|
||||
);
|
||||
|
||||
let result = block3.wait_cloned();
|
||||
|
||||
// Verify all three accounts are in the cumulative overlay
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 3);
|
||||
|
||||
// Accounts should be sorted by key (B256 ordering)
|
||||
let accounts = &overlay.trie_input.state.accounts;
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key1 && a.unwrap().nonce == 1));
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key2 && a.unwrap().nonce == 2));
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key3 && a.unwrap().nonce == 3));
|
||||
}
|
||||
|
||||
/// Verifies that child block's state overwrites parent's state for the same key.
|
||||
#[test]
|
||||
fn child_state_overwrites_parent() {
|
||||
let anchor = B256::with_last_byte(1);
|
||||
let key = B256::with_last_byte(42);
|
||||
|
||||
// Parent sets nonce to 10
|
||||
let parent = ready_block_with_state(
|
||||
anchor,
|
||||
vec![(key, Some(Account { nonce: 10, balance: U256::ZERO, bytecode_hash: None }))],
|
||||
);
|
||||
|
||||
// Child overwrites nonce to 99
|
||||
let child_hashed = HashedPostState::default().with_accounts([(
|
||||
key,
|
||||
Some(Account { nonce: 99, balance: U256::ZERO, bytecode_hash: None }),
|
||||
)]);
|
||||
let child = DeferredTrieData::pending(
|
||||
Arc::new(child_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
vec![parent],
|
||||
);
|
||||
|
||||
let result = child.wait_cloned();
|
||||
|
||||
// Verify child's value wins (extend_ref uses later value)
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
// Note: extend_ref may result in duplicate keys; check the last occurrence
|
||||
let accounts = &overlay.trie_input.state.accounts;
|
||||
let last_account = accounts.iter().rfind(|(k, _)| *k == key).unwrap();
|
||||
assert_eq!(last_account.1.unwrap().nonce, 99);
|
||||
}
|
||||
|
||||
/// Stress test: verify O(N) behavior by building a chain of many blocks.
|
||||
/// This test ensures the fix doesn't regress - previously this would be O(N²).
|
||||
#[test]
|
||||
fn long_chain_builds_in_linear_time() {
|
||||
let anchor = B256::with_last_byte(1);
|
||||
let num_blocks = 50; // Enough to notice O(N²) vs O(N) difference
|
||||
|
||||
let mut ancestors: Vec<DeferredTrieData> = Vec::new();
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_blocks {
|
||||
let key = B256::with_last_byte(i as u8);
|
||||
let account = Account { nonce: i as u64, balance: U256::ZERO, bytecode_hash: None };
|
||||
let hashed = HashedPostState::default().with_accounts([(key, Some(account))]);
|
||||
|
||||
let block = DeferredTrieData::pending(
|
||||
Arc::new(hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
ancestors.clone(),
|
||||
);
|
||||
|
||||
// Compute and add to ancestors for next iteration
|
||||
let computed = block.wait_cloned();
|
||||
ancestors.push(DeferredTrieData::ready(computed));
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// With O(N) fix, 50 blocks should complete quickly (< 1 second)
|
||||
// With O(N²), this would take significantly longer
|
||||
assert!(
|
||||
elapsed < Duration::from_secs(2),
|
||||
"Chain of {num_blocks} blocks took {:?}, possible O(N²) regression",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// Verify final overlay has all accounts
|
||||
let final_result = ancestors.last().unwrap().wait_cloned();
|
||||
let overlay = final_result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), num_blocks);
|
||||
}
|
||||
|
||||
/// Verifies that a multi-ancestor overlay is rebuilt when anchor changes.
|
||||
/// This simulates the "persist prefix then keep building" scenario where:
|
||||
/// 1. A chain of blocks is built with anchor A
|
||||
/// 2. Some blocks are persisted, changing anchor to B
|
||||
/// 3. New blocks must rebuild the overlay from the remaining ancestors
|
||||
#[test]
|
||||
fn multi_ancestor_overlay_rebuilt_after_anchor_change() {
|
||||
let old_anchor = B256::with_last_byte(1);
|
||||
let new_anchor = B256::with_last_byte(2);
|
||||
let key1 = B256::with_last_byte(1);
|
||||
let key2 = B256::with_last_byte(2);
|
||||
let key3 = B256::with_last_byte(3);
|
||||
let key4 = B256::with_last_byte(4);
|
||||
|
||||
// Build a chain of 3 blocks with old_anchor
|
||||
let block1 = ready_block_with_state(
|
||||
old_anchor,
|
||||
vec![(key1, Some(Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None }))],
|
||||
);
|
||||
|
||||
let block2_hashed = HashedPostState::default().with_accounts([(
|
||||
key2,
|
||||
Some(Account { nonce: 2, balance: U256::ZERO, bytecode_hash: None }),
|
||||
)]);
|
||||
let block2 = DeferredTrieData::pending(
|
||||
Arc::new(block2_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
old_anchor,
|
||||
vec![block1.clone()],
|
||||
);
|
||||
let block2_ready = DeferredTrieData::ready(block2.wait_cloned());
|
||||
|
||||
let block3_hashed = HashedPostState::default().with_accounts([(
|
||||
key3,
|
||||
Some(Account { nonce: 3, balance: U256::ZERO, bytecode_hash: None }),
|
||||
)]);
|
||||
let block3 = DeferredTrieData::pending(
|
||||
Arc::new(block3_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
old_anchor,
|
||||
vec![block1.clone(), block2_ready.clone()],
|
||||
);
|
||||
let block3_ready = DeferredTrieData::ready(block3.wait_cloned());
|
||||
|
||||
// Verify block3's overlay has all 3 accounts with old_anchor
|
||||
let block3_overlay = block3_ready.wait_cloned().anchored_trie_input.unwrap();
|
||||
assert_eq!(block3_overlay.anchor_hash, old_anchor);
|
||||
assert_eq!(block3_overlay.trie_input.state.accounts.len(), 3);
|
||||
|
||||
// Now simulate persist: create block4 with NEW anchor but same ancestors.
|
||||
// To verify correct rebuilding, we must provide ALL unpersisted ancestors.
|
||||
// If we only provided block3, the rebuild would only see block3's state.
|
||||
// We pass block1, block2, block3 to simulate that they are all still in memory
|
||||
// but the anchor check forces a rebuild (e.g. artificial anchor change).
|
||||
let block4_hashed = HashedPostState::default().with_accounts([(
|
||||
key4,
|
||||
Some(Account { nonce: 4, balance: U256::ZERO, bytecode_hash: None }),
|
||||
)]);
|
||||
let block4 = DeferredTrieData::pending(
|
||||
Arc::new(block4_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
new_anchor, // Different anchor - simulates post-persist
|
||||
vec![block1, block2_ready, block3_ready],
|
||||
);
|
||||
|
||||
let result = block4.wait_cloned();
|
||||
|
||||
// Verify:
|
||||
// 1. New anchor is used in result
|
||||
assert_eq!(result.anchor_hash(), Some(new_anchor));
|
||||
|
||||
// 2. All 4 accounts are in the overlay (rebuilt from ancestors + extended)
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 4);
|
||||
|
||||
// 3. All accounts have correct values
|
||||
let accounts = &overlay.trie_input.state.accounts;
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key1 && a.unwrap().nonce == 1));
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key2 && a.unwrap().nonce == 2));
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key3 && a.unwrap().nonce == 3));
|
||||
assert!(accounts.iter().any(|(k, a)| *k == key4 && a.unwrap().nonce == 4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,18 @@ use alloy_primitives::{map::HashMap, BlockNumber, TxHash, B256};
|
||||
use parking_lot::RwLock;
|
||||
use reth_chainspec::ChainInfo;
|
||||
use reth_ethereum_primitives::EthPrimitives;
|
||||
use reth_execution_types::{Chain, ExecutionOutcome};
|
||||
use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, Chain, ExecutionOutcome};
|
||||
use reth_metrics::{metrics::Gauge, Metrics};
|
||||
use reth_primitives_traits::{
|
||||
BlockBody as _, IndexedTx, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
|
||||
SignedTransaction,
|
||||
};
|
||||
use reth_storage_api::StateProviderBox;
|
||||
use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
|
||||
use std::{collections::BTreeMap, ops::Deref, sync::Arc, time::Instant};
|
||||
use reth_trie::{
|
||||
updates::TrieUpdatesSorted, HashedPostStateSorted, LazyTrieData, SortedTrieData,
|
||||
TrieInputSorted,
|
||||
};
|
||||
use std::{collections::BTreeMap, sync::Arc, time::Instant};
|
||||
use tokio::sync::{broadcast, watch};
|
||||
|
||||
/// Size of the broadcast channel used to notify canonical state events.
|
||||
@@ -86,14 +89,20 @@ impl<N: NodePrimitives> InMemoryState<N> {
|
||||
///
|
||||
/// This tries to acquire a read lock. Drop any write locks before calling this.
|
||||
pub(crate) fn update_metrics(&self) {
|
||||
let numbers = self.numbers.read();
|
||||
if let Some((earliest_block_number, _)) = numbers.first_key_value() {
|
||||
self.metrics.earliest_block.set(*earliest_block_number as f64);
|
||||
let (count, earliest, latest) = {
|
||||
let numbers = self.numbers.read();
|
||||
let count = numbers.len();
|
||||
let earliest = numbers.first_key_value().map(|(number, _)| *number);
|
||||
let latest = numbers.last_key_value().map(|(number, _)| *number);
|
||||
(count, earliest, latest)
|
||||
};
|
||||
if let Some(earliest_block_number) = earliest {
|
||||
self.metrics.earliest_block.set(earliest_block_number as f64);
|
||||
}
|
||||
if let Some((latest_block_number, _)) = numbers.last_key_value() {
|
||||
self.metrics.latest_block.set(*latest_block_number as f64);
|
||||
if let Some(latest_block_number) = latest {
|
||||
self.metrics.latest_block.set(latest_block_number as f64);
|
||||
}
|
||||
self.metrics.num_blocks.set(numbers.len() as f64);
|
||||
self.metrics.num_blocks.set(count as f64);
|
||||
}
|
||||
|
||||
/// Returns the state for a given block hash.
|
||||
@@ -311,6 +320,7 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
|
||||
/// This will update the links between blocks and remove all blocks that are [..
|
||||
/// `persisted_height`].
|
||||
pub fn remove_persisted_blocks(&self, persisted_num_hash: BlockNumHash) {
|
||||
self.set_persisted(persisted_num_hash);
|
||||
// if the persisted hash is not in the canonical in memory state, do nothing, because it
|
||||
// means canonical blocks were not actually persisted.
|
||||
//
|
||||
@@ -438,6 +448,11 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
|
||||
self.inner.chain_info_tracker.set_finalized(header);
|
||||
}
|
||||
|
||||
/// Persisted block setter.
|
||||
pub fn set_persisted(&self, num_hash: BlockNumHash) {
|
||||
self.inner.chain_info_tracker.set_persisted(num_hash);
|
||||
}
|
||||
|
||||
/// Canonical head getter.
|
||||
pub fn get_canonical_head(&self) -> SealedHeader<N::BlockHeader> {
|
||||
self.inner.chain_info_tracker.get_canonical_head()
|
||||
@@ -453,6 +468,11 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
|
||||
self.inner.chain_info_tracker.get_safe_header()
|
||||
}
|
||||
|
||||
/// Persisted block `BlockNumHash` getter.
|
||||
pub fn get_persisted_num_hash(&self) -> Option<BlockNumHash> {
|
||||
self.inner.chain_info_tracker.get_persisted_num_hash()
|
||||
}
|
||||
|
||||
/// Returns the `SealedHeader` corresponding to the pending state.
|
||||
pub fn pending_sealed_header(&self) -> Option<SealedHeader<N::BlockHeader>> {
|
||||
self.pending_state().map(|h| h.block_ref().recovered_block().clone_sealed_header())
|
||||
@@ -505,6 +525,11 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
|
||||
self.inner.chain_info_tracker.subscribe_finalized_block()
|
||||
}
|
||||
|
||||
/// Subscribe to new persisted block events.
|
||||
pub fn subscribe_persisted_block(&self) -> watch::Receiver<Option<BlockNumHash>> {
|
||||
self.inner.chain_info_tracker.subscribe_persisted_block()
|
||||
}
|
||||
|
||||
/// Attempts to send a new [`CanonStateNotification`] to all active Receiver handles.
|
||||
pub fn notify_canon_state(&self, event: CanonStateNotification<N>) {
|
||||
self.inner.canon_state_notification_sender.send(event).ok();
|
||||
@@ -626,7 +651,7 @@ impl<N: NodePrimitives> BlockState<N> {
|
||||
}
|
||||
|
||||
/// Returns the `Receipts` of executed block that determines the state.
|
||||
pub fn receipts(&self) -> &Vec<Vec<N::Receipt>> {
|
||||
pub fn receipts(&self) -> &Vec<N::Receipt> {
|
||||
&self.block.execution_outcome().receipts
|
||||
}
|
||||
|
||||
@@ -637,15 +662,7 @@ impl<N: NodePrimitives> BlockState<N> {
|
||||
///
|
||||
/// This clones the vector of receipts. To avoid it, use [`Self::executed_block_receipts_ref`].
|
||||
pub fn executed_block_receipts(&self) -> Vec<N::Receipt> {
|
||||
let receipts = self.receipts();
|
||||
|
||||
debug_assert!(
|
||||
receipts.len() <= 1,
|
||||
"Expected at most one block's worth of receipts, found {}",
|
||||
receipts.len()
|
||||
);
|
||||
|
||||
receipts.first().cloned().unwrap_or_default()
|
||||
self.receipts().clone()
|
||||
}
|
||||
|
||||
/// Returns a slice of `Receipt` of executed block that determines the state.
|
||||
@@ -653,33 +670,17 @@ impl<N: NodePrimitives> BlockState<N> {
|
||||
/// has only one element corresponding to the executed block associated to
|
||||
/// the state.
|
||||
pub fn executed_block_receipts_ref(&self) -> &[N::Receipt] {
|
||||
let receipts = self.receipts();
|
||||
|
||||
debug_assert!(
|
||||
receipts.len() <= 1,
|
||||
"Expected at most one block's worth of receipts, found {}",
|
||||
receipts.len()
|
||||
);
|
||||
|
||||
receipts.first().map(|receipts| receipts.deref()).unwrap_or_default()
|
||||
self.receipts()
|
||||
}
|
||||
|
||||
/// Returns a vector of __parent__ `BlockStates`.
|
||||
/// Returns an iterator over __parent__ `BlockStates`.
|
||||
///
|
||||
/// The block state order in the output vector is newest to oldest (highest to lowest):
|
||||
/// The block state order is newest to oldest (highest to lowest):
|
||||
/// `[5,4,3,2,1]`
|
||||
///
|
||||
/// Note: This does not include self.
|
||||
pub fn parent_state_chain(&self) -> Vec<&Self> {
|
||||
let mut parents = Vec::new();
|
||||
let mut current = self.parent.as_deref();
|
||||
|
||||
while let Some(parent) = current {
|
||||
parents.push(parent);
|
||||
current = parent.parent.as_deref();
|
||||
}
|
||||
|
||||
parents
|
||||
pub fn parent_state_chain(&self) -> impl Iterator<Item = &Self> + '_ {
|
||||
std::iter::successors(self.parent.as_deref(), |state| state.parent.as_deref())
|
||||
}
|
||||
|
||||
/// Returns a vector of `BlockStates` representing the entire in memory chain.
|
||||
@@ -690,6 +691,11 @@ impl<N: NodePrimitives> BlockState<N> {
|
||||
}
|
||||
|
||||
/// Appends the parent chain of this [`BlockState`] to the given vector.
|
||||
///
|
||||
/// Parents are appended in order from newest to oldest (highest to lowest).
|
||||
/// This does not include self, only the parent states.
|
||||
///
|
||||
/// This is a convenience method equivalent to `chain.extend(self.parent_state_chain())`.
|
||||
pub fn append_parent_chain<'a>(&'a self, chain: &mut Vec<&'a Self>) {
|
||||
chain.extend(self.parent_state_chain());
|
||||
}
|
||||
@@ -748,7 +754,7 @@ pub struct ExecutedBlock<N: NodePrimitives = EthPrimitives> {
|
||||
/// Recovered Block
|
||||
pub recovered_block: Arc<RecoveredBlock<N::Block>>,
|
||||
/// Block's execution outcome.
|
||||
pub execution_output: Arc<ExecutionOutcome<N::Receipt>>,
|
||||
pub execution_output: Arc<BlockExecutionOutput<N::Receipt>>,
|
||||
/// Deferred trie data produced by execution.
|
||||
///
|
||||
/// This allows deferring the computation of the trie data which can be expensive.
|
||||
@@ -760,7 +766,15 @@ impl<N: NodePrimitives> Default for ExecutedBlock<N> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
recovered_block: Default::default(),
|
||||
execution_output: Default::default(),
|
||||
execution_output: Arc::new(BlockExecutionOutput {
|
||||
result: BlockExecutionResult {
|
||||
receipts: Default::default(),
|
||||
requests: Default::default(),
|
||||
gas_used: 0,
|
||||
blob_gas_used: 0,
|
||||
},
|
||||
state: Default::default(),
|
||||
}),
|
||||
trie_data: DeferredTrieData::ready(ComputedTrieData::default()),
|
||||
}
|
||||
}
|
||||
@@ -781,7 +795,7 @@ impl<N: NodePrimitives> ExecutedBlock<N> {
|
||||
/// payload builders). This is the safe default path.
|
||||
pub fn new(
|
||||
recovered_block: Arc<RecoveredBlock<N::Block>>,
|
||||
execution_output: Arc<ExecutionOutcome<N::Receipt>>,
|
||||
execution_output: Arc<BlockExecutionOutput<N::Receipt>>,
|
||||
trie_data: ComputedTrieData,
|
||||
) -> Self {
|
||||
Self { recovered_block, execution_output, trie_data: DeferredTrieData::ready(trie_data) }
|
||||
@@ -803,7 +817,7 @@ impl<N: NodePrimitives> ExecutedBlock<N> {
|
||||
/// Use [`Self::new()`] instead when trie data is already computed and available immediately.
|
||||
pub const fn with_deferred_trie_data(
|
||||
recovered_block: Arc<RecoveredBlock<N::Block>>,
|
||||
execution_output: Arc<ExecutionOutcome<N::Receipt>>,
|
||||
execution_output: Arc<BlockExecutionOutput<N::Receipt>>,
|
||||
trie_data: DeferredTrieData,
|
||||
) -> Self {
|
||||
Self { recovered_block, execution_output, trie_data }
|
||||
@@ -823,7 +837,7 @@ impl<N: NodePrimitives> ExecutedBlock<N> {
|
||||
|
||||
/// Returns a reference to the block's execution outcome
|
||||
#[inline]
|
||||
pub fn execution_outcome(&self) -> &ExecutionOutcome<N::Receipt> {
|
||||
pub fn execution_outcome(&self) -> &BlockExecutionOutput<N::Receipt> {
|
||||
&self.execution_output
|
||||
}
|
||||
|
||||
@@ -923,31 +937,53 @@ impl<N: NodePrimitives<SignedTx: SignedTransaction>> NewCanonicalChain<N> {
|
||||
pub fn to_chain_notification(&self) -> CanonStateNotification<N> {
|
||||
match self {
|
||||
Self::Commit { new } => {
|
||||
let new = Arc::new(new.iter().fold(Chain::default(), |mut chain, exec| {
|
||||
chain.append_block(
|
||||
exec.recovered_block().clone(),
|
||||
exec.execution_outcome().clone(),
|
||||
);
|
||||
chain
|
||||
}));
|
||||
CanonStateNotification::Commit { new }
|
||||
CanonStateNotification::Commit { new: Arc::new(Self::blocks_to_chain(new)) }
|
||||
}
|
||||
Self::Reorg { new, old } => {
|
||||
let new = Arc::new(new.iter().fold(Chain::default(), |mut chain, exec| {
|
||||
Self::Reorg { new, old } => CanonStateNotification::Reorg {
|
||||
new: Arc::new(Self::blocks_to_chain(new)),
|
||||
old: Arc::new(Self::blocks_to_chain(old)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a slice of executed blocks into a [`Chain`].
|
||||
fn blocks_to_chain(blocks: &[ExecutedBlock<N>]) -> Chain<N> {
|
||||
match blocks {
|
||||
[] => Chain::default(),
|
||||
[first, rest @ ..] => {
|
||||
let trie_data_handle = first.trie_data_handle();
|
||||
let mut chain = Chain::from_block(
|
||||
first.recovered_block().clone(),
|
||||
ExecutionOutcome::from((
|
||||
first.execution_outcome().clone(),
|
||||
first.block_number(),
|
||||
)),
|
||||
LazyTrieData::deferred(move || {
|
||||
let trie_data = trie_data_handle.wait_cloned();
|
||||
SortedTrieData {
|
||||
hashed_state: trie_data.hashed_state,
|
||||
trie_updates: trie_data.trie_updates,
|
||||
}
|
||||
}),
|
||||
);
|
||||
for exec in rest {
|
||||
let trie_data_handle = exec.trie_data_handle();
|
||||
chain.append_block(
|
||||
exec.recovered_block().clone(),
|
||||
exec.execution_outcome().clone(),
|
||||
ExecutionOutcome::from((
|
||||
exec.execution_outcome().clone(),
|
||||
exec.block_number(),
|
||||
)),
|
||||
LazyTrieData::deferred(move || {
|
||||
let trie_data = trie_data_handle.wait_cloned();
|
||||
SortedTrieData {
|
||||
hashed_state: trie_data.hashed_state,
|
||||
trie_updates: trie_data.trie_updates,
|
||||
}
|
||||
}),
|
||||
);
|
||||
chain
|
||||
}));
|
||||
let old = Arc::new(old.iter().fold(Chain::default(), |mut chain, exec| {
|
||||
chain.append_block(
|
||||
exec.recovered_block().clone(),
|
||||
exec.execution_outcome().clone(),
|
||||
);
|
||||
chain
|
||||
}));
|
||||
CanonStateNotification::Reorg { new, old }
|
||||
}
|
||||
chain
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1241,7 +1277,7 @@ mod tests {
|
||||
|
||||
let state = BlockState::new(block);
|
||||
|
||||
assert_eq!(state.receipts(), &receipts);
|
||||
assert_eq!(state.receipts(), receipts.first().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1453,19 +1489,18 @@ mod tests {
|
||||
let mut test_block_builder: TestBlockBuilder = TestBlockBuilder::default();
|
||||
let chain = create_mock_state_chain(&mut test_block_builder, 4);
|
||||
|
||||
let parents = chain[3].parent_state_chain();
|
||||
let parents: Vec<_> = chain[3].parent_state_chain().collect();
|
||||
assert_eq!(parents.len(), 3);
|
||||
assert_eq!(parents[0].block().recovered_block().number, 3);
|
||||
assert_eq!(parents[1].block().recovered_block().number, 2);
|
||||
assert_eq!(parents[2].block().recovered_block().number, 1);
|
||||
|
||||
let parents = chain[2].parent_state_chain();
|
||||
let parents: Vec<_> = chain[2].parent_state_chain().collect();
|
||||
assert_eq!(parents.len(), 2);
|
||||
assert_eq!(parents[0].block().recovered_block().number, 2);
|
||||
assert_eq!(parents[1].block().recovered_block().number, 1);
|
||||
|
||||
let parents = chain[0].parent_state_chain();
|
||||
assert_eq!(parents.len(), 0);
|
||||
assert_eq!(chain[0].parent_state_chain().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1476,8 +1511,7 @@ mod tests {
|
||||
create_mock_state(&mut test_block_builder, single_block_number, B256::random());
|
||||
let single_block_hash = single_block.block().recovered_block().hash();
|
||||
|
||||
let parents = single_block.parent_state_chain();
|
||||
assert_eq!(parents.len(), 0);
|
||||
assert_eq!(single_block.parent_state_chain().count(), 0);
|
||||
|
||||
let block_state_chain = single_block.chain().collect::<Vec<_>>();
|
||||
assert_eq!(block_state_chain.len(), 1);
|
||||
@@ -1520,22 +1554,31 @@ mod tests {
|
||||
let block2a =
|
||||
test_block_builder.get_executed_block_with_number(2, block1.recovered_block.hash());
|
||||
|
||||
let sample_execution_outcome = ExecutionOutcome {
|
||||
receipts: vec![vec![], vec![]],
|
||||
requests: vec![Requests::default(), Requests::default()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Test commit notification
|
||||
let chain_commit = NewCanonicalChain::Commit { new: vec![block0.clone(), block1.clone()] };
|
||||
|
||||
// Build expected trie data map
|
||||
let mut expected_trie_data = BTreeMap::new();
|
||||
expected_trie_data
|
||||
.insert(0, LazyTrieData::ready(block0.hashed_state(), block0.trie_updates()));
|
||||
expected_trie_data
|
||||
.insert(1, LazyTrieData::ready(block1.hashed_state(), block1.trie_updates()));
|
||||
|
||||
// Build expected execution outcome (first_block matches first block number)
|
||||
let commit_execution_outcome = ExecutionOutcome {
|
||||
receipts: vec![vec![], vec![]],
|
||||
requests: vec![Requests::default(), Requests::default()],
|
||||
first_block: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
chain_commit.to_chain_notification(),
|
||||
CanonStateNotification::Commit {
|
||||
new: Arc::new(Chain::new(
|
||||
vec![block0.recovered_block().clone(), block1.recovered_block().clone()],
|
||||
sample_execution_outcome.clone(),
|
||||
None
|
||||
commit_execution_outcome,
|
||||
expected_trie_data,
|
||||
))
|
||||
}
|
||||
);
|
||||
@@ -1546,18 +1589,39 @@ mod tests {
|
||||
old: vec![block1.clone(), block2.clone()],
|
||||
};
|
||||
|
||||
// Build expected trie data for old chain
|
||||
let mut old_trie_data = BTreeMap::new();
|
||||
old_trie_data.insert(1, LazyTrieData::ready(block1.hashed_state(), block1.trie_updates()));
|
||||
old_trie_data.insert(2, LazyTrieData::ready(block2.hashed_state(), block2.trie_updates()));
|
||||
|
||||
// Build expected trie data for new chain
|
||||
let mut new_trie_data = BTreeMap::new();
|
||||
new_trie_data
|
||||
.insert(1, LazyTrieData::ready(block1a.hashed_state(), block1a.trie_updates()));
|
||||
new_trie_data
|
||||
.insert(2, LazyTrieData::ready(block2a.hashed_state(), block2a.trie_updates()));
|
||||
|
||||
// Build expected execution outcome for reorg chains (first_block matches first block
|
||||
// number)
|
||||
let reorg_execution_outcome = ExecutionOutcome {
|
||||
receipts: vec![vec![], vec![]],
|
||||
requests: vec![Requests::default(), Requests::default()],
|
||||
first_block: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
chain_reorg.to_chain_notification(),
|
||||
CanonStateNotification::Reorg {
|
||||
old: Arc::new(Chain::new(
|
||||
vec![block1.recovered_block().clone(), block2.recovered_block().clone()],
|
||||
sample_execution_outcome.clone(),
|
||||
None
|
||||
reorg_execution_outcome.clone(),
|
||||
old_trie_data,
|
||||
)),
|
||||
new: Arc::new(Chain::new(
|
||||
vec![block1a.recovered_block().clone(), block2a.recovered_block().clone()],
|
||||
sample_execution_outcome,
|
||||
None
|
||||
reorg_execution_outcome,
|
||||
new_trie_data,
|
||||
))
|
||||
}
|
||||
);
|
||||
|
||||
186
crates/chain-state/src/lazy_overlay.rs
Normal file
186
crates/chain-state/src/lazy_overlay.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
//! Lazy overlay computation for trie input.
|
||||
//!
|
||||
//! This module provides [`LazyOverlay`], a type that computes the [`TrieInputSorted`]
|
||||
//! lazily on first access. This allows execution to start before the trie overlay
|
||||
//! is fully computed.
|
||||
|
||||
use crate::DeferredTrieData;
|
||||
use alloy_primitives::B256;
|
||||
use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
/// Inputs captured for lazy overlay computation.
|
||||
#[derive(Clone)]
|
||||
struct LazyOverlayInputs {
|
||||
/// The persisted ancestor hash (anchor) this overlay should be built on.
|
||||
anchor_hash: B256,
|
||||
/// Deferred trie data handles for all in-memory blocks (newest to oldest).
|
||||
blocks: Vec<DeferredTrieData>,
|
||||
}
|
||||
|
||||
/// Lazily computed trie overlay.
|
||||
///
|
||||
/// Captures the inputs needed to compute a [`TrieInputSorted`] and defers the actual
|
||||
/// computation until first access. This is conceptually similar to [`DeferredTrieData`]
|
||||
/// but for overlay computation.
|
||||
///
|
||||
/// # Fast Path vs Slow Path
|
||||
///
|
||||
/// - **Fast path**: If the tip block's cached `anchored_trie_input` is ready and its `anchor_hash`
|
||||
/// matches our expected anchor, we can reuse it directly (O(1)).
|
||||
/// - **Slow path**: Otherwise, we merge all ancestor blocks' trie data into a new overlay.
|
||||
#[derive(Clone)]
|
||||
pub struct LazyOverlay {
|
||||
/// Computed result, cached after first access.
|
||||
inner: Arc<OnceLock<TrieInputSorted>>,
|
||||
/// Inputs for lazy computation.
|
||||
inputs: LazyOverlayInputs,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LazyOverlay {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LazyOverlay")
|
||||
.field("anchor_hash", &self.inputs.anchor_hash)
|
||||
.field("num_blocks", &self.inputs.blocks.len())
|
||||
.field("computed", &self.inner.get().is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl LazyOverlay {
|
||||
/// Create a new lazy overlay with the given anchor hash and block handles.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `anchor_hash` - The persisted ancestor hash this overlay is built on top of
|
||||
/// * `blocks` - Deferred trie data handles for in-memory blocks (newest to oldest)
|
||||
pub fn new(anchor_hash: B256, blocks: Vec<DeferredTrieData>) -> Self {
|
||||
Self { inner: Arc::new(OnceLock::new()), inputs: LazyOverlayInputs { anchor_hash, blocks } }
|
||||
}
|
||||
|
||||
/// Returns the anchor hash this overlay is built on.
|
||||
pub const fn anchor_hash(&self) -> B256 {
|
||||
self.inputs.anchor_hash
|
||||
}
|
||||
|
||||
/// Returns the number of in-memory blocks this overlay covers.
|
||||
pub const fn num_blocks(&self) -> usize {
|
||||
self.inputs.blocks.len()
|
||||
}
|
||||
|
||||
/// Returns true if the overlay has already been computed.
|
||||
pub fn is_computed(&self) -> bool {
|
||||
self.inner.get().is_some()
|
||||
}
|
||||
|
||||
/// Returns the computed trie input, computing it if necessary.
|
||||
///
|
||||
/// The first call triggers computation (which may block waiting for deferred data).
|
||||
/// Subsequent calls return the cached result immediately.
|
||||
pub fn get(&self) -> &TrieInputSorted {
|
||||
self.inner.get_or_init(|| self.compute())
|
||||
}
|
||||
|
||||
/// Returns the overlay as (nodes, state) tuple for use with `OverlayStateProviderFactory`.
|
||||
pub fn as_overlay(&self) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
|
||||
let input = self.get();
|
||||
(Arc::clone(&input.nodes), Arc::clone(&input.state))
|
||||
}
|
||||
|
||||
/// Compute the trie input overlay.
|
||||
fn compute(&self) -> TrieInputSorted {
|
||||
let anchor_hash = self.inputs.anchor_hash;
|
||||
let blocks = &self.inputs.blocks;
|
||||
|
||||
if blocks.is_empty() {
|
||||
debug!(target: "chain_state::lazy_overlay", "No in-memory blocks, returning empty overlay");
|
||||
return TrieInputSorted::default();
|
||||
}
|
||||
|
||||
// Fast path: Check if tip block's overlay is ready and anchor matches.
|
||||
// The tip block (first in list) has the cumulative overlay from all ancestors.
|
||||
if let Some(tip) = blocks.first() {
|
||||
let data = tip.wait_cloned();
|
||||
if let Some(anchored) = &data.anchored_trie_input {
|
||||
if anchored.anchor_hash == anchor_hash {
|
||||
trace!(target: "chain_state::lazy_overlay", %anchor_hash, "Reusing tip block's cached overlay (fast path)");
|
||||
return (*anchored.trie_input).clone();
|
||||
}
|
||||
debug!(
|
||||
target: "chain_state::lazy_overlay",
|
||||
computed_anchor = %anchored.anchor_hash,
|
||||
%anchor_hash,
|
||||
"Anchor mismatch, falling back to merge"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: Merge all blocks' trie data into a new overlay.
|
||||
debug!(target: "chain_state::lazy_overlay", num_blocks = blocks.len(), "Merging blocks (slow path)");
|
||||
Self::merge_blocks(blocks)
|
||||
}
|
||||
|
||||
/// Merge all blocks' trie data into a single [`TrieInputSorted`].
|
||||
///
|
||||
/// Blocks are ordered newest to oldest.
|
||||
fn merge_blocks(blocks: &[DeferredTrieData]) -> TrieInputSorted {
|
||||
if blocks.is_empty() {
|
||||
return TrieInputSorted::default();
|
||||
}
|
||||
|
||||
let state =
|
||||
HashedPostStateSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().hashed_state));
|
||||
let nodes =
|
||||
TrieUpdatesSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().trie_updates));
|
||||
|
||||
TrieInputSorted { state, nodes, prefix_sets: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reth_trie::{updates::TrieUpdates, HashedPostState};
|
||||
|
||||
fn empty_deferred(anchor: B256) -> DeferredTrieData {
|
||||
DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default()),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
anchor,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_blocks_returns_default() {
|
||||
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
|
||||
let result = overlay.get();
|
||||
assert!(result.state.is_empty());
|
||||
assert!(result.nodes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_block_uses_data_directly() {
|
||||
let anchor = B256::random();
|
||||
let deferred = empty_deferred(anchor);
|
||||
let overlay = LazyOverlay::new(anchor, vec![deferred]);
|
||||
|
||||
assert!(!overlay.is_computed());
|
||||
let _ = overlay.get();
|
||||
assert!(overlay.is_computed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_after_first_access() {
|
||||
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
|
||||
|
||||
// First access computes
|
||||
let _ = overlay.get();
|
||||
assert!(overlay.is_computed());
|
||||
|
||||
// Second access uses cache
|
||||
let _ = overlay.get();
|
||||
assert!(overlay.is_computed());
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ pub use in_memory::*;
|
||||
mod deferred_trie;
|
||||
pub use deferred_trie::*;
|
||||
|
||||
mod lazy_overlay;
|
||||
pub use lazy_overlay::*;
|
||||
|
||||
mod noop;
|
||||
|
||||
mod chain_info;
|
||||
@@ -23,7 +26,8 @@ mod notifications;
|
||||
pub use notifications::{
|
||||
CanonStateNotification, CanonStateNotificationSender, CanonStateNotificationStream,
|
||||
CanonStateNotifications, CanonStateSubscriptions, ForkChoiceNotifications, ForkChoiceStream,
|
||||
ForkChoiceSubscriptions,
|
||||
ForkChoiceSubscriptions, PersistedBlockNotifications, PersistedBlockSubscriptions,
|
||||
WatchValueStream,
|
||||
};
|
||||
|
||||
mod memory_overlay;
|
||||
|
||||
@@ -5,14 +5,14 @@ use reth_errors::ProviderResult;
|
||||
use reth_primitives_traits::{Account, Bytecode, NodePrimitives};
|
||||
use reth_storage_api::{
|
||||
AccountReader, BlockHashReader, BytecodeReader, HashedPostStateProvider, StateProofProvider,
|
||||
StateProvider, StateRootProvider, StorageRootProvider,
|
||||
StateProvider, StateProviderBox, StateRootProvider, StorageRootProvider,
|
||||
};
|
||||
use reth_trie::{
|
||||
updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof,
|
||||
MultiProofTargets, StorageMultiProof, TrieInput,
|
||||
};
|
||||
use revm_database::BundleState;
|
||||
use std::sync::OnceLock;
|
||||
use std::{borrow::Cow, sync::OnceLock};
|
||||
|
||||
/// A state provider that stores references to in-memory blocks along with their state as well as a
|
||||
/// reference of the historical state provider for fallback lookups.
|
||||
@@ -24,15 +24,11 @@ pub struct MemoryOverlayStateProviderRef<
|
||||
/// Historical state provider for state lookups that are not found in memory blocks.
|
||||
pub(crate) historical: Box<dyn StateProvider + 'a>,
|
||||
/// The collection of executed parent blocks. Expected order is newest to oldest.
|
||||
pub(crate) in_memory: Vec<ExecutedBlock<N>>,
|
||||
pub(crate) in_memory: Cow<'a, [ExecutedBlock<N>]>,
|
||||
/// Lazy-loaded in-memory trie data.
|
||||
pub(crate) trie_input: OnceLock<TrieInput>,
|
||||
}
|
||||
|
||||
/// A state provider that stores references to in-memory blocks along with their state as well as
|
||||
/// the historical state provider for fallback lookups.
|
||||
pub type MemoryOverlayStateProvider<N> = MemoryOverlayStateProviderRef<'static, N>;
|
||||
|
||||
impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
|
||||
/// Create new memory overlay state provider.
|
||||
///
|
||||
@@ -42,7 +38,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
|
||||
/// - `historical` - a historical state provider for the latest ancestor block stored in the
|
||||
/// database.
|
||||
pub fn new(historical: Box<dyn StateProvider + 'a>, in_memory: Vec<ExecutedBlock<N>>) -> Self {
|
||||
Self { historical, in_memory, trie_input: OnceLock::new() }
|
||||
Self { historical, in_memory: Cow::Owned(in_memory), trie_input: OnceLock::new() }
|
||||
}
|
||||
|
||||
/// Turn this state provider into a state provider
|
||||
@@ -53,11 +49,14 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
|
||||
/// Return lazy-loaded trie state aggregated from in-memory blocks.
|
||||
fn trie_input(&self) -> &TrieInput {
|
||||
self.trie_input.get_or_init(|| {
|
||||
let bundles: Vec<_> =
|
||||
self.in_memory.iter().rev().map(|block| block.trie_data()).collect();
|
||||
TrieInput::from_blocks_sorted(
|
||||
bundles.iter().map(|data| (data.hashed_state.as_ref(), data.trie_updates.as_ref())),
|
||||
)
|
||||
let mut input = TrieInput::default();
|
||||
// Iterate from oldest to newest
|
||||
for block in self.in_memory.iter().rev() {
|
||||
let data = block.trie_data();
|
||||
input.nodes.extend_from_sorted(&data.trie_updates);
|
||||
input.state.extend_from_sorted(&data.hashed_state);
|
||||
}
|
||||
input
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +70,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
|
||||
|
||||
impl<N: NodePrimitives> BlockHashReader for MemoryOverlayStateProviderRef<'_, N> {
|
||||
fn block_hash(&self, number: BlockNumber) -> ProviderResult<Option<B256>> {
|
||||
for block in &self.in_memory {
|
||||
for block in self.in_memory.iter() {
|
||||
if block.recovered_block().number() == number {
|
||||
return Ok(Some(block.recovered_block().hash()));
|
||||
}
|
||||
@@ -90,7 +89,7 @@ impl<N: NodePrimitives> BlockHashReader for MemoryOverlayStateProviderRef<'_, N>
|
||||
let mut in_memory_hashes = Vec::with_capacity(range.size_hint().0);
|
||||
|
||||
// iterate in ascending order (oldest to newest = low to high)
|
||||
for block in &self.in_memory {
|
||||
for block in self.in_memory.iter() {
|
||||
let block_num = block.recovered_block().number();
|
||||
if range.contains(&block_num) {
|
||||
in_memory_hashes.push(block.recovered_block().hash());
|
||||
@@ -112,7 +111,7 @@ impl<N: NodePrimitives> BlockHashReader for MemoryOverlayStateProviderRef<'_, N>
|
||||
|
||||
impl<N: NodePrimitives> AccountReader for MemoryOverlayStateProviderRef<'_, N> {
|
||||
fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
|
||||
for block in &self.in_memory {
|
||||
for block in self.in_memory.iter() {
|
||||
if let Some(account) = block.execution_output.account(address) {
|
||||
return Ok(account);
|
||||
}
|
||||
@@ -216,7 +215,7 @@ impl<N: NodePrimitives> StateProvider for MemoryOverlayStateProviderRef<'_, N> {
|
||||
address: Address,
|
||||
storage_key: StorageKey,
|
||||
) -> ProviderResult<Option<StorageValue>> {
|
||||
for block in &self.in_memory {
|
||||
for block in self.in_memory.iter() {
|
||||
if let Some(value) = block.execution_output.storage(&address, storage_key.into()) {
|
||||
return Ok(Some(value));
|
||||
}
|
||||
@@ -228,7 +227,7 @@ impl<N: NodePrimitives> StateProvider for MemoryOverlayStateProviderRef<'_, N> {
|
||||
|
||||
impl<N: NodePrimitives> BytecodeReader for MemoryOverlayStateProviderRef<'_, N> {
|
||||
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
|
||||
for block in &self.in_memory {
|
||||
for block in self.in_memory.iter() {
|
||||
if let Some(contract) = block.execution_output.bytecode(code_hash) {
|
||||
return Ok(Some(contract));
|
||||
}
|
||||
@@ -237,3 +236,46 @@ impl<N: NodePrimitives> BytecodeReader for MemoryOverlayStateProviderRef<'_, N>
|
||||
self.historical.bytecode_by_hash(code_hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned state provider that stores references to in-memory blocks along with their state as
|
||||
/// well as a reference of the historical state provider for fallback lookups.
|
||||
#[expect(missing_debug_implementations)]
|
||||
pub struct MemoryOverlayStateProvider<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives> {
|
||||
/// Historical state provider for state lookups that are not found in memory blocks.
|
||||
pub(crate) historical: StateProviderBox,
|
||||
/// The collection of executed parent blocks. Expected order is newest to oldest.
|
||||
pub(crate) in_memory: Vec<ExecutedBlock<N>>,
|
||||
/// Lazy-loaded in-memory trie data.
|
||||
pub(crate) trie_input: OnceLock<TrieInput>,
|
||||
}
|
||||
|
||||
impl<N: NodePrimitives> MemoryOverlayStateProvider<N> {
|
||||
/// Create new memory overlay state provider.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// - `in_memory` - the collection of executed ancestor blocks in reverse.
|
||||
/// - `historical` - a historical state provider for the latest ancestor block stored in the
|
||||
/// database.
|
||||
pub fn new(historical: StateProviderBox, in_memory: Vec<ExecutedBlock<N>>) -> Self {
|
||||
Self { historical, in_memory, trie_input: OnceLock::new() }
|
||||
}
|
||||
|
||||
/// Returns a new provider that takes the `TX` as reference
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> MemoryOverlayStateProviderRef<'_, N> {
|
||||
MemoryOverlayStateProviderRef {
|
||||
historical: Box::new(self.historical.as_ref()),
|
||||
in_memory: Cow::Borrowed(&self.in_memory),
|
||||
trie_input: self.trie_input.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the [`Self`] in a `Box`.
|
||||
pub fn boxed(self) -> StateProviderBox {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Delegates all provider impls to [`MemoryOverlayStateProviderRef`]
|
||||
reth_storage_api::macros::delegate_provider_impls!(MemoryOverlayStateProvider<N> where [N: NodePrimitives]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::{
|
||||
CanonStateNotifications, CanonStateSubscriptions, ForkChoiceNotifications,
|
||||
ForkChoiceSubscriptions,
|
||||
ForkChoiceSubscriptions, PersistedBlockNotifications, PersistedBlockSubscriptions,
|
||||
};
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
use reth_storage_api::noop::NoopProvider;
|
||||
@@ -27,3 +27,10 @@ impl<C: Send + Sync, N: NodePrimitives> ForkChoiceSubscriptions for NoopProvider
|
||||
ForkChoiceNotifications(rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Send + Sync, N: NodePrimitives> PersistedBlockSubscriptions for NoopProvider<C, N> {
|
||||
fn subscribe_persisted_block(&self) -> PersistedBlockNotifications {
|
||||
let (_, rx) = watch::channel(None);
|
||||
PersistedBlockNotifications(rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Canonical chain state notification trait and types.
|
||||
|
||||
use alloy_eips::eip2718::Encodable2718;
|
||||
use alloy_eips::{eip2718::Encodable2718, BlockNumHash};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use reth_execution_types::{BlockReceipts, Chain};
|
||||
use reth_primitives_traits::{NodePrimitives, RecoveredBlock, SealedHeader};
|
||||
@@ -205,22 +205,22 @@ pub trait ForkChoiceSubscriptions: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream for fork choice watch channels (pending, safe or finalized watchers)
|
||||
/// A stream that yields values from a `watch::Receiver<Option<T>>`, filtering out `None` values.
|
||||
#[derive(Debug)]
|
||||
#[pin_project::pin_project]
|
||||
pub struct ForkChoiceStream<T> {
|
||||
pub struct WatchValueStream<T> {
|
||||
#[pin]
|
||||
st: WatchStream<Option<T>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Sync + Send + 'static> ForkChoiceStream<T> {
|
||||
/// Creates a new `ForkChoiceStream`
|
||||
impl<T: Clone + Sync + Send + 'static> WatchValueStream<T> {
|
||||
/// Creates a new [`WatchValueStream`]
|
||||
pub fn new(rx: watch::Receiver<Option<T>>) -> Self {
|
||||
Self { st: WatchStream::from_changes(rx) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Sync + Send + 'static> Stream for ForkChoiceStream<T> {
|
||||
impl<T: Clone + Sync + Send + 'static> Stream for WatchValueStream<T> {
|
||||
type Item = T;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
@@ -234,6 +234,24 @@ impl<T: Clone + Sync + Send + 'static> Stream for ForkChoiceStream<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for [`WatchValueStream`] for fork choice watch channels.
|
||||
pub type ForkChoiceStream<T> = WatchValueStream<T>;
|
||||
|
||||
/// Wrapper around a watch receiver that receives persisted block notifications.
|
||||
#[derive(Debug, Deref, DerefMut)]
|
||||
pub struct PersistedBlockNotifications(pub watch::Receiver<Option<BlockNumHash>>);
|
||||
|
||||
/// A trait that allows subscribing to persisted block events.
|
||||
pub trait PersistedBlockSubscriptions: Send + Sync {
|
||||
/// Get notified when a new block is persisted to disk.
|
||||
fn subscribe_persisted_block(&self) -> PersistedBlockNotifications;
|
||||
|
||||
/// Convenience method to get a stream of the persisted blocks.
|
||||
fn persisted_block_stream(&self) -> WatchValueStream<BlockNumHash> {
|
||||
WatchValueStream::new(self.subscribe_persisted_block().0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -242,6 +260,7 @@ mod tests {
|
||||
use reth_ethereum_primitives::{Receipt, TransactionSigned, TxType};
|
||||
use reth_execution_types::ExecutionOutcome;
|
||||
use reth_primitives_traits::SealedBlock;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn test_commit_notification() {
|
||||
@@ -260,7 +279,7 @@ mod tests {
|
||||
let chain: Arc<Chain> = Arc::new(Chain::new(
|
||||
vec![block1.clone(), block2.clone()],
|
||||
ExecutionOutcome::default(),
|
||||
None,
|
||||
BTreeMap::new(),
|
||||
));
|
||||
|
||||
// Create a commit notification
|
||||
@@ -295,12 +314,15 @@ mod tests {
|
||||
block3.set_block_number(3);
|
||||
block3.set_hash(block3_hash);
|
||||
|
||||
let old_chain: Arc<Chain> =
|
||||
Arc::new(Chain::new(vec![block1.clone()], ExecutionOutcome::default(), None));
|
||||
let old_chain: Arc<Chain> = Arc::new(Chain::new(
|
||||
vec![block1.clone()],
|
||||
ExecutionOutcome::default(),
|
||||
BTreeMap::new(),
|
||||
));
|
||||
let new_chain = Arc::new(Chain::new(
|
||||
vec![block2.clone(), block3.clone()],
|
||||
ExecutionOutcome::default(),
|
||||
None,
|
||||
BTreeMap::new(),
|
||||
));
|
||||
|
||||
// Create a reorg notification
|
||||
@@ -362,8 +384,11 @@ mod tests {
|
||||
let execution_outcome = ExecutionOutcome { receipts, ..Default::default() };
|
||||
|
||||
// Create a new chain segment with `block1` and `block2` and the execution outcome.
|
||||
let new_chain: Arc<Chain> =
|
||||
Arc::new(Chain::new(vec![block1.clone(), block2.clone()], execution_outcome, None));
|
||||
let new_chain: Arc<Chain> = Arc::new(Chain::new(
|
||||
vec![block1.clone(), block2.clone()],
|
||||
execution_outcome,
|
||||
BTreeMap::new(),
|
||||
));
|
||||
|
||||
// Create a commit notification containing the new chain segment.
|
||||
let notification = CanonStateNotification::Commit { new: new_chain };
|
||||
@@ -421,7 +446,7 @@ mod tests {
|
||||
|
||||
// Create an old chain segment to be reverted, containing `old_block1`.
|
||||
let old_chain: Arc<Chain> =
|
||||
Arc::new(Chain::new(vec![old_block1.clone()], old_execution_outcome, None));
|
||||
Arc::new(Chain::new(vec![old_block1.clone()], old_execution_outcome, BTreeMap::new()));
|
||||
|
||||
// Define block2 for the new chain segment, which will be committed.
|
||||
let mut body = BlockBody::<TransactionSigned>::default();
|
||||
@@ -449,7 +474,8 @@ mod tests {
|
||||
ExecutionOutcome { receipts: new_receipts, ..Default::default() };
|
||||
|
||||
// Create a new chain segment to be committed, containing `new_block1`.
|
||||
let new_chain = Arc::new(Chain::new(vec![new_block1.clone()], new_execution_outcome, None));
|
||||
let new_chain =
|
||||
Arc::new(Chain::new(vec![new_block1.clone()], new_execution_outcome, BTreeMap::new()));
|
||||
|
||||
// Create a reorg notification with both reverted (old) and committed (new) chain segments.
|
||||
let notification = CanonStateNotification::Reorg { old: old_chain, new: new_chain };
|
||||
|
||||
@@ -3,10 +3,7 @@ use crate::{
|
||||
CanonStateSubscriptions, ComputedTrieData,
|
||||
};
|
||||
use alloy_consensus::{Header, SignableTransaction, TxEip1559, TxReceipt, EMPTY_ROOT_HASH};
|
||||
use alloy_eips::{
|
||||
eip1559::{ETHEREUM_BLOCK_GAS_LIMIT_30M, INITIAL_BASE_FEE},
|
||||
eip7685::Requests,
|
||||
};
|
||||
use alloy_eips::eip1559::{ETHEREUM_BLOCK_GAS_LIMIT_30M, INITIAL_BASE_FEE};
|
||||
use alloy_primitives::{Address, BlockNumber, B256, U256};
|
||||
use alloy_signer::SignerSync;
|
||||
use alloy_signer_local::PrivateKeySigner;
|
||||
@@ -16,7 +13,7 @@ use reth_chainspec::{ChainSpec, EthereumHardfork, MIN_TRANSACTION_GAS};
|
||||
use reth_ethereum_primitives::{
|
||||
Block, BlockBody, EthPrimitives, Receipt, Transaction, TransactionSigned,
|
||||
};
|
||||
use reth_execution_types::{Chain, ExecutionOutcome};
|
||||
use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, Chain, ExecutionOutcome};
|
||||
use reth_primitives_traits::{
|
||||
proofs::{calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root},
|
||||
Account, NodePrimitives, Recovered, RecoveredBlock, SealedBlock, SealedHeader,
|
||||
@@ -117,7 +114,7 @@ impl<N: NodePrimitives> TestBlockBuilder<N> {
|
||||
.map(|_| {
|
||||
let tx = mock_tx(self.signer_build_account_info.nonce);
|
||||
self.signer_build_account_info.nonce += 1;
|
||||
self.signer_build_account_info.balance -= signer_balance_decrease;
|
||||
self.signer_build_account_info.balance -= Self::single_tx_cost();
|
||||
tx
|
||||
})
|
||||
.collect();
|
||||
@@ -201,7 +198,7 @@ impl<N: NodePrimitives> TestBlockBuilder<N> {
|
||||
fn get_executed_block(
|
||||
&mut self,
|
||||
block_number: BlockNumber,
|
||||
receipts: Vec<Vec<Receipt>>,
|
||||
mut receipts: Vec<Vec<Receipt>>,
|
||||
parent_hash: B256,
|
||||
) -> ExecutedBlock {
|
||||
let block = self.generate_random_block(block_number, parent_hash);
|
||||
@@ -209,12 +206,15 @@ impl<N: NodePrimitives> TestBlockBuilder<N> {
|
||||
let trie_data = ComputedTrieData::default();
|
||||
ExecutedBlock::new(
|
||||
Arc::new(RecoveredBlock::new_sealed(block, senders)),
|
||||
Arc::new(ExecutionOutcome::new(
|
||||
BundleState::default(),
|
||||
receipts,
|
||||
block_number,
|
||||
vec![Requests::default()],
|
||||
)),
|
||||
Arc::new(BlockExecutionOutput {
|
||||
result: BlockExecutionResult {
|
||||
receipts: receipts.pop().unwrap_or_default(),
|
||||
requests: Default::default(),
|
||||
gas_used: 0,
|
||||
blob_gas_used: 0,
|
||||
},
|
||||
state: BundleState::default(),
|
||||
}),
|
||||
trie_data,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub use alloy_chains::{Chain, ChainKind, NamedChain};
|
||||
/// Re-export for convenience
|
||||
pub use reth_ethereum_forks::*;
|
||||
|
||||
pub use alloy_evm::EvmLimitParams;
|
||||
pub use api::EthChainSpec;
|
||||
pub use info::ChainInfo;
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
|
||||
@@ -80,6 +80,8 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
|
||||
.then_some(EMPTY_REQUESTS_HASH);
|
||||
|
||||
Header {
|
||||
number: genesis.number.unwrap_or_default(),
|
||||
parent_hash: genesis.parent_hash.unwrap_or_default(),
|
||||
gas_limit: genesis.gas_limit,
|
||||
difficulty: genesis.difficulty,
|
||||
nonce: genesis.nonce.into(),
|
||||
@@ -276,6 +278,7 @@ pub fn create_chain_config(
|
||||
// Check if DAO fork is supported (it has an activation block)
|
||||
let dao_fork_support = hardforks.fork(EthereumHardfork::Dao) != ForkCondition::Never;
|
||||
|
||||
#[allow(clippy::needless_update)]
|
||||
ChainConfig {
|
||||
chain_id: chain.map(|c| c.id()).unwrap_or(0),
|
||||
homestead_block: block_num(EthereumHardfork::Homestead),
|
||||
@@ -311,6 +314,7 @@ pub fn create_chain_config(
|
||||
extra_fields: Default::default(),
|
||||
deposit_contract_address,
|
||||
blob_schedule,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +462,18 @@ impl ChainSpec {
|
||||
pub fn builder() -> ChainSpecBuilder {
|
||||
ChainSpecBuilder::default()
|
||||
}
|
||||
|
||||
/// Map a chain ID to a known chain spec, if available.
|
||||
pub fn from_chain_id(chain_id: u64) -> Option<Arc<Self>> {
|
||||
match NamedChain::try_from(chain_id).ok()? {
|
||||
NamedChain::Mainnet => Some(MAINNET.clone()),
|
||||
NamedChain::Sepolia => Some(SEPOLIA.clone()),
|
||||
NamedChain::Holesky => Some(HOLESKY.clone()),
|
||||
NamedChain::Hoodi => Some(HOODI.clone()),
|
||||
NamedChain::Dev => Some(DEV.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: BlockHeader> ChainSpec<H> {
|
||||
@@ -968,7 +984,7 @@ impl<H: BlockHeader> EthereumHardforks for ChainSpec<H> {
|
||||
|
||||
/// A trait for reading the current chainspec.
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait ChainSpecProvider: Debug + Send + Sync {
|
||||
pub trait ChainSpecProvider: Debug + Send {
|
||||
/// The chain spec type.
|
||||
type ChainSpec: EthChainSpec + 'static;
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ reth-stages-types = { workspace = true, optional = true }
|
||||
reth-static-file-types = { workspace = true, features = ["clap"] }
|
||||
reth-static-file.workspace = true
|
||||
reth-tasks.workspace = true
|
||||
reth-storage-api.workspace = true
|
||||
reth-trie = { workspace = true, features = ["metrics"] }
|
||||
reth-trie-db = { workspace = true, features = ["metrics"] }
|
||||
reth-trie-common.workspace = true
|
||||
@@ -83,6 +84,7 @@ backon.workspace = true
|
||||
secp256k1 = { workspace = true, features = ["global-context", "std", "recovery"] }
|
||||
tokio-stream.workspace = true
|
||||
reqwest.workspace = true
|
||||
url.workspace = true
|
||||
metrics.workspace = true
|
||||
|
||||
# io
|
||||
@@ -129,3 +131,5 @@ arbitrary = [
|
||||
"reth-primitives-traits/arbitrary",
|
||||
"reth-ethereum-primitives/arbitrary",
|
||||
]
|
||||
|
||||
edge = ["reth-db-common/edge", "reth-stages/rocksdb", "reth-provider/rocksdb"]
|
||||
|
||||
@@ -19,12 +19,15 @@ use reth_node_builder::{
|
||||
Node, NodeComponents, NodeComponentsBuilder, NodeTypes, NodeTypesWithDBAdapter,
|
||||
};
|
||||
use reth_node_core::{
|
||||
args::{DatabaseArgs, DatadirArgs, StaticFilesArgs},
|
||||
args::{DatabaseArgs, DatadirArgs, RocksDbArgs, StaticFilesArgs},
|
||||
dirs::{ChainPath, DataDirPath},
|
||||
};
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, NodeTypesForProvider, RocksDBProvider, StaticFileProvider},
|
||||
ProviderFactory, StaticFileProviderFactory,
|
||||
providers::{
|
||||
BlockchainProvider, NodeTypesForProvider, RocksDBProvider, StaticFileProvider,
|
||||
StaticFileProviderBuilder,
|
||||
},
|
||||
ProviderFactory, StaticFileProviderFactory, StorageSettings,
|
||||
};
|
||||
use reth_stages::{sets::DefaultStages, Pipeline, PipelineTarget};
|
||||
use reth_static_file::StaticFileProducer;
|
||||
@@ -63,9 +66,24 @@ pub struct EnvironmentArgs<C: ChainSpecParser> {
|
||||
/// All static files related arguments
|
||||
#[command(flatten)]
|
||||
pub static_files: StaticFilesArgs,
|
||||
|
||||
/// All `RocksDB` related arguments
|
||||
#[command(flatten)]
|
||||
pub rocksdb: RocksDbArgs,
|
||||
}
|
||||
|
||||
impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
/// Returns the effective storage settings derived from static-file and `RocksDB` CLI args.
|
||||
pub fn storage_settings(&self) -> StorageSettings {
|
||||
StorageSettings::base()
|
||||
.with_receipts_in_static_files(self.static_files.receipts)
|
||||
.with_transaction_senders_in_static_files(self.static_files.transaction_senders)
|
||||
.with_account_changesets_in_static_files(self.static_files.account_changesets)
|
||||
.with_transaction_hash_numbers_in_rocksdb(self.rocksdb.all || self.rocksdb.tx_hash)
|
||||
.with_storages_history_in_rocksdb(self.rocksdb.all || self.rocksdb.storages_history)
|
||||
.with_account_history_in_rocksdb(self.rocksdb.all || self.rocksdb.account_history)
|
||||
}
|
||||
|
||||
/// Initializes environment according to [`AccessRights`] and returns an instance of
|
||||
/// [`Environment`].
|
||||
pub fn init<N: CliNodeTypes>(&self, access: AccessRights) -> eyre::Result<Environment<N>>
|
||||
@@ -100,27 +118,35 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", ?db_path, ?sf_path, "Opening storage");
|
||||
let genesis_block_number = self.chain.genesis().number.unwrap_or_default();
|
||||
let (db, sfp) = match access {
|
||||
AccessRights::RW => (
|
||||
Arc::new(init_db(db_path, self.db.database_args())?),
|
||||
StaticFileProvider::read_write(sf_path)?,
|
||||
),
|
||||
AccessRights::RO | AccessRights::RoInconsistent => (
|
||||
Arc::new(open_db_read_only(&db_path, self.db.database_args())?),
|
||||
StaticFileProvider::read_only(sf_path, false)?,
|
||||
StaticFileProviderBuilder::read_write(sf_path)
|
||||
.with_genesis_block_number(genesis_block_number)
|
||||
.build()?,
|
||||
),
|
||||
AccessRights::RO | AccessRights::RoInconsistent => {
|
||||
(Arc::new(open_db_read_only(&db_path, self.db.database_args())?), {
|
||||
let provider = StaticFileProviderBuilder::read_only(sf_path)
|
||||
.with_genesis_block_number(genesis_block_number)
|
||||
.build()?;
|
||||
provider.watch_directory();
|
||||
provider
|
||||
})
|
||||
}
|
||||
};
|
||||
// TransactionDB only support read-write mode
|
||||
let rocksdb_provider = RocksDBProvider::builder(data_dir.rocksdb())
|
||||
.with_default_tables()
|
||||
.with_database_log_level(self.db.log_level)
|
||||
.with_read_only(!access.is_read_write())
|
||||
.build()?;
|
||||
|
||||
let provider_factory =
|
||||
self.create_provider_factory(&config, db, sfp, rocksdb_provider, access)?;
|
||||
if access.is_read_write() {
|
||||
debug!(target: "reth::cli", chain=%self.chain.chain(), genesis=?self.chain.genesis_hash(), "Initializing genesis");
|
||||
init_genesis_with_settings(&provider_factory, self.static_files.to_settings())?;
|
||||
init_genesis_with_settings(&provider_factory, self.storage_settings())?;
|
||||
}
|
||||
|
||||
Ok(Environment { config, provider_factory, data_dir })
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
use crate::{
|
||||
common::CliNodeTypes,
|
||||
db::get::{maybe_json_value_parser, table_key},
|
||||
};
|
||||
use alloy_primitives::map::foldhash::fast::FixedState;
|
||||
use clap::Parser;
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_db::DatabaseEnv;
|
||||
use reth_db_api::{
|
||||
cursor::DbCursorRO, table::Table, transaction::DbTx, RawKey, RawTable, RawValue, TableViewer,
|
||||
Tables,
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::{NodeTypesWithDB, NodeTypesWithDBAdapter};
|
||||
use reth_provider::{providers::ProviderNodeTypes, DBProvider};
|
||||
use std::{
|
||||
hash::{BuildHasher, Hasher},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
/// The arguments for the `reth db checksum` command
|
||||
pub struct Command {
|
||||
/// The table name
|
||||
table: Tables,
|
||||
|
||||
/// The start of the range to checksum.
|
||||
#[arg(long, value_parser = maybe_json_value_parser)]
|
||||
start_key: Option<String>,
|
||||
|
||||
/// The end of the range to checksum.
|
||||
#[arg(long, value_parser = maybe_json_value_parser)]
|
||||
end_key: Option<String>,
|
||||
|
||||
/// The maximum number of records that are queried and used to compute the
|
||||
/// checksum.
|
||||
#[arg(long)]
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute `db checksum` command
|
||||
pub fn execute<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
self,
|
||||
tool: &DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
|
||||
) -> eyre::Result<()> {
|
||||
warn!("This command should be run without the node running!");
|
||||
self.table.view(&ChecksumViewer {
|
||||
tool,
|
||||
start_key: self.start_key,
|
||||
end_key: self.end_key,
|
||||
limit: self.limit,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ChecksumViewer<'a, N: NodeTypesWithDB> {
|
||||
tool: &'a DbTool<N>,
|
||||
start_key: Option<String>,
|
||||
end_key: Option<String>,
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> ChecksumViewer<'_, N> {
|
||||
pub(crate) const fn new(tool: &'_ DbTool<N>) -> ChecksumViewer<'_, N> {
|
||||
ChecksumViewer { tool, start_key: None, end_key: None, limit: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N> {
|
||||
type Error = eyre::Report;
|
||||
|
||||
fn view<T: Table>(&self) -> Result<(u64, Duration), Self::Error> {
|
||||
let provider =
|
||||
self.tool.provider_factory.provider()?.disable_long_read_transaction_safety();
|
||||
let tx = provider.tx_ref();
|
||||
info!(
|
||||
"Start computing checksum, start={:?}, end={:?}, limit={:?}",
|
||||
self.start_key, self.end_key, self.limit
|
||||
);
|
||||
|
||||
let mut cursor = tx.cursor_read::<RawTable<T>>()?;
|
||||
let walker = match (self.start_key.as_deref(), self.end_key.as_deref()) {
|
||||
(Some(start), Some(end)) => {
|
||||
let start_key = table_key::<T>(start).map(RawKey::new)?;
|
||||
let end_key = table_key::<T>(end).map(RawKey::new)?;
|
||||
cursor.walk_range(start_key..=end_key)?
|
||||
}
|
||||
(None, Some(end)) => {
|
||||
let end_key = table_key::<T>(end).map(RawKey::new)?;
|
||||
|
||||
cursor.walk_range(..=end_key)?
|
||||
}
|
||||
(Some(start), None) => {
|
||||
let start_key = table_key::<T>(start).map(RawKey::new)?;
|
||||
cursor.walk_range(start_key..)?
|
||||
}
|
||||
(None, None) => cursor.walk_range(..)?,
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut hasher = FixedState::with_seed(u64::from_be_bytes(*b"RETHRETH")).build_hasher();
|
||||
let mut total = 0;
|
||||
|
||||
let limit = self.limit.unwrap_or(usize::MAX);
|
||||
let mut enumerate_start_key = None;
|
||||
let mut enumerate_end_key = None;
|
||||
for (index, entry) in walker.enumerate() {
|
||||
let (k, v): (RawKey<T::Key>, RawValue<T::Value>) = entry?;
|
||||
|
||||
if index.is_multiple_of(100_000) {
|
||||
info!("Hashed {index} entries.");
|
||||
}
|
||||
|
||||
hasher.write(k.raw_key());
|
||||
hasher.write(v.raw_value());
|
||||
|
||||
if enumerate_start_key.is_none() {
|
||||
enumerate_start_key = Some(k.clone());
|
||||
}
|
||||
enumerate_end_key = Some(k);
|
||||
|
||||
total = index + 1;
|
||||
if total >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
info!("Hashed {total} entries.");
|
||||
if let (Some(s), Some(e)) = (enumerate_start_key, enumerate_end_key) {
|
||||
info!("start-key: {}", serde_json::to_string(&s.key()?).unwrap_or_default());
|
||||
info!("end-key: {}", serde_json::to_string(&e.key()?).unwrap_or_default());
|
||||
}
|
||||
|
||||
let checksum = hasher.finish();
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
info!("Checksum for table `{}`: {:#x} (elapsed: {:?})", T::NAME, checksum, elapsed);
|
||||
|
||||
Ok((checksum, elapsed))
|
||||
}
|
||||
}
|
||||
288
crates/cli/commands/src/db/checksum/mod.rs
Normal file
288
crates/cli/commands/src/db/checksum/mod.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use crate::{
|
||||
common::CliNodeTypes,
|
||||
db::get::{maybe_json_value_parser, table_key},
|
||||
};
|
||||
use alloy_primitives::map::foldhash::fast::FixedState;
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_db::{static_file::iter_static_files, DatabaseEnv};
|
||||
use reth_db_api::{
|
||||
cursor::DbCursorRO, table::Table, transaction::DbTx, RawKey, RawTable, RawValue, TableViewer,
|
||||
Tables,
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::{NodeTypesWithDB, NodeTypesWithDBAdapter};
|
||||
use reth_provider::{providers::ProviderNodeTypes, DBProvider, StaticFileProviderFactory};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use std::{
|
||||
hash::{BuildHasher, Hasher},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[cfg(all(unix, feature = "edge"))]
|
||||
mod rocksdb;
|
||||
|
||||
/// Interval for logging progress during checksum computation.
|
||||
const PROGRESS_LOG_INTERVAL: usize = 100_000;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
/// The arguments for the `reth db checksum` command
|
||||
pub struct Command {
|
||||
#[command(subcommand)]
|
||||
subcommand: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Subcommand {
|
||||
/// Calculates the checksum of a database table
|
||||
Mdbx {
|
||||
/// The table name
|
||||
table: Tables,
|
||||
|
||||
/// The start of the range to checksum.
|
||||
#[arg(long, value_parser = maybe_json_value_parser)]
|
||||
start_key: Option<String>,
|
||||
|
||||
/// The end of the range to checksum.
|
||||
#[arg(long, value_parser = maybe_json_value_parser)]
|
||||
end_key: Option<String>,
|
||||
|
||||
/// The maximum number of records that are queried and used to compute the
|
||||
/// checksum.
|
||||
#[arg(long)]
|
||||
limit: Option<usize>,
|
||||
},
|
||||
/// Calculates the checksum of a static file segment
|
||||
StaticFile {
|
||||
/// The static file segment
|
||||
#[arg(value_enum)]
|
||||
segment: StaticFileSegment,
|
||||
|
||||
/// The block number to start from (inclusive).
|
||||
#[arg(long)]
|
||||
start_block: Option<u64>,
|
||||
|
||||
/// The block number to end at (inclusive).
|
||||
#[arg(long)]
|
||||
end_block: Option<u64>,
|
||||
|
||||
/// The maximum number of rows to checksum.
|
||||
#[arg(long)]
|
||||
limit: Option<usize>,
|
||||
},
|
||||
/// Calculates the checksum of a RocksDB table
|
||||
#[cfg(all(unix, feature = "edge"))]
|
||||
Rocksdb {
|
||||
/// The RocksDB table
|
||||
#[arg(value_enum)]
|
||||
table: rocksdb::RocksDbTable,
|
||||
|
||||
/// The maximum number of records to checksum.
|
||||
#[arg(long)]
|
||||
limit: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Execute `db checksum` command
|
||||
pub fn execute<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
self,
|
||||
tool: &DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
|
||||
) -> eyre::Result<()> {
|
||||
warn!("This command should be run without the node running!");
|
||||
|
||||
match self.subcommand {
|
||||
Subcommand::Mdbx { table, start_key, end_key, limit } => {
|
||||
table.view(&ChecksumViewer { tool, start_key, end_key, limit })?;
|
||||
}
|
||||
Subcommand::StaticFile { segment, start_block, end_block, limit } => {
|
||||
checksum_static_file(tool, segment, start_block, end_block, limit)?;
|
||||
}
|
||||
#[cfg(all(unix, feature = "edge"))]
|
||||
Subcommand::Rocksdb { table, limit } => {
|
||||
rocksdb::checksum_rocksdb(tool, table, limit)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new hasher with the standard seed used for checksum computation.
|
||||
fn checksum_hasher() -> impl Hasher {
|
||||
FixedState::with_seed(u64::from_be_bytes(*b"RETHRETH")).build_hasher()
|
||||
}
|
||||
|
||||
fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
tool: &DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
|
||||
segment: StaticFileSegment,
|
||||
start_block: Option<u64>,
|
||||
end_block: Option<u64>,
|
||||
limit: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
let static_file_provider = tool.provider_factory.static_file_provider();
|
||||
if let Err(err) = static_file_provider.check_consistency(&tool.provider_factory.provider()?) {
|
||||
warn!("Error checking consistency of static files: {err}");
|
||||
}
|
||||
|
||||
let static_files = iter_static_files(static_file_provider.directory())?;
|
||||
|
||||
let ranges = static_files
|
||||
.get(segment)
|
||||
.ok_or_else(|| eyre::eyre!("No static files found for segment: {}", segment))?;
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut hasher = checksum_hasher();
|
||||
let mut total = 0usize;
|
||||
let limit = limit.unwrap_or(usize::MAX);
|
||||
|
||||
let start_block = start_block.unwrap_or(0);
|
||||
let end_block = end_block.unwrap_or(u64::MAX);
|
||||
|
||||
info!(
|
||||
"Computing checksum for {} static files, start_block={}, end_block={}, limit={:?}",
|
||||
segment,
|
||||
start_block,
|
||||
end_block,
|
||||
if limit == usize::MAX { None } else { Some(limit) }
|
||||
);
|
||||
|
||||
'outer: for (block_range, _header) in ranges.iter().sorted_by_key(|(range, _)| range.start()) {
|
||||
if block_range.end() < start_block || block_range.start() > end_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fixed_block_range = static_file_provider.find_fixed_range(segment, block_range.start());
|
||||
let jar_provider = static_file_provider
|
||||
.get_segment_provider_for_range(segment, || Some(fixed_block_range), None)?
|
||||
.ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"Failed to get segment provider for segment {} at range {}",
|
||||
segment,
|
||||
block_range
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut cursor = jar_provider.cursor()?;
|
||||
|
||||
while let Ok(Some(row)) = cursor.next_row() {
|
||||
for col_data in row.iter() {
|
||||
hasher.write(col_data);
|
||||
}
|
||||
|
||||
total += 1;
|
||||
|
||||
if total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
|
||||
info!("Hashed {total} entries.");
|
||||
}
|
||||
|
||||
if total >= limit {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly drop provider before removing from cache to avoid deadlock
|
||||
drop(jar_provider);
|
||||
static_file_provider.remove_cached_provider(segment, fixed_block_range.end());
|
||||
}
|
||||
|
||||
let checksum = hasher.finish();
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
info!(
|
||||
"Checksum for static file segment `{}`: {:#x} ({} entries, elapsed: {:?})",
|
||||
segment, checksum, total, elapsed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) struct ChecksumViewer<'a, N: NodeTypesWithDB> {
|
||||
tool: &'a DbTool<N>,
|
||||
start_key: Option<String>,
|
||||
end_key: Option<String>,
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> ChecksumViewer<'_, N> {
|
||||
pub(crate) const fn new(tool: &'_ DbTool<N>) -> ChecksumViewer<'_, N> {
|
||||
ChecksumViewer { tool, start_key: None, end_key: None, limit: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N> {
|
||||
type Error = eyre::Report;
|
||||
|
||||
fn view<T: Table>(&self) -> Result<(u64, Duration), Self::Error> {
|
||||
let provider =
|
||||
self.tool.provider_factory.provider()?.disable_long_read_transaction_safety();
|
||||
let tx = provider.tx_ref();
|
||||
info!(
|
||||
"Start computing checksum, start={:?}, end={:?}, limit={:?}",
|
||||
self.start_key, self.end_key, self.limit
|
||||
);
|
||||
|
||||
let mut cursor = tx.cursor_read::<RawTable<T>>()?;
|
||||
let walker = match (self.start_key.as_deref(), self.end_key.as_deref()) {
|
||||
(Some(start), Some(end)) => {
|
||||
let start_key = table_key::<T>(start).map(RawKey::new)?;
|
||||
let end_key = table_key::<T>(end).map(RawKey::new)?;
|
||||
cursor.walk_range(start_key..=end_key)?
|
||||
}
|
||||
(None, Some(end)) => {
|
||||
let end_key = table_key::<T>(end).map(RawKey::new)?;
|
||||
|
||||
cursor.walk_range(..=end_key)?
|
||||
}
|
||||
(Some(start), None) => {
|
||||
let start_key = table_key::<T>(start).map(RawKey::new)?;
|
||||
cursor.walk_range(start_key..)?
|
||||
}
|
||||
(None, None) => cursor.walk_range(..)?,
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut hasher = checksum_hasher();
|
||||
let mut total = 0;
|
||||
|
||||
let limit = self.limit.unwrap_or(usize::MAX);
|
||||
let mut enumerate_start_key = None;
|
||||
let mut enumerate_end_key = None;
|
||||
for (index, entry) in walker.enumerate() {
|
||||
let (k, v): (RawKey<T::Key>, RawValue<T::Value>) = entry?;
|
||||
|
||||
if index.is_multiple_of(PROGRESS_LOG_INTERVAL) {
|
||||
info!("Hashed {index} entries.");
|
||||
}
|
||||
|
||||
hasher.write(k.raw_key());
|
||||
hasher.write(v.raw_value());
|
||||
|
||||
if enumerate_start_key.is_none() {
|
||||
enumerate_start_key = Some(k.clone());
|
||||
}
|
||||
enumerate_end_key = Some(k);
|
||||
|
||||
total = index + 1;
|
||||
if total >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
info!("Hashed {total} entries.");
|
||||
if let (Some(s), Some(e)) = (enumerate_start_key, enumerate_end_key) {
|
||||
info!("start-key: {}", serde_json::to_string(&s.key()?).unwrap_or_default());
|
||||
info!("end-key: {}", serde_json::to_string(&e.key()?).unwrap_or_default());
|
||||
}
|
||||
|
||||
let checksum = hasher.finish();
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
info!("Checksum for table `{}`: {:#x} (elapsed: {:?})", T::NAME, checksum, elapsed);
|
||||
|
||||
Ok((checksum, elapsed))
|
||||
}
|
||||
}
|
||||
106
crates/cli/commands/src/db/checksum/rocksdb.rs
Normal file
106
crates/cli/commands/src/db/checksum/rocksdb.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
//! RocksDB checksum implementation.
|
||||
|
||||
use super::{checksum_hasher, PROGRESS_LOG_INTERVAL};
|
||||
use crate::common::CliNodeTypes;
|
||||
use clap::ValueEnum;
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_db::{tables, DatabaseEnv};
|
||||
use reth_db_api::table::Table;
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_builder::NodeTypesWithDBAdapter;
|
||||
use reth_provider::RocksDBProviderFactory;
|
||||
use std::{hash::Hasher, sync::Arc, time::Instant};
|
||||
use tracing::info;
|
||||
|
||||
/// RocksDB tables that can be checksummed.
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RocksDbTable {
|
||||
/// Transaction hash to transaction number mapping
|
||||
TransactionHashNumbers,
|
||||
/// Account history indices
|
||||
AccountsHistory,
|
||||
/// Storage history indices
|
||||
StoragesHistory,
|
||||
}
|
||||
|
||||
impl RocksDbTable {
|
||||
/// Returns the table name as a string
|
||||
const fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::TransactionHashNumbers => tables::TransactionHashNumbers::NAME,
|
||||
Self::AccountsHistory => tables::AccountsHistory::NAME,
|
||||
Self::StoragesHistory => tables::StoragesHistory::NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a checksum for a RocksDB table.
|
||||
pub fn checksum_rocksdb<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
|
||||
tool: &DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
|
||||
table: RocksDbTable,
|
||||
limit: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
let rocksdb = tool.provider_factory.rocksdb_provider();
|
||||
|
||||
let start_time = Instant::now();
|
||||
let limit = limit.unwrap_or(usize::MAX);
|
||||
|
||||
info!(
|
||||
"Computing checksum for RocksDB table `{}`, limit={:?}",
|
||||
table.name(),
|
||||
if limit == usize::MAX { None } else { Some(limit) }
|
||||
);
|
||||
|
||||
let (checksum, total) = match table {
|
||||
RocksDbTable::TransactionHashNumbers => {
|
||||
checksum_rocksdb_table::<tables::TransactionHashNumbers>(&rocksdb, limit)?
|
||||
}
|
||||
RocksDbTable::AccountsHistory => {
|
||||
checksum_rocksdb_table::<tables::AccountsHistory>(&rocksdb, limit)?
|
||||
}
|
||||
RocksDbTable::StoragesHistory => {
|
||||
checksum_rocksdb_table::<tables::StoragesHistory>(&rocksdb, limit)?
|
||||
}
|
||||
};
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
info!(
|
||||
"Checksum for RocksDB table `{}`: {:#x} ({} entries, elapsed: {:?})",
|
||||
table.name(),
|
||||
checksum,
|
||||
total,
|
||||
elapsed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes checksum for a specific RocksDB table by iterating over rows.
|
||||
fn checksum_rocksdb_table<T: Table>(
|
||||
rocksdb: &reth_provider::providers::RocksDBProvider,
|
||||
limit: usize,
|
||||
) -> eyre::Result<(u64, usize)> {
|
||||
let iter = rocksdb.raw_iter::<T>()?;
|
||||
let mut hasher = checksum_hasher();
|
||||
let mut total = 0usize;
|
||||
|
||||
for entry in iter {
|
||||
let (key_bytes, value_bytes) = entry?;
|
||||
|
||||
hasher.write(&key_bytes);
|
||||
hasher.write(&value_bytes);
|
||||
|
||||
total += 1;
|
||||
|
||||
if total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
|
||||
info!("Hashed {total} entries.");
|
||||
}
|
||||
|
||||
if total >= limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((hasher.finish(), total))
|
||||
}
|
||||
@@ -29,7 +29,7 @@ impl Command {
|
||||
let static_file_provider = tool.provider_factory.static_file_provider();
|
||||
let static_files = iter_static_files(static_file_provider.directory())?;
|
||||
|
||||
if let Some(segment_static_files) = static_files.get(&segment) {
|
||||
if let Some(segment_static_files) = static_files.get(segment) {
|
||||
for (block_range, _) in segment_static_files {
|
||||
static_file_provider.delete_jar(segment, block_range.start())?;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use alloy_primitives::{hex, BlockHash};
|
||||
use clap::Parser;
|
||||
use reth_db::{
|
||||
static_file::{
|
||||
ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask, ReceiptMask, TransactionMask,
|
||||
TransactionSenderMask,
|
||||
AccountChangesetMask, ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask,
|
||||
ReceiptMask, TransactionMask, TransactionSenderMask,
|
||||
},
|
||||
RawDupSort,
|
||||
};
|
||||
@@ -19,8 +19,9 @@ use reth_db_common::DbTool;
|
||||
use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
|
||||
use reth_node_builder::NodeTypesWithDB;
|
||||
use reth_primitives_traits::ValueWithSubKey;
|
||||
use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory};
|
||||
use reth_provider::{providers::ProviderNodeTypes, ChangeSetReader, StaticFileProviderFactory};
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use reth_storage_api::StorageChangeSetReader;
|
||||
use tracing::error;
|
||||
|
||||
/// The arguments for the `reth db get` command
|
||||
@@ -64,6 +65,10 @@ enum Subcommand {
|
||||
#[arg(value_parser = maybe_json_value_parser)]
|
||||
key: String,
|
||||
|
||||
/// The subkey to get content for, for example address in changeset
|
||||
#[arg(value_parser = maybe_json_value_parser)]
|
||||
subkey: Option<String>,
|
||||
|
||||
/// Output bytes instead of human-readable decoded value
|
||||
#[arg(long)]
|
||||
raw: bool,
|
||||
@@ -77,33 +82,107 @@ impl Command {
|
||||
Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
|
||||
table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
|
||||
}
|
||||
Subcommand::StaticFile { segment, key, raw } => {
|
||||
let (key, mask): (u64, _) = match segment {
|
||||
Subcommand::StaticFile { segment, key, subkey, raw } => {
|
||||
if let StaticFileSegment::StorageChangeSets = segment {
|
||||
let storage_key =
|
||||
table_subkey::<tables::StorageChangeSets>(subkey.as_deref()).ok();
|
||||
let key = table_key::<tables::StorageChangeSets>(&key)?;
|
||||
|
||||
let provider = tool.provider_factory.static_file_provider();
|
||||
|
||||
if let Some(storage_key) = storage_key {
|
||||
let entry = provider.get_storage_before_block(
|
||||
key.block_number(),
|
||||
key.address(),
|
||||
storage_key,
|
||||
)?;
|
||||
|
||||
if let Some(entry) = entry {
|
||||
println!("{}", serde_json::to_string_pretty(&entry)?);
|
||||
} else {
|
||||
error!(target: "reth::cli", "No content for the given table key.");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let changesets = provider.storage_changeset(key.block_number())?;
|
||||
println!("{}", serde_json::to_string_pretty(&changesets)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (key, subkey, mask): (u64, _, _) = match segment {
|
||||
StaticFileSegment::Headers => (
|
||||
table_key::<tables::Headers>(&key)?,
|
||||
None,
|
||||
<HeaderWithHashMask<HeaderTy<N>>>::MASK,
|
||||
),
|
||||
StaticFileSegment::Transactions => {
|
||||
(table_key::<tables::Transactions>(&key)?, <TransactionMask<TxTy<N>>>::MASK)
|
||||
}
|
||||
StaticFileSegment::Receipts => {
|
||||
(table_key::<tables::Receipts>(&key)?, <ReceiptMask<ReceiptTy<N>>>::MASK)
|
||||
}
|
||||
StaticFileSegment::Transactions => (
|
||||
table_key::<tables::Transactions>(&key)?,
|
||||
None,
|
||||
<TransactionMask<TxTy<N>>>::MASK,
|
||||
),
|
||||
StaticFileSegment::Receipts => (
|
||||
table_key::<tables::Receipts>(&key)?,
|
||||
None,
|
||||
<ReceiptMask<ReceiptTy<N>>>::MASK,
|
||||
),
|
||||
StaticFileSegment::TransactionSenders => (
|
||||
table_key::<tables::TransactionSenders>(&key)?,
|
||||
<TransactionSenderMask>::MASK,
|
||||
None,
|
||||
TransactionSenderMask::MASK,
|
||||
),
|
||||
StaticFileSegment::AccountChangeSets => {
|
||||
let subkey =
|
||||
table_subkey::<tables::AccountChangeSets>(subkey.as_deref()).ok();
|
||||
(
|
||||
table_key::<tables::AccountChangeSets>(&key)?,
|
||||
subkey,
|
||||
AccountChangesetMask::MASK,
|
||||
)
|
||||
}
|
||||
StaticFileSegment::StorageChangeSets => {
|
||||
unreachable!("storage changesets handled above");
|
||||
}
|
||||
};
|
||||
|
||||
let content = tool
|
||||
.provider_factory
|
||||
.static_file_provider()
|
||||
.get_segment_provider(segment, key)?
|
||||
.cursor()?
|
||||
.get(key.into(), mask)
|
||||
.map(|result| {
|
||||
result.map(|vec| vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>())
|
||||
})?;
|
||||
// handle account changesets differently if a subkey is provided.
|
||||
if let StaticFileSegment::AccountChangeSets = segment {
|
||||
let Some(subkey) = subkey else {
|
||||
// get all changesets for the block
|
||||
let changesets = tool
|
||||
.provider_factory
|
||||
.static_file_provider()
|
||||
.account_block_changeset(key)?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&changesets)?);
|
||||
return Ok(())
|
||||
};
|
||||
|
||||
let account = tool
|
||||
.provider_factory
|
||||
.static_file_provider()
|
||||
.get_account_before_block(key, subkey)?;
|
||||
|
||||
if let Some(account) = account {
|
||||
println!("{}", serde_json::to_string_pretty(&account)?);
|
||||
} else {
|
||||
error!(target: "reth::cli", "No content for the given table key.");
|
||||
}
|
||||
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let content = tool.provider_factory.static_file_provider().find_static_file(
|
||||
segment,
|
||||
|provider| {
|
||||
let mut cursor = provider.cursor()?;
|
||||
cursor.get(key.into(), mask).map(|result| {
|
||||
result.map(|vec| {
|
||||
vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
match content {
|
||||
Some(content) => {
|
||||
@@ -139,6 +218,12 @@ impl Command {
|
||||
)?;
|
||||
println!("{}", serde_json::to_string_pretty(&sender)?);
|
||||
}
|
||||
StaticFileSegment::AccountChangeSets => {
|
||||
unreachable!("account changeset static files are special cased before this match")
|
||||
}
|
||||
StaticFileSegment::StorageChangeSets => {
|
||||
unreachable!("storage changeset static files are special cased before this match")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +102,7 @@ impl<N: NodeTypes> TableViewer<()> for ListTableViewer<'_, N> {
|
||||
tx.disable_long_read_transaction_safety();
|
||||
|
||||
let table_db = tx.inner.open_db(Some(self.args.table.name())).wrap_err("Could not open db.")?;
|
||||
let stats = tx.inner.db_stat(&table_db).wrap_err(format!("Could not find table: {}", self.args.table.name()))?;
|
||||
let stats = tx.inner.db_stat(table_db.dbi()).wrap_err(format!("Could not find table: {}", self.args.table.name()))?;
|
||||
let total_entries = stats.entries();
|
||||
let final_entry_idx = total_entries.saturating_sub(1);
|
||||
if self.args.skip > final_entry_idx {
|
||||
@@ -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)?;
|
||||
|
||||
@@ -39,7 +39,7 @@ pub enum Subcommands {
|
||||
Stats(stats::Command),
|
||||
/// Lists the contents of a table
|
||||
List(list::Command),
|
||||
/// Calculates the content checksum of a table
|
||||
/// Calculates the content checksum of a table or static file segment
|
||||
Checksum(checksum::Command),
|
||||
/// Create a diff between two database tables or two entire databases.
|
||||
Diff(diff::Command),
|
||||
@@ -162,7 +162,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
|
||||
let access_rights =
|
||||
if command.dry_run { AccessRights::RO } else { AccessRights::RW };
|
||||
db_exec!(self.env, tool, N, access_rights, {
|
||||
command.execute(&tool, ctx.task_executor.clone())?;
|
||||
command.execute(&tool, ctx.task_executor, &data_dir)?;
|
||||
});
|
||||
}
|
||||
Subcommands::StaticFileHeader(command) => {
|
||||
|
||||
@@ -9,7 +9,10 @@ use reth_db_api::{
|
||||
transaction::{DbTx, DbTxMut},
|
||||
};
|
||||
use reth_db_common::DbTool;
|
||||
use reth_node_core::version::version_metadata;
|
||||
use reth_node_core::{
|
||||
dirs::{ChainPath, DataDirPath},
|
||||
version::version_metadata,
|
||||
};
|
||||
use reth_node_metrics::{
|
||||
chain::ChainSpecInfo,
|
||||
hooks::Hooks,
|
||||
@@ -53,11 +56,13 @@ impl Command {
|
||||
self,
|
||||
tool: &DbTool<N>,
|
||||
task_executor: TaskExecutor,
|
||||
data_dir: &ChainPath<DataDirPath>,
|
||||
) -> eyre::Result<()> {
|
||||
// Set up metrics server if requested
|
||||
let _metrics_handle = if let Some(listen_addr) = self.metrics {
|
||||
let chain_name = tool.provider_factory.chain_spec().chain().to_string();
|
||||
let executor = task_executor.clone();
|
||||
let pprof_dump_dir = data_dir.pprof_dumps();
|
||||
|
||||
let handle = task_executor.spawn_critical("metrics server", async move {
|
||||
let config = MetricServerConfig::new(
|
||||
@@ -73,6 +78,7 @@ impl Command {
|
||||
ChainSpecInfo { name: chain_name },
|
||||
executor,
|
||||
Hooks::builder().build(),
|
||||
pprof_dump_dir,
|
||||
);
|
||||
|
||||
// Spawn the metrics server
|
||||
@@ -301,8 +307,8 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
if inconsistent_nodes == 0 {
|
||||
info!("No inconsistencies found");
|
||||
} else {
|
||||
info!("Repaired {} inconsistencies, committing changes", inconsistent_nodes);
|
||||
provider_rw.commit()?;
|
||||
info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -40,12 +40,37 @@ enum Subcommands {
|
||||
#[clap(rename_all = "snake_case")]
|
||||
pub enum SetCommand {
|
||||
/// Store receipts in static files instead of the database
|
||||
ReceiptsInStaticFiles {
|
||||
Receipts {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
/// Store transaction senders in static files instead of the database
|
||||
TransactionSendersInStaticFiles {
|
||||
TransactionSenders {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
/// Store account changesets in static files instead of the database
|
||||
AccountChangesets {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
/// Store storage history in rocksdb instead of MDBX
|
||||
StoragesHistory {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
/// Store transaction hash to number mapping in rocksdb instead of MDBX
|
||||
TransactionHashNumbers {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
/// Store account history in rocksdb instead of MDBX
|
||||
AccountHistory {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
/// Store storage changesets in static files instead of the database
|
||||
StorageChangesets {
|
||||
#[clap(action(ArgAction::Set))]
|
||||
value: bool,
|
||||
},
|
||||
@@ -94,11 +119,13 @@ impl Command {
|
||||
storages_history_in_rocksdb: _,
|
||||
transaction_hash_numbers_in_rocksdb: _,
|
||||
account_history_in_rocksdb: _,
|
||||
account_changesets_in_static_files: _,
|
||||
storage_changesets_in_static_files: _,
|
||||
} = settings.unwrap_or_else(StorageSettings::legacy);
|
||||
|
||||
// Update the setting based on the key
|
||||
match cmd {
|
||||
SetCommand::ReceiptsInStaticFiles { value } => {
|
||||
SetCommand::Receipts { value } => {
|
||||
if settings.receipts_in_static_files == value {
|
||||
println!("receipts_in_static_files is already set to {}", value);
|
||||
return Ok(());
|
||||
@@ -106,7 +133,7 @@ impl Command {
|
||||
settings.receipts_in_static_files = value;
|
||||
println!("Set receipts_in_static_files = {}", value);
|
||||
}
|
||||
SetCommand::TransactionSendersInStaticFiles { value } => {
|
||||
SetCommand::TransactionSenders { value } => {
|
||||
if settings.transaction_senders_in_static_files == value {
|
||||
println!("transaction_senders_in_static_files is already set to {}", value);
|
||||
return Ok(());
|
||||
@@ -114,6 +141,46 @@ impl Command {
|
||||
settings.transaction_senders_in_static_files = value;
|
||||
println!("Set transaction_senders_in_static_files = {}", value);
|
||||
}
|
||||
SetCommand::AccountChangesets { value } => {
|
||||
if settings.account_changesets_in_static_files == value {
|
||||
println!("account_changesets_in_static_files is already set to {}", value);
|
||||
return Ok(());
|
||||
}
|
||||
settings.account_changesets_in_static_files = value;
|
||||
println!("Set account_changesets_in_static_files = {}", value);
|
||||
}
|
||||
SetCommand::StoragesHistory { value } => {
|
||||
if settings.storages_history_in_rocksdb == value {
|
||||
println!("storages_history_in_rocksdb is already set to {}", value);
|
||||
return Ok(());
|
||||
}
|
||||
settings.storages_history_in_rocksdb = value;
|
||||
println!("Set storages_history_in_rocksdb = {}", value);
|
||||
}
|
||||
SetCommand::TransactionHashNumbers { value } => {
|
||||
if settings.transaction_hash_numbers_in_rocksdb == value {
|
||||
println!("transaction_hash_numbers_in_rocksdb is already set to {}", value);
|
||||
return Ok(());
|
||||
}
|
||||
settings.transaction_hash_numbers_in_rocksdb = value;
|
||||
println!("Set transaction_hash_numbers_in_rocksdb = {}", value);
|
||||
}
|
||||
SetCommand::AccountHistory { value } => {
|
||||
if settings.account_history_in_rocksdb == value {
|
||||
println!("account_history_in_rocksdb is already set to {}", value);
|
||||
return Ok(());
|
||||
}
|
||||
settings.account_history_in_rocksdb = value;
|
||||
println!("Set account_history_in_rocksdb = {}", value);
|
||||
}
|
||||
SetCommand::StorageChangesets { value } => {
|
||||
if settings.storage_changesets_in_static_files == value {
|
||||
println!("storage_changesets_in_static_files is already set to {}", value);
|
||||
return Ok(());
|
||||
}
|
||||
settings.storage_changesets_in_static_files = value;
|
||||
println!("Set storage_changesets_in_static_files = {}", value);
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
|
||||
@@ -11,7 +11,10 @@ use reth_db_common::DbTool;
|
||||
use reth_fs_util as fs;
|
||||
use reth_node_builder::{NodePrimitives, NodeTypesWithDB, NodeTypesWithDBAdapter};
|
||||
use reth_node_core::dirs::{ChainPath, DataDirPath};
|
||||
use reth_provider::providers::{ProviderNodeTypes, StaticFileProvider};
|
||||
use reth_provider::{
|
||||
providers::{ProviderNodeTypes, StaticFileProvider},
|
||||
RocksDBProviderFactory,
|
||||
};
|
||||
use reth_static_file_types::SegmentRangeInclusive;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
@@ -61,6 +64,11 @@ impl Command {
|
||||
let db_stats_table = self.db_stats_table(tool)?;
|
||||
println!("{db_stats_table}");
|
||||
|
||||
println!("\n");
|
||||
|
||||
let rocksdb_stats_table = self.rocksdb_stats_table(tool);
|
||||
println!("{rocksdb_stats_table}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -88,7 +96,7 @@ impl Command {
|
||||
|
||||
let stats = tx
|
||||
.inner
|
||||
.db_stat(&table_db)
|
||||
.db_stat(table_db.dbi())
|
||||
.wrap_err(format!("Could not find table: {db_table}"))?;
|
||||
|
||||
// Defaults to 16KB right now but we should
|
||||
@@ -129,7 +137,8 @@ impl Command {
|
||||
table.add_row(row);
|
||||
|
||||
let freelist = tx.inner.env().freelist()?;
|
||||
let pagesize = tx.inner.db_stat(&mdbx::Database::freelist_db())?.page_size() as usize;
|
||||
let pagesize =
|
||||
tx.inner.db_stat(mdbx::Database::freelist_db().dbi())?.page_size() as usize;
|
||||
let freelist_size = freelist * pagesize;
|
||||
|
||||
let mut row = Row::new();
|
||||
@@ -147,6 +156,70 @@ impl Command {
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
fn rocksdb_stats_table<N: NodeTypesWithDB>(&self, tool: &DbTool<N>) -> ComfyTable {
|
||||
let mut table = ComfyTable::new();
|
||||
table.load_preset(comfy_table::presets::ASCII_MARKDOWN);
|
||||
table.set_header([
|
||||
"RocksDB Table Name",
|
||||
"# Entries",
|
||||
"SST Size",
|
||||
"Memtable Size",
|
||||
"Total Size",
|
||||
"Pending Compaction",
|
||||
]);
|
||||
|
||||
let stats = tool.provider_factory.rocksdb_provider().table_stats();
|
||||
let mut total_sst: u64 = 0;
|
||||
let mut total_memtable: u64 = 0;
|
||||
let mut total_size: u64 = 0;
|
||||
let mut total_pending: u64 = 0;
|
||||
|
||||
for stat in &stats {
|
||||
total_sst += stat.sst_size_bytes;
|
||||
total_memtable += stat.memtable_size_bytes;
|
||||
total_size += stat.estimated_size_bytes;
|
||||
total_pending += stat.pending_compaction_bytes;
|
||||
let mut row = Row::new();
|
||||
row.add_cell(Cell::new(&stat.name))
|
||||
.add_cell(Cell::new(stat.estimated_num_keys))
|
||||
.add_cell(Cell::new(human_bytes(stat.sst_size_bytes as f64)))
|
||||
.add_cell(Cell::new(human_bytes(stat.memtable_size_bytes as f64)))
|
||||
.add_cell(Cell::new(human_bytes(stat.estimated_size_bytes as f64)))
|
||||
.add_cell(Cell::new(human_bytes(stat.pending_compaction_bytes as f64)));
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
if !stats.is_empty() {
|
||||
let max_widths = table.column_max_content_widths();
|
||||
let mut separator = Row::new();
|
||||
for width in max_widths {
|
||||
separator.add_cell(Cell::new("-".repeat(width as usize)));
|
||||
}
|
||||
table.add_row(separator);
|
||||
|
||||
let mut row = Row::new();
|
||||
row.add_cell(Cell::new("RocksDB Total"))
|
||||
.add_cell(Cell::new(""))
|
||||
.add_cell(Cell::new(human_bytes(total_sst as f64)))
|
||||
.add_cell(Cell::new(human_bytes(total_memtable as f64)))
|
||||
.add_cell(Cell::new(human_bytes(total_size as f64)))
|
||||
.add_cell(Cell::new(human_bytes(total_pending as f64)));
|
||||
table.add_row(row);
|
||||
|
||||
let wal_size = tool.provider_factory.rocksdb_provider().wal_size_bytes();
|
||||
let mut row = Row::new();
|
||||
row.add_cell(Cell::new("WAL"))
|
||||
.add_cell(Cell::new(""))
|
||||
.add_cell(Cell::new(""))
|
||||
.add_cell(Cell::new(""))
|
||||
.add_cell(Cell::new(human_bytes(wal_size as f64)))
|
||||
.add_cell(Cell::new(""));
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
fn static_files_stats_table<N: NodePrimitives>(
|
||||
&self,
|
||||
data_dir: ChainPath<DataDirPath>,
|
||||
|
||||
@@ -2,20 +2,22 @@ use crate::common::EnvironmentArgs;
|
||||
use clap::Parser;
|
||||
use eyre::Result;
|
||||
use lz4::Decoder;
|
||||
use reqwest::Client;
|
||||
use reqwest::{blocking::Client as BlockingClient, header::RANGE, Client, StatusCode};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_fs_util as fs;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::{self, Read, Write},
|
||||
path::Path,
|
||||
fs::OpenOptions,
|
||||
io::{self, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tar::Archive;
|
||||
use tokio::task;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
use zstd::stream::read::Decoder as ZstdDecoder;
|
||||
|
||||
const BYTE_UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
@@ -85,6 +87,9 @@ impl DownloadDefaults {
|
||||
"\nIf no URL is provided, the latest mainnet archive snapshot\nwill be proposed for download from ",
|
||||
);
|
||||
help.push_str(self.default_base_url.as_ref());
|
||||
help.push_str(
|
||||
".\n\nLocal file:// URLs are also supported for extracting snapshots from disk.",
|
||||
);
|
||||
help
|
||||
}
|
||||
|
||||
@@ -170,12 +175,14 @@ struct DownloadProgress {
|
||||
downloaded: u64,
|
||||
total_size: u64,
|
||||
last_displayed: Instant,
|
||||
started_at: Instant,
|
||||
}
|
||||
|
||||
impl DownloadProgress {
|
||||
/// Creates new progress tracker with given total size
|
||||
fn new(total_size: u64) -> Self {
|
||||
Self { downloaded: 0, total_size, last_displayed: Instant::now() }
|
||||
let now = Instant::now();
|
||||
Self { downloaded: 0, total_size, last_displayed: now, started_at: now }
|
||||
}
|
||||
|
||||
/// Converts bytes to human readable format (B, KB, MB, GB)
|
||||
@@ -191,6 +198,18 @@ impl DownloadProgress {
|
||||
format!("{:.2} {}", size, BYTE_UNITS[unit_index])
|
||||
}
|
||||
|
||||
/// Format duration as human readable string
|
||||
fn format_duration(duration: Duration) -> String {
|
||||
let secs = duration.as_secs();
|
||||
if secs < 60 {
|
||||
format!("{secs}s")
|
||||
} else if secs < 3600 {
|
||||
format!("{}m {}s", secs / 60, secs % 60)
|
||||
} else {
|
||||
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates progress bar
|
||||
fn update(&mut self, chunk_size: u64) -> Result<()> {
|
||||
self.downloaded += chunk_size;
|
||||
@@ -201,8 +220,24 @@ impl DownloadProgress {
|
||||
let formatted_total = Self::format_size(self.total_size);
|
||||
let progress = (self.downloaded as f64 / self.total_size as f64) * 100.0;
|
||||
|
||||
// Calculate ETA based on current speed
|
||||
let elapsed = self.started_at.elapsed();
|
||||
let eta = if self.downloaded > 0 {
|
||||
let remaining = self.total_size.saturating_sub(self.downloaded);
|
||||
let speed = self.downloaded as f64 / elapsed.as_secs_f64();
|
||||
if speed > 0.0 {
|
||||
Duration::from_secs_f64(remaining as f64 / speed)
|
||||
} else {
|
||||
Duration::ZERO
|
||||
}
|
||||
} else {
|
||||
Duration::ZERO
|
||||
};
|
||||
let eta_str = Self::format_duration(eta);
|
||||
|
||||
// Pad with spaces to clear any previous longer line
|
||||
print!(
|
||||
"\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total})",
|
||||
"\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total}) ETA: {eta_str} ",
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
self.last_displayed = Instant::now();
|
||||
@@ -246,29 +281,30 @@ enum CompressionFormat {
|
||||
impl CompressionFormat {
|
||||
/// Detect compression format from file extension
|
||||
fn from_url(url: &str) -> Result<Self> {
|
||||
if url.ends_with(EXTENSION_TAR_LZ4) {
|
||||
let path =
|
||||
Url::parse(url).map(|u| u.path().to_string()).unwrap_or_else(|_| url.to_string());
|
||||
|
||||
if path.ends_with(EXTENSION_TAR_LZ4) {
|
||||
Ok(Self::Lz4)
|
||||
} else if url.ends_with(EXTENSION_TAR_ZSTD) {
|
||||
} else if path.ends_with(EXTENSION_TAR_ZSTD) {
|
||||
Ok(Self::Zstd)
|
||||
} else {
|
||||
Err(eyre::eyre!("Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}", url))
|
||||
Err(eyre::eyre!(
|
||||
"Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}",
|
||||
path
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads and extracts a snapshot, blocking until finished.
|
||||
fn blocking_download_and_extract(url: &str, target_dir: &Path) -> Result<()> {
|
||||
let client = reqwest::blocking::Client::builder().build()?;
|
||||
let response = client.get(url).send()?.error_for_status()?;
|
||||
|
||||
let total_size = response.content_length().ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"Server did not provide Content-Length header. This is required for snapshot downloads"
|
||||
)
|
||||
})?;
|
||||
|
||||
let progress_reader = ProgressReader::new(response, total_size);
|
||||
let format = CompressionFormat::from_url(url)?;
|
||||
/// Extracts a compressed tar archive to the target directory with progress tracking.
|
||||
fn extract_archive<R: Read>(
|
||||
reader: R,
|
||||
total_size: u64,
|
||||
format: CompressionFormat,
|
||||
target_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let progress_reader = ProgressReader::new(reader, total_size);
|
||||
|
||||
match format {
|
||||
CompressionFormat::Lz4 => {
|
||||
@@ -285,6 +321,185 @@ fn blocking_download_and_extract(url: &str, target_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts a snapshot from a local file.
|
||||
fn extract_from_file(path: &Path, format: CompressionFormat, target_dir: &Path) -> Result<()> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let total_size = file.metadata()?.len();
|
||||
extract_archive(file, total_size, format, target_dir)
|
||||
}
|
||||
|
||||
const MAX_DOWNLOAD_RETRIES: u32 = 10;
|
||||
const RETRY_BACKOFF_SECS: u64 = 5;
|
||||
|
||||
/// Wrapper that tracks download progress while writing data.
|
||||
/// Used with [`io::copy`] to display progress during downloads.
|
||||
struct ProgressWriter<W> {
|
||||
inner: W,
|
||||
progress: DownloadProgress,
|
||||
}
|
||||
|
||||
impl<W: Write> Write for ProgressWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.inner.write(buf)?;
|
||||
let _ = self.progress.update(n as u64);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a file with resume support using HTTP Range requests.
|
||||
/// Automatically retries on failure, resuming from where it left off.
|
||||
/// Returns the path to the downloaded file and its total size.
|
||||
fn resumable_download(url: &str, target_dir: &Path) -> Result<(PathBuf, u64)> {
|
||||
let file_name = Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|u| u.path_segments()?.next_back().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "snapshot.tar".to_string());
|
||||
|
||||
let final_path = target_dir.join(&file_name);
|
||||
let part_path = target_dir.join(format!("{file_name}.part"));
|
||||
|
||||
let client = BlockingClient::builder().timeout(Duration::from_secs(30)).build()?;
|
||||
|
||||
let mut total_size: Option<u64> = None;
|
||||
let mut last_error: Option<eyre::Error> = None;
|
||||
|
||||
for attempt in 1..=MAX_DOWNLOAD_RETRIES {
|
||||
let existing_size = fs::metadata(&part_path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
if let Some(total) = total_size &&
|
||||
existing_size >= total
|
||||
{
|
||||
fs::rename(&part_path, &final_path)?;
|
||||
info!(target: "reth::cli", "Download complete: {}", final_path.display());
|
||||
return Ok((final_path, total));
|
||||
}
|
||||
|
||||
if attempt > 1 {
|
||||
info!(target: "reth::cli",
|
||||
"Retry attempt {}/{} - resuming from {} bytes",
|
||||
attempt, MAX_DOWNLOAD_RETRIES, existing_size
|
||||
);
|
||||
}
|
||||
|
||||
let mut request = client.get(url);
|
||||
if existing_size > 0 {
|
||||
request = request.header(RANGE, format!("bytes={existing_size}-"));
|
||||
if attempt == 1 {
|
||||
info!(target: "reth::cli", "Resuming download from {} bytes", existing_size);
|
||||
}
|
||||
}
|
||||
|
||||
let response = match request.send().and_then(|r| r.error_for_status()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
last_error = Some(e.into());
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
info!(target: "reth::cli",
|
||||
"Download failed, retrying in {} seconds...", RETRY_BACKOFF_SECS
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let is_partial = response.status() == StatusCode::PARTIAL_CONTENT;
|
||||
|
||||
let size = if is_partial {
|
||||
response
|
||||
.headers()
|
||||
.get("Content-Range")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.split('/').next_back())
|
||||
.and_then(|v| v.parse().ok())
|
||||
} else {
|
||||
response.content_length()
|
||||
};
|
||||
|
||||
if total_size.is_none() {
|
||||
total_size = size;
|
||||
}
|
||||
|
||||
let current_total = total_size.ok_or_else(|| {
|
||||
eyre::eyre!("Server did not provide Content-Length or Content-Range header")
|
||||
})?;
|
||||
|
||||
let file = if is_partial && existing_size > 0 {
|
||||
OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&part_path)
|
||||
.map_err(|e| fs::FsPathError::open(e, &part_path))?
|
||||
} else {
|
||||
fs::create_file(&part_path)?
|
||||
};
|
||||
|
||||
let start_offset = if is_partial { existing_size } else { 0 };
|
||||
let mut progress = DownloadProgress::new(current_total);
|
||||
progress.downloaded = start_offset;
|
||||
|
||||
let mut writer = ProgressWriter { inner: BufWriter::new(file), progress };
|
||||
let mut reader = response;
|
||||
|
||||
let copy_result = io::copy(&mut reader, &mut writer);
|
||||
let flush_result = writer.inner.flush();
|
||||
println!();
|
||||
|
||||
if let Err(e) = copy_result.and(flush_result) {
|
||||
last_error = Some(e.into());
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
info!(target: "reth::cli",
|
||||
"Download interrupted, retrying in {} seconds...", RETRY_BACKOFF_SECS
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::rename(&part_path, &final_path)?;
|
||||
info!(target: "reth::cli", "Download complete: {}", final_path.display());
|
||||
return Ok((final_path, current_total));
|
||||
}
|
||||
|
||||
Err(last_error
|
||||
.unwrap_or_else(|| eyre::eyre!("Download failed after {} attempts", MAX_DOWNLOAD_RETRIES)))
|
||||
}
|
||||
|
||||
/// Fetches the snapshot from a remote URL with resume support, then extracts it.
|
||||
fn download_and_extract(url: &str, format: CompressionFormat, target_dir: &Path) -> Result<()> {
|
||||
let (downloaded_path, total_size) = resumable_download(url, target_dir)?;
|
||||
|
||||
info!(target: "reth::cli", "Extracting snapshot...");
|
||||
let file = fs::open(&downloaded_path)?;
|
||||
extract_archive(file, total_size, format, target_dir)?;
|
||||
|
||||
fs::remove_file(&downloaded_path)?;
|
||||
info!(target: "reth::cli", "Removed downloaded archive");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads and extracts a snapshot, blocking until finished.
|
||||
///
|
||||
/// Supports both `file://` URLs for local files and HTTP(S) URLs for remote downloads.
|
||||
fn blocking_download_and_extract(url: &str, target_dir: &Path) -> Result<()> {
|
||||
let format = CompressionFormat::from_url(url)?;
|
||||
|
||||
if let Ok(parsed_url) = Url::parse(url) &&
|
||||
parsed_url.scheme() == "file"
|
||||
{
|
||||
let file_path = parsed_url
|
||||
.to_file_path()
|
||||
.map_err(|_| eyre::eyre!("Invalid file:// URL path: {}", url))?;
|
||||
extract_from_file(&file_path, format, target_dir)
|
||||
} else {
|
||||
download_and_extract(url, format, target_dir)
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_and_extract(url: &str, target_dir: &Path) -> Result<()> {
|
||||
let target_dir = target_dir.to_path_buf();
|
||||
let url = url.to_string();
|
||||
@@ -343,6 +558,7 @@ mod tests {
|
||||
assert!(help.contains("Available snapshot sources:"));
|
||||
assert!(help.contains("merkle.io"));
|
||||
assert!(help.contains("publicnode.com"));
|
||||
assert!(help.contains("file://"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -367,4 +583,25 @@ mod tests {
|
||||
assert_eq!(defaults.available_snapshots.len(), 4); // 2 defaults + 2 added
|
||||
assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compression_format_detection() {
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
|
||||
Ok(CompressionFormat::Lz4)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
|
||||
Ok(CompressionFormat::Zstd)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
|
||||
Ok(CompressionFormat::Lz4)
|
||||
));
|
||||
assert!(matches!(
|
||||
CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
|
||||
Ok(CompressionFormat::Zstd)
|
||||
));
|
||||
assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ pub struct ImportCommand<C: ChainSpecParser> {
|
||||
#[arg(long, value_name = "CHUNK_LEN", verbatim_doc_comment)]
|
||||
chunk_len: Option<u64>,
|
||||
|
||||
/// Fail immediately when an invalid block is encountered.
|
||||
///
|
||||
/// By default, the import will stop at the last valid block if an invalid block is
|
||||
/// encountered during execution or validation, leaving the database at the last valid
|
||||
/// block state. When this flag is set, the import will instead fail with an error.
|
||||
#[arg(long, verbatim_doc_comment)]
|
||||
fail_on_invalid_block: bool,
|
||||
|
||||
/// The path(s) to block file(s) for import.
|
||||
///
|
||||
/// The online stages (headers and bodies) are replaced by a file import, after which the
|
||||
@@ -52,7 +60,11 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
|
||||
|
||||
info!(target: "reth::cli", "Starting import of {} file(s)", self.paths.len());
|
||||
|
||||
let import_config = ImportConfig { no_state: self.no_state, chunk_len: self.chunk_len };
|
||||
let import_config = ImportConfig {
|
||||
no_state: self.no_state,
|
||||
chunk_len: self.chunk_len,
|
||||
fail_on_invalid_block: self.fail_on_invalid_block,
|
||||
};
|
||||
|
||||
let executor = components.evm_config().clone();
|
||||
let consensus = Arc::new(components.consensus().clone());
|
||||
@@ -81,7 +93,20 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
|
||||
total_decoded_blocks += result.total_decoded_blocks;
|
||||
total_decoded_txns += result.total_decoded_txns;
|
||||
|
||||
if !result.is_complete() {
|
||||
// Check if we stopped due to an invalid block
|
||||
if result.stopped_on_invalid_block {
|
||||
info!(target: "reth::cli",
|
||||
"Stopped at last valid block {} due to invalid block {} in file: {}. Imported {} blocks, {} transactions",
|
||||
result.last_valid_block.unwrap_or(0),
|
||||
result.bad_block.unwrap_or(0),
|
||||
path.display(),
|
||||
result.total_imported_blocks,
|
||||
result.total_imported_txns);
|
||||
// Stop importing further files and exit successfully
|
||||
break;
|
||||
}
|
||||
|
||||
if !result.is_successful() {
|
||||
return Err(eyre::eyre!(
|
||||
"Chain was partially imported from file: {}. Imported {}/{} blocks, {}/{} transactions",
|
||||
path.display(),
|
||||
@@ -98,7 +123,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
|
||||
}
|
||||
|
||||
info!(target: "reth::cli",
|
||||
"All files imported successfully. Total: {}/{} blocks, {}/{} transactions",
|
||||
"Import complete. Total: {}/{} blocks, {}/{} transactions",
|
||||
total_imported_blocks, total_decoded_blocks, total_imported_txns, total_decoded_txns);
|
||||
|
||||
Ok(())
|
||||
@@ -139,4 +164,20 @@ mod tests {
|
||||
assert_eq!(args.paths[1], PathBuf::from("file2.rlp"));
|
||||
assert_eq!(args.paths[2], PathBuf::from("file3.rlp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_import_command_with_fail_on_invalid_block() {
|
||||
let args: ImportCommand<EthereumChainSpecParser> =
|
||||
ImportCommand::parse_from(["reth", "--fail-on-invalid-block", "chain.rlp"]);
|
||||
assert!(args.fail_on_invalid_block);
|
||||
assert_eq!(args.paths.len(), 1);
|
||||
assert_eq!(args.paths[0], PathBuf::from("chain.rlp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_import_command_default_stops_on_invalid_block() {
|
||||
let args: ImportCommand<EthereumChainSpecParser> =
|
||||
ImportCommand::parse_from(["reth", "chain.rlp"]);
|
||||
assert!(!args.fail_on_invalid_block);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ use reth_provider::{
|
||||
StageCheckpointReader,
|
||||
};
|
||||
use reth_prune::PruneModes;
|
||||
use reth_stages::{prelude::*, Pipeline, StageId, StageSet};
|
||||
use reth_stages::{prelude::*, ControlFlow, Pipeline, StageId, StageSet};
|
||||
use reth_static_file::StaticFileProducer;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use tokio::sync::watch;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Configuration for importing blocks from RLP files.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -35,6 +35,9 @@ pub struct ImportConfig {
|
||||
pub no_state: bool,
|
||||
/// Chunk byte length to read from file.
|
||||
pub chunk_len: Option<u64>,
|
||||
/// If true, fail immediately when an invalid block is encountered.
|
||||
/// By default (false), the import stops at the last valid block and exits successfully.
|
||||
pub fail_on_invalid_block: bool,
|
||||
}
|
||||
|
||||
/// Result of an import operation.
|
||||
@@ -48,6 +51,12 @@ pub struct ImportResult {
|
||||
pub total_imported_blocks: usize,
|
||||
/// Total number of transactions imported into the database.
|
||||
pub total_imported_txns: usize,
|
||||
/// Whether the import was stopped due to an invalid block.
|
||||
pub stopped_on_invalid_block: bool,
|
||||
/// The block number that was invalid, if any.
|
||||
pub bad_block: Option<u64>,
|
||||
/// The last valid block number when stopped due to invalid block.
|
||||
pub last_valid_block: Option<u64>,
|
||||
}
|
||||
|
||||
impl ImportResult {
|
||||
@@ -56,6 +65,14 @@ impl ImportResult {
|
||||
self.total_decoded_blocks == self.total_imported_blocks &&
|
||||
self.total_decoded_txns == self.total_imported_txns
|
||||
}
|
||||
|
||||
/// Returns true if the import was successful, considering stop-on-invalid-block mode.
|
||||
///
|
||||
/// In stop-on-invalid-block mode, a partial import is considered successful if we
|
||||
/// stopped due to an invalid block (leaving the DB at the last valid block).
|
||||
pub fn is_successful(&self) -> bool {
|
||||
self.is_complete() || self.stopped_on_invalid_block
|
||||
}
|
||||
}
|
||||
|
||||
/// Imports blocks from an RLP-encoded file into the database.
|
||||
@@ -69,9 +86,7 @@ pub async fn import_blocks_from_file<N>(
|
||||
provider_factory: ProviderFactory<N>,
|
||||
config: &Config,
|
||||
executor: impl ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
consensus: Arc<
|
||||
impl FullConsensus<N::Primitives, Error = reth_consensus::ConsensusError> + 'static,
|
||||
>,
|
||||
consensus: Arc<impl FullConsensus<N::Primitives> + 'static>,
|
||||
) -> eyre::Result<ImportResult>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
@@ -105,6 +120,11 @@ where
|
||||
let static_file_producer =
|
||||
StaticFileProducer::new(provider_factory.clone(), PruneModes::default());
|
||||
|
||||
// Track if we stopped due to an invalid block
|
||||
let mut stopped_on_invalid_block = false;
|
||||
let mut bad_block_number: Option<u64> = None;
|
||||
let mut last_valid_block_number: Option<u64> = None;
|
||||
|
||||
while let Some(file_client) =
|
||||
reader.next_chunk::<BlockTy<N>>(consensus.clone(), Some(sealed_header)).await?
|
||||
{
|
||||
@@ -139,12 +159,51 @@ where
|
||||
|
||||
// Run pipeline
|
||||
info!(target: "reth::import", "Starting sync pipeline");
|
||||
tokio::select! {
|
||||
res = pipeline.run() => res?,
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!(target: "reth::import", "Import interrupted by user");
|
||||
break;
|
||||
},
|
||||
if import_config.fail_on_invalid_block {
|
||||
// Original behavior: fail on unwind
|
||||
tokio::select! {
|
||||
res = pipeline.run() => res?,
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!(target: "reth::import", "Import interrupted by user");
|
||||
break;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Default behavior: Use run_loop() to handle unwinds gracefully
|
||||
let result = tokio::select! {
|
||||
res = pipeline.run_loop() => res,
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!(target: "reth::import", "Import interrupted by user");
|
||||
break;
|
||||
},
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(ControlFlow::Unwind { target, bad_block }) => {
|
||||
// An invalid block was encountered; stop at last valid block
|
||||
let bad = bad_block.block.number;
|
||||
warn!(
|
||||
target: "reth::import",
|
||||
bad_block = bad,
|
||||
last_valid_block = target,
|
||||
"Invalid block encountered during import; stopping at last valid block"
|
||||
);
|
||||
stopped_on_invalid_block = true;
|
||||
bad_block_number = Some(bad);
|
||||
last_valid_block_number = Some(target);
|
||||
break;
|
||||
}
|
||||
Ok(ControlFlow::Continue { block_number }) => {
|
||||
debug!(target: "reth::import", block_number, "Pipeline chunk completed");
|
||||
}
|
||||
Ok(ControlFlow::NoProgress { block_number }) => {
|
||||
debug!(target: "reth::import", ?block_number, "Pipeline made no progress");
|
||||
}
|
||||
Err(e) => {
|
||||
// Propagate other pipeline errors
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed_header = provider_factory
|
||||
@@ -162,9 +221,20 @@ where
|
||||
total_decoded_txns,
|
||||
total_imported_blocks,
|
||||
total_imported_txns,
|
||||
stopped_on_invalid_block,
|
||||
bad_block: bad_block_number,
|
||||
last_valid_block: last_valid_block_number,
|
||||
};
|
||||
|
||||
if !result.is_complete() {
|
||||
if result.stopped_on_invalid_block {
|
||||
info!(target: "reth::import",
|
||||
total_imported_blocks,
|
||||
total_imported_txns,
|
||||
bad_block = ?result.bad_block,
|
||||
last_valid_block = ?result.last_valid_block,
|
||||
"Import stopped at last valid block due to invalid block"
|
||||
);
|
||||
} else if !result.is_complete() {
|
||||
error!(target: "reth::import",
|
||||
total_decoded_blocks,
|
||||
total_imported_blocks,
|
||||
@@ -198,7 +268,7 @@ pub fn build_import_pipeline_impl<N, C, E>(
|
||||
) -> eyre::Result<(Pipeline<N>, impl futures::Stream<Item = NodeEvent<N::Primitives>> + use<N, C, E>)>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
C: FullConsensus<N::Primitives, Error = reth_consensus::ConsensusError> + 'static,
|
||||
C: FullConsensus<N::Primitives> + 'static,
|
||||
E: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
{
|
||||
if !file_client.has_canonical_blocks() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Command that initializes the node from a genesis file.
|
||||
|
||||
use crate::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs};
|
||||
use alloy_consensus::BlockHeader;
|
||||
use clap::Parser;
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
|
||||
use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_provider::BlockHashReader;
|
||||
use std::sync::Arc;
|
||||
@@ -22,8 +23,9 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitComman
|
||||
|
||||
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
|
||||
|
||||
let genesis_block_number = provider_factory.chain_spec().genesis_header().number();
|
||||
let hash = provider_factory
|
||||
.block_hash(0)?
|
||||
.block_hash(genesis_block_number)?
|
||||
.ok_or_else(|| eyre::eyre!("Genesis hash not found."))?;
|
||||
|
||||
info!(target: "reth::cli", hash = ?hash, "Genesis block written");
|
||||
|
||||
@@ -79,7 +79,7 @@ where
|
||||
+ StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>,
|
||||
{
|
||||
provider_rw.insert_block(
|
||||
SealedBlock::<<Provider::Primitives as NodePrimitives>::Block>::from_sealed_parts(
|
||||
&SealedBlock::<<Provider::Primitives as NodePrimitives>::Block>::from_sealed_parts(
|
||||
header.clone(),
|
||||
Default::default(),
|
||||
)
|
||||
@@ -99,6 +99,7 @@ where
|
||||
/// * Headers: It will push an empty block.
|
||||
/// * Transactions: It will not push any tx, only increments the end block range.
|
||||
/// * Receipts: It will not push any receipt, only increments the end block range.
|
||||
/// * TransactionSenders: If the segment exists, increments the end block range.
|
||||
fn append_dummy_chain<N, F>(
|
||||
sf_provider: &StaticFileProvider<N>,
|
||||
target_height: BlockNumber,
|
||||
@@ -110,8 +111,15 @@ where
|
||||
{
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
// Spawn jobs for incrementing the block end range of transactions and receipts
|
||||
for segment in [StaticFileSegment::Transactions, StaticFileSegment::Receipts] {
|
||||
// Spawn jobs for incrementing the block end range of transactions, receipts, and senders.
|
||||
for segment in [
|
||||
StaticFileSegment::Transactions,
|
||||
StaticFileSegment::Receipts,
|
||||
StaticFileSegment::TransactionSenders,
|
||||
] {
|
||||
if sf_provider.get_highest_static_file_block(segment).is_none() {
|
||||
continue
|
||||
}
|
||||
let tx_clone = tx.clone();
|
||||
let provider = sf_provider.clone();
|
||||
std::thread::spawn(move || {
|
||||
@@ -151,9 +159,15 @@ where
|
||||
|
||||
// If, for any reason, rayon crashes this verifies if all segments are at the same
|
||||
// target_height.
|
||||
for segment in
|
||||
[StaticFileSegment::Headers, StaticFileSegment::Receipts, StaticFileSegment::Transactions]
|
||||
{
|
||||
for segment in [
|
||||
StaticFileSegment::Headers,
|
||||
StaticFileSegment::Receipts,
|
||||
StaticFileSegment::Transactions,
|
||||
StaticFileSegment::TransactionSenders,
|
||||
] {
|
||||
if sf_provider.get_highest_static_file_block(segment).is_none() {
|
||||
continue
|
||||
}
|
||||
assert_eq!(
|
||||
sf_provider.latest_writer(segment)?.user_header().block_end(),
|
||||
Some(target_height),
|
||||
|
||||
@@ -10,7 +10,8 @@ use reth_node_builder::NodeBuilder;
|
||||
use reth_node_core::{
|
||||
args::{
|
||||
DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, EraArgs, MetricArgs,
|
||||
NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs,
|
||||
NetworkArgs, PayloadBuilderArgs, PruningArgs, RocksDbArgs, RpcServerArgs, StaticFilesArgs,
|
||||
TxPoolArgs,
|
||||
},
|
||||
node_config::NodeConfig,
|
||||
version,
|
||||
@@ -102,6 +103,10 @@ pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs
|
||||
#[command(flatten)]
|
||||
pub pruning: PruningArgs,
|
||||
|
||||
/// All `RocksDB` table routing arguments
|
||||
#[command(flatten)]
|
||||
pub rocksdb: RocksDbArgs,
|
||||
|
||||
/// Engine cli arguments
|
||||
#[command(flatten, next_help_heading = "Engine")]
|
||||
pub engine: EngineArgs,
|
||||
@@ -166,12 +171,16 @@ where
|
||||
db,
|
||||
dev,
|
||||
pruning,
|
||||
rocksdb,
|
||||
engine,
|
||||
era,
|
||||
static_files,
|
||||
ext,
|
||||
} = self;
|
||||
|
||||
// Validate RocksDB arguments
|
||||
rocksdb.validate()?;
|
||||
|
||||
// set up node config
|
||||
let mut node_config = NodeConfig {
|
||||
datadir,
|
||||
@@ -187,6 +196,7 @@ where
|
||||
db,
|
||||
dev,
|
||||
pruning,
|
||||
rocksdb,
|
||||
engine,
|
||||
era,
|
||||
static_files,
|
||||
|
||||
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
|
||||
@@ -72,7 +73,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
.split();
|
||||
if result.len() != 1 {
|
||||
eyre::bail!(
|
||||
"Invalid number of headers received. Expected: 1. Received: {}",
|
||||
"Invalid number of bodies received. Expected: 1. Received: {}",
|
||||
result.len()
|
||||
)
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ pub struct Command<C: ChainSpecParser> {
|
||||
#[arg(long)]
|
||||
to: Option<u64>,
|
||||
|
||||
/// Number of tasks to run in parallel
|
||||
#[arg(long, default_value = "10")]
|
||||
num_tasks: u64,
|
||||
/// Number of tasks to run in parallel. Defaults to the number of available CPUs.
|
||||
#[arg(long)]
|
||||
num_tasks: Option<u64>,
|
||||
|
||||
/// Continues with execution when an invalid block is encountered and collects these blocks.
|
||||
#[arg(long)]
|
||||
@@ -84,12 +84,16 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
}
|
||||
};
|
||||
|
||||
let num_tasks = self.num_tasks.unwrap_or_else(|| {
|
||||
std::thread::available_parallelism().map(|n| n.get() as u64).unwrap_or(10)
|
||||
});
|
||||
|
||||
let total_blocks = max_block - min_block;
|
||||
let total_gas = calculate_gas_used_from_headers(
|
||||
&provider_factory.static_file_provider(),
|
||||
min_block..=max_block,
|
||||
)?;
|
||||
let blocks_per_task = total_blocks / self.num_tasks;
|
||||
let blocks_per_task = total_blocks / num_tasks;
|
||||
|
||||
let db_at = {
|
||||
let provider_factory = provider_factory.clone();
|
||||
@@ -107,10 +111,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
let _guard = cancellation.drop_guard();
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
for i in 0..self.num_tasks {
|
||||
for i in 0..num_tasks {
|
||||
let start_block = min_block + i * blocks_per_task;
|
||||
let end_block =
|
||||
if i == self.num_tasks - 1 { max_block } else { start_block + blocks_per_task };
|
||||
if i == num_tasks - 1 { max_block } else { start_block + blocks_per_task };
|
||||
|
||||
// Spawn thread executing blocks
|
||||
let provider_factory = provider_factory.clone();
|
||||
@@ -148,7 +152,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
};
|
||||
|
||||
if let Err(err) = consensus
|
||||
.validate_block_post_execution(&block, &result)
|
||||
.validate_block_post_execution(&block, &result, None)
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to validate block {} {}", block.number(), block.hash())
|
||||
})
|
||||
|
||||
@@ -15,7 +15,8 @@ use reth_db_common::{
|
||||
use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
|
||||
use reth_node_core::args::StageEnum;
|
||||
use reth_provider::{
|
||||
DBProvider, DatabaseProviderFactory, StaticFileProviderFactory, StaticFileWriter, TrieWriter,
|
||||
DBProvider, RocksDBProviderFactory, StaticFileProviderFactory, StaticFileWriter,
|
||||
StorageSettingsCache,
|
||||
};
|
||||
use reth_prune::PruneSegment;
|
||||
use reth_stages::StageId;
|
||||
@@ -87,11 +88,17 @@ impl<C: ChainSpecParser> Command<C> {
|
||||
.unwrap_or_default();
|
||||
writer.prune_transaction_senders(to_delete, 0)?;
|
||||
}
|
||||
StaticFileSegment::AccountChangeSets => {
|
||||
writer.prune_account_changesets(highest_block)?;
|
||||
}
|
||||
StaticFileSegment::StorageChangeSets => {
|
||||
writer.prune_storage_changesets(highest_block)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let provider_rw = tool.provider_factory.database_provider_rw()?;
|
||||
let provider_rw = tool.provider_factory.unwind_provider_rw()?;
|
||||
let tx = provider_rw.tx_ref();
|
||||
|
||||
match self.stage {
|
||||
@@ -164,13 +171,21 @@ impl<C: ChainSpecParser> Command<C> {
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
StageEnum::MerkleChangeSets => {
|
||||
provider_rw.clear_trie_changesets()?;
|
||||
reset_stage_checkpoint(tx, StageId::MerkleChangeSets)?;
|
||||
}
|
||||
StageEnum::AccountHistory | StageEnum::StorageHistory => {
|
||||
tx.clear::<tables::AccountsHistory>()?;
|
||||
tx.clear::<tables::StoragesHistory>()?;
|
||||
let settings = provider_rw.cached_storage_settings();
|
||||
let rocksdb = tool.provider_factory.rocksdb_provider();
|
||||
|
||||
if settings.account_history_in_rocksdb {
|
||||
rocksdb.clear::<tables::AccountsHistory>()?;
|
||||
} else {
|
||||
tx.clear::<tables::AccountsHistory>()?;
|
||||
}
|
||||
|
||||
if settings.storages_history_in_rocksdb {
|
||||
rocksdb.clear::<tables::StoragesHistory>()?;
|
||||
} else {
|
||||
tx.clear::<tables::StoragesHistory>()?;
|
||||
}
|
||||
|
||||
reset_stage_checkpoint(tx, StageId::IndexAccountHistory)?;
|
||||
reset_stage_checkpoint(tx, StageId::IndexStorageHistory)?;
|
||||
@@ -178,7 +193,14 @@ impl<C: ChainSpecParser> Command<C> {
|
||||
insert_genesis_history(&provider_rw, self.env.chain.genesis().alloc.iter())?;
|
||||
}
|
||||
StageEnum::TxLookup => {
|
||||
tx.clear::<tables::TransactionHashNumbers>()?;
|
||||
if provider_rw.cached_storage_settings().transaction_hash_numbers_in_rocksdb {
|
||||
tool.provider_factory
|
||||
.rocksdb_provider()
|
||||
.clear::<tables::TransactionHashNumbers>()?;
|
||||
} else {
|
||||
tx.clear::<tables::TransactionHashNumbers>()?;
|
||||
}
|
||||
|
||||
reset_prune_checkpoint(tx, PruneSegment::TransactionLookup)?;
|
||||
|
||||
reset_stage_checkpoint(tx, StageId::TransactionLookup)?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::setup;
|
||||
use reth_consensus::{noop::NoopConsensus, ConsensusError, FullConsensus};
|
||||
use reth_consensus::{noop::NoopConsensus, FullConsensus};
|
||||
use reth_db::DatabaseEnv;
|
||||
use reth_db_api::{
|
||||
cursor::DbCursorRO, database::Database, table::TableImporter, tables, transaction::DbTx,
|
||||
@@ -28,7 +28,7 @@ pub(crate) async fn dump_execution_stage<N, E, C>(
|
||||
where
|
||||
N: ProviderNodeTypes<DB = Arc<DatabaseEnv>>,
|
||||
E: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
C: FullConsensus<E::Primitives, Error = ConsensusError> + 'static,
|
||||
C: FullConsensus<E::Primitives> + 'static,
|
||||
{
|
||||
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
|
||||
|
||||
@@ -169,7 +169,7 @@ fn dry_run<N, E, C>(
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
E: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
C: FullConsensus<E::Primitives, Error = ConsensusError> + 'static,
|
||||
C: FullConsensus<E::Primitives> + 'static,
|
||||
{
|
||||
info!(target: "reth::cli", "Executing stage. [dry-run]");
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::setup;
|
||||
use alloy_primitives::{Address, BlockNumber};
|
||||
use eyre::Result;
|
||||
use reth_config::config::EtlConfig;
|
||||
use reth_consensus::{ConsensusError, FullConsensus};
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_db::DatabaseEnv;
|
||||
use reth_db_api::{database::Database, models::BlockNumberAddress, table::TableImporter, tables};
|
||||
use reth_db_common::DbTool;
|
||||
@@ -31,7 +31,7 @@ pub(crate) async fn dump_merkle_stage<N>(
|
||||
output_datadir: ChainPath<DataDirPath>,
|
||||
should_run: bool,
|
||||
evm_config: impl ConfigureEvm<Primitives = N::Primitives>,
|
||||
consensus: impl FullConsensus<N::Primitives, Error = ConsensusError> + 'static,
|
||||
consensus: impl FullConsensus<N::Primitives> + 'static,
|
||||
) -> Result<()>
|
||||
where
|
||||
N: ProviderNodeTypes<DB = Arc<DatabaseEnv>>,
|
||||
@@ -79,7 +79,7 @@ fn unwind_and_copy<N: ProviderNodeTypes>(
|
||||
tip_block_number: u64,
|
||||
output_db: &DatabaseEnv,
|
||||
evm_config: impl ConfigureEvm<Primitives = N::Primitives>,
|
||||
consensus: impl FullConsensus<N::Primitives, Error = ConsensusError> + 'static,
|
||||
consensus: impl FullConsensus<N::Primitives> + 'static,
|
||||
) -> eyre::Result<()> {
|
||||
let (from, to) = range;
|
||||
let provider = db_tool.provider_factory.database_provider_rw()?;
|
||||
|
||||
@@ -11,7 +11,6 @@ use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_cli_runner::CliContext;
|
||||
use reth_cli_util::get_secret_key;
|
||||
use reth_config::config::{HashingConfig, SenderRecoveryConfig, TransactionLookupConfig};
|
||||
use reth_db_api::database_metrics::DatabaseMetrics;
|
||||
use reth_downloaders::{
|
||||
bodies::bodies::BodiesDownloaderBuilder,
|
||||
headers::reverse_headers::ReverseHeadersDownloaderBuilder,
|
||||
@@ -19,19 +18,19 @@ use reth_downloaders::{
|
||||
use reth_exex::ExExManagerHandle;
|
||||
use reth_network::BlockDownloaderProvider;
|
||||
use reth_network_p2p::HeadersClient;
|
||||
use reth_node_builder::common::metrics_hooks;
|
||||
use reth_node_core::{
|
||||
args::{NetworkArgs, StageEnum},
|
||||
version::version_metadata,
|
||||
};
|
||||
use reth_node_metrics::{
|
||||
chain::ChainSpecInfo,
|
||||
hooks::Hooks,
|
||||
server::{MetricServer, MetricServerConfig},
|
||||
version::VersionInfo,
|
||||
};
|
||||
use reth_provider::{
|
||||
ChainSpecProvider, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
|
||||
StageCheckpointWriter, StaticFileProviderFactory,
|
||||
StageCheckpointWriter,
|
||||
};
|
||||
use reth_stages::{
|
||||
stages::{
|
||||
@@ -139,20 +138,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
},
|
||||
ChainSpecInfo { name: provider_factory.chain_spec().chain().to_string() },
|
||||
ctx.task_executor,
|
||||
Hooks::builder()
|
||||
.with_hook({
|
||||
let db = provider_factory.db_ref().clone();
|
||||
move || db.report_metrics()
|
||||
})
|
||||
.with_hook({
|
||||
let sfp = provider_factory.static_file_provider();
|
||||
move || {
|
||||
if let Err(error) = sfp.report_metrics() {
|
||||
error!(%error, "Failed to report metrics from static file provider");
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(),
|
||||
metrics_hooks(&provider_factory),
|
||||
data_dir.pprof_dumps(),
|
||||
);
|
||||
|
||||
MetricServer::new(config).serve().await?;
|
||||
|
||||
@@ -18,8 +18,8 @@ use tracing::{debug, error, trace};
|
||||
///
|
||||
/// Provides utilities for running a cli command to completion.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct CliRunner {
|
||||
config: CliRunnerConfig,
|
||||
tokio_runtime: tokio::runtime::Runtime,
|
||||
}
|
||||
|
||||
@@ -29,12 +29,18 @@ impl CliRunner {
|
||||
///
|
||||
/// The default tokio runtime is multi-threaded, with both I/O and time drivers enabled.
|
||||
pub fn try_default_runtime() -> Result<Self, std::io::Error> {
|
||||
Ok(Self { tokio_runtime: tokio_runtime()? })
|
||||
Ok(Self { config: CliRunnerConfig::default(), tokio_runtime: tokio_runtime()? })
|
||||
}
|
||||
|
||||
/// Create a new [`CliRunner`] from a provided tokio [`Runtime`](tokio::runtime::Runtime).
|
||||
pub const fn from_runtime(tokio_runtime: tokio::runtime::Runtime) -> Self {
|
||||
Self { tokio_runtime }
|
||||
Self { config: CliRunnerConfig::new(), tokio_runtime }
|
||||
}
|
||||
|
||||
/// Sets the [`CliRunnerConfig`] for this runner.
|
||||
pub const fn with_config(mut self, config: CliRunnerConfig) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Executes an async block on the runtime and blocks until completion.
|
||||
@@ -74,7 +80,7 @@ impl CliRunner {
|
||||
// after the command has finished or exit signal was received we shutdown the task
|
||||
// manager which fires the shutdown signal to all tasks spawned via the task
|
||||
// executor and awaiting on tasks spawned with graceful shutdown
|
||||
task_manager.graceful_shutdown_with_timeout(Duration::from_secs(5));
|
||||
task_manager.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
|
||||
}
|
||||
|
||||
// `drop(tokio_runtime)` would block the current thread until its pools
|
||||
@@ -128,7 +134,7 @@ impl CliRunner {
|
||||
error!(target: "reth::cli", "shutting down due to error");
|
||||
} else {
|
||||
debug!(target: "reth::cli", "shutting down gracefully");
|
||||
task_manager.graceful_shutdown_with_timeout(Duration::from_secs(5));
|
||||
task_manager.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
|
||||
}
|
||||
|
||||
// Shutdown the runtime on a separate thread
|
||||
@@ -211,6 +217,38 @@ pub struct CliContext {
|
||||
pub task_executor: TaskExecutor,
|
||||
}
|
||||
|
||||
/// Default timeout for graceful shutdown of tasks.
|
||||
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Configuration for [`CliRunner`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CliRunnerConfig {
|
||||
/// Timeout for graceful shutdown of tasks.
|
||||
///
|
||||
/// After the command completes, this is the maximum time to wait for spawned tasks
|
||||
/// to finish before forcefully terminating them.
|
||||
pub graceful_shutdown_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for CliRunnerConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CliRunnerConfig {
|
||||
/// Creates a new config with default values.
|
||||
pub const fn new() -> Self {
|
||||
Self { graceful_shutdown_timeout: DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT }
|
||||
}
|
||||
|
||||
/// Sets the graceful shutdown timeout.
|
||||
pub const fn with_graceful_shutdown_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.graceful_shutdown_timeout = timeout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new default tokio multi-thread [Runtime](tokio::runtime::Runtime) with all features
|
||||
/// enabled
|
||||
pub fn tokio_runtime() -> Result<tokio::runtime::Runtime, std::io::Error> {
|
||||
|
||||
@@ -26,7 +26,8 @@ rand_08.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
tracy-client = { workspace = true, optional = true, features = ["demangle"] }
|
||||
tracy-client = { workspace = true, optional = true }
|
||||
reth-tracing = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
@@ -46,7 +47,7 @@ jemalloc-prof = ["jemalloc", "tikv-jemallocator?/profiling"]
|
||||
jemalloc-unprefixed = ["jemalloc", "tikv-jemallocator?/unprefixed_malloc_on_supported_platforms"]
|
||||
|
||||
# Wraps the selected allocator in the tracy profiling allocator
|
||||
tracy-allocator = ["dep:tracy-client"]
|
||||
tracy-allocator = ["dep:tracy-client", "dep:reth-tracing"]
|
||||
|
||||
snmalloc = ["dep:snmalloc-rs"]
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ cfg_if::cfg_if! {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "tracy-allocator")] {
|
||||
type AllocatorWrapper = tracy_client::ProfiledAllocator<AllocatorInner>;
|
||||
tracy_client::register_demangler!();
|
||||
const fn new_allocator_wrapper() -> AllocatorWrapper {
|
||||
AllocatorWrapper::new(AllocatorInner {}, 100)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(feature = "tracy-allocator")]
|
||||
use reth_tracing as _;
|
||||
|
||||
pub mod allocator;
|
||||
pub mod cancellation;
|
||||
|
||||
|
||||
@@ -121,7 +121,16 @@ pub fn install() {
|
||||
unsafe {
|
||||
let alt_stack_size: usize = min_sigstack_size() + 64 * 1024;
|
||||
let mut alt_stack: libc::stack_t = mem::zeroed();
|
||||
alt_stack.ss_sp = alloc(Layout::from_size_align(alt_stack_size, 1).unwrap()).cast();
|
||||
// Both SysV AMD64 ABI and aarch64 ABI require 16 bytes alignment. We are going to be
|
||||
// generous here and just use a size of a page.
|
||||
let raw_page_sz = libc::sysconf(libc::_SC_PAGESIZE);
|
||||
let page_sz = if raw_page_sz == -1 {
|
||||
// Fallback alignment in case sysconf fails.
|
||||
4096_usize
|
||||
} else {
|
||||
raw_page_sz as usize
|
||||
};
|
||||
alt_stack.ss_sp = alloc(Layout::from_size_align(alt_stack_size, page_sz).unwrap()).cast();
|
||||
alt_stack.ss_size = alt_stack_size;
|
||||
libc::sigaltstack(&raw const alt_stack, ptr::null_mut());
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
use reth_network_types::{PeersConfig, SessionsConfig};
|
||||
use reth_prune_types::PruneModes;
|
||||
use reth_stages_types::ExecutionStageThresholds;
|
||||
use reth_static_file_types::StaticFileSegment;
|
||||
use reth_static_file_types::{StaticFileMap, StaticFileSegment};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -22,7 +21,6 @@ pub const DEFAULT_BLOCK_INTERVAL: usize = 5;
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Config {
|
||||
/// Configuration for each stage in the pipeline.
|
||||
// TODO(onbjerg): Can we make this easier to maintain when we add/remove stages?
|
||||
pub stages: StageConfig,
|
||||
/// Configuration for pruning.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
@@ -438,6 +436,10 @@ pub struct BlocksPerFileConfig {
|
||||
pub receipts: Option<u64>,
|
||||
/// Number of blocks per file for the transaction senders segment.
|
||||
pub transaction_senders: Option<u64>,
|
||||
/// Number of blocks per file for the account changesets segment.
|
||||
pub account_change_sets: Option<u64>,
|
||||
/// Number of blocks per file for the storage changesets segment.
|
||||
pub storage_change_sets: Option<u64>,
|
||||
}
|
||||
|
||||
impl StaticFilesConfig {
|
||||
@@ -445,8 +447,14 @@ impl StaticFilesConfig {
|
||||
///
|
||||
/// Returns an error if any blocks per file value is zero.
|
||||
pub fn validate(&self) -> eyre::Result<()> {
|
||||
let BlocksPerFileConfig { headers, transactions, receipts, transaction_senders } =
|
||||
self.blocks_per_file;
|
||||
let BlocksPerFileConfig {
|
||||
headers,
|
||||
transactions,
|
||||
receipts,
|
||||
transaction_senders,
|
||||
account_change_sets,
|
||||
storage_change_sets,
|
||||
} = self.blocks_per_file;
|
||||
eyre::ensure!(headers != Some(0), "Headers segment blocks per file must be greater than 0");
|
||||
eyre::ensure!(
|
||||
transactions != Some(0),
|
||||
@@ -460,15 +468,29 @@ impl StaticFilesConfig {
|
||||
transaction_senders != Some(0),
|
||||
"Transaction senders segment blocks per file must be greater than 0"
|
||||
);
|
||||
eyre::ensure!(
|
||||
account_change_sets != Some(0),
|
||||
"Account changesets segment blocks per file must be greater than 0"
|
||||
);
|
||||
eyre::ensure!(
|
||||
storage_change_sets != Some(0),
|
||||
"Storage changesets segment blocks per file must be greater than 0"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts the blocks per file configuration into a [`HashMap`] per segment.
|
||||
pub fn as_blocks_per_file_map(&self) -> HashMap<StaticFileSegment, u64> {
|
||||
let BlocksPerFileConfig { headers, transactions, receipts, transaction_senders } =
|
||||
self.blocks_per_file;
|
||||
/// Converts the blocks per file configuration into a [`StaticFileMap`].
|
||||
pub fn as_blocks_per_file_map(&self) -> StaticFileMap<u64> {
|
||||
let BlocksPerFileConfig {
|
||||
headers,
|
||||
transactions,
|
||||
receipts,
|
||||
transaction_senders,
|
||||
account_change_sets,
|
||||
storage_change_sets,
|
||||
} = self.blocks_per_file;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
let mut map = StaticFileMap::default();
|
||||
// Iterating over all possible segments allows us to do an exhaustive match here,
|
||||
// to not forget to configure new segments in the future.
|
||||
for segment in StaticFileSegment::iter() {
|
||||
@@ -477,6 +499,8 @@ impl StaticFilesConfig {
|
||||
StaticFileSegment::Transactions => transactions,
|
||||
StaticFileSegment::Receipts => receipts,
|
||||
StaticFileSegment::TransactionSenders => transaction_senders,
|
||||
StaticFileSegment::AccountChangeSets => account_change_sets,
|
||||
StaticFileSegment::StorageChangeSets => storage_change_sets,
|
||||
};
|
||||
|
||||
if let Some(blocks_per_file) = blocks_per_file {
|
||||
@@ -528,14 +552,13 @@ impl PruneConfig {
|
||||
|
||||
/// Returns whether there is any kind of receipt pruning configuration.
|
||||
pub fn has_receipts_pruning(&self) -> bool {
|
||||
self.segments.receipts.is_some() || !self.segments.receipts_log_filter.is_empty()
|
||||
self.segments.has_receipts_pruning()
|
||||
}
|
||||
|
||||
/// Merges values from `other` into `self`.
|
||||
/// - `Option<PruneMode>` fields: set from `other` only if `self` is `None`.
|
||||
/// - `block_interval`: set from `other` only if `self.block_interval ==
|
||||
/// DEFAULT_BLOCK_INTERVAL`.
|
||||
/// - `merkle_changesets`: always set from `other`.
|
||||
/// - `receipts_log_filter`: set from `other` only if `self` is empty and `other` is non-empty.
|
||||
pub fn merge(&mut self, other: Self) {
|
||||
let Self {
|
||||
@@ -548,7 +571,6 @@ impl PruneConfig {
|
||||
account_history,
|
||||
storage_history,
|
||||
bodies_history,
|
||||
merkle_changesets,
|
||||
receipts_log_filter,
|
||||
},
|
||||
} = other;
|
||||
@@ -565,8 +587,6 @@ impl PruneConfig {
|
||||
self.segments.account_history = self.segments.account_history.or(account_history);
|
||||
self.segments.storage_history = self.segments.storage_history.or(storage_history);
|
||||
self.segments.bodies_history = self.segments.bodies_history.or(bodies_history);
|
||||
// Merkle changesets is not optional; always take the value from `other`
|
||||
self.segments.merkle_changesets = merkle_changesets;
|
||||
|
||||
if self.segments.receipts_log_filter.0.is_empty() && !receipts_log_filter.0.is_empty() {
|
||||
self.segments.receipts_log_filter = receipts_log_filter;
|
||||
@@ -1063,18 +1083,6 @@ transaction_lookup = 'full'
|
||||
receipts = { distance = 16384 }
|
||||
#";
|
||||
let _conf: Config = toml::from_str(s).unwrap();
|
||||
|
||||
let s = r"#
|
||||
[prune]
|
||||
block_interval = 5
|
||||
|
||||
[prune.segments]
|
||||
sender_recovery = { distance = 16384 }
|
||||
transaction_lookup = 'full'
|
||||
receipts = 'full'
|
||||
#";
|
||||
let err = toml::from_str::<Config>(s).unwrap_err().to_string();
|
||||
assert!(err.contains("invalid value: string \"full\""), "{}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1088,7 +1096,6 @@ receipts = 'full'
|
||||
account_history: None,
|
||||
storage_history: Some(PruneMode::Before(5000)),
|
||||
bodies_history: None,
|
||||
merkle_changesets: PruneMode::Before(0),
|
||||
receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([(
|
||||
Address::random(),
|
||||
PruneMode::Full,
|
||||
@@ -1105,7 +1112,6 @@ receipts = 'full'
|
||||
account_history: Some(PruneMode::Distance(2000)),
|
||||
storage_history: Some(PruneMode::Distance(3000)),
|
||||
bodies_history: None,
|
||||
merkle_changesets: PruneMode::Distance(10000),
|
||||
receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([
|
||||
(Address::random(), PruneMode::Distance(1000)),
|
||||
(Address::random(), PruneMode::Before(2000)),
|
||||
@@ -1124,7 +1130,6 @@ receipts = 'full'
|
||||
assert_eq!(config1.segments.receipts, Some(PruneMode::Distance(1000)));
|
||||
assert_eq!(config1.segments.account_history, Some(PruneMode::Distance(2000)));
|
||||
assert_eq!(config1.segments.storage_history, Some(PruneMode::Before(5000)));
|
||||
assert_eq!(config1.segments.merkle_changesets, PruneMode::Distance(10000));
|
||||
assert_eq!(config1.segments.receipts_log_filter, original_filter);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
//! Collection of methods for block validation.
|
||||
|
||||
use alloy_consensus::{BlockHeader as _, Transaction, EMPTY_OMMER_ROOT_HASH};
|
||||
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
|
||||
use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
|
||||
use reth_consensus::{ConsensusError, TxGasLimitTooHighErr};
|
||||
use reth_consensus::ConsensusError;
|
||||
use reth_primitives_traits::{
|
||||
constants::{
|
||||
GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MAX_TX_GAS_LIMIT_OSAKA, MINIMUM_GAS_LIMIT,
|
||||
},
|
||||
transaction::TxHashRef,
|
||||
constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
|
||||
Block, BlockBody, BlockHeader, GotExpected, SealedBlock, SealedHeader,
|
||||
};
|
||||
|
||||
@@ -146,7 +143,7 @@ pub fn validate_block_pre_execution<B, ChainSpec>(
|
||||
) -> Result<(), ConsensusError>
|
||||
where
|
||||
B: Block,
|
||||
ChainSpec: EthereumHardforks,
|
||||
ChainSpec: EthChainSpec + EthereumHardforks,
|
||||
{
|
||||
post_merge_hardfork_fields(block, chain_spec)?;
|
||||
|
||||
@@ -154,19 +151,6 @@ where
|
||||
if let Err(error) = block.ensure_transaction_root_valid() {
|
||||
return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
|
||||
}
|
||||
// EIP-7825 validation
|
||||
if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) {
|
||||
for tx in block.body().transactions() {
|
||||
if tx.gas_limit() > MAX_TX_GAS_LIMIT_OSAKA {
|
||||
return Err(TxGasLimitTooHighErr {
|
||||
tx_hash: *tx.tx_hash(),
|
||||
gas_limit: tx.gas_limit(),
|
||||
max_allowed: MAX_TX_GAS_LIMIT_OSAKA,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -500,13 +484,11 @@ mod tests {
|
||||
let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
|
||||
|
||||
// validate blob, it should fail blob gas used validation
|
||||
assert_eq!(
|
||||
validate_block_pre_execution(&block, &chain_spec),
|
||||
Err(ConsensusError::BlobGasUsedDiff(GotExpected {
|
||||
got: 1,
|
||||
expected: expected_blob_gas_used
|
||||
}))
|
||||
);
|
||||
assert!(matches!(
|
||||
validate_block_pre_execution(&block, &chain_spec).unwrap_err(),
|
||||
ConsensusError::BlobGasUsedDiff(diff)
|
||||
if diff.got == 1 && diff.expected == expected_blob_gas_used
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -517,10 +499,10 @@ mod tests {
|
||||
|
||||
// Test exceeding default - should fail
|
||||
let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
|
||||
assert_eq!(
|
||||
validate_header_extra_data(&header_33, 32),
|
||||
Err(ConsensusError::ExtraDataExceedsMax { len: 33 })
|
||||
);
|
||||
assert!(matches!(
|
||||
validate_header_extra_data(&header_33, 32).unwrap_err(),
|
||||
ConsensusError::ExtraDataExceedsMax { len } if len == 33
|
||||
));
|
||||
|
||||
// Test with custom larger limit - should pass
|
||||
assert!(validate_header_extra_data(&header_33, 64).is_ok());
|
||||
|
||||
@@ -11,9 +11,16 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::{boxed::Box, fmt::Debug, string::String, vec::Vec};
|
||||
use alloc::{boxed::Box, fmt::Debug, string::String, sync::Arc, vec::Vec};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
|
||||
use core::error::Error;
|
||||
|
||||
/// Pre-computed receipt root and logs bloom.
|
||||
///
|
||||
/// When provided to [`FullConsensus::validate_block_post_execution`], this allows skipping
|
||||
/// the receipt root computation and using the pre-computed values instead.
|
||||
pub type ReceiptRootBloom = (B256, Bloom);
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{
|
||||
constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
|
||||
@@ -38,26 +45,27 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
|
||||
///
|
||||
/// See the Yellow Paper sections 4.3.2 "Holistic Validity".
|
||||
///
|
||||
/// If `receipt_root_bloom` is provided, the implementation should use the pre-computed
|
||||
/// receipt root and logs bloom instead of computing them from the receipts.
|
||||
///
|
||||
/// Note: validating blocks does not include other validations of the Consensus
|
||||
fn validate_block_post_execution(
|
||||
&self,
|
||||
block: &RecoveredBlock<N::Block>,
|
||||
result: &BlockExecutionResult<N::Receipt>,
|
||||
receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
) -> Result<(), ConsensusError>;
|
||||
}
|
||||
|
||||
/// Consensus is a protocol that chooses canonical chain.
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
|
||||
/// The error type related to consensus.
|
||||
type Error;
|
||||
|
||||
/// Ensures that body field values match the header.
|
||||
fn validate_body_against_header(
|
||||
&self,
|
||||
body: &B::Body,
|
||||
header: &SealedHeader<B::Header>,
|
||||
) -> Result<(), Self::Error>;
|
||||
) -> Result<(), ConsensusError>;
|
||||
|
||||
/// Validate a block disregarding world state, i.e. things that can be checked before sender
|
||||
/// recovery and execution.
|
||||
@@ -69,7 +77,7 @@ pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
|
||||
/// **This should not be called for the genesis block**.
|
||||
///
|
||||
/// Note: validating blocks does not include other validations of the Consensus
|
||||
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), Self::Error>;
|
||||
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
|
||||
}
|
||||
|
||||
/// `HeaderValidator` is a protocol that validates headers and their relationships.
|
||||
@@ -125,7 +133,7 @@ pub trait HeaderValidator<H = Header>: Debug + Send + Sync {
|
||||
}
|
||||
|
||||
/// Consensus Errors
|
||||
#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ConsensusError {
|
||||
/// Error when the gas used in the header exceeds the gas limit.
|
||||
#[error("block used gas ({gas_used}) is greater than gas limit ({gas_limit})")]
|
||||
@@ -135,7 +143,7 @@ pub enum ConsensusError {
|
||||
/// The gas limit in the block header.
|
||||
gas_limit: u64,
|
||||
},
|
||||
/// Error when the gas the gas limit is more than the maximum allowed.
|
||||
/// Error when the gas limit is more than the maximum allowed.
|
||||
#[error(
|
||||
"header gas limit ({gas_limit}) exceed the maximum allowed gas limit ({MAXIMUM_GAS_LIMIT_BLOCK})"
|
||||
)]
|
||||
@@ -410,6 +418,9 @@ pub enum ConsensusError {
|
||||
/// Other, likely an injected L2 error.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
/// Other unspecified error.
|
||||
#[error(transparent)]
|
||||
Custom(#[from] Arc<dyn Error + Send + Sync>),
|
||||
}
|
||||
|
||||
impl ConsensusError {
|
||||
@@ -447,3 +458,34 @@ pub struct TxGasLimitTooHighErr {
|
||||
/// The maximum allowed gas limit
|
||||
pub max_allowed: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Custom L2 consensus error")]
|
||||
struct CustomL2Error;
|
||||
|
||||
#[test]
|
||||
fn test_custom_error_conversion() {
|
||||
// Test conversion from custom error to ConsensusError
|
||||
let custom_err = CustomL2Error;
|
||||
let arc_err: Arc<dyn Error + Send + Sync> = Arc::new(custom_err);
|
||||
let consensus_err: ConsensusError = arc_err.into();
|
||||
|
||||
// Verify it's the Custom variant
|
||||
assert!(matches!(consensus_err, ConsensusError::Custom(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_error_display() {
|
||||
let custom_err = CustomL2Error;
|
||||
let arc_err: Arc<dyn Error + Send + Sync> = Arc::new(custom_err);
|
||||
let consensus_err: ConsensusError = arc_err.into();
|
||||
|
||||
// Verify the error message is preserved through transparent attribute
|
||||
let error_message = format!("{}", consensus_err);
|
||||
assert_eq!(error_message, "Custom L2 consensus error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//!
|
||||
//! **Not for production use** - provides no security guarantees or consensus validation.
|
||||
|
||||
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator};
|
||||
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
|
||||
use alloc::sync::Arc;
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
|
||||
@@ -55,19 +55,17 @@ impl<H> HeaderValidator<H> for NoopConsensus {
|
||||
}
|
||||
|
||||
impl<B: Block> Consensus<B> for NoopConsensus {
|
||||
type Error = ConsensusError;
|
||||
|
||||
/// Validates body against header (no-op implementation).
|
||||
fn validate_body_against_header(
|
||||
&self,
|
||||
_body: &B::Body,
|
||||
_header: &SealedHeader<B::Header>,
|
||||
) -> Result<(), Self::Error> {
|
||||
) -> Result<(), ConsensusError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates block before execution (no-op implementation).
|
||||
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), Self::Error> {
|
||||
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), ConsensusError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -78,6 +76,7 @@ impl<N: NodePrimitives> FullConsensus<N> for NoopConsensus {
|
||||
&self,
|
||||
_block: &RecoveredBlock<N::Block>,
|
||||
_result: &BlockExecutionResult<N::Receipt>,
|
||||
_receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
) -> Result<(), ConsensusError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user