Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rpc, sync: support Engine API V3 #1931

Merged
merged 10 commits into from
Mar 27, 2024
289 changes: 244 additions & 45 deletions silkworm/rpc/commands/engine_api.cpp

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions silkworm/rpc/commands/engine_api.hpp
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
#include <boost/asio/io_context.hpp>
#include <boost/asio/thread_pool.hpp>
#include <nlohmann/json.hpp>
#include <tl/expected.hpp>

#include <silkworm/infra/concurrency/private_service.hpp>
#include <silkworm/rpc/ethbackend/backend.hpp>
@@ -50,15 +51,33 @@ class EngineRpcApi {
Task<void> handle_engine_exchange_capabilities(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_get_payload_v1(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_get_payload_v2(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_get_payload_v3(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_get_payload_bodies_by_hash_v1(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_get_payload_bodies_by_range_v1(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_new_payload_v1(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_new_payload_v2(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_new_payload_v3(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_forkchoice_updated_v1(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_forkchoice_updated_v2(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_forkchoice_updated_v3(const nlohmann::json& request, nlohmann::json& reply);
Task<void> handle_engine_exchange_transition_configuration_v1(const nlohmann::json& request, nlohmann::json& reply);

private:
// TODO(canepat) remove this method and pass ChainConfig as constructor parameter
Task<std::optional<silkworm::ChainConfig>> read_chain_config();

using ApiError = std::pair<int, std::string>;
using ValidationError = tl::expected<void, ApiError>;

ValidationError validate_fork_choice_state_v1(const ForkChoiceState& state);

ValidationError validate_payload_attributes_v2(const std::optional<PayloadAttributes>& attributes,
const ForkChoiceUpdatedReply& reply,
const std::optional<silkworm::ChainConfig>& config);
ValidationError validate_payload_attributes_v3(const std::optional<PayloadAttributes>& attributes,
const ForkChoiceUpdatedReply& reply,
const std::optional<silkworm::ChainConfig>& config);

ethdb::Database* database_;
ethbackend::BackEnd* backend_;

3 changes: 3 additions & 0 deletions silkworm/rpc/commands/rpc_api_table.cpp
Original file line number Diff line number Diff line change
@@ -222,12 +222,15 @@ void RpcApiTable::add_engine_handlers() {
method_handlers_[json_rpc::method::k_engine_exchangeCapabilities] = &commands::RpcApi::handle_engine_exchange_capabilities;
method_handlers_[json_rpc::method::k_engine_getPayloadV1] = &commands::RpcApi::handle_engine_get_payload_v1;
method_handlers_[json_rpc::method::k_engine_getPayloadV2] = &commands::RpcApi::handle_engine_get_payload_v2;
method_handlers_[json_rpc::method::k_engine_getPayloadV3] = &commands::RpcApi::handle_engine_get_payload_v3;
method_handlers_[json_rpc::method::k_engine_getPayloadBodiesByHashV1] = &commands::RpcApi::handle_engine_get_payload_bodies_by_hash_v1;
method_handlers_[json_rpc::method::k_engine_getPayloadBodiesByRangeV1] = &commands::RpcApi::handle_engine_get_payload_bodies_by_range_v1;
method_handlers_[json_rpc::method::k_engine_newPayloadV1] = &commands::RpcApi::handle_engine_new_payload_v1;
method_handlers_[json_rpc::method::k_engine_newPayloadV2] = &commands::RpcApi::handle_engine_new_payload_v2;
method_handlers_[json_rpc::method::k_engine_newPayloadV3] = &commands::RpcApi::handle_engine_new_payload_v3;
method_handlers_[json_rpc::method::k_engine_forkchoiceUpdatedV1] = &commands::RpcApi::handle_engine_forkchoice_updated_v1;
method_handlers_[json_rpc::method::k_engine_forkchoiceUpdatedV2] = &commands::RpcApi::handle_engine_forkchoice_updated_v2;
method_handlers_[json_rpc::method::k_engine_forkchoiceUpdatedV3] = &commands::RpcApi::handle_engine_forkchoice_updated_v3;
method_handlers_[json_rpc::method::k_engine_exchangeTransitionConfiguration] = &commands::RpcApi::handle_engine_exchange_transition_configuration_v1;
}

4 changes: 2 additions & 2 deletions silkworm/rpc/ethbackend/backend.hpp
Original file line number Diff line number Diff line change
@@ -40,8 +40,8 @@ class BackEnd {
virtual Task<std::string> client_version() = 0;
virtual Task<uint64_t> net_peer_count() = 0;
virtual Task<ExecutionPayloadAndValue> engine_get_payload(uint64_t payload_id) = 0;
virtual Task<PayloadStatus> engine_new_payload(const ExecutionPayload& payload) = 0;
virtual Task<ForkChoiceUpdatedReply> engine_forkchoice_updated(const ForkChoiceUpdatedRequest& fcu_request) = 0;
virtual Task<PayloadStatus> engine_new_payload(const rpc::NewPayloadRequest& request) = 0;
virtual Task<ForkChoiceUpdatedReply> engine_forkchoice_updated(const ForkChoiceUpdatedRequest& request) = 0;
virtual Task<ExecutionPayloadBodies> engine_get_payload_bodies_by_hash(const std::vector<Hash>& block_hashes) = 0;
virtual Task<ExecutionPayloadBodies> engine_get_payload_bodies_by_range(BlockNum start, uint64_t count) = 0;
virtual Task<NodeInfos> engine_node_info() = 0;
4 changes: 2 additions & 2 deletions silkworm/rpc/ethbackend/remote_backend.cpp
Original file line number Diff line number Diff line change
@@ -129,10 +129,10 @@ Task<ExecutionPayloadAndValue> RemoteBackEnd::engine_get_payload(uint64_t payloa
co_return ExecutionPayloadAndValue{payload, value};
}

Task<PayloadStatus> RemoteBackEnd::engine_new_payload(const ExecutionPayload& payload) {
Task<PayloadStatus> RemoteBackEnd::engine_new_payload(const rpc::NewPayloadRequest& request) {
const auto start_time = clock_time::now();
UnaryRpc<&::remote::ETHBACKEND::StubInterface::AsyncEngineNewPayload> npc_rpc{*stub_, grpc_context_};
auto req{encode_execution_payload(payload)};
auto req{encode_execution_payload(request.execution_payload)};
const auto reply = co_await npc_rpc.finish_on(executor_, req);
PayloadStatus payload_status = decode_payload_status(reply);
SILK_TRACE << "RemoteBackEnd::engine_new_payload data=" << payload_status << " t=" << clock_time::since(start_time);
2 changes: 1 addition & 1 deletion silkworm/rpc/ethbackend/remote_backend.hpp
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ class RemoteBackEnd final : public BackEnd {
Task<std::string> client_version() override;
Task<uint64_t> net_peer_count() override;
Task<ExecutionPayloadAndValue> engine_get_payload(uint64_t payload_id) override;
Task<PayloadStatus> engine_new_payload(const ExecutionPayload& payload) override;
Task<PayloadStatus> engine_new_payload(const rpc::NewPayloadRequest& request) override;
Task<ForkChoiceUpdatedReply> engine_forkchoice_updated(const ForkChoiceUpdatedRequest& fcu_request) override;
Task<ExecutionPayloadBodies> engine_get_payload_bodies_by_hash(const std::vector<Hash>& block_hashes) override;
Task<ExecutionPayloadBodies> engine_get_payload_bodies_by_range(BlockNum start, uint64_t count) override;
111 changes: 59 additions & 52 deletions silkworm/rpc/ethbackend/remote_backend_test.cpp
Original file line number Diff line number Diff line change
@@ -315,64 +315,71 @@ TEST_CASE_METHOD(EthBackendTest, "BackEnd::engine_new_payload", "[silkworm][rpc]
bloom.fill(0);
bloom[0] = 0x12;
const auto transaction{*from_hex("0xf92ebdeab45d368f6354e8c5a8ac586c")};
const ExecutionPayload payload_v1{
.version = ExecutionPayload::V1,
.timestamp = 0x5,
.gas_limit = 0x1c9c380,
.gas_used = 0x9,
.suggested_fee_recipient = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address,
.state_root = 0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf43_bytes32,
.receipts_root = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32,
.parent_hash = 0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a_bytes32,
.block_hash = 0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858_bytes32,
.prev_randao = 0x0000000000000000000000000000000000000000000000000000000000000001_bytes32,
.base_fee = 0x7,
.logs_bloom = bloom,
.transactions = {transaction},
const NewPayloadRequest request_v1{
.execution_payload = ExecutionPayload{
.version = ExecutionPayload::V1,
.timestamp = 0x5,
.gas_limit = 0x1c9c380,
.gas_used = 0x9,
.suggested_fee_recipient = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address,
.state_root = 0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf43_bytes32,
.receipts_root = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32,
.parent_hash = 0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a_bytes32,
.block_hash = 0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858_bytes32,
.prev_randao = 0x0000000000000000000000000000000000000000000000000000000000000001_bytes32,
.base_fee = 0x7,
.logs_bloom = bloom,
.transactions = {transaction},
},
};
const ExecutionPayload payload_v2_no_w{
.version = ExecutionPayload::V2,
.timestamp = 0x5,
.gas_limit = 0x1c9c380,
.gas_used = 0x9,
.suggested_fee_recipient = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address,
.state_root = 0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf43_bytes32,
.receipts_root = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32,
.parent_hash = 0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a_bytes32,
.block_hash = 0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858_bytes32,
.prev_randao = 0x0000000000000000000000000000000000000000000000000000000000000001_bytes32,
.base_fee = 0x7,
.logs_bloom = bloom,
.transactions = {transaction},
.withdrawals = std::vector<Withdrawal>{}};
const ExecutionPayload payload_v2_w{
.version = ExecutionPayload::V2,
.timestamp = 0x5,
.gas_limit = 0x1c9c380,
.gas_used = 0x9,
.suggested_fee_recipient = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address,
.state_root = 0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf43_bytes32,
.receipts_root = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32,
.parent_hash = 0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a_bytes32,
.block_hash = 0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858_bytes32,
.prev_randao = 0x0000000000000000000000000000000000000000000000000000000000000001_bytes32,
.base_fee = 0x7,
.logs_bloom = bloom,
.transactions = {transaction},
.withdrawals = std::vector<Withdrawal>{
{.index = 6, .validator_index = 12, .address = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address, .amount = 10'000}},
const NewPayloadRequest request_v2_no_w{
.execution_payload = ExecutionPayload{
.version = ExecutionPayload::V2,
.timestamp = 0x5,
.gas_limit = 0x1c9c380,
.gas_used = 0x9,
.suggested_fee_recipient = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address,
.state_root = 0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf43_bytes32,
.receipts_root = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32,
.parent_hash = 0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a_bytes32,
.block_hash = 0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858_bytes32,
.prev_randao = 0x0000000000000000000000000000000000000000000000000000000000000001_bytes32,
.base_fee = 0x7,
.logs_bloom = bloom,
.transactions = {transaction},
.withdrawals = std::vector<Withdrawal>{},
},
};
const std::vector<ExecutionPayload> payloads = {payload_v1, payload_v2_no_w, payload_v2_w};
for (std::size_t i{0}; i < payloads.size(); ++i) {
const auto& execution_payload = payloads[i];
const NewPayloadRequest request_v2_w{
.execution_payload = ExecutionPayload{
.version = ExecutionPayload::V2,
.timestamp = 0x5,
.gas_limit = 0x1c9c380,
.gas_used = 0x9,
.suggested_fee_recipient = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address,
.state_root = 0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf43_bytes32,
.receipts_root = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32,
.parent_hash = 0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a_bytes32,
.block_hash = 0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858_bytes32,
.prev_randao = 0x0000000000000000000000000000000000000000000000000000000000000001_bytes32,
.base_fee = 0x7,
.logs_bloom = bloom,
.transactions = {transaction},
.withdrawals = std::vector<Withdrawal>{
{.index = 6, .validator_index = 12, .address = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address, .amount = 10'000}},
},
};
const std::vector<NewPayloadRequest> requests = {request_v1, request_v2_no_w, request_v2_w};
for (std::size_t i{0}; i < requests.size(); ++i) {
const auto& new_payload_request = requests[i];

SECTION("call engine_new_payload and get VALID status [i=" + std::to_string(i) + "]") {
::remote::EnginePayloadStatus response;
response.set_allocated_latest_valid_hash(make_h256(0, 0, 0, 0x40));
response.set_status(::remote::EngineStatus::VALID);
response.set_validation_error("some error");
EXPECT_CALL(reader, Finish).WillOnce(test::finish_with(grpc_context_, std::move(response)));
const auto payload_status = run<&ethbackend::RemoteBackEnd::engine_new_payload>(execution_payload);
const auto payload_status = run<&ethbackend::RemoteBackEnd::engine_new_payload>(new_payload_request);
CHECK(payload_status.status == "VALID");
CHECK(payload_status.latest_valid_hash == 0x0000000000000000000000000000000000000000000000000000000000000040_bytes32);
CHECK(payload_status.validation_error == "some error");
@@ -390,22 +397,22 @@ TEST_CASE_METHOD(EthBackendTest, "BackEnd::engine_new_payload", "[silkworm][rpc]
::remote::EnginePayloadStatus response;
response.set_status(engine_status);
EXPECT_CALL(reader, Finish).WillOnce(test::finish_with(grpc_context_, std::move(response)));
const auto payload_status = run<&ethbackend::RemoteBackEnd::engine_new_payload>(execution_payload);
const auto payload_status = run<&ethbackend::RemoteBackEnd::engine_new_payload>(new_payload_request);
CHECK(payload_status.status == engine_status_name);
}
}

SECTION("call engine_new_payload and get empty payload [i=" + std::to_string(i) + "]") {
EXPECT_CALL(reader, Finish).WillOnce(test::finish_ok(grpc_context_));
const auto payload_status = run<&ethbackend::RemoteBackEnd::engine_new_payload>(execution_payload);
const auto payload_status = run<&ethbackend::RemoteBackEnd::engine_new_payload>(new_payload_request);
CHECK(payload_status.status == "VALID"); // Default value in interfaces is Valid
CHECK(payload_status.latest_valid_hash == std::nullopt);
CHECK(payload_status.validation_error == std::nullopt);
}

SECTION("call engine_new_payload and get error [i=" + std::to_string(i) + "]") {
EXPECT_CALL(reader, Finish).WillOnce(test::finish_cancelled(grpc_context_));
CHECK_THROWS_AS((run<&ethbackend::RemoteBackEnd::engine_new_payload>(execution_payload)), boost::system::system_error);
CHECK_THROWS_AS((run<&ethbackend::RemoteBackEnd::engine_new_payload>(new_payload_request)), boost::system::system_error);
}
}
}
28 changes: 25 additions & 3 deletions silkworm/rpc/json/execution_payload.cpp
Original file line number Diff line number Diff line change
@@ -66,14 +66,23 @@ void from_json(const nlohmann::json& json, ExecutionPayload& execution_payload)
"ExecutionPayload: invalid hex transaction: " + hex_transaction.dump()};
}
}
// Optionally parse withdrawals
// Optionally parse V2 fields
std::optional<std::vector<Withdrawal>> withdrawals;
if (json.contains("withdrawals")) {
withdrawals = json.at("withdrawals").get<std::vector<Withdrawal>>();
}

// Optional parse V3 fields
std::optional<uint64_t> blob_gas_used;
if (json.contains("blobGasUsed")) {
blob_gas_used = from_quantity(json.at("blobGasUsed").get<std::string>());
}
std::optional<uint64_t> excess_blob_gas;
if (json.contains("excessBlobGas")) {
excess_blob_gas = from_quantity(json.at("excessBlobGas").get<std::string>());
}

execution_payload = ExecutionPayload{
.version = withdrawals ? ExecutionPayload::V2 : ExecutionPayload::V1,
.number = from_quantity(json.at("blockNumber").get<std::string>()),
.timestamp = from_quantity(json.at("timestamp").get<std::string>()),
.gas_limit = from_quantity(json.at("gasLimit").get<std::string>()),
@@ -88,7 +97,20 @@ void from_json(const nlohmann::json& json, ExecutionPayload& execution_payload)
.logs_bloom = logs_bloom,
.extra_data = *silkworm::from_hex(json.at("extraData").get<std::string>()),
.transactions = transactions,
.withdrawals = withdrawals};
.withdrawals = std::move(withdrawals),
.blob_gas_used = blob_gas_used,
.excess_blob_gas = excess_blob_gas,
};

// Set the ExecutionPayload version (default is V1)
SILKWORM_ASSERT(execution_payload.version == ExecutionPayload::V1);
if (execution_payload.withdrawals) {
if (execution_payload.blob_gas_used && execution_payload.excess_blob_gas) {
execution_payload.version = ExecutionPayload::V3;
} else {
execution_payload.version = ExecutionPayload::V2;
}
}
}

void to_json(nlohmann::json& json, const ExecutionPayloadAndValue& reply) {
30 changes: 27 additions & 3 deletions silkworm/rpc/json/payload_attributes.cpp
Original file line number Diff line number Diff line change
@@ -16,10 +16,10 @@

#include "payload_attributes.hpp"

#include <cstring>
#include <string>
#include <utility>

#include <silkworm/core/common/util.hpp>
#include <silkworm/core/types/address.hpp>

#include "types.hpp"

@@ -32,11 +32,35 @@ void to_json(nlohmann::json& json, const PayloadAttributes& payload_attributes)
}

void from_json(const nlohmann::json& json, PayloadAttributes& payload_attributes) {
// Optionally parse V2 fields
std::optional<std::vector<Withdrawal>> withdrawals;
if (json.contains("withdrawals")) {
withdrawals = json.at("withdrawals").get<std::vector<Withdrawal>>();
}

// Optionally parse V3 fields
std::optional<evmc::bytes32> parent_beacon_block_root;
if (json.contains("parentBeaconBlockRoot")) {
parent_beacon_block_root = json.at("parentBeaconBlockRoot").get<evmc::bytes32>();
}

payload_attributes = PayloadAttributes{
.timestamp = static_cast<uint64_t>(std::stol(json.at("timestamp").get<std::string>(), nullptr, 16)),
.timestamp = from_quantity(json.at("timestamp").get<std::string>()),
.prev_randao = json.at("prevRandao").get<evmc::bytes32>(),
.suggested_fee_recipient = json.at("suggestedFeeRecipient").get<evmc::address>(),
.withdrawals = std::move(withdrawals),
.parent_beacon_block_root = parent_beacon_block_root,
};

// Set the PayloadAttributes version (default is V1)
SILKWORM_ASSERT(payload_attributes.version == PayloadAttributes::V1);
if (payload_attributes.withdrawals) {
if (payload_attributes.parent_beacon_block_root) {
payload_attributes.version = PayloadAttributes::V3;
} else {
payload_attributes.version = PayloadAttributes::V2;
}
}
}

} // namespace silkworm::rpc
Loading