From cb96fe6d093b596ffae2ce90629cea2805f648ec Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Wed, 10 Jan 2024 05:21:43 -0500 Subject: [PATCH] feat: make payload builder generic over attributes type (#5948) Co-authored-by: Matthias Seitz --- Cargo.lock | 256 ++++---- Cargo.toml | 4 + bin/reth/Cargo.toml | 2 + bin/reth/src/args/rpc_server_args.rs | 10 +- bin/reth/src/builder/mod.rs | 27 +- bin/reth/src/cli/ext.rs | 40 +- .../src/commands/debug_cmd/build_block.rs | 33 +- .../src/commands/debug_cmd/replay_engine.rs | 54 +- crates/consensus/auto-seal/Cargo.toml | 1 + crates/consensus/auto-seal/src/lib.rs | 13 +- crates/consensus/auto-seal/src/task.rs | 16 +- crates/consensus/beacon/Cargo.toml | 2 + crates/consensus/beacon/src/engine/handle.rs | 33 +- crates/consensus/beacon/src/engine/message.rs | 7 +- crates/consensus/beacon/src/engine/mod.rs | 562 +++++++++--------- .../consensus/beacon/src/engine/test_utils.rs | 15 +- crates/node-api/Cargo.toml | 24 + crates/node-api/src/engine/error.rs | 41 ++ crates/node-api/src/engine/mod.rs | 323 ++++++++++ .../src => node-api/src/engine}/payload.rs | 38 +- crates/node-api/src/engine/traits.rs | 189 ++++++ crates/node-api/src/lib.rs | 18 + crates/node-builder/Cargo.toml | 21 + crates/node-builder/src/engine.rs | 23 + crates/node-builder/src/lib.rs | 13 + crates/payload/basic/Cargo.toml | 1 + crates/payload/basic/src/lib.rs | 101 ++-- crates/payload/builder/Cargo.toml | 5 + crates/payload/builder/src/lib.rs | 11 +- crates/payload/builder/src/noop.rs | 17 +- crates/payload/builder/src/payload.rs | 291 ++++++--- crates/payload/builder/src/service.rs | 132 ++-- crates/payload/builder/src/test_utils.rs | 25 +- crates/payload/builder/src/traits.rs | 10 +- crates/payload/ethereum/src/lib.rs | 10 +- crates/payload/optimism/Cargo.toml | 1 + crates/payload/optimism/src/lib.rs | 46 +- crates/rpc/rpc-api/Cargo.toml | 2 + crates/rpc/rpc-api/src/engine.rs | 28 +- crates/rpc/rpc-builder/Cargo.toml | 2 + crates/rpc/rpc-builder/src/auth.rs | 16 +- crates/rpc/rpc-builder/src/lib.rs | 16 +- crates/rpc/rpc-builder/tests/it/auth.rs | 2 + crates/rpc/rpc-builder/tests/it/utils.rs | 3 +- crates/rpc/rpc-engine-api/Cargo.toml | 3 + crates/rpc/rpc-engine-api/src/engine_api.rs | 274 +++------ crates/rpc/rpc-engine-api/src/error.rs | 54 +- crates/rpc/rpc-engine-api/src/lib.rs | 3 - crates/rpc/rpc-types/src/beacon/payload.rs | 83 ++- .../rpc/rpc-types/src/eth/engine/payload.rs | 10 +- 50 files changed, 1912 insertions(+), 999 deletions(-) create mode 100644 crates/node-api/Cargo.toml create mode 100644 crates/node-api/src/engine/error.rs create mode 100644 crates/node-api/src/engine/mod.rs rename crates/{rpc/rpc-engine-api/src => node-api/src/engine}/payload.rs (60%) create mode 100644 crates/node-api/src/engine/traits.rs create mode 100644 crates/node-api/src/lib.rs create mode 100644 crates/node-builder/Cargo.toml create mode 100644 crates/node-builder/src/engine.rs create mode 100644 crates/node-builder/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9a69f1a94..39f61ed22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", - "getrandom 0.2.11", + "getrandom 0.2.12", "once_cell", "version_check", "zerocopy", @@ -182,7 +182,7 @@ dependencies = [ "derive_arbitrary", "derive_more", "ethereum_ssz", - "getrandom 0.2.11", + "getrandom 0.2.12", "hex-literal", "itoa", "keccak-asm", @@ -726,9 +726,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" [[package]] name = "base64ct" @@ -1305,9 +1305,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.13" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +checksum = "33e92c5c1a78c62968ec57dbc2440366a2d6e5a23faf829970ff1585dc6b18e2" dependencies = [ "clap_builder", "clap_derive", @@ -1315,9 +1315,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "f4323769dc8a61e2c39ad7dc26f6f2800524691a44d74fe3d1071a5c24db6370" dependencies = [ "anstream", "anstyle", @@ -1409,7 +1409,7 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5286a0843c21f8367f7be734f89df9b822e0321d8bcce8d6e735aadff7d74979" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "bech32", "bs58", "digest 0.10.7", @@ -1464,14 +1464,14 @@ dependencies = [ [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -1618,44 +1618,37 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-channel" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" @@ -2323,7 +2316,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe81b5c06ecfdbc71dd845216f225f53b62a10cb8a16c946836a3467f701d05b" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "bytes", "ed25519-dalek", "hex", @@ -2531,7 +2524,7 @@ dependencies = [ "serde", "serde_json", "syn 2.0.48", - "toml 0.8.2", + "toml 0.8.8", "walkdir", ] @@ -2566,7 +2559,7 @@ dependencies = [ "ethabi", "generic-array", "k256", - "num_enum 0.7.1", + "num_enum 0.7.2", "once_cell", "open-fastrlp", "rand 0.8.5", @@ -2615,7 +2608,7 @@ checksum = "25d6c0c9455d93d4990c06e049abf9b30daf148cf461ee939c11d88907c60816" dependencies = [ "async-trait", "auto_impl", - "base64 0.21.5", + "base64 0.21.6", "bytes", "const-hex", "enr", @@ -2989,9 +2982,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -4007,7 +4000,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "pem", "ring 0.16.20", "serde", @@ -4017,9 +4010,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", "ecdsa", @@ -4065,9 +4058,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libffi" @@ -4289,7 +4282,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d4fa7ce7c4862db464a37b0b31d89bca874562f034bd7993895572783d02950" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "hyper", "indexmap 1.9.3", "ipnet", @@ -4618,11 +4611,11 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ - "num_enum_derive 0.7.1", + "num_enum_derive 0.7.2", ] [[package]] @@ -4639,11 +4632,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 3.0.0", "proc-macro2", "quote", "syn 2.0.48", @@ -4786,7 +4779,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 1.0.109", @@ -5219,12 +5212,20 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2685dd208a3771337d8d386a89840f0f43cd68be8dae90a5f8c2384effc9cd" +dependencies = [ + "toml_edit 0.21.0", ] [[package]] @@ -5453,7 +5454,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", ] [[package]] @@ -5527,7 +5528,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", "libredox", "thiserror", ] @@ -5592,7 +5593,7 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "bytes", "encoding_rs", "futures-core", @@ -5685,6 +5686,8 @@ dependencies = [ "reth-network", "reth-network-api", "reth-nippy-jar", + "reth-node-api", + "reth-node-builder", "reth-optimism-payload-builder", "reth-payload-builder", "reth-payload-validator", @@ -5712,7 +5715,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "toml 0.8.2", + "toml 0.8.8", "tracing", "tui", "vergen", @@ -5729,6 +5732,7 @@ dependencies = [ "reth", "reth-beacon-consensus", "reth-interfaces", + "reth-node-api", "reth-primitives", "reth-provider", "reth-revm", @@ -5751,6 +5755,7 @@ dependencies = [ "metrics", "reth-interfaces", "reth-metrics", + "reth-node-api", "reth-payload-builder", "reth-primitives", "reth-provider", @@ -5776,6 +5781,8 @@ dependencies = [ "reth-downloaders", "reth-interfaces", "reth-metrics", + "reth-node-api", + "reth-node-builder", "reth-payload-builder", "reth-payload-validator", "reth-primitives", @@ -5855,7 +5862,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "toml 0.8.2", + "toml 0.8.8", ] [[package]] @@ -6288,11 +6295,34 @@ dependencies = [ "zstd 0.12.4", ] +[[package]] +name = "reth-node-api" +version = "0.1.0-alpha.13" +dependencies = [ + "reth-payload-builder", + "reth-primitives", + "reth-rpc-types", + "serde", + "thiserror", +] + +[[package]] +name = "reth-node-builder" +version = "0.1.0-alpha.13" +dependencies = [ + "reth-node-api", + "reth-payload-builder", + "reth-primitives", + "reth-rpc-types", + "serde", +] + [[package]] name = "reth-optimism-payload-builder" version = "0.1.0-alpha.14" dependencies = [ "reth-basic-payload-builder", + "reth-node-api", "reth-payload-builder", "reth-primitives", "reth-provider", @@ -6308,10 +6338,12 @@ name = "reth-payload-builder" version = "0.1.0-alpha.14" dependencies = [ "alloy-rlp", + "async-trait", "futures-util", "metrics", "reth-interfaces", "reth-metrics", + "reth-node-api", "reth-primitives", "reth-provider", "reth-rpc-types", @@ -6320,6 +6352,7 @@ dependencies = [ "reth-transaction-pool", "revm", "revm-primitives", + "serde", "sha2", "thiserror", "tokio", @@ -6356,7 +6389,7 @@ dependencies = [ "hash-db", "itertools 0.11.0", "modular-bitfield", - "num_enum 0.7.1", + "num_enum 0.7.2", "nybbles", "once_cell", "plain_hasher", @@ -6380,7 +6413,7 @@ dependencies = [ "tempfile", "test-fuzz", "thiserror", - "toml 0.8.2", + "toml 0.8.8", "tracing", "triehash", "zstd 0.12.4", @@ -6510,6 +6543,8 @@ name = "reth-rpc-api" version = "0.1.0-alpha.14" dependencies = [ "jsonrpsee", + "reth-node-api", + "reth-payload-builder", "reth-primitives", "reth-rpc-types", "serde_json", @@ -6542,6 +6577,8 @@ dependencies = [ "reth-ipc", "reth-metrics", "reth-network-api", + "reth-node-api", + "reth-node-builder", "reth-payload-builder", "reth-primitives", "reth-provider", @@ -6576,6 +6613,8 @@ dependencies = [ "reth-beacon-consensus", "reth-interfaces", "reth-metrics", + "reth-node-api", + "reth-node-builder", "reth-payload-builder", "reth-primitives", "reth-provider", @@ -6890,7 +6929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "getrandom 0.2.11", + "getrandom 0.2.12", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -7074,7 +7113,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", ] [[package]] @@ -7399,7 +7438,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "chrono", "hex", "indexmap 1.9.3", @@ -8191,21 +8230,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.21.0", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -8223,9 +8262,20 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.1.0", "serde", @@ -8262,7 +8312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "async-compression", - "base64 0.21.5", + "base64 0.21.6", "bitflags 2.4.1", "bytes", "futures-core", @@ -8728,7 +8778,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", "serde", ] @@ -8738,7 +8788,7 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", ] [[package]] @@ -8979,15 +9029,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -9006,21 +9047,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -9051,12 +9077,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -9069,12 +9089,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -9087,12 +9101,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -9105,12 +9113,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -9123,12 +9125,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -9141,12 +9137,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -9159,12 +9149,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index c91c1c51d..1f802ed21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ members = [ "crates/rpc/rpc-testing-util/", "crates/rpc/rpc-types/", "crates/rpc/rpc-types-compat/", + "crates/node-builder/", + "crates/node-api/", "crates/snapshot/", "crates/stages/", "crates/storage/codecs/", @@ -112,6 +114,8 @@ reth-consensus-common = { path = "crates/consensus/common" } reth-db = { path = "crates/storage/db" } reth-discv4 = { path = "crates/net/discv4" } reth-dns-discovery = { path = "crates/net/dns" } +reth-node-builder = { path = "crates/node-builder" } +reth-node-api = { path = "crates/node-api" } reth-downloaders = { path = "crates/net/downloaders" } reth-ecies = { path = "crates/net/ecies" } reth-eth-wire = { path = "crates/net/eth-wire" } diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index f2481e1ec..6d635daf5 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -54,6 +54,8 @@ reth-prune.workspace = true reth-snapshot = { workspace = true, features = ["clap"] } reth-trie.workspace = true reth-nippy-jar.workspace = true +reth-node-api.workspace = true +reth-node-builder.workspace = true # crypto alloy-rlp.workspace = true diff --git a/bin/reth/src/args/rpc_server_args.rs b/bin/reth/src/args/rpc_server_args.rs index 1f0846b1f..13edb3804 100644 --- a/bin/reth/src/args/rpc_server_args.rs +++ b/bin/reth/src/args/rpc_server_args.rs @@ -18,6 +18,7 @@ use clap::{ }; use futures::TryFutureExt; use reth_network_api::{NetworkInfo, Peers}; +use reth_node_api::EngineTypes; use reth_provider::{ AccountReader, BlockReaderIdExt, CanonStateSubscriptions, ChainSpecProvider, ChangeSetReader, EvmEnvProvider, HeaderProvider, StateProviderFactory, @@ -224,7 +225,7 @@ impl RpcServerArgs { /// Returns the handles for the launched regular RPC server(s) (if any) and the server handle /// for the auth server that handles the `engine_` API that's accessed by the consensus /// layer. - pub async fn start_servers( + pub async fn start_servers( &self, components: &Reth, engine_api: Engine, @@ -233,7 +234,7 @@ impl RpcServerArgs { ) -> eyre::Result where Reth: RethNodeComponents, - Engine: EngineApiServer, + Engine: EngineApiServer, Conf: RethNodeCommandConfig, { let auth_config = self.auth_server_config(jwt_secret)?; @@ -322,13 +323,13 @@ impl RpcServerArgs { } /// Create Engine API server. - pub async fn start_auth_server( + pub async fn start_auth_server( &self, provider: Provider, pool: Pool, network: Network, executor: Tasks, - engine_api: EngineApi, + engine_api: EngineApi, jwt_secret: JwtSecret, ) -> Result where @@ -343,6 +344,7 @@ impl RpcServerArgs { Pool: TransactionPool + Clone + 'static, Network: NetworkInfo + Peers + Clone + 'static, Tasks: TaskSpawner + Clone + 'static, + EngineT: EngineTypes + 'static, { let socket_address = SocketAddr::new(self.auth_addr, self.auth_port); diff --git a/bin/reth/src/builder/mod.rs b/bin/reth/src/builder/mod.rs index 1163b9ebb..e6b2385af 100644 --- a/bin/reth/src/builder/mod.rs +++ b/bin/reth/src/builder/mod.rs @@ -59,6 +59,11 @@ use reth_interfaces::{ }; use reth_network::{NetworkBuilder, NetworkConfig, NetworkEvents, NetworkHandle, NetworkManager}; use reth_network_api::{NetworkInfo, PeersInfo}; +#[cfg(not(feature = "optimism"))] +use reth_node_builder::EthEngineTypes; +#[cfg(feature = "optimism")] +use reth_node_builder::OptimismEngineTypes; +use reth_payload_builder::PayloadBuilderHandle; use reth_primitives::{ constants::eip4844::{LoadKzgSettingsError, MAINNET_KZG_TRUSTED_SETUP}, kzg::KzgSettings, @@ -1109,8 +1114,24 @@ impl NodeBuilderWit ext.on_components_initialized(&components)?; debug!(target: "reth::cli", "Spawning payload builder service"); - let payload_builder = - ext.spawn_payload_builder_service(&self.config.builder, &components)?; + + // TODO: stateful node builder should handle this in with_payload_builder + // Optimism's payload builder is implemented on the OptimismPayloadBuilder type. + #[cfg(feature = "optimism")] + let payload_builder = reth_optimism_payload_builder::OptimismPayloadBuilder::default() + .set_compute_pending_block(self.config.builder.compute_pending_block); + + #[cfg(feature = "optimism")] + let payload_builder: PayloadBuilderHandle = + ext.spawn_payload_builder_service(&self.config.builder, &components, payload_builder)?; + + // The default payload builder is implemented on the unit type. + #[cfg(not(feature = "optimism"))] + let payload_builder = reth_ethereum_payload_builder::EthereumPayloadBuilder::default(); + + #[cfg(not(feature = "optimism"))] + let payload_builder: PayloadBuilderHandle = + ext.spawn_payload_builder_service(&self.config.builder, &components, payload_builder)?; let (consensus_engine_tx, mut consensus_engine_rx) = unbounded_channel(); if let Some(store_path) = self.config.debug.engine_api_store.clone() { @@ -1279,7 +1300,7 @@ impl NodeBuilderWit #[cfg(feature = "optimism")] if self.config.chain.is_optimism() && !self.config.rollup.enable_genesis_walkback { let client = rpc_server_handles.auth.http_client(); - reth_rpc_api::EngineApiClient::fork_choice_updated_v2( + reth_rpc_api::EngineApiClient::::fork_choice_updated_v2( &client, reth_rpc_types::engine::ForkchoiceState { head_block_hash: head.hash, diff --git a/bin/reth/src/cli/ext.rs b/bin/reth/src/cli/ext.rs index 266acaa5f..8715d408c 100644 --- a/bin/reth/src/cli/ext.rs +++ b/bin/reth/src/cli/ext.rs @@ -5,7 +5,10 @@ use crate::cli::{ config::{PayloadBuilderConfig, RethNetworkConfig, RethRpcConfig}, }; use clap::Args; -use reth_basic_payload_builder::{BasicPayloadJobGenerator, BasicPayloadJobGeneratorConfig}; +use reth_basic_payload_builder::{ + BasicPayloadJobGenerator, BasicPayloadJobGeneratorConfig, PayloadBuilder, +}; +use reth_node_api::EngineTypes; use reth_payload_builder::{PayloadBuilderHandle, PayloadBuilderService}; use reth_provider::CanonStateSubscriptions; use reth_tasks::TaskSpawner; @@ -125,14 +128,22 @@ pub trait RethNodeCommandConfig: fmt::Debug { /// /// By default this spawns a [BasicPayloadJobGenerator] with the default configuration /// [BasicPayloadJobGeneratorConfig]. - fn spawn_payload_builder_service( + fn spawn_payload_builder_service( &mut self, conf: &Conf, components: &Reth, - ) -> eyre::Result + payload_builder: Builder, + ) -> eyre::Result> where Conf: PayloadBuilderConfig, Reth: RethNodeComponents, + Engine: EngineTypes + 'static, + Builder: PayloadBuilder< + Reth::Pool, + Reth::Provider, + Attributes = Engine::PayloadBuilderAttributes, + > + Unpin + + 'static, { let payload_job_config = BasicPayloadJobGeneratorConfig::default() .interval(conf.interval()) @@ -145,15 +156,6 @@ pub trait RethNodeCommandConfig: fmt::Debug { #[cfg(feature = "optimism")] let payload_job_config = payload_job_config.extradata(Default::default()); - // The default payload builder is implemented on the unit type. - #[cfg(not(feature = "optimism"))] - let payload_builder = reth_ethereum_payload_builder::EthereumPayloadBuilder::default(); - - // Optimism's payload builder is implemented on the OptimismPayloadBuilder type. - #[cfg(feature = "optimism")] - let payload_builder = reth_optimism_payload_builder::OptimismPayloadBuilder::default() - .set_compute_pending_block(conf.compute_pending_block()); - let payload_generator = BasicPayloadJobGenerator::with_builder( components.provider(), components.pool(), @@ -315,18 +317,26 @@ impl RethNodeCommandConfig for NoArgs { } } - fn spawn_payload_builder_service( + fn spawn_payload_builder_service( &mut self, conf: &Conf, components: &Reth, - ) -> eyre::Result + payload_builder: Builder, + ) -> eyre::Result> where Conf: PayloadBuilderConfig, Reth: RethNodeComponents, + Engine: EngineTypes + 'static, + Builder: PayloadBuilder< + Reth::Pool, + Reth::Provider, + Attributes = Engine::PayloadBuilderAttributes, + > + Unpin + + 'static, { self.inner_mut() .ok_or_else(|| eyre::eyre!("config value must be set"))? - .spawn_payload_builder_service(conf, components) + .spawn_payload_builder_service(conf, components, payload_builder) } } diff --git a/bin/reth/src/commands/debug_cmd/build_block.rs b/bin/reth/src/commands/debug_cmd/build_block.rs index c4fe2b52f..9093f4ddc 100644 --- a/bin/reth/src/commands/debug_cmd/build_block.rs +++ b/bin/reth/src/commands/debug_cmd/build_block.rs @@ -20,7 +20,10 @@ use reth_blockchain_tree::{ }; use reth_db::{init_db, DatabaseEnv}; use reth_interfaces::{consensus::Consensus, RethResult}; -use reth_payload_builder::{database::CachedReads, PayloadBuilderAttributes}; +use reth_node_api::PayloadBuilderAttributes; +use reth_payload_builder::database::CachedReads; +#[cfg(feature = "optimism")] +use reth_payload_builder::OptimismPayloadBuilderAttributes; use reth_primitives::{ constants::eip4844::{LoadKzgSettingsError, MAINNET_KZG_TRUSTED_SETUP}, fs, @@ -34,6 +37,8 @@ use reth_provider::{ ProviderFactory, StageCheckpointReader, StateProviderFactory, }; use reth_revm::EvmProcessorFactory; +#[cfg(feature = "optimism")] +use reth_rpc_types::engine::OptimismPayloadAttributes; use reth_rpc_types::engine::{BlobsBundleV1, PayloadAttributes}; use reth_transaction_pool::{ blobstore::InMemoryBlobStore, BlobStore, EthPooledTransaction, PoolConfig, TransactionOrigin, @@ -42,6 +47,9 @@ use reth_transaction_pool::{ use std::{path::PathBuf, str::FromStr, sync::Arc}; use tracing::*; +#[cfg(not(feature = "optimism"))] +use reth_payload_builder::EthPayloadBuilderAttributes; + /// `reth debug build-block` command /// This debug routine requires that the node is positioned at the block before the target. /// The script will then parse the block and attempt to build a similar one. @@ -235,16 +243,31 @@ impl Command { suggested_fee_recipient: self.suggested_fee_recipient, // TODO: add support for withdrawals withdrawals: None, - #[cfg(feature = "optimism")] - optimism_payload_attributes: reth_rpc_types::engine::OptimismPayloadAttributes::default( - ), }; + #[cfg(feature = "optimism")] let payload_config = PayloadConfig::new( Arc::clone(&best_block), Bytes::default(), - PayloadBuilderAttributes::try_new(best_block.hash, payload_attrs)?, + OptimismPayloadBuilderAttributes::try_new( + best_block.hash, + OptimismPayloadAttributes { + payload_attributes: payload_attrs, + transactions: None, + no_tx_pool: None, + gas_limit: None, + }, + )?, self.chain.clone(), ); + + #[cfg(not(feature = "optimism"))] + let payload_config = PayloadConfig::new( + Arc::clone(&best_block), + Bytes::default(), + EthPayloadBuilderAttributes::try_new(best_block.hash, payload_attrs)?, + self.chain.clone(), + ); + let args = BuildArguments::new( blockchain_db.clone(), transaction_pool, diff --git a/bin/reth/src/commands/debug_cmd/replay_engine.rs b/bin/reth/src/commands/debug_cmd/replay_engine.rs index adb0f8c3a..49742863b 100644 --- a/bin/reth/src/commands/debug_cmd/replay_engine.rs +++ b/bin/reth/src/commands/debug_cmd/replay_engine.rs @@ -21,7 +21,12 @@ use reth_db::{init_db, DatabaseEnv}; use reth_interfaces::consensus::Consensus; use reth_network::NetworkHandle; use reth_network_api::NetworkInfo; -use reth_payload_builder::PayloadBuilderService; +use reth_node_api::EngineTypes; +#[cfg(not(feature = "optimism"))] +use reth_node_builder::EthEngineTypes; +#[cfg(feature = "optimism")] +use reth_node_builder::OptimismEngineTypes; +use reth_payload_builder::{PayloadBuilderHandle, PayloadBuilderService}; use reth_primitives::{ fs::{self}, ChainSpec, @@ -29,7 +34,7 @@ use reth_primitives::{ use reth_provider::{providers::BlockchainProvider, CanonStateSubscriptions, ProviderFactory}; use reth_revm::EvmProcessorFactory; use reth_rpc_types::{ - engine::{CancunPayloadFields, ForkchoiceState, PayloadAttributes}, + engine::{CancunPayloadFields, ForkchoiceState}, ExecutionPayload, }; use reth_stages::Pipeline; @@ -175,8 +180,17 @@ impl Command { self.chain.clone(), payload_builder, ); - let (payload_service, payload_builder) = + + #[cfg(feature = "optimism")] + let (payload_service, payload_builder): ( + _, + PayloadBuilderHandle, + ) = PayloadBuilderService::new(payload_generator, blockchain_db.canonical_state_stream()); + + #[cfg(not(feature = "optimism"))] + let (payload_service, payload_builder): (_, PayloadBuilderHandle) = PayloadBuilderService::new(payload_generator, blockchain_db.canonical_state_stream()); + ctx.task_executor.spawn_critical("payload builder service", Box::pin(payload_service)); // Configure the consensus engine @@ -245,8 +259,8 @@ impl Command { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -enum StoredEngineApiMessage { - ForkchoiceUpdated { state: ForkchoiceState, payload_attrs: Option }, +enum StoredEngineApiMessage { + ForkchoiceUpdated { state: ForkchoiceState, payload_attrs: Option }, NewPayload { payload: ExecutionPayload, cancun_fields: Option }, } @@ -260,7 +274,14 @@ impl EngineApiStore { Self { path } } - fn on_message(&self, msg: &BeaconEngineMessage, received_at: SystemTime) -> eyre::Result<()> { + fn on_message( + &self, + msg: &BeaconEngineMessage, + received_at: SystemTime, + ) -> eyre::Result<()> + where + Engine: EngineTypes, + { fs::create_dir_all(&self.path)?; // ensure that store path had been created let timestamp = received_at.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis(); match msg { @@ -278,10 +299,12 @@ impl EngineApiStore { let filename = format!("{}-new_payload-{}.json", timestamp, payload.block_hash()); fs::write( self.path.join(filename), - serde_json::to_vec(&StoredEngineApiMessage::NewPayload { - payload: payload.clone(), - cancun_fields: cancun_fields.clone(), - })?, + serde_json::to_vec( + &StoredEngineApiMessage::::NewPayload { + payload: payload.clone(), + cancun_fields: cancun_fields.clone(), + }, + )?, )?; } // noop @@ -310,11 +333,14 @@ impl EngineApiStore { Ok(filenames_by_ts.into_iter().flat_map(|(_, paths)| paths)) } - pub(crate) async fn intercept( + pub(crate) async fn intercept( self, - mut rx: UnboundedReceiver, - to_engine: UnboundedSender, - ) { + mut rx: UnboundedReceiver>, + to_engine: UnboundedSender>, + ) where + Engine: EngineTypes, + BeaconEngineMessage: std::fmt::Debug, + { loop { let Some(msg) = rx.recv().await else { break }; if let Err(error) = self.on_message(&msg, SystemTime::now()) { diff --git a/crates/consensus/auto-seal/Cargo.toml b/crates/consensus/auto-seal/Cargo.toml index 10538ad19..efb570650 100644 --- a/crates/consensus/auto-seal/Cargo.toml +++ b/crates/consensus/auto-seal/Cargo.toml @@ -20,6 +20,7 @@ reth-provider.workspace = true reth-stages.workspace = true reth-revm.workspace = true reth-transaction-pool.workspace = true +reth-node-api.workspace = true # async futures-util.workspace = true diff --git a/crates/consensus/auto-seal/src/lib.rs b/crates/consensus/auto-seal/src/lib.rs index b219f083b..cd70af97b 100644 --- a/crates/consensus/auto-seal/src/lib.rs +++ b/crates/consensus/auto-seal/src/lib.rs @@ -19,6 +19,7 @@ use reth_interfaces::{ consensus::{Consensus, ConsensusError}, executor::{BlockExecutionError, BlockValidationError}, }; +use reth_node_api::EngineTypes; use reth_primitives::{ constants::{EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, ETHEREUM_BLOCK_GAS_LIMIT}, proofs, Block, BlockBody, BlockHash, BlockHashOrNumber, BlockNumber, BlockWithSenders, Bloom, @@ -93,28 +94,30 @@ impl Consensus for AutoSealConsensus { /// Builder type for configuring the setup #[derive(Debug)] -pub struct AutoSealBuilder { +pub struct AutoSealBuilder { client: Client, consensus: AutoSealConsensus, pool: Pool, mode: MiningMode, storage: Storage, - to_engine: UnboundedSender, + to_engine: UnboundedSender>, canon_state_notification: CanonStateNotificationSender, } // === impl AutoSealBuilder === -impl AutoSealBuilder +impl AutoSealBuilder where Client: BlockReaderIdExt, + Pool: TransactionPool, + Engine: EngineTypes, { /// Creates a new builder instance to configure all parts. pub fn new( chain_spec: Arc, client: Client, pool: Pool, - to_engine: UnboundedSender, + to_engine: UnboundedSender>, canon_state_notification: CanonStateNotificationSender, mode: MiningMode, ) -> Self { @@ -143,7 +146,7 @@ where /// Consumes the type and returns all components #[track_caller] - pub fn build(self) -> (AutoSealConsensus, AutoSealClient, MiningTask) { + pub fn build(self) -> (AutoSealConsensus, AutoSealClient, MiningTask) { let Self { client, consensus, pool, mode, storage, to_engine, canon_state_notification } = self; let auto_client = AutoSealClient::new(storage.clone()); diff --git a/crates/consensus/auto-seal/src/task.rs b/crates/consensus/auto-seal/src/task.rs index a67f1cbeb..3e64e8bdb 100644 --- a/crates/consensus/auto-seal/src/task.rs +++ b/crates/consensus/auto-seal/src/task.rs @@ -2,6 +2,7 @@ use crate::{mode::MiningMode, Storage}; use futures_util::{future::BoxFuture, FutureExt}; use reth_beacon_consensus::{BeaconEngineMessage, ForkchoiceStatus}; use reth_interfaces::consensus::ForkchoiceState; +use reth_node_api::EngineTypes; use reth_primitives::{Block, ChainSpec, IntoRecoveredTransaction, SealedBlockWithSenders}; use reth_provider::{CanonChainTracker, CanonStateNotificationSender, Chain, StateProviderFactory}; use reth_stages::PipelineEvent; @@ -18,7 +19,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, warn}; /// A Future that listens for new ready transactions and puts new blocks into storage -pub struct MiningTask { +pub struct MiningTask { /// The configured chain spec chain_spec: Arc, /// The client used to interact with the state @@ -34,7 +35,7 @@ pub struct MiningTask { /// backlog of sets of transactions ready to be mined queued: VecDeque::Transaction>>>>, // TODO: ideally this would just be a sender of hashes - to_engine: UnboundedSender, + to_engine: UnboundedSender>, /// Used to notify consumers of new blocks canon_state_notification: CanonStateNotificationSender, /// The pipeline events to listen on @@ -43,12 +44,12 @@ pub struct MiningTask { // === impl MiningTask === -impl MiningTask { +impl MiningTask { /// Creates a new instance of the task pub(crate) fn new( chain_spec: Arc, miner: MiningMode, - to_engine: UnboundedSender, + to_engine: UnboundedSender>, canon_state_notification: CanonStateNotificationSender, storage: Storage, client: Client, @@ -74,11 +75,12 @@ impl MiningTask { } } -impl Future for MiningTask +impl Future for MiningTask where Client: StateProviderFactory + CanonChainTracker + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, ::Transaction: IntoRecoveredTransaction, + Engine: EngineTypes + 'static, { type Output = (); @@ -228,7 +230,9 @@ where } } -impl std::fmt::Debug for MiningTask { +impl std::fmt::Debug + for MiningTask +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MiningTask").finish_non_exhaustive() } diff --git a/crates/consensus/beacon/Cargo.toml b/crates/consensus/beacon/Cargo.toml index 9fd50ca82..ac8b903c8 100644 --- a/crates/consensus/beacon/Cargo.toml +++ b/crates/consensus/beacon/Cargo.toml @@ -25,6 +25,7 @@ reth-payload-validator.workspace = true reth-prune.workspace = true reth-snapshot.workspace = true reth-tokio-util.workspace = true +reth-node-api.workspace = true # async tokio = { workspace = true, features = ["sync"] } @@ -53,6 +54,7 @@ reth-rpc-types-compat.workspace = true reth-tracing.workspace = true reth-revm.workspace = true reth-downloaders.workspace = true +reth-node-builder.workspace = true assert_matches.workspace = true diff --git a/crates/consensus/beacon/src/engine/handle.rs b/crates/consensus/beacon/src/engine/handle.rs index 6fa3d2e22..ed51f6b79 100644 --- a/crates/consensus/beacon/src/engine/handle.rs +++ b/crates/consensus/beacon/src/engine/handle.rs @@ -6,9 +6,9 @@ use crate::{ }; use futures::TryFutureExt; use reth_interfaces::RethResult; +use reth_node_api::EngineTypes; use reth_rpc_types::engine::{ - CancunPayloadFields, ExecutionPayload, ForkchoiceState, ForkchoiceUpdated, PayloadAttributes, - PayloadStatus, + CancunPayloadFields, ExecutionPayload, ForkchoiceState, ForkchoiceUpdated, PayloadStatus, }; use tokio::sync::{mpsc, mpsc::UnboundedSender, oneshot}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -17,16 +17,31 @@ use tokio_stream::wrappers::UnboundedReceiverStream; /// engine task. /// /// See also [`BeaconConsensusEngine`](crate::engine::BeaconConsensusEngine). -#[derive(Clone, Debug)] -pub struct BeaconConsensusEngineHandle { - pub(crate) to_engine: UnboundedSender, +#[derive(Debug)] +pub struct BeaconConsensusEngineHandle +where + Engine: EngineTypes, +{ + pub(crate) to_engine: UnboundedSender>, +} + +impl Clone for BeaconConsensusEngineHandle +where + Engine: EngineTypes, +{ + fn clone(&self) -> Self { + Self { to_engine: self.to_engine.clone() } + } } // === impl BeaconConsensusEngineHandle === -impl BeaconConsensusEngineHandle { +impl BeaconConsensusEngineHandle +where + Engine: EngineTypes, +{ /// Creates a new beacon consensus engine handle. - pub fn new(to_engine: UnboundedSender) -> Self { + pub fn new(to_engine: UnboundedSender>) -> Self { Self { to_engine } } @@ -49,7 +64,7 @@ impl BeaconConsensusEngineHandle { pub async fn fork_choice_updated( &self, state: ForkchoiceState, - payload_attrs: Option, + payload_attrs: Option, ) -> Result { Ok(self .send_fork_choice_updated(state, payload_attrs) @@ -63,7 +78,7 @@ impl BeaconConsensusEngineHandle { fn send_fork_choice_updated( &self, state: ForkchoiceState, - payload_attrs: Option, + payload_attrs: Option, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel(); let _ = self.to_engine.send(BeaconEngineMessage::ForkchoiceUpdated { diff --git a/crates/consensus/beacon/src/engine/message.rs b/crates/consensus/beacon/src/engine/message.rs index dba492244..16f43ea16 100644 --- a/crates/consensus/beacon/src/engine/message.rs +++ b/crates/consensus/beacon/src/engine/message.rs @@ -4,10 +4,11 @@ use crate::{ }; use futures::{future::Either, FutureExt}; use reth_interfaces::{consensus::ForkchoiceState, RethResult}; +use reth_node_api::EngineTypes; use reth_payload_builder::error::PayloadBuilderError; use reth_rpc_types::engine::{ CancunPayloadFields, ExecutionPayload, ForkChoiceUpdateResult, ForkchoiceUpdateError, - ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, PayloadStatusEnum, + ForkchoiceUpdated, PayloadId, PayloadStatus, PayloadStatusEnum, }; use std::{ future::Future, @@ -141,7 +142,7 @@ impl Future for PendingPayloadId { /// consensus layer). #[derive(Debug)] #[allow(clippy::large_enum_variant)] -pub enum BeaconEngineMessage { +pub enum BeaconEngineMessage { /// Message with new payload. NewPayload { /// The execution payload received by Engine API. @@ -156,7 +157,7 @@ pub enum BeaconEngineMessage { /// The updated forkchoice state. state: ForkchoiceState, /// The payload attributes for block building. - payload_attrs: Option, + payload_attrs: Option, /// The sender for returning forkchoice updated result. tx: oneshot::Sender>, }, diff --git a/crates/consensus/beacon/src/engine/mod.rs b/crates/consensus/beacon/src/engine/mod.rs index 78f20380f..061e87cd4 100644 --- a/crates/consensus/beacon/src/engine/mod.rs +++ b/crates/consensus/beacon/src/engine/mod.rs @@ -20,7 +20,8 @@ use reth_interfaces::{ sync::{NetworkSyncUpdater, SyncState}, RethError, RethResult, }; -use reth_payload_builder::{PayloadBuilderAttributes, PayloadBuilderHandle}; +use reth_node_api::{EngineTypes, PayloadAttributes, PayloadBuilderAttributes}; +use reth_payload_builder::PayloadBuilderHandle; use reth_primitives::{ constants::EPOCH_SLOTS, stage::StageId, BlockNumHash, BlockNumber, Head, Header, SealedBlock, SealedHeader, B256, @@ -30,8 +31,7 @@ use reth_provider::{ StageCheckpointReader, }; use reth_rpc_types::engine::{ - CancunPayloadFields, ExecutionPayload, PayloadAttributes, PayloadStatus, PayloadStatusEnum, - PayloadValidationError, + CancunPayloadFields, ExecutionPayload, PayloadStatus, PayloadStatusEnum, PayloadValidationError, }; use reth_stages::{ControlFlow, Pipeline, PipelineError}; @@ -164,7 +164,7 @@ pub const MIN_BLOCKS_FOR_PIPELINE_RUN: u64 = EPOCH_SLOTS; /// If the future is polled more than once. Leads to undefined state. #[must_use = "Future does nothing unless polled"] #[allow(missing_debug_implementations)] -pub struct BeaconConsensusEngine +pub struct BeaconConsensusEngine where DB: Database, Client: HeadersClient + BodiesClient, @@ -173,6 +173,7 @@ where + BlockIdReader + CanonChainTracker + StageCheckpointReader, + EngineT: EngineTypes, { /// Controls syncing triggered by engine updates. sync: EngineSyncController, @@ -181,13 +182,13 @@ where /// Used for emitting updates about whether the engine is syncing or not. sync_state_updater: Box, /// The Engine API message receiver. - engine_message_rx: UnboundedReceiverStream, + engine_message_rx: UnboundedReceiverStream>, /// A clone of the handle - handle: BeaconConsensusEngineHandle, + handle: BeaconConsensusEngineHandle, /// Tracks the received forkchoice state updates received by the CL. forkchoice_state_tracker: ForkchoiceStateTracker, /// The payload store. - payload_builder: PayloadBuilderHandle, + payload_builder: PayloadBuilderHandle, /// Validator for execution payloads payload_validator: ExecutionPayloadValidator, /// Listeners for engine events. @@ -212,7 +213,7 @@ where hooks: EngineHooksController, } -impl BeaconConsensusEngine +impl BeaconConsensusEngine where DB: Database + Unpin + 'static, BT: BlockchainTreeEngine @@ -223,6 +224,7 @@ where + ChainSpecProvider + 'static, Client: HeadersClient + BodiesClient + Clone + Unpin + 'static, + EngineT: EngineTypes + Unpin + 'static, { /// Create a new instance of the [BeaconConsensusEngine]. #[allow(clippy::too_many_arguments)] @@ -234,11 +236,11 @@ where sync_state_updater: Box, max_block: Option, run_pipeline_continuously: bool, - payload_builder: PayloadBuilderHandle, + payload_builder: PayloadBuilderHandle, target: Option, pipeline_run_threshold: u64, hooks: EngineHooks, - ) -> RethResult<(Self, BeaconConsensusEngineHandle)> { + ) -> RethResult<(Self, BeaconConsensusEngineHandle)> { let (to_engine, rx) = mpsc::unbounded_channel(); Self::with_channel( client, @@ -278,13 +280,13 @@ where sync_state_updater: Box, max_block: Option, run_pipeline_continuously: bool, - payload_builder: PayloadBuilderHandle, + payload_builder: PayloadBuilderHandle, target: Option, pipeline_run_threshold: u64, - to_engine: UnboundedSender, - rx: UnboundedReceiver, + to_engine: UnboundedSender>, + rx: UnboundedReceiver>, hooks: EngineHooks, - ) -> RethResult<(Self, BeaconConsensusEngineHandle)> { + ) -> RethResult<(Self, BeaconConsensusEngineHandle)> { let handle = BeaconConsensusEngineHandle { to_engine }; let sync = EngineSyncController::new( pipeline, @@ -323,6 +325,219 @@ where Ok((this, handle)) } + /// Called to resolve chain forks and ensure that the Execution layer is working with the latest + /// valid chain. + /// + /// These responses should adhere to the [Engine API Spec for + /// `engine_forkchoiceUpdated`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#specification-1). + /// + /// Returns an error if an internal error occurred like a database error. + fn forkchoice_updated( + &mut self, + state: ForkchoiceState, + attrs: Option, + ) -> RethResult { + trace!(target: "consensus::engine", ?state, "Received new forkchoice state update"); + if state.head_block_hash.is_zero() { + return Ok(OnForkChoiceUpdated::invalid_state()); + } + + // check if the new head hash is connected to any ancestor that we previously marked as + // invalid + let lowest_buffered_ancestor_fcu = self.lowest_buffered_ancestor_or(state.head_block_hash); + if let Some(status) = self.check_invalid_ancestor(lowest_buffered_ancestor_fcu) { + return Ok(OnForkChoiceUpdated::with_invalid(status)); + } + + if self.sync.is_pipeline_active() { + // We can only process new forkchoice updates if the pipeline is idle, since it requires + // exclusive access to the database + trace!(target: "consensus::engine", "Pipeline is syncing, skipping forkchoice update"); + return Ok(OnForkChoiceUpdated::syncing()); + } + + if let Some(hook) = self.hooks.active_db_write_hook() { + // We can only process new forkchoice updates if no hook with db write is running, + // since it requires exclusive access to the database + warn!( + target: "consensus::engine", + hook = %hook.name(), + "Hook is in progress, skipping forkchoice update. \ + This may affect the performance of your node as a validator." + ); + return Ok(OnForkChoiceUpdated::syncing()); + } + + let start = Instant::now(); + let make_canonical_result = self.blockchain.make_canonical(&state.head_block_hash); + let elapsed = self.record_make_canonical_latency(start, &make_canonical_result); + + let status = match make_canonical_result { + Ok(outcome) => { + match &outcome { + CanonicalOutcome::AlreadyCanonical { header } => { + // On Optimism, the proposers are allowed to reorg their own chain at will. + cfg_if::cfg_if! { + if #[cfg(feature = "optimism")] { + if self.blockchain.chain_spec().is_optimism() { + debug!( + target: "consensus::engine", + fcu_head_num=?header.number, + current_head_num=?self.blockchain.canonical_tip().number, + "[Optimism] Allowing beacon reorg to old head" + ); + let _ = self.update_head(header.clone()); + self.listeners.notify( + BeaconConsensusEngineEvent::CanonicalChainCommitted( + Box::new(header.clone()), + elapsed, + ), + ); + } else { + debug!( + target: "consensus::engine", + fcu_head_num=?header.number, + current_head_num=?self.blockchain.canonical_tip().number, + "Ignoring beacon update to old head" + ); + } + } else { + debug!( + target: "consensus::engine", + fcu_head_num=?header.number, + current_head_num=?self.blockchain.canonical_tip().number, + "Ignoring beacon update to old head" + ); + } + } + } + CanonicalOutcome::Committed { head } => { + debug!( + target: "consensus::engine", + hash=?state.head_block_hash, + number=head.number, + "Canonicalized new head" + ); + + // new VALID update that moved the canonical chain forward + let _ = self.update_head(head.clone()); + self.listeners.notify(BeaconConsensusEngineEvent::CanonicalChainCommitted( + Box::new(head.clone()), + elapsed, + )); + } + } + + if let Some(attrs) = attrs { + // if we return early then we wouldn't perform these consistency checks, so we + // need to do them here, and should do them before we process any payload + // attributes + if let Some(invalid_fcu_response) = self.ensure_consistent_state(state)? { + trace!(target: "consensus::engine", ?state, head=?state.head_block_hash, "Forkchoice state is inconsistent, returning invalid response"); + return Ok(invalid_fcu_response); + } + + // the CL requested to build a new payload on top of this new VALID head + let payload_response = self.process_payload_attributes( + attrs, + outcome.into_header().unseal(), + state, + ); + + trace!(target: "consensus::engine", status = ?payload_response, ?state, "Returning forkchoice status"); + return Ok(payload_response); + } + + PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)) + } + Err(error) => { + if let RethError::Canonical(ref err) = error { + if err.is_fatal() { + tracing::error!(target: "consensus::engine", ?err, "Encountered fatal error"); + return Err(error); + } + } + + self.on_failed_canonical_forkchoice_update(&state, error) + } + }; + + if let Some(invalid_fcu_response) = + self.ensure_consistent_state_with_status(state, &status)? + { + trace!(target: "consensus::engine", ?status, ?state, "Forkchoice state is inconsistent, returning invalid response"); + return Ok(invalid_fcu_response); + } + + trace!(target: "consensus::engine", ?status, ?state, "Returning forkchoice status"); + Ok(OnForkChoiceUpdated::valid(status)) + } + + /// Invoked when we receive a new forkchoice update message. + /// + /// Returns `true` if the engine now reached its maximum block number, See + /// [EngineSyncController::has_reached_max_block]. + fn on_forkchoice_updated( + &mut self, + state: ForkchoiceState, + attrs: Option, + tx: oneshot::Sender>, + ) -> OnForkchoiceUpdateOutcome { + self.metrics.forkchoice_updated_messages.increment(1); + self.blockchain.on_forkchoice_update_received(&state); + + let on_updated = match self.forkchoice_updated(state, attrs) { + Ok(response) => response, + Err(error) => { + if let RethError::Execution(ref err) = error { + if err.is_fatal() { + // FCU resulted in a fatal error from which we can't recover + let err = err.clone(); + let _ = tx.send(Err(error)); + return OnForkchoiceUpdateOutcome::Fatal(err); + } + } + let _ = tx.send(Err(error)); + return OnForkchoiceUpdateOutcome::Processed; + } + }; + + let fcu_status = on_updated.forkchoice_status(); + + // update the forkchoice state tracker + self.forkchoice_state_tracker.set_latest(state, fcu_status); + + // send the response to the CL ASAP + let _ = tx.send(Ok(on_updated)); + + match fcu_status { + ForkchoiceStatus::Invalid => {} + ForkchoiceStatus::Valid => { + // FCU head is valid, we're no longer syncing + self.sync_state_updater.update_sync_state(SyncState::Idle); + // node's fully synced, clear active download requests + self.sync.clear_block_download_requests(); + + // check if we reached the maximum configured block + let tip_number = self.blockchain.canonical_tip().number; + if self.sync.has_reached_max_block(tip_number) { + // Terminate the sync early if it's reached the maximum user + // configured block. + return OnForkchoiceUpdateOutcome::ReachedMaxBlock; + } + } + ForkchoiceStatus::Syncing => { + // we're syncing + self.sync_state_updater.update_sync_state(SyncState::Syncing); + } + } + + // notify listeners about new processed FCU + self.listeners.notify(BeaconConsensusEngineEvent::ForkchoiceUpdated(state, fcu_status)); + + OnForkchoiceUpdateOutcome::Processed + } + /// Check if the pipeline is consistent (all stages have the checkpoint block numbers no less /// than the checkpoint of the first stage). /// @@ -370,7 +585,7 @@ where /// /// The [`BeaconConsensusEngineHandle`] can be used to interact with this /// [`BeaconConsensusEngine`] - pub fn handle(&self) -> BeaconConsensusEngineHandle { + pub fn handle(&self) -> BeaconConsensusEngineHandle { self.handle.clone() } @@ -554,219 +769,6 @@ where Some(status) } - /// Invoked when we receive a new forkchoice update message. - /// - /// Returns `true` if the engine now reached its maximum block number, See - /// [EngineSyncController::has_reached_max_block]. - fn on_forkchoice_updated( - &mut self, - state: ForkchoiceState, - attrs: Option, - tx: oneshot::Sender>, - ) -> OnForkchoiceUpdateOutcome { - self.metrics.forkchoice_updated_messages.increment(1); - self.blockchain.on_forkchoice_update_received(&state); - - let on_updated = match self.forkchoice_updated(state, attrs) { - Ok(response) => response, - Err(error) => { - if let RethError::Execution(ref err) = error { - if err.is_fatal() { - // FCU resulted in a fatal error from which we can't recover - let err = err.clone(); - let _ = tx.send(Err(error)); - return OnForkchoiceUpdateOutcome::Fatal(err); - } - } - let _ = tx.send(Err(error)); - return OnForkchoiceUpdateOutcome::Processed; - } - }; - - let fcu_status = on_updated.forkchoice_status(); - - // update the forkchoice state tracker - self.forkchoice_state_tracker.set_latest(state, fcu_status); - - // send the response to the CL ASAP - let _ = tx.send(Ok(on_updated)); - - match fcu_status { - ForkchoiceStatus::Invalid => {} - ForkchoiceStatus::Valid => { - // FCU head is valid, we're no longer syncing - self.sync_state_updater.update_sync_state(SyncState::Idle); - // node's fully synced, clear active download requests - self.sync.clear_block_download_requests(); - - // check if we reached the maximum configured block - let tip_number = self.blockchain.canonical_tip().number; - if self.sync.has_reached_max_block(tip_number) { - // Terminate the sync early if it's reached the maximum user - // configured block. - return OnForkchoiceUpdateOutcome::ReachedMaxBlock; - } - } - ForkchoiceStatus::Syncing => { - // we're syncing - self.sync_state_updater.update_sync_state(SyncState::Syncing); - } - } - - // notify listeners about new processed FCU - self.listeners.notify(BeaconConsensusEngineEvent::ForkchoiceUpdated(state, fcu_status)); - - OnForkchoiceUpdateOutcome::Processed - } - - /// Called to resolve chain forks and ensure that the Execution layer is working with the latest - /// valid chain. - /// - /// These responses should adhere to the [Engine API Spec for - /// `engine_forkchoiceUpdated`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#specification-1). - /// - /// Returns an error if an internal error occurred like a database error. - fn forkchoice_updated( - &mut self, - state: ForkchoiceState, - attrs: Option, - ) -> RethResult { - trace!(target: "consensus::engine", ?state, "Received new forkchoice state update"); - if state.head_block_hash.is_zero() { - return Ok(OnForkChoiceUpdated::invalid_state()); - } - - // check if the new head hash is connected to any ancestor that we previously marked as - // invalid - let lowest_buffered_ancestor_fcu = self.lowest_buffered_ancestor_or(state.head_block_hash); - if let Some(status) = self.check_invalid_ancestor(lowest_buffered_ancestor_fcu) { - return Ok(OnForkChoiceUpdated::with_invalid(status)); - } - - if self.sync.is_pipeline_active() { - // We can only process new forkchoice updates if the pipeline is idle, since it requires - // exclusive access to the database - trace!(target: "consensus::engine", "Pipeline is syncing, skipping forkchoice update"); - return Ok(OnForkChoiceUpdated::syncing()); - } - - if let Some(hook) = self.hooks.active_db_write_hook() { - // We can only process new forkchoice updates if no hook with db write is running, - // since it requires exclusive access to the database - warn!( - target: "consensus::engine", - hook = %hook.name(), - "Hook is in progress, skipping forkchoice update. \ - This may affect the performance of your node as a validator." - ); - return Ok(OnForkChoiceUpdated::syncing()); - } - - let start = Instant::now(); - let make_canonical_result = self.blockchain.make_canonical(&state.head_block_hash); - let elapsed = self.record_make_canonical_latency(start, &make_canonical_result); - - let status = match make_canonical_result { - Ok(outcome) => { - match &outcome { - CanonicalOutcome::AlreadyCanonical { header } => { - // On Optimism, the proposers are allowed to reorg their own chain at will. - cfg_if::cfg_if! { - if #[cfg(feature = "optimism")] { - if self.blockchain.chain_spec().is_optimism() { - debug!( - target: "consensus::engine", - fcu_head_num=?header.number, - current_head_num=?self.blockchain.canonical_tip().number, - "[Optimism] Allowing beacon reorg to old head" - ); - let _ = self.update_head(header.clone()); - self.listeners.notify( - BeaconConsensusEngineEvent::CanonicalChainCommitted( - Box::new(header.clone()), - elapsed, - ), - ); - } else { - debug!( - target: "consensus::engine", - fcu_head_num=?header.number, - current_head_num=?self.blockchain.canonical_tip().number, - "Ignoring beacon update to old head" - ); - } - } else { - debug!( - target: "consensus::engine", - fcu_head_num=?header.number, - current_head_num=?self.blockchain.canonical_tip().number, - "Ignoring beacon update to old head" - ); - } - } - } - CanonicalOutcome::Committed { head } => { - debug!( - target: "consensus::engine", - hash=?state.head_block_hash, - number=head.number, - "Canonicalized new head" - ); - - // new VALID update that moved the canonical chain forward - let _ = self.update_head(head.clone()); - self.listeners.notify(BeaconConsensusEngineEvent::CanonicalChainCommitted( - Box::new(head.clone()), - elapsed, - )); - } - } - - if let Some(attrs) = attrs { - // if we return early then we wouldn't perform these consistency checks, so we - // need to do them here, and should do them before we process any payload - // attributes - if let Some(invalid_fcu_response) = self.ensure_consistent_state(state)? { - trace!(target: "consensus::engine", ?state, head=?state.head_block_hash, "Forkchoice state is inconsistent, returning invalid response"); - return Ok(invalid_fcu_response); - } - - // the CL requested to build a new payload on top of this new VALID head - let payload_response = self.process_payload_attributes( - attrs, - outcome.into_header().unseal(), - state, - ); - - trace!(target: "consensus::engine", status = ?payload_response, ?state, "Returning forkchoice status"); - return Ok(payload_response); - } - - PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)) - } - Err(error) => { - if let RethError::Canonical(ref err) = error { - if err.is_fatal() { - tracing::error!(target: "consensus::engine", ?err, "Encountered fatal error"); - return Err(error); - } - } - - self.on_failed_canonical_forkchoice_update(&state, error) - } - }; - - if let Some(invalid_fcu_response) = - self.ensure_consistent_state_with_status(state, &status)? - { - trace!(target: "consensus::engine", ?status, ?state, "Forkchoice state is inconsistent, returning invalid response"); - return Ok(invalid_fcu_response); - } - - trace!(target: "consensus::engine", ?status, ?state, "Returning forkchoice status"); - Ok(OnForkChoiceUpdated::valid(status)) - } - /// Record latency metrics for one call to make a block canonical /// Takes start time of the call and result of the make canonical call /// @@ -1051,55 +1053,6 @@ where .unwrap_or_else(|| hash) } - /// Validates the payload attributes with respect to the header and fork choice state. - /// - /// Note: At this point, the fork choice update is considered to be VALID, however, we can still - /// return an error if the payload attributes are invalid. - fn process_payload_attributes( - &self, - attrs: PayloadAttributes, - head: Header, - state: ForkchoiceState, - ) -> OnForkChoiceUpdated { - // 7. Client software MUST ensure that payloadAttributes.timestamp is greater than timestamp - // of a block referenced by forkchoiceState.headBlockHash. If this condition isn't held - // client software MUST respond with -38003: `Invalid payload attributes` and MUST NOT - // begin a payload build process. In such an event, the forkchoiceState update MUST NOT - // be rolled back. - if attrs.timestamp <= head.timestamp { - return OnForkChoiceUpdated::invalid_payload_attributes(); - } - - // 8. Client software MUST begin a payload build process building on top of - // forkchoiceState.headBlockHash and identified via buildProcessId value if - // payloadAttributes is not null and the forkchoice state has been updated successfully. - // The build process is specified in the Payload building section. - match PayloadBuilderAttributes::try_new(state.head_block_hash, attrs) { - Ok(attributes) => { - // send the payload to the builder and return the receiver for the pending payload - // id, initiating payload job is handled asynchronously - let pending_payload_id = self.payload_builder.send_new_payload(attributes); - - // Client software MUST respond to this method call in the following way: - // { - // payloadStatus: { - // status: VALID, - // latestValidHash: forkchoiceState.headBlockHash, - // validationError: null - // }, - // payloadId: buildProcessId - // } - // - // if the payload is deemed VALID and the build process has begun. - OnForkChoiceUpdated::updated_with_pending_payload_id( - PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)), - pending_payload_id, - ) - } - Err(_) => OnForkChoiceUpdated::invalid_payload_attributes(), - } - } - /// When the Consensus layer receives a new block via the consensus gossip protocol, /// the transactions in the block are sent to the execution layer in the form of a /// [`ExecutionPayload`]. The Execution layer executes the transactions and validates the @@ -1224,6 +1177,58 @@ where } } + /// Validates the payload attributes with respect to the header and fork choice state. + /// + /// Note: At this point, the fork choice update is considered to be VALID, however, we can still + /// return an error if the payload attributes are invalid. + fn process_payload_attributes( + &self, + attrs: EngineT::PayloadAttributes, + head: Header, + state: ForkchoiceState, + ) -> OnForkChoiceUpdated { + // 7. Client software MUST ensure that payloadAttributes.timestamp is greater than timestamp + // of a block referenced by forkchoiceState.headBlockHash. If this condition isn't held + // client software MUST respond with -38003: `Invalid payload attributes` and MUST NOT + // begin a payload build process. In such an event, the forkchoiceState update MUST NOT + // be rolled back. + if attrs.timestamp() <= head.timestamp { + return OnForkChoiceUpdated::invalid_payload_attributes(); + } + + // 8. Client software MUST begin a payload build process building on top of + // forkchoiceState.headBlockHash and identified via buildProcessId value if + // payloadAttributes is not null and the forkchoice state has been updated successfully. + // The build process is specified in the Payload building section. + match ::try_new( + state.head_block_hash, + attrs, + ) { + Ok(attributes) => { + // send the payload to the builder and return the receiver for the pending payload + // id, initiating payload job is handled asynchronously + let pending_payload_id = self.payload_builder.send_new_payload(attributes); + + // Client software MUST respond to this method call in the following way: + // { + // payloadStatus: { + // status: VALID, + // latestValidHash: forkchoiceState.headBlockHash, + // validationError: null + // }, + // payloadId: buildProcessId + // } + // + // if the payload is deemed VALID and the build process has begun. + OnForkChoiceUpdated::updated_with_pending_payload_id( + PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)), + pending_payload_id, + ) + } + Err(_) => OnForkChoiceUpdated::invalid_payload_attributes(), + } + } + /// When the pipeline is active, the tree is unable to commit any additional blocks since the /// pipeline holds exclusive access to the database. /// @@ -1735,7 +1740,7 @@ where /// local forkchoice state, it will launch the pipeline to sync to the head hash. /// While the pipeline is syncing, the consensus engine will keep processing messages from the /// receiver and forwarding them to the blockchain tree. -impl Future for BeaconConsensusEngine +impl Future for BeaconConsensusEngine where DB: Database + Unpin + 'static, Client: HeadersClient + BodiesClient + Clone + Unpin + 'static, @@ -1747,6 +1752,7 @@ where + ChainSpecProvider + Unpin + 'static, + EngineT: EngineTypes + Unpin + 'static, { type Output = Result<(), BeaconConsensusEngineError>; diff --git a/crates/consensus/beacon/src/engine/test_utils.rs b/crates/consensus/beacon/src/engine/test_utils.rs index 8d2be2a1c..3efa1dbb3 100644 --- a/crates/consensus/beacon/src/engine/test_utils.rs +++ b/crates/consensus/beacon/src/engine/test_utils.rs @@ -22,6 +22,7 @@ use reth_interfaces::{ sync::NoopSyncStateUpdater, test_utils::{NoopFullBlockClient, TestConsensus}, }; +use reth_node_builder::EthEngineTypes; use reth_payload_builder::test_utils::spawn_test_payload_service; use reth_primitives::{BlockNumber, ChainSpec, PruneModes, Receipt, B256, U256}; use reth_provider::{ @@ -49,6 +50,7 @@ type TestBeaconConsensusEngine = BeaconConsensusEngine< >, >, Arc>, + EthEngineTypes, >; #[derive(Debug)] @@ -57,14 +59,14 @@ pub struct TestEnv { // Keep the tip receiver around, so it's not dropped. #[allow(dead_code)] tip_rx: watch::Receiver, - engine_handle: BeaconConsensusEngineHandle, + engine_handle: BeaconConsensusEngineHandle, } impl TestEnv { fn new( db: DB, tip_rx: watch::Receiver, - engine_handle: BeaconConsensusEngineHandle, + engine_handle: BeaconConsensusEngineHandle, ) -> Self { Self { db, tip_rx, engine_handle } } @@ -453,7 +455,7 @@ where } TestConsensusConfig::Test => Arc::new(TestConsensus::default()), }; - let payload_builder = spawn_test_payload_service(); + let payload_builder = spawn_test_payload_service::(); // use either noop client or a user provided client (for example TestFullBlockClient) let client = Arc::new( @@ -552,9 +554,12 @@ where } } -pub fn spawn_consensus_engine( +pub fn spawn_consensus_engine( engine: TestBeaconConsensusEngine, -) -> oneshot::Receiver> { +) -> oneshot::Receiver> +where + Client: HeadersClient + BodiesClient + 'static, +{ let (tx, rx) = oneshot::channel(); tokio::spawn(async move { let result = engine.await; diff --git a/crates/node-api/Cargo.toml b/crates/node-api/Cargo.toml new file mode 100644 index 000000000..177fa745f --- /dev/null +++ b/crates/node-api/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "reth-node-api" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +# reth +reth-primitives.workspace = true +reth-rpc-types.workspace = true +thiserror.workspace = true + +# io +serde.workspace = true + +[dev-dependencies] +# for examples +reth-payload-builder.workspace = true diff --git a/crates/node-api/src/engine/error.rs b/crates/node-api/src/engine/error.rs new file mode 100644 index 000000000..1381fa6c7 --- /dev/null +++ b/crates/node-api/src/engine/error.rs @@ -0,0 +1,41 @@ +//! Defines a payload validation error type +use thiserror::Error; + +/// Thrown when the payload or attributes are known to be invalid before processing. +#[derive(Error, Debug)] +pub enum AttributesValidationError { + /// Thrown if `PayloadAttributes` provided in engine_forkchoiceUpdated before V3 contains a + /// parent beacon block root + #[error("parent beacon block root not supported before V3")] + ParentBeaconBlockRootNotSupportedBeforeV3, + /// Thrown if engine_forkchoiceUpdatedV1 contains withdrawals + #[error("withdrawals not supported in V1")] + WithdrawalsNotSupportedInV1, + /// Thrown if engine_forkchoiceUpdated contains no withdrawals after Shanghai + #[error("no withdrawals post-Shanghai")] + NoWithdrawalsPostShanghai, + /// Thrown if engine_forkchoiceUpdated contains withdrawals before Shanghai + #[error("withdrawals pre-Shanghai")] + HasWithdrawalsPreShanghai, + /// Thrown if the `PayloadAttributes` provided in engine_forkchoiceUpdated contains no parent + /// beacon block root after Cancun + #[error("no parent beacon block root post-cancun")] + NoParentBeaconBlockRootPostCancun, + /// Thrown if `PayloadAttributes` were provided with a timestamp, but the version of the engine + /// method called is meant for a fork that occurs after the provided timestamp. + #[error("Unsupported fork")] + UnsupportedFork, + /// Another type of error that is not covered by the above variants. + #[error("Invalid params: {0}")] + InvalidParams(#[from] Box), +} + +impl AttributesValidationError { + /// Creates an instance of the `InvalidParams` variant with the given error. + pub fn invalid_params(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::InvalidParams(Box::new(error)) + } +} diff --git a/crates/node-api/src/engine/mod.rs b/crates/node-api/src/engine/mod.rs new file mode 100644 index 000000000..6948355de --- /dev/null +++ b/crates/node-api/src/engine/mod.rs @@ -0,0 +1,323 @@ +//! This contains the [EngineTypes] trait and implementations for ethereum mainnet types. +//! +//! The [EngineTypes] trait can be implemented to configure the engine to work with custom types, +//! as long as those types implement certain traits. +//! +//! Custom payload attributes can be supported by implementing two main traits: +//! +//! [PayloadAttributes] can be implemented for payload attributes types that are used as +//! arguments to the `engine_forkchoiceUpdated` method. This type should be used to define and +//! _spawn_ payload jobs. +//! +//! [PayloadBuilderAttributes] can be implemented for payload attributes types that _describe_ +//! running payload jobs. +//! +//! Once traits are implemented and custom types are defined, the [EngineTypes] trait can be +//! implemented: +//! ```no_run +//! # use reth_rpc_types::engine::{PayloadAttributes as EthPayloadAttributes, PayloadId, Withdrawal}; +//! # use reth_primitives::{B256, ChainSpec, Address}; +//! # use reth_node_api::{EngineTypes, EngineApiMessageVersion, validate_version_specific_fields, AttributesValidationError, PayloadAttributes, PayloadBuilderAttributes}; +//! # use reth_payload_builder::EthPayloadBuilderAttributes; +//! # use serde::{Deserialize, Serialize}; +//! # use thiserror::Error; +//! # use std::convert::Infallible; +//! +//! /// A custom payload attributes type. +//! #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +//! pub struct CustomPayloadAttributes { +//! /// An inner payload type +//! #[serde(flatten)] +//! pub inner: EthPayloadAttributes, +//! /// A custom field +//! pub custom: u64, +//! } +//! +//! /// Custom error type used in payload attributes validation +//! #[derive(Debug, Error)] +//! pub enum CustomError { +//! #[error("Custom field is not zero")] +//! CustomFieldIsNotZero, +//! } +//! +//! impl PayloadAttributes for CustomPayloadAttributes { +//! fn timestamp(&self) -> u64 { +//! self.inner.timestamp() +//! } +//! +//! fn withdrawals(&self) -> Option<&Vec> { +//! self.inner.withdrawals() +//! } +//! +//! fn parent_beacon_block_root(&self) -> Option { +//! self.inner.parent_beacon_block_root() +//! } +//! +//! fn ensure_well_formed_attributes( +//! &self, +//! chain_spec: &ChainSpec, +//! version: EngineApiMessageVersion, +//! ) -> Result<(), AttributesValidationError> { +//! validate_version_specific_fields(chain_spec, version, &self.into())?; +//! +//! // custom validation logic - ensure that the custom field is not zero +//! if self.custom == 0 { +//! return Err(AttributesValidationError::invalid_params( +//! CustomError::CustomFieldIsNotZero, +//! )) +//! } +//! +//! Ok(()) +//! } +//! } +//! +//! /// Newtype around the payload builder attributes type +//! #[derive(Clone, Debug, PartialEq, Eq)] +//! pub struct CustomPayloadBuilderAttributes(EthPayloadBuilderAttributes); +//! +//! impl PayloadBuilderAttributes for CustomPayloadBuilderAttributes { +//! type RpcPayloadAttributes = CustomPayloadAttributes; +//! type Error = Infallible; +//! +//! fn try_new(parent: B256, attributes: CustomPayloadAttributes) -> Result { +//! Ok(Self(EthPayloadBuilderAttributes::new(parent, attributes.inner))) +//! } +//! +//! fn parent(&self) -> B256 { +//! self.0.parent +//! } +//! +//! fn payload_id(&self) -> PayloadId { +//! self.0.id +//! } +//! +//! fn timestamp(&self) -> u64 { +//! self.0.timestamp +//! } +//! +//! fn parent_beacon_block_root(&self) -> Option { +//! self.0.parent_beacon_block_root +//! } +//! +//! fn suggested_fee_recipient(&self) -> Address { +//! self.0.suggested_fee_recipient +//! } +//! +//! fn prev_randao(&self) -> B256 { +//! self.0.prev_randao +//! } +//! +//! fn withdrawals(&self) -> &Vec { +//! &self.0.withdrawals +//! } +//! } +//! +//! /// Custom engine types - uses a custom payload attributes RPC type, but uses the default +//! /// payload builder attributes type. +//! #[derive(Clone, Debug, Default)] +//! #[non_exhaustive] +//! pub struct CustomEngineTypes; +//! +//! impl EngineTypes for CustomEngineTypes { +//! type PayloadAttributes = CustomPayloadAttributes; +//! type PayloadBuilderAttributes = CustomPayloadBuilderAttributes; +//! } +//! ``` + +use reth_primitives::{ChainSpec, Hardfork}; + +/// Contains traits to abstract over payload attributes types and default implementations of the +/// [PayloadAttributes] trait for ethereum mainnet and optimism types. +pub mod traits; +pub use traits::{PayloadAttributes, PayloadBuilderAttributes}; + +/// Contains error types used in the traits defined in this crate. +pub mod error; +pub use error::AttributesValidationError; + +/// Contains types used in implementations of the [PayloadAttributes] trait. +pub mod payload; +pub use payload::PayloadOrAttributes; + +/// The types that are used by the engine. +pub trait EngineTypes: Send + Sync { + /// The RPC payload attributes type the CL node emits via the engine API. + type PayloadAttributes: PayloadAttributes + Unpin; + + /// The payload attributes type that contains information about a running payload job. + type PayloadBuilderAttributes: PayloadBuilderAttributes + + Clone + + Unpin; + + // TODO: payload type +} + +/// Validates the timestamp depending on the version called: +/// +/// * If V2, this ensure that the payload timestamp is pre-Cancun. +/// * If V3, this ensures that the payload timestamp is within the Cancun timestamp. +/// +/// Otherwise, this will return [AttributesValidationError::UnsupportedFork]. +pub fn validate_payload_timestamp( + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + timestamp: u64, +) -> Result<(), AttributesValidationError> { + let is_cancun = chain_spec.is_cancun_active_at_timestamp(timestamp); + if version == EngineApiMessageVersion::V2 && is_cancun { + // From the Engine API spec: + // + // ### Update the methods of previous forks + // + // This document defines how Cancun payload should be handled by the [`Shanghai + // API`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md). + // + // For the following methods: + // + // - [`engine_forkchoiceUpdatedV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_forkchoiceupdatedv2) + // - [`engine_newPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_newpayloadV2) + // - [`engine_getPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_getpayloadv2) + // + // a validation **MUST** be added: + // + // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of + // payload or payloadAttributes greater or equal to the Cancun activation timestamp. + return Err(AttributesValidationError::UnsupportedFork) + } + + if version == EngineApiMessageVersion::V3 && !is_cancun { + // From the Engine API spec: + // + // + // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of + // the built payload does not fall within the time frame of the Cancun fork. + return Err(AttributesValidationError::UnsupportedFork) + } + Ok(()) +} + +/// Validates the presence of the `withdrawals` field according to the payload timestamp. +/// After Shanghai, withdrawals field must be [Some]. +/// Before Shanghai, withdrawals field must be [None]; +pub fn validate_withdrawals_presence( + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + timestamp: u64, + has_withdrawals: bool, +) -> Result<(), AttributesValidationError> { + let is_shanghai = chain_spec.fork(Hardfork::Shanghai).active_at_timestamp(timestamp); + + match version { + EngineApiMessageVersion::V1 => { + if has_withdrawals { + return Err(AttributesValidationError::WithdrawalsNotSupportedInV1) + } + if is_shanghai { + return Err(AttributesValidationError::NoWithdrawalsPostShanghai) + } + } + EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => { + if is_shanghai && !has_withdrawals { + return Err(AttributesValidationError::NoWithdrawalsPostShanghai) + } + if !is_shanghai && has_withdrawals { + return Err(AttributesValidationError::HasWithdrawalsPreShanghai) + } + } + }; + + Ok(()) +} + +/// Validate the presence of the `parentBeaconBlockRoot` field according to the payload +/// timestamp. +/// +/// After Cancun, `parentBeaconBlockRoot` field must be [Some]. +/// Before Cancun, `parentBeaconBlockRoot` field must be [None]. +/// +/// If the engine API message version is V1 or V2, and the payload attribute's timestamp is +/// post-Cancun, then this will return [AttributesValidationError::UnsupportedFork]. +/// +/// If the payload attribute's timestamp is before the Cancun fork and the engine API message +/// version is V3, then this will return [AttributesValidationError::UnsupportedFork]. +/// +/// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then +/// this will return [AttributesValidationError::NoParentBeaconBlockRootPostCancun]. +/// +/// This implements the following Engine API spec rules: +/// +/// 1. Client software **MUST** check that provided set of parameters and their fields strictly +/// matches the expected one and return `-32602: Invalid params` error if this check fails. Any +/// field having `null` value **MUST** be considered as not provided. +/// +/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `payloadAttributes` +/// is set and the `payloadAttributes.timestamp` does not fall within the time frame of the +/// Cancun fork. +pub fn validate_parent_beacon_block_root_presence( + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + timestamp: u64, + has_parent_beacon_block_root: bool, +) -> Result<(), AttributesValidationError> { + // 1. Client software **MUST** check that provided set of parameters and their fields strictly + // matches the expected one and return `-32602: Invalid params` error if this check fails. + // Any field having `null` value **MUST** be considered as not provided. + match version { + EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => { + if has_parent_beacon_block_root { + return Err(AttributesValidationError::ParentBeaconBlockRootNotSupportedBeforeV3) + } + } + EngineApiMessageVersion::V3 => { + if !has_parent_beacon_block_root { + return Err(AttributesValidationError::NoParentBeaconBlockRootPostCancun) + } + } + }; + + // 2. Client software **MUST** return `-38005: Unsupported fork` error if the + // `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the + // time frame of the Cancun fork. + validate_payload_timestamp(chain_spec, version, timestamp)?; + + Ok(()) +} + +/// Validates the presence or exclusion of fork-specific fields based on the payload attributes +/// and the message version. +pub fn validate_version_specific_fields( + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + payload_or_attrs: &PayloadOrAttributes<'_, Type>, +) -> Result<(), AttributesValidationError> +where + Type: PayloadAttributes, +{ + validate_withdrawals_presence( + chain_spec, + version, + payload_or_attrs.timestamp(), + payload_or_attrs.withdrawals().is_some(), + )?; + validate_parent_beacon_block_root_presence( + chain_spec, + version, + payload_or_attrs.timestamp(), + payload_or_attrs.parent_beacon_block_root().is_some(), + ) +} + +/// The version of Engine API message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EngineApiMessageVersion { + /// Version 1 + V1, + /// Version 2 + /// + /// Added for shanghai hardfork. + V2, + /// Version 3 + /// + /// Added for cancun hardfork. + V3, +} diff --git a/crates/rpc/rpc-engine-api/src/payload.rs b/crates/node-api/src/engine/payload.rs similarity index 60% rename from crates/rpc/rpc-engine-api/src/payload.rs rename to crates/node-api/src/engine/payload.rs index 00fee7938..f212486ab 100644 --- a/crates/rpc/rpc-engine-api/src/payload.rs +++ b/crates/node-api/src/engine/payload.rs @@ -1,8 +1,10 @@ +use crate::PayloadAttributes; use reth_primitives::B256; +use reth_rpc_types::engine::ExecutionPayload; -use reth_rpc_types::engine::{ExecutionPayload, PayloadAttributes}; -/// Either an [ExecutionPayload] or a [PayloadAttributes]. -pub(crate) enum PayloadOrAttributes<'a> { +/// Either an [ExecutionPayload] or a types that implements the [PayloadAttributes] trait. +#[derive(Debug)] +pub enum PayloadOrAttributes<'a, AttributesType> { /// An [ExecutionPayload] and optional parent beacon block root. ExecutionPayload { /// The inner execution payload @@ -10,14 +12,17 @@ pub(crate) enum PayloadOrAttributes<'a> { /// The parent beacon block root parent_beacon_block_root: Option, }, - /// A [PayloadAttributes]. - PayloadAttributes(&'a PayloadAttributes), + /// A payload attributes type. + PayloadAttributes(&'a AttributesType), } -impl<'a> PayloadOrAttributes<'a> { +impl<'a, AttributesType> PayloadOrAttributes<'a, AttributesType> +where + AttributesType: PayloadAttributes, +{ /// Construct a [PayloadOrAttributes] from an [ExecutionPayload] and optional parent beacon /// block root. - pub(crate) fn from_execution_payload( + pub fn from_execution_payload( payload: &'a ExecutionPayload, parent_beacon_block_root: Option, ) -> Self { @@ -25,32 +30,35 @@ impl<'a> PayloadOrAttributes<'a> { } /// Return the withdrawals for the payload or attributes. - pub(crate) fn withdrawals(&self) -> Option<&Vec> { + pub fn withdrawals(&self) -> Option<&Vec> { match self { Self::ExecutionPayload { payload, .. } => payload.withdrawals(), - Self::PayloadAttributes(attributes) => attributes.withdrawals.as_ref(), + Self::PayloadAttributes(attributes) => attributes.withdrawals(), } } /// Return the timestamp for the payload or attributes. - pub(crate) fn timestamp(&self) -> u64 { + pub fn timestamp(&self) -> u64 { match self { Self::ExecutionPayload { payload, .. } => payload.timestamp(), - Self::PayloadAttributes(attributes) => attributes.timestamp, + Self::PayloadAttributes(attributes) => attributes.timestamp(), } } /// Return the parent beacon block root for the payload or attributes. - pub(crate) fn parent_beacon_block_root(&self) -> Option { + pub fn parent_beacon_block_root(&self) -> Option { match self { Self::ExecutionPayload { parent_beacon_block_root, .. } => *parent_beacon_block_root, - Self::PayloadAttributes(attributes) => attributes.parent_beacon_block_root, + Self::PayloadAttributes(attributes) => attributes.parent_beacon_block_root(), } } } -impl<'a> From<&'a PayloadAttributes> for PayloadOrAttributes<'a> { - fn from(attributes: &'a PayloadAttributes) -> Self { +impl<'a, AttributesType> From<&'a AttributesType> for PayloadOrAttributes<'a, AttributesType> +where + AttributesType: PayloadAttributes, +{ + fn from(attributes: &'a AttributesType) -> Self { Self::PayloadAttributes(attributes) } } diff --git a/crates/node-api/src/engine/traits.rs b/crates/node-api/src/engine/traits.rs new file mode 100644 index 000000000..fd8c87203 --- /dev/null +++ b/crates/node-api/src/engine/traits.rs @@ -0,0 +1,189 @@ +use crate::{validate_version_specific_fields, AttributesValidationError, EngineApiMessageVersion}; +use reth_primitives::{ + revm::config::revm_spec_by_timestamp_after_merge, + revm_primitives::{BlobExcessGasAndPrice, BlockEnv, CfgEnv, SpecId}, + Address, ChainSpec, Header, B256, U256, +}; +use reth_rpc_types::engine::{ + OptimismPayloadAttributes, PayloadAttributes as EthPayloadAttributes, PayloadId, Withdrawal, +}; + +/// This can be implemented by types that describe a currently running payload job. +/// +/// This is used as a conversion type, transforming a payload attributes type that the engine API +/// receives, into a type that the payload builder can use. +pub trait PayloadBuilderAttributes: Send + Sync + std::fmt::Debug { + /// The payload attributes that can be used to construct this type. Used as the argument in + /// [PayloadBuilderAttributes::try_new]. + type RpcPayloadAttributes; + /// The error type used in [PayloadBuilderAttributes::try_new]. + type Error: std::error::Error; + + /// Creates a new payload builder for the given parent block and the attributes. + /// + /// Derives the unique [PayloadId] for the given parent and attributes + fn try_new( + parent: B256, + rpc_payload_attributes: Self::RpcPayloadAttributes, + ) -> Result + where + Self: Sized; + + /// Returns the [PayloadId] for the running payload job. + fn payload_id(&self) -> PayloadId; + + /// Returns the parent block hash for the running payload job. + fn parent(&self) -> B256; + + /// Returns the timestmap for the running payload job. + fn timestamp(&self) -> u64; + + /// Returns the parent beacon block root for the running payload job, if it exists. + fn parent_beacon_block_root(&self) -> Option; + + /// Returns the suggested fee recipient for the running payload job. + fn suggested_fee_recipient(&self) -> Address; + + /// Returns the prevrandao field for the running payload job. + fn prev_randao(&self) -> B256; + + /// Returns the withdrawals for the running payload job. + fn withdrawals(&self) -> &Vec; + + /// Returns the configured [CfgEnv] and [BlockEnv] for the targeted payload (that has the + /// `parent` as its parent). + /// + /// The `chain_spec` is used to determine the correct chain id and hardfork for the payload + /// based on its timestamp. + /// + /// Block related settings are derived from the `parent` block and the configured attributes. + /// + /// NOTE: This is only intended for beacon consensus (after merge). + fn cfg_and_block_env(&self, chain_spec: &ChainSpec, parent: &Header) -> (CfgEnv, BlockEnv) { + // TODO: should be different once revm has configurable cfgenv + // configure evm env based on parent block + let mut cfg = CfgEnv::default(); + cfg.chain_id = chain_spec.chain().id(); + + #[cfg(feature = "optimism")] + { + cfg.optimism = chain_spec.is_optimism(); + } + + // ensure we're not missing any timestamp based hardforks + cfg.spec_id = revm_spec_by_timestamp_after_merge(chain_spec, self.timestamp()); + + // if the parent block did not have excess blob gas (i.e. it was pre-cancun), but it is + // cancun now, we need to set the excess blob gas to the default value + let blob_excess_gas_and_price = parent + .next_block_excess_blob_gas() + .map_or_else( + || { + if cfg.spec_id == SpecId::CANCUN { + // default excess blob gas is zero + Some(0) + } else { + None + } + }, + Some, + ) + .map(BlobExcessGasAndPrice::new); + + let block_env = BlockEnv { + number: U256::from(parent.number + 1), + coinbase: self.suggested_fee_recipient(), + timestamp: U256::from(self.timestamp()), + difficulty: U256::ZERO, + prevrandao: Some(self.prev_randao()), + gas_limit: U256::from(parent.gas_limit), + // calculate basefee based on parent block's gas usage + basefee: U256::from( + parent + .next_block_base_fee(chain_spec.base_fee_params(self.timestamp())) + .unwrap_or_default(), + ), + // calculate excess gas based on parent block's blob gas usage + blob_excess_gas_and_price, + }; + + (cfg, block_env) + } +} + +/// The execution payload attribute type the CL node emits via the engine API. +/// This trait should be implemented by types that could be used to spawn a payload job. +/// +/// This type is emitted as part of the forkchoiceUpdated call +pub trait PayloadAttributes: + serde::de::DeserializeOwned + serde::Serialize + std::fmt::Debug + Clone + Send + Sync + 'static +{ + /// Returns the timestamp to be used in the payload job. + fn timestamp(&self) -> u64; + + /// Returns the withdrawals for the given payload attributes. + fn withdrawals(&self) -> Option<&Vec>; + + /// Return the parent beacon block root for the payload attributes. + fn parent_beacon_block_root(&self) -> Option; + + /// Ensures that the payload attributes are valid for the given [ChainSpec] and + /// [EngineApiMessageVersion]. + fn ensure_well_formed_attributes( + &self, + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + ) -> Result<(), AttributesValidationError>; +} + +impl PayloadAttributes for EthPayloadAttributes { + fn timestamp(&self) -> u64 { + self.timestamp + } + + fn withdrawals(&self) -> Option<&Vec> { + self.withdrawals.as_ref() + } + + fn parent_beacon_block_root(&self) -> Option { + self.parent_beacon_block_root + } + + fn ensure_well_formed_attributes( + &self, + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + ) -> Result<(), AttributesValidationError> { + validate_version_specific_fields(chain_spec, version, &self.into()) + } +} + +impl PayloadAttributes for OptimismPayloadAttributes { + fn timestamp(&self) -> u64 { + self.payload_attributes.timestamp + } + + fn withdrawals(&self) -> Option<&Vec> { + self.payload_attributes.withdrawals.as_ref() + } + + fn parent_beacon_block_root(&self) -> Option { + self.payload_attributes.parent_beacon_block_root + } + + fn ensure_well_formed_attributes( + &self, + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + ) -> Result<(), AttributesValidationError> { + validate_version_specific_fields(chain_spec, version, &self.into())?; + + if self.gas_limit.is_none() && chain_spec.is_optimism() { + return Err(AttributesValidationError::InvalidParams( + "MissingGasLimitInPayloadAttributes".to_string().into(), + )) + } + + Ok(()) + } +} diff --git a/crates/node-api/src/lib.rs b/crates/node-api/src/lib.rs new file mode 100644 index 000000000..5a4127d69 --- /dev/null +++ b/crates/node-api/src/lib.rs @@ -0,0 +1,18 @@ +//! Standalone crate for Reth configuration traits and builder types. + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", + html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", + issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +/// Traits, validation methods, and helper types used to abstract over engine types. +/// +/// Notably contains the [EngineTypes] trait and implementations for ethereum mainnet types. +pub mod engine; +pub use engine::{ + validate_payload_timestamp, validate_version_specific_fields, validate_withdrawals_presence, + AttributesValidationError, EngineApiMessageVersion, EngineTypes, PayloadAttributes, + PayloadBuilderAttributes, PayloadOrAttributes, +}; diff --git a/crates/node-builder/Cargo.toml b/crates/node-builder/Cargo.toml new file mode 100644 index 000000000..8ce1972be --- /dev/null +++ b/crates/node-builder/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "reth-node-builder" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +# reth +reth-primitives.workspace = true +reth-payload-builder.workspace = true +reth-rpc-types.workspace = true +reth-node-api.workspace = true + +# io +serde.workspace = true diff --git a/crates/node-builder/src/engine.rs b/crates/node-builder/src/engine.rs new file mode 100644 index 000000000..cc7c578a0 --- /dev/null +++ b/crates/node-builder/src/engine.rs @@ -0,0 +1,23 @@ +use reth_node_api::EngineTypes; +use reth_payload_builder::{EthPayloadBuilderAttributes, OptimismPayloadBuilderAttributes}; +use reth_rpc_types::engine::{OptimismPayloadAttributes, PayloadAttributes}; + +/// The types used in the default mainnet ethereum beacon consensus engine. +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct EthEngineTypes; + +impl EngineTypes for EthEngineTypes { + type PayloadAttributes = PayloadAttributes; + type PayloadBuilderAttributes = EthPayloadBuilderAttributes; +} + +/// The types used in the optimism beacon consensus engine. +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct OptimismEngineTypes; + +impl EngineTypes for OptimismEngineTypes { + type PayloadAttributes = OptimismPayloadAttributes; + type PayloadBuilderAttributes = OptimismPayloadBuilderAttributes; +} diff --git a/crates/node-builder/src/lib.rs b/crates/node-builder/src/lib.rs new file mode 100644 index 000000000..848101d52 --- /dev/null +++ b/crates/node-builder/src/lib.rs @@ -0,0 +1,13 @@ +//! Standalone crate for Reth configuration and builder types. + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", + html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", + issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +/// Exports commonly used concrete instances of the [EngineTypes](reth_node_api::EngineTypes) +/// trait. +pub mod engine; +pub use engine::{EthEngineTypes, OptimismEngineTypes}; diff --git a/crates/payload/basic/Cargo.toml b/crates/payload/basic/Cargo.toml index 374a6e36e..00ca2432c 100644 --- a/crates/payload/basic/Cargo.toml +++ b/crates/payload/basic/Cargo.toml @@ -20,6 +20,7 @@ reth-provider.workspace = true reth-payload-builder.workspace = true reth-tasks.workspace = true reth-interfaces.workspace = true +reth-node-api.workspace = true # ethereum alloy-rlp.workspace = true diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index c540b1065..8c035ba3c 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -7,13 +7,15 @@ )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use crate::metrics::PayloadBuilderMetrics; use alloy_rlp::Encodable; use futures_core::ready; use futures_util::FutureExt; use reth_interfaces::RethResult; +use reth_node_api::PayloadBuilderAttributes; use reth_payload_builder::{ database::CachedReads, error::PayloadBuilderError, BuiltPayload, KeepPayloadJobAlive, - PayloadBuilderAttributes, PayloadId, PayloadJob, PayloadJobGenerator, + PayloadId, PayloadJob, PayloadJobGenerator, }; use reth_primitives::{ bytes::BytesMut, @@ -52,8 +54,6 @@ use tokio::{ }; use tracing::{debug, trace, warn}; -use crate::metrics::PayloadBuilderMetrics; - mod metrics; /// The [`PayloadJobGenerator`] that creates [`BasicPayloadJob`]s. @@ -153,27 +153,28 @@ where Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + Unpin + 'static, Builder: PayloadBuilder + Unpin + 'static, + >::Attributes: Unpin + Clone, { type Job = BasicPayloadJob; fn new_payload_job( &self, - attributes: PayloadBuilderAttributes, + attributes: ::PayloadAttributes, ) -> Result { - let parent_block = if attributes.parent.is_zero() { + let parent_block = if attributes.parent().is_zero() { // use latest block if parent is zero: genesis block self.client .block_by_number_or_tag(BlockNumberOrTag::Latest)? - .ok_or_else(|| PayloadBuilderError::MissingParentBlock(attributes.parent))? + .ok_or_else(|| PayloadBuilderError::MissingParentBlock(attributes.parent()))? .seal_slow() } else { let block = self .client - .find_block_by_hash(attributes.parent, BlockSource::Any)? - .ok_or_else(|| PayloadBuilderError::MissingParentBlock(attributes.parent))?; + .find_block_by_hash(attributes.parent(), BlockSource::Any)? + .ok_or_else(|| PayloadBuilderError::MissingParentBlock(attributes.parent()))?; // we already know the hash, so we can seal it - block.seal(attributes.parent) + block.seal(attributes.parent()) }; let config = PayloadConfig::new( @@ -183,7 +184,7 @@ where Arc::clone(&self.chain_spec), ); - let until = self.job_deadline(config.attributes.timestamp); + let until = self.job_deadline(config.attributes.timestamp()); let deadline = Box::pin(tokio::time::sleep_until(until)); let cached_reads = self.maybe_pre_cached(config.parent_block.hash()); @@ -325,9 +326,12 @@ impl Default for BasicPayloadJobGeneratorConfig { /// A basic payload job that continuously builds a payload with the best transactions from the pool. #[derive(Debug)] -pub struct BasicPayloadJob { +pub struct BasicPayloadJob +where + Builder: PayloadBuilder, +{ /// The configuration for how the payload will be created. - config: PayloadConfig, + config: PayloadConfig, /// The client that can interact with the chain. client: Client, /// The transaction pool. @@ -363,6 +367,7 @@ where Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + 'static, Builder: PayloadBuilder + Unpin + 'static, + >::Attributes: Unpin + Clone, { type Output = Result<(), PayloadBuilderError>; @@ -452,7 +457,9 @@ where Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + 'static, Builder: PayloadBuilder + Unpin + 'static, + >::Attributes: PayloadBuilderAttributes + Unpin + Clone, { + type PayloadAttributes = Builder::Attributes; type ResolvePayloadFuture = ResolveBestPayload; fn best_payload(&self) -> Result, PayloadBuilderError> { @@ -469,7 +476,7 @@ where build_empty_payload(&self.client, self.config.clone()).map(Arc::new) } - fn payload_attributes(&self) -> Result { + fn payload_attributes(&self) -> Result { Ok(self.config.attributes.clone()) } @@ -490,6 +497,9 @@ where best_payload: None, }; + // TODO: create optimism payload job, that wraps this type, that implements PayloadJob + // with this branch. remove this branch from the non-op code. remove + // `on_missing_payload` requirement from builder trait if let Some(payload) = self.builder.on_missing_payload(args) { return ( ResolveBestPayload { best_payload: Some(payload), maybe_better, empty_payload }, @@ -618,7 +628,7 @@ impl Drop for Cancelled { /// Static config for how to build a payload. #[derive(Clone, Debug)] -pub struct PayloadConfig { +pub struct PayloadConfig { /// Pre-configured block environment. pub initialized_block_env: BlockEnv, /// Configuration for the environment. @@ -628,29 +638,27 @@ pub struct PayloadConfig { /// Block extra data. pub extra_data: Bytes, /// Requested attributes for the payload. - pub attributes: PayloadBuilderAttributes, + pub attributes: Attributes, /// The chain spec. pub chain_spec: Arc, } -impl PayloadConfig { +impl PayloadConfig { /// Returns an owned instance of the [PayloadConfig]'s extra_data bytes. pub fn extra_data(&self) -> Bytes { self.extra_data.clone() } - - /// Returns the payload id. - pub fn payload_id(&self) -> PayloadId { - self.attributes.id - } } -impl PayloadConfig { +impl PayloadConfig +where + Attributes: PayloadBuilderAttributes, +{ /// Create new payload config. pub fn new( parent_block: Arc, extra_data: Bytes, - attributes: PayloadBuilderAttributes, + attributes: Attributes, chain_spec: Arc, ) -> Self { // configure evm env based on parent block @@ -666,6 +674,11 @@ impl PayloadConfig { chain_spec, } } + + /// Returns the payload id. + pub fn payload_id(&self) -> PayloadId { + self.attributes.payload_id() + } } /// The possible outcomes of a payload building attempt. @@ -695,7 +708,7 @@ pub enum BuildOutcome { /// building process. It holds references to the Ethereum client, transaction pool, cached reads, /// payload configuration, cancellation status, and the best payload achieved so far. #[derive(Debug)] -pub struct BuildArguments { +pub struct BuildArguments { /// How to interact with the chain. pub client: Client, /// The transaction pool. @@ -703,20 +716,20 @@ pub struct BuildArguments { /// Previously cached disk reads pub cached_reads: CachedReads, /// How to configure the payload. - pub config: PayloadConfig, + pub config: PayloadConfig, /// A marker that can be used to cancel the job. pub cancel: Cancelled, /// The best payload achieved so far. pub best_payload: Option>, } -impl BuildArguments { +impl BuildArguments { /// Create new build arguments. pub fn new( client: Client, pool: Pool, cached_reads: CachedReads, - config: PayloadConfig, + config: PayloadConfig, cancel: Cancelled, best_payload: Option>, ) -> Self { @@ -733,6 +746,9 @@ impl BuildArguments { /// Generic parameters `Pool` and `Client` represent the transaction pool and /// Ethereum client types. pub trait PayloadBuilder: Send + Sync + Clone { + /// The payload attributes type to accept for building. + type Attributes: PayloadBuilderAttributes + Send + Sync + std::fmt::Debug; + /// Tries to build a transaction payload using provided arguments. /// /// Constructs a transaction payload based on the given arguments, @@ -747,7 +763,7 @@ pub trait PayloadBuilder: Send + Sync + Clone { /// A `Result` indicating the build outcome or an error. fn try_build( &self, - args: BuildArguments, + args: BuildArguments, ) -> Result; /// Invoked when the payload job is being resolved and there is no payload yet. @@ -755,19 +771,23 @@ pub trait PayloadBuilder: Send + Sync + Clone { /// If this returns a payload, it will be used as the final payload for the job. /// /// TODO(mattsse): This needs to be refined a bit because this only exists for OP atm - fn on_missing_payload(&self, args: BuildArguments) -> Option> { + fn on_missing_payload( + &self, + args: BuildArguments, + ) -> Option> { let _args = args; None } } /// Builds an empty payload without any transactions. -fn build_empty_payload( +fn build_empty_payload( client: &Client, - config: PayloadConfig, + config: PayloadConfig, ) -> Result where Client: StateProviderFactory, + Attributes: PayloadBuilderAttributes, { let extra_data = config.extra_data(); let PayloadConfig { @@ -808,7 +828,7 @@ where })?; let WithdrawalsOutcome { withdrawals_root, withdrawals } = - commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, attributes.withdrawals).map_err(|err| { + commit_withdrawals(&mut db, &chain_spec, attributes.timestamp(), attributes.withdrawals().clone()).map_err(|err| { warn!(target: "payload_builder", parent_hash=%parent_block.hash,?err, "failed to commit withdrawals for empty payload"); err })?; @@ -834,8 +854,8 @@ where withdrawals_root, receipts_root: EMPTY_RECEIPTS, logs_bloom: Default::default(), - timestamp: attributes.timestamp, - mix_hash: attributes.prev_randao, + timestamp: attributes.timestamp(), + mix_hash: attributes.prev_randao(), nonce: BEACON_NONCE, base_fee_per_gas: Some(base_fee), number: parent_block.number + 1, @@ -845,13 +865,13 @@ where extra_data, blob_gas_used: None, excess_blob_gas: None, - parent_beacon_block_root: attributes.parent_beacon_block_root, + parent_beacon_block_root: attributes.parent_beacon_block_root(), }; let block = Block { header, body: vec![], ommers: vec![], withdrawals }; let sealed_block = block.seal_slow(); - Ok(BuiltPayload::new(attributes.id, sealed_block, U256::ZERO)) + Ok(BuiltPayload::new(attributes.payload_id(), sealed_block, U256::ZERO)) } /// Represents the outcome of committing withdrawals to the runtime database and post state. @@ -919,16 +939,17 @@ pub fn commit_withdrawals>( /// /// This uses [apply_beacon_root_contract_call] to ultimately apply the beacon root contract state /// change. -pub fn pre_block_beacon_root_contract_call( +pub fn pre_block_beacon_root_contract_call( db: &mut DB, chain_spec: &ChainSpec, block_number: u64, initialized_cfg: &CfgEnv, initialized_block_env: &BlockEnv, - attributes: &PayloadBuilderAttributes, + attributes: &Attributes, ) -> Result<(), PayloadBuilderError> where DB::Error: std::fmt::Display, + Attributes: PayloadBuilderAttributes, { // Configure the environment for the block. let env = Env { @@ -944,9 +965,9 @@ where // initialize a block from the env, because the pre block call needs the block itself apply_beacon_root_contract_call( chain_spec, - attributes.timestamp, + attributes.timestamp(), block_number, - attributes.parent_beacon_block_root, + attributes.parent_beacon_block_root(), &mut evm_pre_block, ) .map_err(|err| PayloadBuilderError::Internal(err.into())) diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index b780acc2d..26c9fae1e 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -20,6 +20,7 @@ reth-interfaces.workspace = true reth-rpc-types-compat.workspace = true reth-provider.workspace = true reth-tasks.workspace = true +reth-node-api.workspace = true # ethereum alloy-rlp.workspace = true @@ -29,6 +30,7 @@ revm-primitives.workspace = true tokio = { workspace = true, features = ["sync"] } tokio-stream.workspace = true futures-util.workspace = true +async-trait.workspace = true # metrics reth-metrics.workspace = true @@ -39,6 +41,9 @@ thiserror.workspace = true sha2 = { version = "0.10", default-features = false } tracing.workspace = true +# serde, for traits +serde.workspace = true + [dev-dependencies] revm.workspace = true diff --git a/crates/payload/builder/src/lib.rs b/crates/payload/builder/src/lib.rs index 0aae50e49..ee1f4ab95 100644 --- a/crates/payload/builder/src/lib.rs +++ b/crates/payload/builder/src/lib.rs @@ -28,7 +28,7 @@ //! use std::pin::Pin; //! use std::sync::Arc; //! use std::task::{Context, Poll}; -//! use reth_payload_builder::{BuiltPayload, KeepPayloadJobAlive, PayloadBuilderAttributes, PayloadJob, PayloadJobGenerator}; +//! use reth_payload_builder::{BuiltPayload, KeepPayloadJobAlive, EthPayloadBuilderAttributes, PayloadJob, PayloadJobGenerator}; //! use reth_payload_builder::error::PayloadBuilderError; //! use reth_primitives::{Block, Header, U256}; //! @@ -39,7 +39,7 @@ //! type Job = EmptyBlockPayloadJob; //! //! /// This is invoked when the node receives payload attributes from the beacon node via `engine_forkchoiceUpdatedV1` -//! fn new_payload_job(&self, attr: PayloadBuilderAttributes) -> Result { +//! fn new_payload_job(&self, attr: EthPayloadBuilderAttributes) -> Result { //! Ok(EmptyBlockPayloadJob{ attributes: attr,}) //! } //! @@ -47,10 +47,11 @@ //! //! /// A [PayloadJob] that builds empty blocks. //! pub struct EmptyBlockPayloadJob { -//! attributes: PayloadBuilderAttributes, +//! attributes: EthPayloadBuilderAttributes, //! } //! //! impl PayloadJob for EmptyBlockPayloadJob { +//! type PayloadAttributes = EthPayloadBuilderAttributes; //! type ResolvePayloadFuture = futures_util::future::Ready, PayloadBuilderError>>; //! //! fn best_payload(&self) -> Result, PayloadBuilderError> { @@ -68,7 +69,7 @@ //! Ok(Arc::new(payload)) //! } //! -//! fn payload_attributes(&self) -> Result { +//! fn payload_attributes(&self) -> Result { //! Ok(self.attributes.clone()) //! } //! @@ -111,7 +112,7 @@ pub mod noop; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; -pub use payload::{BuiltPayload, PayloadBuilderAttributes}; +pub use payload::{BuiltPayload, EthPayloadBuilderAttributes, OptimismPayloadBuilderAttributes}; pub use reth_rpc_types::engine::PayloadId; pub use service::{PayloadBuilderHandle, PayloadBuilderService, PayloadStore}; pub use traits::{KeepPayloadJobAlive, PayloadJob, PayloadJobGenerator}; diff --git a/crates/payload/builder/src/noop.rs b/crates/payload/builder/src/noop.rs index 805244f2f..b9d91215d 100644 --- a/crates/payload/builder/src/noop.rs +++ b/crates/payload/builder/src/noop.rs @@ -2,6 +2,7 @@ use crate::{service::PayloadServiceCommand, PayloadBuilderHandle}; use futures_util::{ready, StreamExt}; +use reth_node_api::{EngineTypes, PayloadBuilderAttributes}; use std::{ future::Future, pin::Pin, @@ -12,21 +13,27 @@ use tokio_stream::wrappers::UnboundedReceiverStream; /// A service task that does not build any payloads. #[derive(Debug)] -pub struct NoopPayloadBuilderService { +pub struct NoopPayloadBuilderService { /// Receiver half of the command channel. - command_rx: UnboundedReceiverStream, + command_rx: UnboundedReceiverStream>, } -impl NoopPayloadBuilderService { +impl NoopPayloadBuilderService +where + Engine: EngineTypes, +{ /// Creates a new [NoopPayloadBuilderService]. - pub fn new() -> (Self, PayloadBuilderHandle) { + pub fn new() -> (Self, PayloadBuilderHandle) { let (service_tx, command_rx) = mpsc::unbounded_channel(); let handle = PayloadBuilderHandle::new(service_tx); (Self { command_rx: UnboundedReceiverStream::new(command_rx) }, handle) } } -impl Future for NoopPayloadBuilderService { +impl Future for NoopPayloadBuilderService +where + Engine: EngineTypes, +{ type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { diff --git a/crates/payload/builder/src/payload.rs b/crates/payload/builder/src/payload.rs index da436d911..54143c6c3 100644 --- a/crates/payload/builder/src/payload.rs +++ b/crates/payload/builder/src/payload.rs @@ -1,24 +1,24 @@ //! Contains types required for building a payload. +use std::convert::Infallible; + use alloy_rlp::{Encodable, Error as DecodeError}; +use reth_node_api::PayloadBuilderAttributes; use reth_primitives::{ revm::config::revm_spec_by_timestamp_after_merge, revm_primitives::{BlobExcessGasAndPrice, BlockEnv, CfgEnv, SpecId}, - Address, BlobTransactionSidecar, ChainSpec, Header, SealedBlock, Withdrawal, B256, U256, + Address, BlobTransactionSidecar, ChainSpec, Header, SealedBlock, TransactionSigned, Withdrawal, + B256, U256, }; use reth_rpc_types::engine::{ - ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadV1, PayloadAttributes, - PayloadId, + ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadV1, + OptimismPayloadAttributes, PayloadAttributes, PayloadId, }; - use reth_rpc_types_compat::engine::payload::{ block_to_payload_v3, convert_block_to_payload_field_v2, convert_standalone_withdraw_to_withdrawal, try_block_to_payload_v1, }; -#[cfg(feature = "optimism")] -use reth_primitives::TransactionSigned; - /// Contains the built payload. /// /// According to the [engine API specification](https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md) the execution layer should build the initial version of the payload with an empty transaction set and then keep update it in order to maximize the revenue. @@ -123,7 +123,7 @@ impl From for ExecutionPayloadEnvelopeV3 { /// Container type for all components required to build a payload. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct PayloadBuilderAttributes { +pub struct EthPayloadBuilderAttributes { /// Id of the payload pub id: PayloadId, /// Parent block to build the payload on top @@ -140,71 +140,11 @@ pub struct PayloadBuilderAttributes { pub withdrawals: Vec, /// Root of the parent beacon block pub parent_beacon_block_root: Option, - /// Optimism Payload Builder Attributes - #[cfg(feature = "optimism")] - pub optimism_payload_attributes: OptimismPayloadBuilderAttributes, } -/// Optimism Payload Builder Attributes -#[cfg(feature = "optimism")] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OptimismPayloadBuilderAttributes { - /// NoTxPool option for the generated payload - pub no_tx_pool: bool, - /// Transactions for the generated payload - pub transactions: Vec, - /// The gas limit for the generated payload - pub gas_limit: Option, -} +// === impl EthPayloadBuilderAttributes === -// === impl PayloadBuilderAttributes === - -impl PayloadBuilderAttributes { - /// Creates a new payload builder for the given parent block and the attributes. - /// - /// Derives the unique [PayloadId] for the given parent and attributes - pub fn try_new(parent: B256, attributes: PayloadAttributes) -> Result { - #[cfg(not(feature = "optimism"))] - let id = payload_id(&parent, &attributes); - - #[cfg(feature = "optimism")] - let (id, transactions) = { - let transactions: Vec<_> = attributes - .optimism_payload_attributes - .transactions - .as_deref() - .unwrap_or(&[]) - .iter() - .map(|tx| TransactionSigned::decode_enveloped(&mut tx.as_ref())) - .collect::>()?; - (payload_id(&parent, &attributes, &transactions), transactions) - }; - - let withdraw = attributes.withdrawals.map( - |withdrawals: Vec| { - withdrawals - .into_iter() - .map(convert_standalone_withdraw_to_withdrawal) // Removed the parentheses here - .collect::>() - }, - ); - - Ok(Self { - id, - parent, - timestamp: attributes.timestamp, - suggested_fee_recipient: attributes.suggested_fee_recipient, - prev_randao: attributes.prev_randao, - withdrawals: withdraw.unwrap_or_default(), - parent_beacon_block_root: attributes.parent_beacon_block_root, - #[cfg(feature = "optimism")] - optimism_payload_attributes: OptimismPayloadBuilderAttributes { - no_tx_pool: attributes.optimism_payload_attributes.no_tx_pool.unwrap_or_default(), - transactions, - gas_limit: attributes.optimism_payload_attributes.gas_limit, - }, - }) - } +impl EthPayloadBuilderAttributes { /// Returns the configured [CfgEnv] and [BlockEnv] for the targeted payload (that has the /// `parent` as its parent). /// @@ -268,16 +208,205 @@ impl PayloadBuilderAttributes { pub fn payload_id(&self) -> PayloadId { self.id } + + /// Creates a new payload builder for the given parent block and the attributes. + /// + /// Derives the unique [PayloadId] for the given parent and attributes + pub fn new(parent: B256, attributes: PayloadAttributes) -> Self { + let id = payload_id(&parent, &attributes); + + let withdraw = attributes.withdrawals.map( + |withdrawals: Vec| { + withdrawals + .into_iter() + .map(convert_standalone_withdraw_to_withdrawal) // Removed the parentheses here + .collect::>() + }, + ); + + Self { + id, + parent, + timestamp: attributes.timestamp, + suggested_fee_recipient: attributes.suggested_fee_recipient, + prev_randao: attributes.prev_randao, + withdrawals: withdraw.unwrap_or_default(), + parent_beacon_block_root: attributes.parent_beacon_block_root, + } + } } -/// Generates the payload id for the configured payload +impl PayloadBuilderAttributes for EthPayloadBuilderAttributes { + type RpcPayloadAttributes = PayloadAttributes; + type Error = Infallible; + + /// Creates a new payload builder for the given parent block and the attributes. + /// + /// Derives the unique [PayloadId] for the given parent and attributes + fn try_new(parent: B256, attributes: PayloadAttributes) -> Result { + Ok(Self::new(parent, attributes)) + } + + fn parent(&self) -> B256 { + self.parent + } + + fn payload_id(&self) -> PayloadId { + self.id + } + + fn timestamp(&self) -> u64 { + self.timestamp + } + + fn parent_beacon_block_root(&self) -> Option { + self.parent_beacon_block_root + } + + fn suggested_fee_recipient(&self) -> Address { + self.suggested_fee_recipient + } + + fn prev_randao(&self) -> B256 { + self.prev_randao + } + + fn withdrawals(&self) -> &Vec { + &self.withdrawals + } +} + +/// Optimism Payload Builder Attributes +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OptimismPayloadBuilderAttributes { + /// Inner ethereum payload builder attributes + pub payload_attributes: EthPayloadBuilderAttributes, + /// NoTxPool option for the generated payload + pub no_tx_pool: bool, + /// Transactions for the generated payload + pub transactions: Vec, + /// The gas limit for the generated payload + pub gas_limit: Option, +} + +impl PayloadBuilderAttributes for OptimismPayloadBuilderAttributes { + type RpcPayloadAttributes = OptimismPayloadAttributes; + type Error = DecodeError; + + /// Creates a new payload builder for the given parent block and the attributes. + /// + /// Derives the unique [PayloadId] for the given parent and attributes + fn try_new(parent: B256, attributes: OptimismPayloadAttributes) -> Result { + let (id, transactions) = { + let transactions: Vec<_> = attributes + .transactions + .as_deref() + .unwrap_or(&[]) + .iter() + .map(|tx| TransactionSigned::decode_enveloped(&mut tx.as_ref())) + .collect::>()?; + (payload_id_optimism(&parent, &attributes, &transactions), transactions) + }; + + let withdraw = attributes.payload_attributes.withdrawals.map( + |withdrawals: Vec| { + withdrawals + .into_iter() + .map(convert_standalone_withdraw_to_withdrawal) // Removed the parentheses here + .collect::>() + }, + ); + + let payload_attributes = EthPayloadBuilderAttributes { + id, + parent, + timestamp: attributes.payload_attributes.timestamp, + suggested_fee_recipient: attributes.payload_attributes.suggested_fee_recipient, + prev_randao: attributes.payload_attributes.prev_randao, + withdrawals: withdraw.unwrap_or_default(), + parent_beacon_block_root: attributes.payload_attributes.parent_beacon_block_root, + }; + + Ok(Self { + payload_attributes, + no_tx_pool: attributes.no_tx_pool.unwrap_or_default(), + transactions, + gas_limit: attributes.gas_limit, + }) + } + + fn parent(&self) -> B256 { + self.payload_attributes.parent + } + + fn payload_id(&self) -> PayloadId { + self.payload_attributes.id + } + + fn timestamp(&self) -> u64 { + self.payload_attributes.timestamp + } + + fn parent_beacon_block_root(&self) -> Option { + self.payload_attributes.parent_beacon_block_root + } + + fn suggested_fee_recipient(&self) -> Address { + self.payload_attributes.suggested_fee_recipient + } + + fn prev_randao(&self) -> B256 { + self.payload_attributes.prev_randao + } + + fn withdrawals(&self) -> &Vec { + &self.payload_attributes.withdrawals + } +} + +/// Generates the payload id for the configured payload from the [OptimismPayloadAttributes]. /// /// Returns an 8-byte identifier by hashing the payload components with sha256 hash. -pub(crate) fn payload_id( +pub(crate) fn payload_id_optimism( parent: &B256, - attributes: &PayloadAttributes, - #[cfg(feature = "optimism")] txs: &[TransactionSigned], + attributes: &OptimismPayloadAttributes, + txs: &[TransactionSigned], ) -> PayloadId { + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + hasher.update(parent.as_slice()); + hasher.update(&attributes.payload_attributes.timestamp.to_be_bytes()[..]); + hasher.update(attributes.payload_attributes.prev_randao.as_slice()); + hasher.update(attributes.payload_attributes.suggested_fee_recipient.as_slice()); + if let Some(withdrawals) = &attributes.payload_attributes.withdrawals { + let mut buf = Vec::new(); + withdrawals.encode(&mut buf); + hasher.update(buf); + } + + if let Some(parent_beacon_block) = attributes.payload_attributes.parent_beacon_block_root { + hasher.update(parent_beacon_block); + } + + let no_tx_pool = attributes.no_tx_pool.unwrap_or_default(); + if no_tx_pool || !txs.is_empty() { + hasher.update([no_tx_pool as u8]); + hasher.update(txs.len().to_be_bytes()); + txs.iter().for_each(|tx| hasher.update(tx.hash())); + } + + if let Some(gas_limit) = attributes.gas_limit { + hasher.update(gas_limit.to_be_bytes()); + } + + let out = hasher.finalize(); + PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length")) +} + +/// Generates the payload id for the configured payload from the [PayloadAttributes]. +/// +/// Returns an 8-byte identifier by hashing the payload components with sha256 hash. +pub(crate) fn payload_id(parent: &B256, attributes: &PayloadAttributes) -> PayloadId { use sha2::Digest; let mut hasher = sha2::Sha256::new(); hasher.update(parent.as_slice()); @@ -294,20 +423,6 @@ pub(crate) fn payload_id( hasher.update(parent_beacon_block); } - #[cfg(feature = "optimism")] - { - let no_tx_pool = attributes.optimism_payload_attributes.no_tx_pool.unwrap_or_default(); - if no_tx_pool || !txs.is_empty() { - hasher.update([no_tx_pool as u8]); - hasher.update(txs.len().to_be_bytes()); - txs.iter().for_each(|tx| hasher.update(tx.hash())); - } - - if let Some(gas_limit) = attributes.optimism_payload_attributes.gas_limit { - hasher.update(gas_limit.to_be_bytes()); - } - } - let out = hasher.finalize(); PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length")) } diff --git a/crates/payload/builder/src/service.rs b/crates/payload/builder/src/service.rs index f760bce11..2a330259b 100644 --- a/crates/payload/builder/src/service.rs +++ b/crates/payload/builder/src/service.rs @@ -5,9 +5,10 @@ use crate::{ error::PayloadBuilderError, metrics::PayloadBuilderServiceMetrics, traits::PayloadJobGenerator, - BuiltPayload, KeepPayloadJobAlive, PayloadBuilderAttributes, PayloadJob, + BuiltPayload, KeepPayloadJobAlive, PayloadJob, }; use futures_util::{future::FutureExt, Stream, StreamExt}; +use reth_node_api::{EngineTypes, PayloadBuilderAttributes}; use reth_provider::CanonStateNotification; use reth_rpc_types::engine::PayloadId; use std::{ @@ -23,13 +24,16 @@ use tracing::{debug, info, trace, warn}; /// A communication channel to the [PayloadBuilderService] that can retrieve payloads. #[derive(Debug, Clone)] -pub struct PayloadStore { - inner: PayloadBuilderHandle, +pub struct PayloadStore { + inner: PayloadBuilderHandle, } // === impl PayloadStore === -impl PayloadStore { +impl PayloadStore +where + Engine: EngineTypes, +{ /// Resolves the payload job and returns the best payload that has been built so far. /// /// Note: depending on the installed [PayloadJobGenerator], this may or may not terminate the @@ -57,13 +61,16 @@ impl PayloadStore { pub async fn payload_attributes( &self, id: PayloadId, - ) -> Option> { + ) -> Option> { self.inner.payload_attributes(id).await } } -impl From for PayloadStore { - fn from(inner: PayloadBuilderHandle) -> Self { +impl From> for PayloadStore +where + Engine: EngineTypes, +{ + fn from(inner: PayloadBuilderHandle) -> Self { Self { inner } } } @@ -72,18 +79,24 @@ impl From for PayloadStore { /// /// This is the API used to create new payloads and to get the current state of existing ones. #[derive(Debug, Clone)] -pub struct PayloadBuilderHandle { +pub struct PayloadBuilderHandle { /// Sender half of the message channel to the [PayloadBuilderService]. - to_service: mpsc::UnboundedSender, + to_service: mpsc::UnboundedSender>, } + // === impl PayloadBuilderHandle === -impl PayloadBuilderHandle { +impl PayloadBuilderHandle +where + Engine: EngineTypes, +{ /// Creates a new payload builder handle for the given channel. /// /// Note: this is only used internally by the [PayloadBuilderService] to manage the payload /// building flow See [PayloadBuilderService::poll] for implementation details. - pub fn new(to_service: mpsc::UnboundedSender) -> Self { + pub fn new( + to_service: mpsc::UnboundedSender>, + ) -> Self { Self { to_service } } @@ -91,7 +104,7 @@ impl PayloadBuilderHandle { /// /// Note: depending on the installed [PayloadJobGenerator], this may or may not terminate the /// job, See [PayloadJob::resolve]. - pub async fn resolve( + async fn resolve( &self, id: PayloadId, ) -> Option, PayloadBuilderError>> { @@ -104,7 +117,7 @@ impl PayloadBuilderHandle { } /// Returns the best payload for the given identifier. - pub async fn best_payload( + async fn best_payload( &self, id: PayloadId, ) -> Option, PayloadBuilderError>> { @@ -116,10 +129,10 @@ impl PayloadBuilderHandle { /// Returns the payload attributes associated with the given identifier. /// /// Note: this returns the attributes of the payload and does not resolve the job. - pub async fn payload_attributes( + async fn payload_attributes( &self, id: PayloadId, - ) -> Option> { + ) -> Option> { let (tx, rx) = oneshot::channel(); self.to_service.send(PayloadServiceCommand::PayloadAttributes(id, tx)).ok()?; rx.await.ok()? @@ -131,7 +144,7 @@ impl PayloadBuilderHandle { /// returns the receiver instead pub fn send_new_payload( &self, - attr: PayloadBuilderAttributes, + attr: Engine::PayloadBuilderAttributes, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel(); let _ = self.to_service.send(PayloadServiceCommand::BuildNewPayload(attr, tx)); @@ -145,7 +158,7 @@ impl PayloadBuilderHandle { /// Note: if there's already payload in progress with same identifier, it will be returned. pub async fn new_payload( &self, - attr: PayloadBuilderAttributes, + attr: Engine::PayloadBuilderAttributes, ) -> Result { self.send_new_payload(attr).await? } @@ -161,18 +174,20 @@ impl PayloadBuilderHandle { /// does know nothing about how to build them, it just drives their jobs to completion. #[derive(Debug)] #[must_use = "futures do nothing unless you `.await` or poll them"] -pub struct PayloadBuilderService +pub struct PayloadBuilderService where + Engine: EngineTypes, Gen: PayloadJobGenerator, + Gen::Job: PayloadJob, { /// The type that knows how to create new payloads. generator: Gen, /// All active payload jobs. payload_jobs: Vec<(Gen::Job, PayloadId)>, /// Copy of the sender half, so new [`PayloadBuilderHandle`] can be created on demand. - service_tx: mpsc::UnboundedSender, + service_tx: mpsc::UnboundedSender>, /// Receiver half of the command channel. - command_rx: UnboundedReceiverStream, + command_rx: UnboundedReceiverStream>, /// Metrics for the payload builder service metrics: PayloadBuilderServiceMetrics, /// Chain events notification stream @@ -181,16 +196,18 @@ where // === impl PayloadBuilderService === -impl PayloadBuilderService +impl PayloadBuilderService where + Engine: EngineTypes, Gen: PayloadJobGenerator, + Gen::Job: PayloadJob, { /// Creates a new payload builder service and returns the [PayloadBuilderHandle] to interact /// with it. /// /// This also takes a stream of chain events that will be forwarded to the generator to apply /// additional logic when new state is committed. See also [PayloadJobGenerator::on_new_state]. - pub fn new(generator: Gen, chain_events: St) -> (Self, PayloadBuilderHandle) { + pub fn new(generator: Gen, chain_events: St) -> (Self, PayloadBuilderHandle) { let (service_tx, command_rx) = mpsc::unbounded_channel(); let service = Self { generator, @@ -206,7 +223,7 @@ where } /// Returns a handle to the service. - pub fn handle(&self) -> PayloadBuilderHandle { + pub fn handle(&self) -> PayloadBuilderHandle { PayloadBuilderHandle::new(self.service_tx.clone()) } @@ -232,24 +249,6 @@ where res } - /// Returns the payload attributes for the given payload. - fn payload_attributes( - &self, - id: PayloadId, - ) -> Option> { - let attributes = self - .payload_jobs - .iter() - .find(|(_, job_id)| *job_id == id) - .map(|(j, _)| j.payload_attributes()); - - if attributes.is_none() { - trace!(%id, "no matching payload job found to get attributes for"); - } - - attributes - } - /// Returns the best payload for the given identifier that has been built so far and terminates /// the job if requested. fn resolve(&mut self, id: PayloadId) -> Option { @@ -279,11 +278,38 @@ where } } -impl Future for PayloadBuilderService +impl PayloadBuilderService where + Engine: EngineTypes, + Gen: PayloadJobGenerator, + Gen::Job: PayloadJob, +{ + /// Returns the payload attributes for the given payload. + fn payload_attributes( + &self, + id: PayloadId, + ) -> Option::PayloadAttributes, PayloadBuilderError>> { + let attributes = self + .payload_jobs + .iter() + .find(|(_, job_id)| *job_id == id) + .map(|(j, _)| j.payload_attributes()); + + if attributes.is_none() { + trace!(%id, "no matching payload job found to get attributes for"); + } + + attributes + } +} + +impl Future for PayloadBuilderService +where + Engine: EngineTypes, Gen: PayloadJobGenerator + Unpin + 'static, ::Job: Unpin + 'static, St: Stream + Send + Unpin + 'static, + Gen::Job: PayloadJob, { type Output = (); @@ -330,10 +356,10 @@ where let mut res = Ok(id); if this.contains_payload(id) { - debug!(%id, parent = %attr.parent, "Payload job already in progress, ignoring."); + debug!(%id, parent = %attr.parent(), "Payload job already in progress, ignoring."); } else { // no job for this payload yet, create one - let parent = attr.parent; + let parent = attr.parent(); match this.generator.new_payload_job(attr) { Ok(job) => { info!(%id, %parent, "New payload job created"); @@ -371,28 +397,26 @@ where } } +// TODO: make generic over built payload type type PayloadFuture = Pin, PayloadBuilderError>> + Send + Sync>>; /// Message type for the [PayloadBuilderService]. -pub enum PayloadServiceCommand { +pub enum PayloadServiceCommand { /// Start building a new payload. - BuildNewPayload( - PayloadBuilderAttributes, - oneshot::Sender>, - ), + BuildNewPayload(T, oneshot::Sender>), /// Get the best payload so far BestPayload(PayloadId, oneshot::Sender, PayloadBuilderError>>>), /// Get the payload attributes for the given payload - PayloadAttributes( - PayloadId, - oneshot::Sender>>, - ), + PayloadAttributes(PayloadId, oneshot::Sender>>), /// Resolve the payload and return the payload Resolve(PayloadId, oneshot::Sender>), } -impl fmt::Debug for PayloadServiceCommand { +impl fmt::Debug for PayloadServiceCommand +where + T: fmt::Debug, +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PayloadServiceCommand::BuildNewPayload(f0, f1) => { diff --git a/crates/payload/builder/src/test_utils.rs b/crates/payload/builder/src/test_utils.rs index 871c4828b..b818596c8 100644 --- a/crates/payload/builder/src/test_utils.rs +++ b/crates/payload/builder/src/test_utils.rs @@ -2,9 +2,10 @@ use crate::{ error::PayloadBuilderError, traits::KeepPayloadJobAlive, BuiltPayload, - PayloadBuilderAttributes, PayloadBuilderHandle, PayloadBuilderService, PayloadJob, + EthPayloadBuilderAttributes, PayloadBuilderHandle, PayloadBuilderService, PayloadJob, PayloadJobGenerator, }; +use reth_node_api::EngineTypes; use reth_primitives::{Block, U256}; use reth_provider::CanonStateNotification; use std::{ @@ -15,18 +16,25 @@ use std::{ }; /// Creates a new [PayloadBuilderService] for testing purposes. -pub fn test_payload_service() -> ( +pub fn test_payload_service() -> ( PayloadBuilderService< TestPayloadJobGenerator, futures_util::stream::Empty, + Engine, >, - PayloadBuilderHandle, -) { + PayloadBuilderHandle, +) +where + Engine: EngineTypes, +{ PayloadBuilderService::new(Default::default(), futures_util::stream::empty()) } /// Creates a new [PayloadBuilderService] for testing purposes and spawns it in the background. -pub fn spawn_test_payload_service() -> PayloadBuilderHandle { +pub fn spawn_test_payload_service() -> PayloadBuilderHandle +where + Engine: EngineTypes + 'static, +{ let (service, handle) = test_payload_service(); tokio::spawn(service); handle @@ -42,7 +50,7 @@ impl PayloadJobGenerator for TestPayloadJobGenerator { fn new_payload_job( &self, - attr: PayloadBuilderAttributes, + attr: EthPayloadBuilderAttributes, ) -> Result { Ok(TestPayloadJob { attr }) } @@ -51,7 +59,7 @@ impl PayloadJobGenerator for TestPayloadJobGenerator { /// A [PayloadJobGenerator] for testing purposes #[derive(Debug)] pub struct TestPayloadJob { - attr: PayloadBuilderAttributes, + attr: EthPayloadBuilderAttributes, } impl Future for TestPayloadJob { @@ -63,6 +71,7 @@ impl Future for TestPayloadJob { } impl PayloadJob for TestPayloadJob { + type PayloadAttributes = EthPayloadBuilderAttributes; type ResolvePayloadFuture = futures_util::future::Ready, PayloadBuilderError>>; @@ -74,7 +83,7 @@ impl PayloadJob for TestPayloadJob { ))) } - fn payload_attributes(&self) -> Result { + fn payload_attributes(&self) -> Result { Ok(self.attr.clone()) } diff --git a/crates/payload/builder/src/traits.rs b/crates/payload/builder/src/traits.rs index 60f632817..c6eceb909 100644 --- a/crates/payload/builder/src/traits.rs +++ b/crates/payload/builder/src/traits.rs @@ -1,8 +1,8 @@ //! Trait abstractions used by the payload crate. +use crate::{error::PayloadBuilderError, BuiltPayload}; +use reth_node_api::PayloadBuilderAttributes; use reth_provider::CanonStateNotification; - -use crate::{error::PayloadBuilderError, BuiltPayload, PayloadBuilderAttributes}; use std::{future::Future, sync::Arc}; /// A type that can build a payload. @@ -17,6 +17,8 @@ use std::{future::Future, sync::Arc}; /// /// Note: A `PayloadJob` need to be cancel safe because it might be dropped after the CL has requested the payload via `engine_getPayloadV1` (see also [engine API docs](https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/paris.md#engine_getpayloadv1)) pub trait PayloadJob: Future> + Send + Sync { + /// Represents the payload attributes type that is used to spawn this payload job. + type PayloadAttributes: PayloadBuilderAttributes + std::fmt::Debug; /// Represents the future that resolves the block that's returned to the CL. type ResolvePayloadFuture: Future, PayloadBuilderError>> + Send @@ -29,7 +31,7 @@ pub trait PayloadJob: Future> + Send + fn best_payload(&self) -> Result, PayloadBuilderError>; /// Returns the payload attributes for the payload being built. - fn payload_attributes(&self) -> Result; + fn payload_attributes(&self) -> Result; /// Called when the payload is requested by the CL. /// @@ -80,7 +82,7 @@ pub trait PayloadJobGenerator: Send + Sync { /// returned directly. fn new_payload_job( &self, - attr: PayloadBuilderAttributes, + attr: ::PayloadAttributes, ) -> Result; /// Handles new chain state events diff --git a/crates/payload/ethereum/src/lib.rs b/crates/payload/ethereum/src/lib.rs index 6de0eabe2..3d10c51a9 100644 --- a/crates/payload/ethereum/src/lib.rs +++ b/crates/payload/ethereum/src/lib.rs @@ -16,7 +16,9 @@ mod builder { commit_withdrawals, is_better_payload, pre_block_beacon_root_contract_call, BuildArguments, BuildOutcome, PayloadBuilder, PayloadConfig, WithdrawalsOutcome, }; - use reth_payload_builder::{error::PayloadBuilderError, BuiltPayload}; + use reth_payload_builder::{ + error::PayloadBuilderError, BuiltPayload, EthPayloadBuilderAttributes, + }; use reth_primitives::{ constants::{eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE}, eip4844::calculate_excess_blob_gas, @@ -45,9 +47,11 @@ mod builder { Client: StateProviderFactory, Pool: TransactionPool, { + type Attributes = EthPayloadBuilderAttributes; + fn try_build( &self, - args: BuildArguments, + args: BuildArguments, ) -> Result { default_ethereum_payload_builder(args) } @@ -60,7 +64,7 @@ mod builder { /// a result indicating success with the payload or an error in case of failure. #[inline] pub fn default_ethereum_payload_builder( - args: BuildArguments, + args: BuildArguments, ) -> Result where Client: StateProviderFactory, diff --git a/crates/payload/optimism/Cargo.toml b/crates/payload/optimism/Cargo.toml index 7ce5c044f..76ca79bf8 100644 --- a/crates/payload/optimism/Cargo.toml +++ b/crates/payload/optimism/Cargo.toml @@ -19,6 +19,7 @@ reth-transaction-pool.workspace = true reth-provider.workspace = true reth-payload-builder.workspace = true reth-basic-payload-builder.workspace = true +reth-node-api.workspace = true # ethereum revm.workspace = true diff --git a/crates/payload/optimism/src/lib.rs b/crates/payload/optimism/src/lib.rs index 6be674e35..0939587ea 100644 --- a/crates/payload/optimism/src/lib.rs +++ b/crates/payload/optimism/src/lib.rs @@ -16,7 +16,10 @@ pub mod error; mod builder { use crate::error::OptimismPayloadBuilderError; use reth_basic_payload_builder::*; - use reth_payload_builder::{error::PayloadBuilderError, BuiltPayload}; + use reth_node_api::PayloadBuilderAttributes; + use reth_payload_builder::{ + error::PayloadBuilderError, BuiltPayload, OptimismPayloadBuilderAttributes, + }; use reth_primitives::{ constants::BEACON_NONCE, proofs, @@ -68,22 +71,24 @@ mod builder { Client: StateProviderFactory, Pool: TransactionPool, { + type Attributes = OptimismPayloadBuilderAttributes; + fn try_build( &self, - args: BuildArguments, + args: BuildArguments, ) -> Result { optimism_payload_builder(args, self.compute_pending_block) } fn on_missing_payload( &self, - args: BuildArguments, + args: BuildArguments, ) -> Option> { // In Optimism, the PayloadAttributes can specify a `no_tx_pool` option that implies we // should not pull transactions from the tx pool. In this case, we build the payload // upfront with the list of transactions sent in the attributes without caring about // the results of the polling job, if a best payload has not already been built. - if args.config.attributes.optimism_payload_attributes.no_tx_pool { + if args.config.attributes.no_tx_pool { if let Ok(BuildOutcome::Better { payload, .. }) = self.try_build(args) { trace!(target: "payload_builder", "[OPTIMISM] Forced best payload"); let payload = Arc::new(payload); @@ -105,7 +110,7 @@ mod builder { /// a result indicating success with the payload or an error in case of failure. #[inline] pub(crate) fn optimism_payload_builder( - args: BuildArguments, + args: BuildArguments, _compute_pending_block: bool, ) -> Result where @@ -130,10 +135,9 @@ mod builder { .. } = config; - debug!(target: "payload_builder", id=%attributes.id, parent_hash = ?parent_block.hash, parent_number = parent_block.number, "building new payload"); + debug!(target: "payload_builder", id=%attributes.payload_id(), parent_hash = ?parent_block.hash, parent_number = parent_block.number, "building new payload"); let mut cumulative_gas_used = 0; let block_gas_limit: u64 = attributes - .optimism_payload_attributes .gas_limit .unwrap_or(initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX)); let base_fee = initialized_block_env.basefee.to::(); @@ -146,7 +150,7 @@ mod builder { let block_number = initialized_block_env.number.to::(); let is_regolith = - chain_spec.is_fork_active_at_timestamp(Hardfork::Regolith, attributes.timestamp); + chain_spec.is_fork_active_at_timestamp(Hardfork::Regolith, attributes.timestamp()); // Ensure that the create2deployer is force-deployed at the canyon transition. Optimism // blocks will always have at least a single transaction in them (the L1 info transaction), @@ -154,7 +158,7 @@ mod builder { // the above check for empty blocks will never be hit on OP chains. reth_revm::optimism::ensure_create2_deployer( chain_spec.clone(), - attributes.timestamp, + attributes.timestamp(), &mut db, ) .map_err(|_| { @@ -162,7 +166,7 @@ mod builder { })?; let mut receipts = Vec::new(); - for sequencer_tx in attributes.optimism_payload_attributes.transactions { + for sequencer_tx in &attributes.transactions { // Check if the job was cancelled, if so we can exit early. if cancel.is_cancelled() { return Ok(BuildOutcome::Cancelled) @@ -238,7 +242,7 @@ mod builder { // receipt hashes should be computed when set. The state transition process // ensures this is only set for post-Canyon deposit transactions. deposit_receipt_version: chain_spec - .is_fork_active_at_timestamp(Hardfork::Canyon, attributes.timestamp) + .is_fork_active_at_timestamp(Hardfork::Canyon, attributes.timestamp()) .then_some(1), })); @@ -246,7 +250,7 @@ mod builder { executed_txs.push(sequencer_tx.into_signed()); } - if !attributes.optimism_payload_attributes.no_tx_pool { + if !attributes.no_tx_pool { while let Some(pool_tx) = best_txs.next() { // ensure we still have capacity for this transaction if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit { @@ -336,8 +340,12 @@ mod builder { return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }) } - let WithdrawalsOutcome { withdrawals_root, withdrawals } = - commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, attributes.withdrawals)?; + let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals( + &mut db, + &chain_spec, + attributes.timestamp(), + attributes.withdrawals().clone(), + )?; // merge all transitions into bundle state, this would apply the withdrawal balance changes // and 4788 contract call @@ -349,7 +357,7 @@ mod builder { block_number, ); let receipts_root = bundle - .receipts_root_slow(block_number, chain_spec.as_ref(), attributes.timestamp) + .receipts_root_slow(block_number, chain_spec.as_ref(), attributes.timestamp()) .expect("Number is in range"); let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); @@ -373,8 +381,8 @@ mod builder { receipts_root, withdrawals_root, logs_bloom, - timestamp: attributes.timestamp, - mix_hash: attributes.prev_randao, + timestamp: attributes.timestamp(), + mix_hash: attributes.prev_randao(), nonce: BEACON_NONCE, base_fee_per_gas: Some(base_fee), number: parent_block.number + 1, @@ -382,7 +390,7 @@ mod builder { difficulty: U256::ZERO, gas_used: cumulative_gas_used, extra_data, - parent_beacon_block_root: attributes.parent_beacon_block_root, + parent_beacon_block_root: attributes.parent_beacon_block_root(), blob_gas_used, excess_blob_gas, }; @@ -393,7 +401,7 @@ mod builder { let sealed_block = block.seal_slow(); debug!(target: "payload_builder", ?sealed_block, "sealed built block"); - let mut payload = BuiltPayload::new(attributes.id, sealed_block, total_fees); + let mut payload = BuiltPayload::new(attributes.payload_id(), sealed_block, total_fees); // extend the payload with the blob sidecars from the executed txs payload.extend_sidecars(blob_sidecars); diff --git a/crates/rpc/rpc-api/Cargo.toml b/crates/rpc/rpc-api/Cargo.toml index 13586f119..f66f20ec4 100644 --- a/crates/rpc/rpc-api/Cargo.toml +++ b/crates/rpc/rpc-api/Cargo.toml @@ -15,6 +15,8 @@ workspace = true # reth reth-primitives.workspace = true reth-rpc-types.workspace = true +reth-payload-builder.workspace = true +reth-node-api.workspace = true # misc jsonrpsee = { workspace = true, features = ["server", "macros"] } diff --git a/crates/rpc/rpc-api/src/engine.rs b/crates/rpc/rpc-api/src/engine.rs index 94563267c..87c7184d3 100644 --- a/crates/rpc/rpc-api/src/engine.rs +++ b/crates/rpc/rpc-api/src/engine.rs @@ -1,18 +1,19 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use reth_node_api::EngineTypes; use reth_primitives::{Address, BlockHash, BlockId, BlockNumberOrTag, Bytes, B256, U256, U64}; use reth_rpc_types::{ engine::{ ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadInputV2, ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, - ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, TransitionConfiguration, + ForkchoiceUpdated, PayloadId, PayloadStatus, TransitionConfiguration, }, state::StateOverride, BlockOverrides, CallRequest, Filter, Log, RichBlock, SyncStatus, }; -#[cfg_attr(not(feature = "client"), rpc(server, namespace = "engine"))] -#[cfg_attr(feature = "client", rpc(server, client, namespace = "engine"))] -pub trait EngineApi { +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "engine"), server_bounds(Engine::PayloadAttributes: jsonrpsee::core::DeserializeOwned))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "engine", client_bounds(Engine::PayloadAttributes: jsonrpsee::core::Serialize + Clone), server_bounds(Engine::PayloadAttributes: jsonrpsee::core::DeserializeOwned)))] +pub trait EngineApi { /// See also /// Caution: This should not accept the `withdrawals` field #[method(name = "newPayloadV1")] @@ -40,13 +41,13 @@ pub trait EngineApi { async fn fork_choice_updated_v1( &self, fork_choice_state: ForkchoiceState, - payload_attributes: Option, + payload_attributes: Option, ) -> RpcResult; /// Post Shanghai forkchoice update handler /// /// This is the same as `forkchoiceUpdatedV1`, but expects an additional `withdrawals` field in - /// the [PayloadAttributes], if payload attributes are provided. + /// the `payloadAttributes`, if payload attributes are provided. /// /// See also /// @@ -56,21 +57,21 @@ pub trait EngineApi { async fn fork_choice_updated_v2( &self, fork_choice_state: ForkchoiceState, - payload_attributes: Option, + payload_attributes: Option, ) -> RpcResult; /// Post Cancun forkchoice update handler /// /// This is the same as `forkchoiceUpdatedV2`, but expects an additional - /// `parentBeaconBlockRoot` field in the the [PayloadAttributes], if payload attributes are - /// provided. + /// `parentBeaconBlockRoot` field in the the `payloadAttributes`, if payload attributes + /// are provided. /// /// See also #[method(name = "forkchoiceUpdatedV3")] async fn fork_choice_updated_v3( &self, fork_choice_state: ForkchoiceState, - payload_attributes: Option, + payload_attributes: Option, ) -> RpcResult; /// See also @@ -147,6 +148,13 @@ pub trait EngineApi { async fn exchange_capabilities(&self, capabilities: Vec) -> RpcResult>; } +// NOTE: We can't use associated types in the `EngineApi` trait because of jsonrpsee, so we use a +// generic here. It would be nice if the rpc macro would understand which types need to have serde. +// By default, if the trait has a generic, the rpc macro will add e.g. `Engine: DeserializeOwned` to +// the trait bounds, which is not what we want, because `Types` is not used directly in any of the +// trait methods. Instead, we have to add the bounds manually. This would be disastrous if we had +// more than one associated type used in the trait methods. + /// A subset of the ETH rpc interface: /// /// Specifically for the engine auth server: diff --git a/crates/rpc/rpc-builder/Cargo.toml b/crates/rpc/rpc-builder/Cargo.toml index 88ad4deaf..396dbe805 100644 --- a/crates/rpc/rpc-builder/Cargo.toml +++ b/crates/rpc/rpc-builder/Cargo.toml @@ -25,6 +25,7 @@ reth-rpc-types.workspace = true reth-tasks.workspace = true reth-transaction-pool.workspace = true reth-rpc-types-compat.workspace = true +reth-node-api.workspace = true # rpc/net jsonrpsee = { workspace = true, features = ["server"] } @@ -51,6 +52,7 @@ reth-network-api.workspace = true reth-interfaces = { workspace = true, features = ["test-utils"] } reth-beacon-consensus.workspace = true reth-payload-builder = { workspace = true, features = ["test-utils"] } +reth-node-builder.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } serde_json.workspace = true diff --git a/crates/rpc/rpc-builder/src/auth.rs b/crates/rpc/rpc-builder/src/auth.rs index 7f1158e1c..88ec819ca 100644 --- a/crates/rpc/rpc-builder/src/auth.rs +++ b/crates/rpc/rpc-builder/src/auth.rs @@ -11,6 +11,7 @@ use jsonrpsee::{ server::{RpcModule, ServerHandle}, }; use reth_network_api::{NetworkInfo, Peers}; +use reth_node_api::EngineTypes; use reth_provider::{ BlockReaderIdExt, ChainSpecProvider, EvmEnvProvider, HeaderProvider, ReceiptProviderIdExt, StateProviderFactory, @@ -33,7 +34,7 @@ use std::{ /// Configure and launch a _standalone_ auth server with `engine` and a _new_ `eth` namespace. #[allow(clippy::too_many_arguments)] -pub async fn launch( +pub async fn launch( provider: Provider, pool: Pool, network: Network, @@ -55,7 +56,8 @@ where Pool: TransactionPool + Clone + 'static, Network: NetworkInfo + Peers + Clone + 'static, Tasks: TaskSpawner + Clone + 'static, - EngineApi: EngineApiServer, + EngineT: EngineTypes, + EngineApi: EngineApiServer, { // spawn a new cache task let eth_cache = @@ -85,7 +87,7 @@ where } /// Configure and launch a _standalone_ auth server with existing EthApi implementation. -pub async fn launch_with_eth_api( +pub async fn launch_with_eth_api( eth_api: EthApi, eth_filter: EthFilter, engine_api: EngineApi, @@ -103,7 +105,8 @@ where + 'static, Pool: TransactionPool + Clone + 'static, Network: NetworkInfo + Peers + Clone + 'static, - EngineApi: EngineApiServer, + EngineT: EngineTypes, + EngineApi: EngineApiServer, { // Configure the module and start the server. let mut module = RpcModule::new(()); @@ -253,9 +256,10 @@ pub struct AuthRpcModule { impl AuthRpcModule { /// Create a new `AuthRpcModule` with the given `engine_api`. - pub fn new(engine: EngineApi) -> Self + pub fn new(engine: EngineApi) -> Self where - EngineApi: EngineApiServer, + EngineT: EngineTypes, + EngineApi: EngineApiServer, { let mut module = RpcModule::new(()); module.merge(engine.into_rpc()).expect("No conflicting methods"); diff --git a/crates/rpc/rpc-builder/src/lib.rs b/crates/rpc/rpc-builder/src/lib.rs index a559f3b8a..92a187b42 100644 --- a/crates/rpc/rpc-builder/src/lib.rs +++ b/crates/rpc/rpc-builder/src/lib.rs @@ -69,6 +69,7 @@ //! //! ``` //! use reth_network_api::{NetworkInfo, Peers}; +//! use reth_node_api::EngineTypes; //! use reth_provider::{ //! AccountReader, BlockReaderIdExt, CanonStateSubscriptions, ChainSpecProvider, //! ChangeSetReader, EvmEnvProvider, StateProviderFactory, @@ -82,7 +83,7 @@ //! use reth_tasks::TokioTaskExecutor; //! use reth_transaction_pool::TransactionPool; //! use tokio::try_join; -//! pub async fn launch( +//! pub async fn launch( //! provider: Provider, //! pool: Pool, //! network: Network, @@ -101,7 +102,8 @@ //! Pool: TransactionPool + Clone + 'static, //! Network: NetworkInfo + Peers + Clone + 'static, //! Events: CanonStateSubscriptions + Clone + 'static, -//! EngineApi: EngineApiServer, +//! EngineApi: EngineApiServer, +//! EngineT: EngineTypes, //! { //! // configure the rpc module per transport //! let transports = TransportRpcModuleConfig::default().with_http(vec![ @@ -148,6 +150,7 @@ use jsonrpsee::{ server::{IdProvider, Server, ServerHandle}, Methods, RpcModule, }; +use reth_node_api::EngineTypes; use serde::{Deserialize, Serialize, Serializer}; use strum::{AsRefStr, EnumIter, EnumVariantNames, IntoStaticStr, ParseError, VariantNames}; use tower::layer::util::{Identity, Stack}; @@ -379,7 +382,7 @@ where /// /// This behaves exactly as [RpcModuleBuilder::build] for the [TransportRpcModules], but also /// configures the auth (engine api) server, which exposes a subset of the `eth_` namespace. - pub fn build_with_auth_server( + pub fn build_with_auth_server( self, module_config: TransportRpcModuleConfig, engine: EngineApi, @@ -389,7 +392,7 @@ where RethModuleRegistry, ) where - EngineApi: EngineApiServer, + EngineApi: EngineApiServer, { let mut modules = TransportRpcModules::default(); @@ -1020,9 +1023,10 @@ where /// * `api_` namespace /// /// Note: This does _not_ register the `engine_` in this registry. - pub fn create_auth_module(&mut self, engine_api: EngineApi) -> AuthRpcModule + pub fn create_auth_module(&mut self, engine_api: EngineApi) -> AuthRpcModule where - EngineApi: EngineApiServer, + EngineT: EngineTypes, + EngineApi: EngineApiServer, { let eth_handlers = self.eth_handlers(); let mut module = RpcModule::new(()); diff --git a/crates/rpc/rpc-builder/tests/it/auth.rs b/crates/rpc/rpc-builder/tests/it/auth.rs index d1141483b..e4aada9f0 100644 --- a/crates/rpc/rpc-builder/tests/it/auth.rs +++ b/crates/rpc/rpc-builder/tests/it/auth.rs @@ -2,6 +2,7 @@ use crate::utils::launch_auth; use jsonrpsee::core::client::{ClientT, SubscriptionClientT}; +use reth_node_builder::EthEngineTypes; use reth_primitives::{Block, U64}; use reth_rpc::JwtSecret; use reth_rpc_api::clients::EngineApiClient; @@ -13,6 +14,7 @@ use reth_rpc_types_compat::engine::payload::{ async fn test_basic_engine_calls(client: &C) where C: ClientT + SubscriptionClientT + Sync, + C: EngineApiClient, { let block = Block::default().seal_slow(); EngineApiClient::new_payload_v1(client, try_block_to_payload_v1(block.clone())).await; diff --git a/crates/rpc/rpc-builder/tests/it/utils.rs b/crates/rpc/rpc-builder/tests/it/utils.rs index 7869f6157..239900fd3 100644 --- a/crates/rpc/rpc-builder/tests/it/utils.rs +++ b/crates/rpc/rpc-builder/tests/it/utils.rs @@ -1,5 +1,6 @@ use reth_beacon_consensus::BeaconConsensusEngineHandle; use reth_network_api::noop::NoopNetwork; +use reth_node_builder::EthEngineTypes; use reth_payload_builder::test_utils::spawn_test_payload_service; use reth_primitives::MAINNET; use reth_provider::test_utils::{NoopProvider, TestCanonStateSubscriptions}; @@ -24,7 +25,7 @@ pub fn test_address() -> SocketAddr { pub async fn launch_auth(secret: JwtSecret) -> AuthServerHandle { let config = AuthServerConfig::builder(secret).socket_addr(test_address()).build(); let (tx, _rx) = unbounded_channel(); - let beacon_engine_handle = BeaconConsensusEngineHandle::new(tx); + let beacon_engine_handle = BeaconConsensusEngineHandle::::new(tx); let engine_api = EngineApi::new( NoopProvider::default(), MAINNET.clone(), diff --git a/crates/rpc/rpc-engine-api/Cargo.toml b/crates/rpc/rpc-engine-api/Cargo.toml index 18f31ee85..358737dcc 100644 --- a/crates/rpc/rpc-engine-api/Cargo.toml +++ b/crates/rpc/rpc-engine-api/Cargo.toml @@ -22,6 +22,8 @@ reth-beacon-consensus.workspace = true reth-payload-builder.workspace = true reth-tasks.workspace = true reth-rpc-types-compat.workspace = true +reth-node-api.workspace = true + # async tokio = { workspace = true, features = ["sync"] } @@ -39,6 +41,7 @@ serde.workspace = true [dev-dependencies] alloy-rlp.workspace = true +reth-node-builder.workspace = true reth-interfaces = { workspace = true, features = ["test-utils"] } reth-provider = { workspace = true, features = ["test-utils"] } reth-payload-builder = { workspace = true, features = ["test-utils"] } diff --git a/crates/rpc/rpc-engine-api/src/engine_api.rs b/crates/rpc/rpc-engine-api/src/engine_api.rs index 839e53c30..70713b4ef 100644 --- a/crates/rpc/rpc-engine-api/src/engine_api.rs +++ b/crates/rpc/rpc-engine-api/src/engine_api.rs @@ -1,20 +1,20 @@ -use crate::{ - metrics::EngineApiMetrics, payload::PayloadOrAttributes, EngineApiError, - EngineApiMessageVersion, EngineApiResult, -}; +use crate::{metrics::EngineApiMetrics, EngineApiError, EngineApiResult}; use async_trait::async_trait; use jsonrpsee_core::RpcResult; use reth_beacon_consensus::BeaconConsensusEngineHandle; use reth_interfaces::consensus::ForkchoiceState; -use reth_payload_builder::{PayloadBuilderAttributes, PayloadStore}; +use reth_node_api::{ + validate_payload_timestamp, validate_version_specific_fields, EngineApiMessageVersion, + EngineTypes, PayloadAttributes, PayloadBuilderAttributes, PayloadOrAttributes, +}; +use reth_payload_builder::PayloadStore; use reth_primitives::{BlockHash, BlockHashOrNumber, BlockNumber, ChainSpec, Hardfork, B256, U64}; use reth_provider::{BlockReader, EvmEnvProvider, HeaderProvider, StateProviderFactory}; use reth_rpc_api::EngineApiServer; use reth_rpc_types::engine::{ CancunPayloadFields, ExecutionPayload, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadInputV2, ExecutionPayloadV1, ExecutionPayloadV3, - ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, TransitionConfiguration, - CAPABILITIES, + ForkchoiceUpdated, PayloadId, PayloadStatus, TransitionConfiguration, CAPABILITIES, }; use reth_rpc_types_compat::engine::payload::{ convert_payload_input_v2_to_payload, convert_to_payload_body_v1, @@ -32,35 +32,37 @@ const MAX_PAYLOAD_BODIES_LIMIT: u64 = 1024; /// The Engine API implementation that grants the Consensus layer access to data and /// functions in the Execution layer that are crucial for the consensus process. -pub struct EngineApi { - inner: Arc>, +pub struct EngineApi { + inner: Arc>, } -struct EngineApiInner { +struct EngineApiInner { /// The provider to interact with the chain. provider: Provider, /// Consensus configuration chain_spec: Arc, /// The channel to send messages to the beacon consensus engine. - beacon_consensus: BeaconConsensusEngineHandle, + beacon_consensus: BeaconConsensusEngineHandle, /// The type that can communicate with the payload service to retrieve payloads. - payload_store: PayloadStore, + payload_store: PayloadStore, /// For spawning and executing async tasks task_spawner: Box, /// The metrics for engine api calls metrics: EngineApiMetrics, } -impl EngineApi +impl EngineApi where Provider: HeaderProvider + BlockReader + StateProviderFactory + EvmEnvProvider + 'static, + EngineT: EngineTypes + 'static, + EngineT::PayloadBuilderAttributes: Send, { /// Create new instance of [EngineApi]. pub fn new( provider: Provider, chain_spec: Arc, - beacon_consensus: BeaconConsensusEngineHandle, - payload_store: PayloadStore, + beacon_consensus: BeaconConsensusEngineHandle, + payload_store: PayloadStore, task_spawner: Box, ) -> Self { let inner = Arc::new(EngineApiInner { @@ -78,7 +80,7 @@ where async fn get_payload_attributes( &self, payload_id: PayloadId, - ) -> EngineApiResult { + ) -> EngineApiResult { Ok(self .inner .payload_store @@ -94,8 +96,15 @@ where payload: ExecutionPayloadV1, ) -> EngineApiResult { let payload = ExecutionPayload::from(payload); - let payload_or_attrs = PayloadOrAttributes::from_execution_payload(&payload, None); - self.validate_version_specific_fields(EngineApiMessageVersion::V1, &payload_or_attrs)?; + let payload_or_attrs = + PayloadOrAttributes::<'_, EngineT::PayloadAttributes>::from_execution_payload( + &payload, None, + ); + validate_version_specific_fields( + &self.inner.chain_spec, + EngineApiMessageVersion::V1, + &payload_or_attrs, + )?; Ok(self.inner.beacon_consensus.new_payload(payload, None).await?) } @@ -105,8 +114,15 @@ where payload: ExecutionPayloadInputV2, ) -> EngineApiResult { let payload = convert_payload_input_v2_to_payload(payload); - let payload_or_attrs = PayloadOrAttributes::from_execution_payload(&payload, None); - self.validate_version_specific_fields(EngineApiMessageVersion::V2, &payload_or_attrs)?; + let payload_or_attrs = + PayloadOrAttributes::<'_, EngineT::PayloadAttributes>::from_execution_payload( + &payload, None, + ); + validate_version_specific_fields( + &self.inner.chain_spec, + EngineApiMessageVersion::V2, + &payload_or_attrs, + )?; Ok(self.inner.beacon_consensus.new_payload(payload, None).await?) } @@ -119,8 +135,15 @@ where ) -> EngineApiResult { let payload = ExecutionPayload::from(payload); let payload_or_attrs = - PayloadOrAttributes::from_execution_payload(&payload, Some(parent_beacon_block_root)); - self.validate_version_specific_fields(EngineApiMessageVersion::V3, &payload_or_attrs)?; + PayloadOrAttributes::<'_, EngineT::PayloadAttributes>::from_execution_payload( + &payload, + Some(parent_beacon_block_root), + ); + validate_version_specific_fields( + &self.inner.chain_spec, + EngineApiMessageVersion::V3, + &payload_or_attrs, + )?; let cancun_fields = CancunPayloadFields { versioned_hashes, parent_beacon_block_root }; @@ -136,7 +159,7 @@ where pub async fn fork_choice_updated_v1( &self, state: ForkchoiceState, - payload_attrs: Option, + payload_attrs: Option, ) -> EngineApiResult { self.validate_and_execute_forkchoice(EngineApiMessageVersion::V1, state, payload_attrs) .await @@ -149,7 +172,7 @@ where pub async fn fork_choice_updated_v2( &self, state: ForkchoiceState, - payload_attrs: Option, + payload_attrs: Option, ) -> EngineApiResult { self.validate_and_execute_forkchoice(EngineApiMessageVersion::V2, state, payload_attrs) .await @@ -162,7 +185,7 @@ where pub async fn fork_choice_updated_v3( &self, state: ForkchoiceState, - payload_attrs: Option, + payload_attrs: Option, ) -> EngineApiResult { self.validate_and_execute_forkchoice(EngineApiMessageVersion::V3, state, payload_attrs) .await @@ -205,7 +228,11 @@ where let attributes = self.get_payload_attributes(payload_id).await?; // validate timestamp according to engine rules - self.validate_payload_timestamp(EngineApiMessageVersion::V2, attributes.timestamp)?; + validate_payload_timestamp( + &self.inner.chain_spec, + EngineApiMessageVersion::V2, + attributes.timestamp(), + )?; // Now resolve the payload Ok(self @@ -232,7 +259,11 @@ where let attributes = self.get_payload_attributes(payload_id).await?; // validate timestamp according to engine rules - self.validate_payload_timestamp(EngineApiMessageVersion::V3, attributes.timestamp)?; + validate_payload_timestamp( + &self.inner.chain_spec, + EngineApiMessageVersion::V3, + attributes.timestamp(), + )?; // Now resolve the payload Ok(self @@ -385,159 +416,6 @@ where } } - /// Validates the timestamp depending on the version called: - /// - /// * If V2, this ensure that the payload timestamp is pre-Cancun. - /// * If V3, this ensures that the payload timestamp is within the Cancun timestamp. - /// - /// Otherwise, this will return [EngineApiError::UnsupportedFork]. - fn validate_payload_timestamp( - &self, - version: EngineApiMessageVersion, - timestamp: u64, - ) -> EngineApiResult<()> { - let is_cancun = self.inner.chain_spec.is_cancun_active_at_timestamp(timestamp); - if version == EngineApiMessageVersion::V2 && is_cancun { - // From the Engine API spec: - // - // ### Update the methods of previous forks - // - // This document defines how Cancun payload should be handled by the [`Shanghai - // API`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md). - // - // For the following methods: - // - // - [`engine_forkchoiceUpdatedV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_forkchoiceupdatedv2) - // - [`engine_newPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_newpayloadV2) - // - [`engine_getPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_getpayloadv2) - // - // a validation **MUST** be added: - // - // 1. Client software **MUST** return `-38005: Unsupported fork` error if the - // `timestamp` of payload or payloadAttributes greater or equal to the Cancun - // activation timestamp. - return Err(EngineApiError::UnsupportedFork) - } - - if version == EngineApiMessageVersion::V3 && !is_cancun { - // From the Engine API spec: - // - // - // 1. Client software **MUST** return `-38005: Unsupported fork` error if the - // `timestamp` of the built payload does not fall within the time frame of the Cancun - // fork. - return Err(EngineApiError::UnsupportedFork) - } - Ok(()) - } - - /// Validates the presence of the `withdrawals` field according to the payload timestamp. - /// After Shanghai, withdrawals field must be [Some]. - /// Before Shanghai, withdrawals field must be [None]; - fn validate_withdrawals_presence( - &self, - version: EngineApiMessageVersion, - timestamp: u64, - has_withdrawals: bool, - ) -> EngineApiResult<()> { - let is_shanghai = - self.inner.chain_spec.fork(Hardfork::Shanghai).active_at_timestamp(timestamp); - - match version { - EngineApiMessageVersion::V1 => { - if has_withdrawals { - return Err(EngineApiError::WithdrawalsNotSupportedInV1) - } - if is_shanghai { - return Err(EngineApiError::NoWithdrawalsPostShanghai) - } - } - EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => { - if is_shanghai && !has_withdrawals { - return Err(EngineApiError::NoWithdrawalsPostShanghai) - } - if !is_shanghai && has_withdrawals { - return Err(EngineApiError::HasWithdrawalsPreShanghai) - } - } - }; - - Ok(()) - } - - /// Validate the presence of the `parentBeaconBlockRoot` field according to the payload - /// timestamp. - /// - /// After Cancun, `parentBeaconBlockRoot` field must be [Some]. - /// Before Cancun, `parentBeaconBlockRoot` field must be [None]. - /// - /// If the engine API message version is V1 or V2, and the payload attribute's timestamp is - /// post-Cancun, then this will return [EngineApiError::UnsupportedFork]. - /// - /// If the payload attribute's timestamp is before the Cancun fork and the engine API message - /// version is V3, then this will return [EngineApiError::UnsupportedFork]. - /// - /// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then - /// this will return [EngineApiError::NoParentBeaconBlockRootPostCancun]. - /// - /// This implements the following Engine API spec rules: - /// - /// 1. Client software **MUST** check that provided set of parameters and their fields strictly - /// matches the expected one and return `-32602: Invalid params` error if this check fails. - /// Any field having `null` value **MUST** be considered as not provided. - /// - /// 2. Client software **MUST** return `-38005: Unsupported fork` error if the - /// `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the - /// time frame of the Cancun fork. - fn validate_parent_beacon_block_root_presence( - &self, - version: EngineApiMessageVersion, - timestamp: u64, - has_parent_beacon_block_root: bool, - ) -> EngineApiResult<()> { - // 1. Client software **MUST** check that provided set of parameters and their fields - // strictly matches the expected one and return `-32602: Invalid params` error if this - // check fails. Any field having `null` value **MUST** be considered as not provided. - match version { - EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => { - if has_parent_beacon_block_root { - return Err(EngineApiError::ParentBeaconBlockRootNotSupportedBeforeV3) - } - } - EngineApiMessageVersion::V3 => { - if !has_parent_beacon_block_root { - return Err(EngineApiError::NoParentBeaconBlockRootPostCancun) - } - } - }; - - // 2. Client software **MUST** return `-38005: Unsupported fork` error if the - // `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within - // the time frame of the Cancun fork. - self.validate_payload_timestamp(version, timestamp)?; - - Ok(()) - } - - /// Validates the presence or exclusion of fork-specific fields based on the payload attributes - /// and the message version. - fn validate_version_specific_fields( - &self, - version: EngineApiMessageVersion, - payload_or_attrs: &PayloadOrAttributes<'_>, - ) -> EngineApiResult<()> { - self.validate_withdrawals_presence( - version, - payload_or_attrs.timestamp(), - payload_or_attrs.withdrawals().is_some(), - )?; - self.validate_parent_beacon_block_root_presence( - version, - payload_or_attrs.timestamp(), - payload_or_attrs.parent_beacon_block_root().is_some(), - ) - } - /// Validates the `engine_forkchoiceUpdated` payload attributes and executes the forkchoice /// update. /// @@ -555,17 +433,11 @@ where &self, version: EngineApiMessageVersion, state: ForkchoiceState, - payload_attrs: Option, + payload_attrs: Option, ) -> EngineApiResult { if let Some(ref attrs) = payload_attrs { - let attr_validation_res = self.validate_version_specific_fields(version, &attrs.into()); - - #[cfg(feature = "optimism")] - if attrs.optimism_payload_attributes.gas_limit.is_none() && - self.inner.chain_spec.is_optimism() - { - return Err(EngineApiError::MissingGasLimitInPayloadAttributes) - } + let attr_validation_res = + attrs.ensure_well_formed_attributes(&self.inner.chain_spec, version); // From the engine API spec: // @@ -587,7 +459,7 @@ where if fcu_res.is_invalid() { return Ok(fcu_res) } - return Err(err) + return Err(err.into()) } } @@ -596,9 +468,12 @@ where } #[async_trait] -impl EngineApiServer for EngineApi +impl EngineApiServer for EngineApi where Provider: HeaderProvider + BlockReader + StateProviderFactory + EvmEnvProvider + 'static, + EngineT: EngineTypes + 'static + Send, + EngineT::PayloadAttributes: Send, + EngineT::PayloadBuilderAttributes: Send, { /// Handler for `engine_newPayloadV1` /// See also @@ -645,7 +520,7 @@ where async fn fork_choice_updated_v1( &self, fork_choice_state: ForkchoiceState, - payload_attributes: Option, + payload_attributes: Option, ) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV1"); let start = Instant::now(); @@ -660,7 +535,7 @@ where async fn fork_choice_updated_v2( &self, fork_choice_state: ForkchoiceState, - payload_attributes: Option, + payload_attributes: Option, ) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV2"); let start = Instant::now(); @@ -676,7 +551,7 @@ where async fn fork_choice_updated_v3( &self, fork_choice_state: ForkchoiceState, - payload_attributes: Option, + payload_attributes: Option, ) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_forkchoiceUpdatedV3"); let start = Instant::now(); @@ -800,7 +675,10 @@ where } } -impl std::fmt::Debug for EngineApi { +impl std::fmt::Debug for EngineApi +where + EngineT: EngineTypes, +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EngineApi").finish_non_exhaustive() } @@ -812,6 +690,7 @@ mod tests { use assert_matches::assert_matches; use reth_beacon_consensus::BeaconEngineMessage; use reth_interfaces::test_utils::generators::random_block; + use reth_node_builder::EthEngineTypes; use reth_payload_builder::test_utils::spawn_test_payload_service; use reth_primitives::{SealedBlock, B256, MAINNET}; use reth_provider::test_utils::MockEthProvider; @@ -820,7 +699,8 @@ mod tests { use std::sync::Arc; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; - fn setup_engine_api() -> (EngineApiTestHandle, EngineApi>) { + fn setup_engine_api() -> (EngineApiTestHandle, EngineApi, EthEngineTypes>) + { let chain_spec: Arc = MAINNET.clone(); let provider = Arc::new(MockEthProvider::default()); let payload_store = spawn_test_payload_service(); @@ -840,7 +720,7 @@ mod tests { struct EngineApiTestHandle { chain_spec: Arc, provider: Arc, - from_api: UnboundedReceiver, + from_api: UnboundedReceiver>, } #[tokio::test] diff --git a/crates/rpc/rpc-engine-api/src/error.rs b/crates/rpc/rpc-engine-api/src/error.rs index f05befc44..96a5f7ea6 100644 --- a/crates/rpc/rpc-engine-api/src/error.rs +++ b/crates/rpc/rpc-engine-api/src/error.rs @@ -2,6 +2,7 @@ use jsonrpsee_types::error::{ INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, INVALID_PARAMS_MSG, SERVER_ERROR_MSG, }; use reth_beacon_consensus::{BeaconForkChoiceUpdateError, BeaconOnNewPayloadError}; +use reth_node_api::AttributesValidationError; use reth_payload_builder::error::PayloadBuilderError; use reth_primitives::{B256, U256}; use thiserror::Error; @@ -43,27 +44,6 @@ pub enum EngineApiError { /// Requested number of items count: u64, }, - /// Thrown if `PayloadAttributes` provided in engine_forkchoiceUpdated before V3 contains a - /// parent beacon block root - #[error("parent beacon block root not supported before V3")] - ParentBeaconBlockRootNotSupportedBeforeV3, - /// Thrown if engine_forkchoiceUpdatedV1 contains withdrawals - #[error("withdrawals not supported in V1")] - WithdrawalsNotSupportedInV1, - /// Thrown if engine_forkchoiceUpdated contains no withdrawals after Shanghai - #[error("no withdrawals post-Shanghai")] - NoWithdrawalsPostShanghai, - /// Thrown if engine_forkchoiceUpdated contains withdrawals before Shanghai - #[error("withdrawals pre-Shanghai")] - HasWithdrawalsPreShanghai, - /// Thrown if the `PayloadAttributes` provided in engine_forkchoiceUpdated contains no parent - /// beacon block root after Cancun - #[error("no parent beacon block root post-cancun")] - NoParentBeaconBlockRootPostCancun, - /// Thrown if `PayloadAttributes` were provided with a timestamp, but the version of the engine - /// method called is meant for a fork that occurs after the provided timestamp. - #[error("Unsupported fork")] - UnsupportedFork, /// Terminal total difficulty mismatch during transition configuration exchange. #[error( "invalid transition terminal total difficulty: \ @@ -98,6 +78,9 @@ pub enum EngineApiError { /// Fetching the payload failed #[error(transparent)] GetPayloadError(#[from] PayloadBuilderError), + /// The payload or attributes are known to be malformed before processing. + #[error(transparent)] + AttributesValidationError(#[from] AttributesValidationError), /// If the optimism feature flag is enabled, the payload attributes must have a present /// gas limit for the forkchoice updated method. #[cfg(feature = "optimism")] @@ -123,11 +106,24 @@ impl From for jsonrpsee_types::error::ErrorObject<'static> { fn from(error: EngineApiError) -> Self { match error { EngineApiError::InvalidBodiesRange { .. } | - EngineApiError::WithdrawalsNotSupportedInV1 | - EngineApiError::ParentBeaconBlockRootNotSupportedBeforeV3 | - EngineApiError::NoParentBeaconBlockRootPostCancun | - EngineApiError::NoWithdrawalsPostShanghai | - EngineApiError::HasWithdrawalsPreShanghai => { + EngineApiError::AttributesValidationError( + AttributesValidationError::WithdrawalsNotSupportedInV1, + ) | + EngineApiError::AttributesValidationError( + AttributesValidationError::ParentBeaconBlockRootNotSupportedBeforeV3, + ) | + EngineApiError::AttributesValidationError( + AttributesValidationError::NoParentBeaconBlockRootPostCancun, + ) | + EngineApiError::AttributesValidationError( + AttributesValidationError::NoWithdrawalsPostShanghai, + ) | + EngineApiError::AttributesValidationError( + AttributesValidationError::HasWithdrawalsPreShanghai, + ) | + EngineApiError::AttributesValidationError( + AttributesValidationError::InvalidParams(_), + ) => { // Note: the data field is not required by the spec, but is also included by other // clients jsonrpsee_types::error::ErrorObject::owned( @@ -148,7 +144,9 @@ impl From for jsonrpsee_types::error::ErrorObject<'static> { Some(ErrorData::new(error)), ) } - EngineApiError::UnsupportedFork => jsonrpsee_types::error::ErrorObject::owned( + EngineApiError::AttributesValidationError( + AttributesValidationError::UnsupportedFork, + ) => jsonrpsee_types::error::ErrorObject::owned( UNSUPPORTED_FORK_CODE, error.to_string(), None::<()>, @@ -238,7 +236,7 @@ mod tests { ensure_engine_rpc_error( UNSUPPORTED_FORK_CODE, "Unsupported fork", - EngineApiError::UnsupportedFork, + EngineApiError::AttributesValidationError(AttributesValidationError::UnsupportedFork), ); ensure_engine_rpc_error( diff --git a/crates/rpc/rpc-engine-api/src/lib.rs b/crates/rpc/rpc-engine-api/src/lib.rs index 965b0448a..e6e5507e0 100644 --- a/crates/rpc/rpc-engine-api/src/lib.rs +++ b/crates/rpc/rpc-engine-api/src/lib.rs @@ -14,9 +14,6 @@ mod engine_api; /// The Engine API message type. mod message; -/// An type representing either an execution payload or payload attributes. -mod payload; - /// Engine API error. mod error; diff --git a/crates/rpc/rpc-types/src/beacon/payload.rs b/crates/rpc/rpc-types/src/beacon/payload.rs index d660bbbcf..42dbf9eb1 100644 --- a/crates/rpc/rpc-types/src/beacon/payload.rs +++ b/crates/rpc/rpc-types/src/beacon/payload.rs @@ -81,16 +81,14 @@ struct BeaconPayloadAttributes { withdrawals: Option>, #[serde(skip_serializing_if = "Option::is_none")] parent_beacon_block_root: Option, - #[cfg(feature = "optimism")] - #[serde(flatten)] - optimism_payload_attributes: BeaconOptimismPayloadAttributes, } /// Optimism Payload Attributes -#[cfg(feature = "optimism")] #[serde_as] #[derive(Serialize, Deserialize)] struct BeaconOptimismPayloadAttributes { + #[serde(flatten)] + payload_attributes: BeaconPayloadAttributes, #[serde(default, skip_serializing_if = "Option::is_none")] transactions: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -100,6 +98,69 @@ struct BeaconOptimismPayloadAttributes { gas_limit: Option, } +/// A helper module for serializing and deserializing optimism payload attributes for the beacon +/// API. +/// +/// See docs for [beacon_api_payload_attributes]. +pub mod beacon_api_payload_attributes_optimism { + use super::*; + use crate::engine::{OptimismPayloadAttributes, PayloadAttributes}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Serialize the payload attributes for the beacon API. + pub fn serialize( + payload_attributes: &OptimismPayloadAttributes, + serializer: S, + ) -> Result + where + S: Serializer, + { + let beacon_api_payload_attributes = BeaconPayloadAttributes { + timestamp: payload_attributes.payload_attributes.timestamp, + prev_randao: payload_attributes.payload_attributes.prev_randao, + suggested_fee_recipient: payload_attributes.payload_attributes.suggested_fee_recipient, + withdrawals: payload_attributes.payload_attributes.withdrawals.clone(), + parent_beacon_block_root: payload_attributes + .payload_attributes + .parent_beacon_block_root, + }; + + let op_beacon_api_payload_attributes = BeaconOptimismPayloadAttributes { + payload_attributes: beacon_api_payload_attributes, + transactions: payload_attributes.transactions.clone(), + no_tx_pool: payload_attributes.no_tx_pool, + gas_limit: payload_attributes.gas_limit, + }; + + op_beacon_api_payload_attributes.serialize(serializer) + } + + /// Deserialize the payload attributes for the beacon API. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let beacon_api_payload_attributes = + BeaconOptimismPayloadAttributes::deserialize(deserializer)?; + Ok(OptimismPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: beacon_api_payload_attributes.payload_attributes.timestamp, + prev_randao: beacon_api_payload_attributes.payload_attributes.prev_randao, + suggested_fee_recipient: beacon_api_payload_attributes + .payload_attributes + .suggested_fee_recipient, + withdrawals: beacon_api_payload_attributes.payload_attributes.withdrawals, + parent_beacon_block_root: beacon_api_payload_attributes + .payload_attributes + .parent_beacon_block_root, + }, + transactions: beacon_api_payload_attributes.transactions, + no_tx_pool: beacon_api_payload_attributes.no_tx_pool, + gas_limit: beacon_api_payload_attributes.gas_limit, + }) + } +} + /// A helper module for serializing and deserializing the payload attributes for the beacon API. /// /// The beacon API encoded object has equivalent fields to the @@ -125,12 +186,6 @@ pub mod beacon_api_payload_attributes { suggested_fee_recipient: payload_attributes.suggested_fee_recipient, withdrawals: payload_attributes.withdrawals.clone(), parent_beacon_block_root: payload_attributes.parent_beacon_block_root, - #[cfg(feature = "optimism")] - optimism_payload_attributes: BeaconOptimismPayloadAttributes { - transactions: payload_attributes.optimism_payload_attributes.transactions.clone(), - no_tx_pool: payload_attributes.optimism_payload_attributes.no_tx_pool, - gas_limit: payload_attributes.optimism_payload_attributes.gas_limit, - }, }; beacon_api_payload_attributes.serialize(serializer) } @@ -147,14 +202,6 @@ pub mod beacon_api_payload_attributes { suggested_fee_recipient: beacon_api_payload_attributes.suggested_fee_recipient, withdrawals: beacon_api_payload_attributes.withdrawals, parent_beacon_block_root: beacon_api_payload_attributes.parent_beacon_block_root, - #[cfg(feature = "optimism")] - optimism_payload_attributes: crate::eth::engine::OptimismPayloadAttributes { - transactions: beacon_api_payload_attributes - .optimism_payload_attributes - .transactions, - no_tx_pool: beacon_api_payload_attributes.optimism_payload_attributes.no_tx_pool, - gas_limit: beacon_api_payload_attributes.optimism_payload_attributes.gas_limit, - }, }) } } diff --git a/crates/rpc/rpc-types/src/eth/engine/payload.rs b/crates/rpc/rpc-types/src/eth/engine/payload.rs index aa3ca4223..3887544d1 100644 --- a/crates/rpc/rpc-types/src/eth/engine/payload.rs +++ b/crates/rpc/rpc-types/src/eth/engine/payload.rs @@ -623,17 +623,15 @@ pub struct PayloadAttributes { /// See also #[serde(skip_serializing_if = "Option::is_none")] pub parent_beacon_block_root: Option, - /// Optimism Payload Attributes - #[cfg(feature = "optimism")] - #[serde(flatten)] - pub optimism_payload_attributes: OptimismPayloadAttributes, } /// Optimism Payload Attributes -#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg(feature = "optimism")] pub struct OptimismPayloadAttributes { + /// The payload attributes + #[serde(flatten)] + pub payload_attributes: PayloadAttributes, /// Transactions is a field for rollups: the transactions list is forced into the block #[serde(skip_serializing_if = "Option::is_none")] pub transactions: Option>,