From c62e62a0da2cb4eab69e01139b9d7ec643826214 Mon Sep 17 00:00:00 2001 From: Louis Ponet Date: Tue, 27 May 2025 20:44:26 +0100 Subject: [PATCH 1/7] Makefile improvements + pectra v4 messages (#181) --- Makefile | 67 +++++++++++++++++++--------------- based/bin/overseer/src/main.rs | 2 +- based/crates/common/src/api.rs | 29 ++++++++++++++- follower_node/compose.yml | 9 +++-- main_node/compose.yml | 7 +++- 5 files changed, 77 insertions(+), 37 deletions(-) diff --git a/Makefile b/Makefile index 4b849e092..98b833600 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ OS := $(shell uname -s) .DEFAULT_GOAL := help # Variables -IMAGE_KEY_TO_ADDRESS:=ghcr.io/gattaca-com/based-op/key-to-address:v0.1.0 +IMAGE_KEY_TO_ADDRESS:=ghcr.io/gattaca-com/based-op/key-to-address:latest + START_GATEWAY_COMPOSE_FILES := -f .local_gateway_and_follower/compose.yml ifeq ($(OS),Darwin) @@ -33,22 +34,28 @@ docs: ## πŸ“š Build local docs npm run build && \ npm run start -build: build-portal build-gateway build-based-op-node build-based-op-geth build-registry build-overseer## πŸ—οΈ Build +build: build-portal build-gateway build-based-op-node build-based-op-geth build-registry ## πŸ—οΈ Build build-portal: ## πŸ—οΈ Build based portal - docker build -t based_portal_local -f ./based/portal.Dockerfile --build-context reth=./reth ./based + docker build -t local_based_portal -f ./based/portal.Dockerfile --build-context reth=./reth ./based build-registry: ## πŸ—οΈ Build based registry - docker build -t based_registry_local -f ./based/registry.Dockerfile --build-context reth=./reth ./based + docker build -t local_based_registry -f ./based/registry.Dockerfile --build-context reth=./reth ./based build-gateway: ## πŸ—οΈ Build based gateway - docker build -t based_gateway_local -f ./based/gateway.Dockerfile --build-context reth=./reth ./based + docker build -t local_based_gateway -f ./based/gateway.Dockerfile --build-context reth=./reth ./based build-based-op-geth: ## πŸ—οΈ Build OP geth from op-eth directory - docker build -t based_op_geth ../based-op-geth + docker build -t local_based_op_geth ../based-op-geth build-based-op-node: ## πŸ—οΈ Build OP geth from op-eth directory - docker build -t based_op_node ../based-op-node + cd ../based-optimism && \ + IMAGE_TAGS=develop \ + docker buildx bake \ + -f docker-bake.hcl \ + --set op-node.tags=local_based_op_node \ + --load \ + op-node build-rabby-chrom: ## πŸ—οΈ Build modified Rabby wallet for Google Chrome and Firefox cd rabby && \ @@ -56,17 +63,12 @@ build-rabby-chrom: ## πŸ—οΈ Build modified Rabby wallet for Google Chrome and yarn build:pro && \ yarn build:pro:mv2 -build-key_to_address: ## πŸ—οΈ Build based gateway from based directory - docker build -t key_to_address -f ./based/key_to_address.Dockerfile --build-context reth=./reth ./based - L1_CHAIN_ID?=11155111 L2_CHAIN_ID?=$(shell \ RAW=$$(od -An -N2 -tu2 /dev/urandom | tr -d ' '); \ echo $$((RAW % 50000 + 1)); \ ) - -L2_CHAIN_ID_HEX := $(shell ) - +L2_CHAIN_ID_HEX:=$(shell printf "0x%064x" $(L2_CHAIN_ID)) PORTAL?=http://18.185.199.51:8080 L1_RPC_URL?=http://34.194.193.217:8545 L1_BEACON_RPC_URL?=http://34.194.193.217:5052 @@ -76,7 +78,7 @@ GATEWAY_SEQUENCING_KEY ?= $(shell \ grep -m1 '^GATEWAY_SEQUENCING_KEY=' .local_gateway_and_follower/.env \ | cut -d= -f2 \ ) -_GATEWAY_KEY_AND_WALLET:=$(shell docker run --rm -i key_to_address $(GATEWAY_SEQUENCING_KEY)) +_GATEWAY_KEY_AND_WALLET:=$(shell docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(GATEWAY_SEQUENCING_KEY)) GATEWAY_SEQUENCING_KEY:=$(word 1,$(_GATEWAY_KEY_AND_WALLET)) GATEWAY_SEQUENCING_ADDRESS:=$(word 2,$(_GATEWAY_KEY_AND_WALLET)) @@ -159,7 +161,7 @@ start-gateway: mkdir -p $(BASED_GATEWAY_DATA_DIR); \ fi - @wallet=$$(docker run --rm -i $(KEY_TO_ADDRESS_IMAGE) $(GATEWAY_SEQUENCING_KEY)); \ + @wallet=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(GATEWAY_SEQUENCING_KEY)); \ echo "...Done"; \ echo; \ echo "Starting with the following generated .env:"; \ @@ -218,13 +220,12 @@ deploy-chain: fi @mkdir -p .local_main_node/config @docker run -v $$(pwd)/.local_main_node/config:/config --entrypoint sh --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.11 -c "/op-deployer init --l1-chain-id $(L1_CHAIN_ID) --l2-chain-ids $(L2_CHAIN_ID) --workdir /config && chmod 666 /config/*" - @wallet_batcher=$$(docker run --rm -i $(KEY_TO_ADDRESS_IMAGE) $(OP_PROPOSER_KEY) | tail -n1); \ - wallet_proposer=$$(docker run --rm -i $(KEY_TO_ADDRESS_IMAGE) $(OP_BATCHER_KEY) | tail -n1); \ - wallet_main=$$(docker run --rm -i $(KEY_TO_ADDRESS_IMAGE) $(MAIN_KEY) | tail -n1); \ - l2_chain_id_hex=$$(printf "0x%064x" $(L2_CHAIN_ID)); \ + @wallet_batcher=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(OP_PROPOSER_KEY) | tail -n1); \ + wallet_proposer=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(OP_PROPOSER_KEY) | tail -n1); \ + wallet_main=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(MAIN_KEY) | tail -n1); \ sed -E \ -e "s@L1_CHAIN_ID@$(L1_CHAIN_ID)@g" \ - -e "s@L2_CHAIN_ID@$${l2_chain_id_hex}@g" \ + -e "s@L2_CHAIN_ID@$(L2_CHAIN_ID_HEX)@g" \ -e "s@VAULT_WALLET@$${wallet_main}@g" \ -e "s@OP_BATCHER_WALLET@$${wallet_batcher}@g" \ -e "s@OP_PROPOSER_WALLET@$${wallet_proposer}@g" \ @@ -232,16 +233,16 @@ deploy-chain: > .local_main_node/config/intent.toml @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.11 apply --workdir /config --l1-rpc-url $(L1_RPC_URL) --private-key $(MAIN_KEY) - @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 inspect genesis --workdir /config $${l2_chain_id_hex} > $$(pwd)/.local_main_node/config/genesis.json - @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 inspect rollup --workdir /config $${l2_chain_id_hex} > $$(pwd)/.local_main_node/config/rollup.json + @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 inspect genesis --workdir /config $(L2_CHAIN_ID_HEX) > $$(pwd)/.local_main_node/config/genesis.json + @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 inspect rollup --workdir /config $(L2_CHAIN_ID_HEX) > $$(pwd)/.local_main_node/config/rollup.json @docker run -v $$(pwd)/.local_main_node/config:/config --entrypoint sh --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 -c "chmod 666 /config/*" @docker run -v $$(pwd)/.local_main_node/config:/config --rm -i imega/jq '.chain_op_config = {"eip1559Elasticity":6, "eip1559Denominator":50, "eip1559DenominatorCanyon":250}' /config/rollup.json \ > $$(pwd)/.local_main_node/config/rollup.json.tmp && mv $$(pwd)/.local_main_node/config/rollup.json.tmp $$(pwd)/.local_main_node/config/config.json - @blockNumber=$$(docker run -v $$(pwd)/.local_main_node/config:/config -i imega/jq -r '.genesis.l1.number' /config/rollup.json); \ + @blockNumber=$$(docker run -v $$(pwd)/.local_main_node/config:/config --rm -i imega/jq -r '.genesis.l1.number' /config/rollup.json); \ hex=$$(printf "0x%x" $$blockNumber); \ hash=$$(curl -s -X POST -H 'Content-Type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["'"$$hex"'",false]}' \ - $(L1_RPC_URL) | docker run -i imega/jq -r '.result.hash'); \ + $(L1_RPC_URL) | docker run --rm -i imega/jq -r '.result.hash'); \ docker run -v $$(pwd)/.local_main_node/config:/config --rm -i imega/jq --arg h "$$hash" '.genesis.l1.hash = $$h' /config/rollup.json > $$(pwd)/.local_main_node/config/rollup.json.tmp && mv $$(pwd)/.local_main_node/config/rollup.json.tmp $$(pwd)/.local_main_node/config/rollup.json @openssl rand -hex 32 | tr -d '\n' | sed 's/^/0x/' > .local_main_node/config/jwt @@ -273,8 +274,16 @@ $(error STATE_JSON is undefined! Please invoke like \ `make $(MAKECMDGOALS) ROLLUP_JSON=… GENESIS_JSON=… STATE_JSON=… OP_GETH_DATA_DIR=… OP_NODE_DATA_DIR=…`) endif -OP_GETH_DATA_DIR?=.local_main_node/data/geth -OP_NODE_DATA_DIR?=.local_main_node/data/node +ifndef OP_GETH_DATA_DIR +$(error OP_GETH_DATA_DIR is undefined! Please invoke like \ + `make $(MAKECMDGOALS) ROLLUP_JSON=… GENESIS_JSON=… STATE_JSON=… OP_GETH_DATA_DIR=… OP_NODE_DATA_DIR=…`) +endif + +ifndef OP_NODE_DATA_DIR +$(error OP_NODE_DATA_DIR is undefined! Please invoke like \ + `make $(MAKECMDGOALS) ROLLUP_JSON=… GENESIS_JSON=… STATE_JSON=… OP_GETH_DATA_DIR=… OP_NODE_DATA_DIR=…`) +endif + endif # ──────────────────────────────────────────────────────────────────────────────── config-main-node: @@ -288,12 +297,12 @@ config-main-node: @cp $(ROLLUP_JSON) .local_main_node/config @cp $(GENESIS_JSON) .local_main_node/config @cp $(STATE_JSON) .local_main_node/config - @if [ "$(OP_GETH_DATA_DIR)" != ".local_main_node/data/geth" ] && [ ! -d ".local_main_node/data/geth" ] && [ -d "$(OP_GETH_DATA_DIR)" ]; then \ + @if [ "$(OP_GETH_DATA_DIR)" != ".local_main_node/data/geth" ] && [ ! -d ".local_main_node/data/geth" ]; then \ ln -s $(OP_GETH_DATA_DIR) .local_main_node/data/geth; \ else \ mkdir -p $(BASED_OP_GETH_DATA_DIR); \ fi - @if [ "$(OP_NODE_DATA_DIR)" != ".local_main_node/data/node" ] && [ ! -d ".local_main_node/data/node" ] && [ -d "$(OP_NODE_DATA_DIR)" ]; then \ + @if [ "$(OP_NODE_DATA_DIR)" != ".local_main_node/data/node" ] && [ ! -d ".local_main_node/data/node" ]; then \ ln -s $(OP_NODE_DATA_DIR) .local_main_node/data/node; \ else \ mkdir -p $(OP_NODE_DATA_DIR); \ @@ -308,7 +317,7 @@ config-main-node: @echo # By default these will be pointing to directories under .local_ -start-main-node: build-portal build-registry +start-main-node: @if docker ps --format '{{.Names}}' | grep -wq op-node ; then \ echo "❌ Main node already running."; \ exit 1; \ diff --git a/based/bin/overseer/src/main.rs b/based/bin/overseer/src/main.rs index 1a19e7991..4994021f7 100644 --- a/based/bin/overseer/src/main.rs +++ b/based/bin/overseer/src/main.rs @@ -275,7 +275,7 @@ fn main() { tracing::info!("Overseer starting"); let args = OverseerArgs::parse(); let mut consumers = OverseerConsumers::new(&args); - let mut overseer: Overseer = consumers.rollup_config().expect("couldn't connect to portal").into(); + let mut overseer: Overseer = consumers.rollup_config().expect("couldn't read rollup_config").into(); let genesis_time = overseer.genesis_time(); let block_duration = overseer.block_duration(); diff --git a/based/crates/common/src/api.rs b/based/crates/common/src/api.rs index 472792673..7025d9062 100644 --- a/based/crates/common/src/api.rs +++ b/based/crates/common/src/api.rs @@ -3,12 +3,12 @@ use std::collections::HashMap; use alloy_primitives::{Address, B256, Bytes, U256}; use alloy_rpc_types::{ BlockId, BlockNumberOrTag, - engine::{ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, + engine::{ExecutionPayloadV3, ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, }; use jsonrpsee::proc_macros::rpc; use op_alloy_consensus::OpTxEnvelope; use op_alloy_rpc_types::OpTransactionReceipt; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpPayloadAttributes}; use reqwest::Url; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -59,9 +59,34 @@ pub trait EngineApi { parent_beacon_block_root: B256, ) -> RpcResult; + #[method(name = "newPayloadV4")] + async fn new_payload_v4( + &self, + payload: ExecutionPayloadV3, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + _execution_requests: Vec, + ) -> RpcResult { + self.new_payload_v3(payload, versioned_hashes, parent_beacon_block_root) + } + /// Used to fetch an execution payload from a previous `payload_id` set in `forkchoiceUpdatedV3` #[method(name = "getPayloadV3")] async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult; + + #[method(name = "getPayloadV4")] + async fn get_payload_v4(&self, payload_id: PayloadId) -> RpcResult { + let execution_payload = self.get_payload_v3(payload_id).await?; + + Ok(OpExecutionPayloadEnvelopeV4 { + execution_payload: execution_payload.execution_payload, + block_value: execution_payload.block_value, + blobs_bundle: execution_payload.blobs_bundle, + should_override_builder: execution_payload.should_override_builder, + parent_beacon_block_root: execution_payload.parent_beacon_block_root, + execution_requests: vec![], + }) + } } /// The Eth API is used to interact with the EL directly. diff --git a/follower_node/compose.yml b/follower_node/compose.yml index c8259f35a..0ae06e1e6 100644 --- a/follower_node/compose.yml +++ b/follower_node/compose.yml @@ -1,6 +1,7 @@ services: based-op-geth: - image: ghcr.io/gattaca-com/based-op/based-op-geth:v0.1.0 + image: ghcr.io/gattaca-com/based-op-geth/based-op-geth:latest + pull_policy: always container_name: based-op-geth network_mode: "host" entrypoint: ["/bin/sh", "-c"] @@ -49,7 +50,8 @@ services: restart: unless-stopped based-op-node: - image: ghcr.io/gattaca-com/based-op/based-op-node:v0.1.0 + image: ghcr.io/gattaca-com/based-optimism/based-op-node:latest + pull_policy: always container_name: based-op-node network_mode: "host" command: @@ -84,7 +86,8 @@ services: restart: unless-stopped gateway: - image: ghcr.io/gattaca-com/based-op/based-gateway:v0.1.0 + image: ghcr.io/gattaca-com/based-op/based-gateway:latest + pull_policy: always container_name: based-op-gateway network_mode: "host" command: diff --git a/main_node/compose.yml b/main_node/compose.yml index 1dd2ebc84..236f1d5cb 100644 --- a/main_node/compose.yml +++ b/main_node/compose.yml @@ -128,12 +128,14 @@ services: restart: unless-stopped based-portal: - image: based_portal_local + image: ghcr.io/gattaca-com/based-op/based-portal:latest + pull_policy: always network_mode: "host" container_name: based-portal command: - --fallback.eth_url=http://0.0.0.0:$OP_GETH_RPC_PORT - --fallback.engine_url=http://0.0.0.0:$OP_GETH_ENGINE_RPC_PORT + - --op_node.url=http://0.0.0.0:$OP_NODE_RPC_PORT - --registry.url=http://0.0.0.0:$REGISTRY_PORT - --gateway.timeout_ms=200 volumes: @@ -141,7 +143,8 @@ services: restart: unless-stopped based-registry: - image: based_registry_local + image: ghcr.io/gattaca-com/based-op/based-registry:latest + pull_policy: always network_mode: "host" container_name: based-registry command: From 1a4e5622b322e45d2474159e495cb8d9b7ca859b Mon Sep 17 00:00:00 2001 From: Louis Ponet Date: Tue, 27 May 2025 21:02:46 +0100 Subject: [PATCH 2/7] check whether specified op-geth and op-node datadirs are indeed dirs --- Makefile | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 98b833600..143509165 100644 --- a/Makefile +++ b/Makefile @@ -273,18 +273,10 @@ ifndef STATE_JSON $(error STATE_JSON is undefined! Please invoke like \ `make $(MAKECMDGOALS) ROLLUP_JSON=… GENESIS_JSON=… STATE_JSON=… OP_GETH_DATA_DIR=… OP_NODE_DATA_DIR=…`) endif - -ifndef OP_GETH_DATA_DIR -$(error OP_GETH_DATA_DIR is undefined! Please invoke like \ - `make $(MAKECMDGOALS) ROLLUP_JSON=… GENESIS_JSON=… STATE_JSON=… OP_GETH_DATA_DIR=… OP_NODE_DATA_DIR=…`) -endif - -ifndef OP_NODE_DATA_DIR -$(error OP_NODE_DATA_DIR is undefined! Please invoke like \ - `make $(MAKECMDGOALS) ROLLUP_JSON=… GENESIS_JSON=… STATE_JSON=… OP_GETH_DATA_DIR=… OP_NODE_DATA_DIR=…`) endif -endif +OP_GETH_DATA_DIR?=.local_main_node/data/geth +OP_NODE_DATA_DIR?=.local_main_node/data/node # ──────────────────────────────────────────────────────────────────────────────── config-main-node: @if [ -d .local_main_node/config ]; then \ @@ -297,12 +289,12 @@ config-main-node: @cp $(ROLLUP_JSON) .local_main_node/config @cp $(GENESIS_JSON) .local_main_node/config @cp $(STATE_JSON) .local_main_node/config - @if [ "$(OP_GETH_DATA_DIR)" != ".local_main_node/data/geth" ] && [ ! -d ".local_main_node/data/geth" ]; then \ + @if [ "$(OP_GETH_DATA_DIR)" != ".local_main_node/data/geth" ] && [ ! -d ".local_main_node/data/geth" ] && [ -d "$(OP_GETH_DATA_DIR)"]; then \ ln -s $(OP_GETH_DATA_DIR) .local_main_node/data/geth; \ else \ mkdir -p $(BASED_OP_GETH_DATA_DIR); \ fi - @if [ "$(OP_NODE_DATA_DIR)" != ".local_main_node/data/node" ] && [ ! -d ".local_main_node/data/node" ]; then \ + @if [ "$(OP_NODE_DATA_DIR)" != ".local_main_node/data/node" ] && [ ! -d ".local_main_node/data/node" ] && [ -d "$(OP_NODE_DATA_DIR)"]; then \ ln -s $(OP_NODE_DATA_DIR) .local_main_node/data/node; \ else \ mkdir -p $(OP_NODE_DATA_DIR); \ From 17063966e19f75b5230b61452b58edd97323dbcb Mon Sep 17 00:00:00 2001 From: Louis Ponet Date: Thu, 29 May 2025 11:36:06 +0100 Subject: [PATCH 3/7] update docs --- docs/docs/architecture/consensus.md | 4 +++- docs/docs/architecture/execution.md | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/docs/architecture/consensus.md b/docs/docs/architecture/consensus.md index d920d4bca..4eee9c988 100644 --- a/docs/docs/architecture/consensus.md +++ b/docs/docs/architecture/consensus.md @@ -13,6 +13,8 @@ The main changes include: Importantly, all these changes are incremental and backwards compatible. Non-modified nodes will still be able to stay in the network and receive new blocks, they just won't be able to process `Frag`s and provide preconfirmations. +The forked repo with changes can be found in [based-optimism](https://github.com/gattaca-com/based-optimism). + ![op-node](../../static/img/architecture_consensus.png) ## P2P Capability @@ -138,4 +140,4 @@ New methods in the `based_` namespace are added to enable the OP node to send th - `engine_sealFragV0` - `engine_envV0` -The processing is detailed in the [next](/architecture/execution.md) section. \ No newline at end of file +The processing is detailed in the [next](/architecture/execution.md) section. diff --git a/docs/docs/architecture/execution.md b/docs/docs/architecture/execution.md index c8350bee8..1f681c844 100644 --- a/docs/docs/architecture/execution.md +++ b/docs/docs/architecture/execution.md @@ -5,7 +5,10 @@ description: Upgrades to the OP execution layer # Execution -The OP geth was upgraded to receive `Frag`s messages from the OP node, as well as to serve state for RPC calls off the "unsealed block". Because `Frag`s are shared continously from the gateway, the EL can already start pre-processing and preparing for the block seal and sync. +The OP geth was upgraded to receive `Frag`s messages from the OP node, as well as to serve state for RPC calls off the "unsealed block". +Because `Frag`s are shared continously from the gateway, the EL can already start pre-processing and preparing for the block seal and sync. + +The forked repo with changes can be found in [based-op-geth](https://github.com/gattaca-com/based-op-geth). ![op-el](../../static/img/architecture_execution.png) From 77a6e5dba66a3d1b3926648e90fd6f256044da6d Mon Sep 17 00:00:00 2001 From: eltitanb Date: Fri, 30 May 2025 09:52:20 +0100 Subject: [PATCH 4/7] update docs deploy branch --- .github/workflows/docs.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c2982c3f4..26541674f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,9 +3,7 @@ name: Docs on: push: branches: - - main - # Review gh actions docs if you want to further define triggers, paths, etc - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + - develop jobs: build: From f7755ccb248a77d6f5809b6e6df34bc0042b8667 Mon Sep 17 00:00:00 2001 From: Louis Ponet Date: Mon, 2 Jun 2025 15:41:59 +0100 Subject: [PATCH 5/7] Version bumps, pectra tested (#184) --- Makefile | 99 ++++++------ README.md | 2 +- based/Cargo.lock | 4 + based/bin/overseer/src/data.rs | 2 +- based/bin/overseer/src/main.rs | 4 +- based/bin/portal/Cargo.toml | 2 + based/bin/portal/src/server.rs | 84 ++++++++-- based/crates/common/src/api.rs | 33 ++-- .../common/src/communication/messages.rs | 15 +- based/crates/common/src/config.rs | 3 +- based/crates/db/src/alloy_db.rs | 27 +++- based/crates/db/src/lib.rs | 28 +++- based/crates/rpc/Cargo.toml | 1 + based/crates/rpc/src/engine.rs | 27 +++- based/crates/sequencer/Cargo.toml | 1 + .../sequencer/src/block_sync/mock_fetcher.rs | 20 +-- based/crates/sequencer/src/context.rs | 54 +++++-- based/crates/sequencer/src/lib.rs | 71 ++++++--- .../sequencer/src/sorting/frag_sequence.rs | 2 +- docs/docs/getting_started/develop.md | 2 +- follower_node/compose.mac.yml | 25 --- follower_node/compose.yml | 30 +++- main_node/compose.yml | 146 ++++++++++++------ main_node/env_example | 1 + main_node/intent.template.toml | 8 +- 25 files changed, 467 insertions(+), 224 deletions(-) delete mode 100644 follower_node/compose.mac.yml diff --git a/Makefile b/Makefile index 143509165..61ecaf771 100644 --- a/Makefile +++ b/Makefile @@ -13,14 +13,39 @@ OS := $(shell uname -s) # Variables IMAGE_KEY_TO_ADDRESS:=ghcr.io/gattaca-com/based-op/key-to-address:latest +## This image is totally vanilla, but automatically sets isthmus at genesis when using v3.0.0 contracts +IMAGE_OP_DEPLOYER:=ghcr.io/gattaca-com/based-optimism/based-op-deployer:latest START_GATEWAY_COMPOSE_FILES := -f .local_gateway_and_follower/compose.yml -ifeq ($(OS),Darwin) - START_GATEWAY_COMPOSE_FILES += -f .local_gateway_and_follower/compose.mac.yml -endif +START_MAIN_NODE_COMPOSE_FILES := -f .local_main_node/compose.yml # Overridable Variables +L1_CHAIN_ID?=11155111 +L2_CHAIN_ID?=$(shell \ + RAW=$$(od -An -N2 -tu2 /dev/urandom | tr -d ' '); \ + echo $$((RAW % 50000 + 1)); \ +) +L2_CHAIN_ID_HEX:=$(shell printf "0x%064x" $(L2_CHAIN_ID)) +PORTAL?=http://18.185.199.51:8080 +L1_RPC_URL?=http://34.194.193.217:8545 +L1_BEACON_RPC_URL?=http://34.194.193.217:5052 +PUBLIC_IP?=$(shell curl ifconfig.me) +# if GATEWAY_SEQUENCING_KEY is set, use that one, otherwise key_to_address will generate a new one +GATEWAY_SEQUENCING_KEY ?= $(shell \ + [ -f .local_gateway_and_follower/.env ] && \ + grep -m1 '^GATEWAY_SEQUENCING_KEY=' .local_gateway_and_follower/.env \ + | cut -d= -f2 \ +) +_GATEWAY_KEY_AND_WALLET:=$(shell docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(GATEWAY_SEQUENCING_KEY)) +GATEWAY_SEQUENCING_KEY:=$(word 1,$(_GATEWAY_KEY_AND_WALLET)) +GATEWAY_SEQUENCING_ADDRESS:=$(word 2,$(_GATEWAY_KEY_AND_WALLET)) + +BASED_GATEWAY_DATA_DIR?=.local_gateway_and_follower/data/gateway +BASED_OP_NODE_DATA_DIR?=.local_gateway_and_follower/data/node +BASED_OP_GETH_DATA_DIR?=.local_gateway_and_follower/data/geth + +DEPLOYER_CACHE_DIR:=/tmp/op-deployer-cache # Some servers default to executing shell scripts below with /bin/sh, we set bash to make sure our bash syntax works SHELL := /bin/bash @@ -57,35 +82,25 @@ build-based-op-node: ## πŸ—οΈ Build OP geth from op-eth directory --load \ op-node +build-based-op-deployer: ## πŸ—οΈ Build OP deployer from op-eth directory + cd ../based-optimism && \ + IMAGE_TAGS=develop \ + docker buildx bake \ + -f docker-bake.hcl \ + --set op-deployer.tags=local_based_op_deployer \ + --load \ + op-deployer + build-rabby-chrom: ## πŸ—οΈ Build modified Rabby wallet for Google Chrome and Firefox cd rabby && \ yarn && \ yarn build:pro && \ yarn build:pro:mv2 -L1_CHAIN_ID?=11155111 -L2_CHAIN_ID?=$(shell \ - RAW=$$(od -An -N2 -tu2 /dev/urandom | tr -d ' '); \ - echo $$((RAW % 50000 + 1)); \ -) -L2_CHAIN_ID_HEX:=$(shell printf "0x%064x" $(L2_CHAIN_ID)) -PORTAL?=http://18.185.199.51:8080 -L1_RPC_URL?=http://34.194.193.217:8545 -L1_BEACON_RPC_URL?=http://34.194.193.217:5052 -# if GATEWAY_SEQUENCING_KEY is set, use that one, otherwise key_to_address will generate a new one -GATEWAY_SEQUENCING_KEY ?= $(shell \ - [ -f .local_gateway_and_follower/.env ] && \ - grep -m1 '^GATEWAY_SEQUENCING_KEY=' .local_gateway_and_follower/.env \ - | cut -d= -f2 \ -) -_GATEWAY_KEY_AND_WALLET:=$(shell docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(GATEWAY_SEQUENCING_KEY)) -GATEWAY_SEQUENCING_KEY:=$(word 1,$(_GATEWAY_KEY_AND_WALLET)) -GATEWAY_SEQUENCING_ADDRESS:=$(word 2,$(_GATEWAY_KEY_AND_WALLET)) +create-network: + docker network inspect based_op_net >/dev/null 2>&1 || docker network create based_op_net -BASED_GATEWAY_DATA_DIR?=.local_gateway_and_follower/data/gateway -BASED_OP_NODE_DATA_DIR?=.local_gateway_and_follower/data/node -BASED_OP_GETH_DATA_DIR?=.local_gateway_and_follower/data/geth -start-gateway: +start-gateway: create-network @if docker ps --format '{{.Names}}' | grep -wq based-op-gateway ; then \ echo "❌ Gateway already running."; \ exit 1; \ @@ -106,7 +121,7 @@ start-gateway: echo "Gateway Sequencing Wallet: $(GATEWAY_SEQUENCING_ADDRESS)"; \ { \ echo "PORTAL=$(PORTAL)"; \ - echo "OP_NODE_GOSSIP_IP=$$(curl ifconfig.me)"; \ + echo "OP_NODE_GOSSIP_IP=$(PUBLIC_IP)"; \ echo "GATEWAY_SEQUENCING_KEY=$(GATEWAY_SEQUENCING_KEY)"; \ echo "MAIN_OP_NODE_GOSSIP_STATIC=$$(curl -s -X POST -H 'Content-Type: application/json' \ --data '{"jsonrpc":"2.0","method":"portal_opNodeGossipStatic","params":[],"id":1}' \ @@ -168,7 +183,7 @@ start-gateway: cat .local_gateway_and_follower/.env; \ echo; echo; \ echo "Calling registerGateway method via JSON-RPC:"; \ - GATEWAY_URL=http://$$(curl -s ifconfig.me):$$(grep -m1 '^GATEWAY_PORT[[:space:]]*=' .local_gateway_and_follower/.env | cut -d= -f2); \ + GATEWAY_URL=http://$(PUBLIC_IP):$$(grep -m1 '^GATEWAY_PORT[[:space:]]*=' .local_gateway_and_follower/.env | cut -d= -f2); \ GATEWAY_ADDRESS=$$wallet; \ JWT=$$(cat .local_gateway_and_follower/config/jwt); \ curl -X POST "$(PORTAL)" \ @@ -185,7 +200,7 @@ start-gateway: $(MAKE) start-overseer start-overseer: - docker exec -it based-op-gateway overseer --portal-url $(PORTAL) --based-op-node-url http://based-op-node:8547 --based-op-geth-url http://based-op-geth:8645 + docker exec -it based-op-gateway overseer --portal-url $(PORTAL) # ──────────────────────────────────────────────────────────────────────────────── @@ -219,7 +234,7 @@ deploy-chain: exit 1; \ fi @mkdir -p .local_main_node/config - @docker run -v $$(pwd)/.local_main_node/config:/config --entrypoint sh --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.11 -c "/op-deployer init --l1-chain-id $(L1_CHAIN_ID) --l2-chain-ids $(L2_CHAIN_ID) --workdir /config && chmod 666 /config/*" + @docker run -v $$(pwd)/.local_main_node/config:/config --entrypoint sh -e DEPLOYER_CACHE_DIR=$(DEPLOYER_CACHE_DIR) $(IMAGE_OP_DEPLOYER) -c "/usr/local/bin/op-deployer init --l1-chain-id $(L1_CHAIN_ID) --l2-chain-ids $(L2_CHAIN_ID) --workdir /config && chmod 666 /config/*" @wallet_batcher=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(OP_PROPOSER_KEY) | tail -n1); \ wallet_proposer=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(OP_PROPOSER_KEY) | tail -n1); \ wallet_main=$$(docker run --rm -i $(IMAGE_KEY_TO_ADDRESS) $(MAIN_KEY) | tail -n1); \ @@ -232,18 +247,10 @@ deploy-chain: main_node/intent.template.toml \ > .local_main_node/config/intent.toml - @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.0.11 apply --workdir /config --l1-rpc-url $(L1_RPC_URL) --private-key $(MAIN_KEY) - @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 inspect genesis --workdir /config $(L2_CHAIN_ID_HEX) > $$(pwd)/.local_main_node/config/genesis.json - @docker run -v $$(pwd)/.local_main_node/config:/config --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 inspect rollup --workdir /config $(L2_CHAIN_ID_HEX) > $$(pwd)/.local_main_node/config/rollup.json - @docker run -v $$(pwd)/.local_main_node/config:/config --entrypoint sh --rm us-docker.pkg.dev/oplabs-tools-artifacts/images/op-deployer:v0.2.0 -c "chmod 666 /config/*" - @docker run -v $$(pwd)/.local_main_node/config:/config --rm -i imega/jq '.chain_op_config = {"eip1559Elasticity":6, "eip1559Denominator":50, "eip1559DenominatorCanyon":250}' /config/rollup.json \ - > $$(pwd)/.local_main_node/config/rollup.json.tmp && mv $$(pwd)/.local_main_node/config/rollup.json.tmp $$(pwd)/.local_main_node/config/config.json - @blockNumber=$$(docker run -v $$(pwd)/.local_main_node/config:/config --rm -i imega/jq -r '.genesis.l1.number' /config/rollup.json); \ - hex=$$(printf "0x%x" $$blockNumber); \ - hash=$$(curl -s -X POST -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["'"$$hex"'",false]}' \ - $(L1_RPC_URL) | docker run --rm -i imega/jq -r '.result.hash'); \ - docker run -v $$(pwd)/.local_main_node/config:/config --rm -i imega/jq --arg h "$$hash" '.genesis.l1.hash = $$h' /config/rollup.json > $$(pwd)/.local_main_node/config/rollup.json.tmp && mv $$(pwd)/.local_main_node/config/rollup.json.tmp $$(pwd)/.local_main_node/config/rollup.json + @docker run -v $$(pwd)/.local_main_node/config:/config -e DEPLOYER_CACHE_DIR=$(DEPLOYER_CACHE_DIR) $(IMAGE_OP_DEPLOYER) op-deployer apply --workdir /config --l1-rpc-url $(L1_RPC_URL) --private-key $(MAIN_KEY) + @docker run -v $$(pwd)/.local_main_node/config:/config -e DEPLOYER_CACHE_DIR=$(DEPLOYER_CACHE_DIR) $(IMAGE_OP_DEPLOYER) op-deployer inspect genesis --workdir /config $(L2_CHAIN_ID_HEX) > $$(pwd)/.local_main_node/config/genesis.json + @docker run -v $$(pwd)/.local_main_node/config:/config -e DEPLOYER_CACHE_DIR=$(DEPLOYER_CACHE_DIR) $(IMAGE_OP_DEPLOYER) op-deployer inspect rollup --workdir /config $(L2_CHAIN_ID_HEX) > $$(pwd)/.local_main_node/config/rollup.json + @docker run -v $$(pwd)/.local_main_node/config:/config -e DEPLOYER_CACHE_DIR=$(DEPLOYER_CACHE_DIR) --entrypoint sh $(IMAGE_OP_DEPLOYER) -c "chmod 666 /config/*" @openssl rand -hex 32 | tr -d '\n' | sed 's/^/0x/' > .local_main_node/config/jwt @echo "...Done deploying. See chain config in" @@ -309,7 +316,7 @@ config-main-node: @echo # By default these will be pointing to directories under .local_ -start-main-node: +start-main-node: create-network @if docker ps --format '{{.Names}}' | grep -wq op-node ; then \ echo "❌ Main node already running."; \ exit 1; \ @@ -325,7 +332,7 @@ start-main-node: @# generate .env and fetch JSON if missing @if [ ! -f .local_main_node/.env ]; then \ cp main_node/env_example .local_main_node/.env; \ - cp main_node/compose.yml .local_main_node/compose.yml; \ + cp main_node/compose* .local_main_node; \ $(MAKE) fix-compose; \ echo "Initializing all components of a main sequencing node in ./.local_main_node ..."; \ { \ @@ -334,7 +341,7 @@ start-main-node: echo "L1_RPC_URL=$(L1_RPC_URL)"; \ echo "L1_BEACON_RPC_URL=$(L1_BEACON_RPC_URL)"; \ echo "OP_NODE_SEQUENCER_KEY=$(MAIN_KEY)"; \ - echo "OP_NODE_GOSSIP_IP=$$(curl ifconfig.me)"; \ + echo "OP_NODE_GOSSIP_IP=$(PUBLIC_IP)"; \ echo "OP_BATCHER_PRIVATE_KEY=$(OP_BATCHER_KEY)"; \ echo "OP_PROPOSER_PRIVATE_KEY=$(OP_PROPOSER_KEY)"; \ } >> .local_main_node/.env; \ @@ -349,7 +356,7 @@ start-main-node: @echo @echo - @cd .local_main_node && docker compose up -d + @docker compose $(START_MAIN_NODE_COMPOSE_FILES) up -d $(MAKE) logs-main-node start-portal: build-portal @@ -421,7 +428,7 @@ logs-based-op-node: ## πŸ“œ Show based op-node logs docker logs based-op-node --tail 100 -f logs-based-op-geth: ## πŸ“œ Show based op-geth logs - docker logs based-op-node --tail 100 -f + docker logs based-op-geth --tail 100 -f logs-op-node: ## πŸ“œ Show main op-node logs (only for main sequencing node) docker logs op-node --tail 100 -f diff --git a/README.md b/README.md index 0b436b42a..6f9f9279d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Local Development/Quick start ### With existing OP chain -The following steps have been tested on Sepolia, with a previously deployed L2 chain (l2 non-pectra) +The following steps have been tested on Sepolia, with a previously deployed L2 chain 1. locate your `rollup.json`, `genesis.json` and `state.json` files 2. run `make config-main-node OP_NODE_DATA_DIR= OP_GETH_DATA_DIR= ROLLUP_JSON= GENESIS_JSON= STATE_JSON=` 3. there should be some files set up in `.local_main_node` diff --git a/based/Cargo.lock b/based/Cargo.lock index 8829f5d61..80f259b8e 100644 --- a/based/Cargo.lock +++ b/based/Cargo.lock @@ -1549,6 +1549,7 @@ checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" name = "based-portal" version = "0.1.0" dependencies = [ + "alloy-eips 0.14.0", "alloy-primitives", "alloy-rpc-types", "bop-common", @@ -1561,6 +1562,7 @@ dependencies = [ "parking_lot", "reqwest", "reth-rpc-layer", + "serde", "serde_json", "tokio", "tower 0.4.13", @@ -1874,6 +1876,7 @@ dependencies = [ name = "bop-rpc" version = "0.1.0" dependencies = [ + "alloy-eips 0.14.0", "alloy-primitives", "alloy-rpc-types", "bop-common", @@ -1928,6 +1931,7 @@ dependencies = [ "reth-optimism-primitives", "reth-primitives", "reth-primitives-traits", + "reth-provider", "reth-trie-common", "revm", "revm-handler", diff --git a/based/bin/overseer/src/data.rs b/based/bin/overseer/src/data.rs index ac191bec2..576774baf 100644 --- a/based/bin/overseer/src/data.rs +++ b/based/bin/overseer/src/data.rs @@ -506,7 +506,7 @@ impl Data { } if let Ok(mut peers) = consumers .peers_based_op_geth() - .inspect_err(|e| tracing::warn!("couldn't get peers of local based-op-node: {e}")) + .inspect_err(|e| tracing::warn!("couldn't get peers of local based-op-geth: {e}")) { peers.sort_unstable_by(|d1, d2| d1.id.cmp(&d2.id)); self.peers_local_op_geth = peers; diff --git a/based/bin/overseer/src/main.rs b/based/bin/overseer/src/main.rs index 4994021f7..b61986b77 100644 --- a/based/bin/overseer/src/main.rs +++ b/based/bin/overseer/src/main.rs @@ -50,10 +50,10 @@ pub struct OverseerArgs { #[arg(short, long)] pub portal_url: Url, /// The url of the based-op-node running next to the based-gateway - #[arg(long, default_value = "http://0.0.0.0:8547")] + #[arg(long, default_value = "http://127.0.0.1:8547")] pub based_op_node_url: Url, /// The url of the based-op-geth running next to the based-gateway - #[arg(long, default_value = "http://0.0.0.0:8645")] + #[arg(long, default_value = "http://127.0.0.1:8645")] pub based_op_geth_url: Url, } diff --git a/based/bin/portal/Cargo.toml b/based/bin/portal/Cargo.toml index 1d32f9671..d8b9d764e 100644 --- a/based/bin/portal/Cargo.toml +++ b/based/bin/portal/Cargo.toml @@ -14,9 +14,11 @@ futures.workspace = true jsonrpsee.workspace = true op-alloy-rpc-types.workspace = true op-alloy-rpc-types-engine.workspace = true +alloy-eips.workspace = true parking_lot.workspace = true reqwest.workspace = true reth-rpc-layer.workspace = true +serde.workspace = true serde_json.workspace = true tokio.workspace = true tower.workspace = true diff --git a/based/bin/portal/src/server.rs b/based/bin/portal/src/server.rs index a3cb37e30..cecdedda3 100644 --- a/based/bin/portal/src/server.rs +++ b/based/bin/portal/src/server.rs @@ -1,14 +1,15 @@ use std::{fmt, net::SocketAddr, sync::Arc, time::Duration}; +use alloy_eips::eip7685::RequestsOrHash; use alloy_primitives::{Address, B256, Bytes, U256}; use alloy_rpc_types::{ - BlockId, BlockNumberOrTag, + BlockId, BlockNumberOrTag, engine::{ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, }; use bop_common::{ api::{ - EngineApiClient, EngineApiServer, EthApiClient, EthApiServer, OpGethAdminApiClient, OpNodeApiClient, - OpNodeP2PApiClient, OpRpcBlock, PORTAL_CAPABILITIES, PortalApiServer, RegistryApiClient, + EngineApiClient, EngineApiServer, EthApiClient, EthApiServer, OpGethAdminApiClient, + OpNodeApiClient, OpNodeP2PApiClient, OpRpcBlock, PORTAL_CAPABILITIES, PortalApiServer, RegistryApiClient, }, communication::messages::{RpcError, RpcResult}, utils::{uuid, wait_for_signal}, @@ -19,7 +20,7 @@ use jsonrpsee::{ server::{RpcServiceBuilder, ServerBuilder}, }; use op_alloy_rpc_types::OpTransactionReceipt; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; use parking_lot::RwLock; use reqwest::Url; use reth_rpc_layer::{AuthClientLayer, AuthClientService, JwtSecret}; @@ -436,6 +437,61 @@ impl EngineApiServer for PortalServer { Ok(response) } + #[tracing::instrument(skip_all, err, ret(level = Level::DEBUG), fields(req_id = %uuid()))] + async fn new_payload_v4( + &self, + payload: OpExecutionPayloadV4, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + requests: RequestsOrHash, + ) -> RpcResult { + let block_number = payload.payload_inner.payload_inner.payload_inner.block_number; + let block_hash = payload.payload_inner.payload_inner.payload_inner.block_hash; + let gas_limit = payload.payload_inner.payload_inner.payload_inner.gas_limit; + let gas_used = payload.payload_inner.payload_inner.payload_inner.gas_used; + let n_txs = payload.payload_inner.payload_inner.payload_inner.transactions.len(); + let n_withdrawals = payload.payload_inner.payload_inner.withdrawals.len(); + let blob_gas_used = payload.payload_inner.blob_gas_used; + let excess_blob_gas = payload.payload_inner.excess_blob_gas; + + debug!(block_number, %block_hash, gas_limit, gas_used, n_txs, n_withdrawals, blob_gas_used, excess_blob_gas, "new request"); + let response = self + .fallback_client + .new_payload_v4(payload.clone(), versioned_hashes.clone(), parent_beacon_block_root, requests.clone()) + .await + .inspect_err(|e| tracing::error!("issue sending new_payload_v4 to el {e}"))?; + + // send to all gateways + for gateway in self.gateways() { + let payload = payload.clone(); + let requests = requests.clone(); + let versioned_hashes = versioned_hashes.clone(); + + tokio::spawn( + async move { + match gateway + .client + .new_payload_v4(payload, versioned_hashes, parent_beacon_block_root, requests) + .await + { + Ok(res) => { + if res.is_valid() { + debug!(?gateway, ?res, "gateway response"); + } else { + error!(?gateway, ?res, "gateway response"); + } + } + Err(ClientError::Call(_)) => {} + Err(err) => error!(?gateway, %err, "failed gateway"), + } + } + .in_current_span(), + ); + } + + Ok(response) + } + #[tracing::instrument(skip_all, err, ret(level = Level::DEBUG), fields(req_id = %uuid()))] async fn new_payload_v3( &self, @@ -486,18 +542,18 @@ impl EngineApiServer for PortalServer { } #[tracing::instrument(skip_all, err, ret(level = Level::DEBUG), fields(req_id = %uuid()))] - async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult { + async fn get_payload_v4(&self, payload_id: PayloadId) -> RpcResult { debug!(%payload_id, "new request"); let fallback_fut = tokio::spawn({ let client = self.fallback_client.clone(); - async move { client.get_payload_v3(payload_id).await } + async move { client.get_payload_v4(payload_id).await } }); let Some(gateway) = self.current_gateway.lock().await.clone() else { return Ok(fallback_fut.await??) }; - let gateway_fut: tokio::task::JoinHandle> = tokio::spawn( + let gateway_fut: tokio::task::JoinHandle> = tokio::spawn( { // only get payload from previously picked gateway let fallback_client = self.fallback_client.clone(); @@ -505,15 +561,19 @@ impl EngineApiServer for PortalServer { async move { let gateway_payload = gateway .client - .get_payload_v3(payload_id) + .get_payload_v4(payload_id) .await .inspect_err(|err| error!(%err, "failed gateway"))?; let payload_status = fallback_client - .new_payload_v3( - gateway_payload.execution_payload.clone(), + .new_payload_v4( + OpExecutionPayloadV4 { + payload_inner: gateway_payload.execution_payload.payload_inner.clone(), + withdrawals_root: gateway_payload.execution_payload.withdrawals_root, + }, vec![], gateway_payload.parent_beacon_block_root, + RequestsOrHash::default(), ) .await .inspect_err(|err| error!(%err, "failed fallback validation"))?; @@ -538,13 +598,13 @@ impl EngineApiServer for PortalServer { if let Ok(gateway) = gateway.as_ref() { info!( "block {}: successfully served from based-gateway {:?}", - gateway.execution_payload.payload_inner.payload_inner.block_number, + gateway.execution_payload.payload_inner.payload_inner.payload_inner.block_number, self.current_gateway.lock().await.as_ref().unwrap() ); } else if let Ok(fallback) = fallback.as_ref() { info!( "block {}: successfully served from fallback", - fallback.execution_payload.payload_inner.payload_inner.block_number + fallback.execution_payload.payload_inner.payload_inner.payload_inner.block_number ); } else { error!("couldn't serve a block from fallback or gateway"); diff --git a/based/crates/common/src/api.rs b/based/crates/common/src/api.rs index 7025d9062..6a6086891 100644 --- a/based/crates/common/src/api.rs +++ b/based/crates/common/src/api.rs @@ -1,14 +1,15 @@ use std::collections::HashMap; +use alloy_eips::eip7685::RequestsOrHash; use alloy_primitives::{Address, B256, Bytes, U256}; use alloy_rpc_types::{ BlockId, BlockNumberOrTag, - engine::{ExecutionPayloadV3, ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, + engine::{ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, }; use jsonrpsee::proc_macros::rpc; use op_alloy_consensus::OpTxEnvelope; use op_alloy_rpc_types::OpTransactionReceipt; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; use reqwest::Url; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -18,7 +19,9 @@ use crate::communication::messages::RpcResult; pub const PORTAL_CAPABILITIES: &[&str] = &[ "engine_forkchoiceUpdatedV3", "engine_getPayloadV3", + "engine_getPayloadV4", "engine_newPayloadV3", + "engine_newPayloadV4", "eth_sendRawTransaction", // "eth_getTransactionReceipt", // "eth_getBlockByNumber", @@ -35,8 +38,6 @@ pub type OpRpcBlock = alloy_rpc_types::Block; /// /// ref: https://github.com/ethereum/execution-apis/tree/main/src/engine /// ref: https://specs.optimism.io/protocol/exec-engine.html#engine-api -/// -/// NOTE: currently only v3 endpoints are supported #[rpc(client, server, namespace = "engine")] pub trait EngineApi { /// Used by the op-node to set which blocks are considered canonical. @@ -62,31 +63,27 @@ pub trait EngineApi { #[method(name = "newPayloadV4")] async fn new_payload_v4( &self, - payload: ExecutionPayloadV3, + payload: OpExecutionPayloadV4, versioned_hashes: Vec, parent_beacon_block_root: B256, - _execution_requests: Vec, - ) -> RpcResult { - self.new_payload_v3(payload, versioned_hashes, parent_beacon_block_root) - } - + _execution_requests: RequestsOrHash, + ) -> RpcResult; /// Used to fetch an execution payload from a previous `payload_id` set in `forkchoiceUpdatedV3` #[method(name = "getPayloadV3")] - async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult; + async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult { + let execution_payload = self.get_payload_v4(payload_id).await?; - #[method(name = "getPayloadV4")] - async fn get_payload_v4(&self, payload_id: PayloadId) -> RpcResult { - let execution_payload = self.get_payload_v3(payload_id).await?; - - Ok(OpExecutionPayloadEnvelopeV4 { - execution_payload: execution_payload.execution_payload, + Ok(OpExecutionPayloadEnvelopeV3 { + execution_payload: execution_payload.execution_payload.payload_inner, block_value: execution_payload.block_value, blobs_bundle: execution_payload.blobs_bundle, should_override_builder: execution_payload.should_override_builder, parent_beacon_block_root: execution_payload.parent_beacon_block_root, - execution_requests: vec![], }) } + + #[method(name = "getPayloadV4")] + async fn get_payload_v4(&self, payload_id: PayloadId) -> RpcResult; } /// The Eth API is used to interact with the EL directly. diff --git a/based/crates/common/src/communication/messages.rs b/based/crates/common/src/communication/messages.rs index 0b8f9e154..7bf77671e 100644 --- a/based/crates/common/src/communication/messages.rs +++ b/based/crates/common/src/communication/messages.rs @@ -3,15 +3,15 @@ use std::{ sync::Arc, }; -use alloy_consensus::{BlockHeader, Transaction as TransactionTrait}; +use alloy_consensus::{constants::EMPTY_WITHDRAWALS, BlockHeader, Transaction as TransactionTrait}; use alloy_eips::eip2718::Encodable2718; use alloy_rpc_types::engine::{ ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ForkchoiceState, PayloadAttributes, PayloadError, PayloadId, }; use jsonrpsee::types::{ErrorCode, ErrorObject as RpcErrorObject}; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpPayloadAttributes}; -use reth_evm::{NextBlockEnvAttributes, execute::BlockExecutionError}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; +use reth_evm::{execute::BlockExecutionError, NextBlockEnvAttributes}; use reth_primitives_traits::transaction::signed::RecoveryError; use revm_primitives::{Address, U256}; use serde::{Deserialize, Serialize}; @@ -162,8 +162,8 @@ impl From> for Nanos { #[derive(Debug, AsRefStr)] pub enum EngineApi { ForkChoiceUpdatedV3 { fork_choice_state: ForkchoiceState, payload_attributes: Option> }, - NewPayloadV3 { payload: ExecutionPayloadV3, versioned_hashes: Vec, parent_beacon_block_root: B256 }, - GetPayloadV3 { payload_id: PayloadId, res: oneshot::Sender }, + NewPayloadV4 { payload: OpExecutionPayloadV4, versioned_hashes: Vec, parent_beacon_block_root: B256 }, + GetPayloadV4 { payload_id: PayloadId, res: oneshot::Sender }, } impl EngineApi { pub fn messages_from_block( @@ -216,8 +216,9 @@ impl EngineApi { blob_gas_used: Default::default(), excess_blob_gas: Default::default(), }; - let new_payload = EngineApi::NewPayloadV3 { - payload: v3, + let v4 = OpExecutionPayloadV4 { payload_inner: v3, withdrawals_root: EMPTY_WITHDRAWALS }; + let new_payload = EngineApi::NewPayloadV4 { + payload: v4, versioned_hashes: Default::default(), parent_beacon_block_root: block .parent_beacon_block_root() diff --git a/based/crates/common/src/config.rs b/based/crates/common/src/config.rs index f880f7bba..a92484aa8 100644 --- a/based/crates/common/src/config.rs +++ b/based/crates/common/src/config.rs @@ -24,7 +24,6 @@ pub struct GatewayArgs { long, value_name = "CHAIN_OR_PATH", long_help = OpChainSpecParser::help_message(), - default_value = OpChainSpecParser::SUPPORTED_CHAINS[6], value_parser = OpChainSpecParser::parser(), )] pub chain: Arc, @@ -32,7 +31,7 @@ pub struct GatewayArgs { #[arg(long = "rpc.host", default_value_t = Ipv4Addr::UNSPECIFIED)] pub rpc_host: Ipv4Addr, /// The port to run the engine_ and eth_ RPC - #[arg(long = "rpc.port", default_value_t = 9090)] + #[arg(long = "rpc.port", default_value_t = 9997)] pub rpc_port: u16, #[arg(long = "rpc.jwt")] pub rpc_jwt: String, diff --git a/based/crates/db/src/alloy_db.rs b/based/crates/db/src/alloy_db.rs index f222786f9..ee21bd6fe 100644 --- a/based/crates/db/src/alloy_db.rs +++ b/based/crates/db/src/alloy_db.rs @@ -19,7 +19,8 @@ use bop_common::{time::BlockSyncTimers, typedefs::*}; use op_alloy_network::Optimism; use reth_db::DatabaseError; use reth_optimism_primitives::{OpBlock, OpReceipt}; -use reth_provider::{BlockExecutionOutput, ProviderError}; +use reth_provider::{BlockExecutionOutput, ProviderError, ProviderResult, StorageRootProvider}; +use reth_trie::{HashedStorage, StorageMultiProof, StorageProof}; use reth_trie_common::updates::TrieUpdates; use revm_primitives::HashMap; use tokio::runtime::Runtime; @@ -193,6 +194,30 @@ impl DatabaseWrite for AlloyDB { } } +impl StorageRootProvider for AlloyDB { + fn storage_root(&self, _address: Address, _hashed_storage: HashedStorage) -> ProviderResult { + Ok(B256::ZERO) + } + + fn storage_proof( + &self, + _address: Address, + _slot: B256, + _hashed_storage: HashedStorage, + ) -> ProviderResult { + Ok(StorageProof::default()) + } + + fn storage_multiproof( + &self, + _address: Address, + _slots: &[B256], + _hashed_storage: HashedStorage, + ) -> ProviderResult { + Ok(StorageMultiProof::empty()) + } +} + #[cfg(test)] mod tests { use revm_primitives::address; diff --git a/based/crates/db/src/lib.rs b/based/crates/db/src/lib.rs index 183d00dd9..a209b5986 100644 --- a/based/crates/db/src/lib.rs +++ b/based/crates/db/src/lib.rs @@ -19,10 +19,10 @@ use reth_optimism_primitives::{OpBlock, OpReceipt}; use reth_primitives::{RecoveredBlock, StorageEntry}; use reth_provider::{ BlockExecutionOutput, DatabaseProviderRO, LatestStateProviderRef, OriginalValuesKnown, ProviderFactory, - StateWriter, TrieWriter, providers::ConsistentDbView, + ProviderResult, StateWriter, StorageRootProvider, TrieWriter, providers::ConsistentDbView, }; use reth_storage_api::{DBProvider, HashedPostStateProvider}; -use reth_trie::{StateRoot, TrieInput}; +use reth_trie::{HashedStorage, StateRoot, StorageMultiProof, StorageProof, TrieInput}; use reth_trie_common::updates::TrieUpdates; use reth_trie_db::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory}; use reth_trie_parallel::root::ParallelStateRoot; @@ -272,3 +272,27 @@ impl Database for SequencerDB { self.block_hash_ref(number) } } + +impl StorageRootProvider for SequencerDB { + fn storage_root(&self, address: Address, hashed_storage: HashedStorage) -> ProviderResult { + self.provider()?.latest().storage_root(address, hashed_storage) + } + + fn storage_proof( + &self, + address: Address, + slot: B256, + hashed_storage: HashedStorage, + ) -> ProviderResult { + self.provider()?.latest().storage_proof(address, slot, hashed_storage) + } + + fn storage_multiproof( + &self, + address: Address, + slots: &[B256], + hashed_storage: HashedStorage, + ) -> ProviderResult { + self.provider()?.latest().storage_multiproof(address, slots, hashed_storage) + } +} diff --git a/based/crates/rpc/Cargo.toml b/based/crates/rpc/Cargo.toml index e3ece3073..95dfd5248 100644 --- a/based/crates/rpc/Cargo.toml +++ b/based/crates/rpc/Cargo.toml @@ -12,6 +12,7 @@ jsonrpsee.workspace = true op-alloy-consensus.workspace = true op-alloy-rpc-types.workspace = true op-alloy-rpc-types-engine.workspace = true +alloy-eips.workspace =true reqwest.workspace = true reth-optimism-primitives.workspace = true serde_json.workspace = true diff --git a/based/crates/rpc/src/engine.rs b/based/crates/rpc/src/engine.rs index 6dc5b4a7e..4919701bb 100644 --- a/based/crates/rpc/src/engine.rs +++ b/based/crates/rpc/src/engine.rs @@ -1,3 +1,4 @@ +use alloy_eips::eip7685::RequestsOrHash; use alloy_primitives::B256; use alloy_rpc_types::engine::{ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}; use bop_common::{ @@ -5,7 +6,7 @@ use bop_common::{ communication::messages::{self, RpcError, RpcResult}, }; use jsonrpsee::core::async_trait; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; use tokio::sync::oneshot; use tracing::{Level, trace}; @@ -43,16 +44,34 @@ impl EngineApiServer for RpcServer { ) -> RpcResult { trace!(?payload, ?versioned_hashes, %parent_beacon_block_root, "new request"); - self.send(messages::EngineApi::NewPayloadV3 { payload, versioned_hashes, parent_beacon_block_root }); + self.send(messages::EngineApi::NewPayloadV4 { + payload: OpExecutionPayloadV4 { payload_inner: payload, withdrawals_root: B256::ZERO }, + versioned_hashes, + parent_beacon_block_root, + }); + Err(RpcError::NoReturn) + } + + #[tracing::instrument(skip_all, ret(level = Level::TRACE))] + async fn new_payload_v4( + &self, + payload: OpExecutionPayloadV4, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + _requests: RequestsOrHash, + ) -> RpcResult { + trace!(?payload, ?versioned_hashes, %parent_beacon_block_root, "new request"); + + self.send(messages::EngineApi::NewPayloadV4 { payload, versioned_hashes, parent_beacon_block_root }); Err(RpcError::NoReturn) } #[tracing::instrument(skip_all, ret(level = Level::TRACE))] - async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult { + async fn get_payload_v4(&self, payload_id: PayloadId) -> RpcResult { trace!(%payload_id, "new request"); let (tx, rx) = oneshot::channel(); - self.send(messages::EngineApi::GetPayloadV3 { payload_id, res: tx }); + self.send(messages::EngineApi::GetPayloadV4 { payload_id, res: tx }); // wait with timeout let res = tokio::time::timeout(self.engine_timeout.into(), rx).await??; diff --git a/based/crates/sequencer/Cargo.toml b/based/crates/sequencer/Cargo.toml index 0c83e0ab2..8a4bf7f2f 100644 --- a/based/crates/sequencer/Cargo.toml +++ b/based/crates/sequencer/Cargo.toml @@ -38,6 +38,7 @@ reth-optimism-forks.workspace = true reth-optimism-primitives.workspace = true reth-primitives.workspace = true reth-primitives-traits.workspace = true +reth-provider.workspace = true reth-trie-common.workspace = true op-revm.workspace = true revm-primitives.workspace = true diff --git a/based/crates/sequencer/src/block_sync/mock_fetcher.rs b/based/crates/sequencer/src/block_sync/mock_fetcher.rs index 228f9f109..a334b997b 100644 --- a/based/crates/sequencer/src/block_sync/mock_fetcher.rs +++ b/based/crates/sequencer/src/block_sync/mock_fetcher.rs @@ -250,7 +250,7 @@ impl MockFetcher { Duration::from_millis(2000).sleep(); let (block_tx, mut block_rx) = oneshot::channel(); - connections.send(EngineApi::GetPayloadV3 { payload_id: PayloadId::new([0; 8]), res: block_tx }); + connections.send(EngineApi::GetPayloadV4 { payload_id: PayloadId::new([0; 8]), res: block_tx }); Duration::from_millis(100).sleep(); let curt = Instant::now(); let mut sealed_block = loop { @@ -264,10 +264,10 @@ impl MockFetcher { }; let hash = block.hash_slow(); - let hash1 = sealed_block.execution_payload.payload_inner.payload_inner.block_hash; + let hash1 = sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.block_hash; if hash1 != hash { - sealed_block.execution_payload.payload_inner.payload_inner.transactions = vec![]; - let receipt = sealed_block.execution_payload.payload_inner.payload_inner.receipts_root; + sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.transactions = vec![]; + let receipt = sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.receipts_root; if receipt == block.receipts_root { info!("receipts match"); } else { @@ -275,7 +275,7 @@ impl MockFetcher { debug_assert!(false, "receipts don't match"); }; - let gas_used = sealed_block.execution_payload.payload_inner.payload_inner.gas_used; + let gas_used = sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.gas_used; if gas_used == block.gas_used() { info!("gas_used matches") @@ -284,7 +284,7 @@ impl MockFetcher { debug_assert!(false, "gas_used doesn't match"); }; - let state_root = sealed_block.execution_payload.payload_inner.payload_inner.state_root; + let state_root = sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.state_root; if state_root == block.state_root() { info!("state_root matches") @@ -297,7 +297,7 @@ impl MockFetcher { } assert_eq!( - sealed_block.execution_payload.payload_inner.payload_inner.block_hash, + sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.block_hash, block.hash_slow(), "{block:#?} vs {sealed_block:#?}" ); @@ -356,14 +356,14 @@ impl MockFetcher { while curt.elapsed() < *get_payload_delay {} let (block_tx, block_rx) = oneshot::channel(); - connections.send(EngineApi::GetPayloadV3 { payload_id: PayloadId::new([0; 8]), res: block_tx }); + connections.send(EngineApi::GetPayloadV4 { payload_id: PayloadId::new([0; 8]), res: block_tx }); let Ok(sealed_block) = block_rx.blocking_recv() else { warn!("issue getting block"); return; }; - let gas = sealed_block.execution_payload.payload_inner.payload_inner.gas_used; - let n_txs = sealed_block.execution_payload.payload_inner.payload_inner.transactions.len(); + let gas = sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.gas_used; + let n_txs = sealed_block.execution_payload.payload_inner.payload_inner.payload_inner.transactions.len(); let el = curt.elapsed(); info!( "in {}: sequenced {n_txs} txs, {gas} ({:.3} MGas/s)", diff --git a/based/crates/sequencer/src/context.rs b/based/crates/sequencer/src/context.rs index cb906eb59..dfef2de77 100644 --- a/based/crates/sequencer/src/context.rs +++ b/based/crates/sequencer/src/context.rs @@ -1,6 +1,6 @@ use std::{collections::VecDeque, fmt::Display, sync::Arc}; -use alloy_consensus::{BlockHeader, EMPTY_OMMER_ROOT_HASH, Header}; +use alloy_consensus::{BlockHeader, Header, EMPTY_OMMER_ROOT_HASH}; use alloy_eips::merge::BEACON_NONCE; use alloy_rpc_types::engine::{ BlobsBundleV1, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ForkchoiceState, @@ -10,23 +10,26 @@ use bop_common::{ debug_panic, p2p::{FragV0, SealV0}, shared::SharedState, - telemetry::{TelemetryUpdate, telemetry_queue}, + telemetry::{telemetry_queue, TelemetryUpdate}, time::Timer, transaction::Transaction, typedefs::*, }; use bop_db::{DatabaseRead, DatabaseWrite}; use bop_pool::transaction::pool::TxPool; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpPayloadAttributes}; use op_revm::OpSpecId; -use reth_evm::{ConfigureEvm, block::SystemCaller, env::EvmEnv, execute::ProviderError}; +use reth_chainspec::EthereumHardforks; +use reth_evm::{block::SystemCaller, env::EvmEnv, execute::ProviderError, ConfigureEvm}; use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_consensus::isthmus::withdrawals_root; use reth_optimism_evm::OpNextBlockEnvAttributes; use reth_optimism_forks::{OpHardfork, OpHardforks}; -use revm_primitives::{B256, Bytes, U256, b256}; +use reth_provider::StorageRootProvider; +use revm_primitives::{b256, Bytes, B256, U256}; use tracing::info; -use crate::{FragSequence, SequencerConfig, block_sync::BlockSync, sorting::SortingData}; +use crate::{block_sync::BlockSync, sorting::SortingData, FragSequence, SequencerConfig}; /// These are used to time different parts of the sequencer loop pub struct SequencerTimers { @@ -140,6 +143,10 @@ impl SequencerContext { pub fn timestamp(&self) -> u64 { self.block_env.timestamp } + + pub fn is_prague_active(&self) -> bool { + self.chain_spec().is_prague_active_at_timestamp(self.timestamp()) + } } impl SequencerContext { @@ -161,7 +168,7 @@ impl SequencerContext { } } -impl + Display>> SequencerContext { +impl + Display> + StorageRootProvider> SequencerContext { pub fn handle_tx(&mut self, tx: Arc, senders: &SendersSpine) { if tx.is_deposit() { self.deposits.push_back(tx); @@ -230,7 +237,7 @@ impl + Display>> Sequence } /// Finalize the block after the last frag has been sealed - pub fn seal_block(&mut self, frag_seq: FragSequence) -> (SealV0, OpExecutionPayloadEnvelopeV3) { + pub fn seal_block(&mut self, frag_seq: FragSequence) -> (SealV0, OpExecutionPayloadEnvelopeV4) { frag_seq.sorting_telemetry.report(); let gas_used = frag_seq.gas_used; let canyon_active = self.chain_spec().fork(OpHardfork::Canyon).active_at_timestamp(self.timestamp()); @@ -240,6 +247,18 @@ impl + Display>> Sequence let state_changes = self.shared_state.as_mut().take_state_changes(); let state_root = self.db.calculate_state_root(&state_changes).unwrap().0; + let (withdrawals_root, requests_hash) = if self.is_prague_active() { + ( + Some( + withdrawals_root(&state_changes, &self.db) + .expect("something wrong with withdrawals root calculation"), + ), + Some(b256!("0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")), + ) + } else { + (Some(b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")), None) + }; + let extra_data = self.extra_data(); let parent_beacon_block_root = self.parent_beacon_block_root(); @@ -251,7 +270,7 @@ impl + Display>> Sequence state_root, transactions_root, receipts_root, - withdrawals_root: Some(b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")), + withdrawals_root, logs_bloom, timestamp: self.block_env.timestamp, mix_hash: self.block_env.prevrandao.unwrap_or_default(), @@ -265,7 +284,7 @@ impl + Display>> Sequence parent_beacon_block_root, blob_gas_used: Some(0), excess_blob_gas: Some(0), - requests_hash: None, + requests_hash, }; let v1 = ExecutionPayloadV1 { @@ -302,16 +321,21 @@ impl + Display>> Sequence frag_seq.txs.len(), mgas / frag_seq.start_t.elapsed().as_secs() ); - (seal, OpExecutionPayloadEnvelopeV3 { - execution_payload: ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { payload_inner: v1, withdrawals: vec![] }, - blob_gas_used: 0, - excess_blob_gas: 0, + let payload_inner = ExecutionPayloadV3 { + payload_inner: ExecutionPayloadV2 { payload_inner: v1, withdrawals: vec![] }, + blob_gas_used: 0, + excess_blob_gas: 0, + }; + (seal, OpExecutionPayloadEnvelopeV4 { + execution_payload: op_alloy_rpc_types_engine::OpExecutionPayloadV4 { + payload_inner, + withdrawals_root: withdrawals_root.unwrap_or(B256::ZERO), }, block_value: frag_seq.payment.to(), blobs_bundle: BlobsBundleV1::new(vec![]), should_override_builder: false, parent_beacon_block_root: parent_beacon_block_root.expect("should always be set"), + execution_requests: vec![], }) } } diff --git a/based/crates/sequencer/src/lib.rs b/based/crates/sequencer/src/lib.rs index 62d4dbcc8..5fcc9f008 100644 --- a/based/crates/sequencer/src/lib.rs +++ b/based/crates/sequencer/src/lib.rs @@ -1,29 +1,32 @@ use std::sync::Arc; use alloy_consensus::BlockHeader; +use alloy_eips::eip7685::RequestsOrHash; use alloy_primitives::B256; use alloy_rpc_types::engine::{ - CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ExecutionPayloadV3, ForkchoiceState, + CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ForkchoiceState, PraguePayloadFields, }; use bop_common::{ actor::Actor, communication::{ - Connections, ReceiversSpine, SendersSpine, SpineConnections, TrackedSenders, messages::{self, BlockFetch, BlockSyncError, EngineApi, SimulatorToSequencer, SimulatorToSequencerMsg}, + Connections, ReceiversSpine, SendersSpine, SpineConnections, TrackedSenders, }, db::DatabaseWrite, p2p::{EnvV0, VersionedMessage}, shared::SharedState, - telemetry::{self, Telemetry, TelemetryUpdate, system::SystemNotification}, + telemetry::{self, system::SystemNotification, Telemetry, TelemetryUpdate}, time::{Duration, Repeater}, transaction::Transaction, typedefs::{BlockSyncMessage, DatabaseRef}, }; use bop_db::DatabaseRead; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; use reth_optimism_primitives::OpTransactionSigned; use reth_primitives::RecoveredBlock; -use reth_primitives_traits::SignedTransaction; +use reth_primitives_traits::{block::TestBlock, SignedTransaction}; +use reth_provider::StorageRootProvider; +use revm_primitives::b256; use sorting::FragSequence; use strum_macros::AsRefStr; use tokio::sync::oneshot; @@ -43,14 +46,22 @@ use tracing::{info, warn}; use uuid::Uuid; pub fn payload_to_block( - payload: ExecutionPayload, + payload: OpExecutionPayloadV4, sidecar: ExecutionPayloadSidecar, ) -> Result { - let block = payload.try_into_block_with_sidecar::(&sidecar)?; + let withdrawals_root = payload.withdrawals_root; + let mut block = + ExecutionPayload::V3(payload.payload_inner).try_into_block_with_sidecar::(&sidecar)?; let mut block_senders = vec![]; for tx in &block.body.transactions { block_senders.push(tx.recover_signer_unchecked()?); } + if withdrawals_root != B256::ZERO { + block.header_mut().withdrawals_root = Some(withdrawals_root) + } else { + block.header_mut().withdrawals_root = + Some(b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + } Ok(RecoveredBlock::new_unhashed(block, block_senders)) } @@ -75,7 +86,7 @@ impl Sequencer { impl Actor for Sequencer where - Db: DatabaseWrite + DatabaseRead, + Db: DatabaseWrite + DatabaseRead + StorageRootProvider, { fn loop_body(&mut self, connections: &mut Connections, ReceiversSpine>) { // handle block sync @@ -95,8 +106,8 @@ where // handle new transaction connections.receive_for(Duration::from_millis(10), |msg, senders| { - if self.data.timestamp() != 0 - && self.supervisor.as_ref().is_some_and(|validator| !validator.is_valid(&msg, self.data.timestamp())) + if self.data.timestamp() != 0 && + self.supervisor.as_ref().is_some_and(|validator| !validator.is_valid(&msg, self.data.timestamp())) { return; } @@ -187,7 +198,7 @@ impl SequencerState { impl SequencerState where - Db: DatabaseWrite + DatabaseRead, + Db: DatabaseWrite + DatabaseRead + StorageRootProvider, { /// Processes Engine API messages that drive state transitions in the sequencer. /// @@ -207,13 +218,13 @@ where use EngineApi::*; match msg { - NewPayloadV3 { payload, versioned_hashes, parent_beacon_block_root, .. } => { + NewPayloadV4 { payload, versioned_hashes, parent_beacon_block_root, .. } => { self.handle_new_payload_engine_api(ctx, senders, payload, versioned_hashes, parent_beacon_block_root) } ForkChoiceUpdatedV3 { fork_choice_state, payload_attributes, .. } => { self.handle_fork_choice_updated_engine_api(fork_choice_state, payload_attributes, ctx, senders) } - GetPayloadV3 { res, .. } => self.handle_get_payload_engine_api(res, ctx, senders), + GetPayloadV4 { res, .. } => self.handle_get_payload_engine_api(res, ctx, senders), } } @@ -227,14 +238,16 @@ where self, ctx: &mut SequencerContext, senders: &SendersSpine, - payload: ExecutionPayloadV3, + payload: OpExecutionPayloadV4, versioned_hashes: Vec, parent_beacon_block_root: B256, ) -> SequencerState { use SequencerState::*; TelemetryUpdate::send( Uuid::nil(), - Telemetry::System(SystemNotification::NewPayload(payload.payload_inner.payload_inner.block_number)), + Telemetry::System(SystemNotification::NewPayload( + payload.payload_inner.payload_inner.payload_inner.block_number, + )), &mut ctx.telemetry, ); if matches!(self, Sorting(_, _)) { @@ -242,7 +255,7 @@ where return self; } let head_bn = ctx.db.head_block_number().expect("couldn't get db"); - let bn = payload.payload_inner.payload_inner.block_number; + let bn = payload.payload_inner.payload_inner.payload_inner.block_number; if bn > head_bn + 1 { return Self::sync_until(head_bn + 1, bn, senders); }; @@ -250,15 +263,21 @@ where match self { // Default path once synced. Apply and commit the payload. WaitingForNewPayload | WaitingForForkChoiceWithAttributes => { - let payload = ExecutionPayload::V3(payload); - let sidecar = - ExecutionPayloadSidecar::v3(CancunPayloadFields::new(parent_beacon_block_root, versioned_hashes)); + // let payload = ExecutionPayload::V3(payload); + let sidecar = if ctx.is_prague_active() { + ExecutionPayloadSidecar::v4( + CancunPayloadFields::new(parent_beacon_block_root, versioned_hashes), + PraguePayloadFields::new(RequestsOrHash::empty()), + ) + } else { + ExecutionPayloadSidecar::v3(CancunPayloadFields::new(parent_beacon_block_root, versioned_hashes)) + }; // Clear shared state for each NewPayload event ctx.shared_state.reset(); // NewPayload skipped some blocks. Signal to fetch them all and set state to syncing. - let payload_hash = payload.block_hash(); + let payload_hash = payload.payload_inner.payload_inner.payload_inner.block_hash; // Check if we have already committed this payload. if payload_hash == ctx.db.head_block_hash().expect("couldn't get db head block hash") { return WaitingForForkChoiceWithAttributes; @@ -370,7 +389,7 @@ where /// 4. Returns payload to consensus layer fn handle_get_payload_engine_api( self, - res: oneshot::Sender, + res: oneshot::Sender, ctx: &mut SequencerContext, senders: &SendersSpine, ) -> SequencerState { @@ -404,10 +423,12 @@ where // Commit the block to the db if ctx.config.commit_sealed_frags_to_db { - let sidecar = - ExecutionPayloadSidecar::v3(CancunPayloadFields::new(block.parent_beacon_block_root, vec![])); - let block = payload_to_block(ExecutionPayload::V3(block.execution_payload), sidecar) - .expect("couldn't get block from payload"); + let sidecar = ExecutionPayloadSidecar::v4( + CancunPayloadFields::new(block.parent_beacon_block_root, vec![]), + PraguePayloadFields::new(RequestsOrHash::empty()), + ); + let block = + payload_to_block(block.execution_payload, sidecar).expect("couldn't get block from payload"); ctx.commit_block(&block); ctx.shared_state.reset(); info!("committing to db"); diff --git a/based/crates/sequencer/src/sorting/frag_sequence.rs b/based/crates/sequencer/src/sorting/frag_sequence.rs index 1d7e99bb9..904811546 100644 --- a/based/crates/sequencer/src/sorting/frag_sequence.rs +++ b/based/crates/sequencer/src/sorting/frag_sequence.rs @@ -227,6 +227,6 @@ mod tests { // Seal the block let (_seal, payload) = ctx.seal_block(seq); - assert_eq!(block.hash_slow(), payload.execution_payload.payload_inner.payload_inner.block_hash); + assert_eq!(block.hash_slow(), payload.execution_payload.payload_inner.payload_inner.payload_inner.block_hash); } } diff --git a/docs/docs/getting_started/develop.md b/docs/docs/getting_started/develop.md index 2c816803b..5485d973b 100644 --- a/docs/docs/getting_started/develop.md +++ b/docs/docs/getting_started/develop.md @@ -13,7 +13,7 @@ For local development, you will also need: ### Quick start #### With existing OP chain -The following steps have been tested on Sepolia, with a previously deployed L2 chain (non-pectra) +The following steps have been tested on Sepolia, with a previously deployed L2 chain 1. locate your `rollup.json`, `genesis.json` and `state.json` files 2. run `make config-main-node OP_NODE_DATA_DIR= OP_GETH_DATA_DIR= ROLLUP_JSON= GENESIS_JSON= STATE_JSON=` 3. there should be some files set up in `.local_main_node` diff --git a/follower_node/compose.mac.yml b/follower_node/compose.mac.yml deleted file mode 100644 index 62ad3950c..000000000 --- a/follower_node/compose.mac.yml +++ /dev/null @@ -1,25 +0,0 @@ -services: - based-op-geth: - # macOS: no host networking, map ports instead - ports: - - "${OP_GETH_RPC_PORT:-8645}:${OP_GETH_RPC_PORT:-8645}" - - "${OP_GETH_WS_PORT:-8646}:${OP_GETH_WS_PORT:-8646}" - - "${OP_GETH_ENGINE_RPC_PORT:-8651}:${OP_GETH_ENGINE_RPC_PORT:-8651}" - extra_hosts: - - "host.docker.internal:host-gateway" - - based-op-node: - ports: - # RPC API - - "8547:8547" - # libp2p gossip port (TCP + UDP) - - "${OP_NODE_GOSSIP_PORT:-9103}:${OP_NODE_GOSSIP_PORT:-9103}/tcp" - - "${OP_NODE_GOSSIP_PORT:-9103}:${OP_NODE_GOSSIP_PORT:-9103}/udp" - extra_hosts: - - "host.docker.internal:host-gateway" - - gateway: - ports: - - "${GATEWAY_PORT:-9997}:${GATEWAY_PORT:-9997}" - extra_hosts: - - "host.docker.internal:host-gateway" diff --git a/follower_node/compose.yml b/follower_node/compose.yml index 0ae06e1e6..2632f47c3 100644 --- a/follower_node/compose.yml +++ b/follower_node/compose.yml @@ -1,9 +1,12 @@ +networks: + based_op_net: + external: true + services: based-op-geth: image: ghcr.io/gattaca-com/based-op-geth/based-op-geth:latest pull_policy: always container_name: based-op-geth - network_mode: "host" entrypoint: ["/bin/sh", "-c"] command: - | @@ -44,16 +47,34 @@ services: --bootnodes=$MAIN_OP_GETH_ENODE,enode://3ce86ecae36ae4bc4197e555b0c518c642602596e66f851090fc7a2686d29924b7baa6c4fcc8ca886f00f51f6951c4b4251d1cb67f61dc491a70cec212fd0498@18.185.199.51:31303 \ --rollup.sequencerhttp=$PORTAL + ports: + - "${OP_GETH_RPC_PORT:-8645}:${OP_GETH_RPC_PORT:-8645}" + - "${OP_GETH_WS_PORT:-8646}:${OP_GETH_WS_PORT:-8646}" + - "${OP_GETH_GOSSIP_PORT}:${OP_GETH_GOSSIP_PORT}" + - "${OP_GETH_ENGINE_RPC_PORT:-8651}:${OP_GETH_ENGINE_RPC_PORT:-8651}" + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - ./data/geth:/data/geth - ./config:/config restart: unless-stopped + networks: + - based_op_net based-op-node: image: ghcr.io/gattaca-com/based-optimism/based-op-node:latest pull_policy: always container_name: based-op-node - network_mode: "host" + ports: + # RPC API + - "8547:8547" + # libp2p gossip port (TCP + UDP) + - "${OP_NODE_GOSSIP_PORT:-9103}:${OP_NODE_GOSSIP_PORT:-9103}/tcp" + - "${OP_NODE_GOSSIP_PORT:-9103}:${OP_NODE_GOSSIP_PORT:-9103}/udp" + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - based_op_net command: - op-node - --l2=http://based-op-geth:$OP_GETH_ENGINE_RPC_PORT @@ -89,7 +110,6 @@ services: image: ghcr.io/gattaca-com/based-op/based-gateway:latest pull_policy: always container_name: based-op-gateway - network_mode: "host" command: - --rpc.port=$GATEWAY_PORT - --rpc.host=0.0.0.0 @@ -104,4 +124,8 @@ services: - ./data/gateway:/data/gateway - /tmp:/tmp - /dev/shm:/dev/shm + ports: + - "${GATEWAY_PORT}:${GATEWAY_PORT}" restart: unless-stopped + networks: + - based_op_net diff --git a/main_node/compose.yml b/main_node/compose.yml index 236f1d5cb..bf1ba3168 100644 --- a/main_node/compose.yml +++ b/main_node/compose.yml @@ -1,9 +1,21 @@ -# fill out env_example and copy it to .env besides this file +networks: + based_op_net: + external: true + services: op-geth: - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.8-rc.1 + image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101503.4-rc.1 container_name: op-geth - network_mode: "host" + networks: + - based_op_net + ports: + - "${OP_GETH_RPC_PORT}:${OP_GETH_RPC_PORT}" + - "${OP_GETH_WS_PORT}:${OP_GETH_WS_PORT}" + - "${OP_GETH_ENGINE_RPC_PORT}:${OP_GETH_ENGINE_RPC_PORT}" + - "${OP_GETH_GOSSIP_PORT}:${OP_GETH_GOSSIP_PORT}/tcp" + - "${OP_GETH_GOSSIP_PORT}:${OP_GETH_GOSSIP_PORT}/udp" + extra_hosts: + - "host.docker.internal:host-gateway" entrypoint: ["/bin/sh", "-c"] command: - | @@ -39,41 +51,48 @@ services: --rpc.allow-unprotected-txs \ --discovery.port=$OP_GETH_GOSSIP_PORT \ --port=$OP_GETH_GOSSIP_PORT - volumes: - ./data/geth:/data/geth - ./config:/config restart: unless-stopped op-node: - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node:v1.11.2 - network_mode: "host" + image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node:v1.13.2 container_name: op-node entrypoint: - sh - -c - | - sleep "${OP_NODE_DELAY:-15}" && + sleep "${OP_NODE_DELAY:-10}" && exec op-node "$@" + networks: + - based_op_net + ports: + - "${OP_NODE_RPC_PORT}:${OP_NODE_RPC_PORT}" + - "${OP_NODE_GOSSIP_PORT}:${OP_NODE_GOSSIP_PORT}/tcp" + - "${OP_NODE_GOSSIP_PORT}:${OP_NODE_GOSSIP_PORT}/udp" + extra_hosts: + - "host.docker.internal:host-gateway" + # Override inter-container URLs for bridge networking command: - op-node - - --l2=http://0.0.0.0:$PORTAL_PORT + - --l2=http://based-portal:${PORTAL_PORT} - --l2.jwt-secret=/config/jwt - --verifier.l1-confs=1 - --rollup.config=/config/rollup.json - --rpc.addr=0.0.0.0 - - --rpc.port=$OP_NODE_RPC_PORT + - --rpc.port=${OP_NODE_RPC_PORT} - --rpc.enable-admin - - --l1=$L1_RPC_URL + - --l1=${L1_RPC_URL} - --l1.rpckind=standard - - --l1.beacon=$L1_BEACON_RPC_URL - - --p2p.advertise.ip=$OP_NODE_GOSSIP_IP - - --p2p.advertise.tcp=$OP_NODE_GOSSIP_PORT - - --p2p.advertise.udp=$OP_NODE_GOSSIP_PORT + - --l1.beacon=${L1_BEACON_RPC_URL} + - --p2p.advertise.ip=${OP_NODE_GOSSIP_IP} + - --p2p.advertise.tcp=${OP_NODE_GOSSIP_PORT} + - --p2p.advertise.udp=${OP_NODE_GOSSIP_PORT} - --p2p.listen.ip=0.0.0.0 - - --p2p.listen.tcp=$OP_NODE_GOSSIP_PORT - - --p2p.listen.udp=$OP_NODE_GOSSIP_PORT - - --p2p.sequencer.key=$OP_NODE_SEQUENCER_KEY + - --p2p.listen.tcp=${OP_NODE_GOSSIP_PORT} + - --p2p.listen.udp=${OP_NODE_GOSSIP_PORT} + - --p2p.sequencer.key=${OP_NODE_SEQUENCER_KEY} - --safedb.path=/data/op-node/op-node-beacon-data - --sequencer.enabled @@ -86,13 +105,19 @@ services: restart: unless-stopped op-batcher: - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-batcher:v1.11.2 - network_mode: "host" + image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-batcher:v1.13.2 container_name: op-batcher + networks: + - based_op_net + ports: + - "8548:8548" + extra_hosts: + - "host.docker.internal:host-gateway" + # Override inter-container URLs for bridge networking command: - op-batcher - - --l2-eth-rpc=http://0.0.0.0:$PORTAL_PORT - - --rollup-rpc=http://0.0.0.0:$OP_NODE_RPC_PORT + - --l2-eth-rpc=http://based-portal:${PORTAL_PORT} + - --rollup-rpc=http://op-node:${OP_NODE_RPC_PORT} - --sub-safety-margin=6 - --num-confirmations=1 - --safe-abort-nonce-too-low-count=3 @@ -101,8 +126,8 @@ services: - --rpc.port=8548 - --rpc.enable-admin - --max-channel-duration=50 - - --l1-eth-rpc=$L1_RPC_URL - - --private-key=$OP_BATCHER_PRIVATE_KEY + - --l1-eth-rpc=${L1_RPC_URL} + - --private-key=${OP_BATCHER_PRIVATE_KEY} - --max-blocks-per-span-batch=10 - --data-availability-type=blobs depends_on: @@ -111,18 +136,25 @@ services: op-proposer: image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-proposer:v1.10.0 - network_mode: "host" container_name: op-proposer + networks: + - based_op_net + ports: + - "8560:8560" + extra_hosts: + - "host.docker.internal:host-gateway" + # Override inter-container URLs for bridge networking command: - op-proposer - --poll-interval=12s - --rpc.port=8560 - - --rollup-rpc=http://0.0.0.0:$OP_NODE_RPC_PORT - - --game-factory-address=$DISPUTE_GAME_FACTORY_ADDRESS - - --game-type=$DISPUTE_GAME_TYPE - - --private-key=$OP_PROPOSER_PRIVATE_KEY - - --l1-eth-rpc=$L1_RPC_URL + - --rollup-rpc=http://op-node:${OP_NODE_RPC_PORT} + - --game-factory-address=${DISPUTE_GAME_FACTORY_ADDRESS} + - --game-type=${DISPUTE_GAME_TYPE} + - --private-key=${OP_PROPOSER_PRIVATE_KEY} + - --l1-eth-rpc=${L1_RPC_URL} - --proposal-interval=2s + depends_on: - op-node restart: unless-stopped @@ -130,13 +162,19 @@ services: based-portal: image: ghcr.io/gattaca-com/based-op/based-portal:latest pull_policy: always - network_mode: "host" container_name: based-portal + networks: + - based_op_net + ports: + - "${PORTAL_PORT}:${PORTAL_PORT}" + extra_hosts: + - "host.docker.internal:host-gateway" + # Override inter-container URLs for bridge networking command: - - --fallback.eth_url=http://0.0.0.0:$OP_GETH_RPC_PORT - - --fallback.engine_url=http://0.0.0.0:$OP_GETH_ENGINE_RPC_PORT - - --op_node.url=http://0.0.0.0:$OP_NODE_RPC_PORT - - --registry.url=http://0.0.0.0:$REGISTRY_PORT + - --fallback.eth_url=http://op-geth:${OP_GETH_RPC_PORT} + - --fallback.engine_url=http://op-geth:${OP_GETH_ENGINE_RPC_PORT} + - --op_node.url=http://op-node:${OP_NODE_RPC_PORT} + - --registry.url=http://based-registry:${REGISTRY_PORT} - --gateway.timeout_ms=200 volumes: - ./config:/config @@ -145,21 +183,30 @@ services: based-registry: image: ghcr.io/gattaca-com/based-op/based-registry:latest pull_policy: always - network_mode: "host" container_name: based-registry + networks: + - based_op_net + ports: + - "${REGISTRY_PORT}:${REGISTRY_PORT}" + extra_hosts: + - "host.docker.internal:host-gateway" + # Override inter-container URLs for bridge networking command: - --registry.path=/config/registry.json - - --portal.url=http://0.0.0.0:$OP_GETH_RPC_PORT + - --portal.url=http://op-geth:${OP_GETH_RPC_PORT} - --portal.timeout_ms=100 - - --port=$REGISTRY_PORT + - --port=${REGISTRY_PORT} volumes: - ./config:/config restart: unless-stopped + restarter: image: docker:cli volumes: ["/var/run/docker.sock:/var/run/docker.sock"] command: ["/bin/sh", "-c", "while true; do sleep 3600; docker restart op-batcher; done"] restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" ################################################################################ # Postgres for Blockscout @@ -168,7 +215,6 @@ services: image: postgres:14-alpine container_name: blockscout-postgres restart: unless-stopped - network_mode: "host" # everything else in your stack is host-mode already environment: # these credentials will be baked into the Blockscout DATABASE_URL below POSTGRES_USER: blockscout @@ -177,6 +223,12 @@ services: volumes: - ./data/blockscout-postgres:/var/lib/postgresql/data shm_size: '50gb' + networks: + - based_op_net + ports: + - "5432:5432" + extra_hosts: + - "host.docker.internal:host-gateway" ################################################################################ # Blockscout explorer @@ -189,18 +241,21 @@ services: - -c - bin/blockscout eval "Elixir.Explorer.ReleaseTasks.create_and_migrate()" && bin/blockscout start - network_mode: "host" # everything else in your stack is host-mode already + networks: + - based_op_net + ports: + - "${BLOCKSCOUT_PORT}:${BLOCKSCOUT_PORT}" + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: - op-node # need the L2 node up to serve RPC - blockscout-postgres # need the DB up first environment: - # point to the Postgres above - DATABASE_URL: "postgresql://blockscout:blockscout@0.0.0.0:5432/blockscout" + DATABASE_URL: "postgresql://blockscout:blockscout@blockscout-postgres:5432/blockscout" # point to your L2 JSON-RPC: - ETHEREUM_JSONRPC_HTTP_URL: "http://0.0.0.0:${OP_GETH_RPC_PORT}" - # WebSockets (if you want to enable filters/tx-watch) - ETHEREUM_JSONRPC_WS_URL: "ws://0.0.0.0:${OP_GETH_WS_PORT}" - ETHEREUM_JSONRPC_TRACE_URL: "http://0.0.0.0:${OP_GETH_RPC_PORT}" + ETHEREUM_JSONRPC_HTTP_URL: "http://op-geth:${OP_GETH_RPC_PORT}" + ETHEREUM_JSONRPC_WS_URL: "ws://op-geth:${OP_GETH_WS_PORT}" + ETHEREUM_JSONRPC_TRACE_URL: "http://op-geth:${OP_GETH_RPC_PORT}" ETHEREUM_JSONRPC_VARIANT: "geth" SECRET_KEY_BASE: "56NtB48ear7+wMSf0IQuWDAAazhpb31qyc7GiyspBP2vh7t5zlCsF5QDv76chXeN" # A β€œnetwork name” for Blockscout’s UI @@ -209,5 +264,6 @@ services: # disable Subgraph support if you don’t have one ECTO_USE_SSL: "false" SSG_ENABLED: "false" + PORT: "${BLOCKSCOUT_PORT}" # no ports: host-mode means Blockscout binds to 4000 on the host automatically diff --git a/main_node/env_example b/main_node/env_example index 116ed29a0..46920a91d 100644 --- a/main_node/env_example +++ b/main_node/env_example @@ -9,3 +9,4 @@ PORTAL_PORT=8080 REGISTRY_PORT=8082 GATEWAY_PORT=9997 DISPUTE_GAME_TYPE=1 +BLOCKSCOUT_PORT=4000 diff --git a/main_node/intent.template.toml b/main_node/intent.template.toml index 1b469c705..a340938a8 100644 --- a/main_node/intent.template.toml +++ b/main_node/intent.template.toml @@ -3,12 +3,14 @@ configType = "standard-overrides" l1ChainID = L1_CHAIN_ID fundDevAccounts = true useInterop = false -l1ContractsLocator = "tag://op-contracts/v1.8.0-rc.4" -l2ContractsLocator = "tag://op-contracts/v1.7.0-beta.1+l2-contracts" +l1ContractsLocator = "tag://op-contracts/v3.0.0" +l2ContractsLocator = "tag://op-contracts/v3.0.0" + +l1DevGenesisParams.pragueTimeOffset = 0 [superchainRoles] proxyAdminOwner = "0x1eb2ffc903729a0f03966b917003800b145f56e2" - protocolVersionsOwner = "0x79add5713b383daa0a138d3c4780c7a1804a8090" + protocolVersionsOwner = "0xfd1d2e729ae8eee2e146c033bf4400fe75284301" guardian = "0x7a50f00e8d05b95f98fe38d8bee366a7324dcf7e" [[chains]] From bee11e7e4a27e277dfda086c0f331791dff7d641 Mon Sep 17 00:00:00 2001 From: gd <90608901+gd-0@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:29:37 +0100 Subject: [PATCH 6/7] impl fabric gateway commitments api (#185) Co-authored-by: Louis Ponet --- based/Cargo.lock | 6 + based/Cargo.toml | 2 +- based/bin/gateway/src/main.rs | 5 +- based/bin/portal/src/server.rs | 6 +- based/crates/common/src/api.rs | 4 +- .../common/src/communication/messages.rs | 20 ++- based/crates/common/src/config.rs | 2 + based/crates/common/src/fabric.rs | 95 +++++++++++++ based/crates/common/src/lib.rs | 1 + based/crates/common/src/p2p.rs | 75 +++++----- based/crates/rpc/Cargo.toml | 12 ++ .../crates/rpc/examples/fabric_commitment.rs | 132 ++++++++++++++++++ based/crates/rpc/src/fabric.rs | 83 +++++++++++ based/crates/rpc/src/gossiper.rs | 29 +++- based/crates/rpc/src/lib.rs | 52 +++++-- based/crates/sequencer/src/context.rs | 10 +- based/crates/sequencer/src/lib.rs | 6 +- based/crates/sequencer/src/simulator.rs | 4 +- 18 files changed, 476 insertions(+), 68 deletions(-) create mode 100644 based/crates/common/src/fabric.rs create mode 100644 based/crates/rpc/examples/fabric_commitment.rs create mode 100644 based/crates/rpc/src/fabric.rs diff --git a/based/Cargo.lock b/based/Cargo.lock index 80f259b8e..d2829c1e2 100644 --- a/based/Cargo.lock +++ b/based/Cargo.lock @@ -1876,10 +1876,13 @@ dependencies = [ name = "bop-rpc" version = "0.1.0" dependencies = [ + "alloy-consensus 0.14.0", "alloy-eips 0.14.0", "alloy-primitives", "alloy-rpc-types", "bop-common", + "clap", + "eyre", "jsonrpsee", "op-alloy-consensus 0.14.1", "op-alloy-rpc-types 0.14.1", @@ -1887,10 +1890,13 @@ dependencies = [ "reqwest", "reth-optimism-primitives", "reth-rpc-layer", + "serde", "serde_json", "tokio", "tower 0.4.13", "tracing", + "tracing-subscriber 0.3.19", + "tree_hash", ] [[package]] diff --git a/based/Cargo.toml b/based/Cargo.toml index 4292b508d..91bacada7 100644 --- a/based/Cargo.toml +++ b/based/Cargo.toml @@ -131,4 +131,4 @@ tree_hash_derive = "0.10" uuid = { version = "1.12.1", features = ["serde", "v4"] } backtrace = "0.3.73" -tabwriter = "1.4.1" +tabwriter = "1.4.1" \ No newline at end of file diff --git a/based/bin/gateway/src/main.rs b/based/bin/gateway/src/main.rs index 50ed4c1d9..67bc45aa1 100644 --- a/based/bin/gateway/src/main.rs +++ b/based/bin/gateway/src/main.rs @@ -63,6 +63,7 @@ fn run(args: GatewayArgs) -> eyre::Result<()> { }; let sequencer_config: SequencerConfig = (&args).into(); let evm_config = sequencer_config.evm_config.clone(); + let (tx, _) = tokio::sync::broadcast::channel(10_000); std::thread::scope(|s| { let rt: Arc = tokio::runtime::Builder::new_current_thread() @@ -74,7 +75,7 @@ fn run(args: GatewayArgs) -> eyre::Result<()> { s.spawn({ let rt = rt.clone(); - start_rpc(&args, &spine, &rt); + start_rpc(&args, &spine, &rt, tx.clone()); move || rt.block_on(wait_for_signal()) }); @@ -103,7 +104,7 @@ fn run(args: GatewayArgs) -> eyre::Result<()> { let root_peer_url = args.gossip_root_peer_url.clone(); let gossip_signer_private_key = args.gossip_signer_private_key.map(|key| ECDSASigner::new(key).unwrap()); s.spawn(|| { - Gossiper::new(root_peer_url, gossip_signer_private_key).run( + Gossiper::new(root_peer_url, gossip_signer_private_key, tx).run( spine.to_connections("Gossiper"), ActorConfig::default().with_min_loop_duration(Duration::from_millis(10)), ); diff --git a/based/bin/portal/src/server.rs b/based/bin/portal/src/server.rs index cecdedda3..45afc428f 100644 --- a/based/bin/portal/src/server.rs +++ b/based/bin/portal/src/server.rs @@ -3,13 +3,13 @@ use std::{fmt, net::SocketAddr, sync::Arc, time::Duration}; use alloy_eips::eip7685::RequestsOrHash; use alloy_primitives::{Address, B256, Bytes, U256}; use alloy_rpc_types::{ - BlockId, BlockNumberOrTag, + BlockId, BlockNumberOrTag, engine::{ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, }; use bop_common::{ api::{ - EngineApiClient, EngineApiServer, EthApiClient, EthApiServer, OpGethAdminApiClient, - OpNodeApiClient, OpNodeP2PApiClient, OpRpcBlock, PORTAL_CAPABILITIES, PortalApiServer, RegistryApiClient, + EngineApiClient, EngineApiServer, EthApiClient, EthApiServer, OpGethAdminApiClient, OpNodeApiClient, + OpNodeP2PApiClient, OpRpcBlock, PORTAL_CAPABILITIES, PortalApiServer, RegistryApiClient, }, communication::messages::{RpcError, RpcResult}, utils::{uuid, wait_for_signal}, diff --git a/based/crates/common/src/api.rs b/based/crates/common/src/api.rs index 6a6086891..d3731c36f 100644 --- a/based/crates/common/src/api.rs +++ b/based/crates/common/src/api.rs @@ -9,7 +9,9 @@ use alloy_rpc_types::{ use jsonrpsee::proc_macros::rpc; use op_alloy_consensus::OpTxEnvelope; use op_alloy_rpc_types::OpTransactionReceipt; -use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; +use op_alloy_rpc_types_engine::{ + OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes, +}; use reqwest::Url; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/based/crates/common/src/communication/messages.rs b/based/crates/common/src/communication/messages.rs index 7bf77671e..998625e01 100644 --- a/based/crates/common/src/communication/messages.rs +++ b/based/crates/common/src/communication/messages.rs @@ -3,7 +3,7 @@ use std::{ sync::Arc, }; -use alloy_consensus::{constants::EMPTY_WITHDRAWALS, BlockHeader, Transaction as TransactionTrait}; +use alloy_consensus::{BlockHeader, Transaction as TransactionTrait, constants::EMPTY_WITHDRAWALS}; use alloy_eips::eip2718::Encodable2718; use alloy_rpc_types::engine::{ ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ForkchoiceState, PayloadAttributes, PayloadError, @@ -11,7 +11,7 @@ use alloy_rpc_types::engine::{ }; use jsonrpsee::types::{ErrorCode, ErrorObject as RpcErrorObject}; use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; -use reth_evm::{execute::BlockExecutionError, NextBlockEnvAttributes}; +use reth_evm::{NextBlockEnvAttributes, execute::BlockExecutionError}; use reth_primitives_traits::transaction::signed::RecoveryError; use revm_primitives::{Address, U256}; use serde::{Deserialize, Serialize}; @@ -258,6 +258,9 @@ pub enum RpcError { #[error("response channel closed {0}")] ChannelClosed(#[from] oneshot::error::RecvError), + #[error("broadcast channel closed")] + BroadcastChannelClosed, + #[error("invalid transaction bytes")] InvalidTransaction(#[from] alloy_rlp::Error), @@ -275,6 +278,12 @@ pub enum RpcError { #[error("no return")] NoReturn, + + #[error("no commitment for request: {0:?}")] + NoCommitmentForRequest(B256), + + #[error("serialization error: {0}")] + Serialization(#[from] serde_json::Error), } impl From for RpcErrorObject<'static> { @@ -283,16 +292,23 @@ impl From for RpcErrorObject<'static> { RpcError::Internal | RpcError::Timeout(_) | RpcError::ChannelClosed(_) | + RpcError::BroadcastChannelClosed | RpcError::Jsonrpsee(_) | RpcError::TokioJoin(_) | RpcError::Db(_) | RpcError::Io(_) | + RpcError::Serialization(_) | RpcError::NoReturn => internal_error(), RpcError::InvalidTransaction(error) => RpcErrorObject::owned( ErrorCode::InvalidParams.code(), ErrorCode::InvalidParams.message(), Some(error.to_string()), ), + RpcError::NoCommitmentForRequest(slot) => RpcErrorObject::owned( + ErrorCode::InvalidParams.code(), + ErrorCode::InvalidParams.message(), + Some(format!("no commitment for request: {slot}")), + ), } } } diff --git a/based/crates/common/src/config.rs b/based/crates/common/src/config.rs index a92484aa8..044ae5975 100644 --- a/based/crates/common/src/config.rs +++ b/based/crates/common/src/config.rs @@ -33,6 +33,8 @@ pub struct GatewayArgs { /// The port to run the engine_ and eth_ RPC #[arg(long = "rpc.port", default_value_t = 9997)] pub rpc_port: u16, + #[arg(long = "rpc.port_no_auth", default_value_t = 9998)] + pub rpc_port_no_auth: u16, #[arg(long = "rpc.jwt")] pub rpc_jwt: String, /// Url to an L2 eth api rpc diff --git a/based/crates/common/src/fabric.rs b/based/crates/common/src/fabric.rs new file mode 100644 index 000000000..8416a6dc9 --- /dev/null +++ b/based/crates/common/src/fabric.rs @@ -0,0 +1,95 @@ +use alloy_primitives::{Address, B256, Bytes}; +use jsonrpsee::proc_macros::rpc; +use serde::{Deserialize, Serialize}; +use ssz_types::{VariableList, typenum}; +use tree_hash_derive::TreeHash; + +use crate::communication::messages::RpcResult; + +pub type MaxCommitmentRequestPayloadSize = typenum::U1048576; // 1MB. +pub type CommitmentRequestPayload = VariableList; + +pub const FRAG_COMMITMENT_TYPE: u64 = 7; + +/// A CommitmentRequest message created by a user +#[derive(Clone, Serialize, Deserialize, Debug, TreeHash)] +#[serde(rename_all = "camelCase")] +pub struct CommitmentRequest { + pub commitment_type: u64, + pub payload: CommitmentRequestPayload, + pub slasher: Address, +} + +/// A Commitment message responding to a CommitmentRequest +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Commitment { + pub commitment_type: u64, + pub payload: Bytes, + pub request_hash: B256, + pub slasher: Address, +} + +/// A signed Commitment binding to a CommitmentRequest +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SignedCommitment { + pub commitment: Commitment, + pub signature: Bytes, +} + +/// Specifies which commitments can be made for a specific chain +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Offering { + pub chain_id: u64, + pub commitment_types: Vec, +} + +/// Information about a Gateway's offerings at a specific slot +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SlotInfo { + pub slot: u64, + pub offerings: Vec, +} + +/// Response containing multiple SlotInfo +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct SlotInfoResponse { + pub slots: Vec, +} + +/// Fee information for a specific commitment request +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FeeInfo { + pub payload: Bytes, + pub commitment_type: u64, +} + +impl Default for FeeInfo { + fn default() -> Self { + Self { payload: Bytes::new(), commitment_type: FRAG_COMMITMENT_TYPE } + } +} + +#[rpc(client, server, namespace = "gateway")] +pub trait FabricGatewayApi { + /// Request a new SignedCommitment + #[method(name = "commitment")] + async fn post_commitment(&self, commitment_request: CommitmentRequest) -> RpcResult; + + /// Request an existing SignedCommitment by request hash + #[method(name = "getCommitment")] + async fn get_commitment(&self, request_hash: B256) -> RpcResult; + + /// Get Gateway information for upcoming slots + #[method(name = "slots")] + async fn get_slots(&self) -> RpcResult; + + /// Get commitment fee information + #[method(name = "fee")] + async fn get_fee_info(&self, commitment_request: CommitmentRequest) -> RpcResult; +} diff --git a/based/crates/common/src/lib.rs b/based/crates/common/src/lib.rs index 06a076712..9e4519690 100644 --- a/based/crates/common/src/lib.rs +++ b/based/crates/common/src/lib.rs @@ -4,6 +4,7 @@ pub mod communication; pub mod config; pub mod db; pub mod eth; +pub mod fabric; pub mod p2p; pub mod shared; pub mod signing; diff --git a/based/crates/common/src/p2p.rs b/based/crates/common/src/p2p.rs index fa2a5f6f6..8daf65a0b 100644 --- a/based/crates/common/src/p2p.rs +++ b/based/crates/common/src/p2p.rs @@ -8,34 +8,6 @@ use tree_hash_derive::TreeHash; use crate::{signing::ECDSASigner, transaction::Transaction as BuilderTransaction}; -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize, AsRefStr)] -#[tree_hash(enum_behaviour = "union")] -#[serde(untagged)] -#[non_exhaustive] -pub enum VersionedMessage { - FragV0(FragV0), - SealV0(SealV0), - EnvV0(EnvV0), -} - -impl From for VersionedMessage { - fn from(value: FragV0) -> Self { - Self::FragV0(value) - } -} - -impl From for VersionedMessage { - fn from(value: SealV0) -> Self { - Self::SealV0(value) - } -} - -impl From for VersionedMessage { - fn from(value: EnvV0) -> Self { - Self::EnvV0(value) - } -} - pub type MaxExtraDataSize = typenum::U256; pub type ExtraData = VariableList; @@ -91,7 +63,7 @@ pub struct FragV0 { pub is_last: bool, /// Ordered list of EIP-2718 encoded transactions #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] - txs: Transactions, + pub txs: Transactions, } impl FragV0 { @@ -124,13 +96,52 @@ pub struct SealV0 { pub block_hash: B256, } +#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize, AsRefStr)] +#[tree_hash(enum_behaviour = "union")] +#[serde(untagged)] +#[non_exhaustive] +pub enum VersionedMessage { + FragV0(FragV0), + SealV0(SealV0), + EnvV0(EnvV0), +} + impl VersionedMessage { - pub fn to_json(&self, signer: &ECDSASigner) -> serde_json::Value { + pub fn to_signed(self, signer: &ECDSASigner) -> SignedVersionedMessage { let hash = self.tree_hash_root(); let signature = signer.sign_message(hash).expect("couldn't sign message"); let signature = Bytes::from(signature.as_bytes()); + SignedVersionedMessage { message: self, signature } + } +} + +impl From for VersionedMessage { + fn from(value: FragV0) -> Self { + Self::FragV0(value) + } +} + +impl From for VersionedMessage { + fn from(value: SealV0) -> Self { + Self::SealV0(value) + } +} + +impl From for VersionedMessage { + fn from(value: EnvV0) -> Self { + Self::EnvV0(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedVersionedMessage { + pub message: VersionedMessage, + pub signature: Bytes, +} - let method = match &self { +impl SignedVersionedMessage { + pub fn to_json(&self) -> serde_json::Value { + let method = match &self.message { VersionedMessage::FragV0(_) => "based_newFrag", VersionedMessage::SealV0(_) => "based_sealFrag", VersionedMessage::EnvV0(_) => "based_env", @@ -139,7 +150,7 @@ impl VersionedMessage { serde_json::json!({ "jsonrpc": "2.0", "method": method, - "params": [{"signature": signature, "message": self}], + "params": [{"signature": self.signature, "message": self.message}], "id": 1 }) } diff --git a/based/crates/rpc/Cargo.toml b/based/crates/rpc/Cargo.toml index 95dfd5248..3f3bed08f 100644 --- a/based/crates/rpc/Cargo.toml +++ b/based/crates/rpc/Cargo.toml @@ -19,4 +19,16 @@ serde_json.workspace = true tokio.workspace = true tower.workspace = true tracing.workspace = true +tree_hash.workspace = true reth-rpc-layer.workspace = true + +[dev-dependencies] +clap.workspace = true +tracing-subscriber.workspace = true +eyre.workspace = true +alloy-consensus.workspace = true +serde.workspace = true + +[[example]] +name = "fabric_commitment" +path = "examples/fabric_commitment.rs" \ No newline at end of file diff --git a/based/crates/rpc/examples/fabric_commitment.rs b/based/crates/rpc/examples/fabric_commitment.rs new file mode 100644 index 000000000..a9b235c6c --- /dev/null +++ b/based/crates/rpc/examples/fabric_commitment.rs @@ -0,0 +1,132 @@ +use std::time::Duration; + +use alloy_consensus::TxEip1559; +use alloy_eips::{Decodable2718, eip2718::Encodable2718}; +use alloy_primitives::{Address, Bytes, TxKind, U256, b256, hex}; +use bop_common::{ + fabric::{CommitmentRequest, FRAG_COMMITMENT_TYPE, FabricGatewayApiClient}, + p2p::{SignedVersionedMessage, VersionedMessage}, + signing::ECDSASigner, +}; +use clap::Parser; +use jsonrpsee::http_client::HttpClientBuilder; +use op_alloy_consensus::OpTxEnvelope; +use reqwest::Url; +use serde::Deserialize; +use serde_json::json; +use tracing::{error, info}; + +#[derive(Parser, Debug)] +#[command(version, about = "Test fabric commitment RPC")] +struct Args { + #[arg(long, default_value = "http://0.0.0.0:9998")] + gateway_url: Url, + + /// ETH RPC URL for getting nonce + #[arg(long, default_value = "http://0.0.0.0:8545")] + eth_rpc_url: Url, + + /// Private key for signing (hex string, defaults to test account) + #[arg(long)] + private_key: Option, +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize)] +struct RpcResponse { + jsonrpc: String, + id: u64, + result: Option, +} + +async fn get_nonce(eth_client: &reqwest::Client, eth_url: &Url, address: Address) -> eyre::Result { + let payload = json!({ + "id": 1, + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [address.to_string(), "latest"] + }); + + let response = eth_client.post(eth_url.clone()).json(&payload).send().await?; + let response_text = response.text().await?; + let rpc_response: RpcResponse = serde_json::from_str(&response_text)?; + + Ok(rpc_response.result.unwrap_or_default().to()) +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).init(); + + let args = Args::parse(); + info!(?args, "Starting fabric commitment test"); + + let commitment_client = + HttpClientBuilder::default().request_timeout(Duration::from_secs(30)).build(args.gateway_url)?; + + let eth_client = reqwest::Client::builder().timeout(Duration::from_secs(10)).build()?; + + let signer = if let Some(pk) = args.private_key { + let key_bytes = hex::decode(pk.strip_prefix("0x").unwrap_or(&pk))?; + ECDSASigner::try_from_secret(&key_bytes)? + } else { + // Default test account + ECDSASigner::try_from_secret( + b256!("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80").as_ref(), + )? + }; + + // Create test transaction + let nonce = get_nonce(ð_client, &args.eth_rpc_url, signer.address).await?; + let tx = TxEip1559 { + chain_id: 43444, + nonce, + gas_limit: 21000, + max_fee_per_gas: 10_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + to: TxKind::Call(signer.address), + value: U256::from(1000000000000000u64), + ..Default::default() + }; + + let signed_tx = signer.sign_tx(tx)?; + let op_tx = OpTxEnvelope::Eip1559(signed_tx); + let encoded_tx = op_tx.encoded_2718(); + info!(?op_tx, sender = %signer.address, hash = %op_tx.tx_hash(), "Created test transaction"); + + // Create commitment request + let commitment_request = CommitmentRequest { + commitment_type: FRAG_COMMITMENT_TYPE, + payload: encoded_tx.into(), + slasher: Address::random(), + }; + info!(?commitment_request, "Sending commitment request"); + + match commitment_client.post_commitment(commitment_request.clone()).await { + Ok(signed_commitment) => { + info!(?signed_commitment, "Received signed commitment"); + + // Deserialise the response into a SignedFrag and verify that the tx is in the frag + let msg: SignedVersionedMessage = serde_json::from_slice(&signed_commitment.commitment.payload)?; + let VersionedMessage::FragV0(frag) = &msg.message else { + return Err(eyre::eyre!("Expected FragV0 message got {:?}", msg.message)); + }; + if frag + .txs + .iter() + .filter_map(|t| OpTxEnvelope::decode_2718(&mut t.as_ref()).ok()) + .any(|t| *t.hash() == op_tx.tx_hash()) + { + info!("SUCCESS: Transaction found in frag! Commitment received"); + } else { + return Err(eyre::eyre!("Transaction not found in frag commitment")); + } + } + Err(e) => { + error!("FAILED: Could not get commitment: {}", e); + return Err(e.into()); + } + } + + Ok(()) +} diff --git a/based/crates/rpc/src/fabric.rs b/based/crates/rpc/src/fabric.rs new file mode 100644 index 000000000..b9637fbea --- /dev/null +++ b/based/crates/rpc/src/fabric.rs @@ -0,0 +1,83 @@ +use std::{sync::Arc, time::Duration}; + +use alloy_eips::Decodable2718; +use alloy_primitives::B256; +use bop_common::{ + communication::messages::{RpcError, RpcResult}, + fabric::{Commitment, CommitmentRequest, FabricGatewayApiServer, FeeInfo, SignedCommitment, SlotInfoResponse}, + p2p::VersionedMessage, + telemetry::TelemetryUpdate, + transaction::Transaction, +}; +use jsonrpsee::core::async_trait; +use op_alloy_consensus::OpTxEnvelope; +use tokio::time::timeout; +use tracing::Level; +use tree_hash::TreeHash; + +use crate::RpcServer; + +const COMMITMENT_TIMEOUT: Duration = Duration::from_secs(1); + +#[async_trait] +impl FabricGatewayApiServer for RpcServer { + #[tracing::instrument(skip_all, err, ret(level = Level::TRACE))] + async fn post_commitment(&self, commitment: CommitmentRequest) -> RpcResult { + let request_hash = commitment.tree_hash_root(); + let tx = Arc::new(Transaction::decode(commitment.payload.to_vec().into())?); + let tx_hash = tx.tx_hash(); + + let mut receiver = self.frag_receiver_spawner.subscribe(); + + // Send the transaction to the sequencer + TelemetryUpdate::send_ref(tx.uuid, tx.to_ingested_telemetry(), &self.telemetry_producer); + let _ = self.new_order_tx.send(tx.into()); + + // Wait for the transaction to be committed + let commitment_future = async { + while let Ok(msg) = receiver.recv().await { + tracing::info!("got {msg:?}"); + let VersionedMessage::FragV0(frag) = &msg.message else { + continue; + }; + if frag + .txs + .iter() + .filter_map(|t| OpTxEnvelope::decode_2718(&mut t.as_ref()).ok()) + .any(|t| *t.hash() == tx_hash) + { + let commitment = Commitment { + commitment_type: commitment.commitment_type, + payload: serde_json::to_vec(&msg)?.into(), + request_hash, + slasher: commitment.slasher, + }; + return Ok(SignedCommitment { commitment, signature: msg.signature }); + } + } + Err(RpcError::BroadcastChannelClosed) + }; + + timeout(COMMITMENT_TIMEOUT, commitment_future) + .await + .map_err(|_| RpcError::NoCommitmentForRequest(request_hash))? + } + + #[tracing::instrument(skip_all, err, ret(level = Level::TRACE))] + async fn get_commitment(&self, request_hash: B256) -> RpcResult { + // TODO: fetch from datastore + Err(RpcError::NoCommitmentForRequest(request_hash)) + } + + #[tracing::instrument(skip_all, err, ret(level = Level::TRACE))] + async fn get_slots(&self) -> RpcResult { + // Not implemented for L2s + Ok(SlotInfoResponse { slots: vec![] }) + } + + #[tracing::instrument(skip_all, err, ret(level = Level::TRACE))] + async fn get_fee_info(&self, _commitment_request: CommitmentRequest) -> RpcResult { + // No additional fees in the gateway + return Ok(FeeInfo::default()); + } +} diff --git a/based/crates/rpc/src/gossiper.rs b/based/crates/rpc/src/gossiper.rs index cce2edbae..3093613c2 100644 --- a/based/crates/rpc/src/gossiper.rs +++ b/based/crates/rpc/src/gossiper.rs @@ -1,16 +1,26 @@ -use bop_common::{actor::Actor, communication::SpineConnections, p2p, signing::ECDSASigner}; +use bop_common::{ + actor::Actor, + communication::SpineConnections, + p2p::{self, SignedVersionedMessage}, + signing::ECDSASigner, +}; use jsonrpsee::client_transport::ws::Url; use reqwest::blocking::{Client, ClientBuilder}; -use tracing::{error, info}; +use tracing::error; pub struct Gossiper { target_rpc: Url, client: Client, signer: ECDSASigner, + frag_broadcast: tokio::sync::broadcast::Sender, } impl Gossiper { - pub fn new(target_rpc: Url, signer: Option) -> Self { + pub fn new( + target_rpc: Url, + signer: Option, + frag_broadcast: tokio::sync::broadcast::Sender, + ) -> Self { let client = ClientBuilder::new() .timeout(std::time::Duration::from_secs(10)) .build() @@ -18,11 +28,18 @@ impl Gossiper { let signer = signer.unwrap_or_else(ECDSASigner::random); - Self { target_rpc, client, signer } + Self { target_rpc, client, signer, frag_broadcast } } fn gossip(&self, msg: p2p::VersionedMessage) { - let payload = msg.to_json(&self.signer); + let signed = msg.to_signed(&self.signer); + let payload = signed.to_json(); + + if matches!(signed.message, p2p::VersionedMessage::FragV0(_)) { + if let Err(e) = self.frag_broadcast.send(signed) { + tracing::debug!(" broadcast of frag failed {e}") + } + } let Ok(res) = self.client.post(self.target_rpc.clone()).json(&payload).send() else { tracing::error!("couldn't send {}", payload); @@ -33,7 +50,7 @@ impl Gossiper { let body = res.text().expect("couldn't read response"); if code.is_success() { - info!("successfully sent {}", msg.as_ref()); + tracing::debug!("successfully sent {:?}", payload); } else { error!(body, %payload, code = code.as_u16(), "failed to send"); } diff --git a/based/crates/rpc/src/lib.rs b/based/crates/rpc/src/lib.rs index 4d641f181..3156d523b 100644 --- a/based/crates/rpc/src/lib.rs +++ b/based/crates/rpc/src/lib.rs @@ -10,6 +10,8 @@ use bop_common::{ }, config::GatewayArgs, db::DatabaseRead, + fabric::FabricGatewayApiServer, + p2p::SignedVersionedMessage, telemetry::{TelemetryUpdate, telemetry_queue}, time::Duration, transaction::Transaction, @@ -20,12 +22,19 @@ use tokio::runtime::Runtime; use tracing::{Level, error, info, trace}; mod engine; +mod fabric; pub mod gossiper; -pub fn start_rpc(config: &GatewayArgs, spine: &Spine, rt: &Runtime) { - let addr = SocketAddr::new(config.rpc_host.into(), config.rpc_port); - let server = RpcServer::new(spine, config.sequencer_jwt()); - rt.spawn(server.run(addr)); +pub fn start_rpc( + config: &GatewayArgs, + spine: &Spine, + rt: &Runtime, + rx_spawner: tokio::sync::broadcast::Sender, +) { + let addr_auth = SocketAddr::new(config.rpc_host.into(), config.rpc_port); + let addr_no_auth = SocketAddr::new(config.rpc_host.into(), config.rpc_port_no_auth); + let server = RpcServer::new(spine, config.sequencer_jwt(), rx_spawner); + rt.spawn(server.run(addr_auth, addr_no_auth)); } // TODO: jwt auth @@ -37,22 +46,28 @@ struct RpcServer { engine_rpc_tx: Sender, jwt: JwtSecret, telemetry_producer: Producer, + frag_receiver_spawner: tokio::sync::broadcast::Sender, } impl RpcServer { - pub fn new(spine: &Spine, jwt: JwtSecret) -> Self { + pub fn new( + spine: &Spine, + jwt: JwtSecret, + frag_receiver_spawner: tokio::sync::broadcast::Sender, + ) -> Self { Self { new_order_tx: spine.into(), engine_rpc_tx: spine.into(), engine_timeout: Duration::from_secs(1), jwt, telemetry_producer: telemetry_queue().into(), + frag_receiver_spawner, } } #[tracing::instrument(skip_all, name = "rpc")] - pub async fn run(self, addr: SocketAddr) { - info!(%addr, "starting RPC server"); + pub async fn run(self, addr_auth: SocketAddr, addr_no_auth: SocketAddr) { + info!(%addr_auth, "starting RPC server"); let validator = JwtAuthValidator::new(self.jwt); let auth_layer = AuthLayer::new(validator); let service_builder = tower::ServiceBuilder::new() @@ -60,22 +75,35 @@ impl RpcServer { .layer(auth_layer) .timeout(std::time::Duration::from_secs(2)); - let server = ServerBuilder::default() + let server_auth = ServerBuilder::default() .max_request_body_size(u32::MAX) .max_response_body_size(u32::MAX) .set_http_middleware(service_builder) - .build(addr) + .build(addr_auth) .await .expect("failed to create eth RPC server"); let mut module = MinimalEthApiServer::into_rpc(self.clone()); - module.merge(EngineApiServer::into_rpc(self)).expect("failed to merge modules"); + module.merge(EngineApiServer::into_rpc(self.clone())).expect("failed to merge modules"); + let server_handle_auth = server_auth.start(module); + + let service_builder = tower::ServiceBuilder::new().timeout(std::time::Duration::from_secs(2)); + + let server_no_auth = ServerBuilder::default() + .max_request_body_size(u32::MAX) + .max_response_body_size(u32::MAX) + .set_http_middleware(service_builder) + .build(addr_no_auth) + .await + .expect("failed to create eth RPC server"); + let module = FabricGatewayApiServer::into_rpc(self); + let server_handle_no_auth = server_no_auth.start(module); - let server_handle = server.start(module); //TODO: Handle other communcation from sequencer ? // Idea: we have this part do rpc requests, using the rpc->sequencer channel, // but we make it part of another sync actor that uses the connections and gathers // state etc in a spinloop that the rpc runtime can use to serve requests with? - server_handle.stopped().await; + server_handle_auth.stopped().await; + server_handle_no_auth.stopped().await; error!("server stopped"); } diff --git a/based/crates/sequencer/src/context.rs b/based/crates/sequencer/src/context.rs index dfef2de77..95c3e68ec 100644 --- a/based/crates/sequencer/src/context.rs +++ b/based/crates/sequencer/src/context.rs @@ -1,6 +1,6 @@ use std::{collections::VecDeque, fmt::Display, sync::Arc}; -use alloy_consensus::{BlockHeader, Header, EMPTY_OMMER_ROOT_HASH}; +use alloy_consensus::{BlockHeader, EMPTY_OMMER_ROOT_HASH, Header}; use alloy_eips::merge::BEACON_NONCE; use alloy_rpc_types::engine::{ BlobsBundleV1, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ForkchoiceState, @@ -10,7 +10,7 @@ use bop_common::{ debug_panic, p2p::{FragV0, SealV0}, shared::SharedState, - telemetry::{telemetry_queue, TelemetryUpdate}, + telemetry::{TelemetryUpdate, telemetry_queue}, time::Timer, transaction::Transaction, typedefs::*, @@ -20,16 +20,16 @@ use bop_pool::transaction::pool::TxPool; use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpPayloadAttributes}; use op_revm::OpSpecId; use reth_chainspec::EthereumHardforks; -use reth_evm::{block::SystemCaller, env::EvmEnv, execute::ProviderError, ConfigureEvm}; +use reth_evm::{ConfigureEvm, block::SystemCaller, env::EvmEnv, execute::ProviderError}; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_consensus::isthmus::withdrawals_root; use reth_optimism_evm::OpNextBlockEnvAttributes; use reth_optimism_forks::{OpHardfork, OpHardforks}; use reth_provider::StorageRootProvider; -use revm_primitives::{b256, Bytes, B256, U256}; +use revm_primitives::{B256, Bytes, U256, b256}; use tracing::info; -use crate::{block_sync::BlockSync, sorting::SortingData, FragSequence, SequencerConfig}; +use crate::{FragSequence, SequencerConfig, block_sync::BlockSync, sorting::SortingData}; /// These are used to time different parts of the sequencer loop pub struct SequencerTimers { diff --git a/based/crates/sequencer/src/lib.rs b/based/crates/sequencer/src/lib.rs index 5fcc9f008..319682ac2 100644 --- a/based/crates/sequencer/src/lib.rs +++ b/based/crates/sequencer/src/lib.rs @@ -9,13 +9,13 @@ use alloy_rpc_types::engine::{ use bop_common::{ actor::Actor, communication::{ - messages::{self, BlockFetch, BlockSyncError, EngineApi, SimulatorToSequencer, SimulatorToSequencerMsg}, Connections, ReceiversSpine, SendersSpine, SpineConnections, TrackedSenders, + messages::{self, BlockFetch, BlockSyncError, EngineApi, SimulatorToSequencer, SimulatorToSequencerMsg}, }, db::DatabaseWrite, p2p::{EnvV0, VersionedMessage}, shared::SharedState, - telemetry::{self, system::SystemNotification, Telemetry, TelemetryUpdate}, + telemetry::{self, Telemetry, TelemetryUpdate, system::SystemNotification}, time::{Duration, Repeater}, transaction::Transaction, typedefs::{BlockSyncMessage, DatabaseRef}, @@ -24,7 +24,7 @@ use bop_db::DatabaseRead; use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, OpPayloadAttributes}; use reth_optimism_primitives::OpTransactionSigned; use reth_primitives::RecoveredBlock; -use reth_primitives_traits::{block::TestBlock, SignedTransaction}; +use reth_primitives_traits::{SignedTransaction, block::TestBlock}; use reth_provider::StorageRootProvider; use revm_primitives::b256; use sorting::FragSequence; diff --git a/based/crates/sequencer/src/simulator.rs b/based/crates/sequencer/src/simulator.rs index aa41ee60d..6334c06ee 100644 --- a/based/crates/sequencer/src/simulator.rs +++ b/based/crates/sequencer/src/simulator.rs @@ -87,7 +87,9 @@ impl< pub fn update_evm_environments(&mut self, evm_block_params: EvmEnv) { let timestamp = evm_block_params.block_env.timestamp(); self.evm_tof.modify_block(|b| *b = evm_block_params.block_env.clone()); // TODO: re-use mem - self.evm_tof.modify_cfg(|c| c.spec = *evm_block_params.spec_id()); + self.evm_tof.modify_cfg(|c| *c = evm_block_params.cfg_env.clone()); + self.evm_sorting.modify_block(|b| *b = evm_block_params.block_env.clone()); // TODO: re-use mem + self.evm_sorting.modify_cfg(|c| *c = evm_block_params.cfg_env.clone()); self.regolith_active = self.evm_config.chain_spec().fork(OpHardfork::Regolith).active_at_timestamp(timestamp); } From 426a4aa46d94b318676f5644ffdce11ad91a6cc1 Mon Sep 17 00:00:00 2001 From: Suthiwat Umpornpaiboon <91750004+BrycePy@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:46:05 +0100 Subject: [PATCH 7/7] Gateway heartbeat (#186) --- .gitignore | 6 + based/Cargo.lock | 2 + based/Cargo.toml | 1 + based/bin/gateway/Cargo.toml | 2 + based/bin/gateway/src/main.rs | 2 +- based/bin/gateway_registry/src/main.rs | 13 +- based/bin/portal/src/cli.rs | 4 + based/bin/portal/src/server.rs | 308 ++++++++++++++++++++----- based/crates/common/src/api.rs | 10 +- based/crates/common/src/config.rs | 3 + based/crates/common/src/utils.rs | 4 + based/crates/rpc/src/lib.rs | 16 +- follower_node/compose.yml | 2 +- 13 files changed, 305 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index e1c239e56..8286a152b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,11 @@ follower_node/.env .local/ .local_gateway_and_follower .local_main_node +.local_main_node2 based/.envrc .envrc + +based/data +based/genesis.json +based/temp_registry.json +temp diff --git a/based/Cargo.lock b/based/Cargo.lock index d2829c1e2..73addb744 100644 --- a/based/Cargo.lock +++ b/based/Cargo.lock @@ -1844,11 +1844,13 @@ dependencies = [ "bop-sequencer", "clap", "eyre", + "jsonrpsee", "op-alloy-consensus 0.14.1", "op-alloy-rpc-types 0.14.1", "reqwest", "reth-optimism-chainspec", "reth-primitives", + "reth-rpc-builder", "revm-primitives", "serde", "serde_json", diff --git a/based/Cargo.toml b/based/Cargo.toml index 91bacada7..abd4b5f2b 100644 --- a/based/Cargo.toml +++ b/based/Cargo.toml @@ -97,6 +97,7 @@ reth-primitives = { path = "../reth/crates/primitives" } reth-primitives-traits = { path = "../reth/crates/primitives-traits" } reth-provider = { path = "../reth/crates/storage/provider", features = ["test-utils"] } reth-revm = { path = "../reth/crates/revm" } +reth-rpc-builder = { path = "../reth/crates/rpc/rpc-builder" } reth-rpc-layer = { path = "../reth/crates/rpc/rpc-layer" } reth-stages-types = { path = "../reth/crates/stages/types" } reth-storage-api = { path = "../reth/crates/storage/storage-api" } diff --git a/based/bin/gateway/Cargo.toml b/based/bin/gateway/Cargo.toml index 3afef2025..efc879527 100644 --- a/based/bin/gateway/Cargo.toml +++ b/based/bin/gateway/Cargo.toml @@ -15,9 +15,11 @@ clap.workspace = true eyre.workspace = true reqwest.workspace = true reth-optimism-chainspec.workspace = true +reth-rpc-builder.workspace = true revm-primitives.workspace = true tokio.workspace = true tracing.workspace = true +jsonrpsee.workspace = true [dev-dependencies] alloy-consensus.workspace = true diff --git a/based/bin/gateway/src/main.rs b/based/bin/gateway/src/main.rs index 67bc45aa1..e9db94711 100644 --- a/based/bin/gateway/src/main.rs +++ b/based/bin/gateway/src/main.rs @@ -101,6 +101,7 @@ fn run(args: GatewayArgs) -> eyre::Result<()> { ); }); } + let root_peer_url = args.gossip_root_peer_url.clone(); let gossip_signer_private_key = args.gossip_signer_private_key.map(|key| ECDSASigner::new(key).unwrap()); s.spawn(|| { @@ -122,6 +123,5 @@ fn run(args: GatewayArgs) -> eyre::Result<()> { }); } }); - Ok(()) } diff --git a/based/bin/gateway_registry/src/main.rs b/based/bin/gateway_registry/src/main.rs index 82779ef66..a4bd52489 100644 --- a/based/bin/gateway_registry/src/main.rs +++ b/based/bin/gateway_registry/src/main.rs @@ -56,6 +56,10 @@ pub struct RegistryArgs { /// Prefix of log files #[arg(long = "log.prefix", default_value = "bop-portal.log")] pub log_prefix: String, + + /// mock blocknumber + #[arg(long = "use_mock_blocknumber", default_value_t = false)] + pub use_mock_blocknumber: bool, } impl From<&RegistryArgs> for LoggingConfig { @@ -99,6 +103,9 @@ pub struct RegistryServer { gateway_clients: Arc>>, gateway_update_blocks: u64, registry_path: Arc, + + mock_blocknumber: U256, + use_mock_blocknumber: bool, } impl RegistryServer { @@ -128,6 +135,8 @@ impl RegistryServer { gateway_clients, gateway_update_blocks: args.gateway_update_blocks, registry_path, + mock_blocknumber: U256::from(0), + use_mock_blocknumber: args.use_mock_blocknumber, }) } @@ -161,7 +170,9 @@ impl RegistryServer { impl RegistryApiServer for RegistryServer { #[tracing::instrument(skip_all, err, ret(level = Level::DEBUG))] async fn get_future_gateway(&self, n_blocks_into_the_future: u64) -> RpcResult<(u64, Url, Address, B256)> { - let curblock = self.eth_client.block_number().await?; + // let curblock = self.eth_client.block_number().await?; + let curblock = + if !self.use_mock_blocknumber { self.eth_client.block_number().await? } else { self.mock_blocknumber }; let gateways = self.gateway_clients.read(); let n_gateways = gateways.len(); if n_gateways == 0 { diff --git a/based/bin/portal/src/cli.rs b/based/bin/portal/src/cli.rs index 31d45f62b..cf5f32de6 100644 --- a/based/bin/portal/src/cli.rs +++ b/based/bin/portal/src/cli.rs @@ -61,6 +61,10 @@ pub struct PortalArgs { /// Prefix of log files #[arg(long = "log.prefix", default_value = "bop-portal.log")] pub log_prefix: String, + + /// gateway inactivity timeout in milliseconds + #[arg(long = "gateway.inactivity_timeout_ms", default_value_t = 3000)] + pub gateway_inactivity_timeout_ms: u64, } impl PortalArgs { diff --git a/based/bin/portal/src/server.rs b/based/bin/portal/src/server.rs index 45afc428f..7b5ed7c95 100644 --- a/based/bin/portal/src/server.rs +++ b/based/bin/portal/src/server.rs @@ -1,17 +1,26 @@ -use std::{fmt, net::SocketAddr, sync::Arc, time::Duration}; +use std::{ + fmt, + net::SocketAddr, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; use alloy_eips::eip7685::RequestsOrHash; -use alloy_primitives::{Address, B256, Bytes, U256}; +use alloy_primitives::{Address, B256, Bytes, U256, hex}; use alloy_rpc_types::{ BlockId, BlockNumberOrTag, engine::{ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus}, }; use bop_common::{ api::{ - EngineApiClient, EngineApiServer, EthApiClient, EthApiServer, OpGethAdminApiClient, OpNodeApiClient, - OpNodeP2PApiClient, OpRpcBlock, PORTAL_CAPABILITIES, PortalApiServer, RegistryApiClient, + ControlApiClient, EngineApiClient, EngineApiServer, EthApiClient, EthApiServer, OpGethAdminApiClient, + OpNodeApiClient, OpNodeP2PApiClient, OpRpcBlock, PORTAL_CAPABILITIES, PortalApiServer, RegistryApiClient, + RegistryApiServer, }, communication::messages::{RpcError, RpcResult}, + time::{Duration, Instant}, utils::{uuid, wait_for_signal}, }; use jsonrpsee::{ @@ -37,7 +46,23 @@ pub type AuthRpcClient = jsonrpsee::http_client::HttpClient, + last_seen: Arc>, +} + +impl Gateway { + pub fn is_active(&self, gateway_inactivity_timeout_ms: u64) -> bool { + match self.last_seen.as_ref() { + Some(last_seen) => { + let elapsed = last_seen.elapsed(); + elapsed < Duration::from_millis(gateway_inactivity_timeout_ms) + } + None => false, + } + } } impl fmt::Debug for Gateway { @@ -46,15 +71,25 @@ impl fmt::Debug for Gateway { } } +impl Gateway { + fn new(id: Url, client: AuthRpcClient, jwt: String, address: Address) -> Self { + Self { id, jwt, client, ping: Arc::new(Duration::from_millis(0)), last_seen: Arc::new(None), address } + } +} + #[derive(Clone)] pub struct PortalServer { fallback_eth_client: RpcClient, fallback_client: AuthRpcClient, op_node_client: RpcClient, registry_client: RpcClient, + current_gateway_candidate: Arc>>, current_gateway: Arc>>, gateway_timeout: Duration, gateways: Arc>>, + new_payload_block_number: Arc, + new_payload_block_hash: Arc>, + current_block_number: Arc, args: Arc, } @@ -77,42 +112,44 @@ impl PortalServer { let gateway_timeout = Duration::from_millis(args.gateway_timeout_ms); - let current_gateway = Arc::new(Mutex::new(registry_client.current_gateway().await.ok().and_then( - |(_, gateway_url, _, jwt_as_b256)| { - create_gateway_client( - gateway_url, - unsafe { - std::mem::transmute::, reth_rpc_layer::JwtSecret>(jwt_as_b256) - }, - gateway_timeout, - ) - .ok() - }, - ))); - - let mut gateways = vec![]; - for (gateway_url, _, jwt_as_b256) in registry_client.registered_gateways().await.unwrap_or_else(|_| vec![]) { - gateways.push(create_gateway_client( - gateway_url, - unsafe { - std::mem::transmute::, reth_rpc_layer::JwtSecret>(jwt_as_b256) - }, - gateway_timeout, - )?) - } - + let gateways = vec![]; let gateways = Arc::new(RwLock::new(gateways)); - Ok(Self { + let temp = Self { fallback_eth_client, fallback_client, op_node_client, registry_client, - current_gateway, - gateways, + current_gateway_candidate: Arc::new(Mutex::new(None)), + current_gateway: Arc::new(Mutex::new(None)), gateway_timeout, + gateways, + new_payload_block_number: Arc::new(AtomicU64::new(0)), + new_payload_block_hash: Arc::new(Mutex::new(B256::ZERO)), + current_block_number: Arc::new(AtomicU64::new(0)), args: Arc::new(args), - }) + }; + + match temp.refresh_gateway_list().await { + Ok(_) => { + temp.update_current_gateway().await?; + info!("Successfully fetched registered gateways"); + } + Err(err) => { + error!(%err, "Failed to fetch registered gateways"); + } + } + + match temp.registry_client.current_gateway().await { + Ok((block_number, _, _, _)) => { + temp.current_block_number.store(block_number, Ordering::Relaxed); + } + Err(err) => { + error!(%err, "Failed to get future gateway from registry"); + } + }; + + Ok(temp) } pub async fn run(self, addr: SocketAddr) -> eyre::Result<()> { @@ -150,8 +187,13 @@ impl PortalServer { tokio::spawn(async move { loop { - let _ = self.refresh().await; - tokio::time::sleep(Duration::from_secs(1)).await; + match self.refresh_gateway_list().await { + Ok(_) => {} + Err(err) => { + error!(%err, "Failed to fetch registered gateways"); + } + } + tokio::time::sleep(Duration::from_secs(1).into()).await; } }); let server_handle = server.start(module); @@ -173,33 +215,117 @@ impl PortalServer { self.gateways.read().clone() } - pub async fn refresh(&self) -> eyre::Result<()> { + async fn fetch_registered_gateways(&self) -> eyre::Result<()> { let mut gateways = vec![]; - for (gateway_url, _, jwt_as_b256) in self.registry_client.registered_gateways().await? { - let Ok(client) = create_gateway_client( - gateway_url, - unsafe { - std::mem::transmute::, reth_rpc_layer::JwtSecret>(jwt_as_b256) - }, - self.gateway_timeout, - ) else { - continue; - }; - gateways.push(client); + let registered_gateways = self.registry_client.registered_gateways().await?; + for (gateway_url, address, jwt_as_b256) in registered_gateways { + let jwt_str = hex::encode(jwt_as_b256); + let client = create_gateway_client(gateway_url, jwt_str.clone(), address, self.gateway_timeout); + if let Ok(client) = client { + gateways.push(client); + } + } + + for gateway in gateways.iter_mut() { + let ping_start = Instant::now(); + match ControlApiClient::heartbeat(&gateway.client).await { + Ok(_) => { + let ping_duration = ping_start.elapsed(); + gateway.ping = Arc::new(ping_duration); + gateway.last_seen = Arc::new(Some(Instant::now())); + info!("successfully pinged gateway={} ping={:>9}", gateway.id, ping_duration.to_string()); + } + Err(err) => { + error!(%err, ?gateway, "failed to ping gateway"); + } + } } + *self.gateways.write() = gateways; + Ok(()) + } + + async fn update_current_gateway_candidate( + &self, + n_blocks_into_future: u64, + expected_block_number: Option, + ) -> eyre::Result<()> { + let (block_number, gateway_url, _, _) = self.registry_client.get_future_gateway(n_blocks_into_future).await?; + if let Some(expected_block_number) = expected_block_number { + if block_number != expected_block_number { + error!( + "CRITICAL: The block number we got from the registry ({}) does not match the expected block number ({})", + block_number, expected_block_number + ); + panic!( + "CRITICAL: The block number we got from the registry ({}) does not match the expected block number ({})", + block_number, expected_block_number + ); + // return Ok(()); + } + } + let current_gateway_index = self.gateways().iter().position(|g| g.id == gateway_url); + match current_gateway_index { + Some(index) => { + let gateway = self.gateways().get(index).cloned().unwrap(); + *self.current_gateway_candidate.lock().await = Some(gateway); + let mut i = index; + while !self + .current_gateway_candidate + .lock() + .await + .as_ref() + .unwrap() + .is_active(self.args.gateway_inactivity_timeout_ms) + { + i = (i + 1) % self.gateways().len(); + if i == index { + error!("CRITICAL: No gateway is available, all gateways are stale"); + return Ok(()); + } + *self.current_gateway_candidate.lock().await = Some(self.gateways().get(i).cloned().unwrap()); + } + } + None => { + error!( + "CRITICAL: Couldn't find the current gateway in the list we got from the registry. This means the registry is inconsistent" + ); + } + } - let (_, gateway_url, _, _) = self.registry_client.current_gateway().await?; - for g in self.gateways() { - if g.id == gateway_url { - *self.current_gateway.lock().await = Some(g); - return Ok(()); + Ok(()) + } + + async fn update_current_gateway(&self) -> eyre::Result<()> { + match self.current_gateway_candidate.lock().await.clone() { + Some(new_gateway) => { + self.current_gateway.lock().await.replace(new_gateway); + } + None => { + error!("CRITICAL: Couldn't find the current gateway"); } } + // match self.current_gateway.lock().await.replace() { + // Some(old_gateway) => { + // info!(?old_gateway, "updated current gateway"); + // } + // None => { + // error!("CRITICAL: Couldn't find the current gateway"); + // } + // } + Ok(()) + } - error!( - "CRITICAL: Couldn't find the current gateway in the list we got from the registry. This means the registry is inconsistent" - ); + pub async fn refresh_gateway_list(&self) -> eyre::Result<()> { + self.fetch_registered_gateways().await?; + // self.update_current_gateway_candidate(0, None).await?; + Ok(()) + } + + pub async fn on_fork(&self, expected_block_number: Option) -> eyre::Result<()> { + self.update_current_gateway_candidate(0, expected_block_number).await?; + self.update_current_gateway().await?; + self.current_block_number.store(0, Ordering::Relaxed); Ok(()) } @@ -418,6 +544,17 @@ impl EngineApiServer for PortalServer { debug!(%parent_block_hash, "new request (no attributes)"); } + if payload_attributes.is_some() && *self.new_payload_block_hash.lock().await == parent_block_hash { + let new_block_number = self.new_payload_block_number.load(Ordering::Relaxed) + 1; + self.current_block_number.store(new_block_number, Ordering::Relaxed); + match self.on_fork(Some(new_block_number)).await { + Ok(_) => {} + Err(err) => { + error!(%err, "failed to process new payload"); + } + }; + } + let response = self.fallback_client.fork_choice_updated_v3(fork_choice_state, payload_attributes.clone()).await?; @@ -455,6 +592,9 @@ impl EngineApiServer for PortalServer { let excess_blob_gas = payload.payload_inner.excess_blob_gas; debug!(block_number, %block_hash, gas_limit, gas_used, n_txs, n_withdrawals, blob_gas_used, excess_blob_gas, "new request"); + self.new_payload_block_number.store(block_number, Ordering::Relaxed); + *self.new_payload_block_hash.lock().await = block_hash; + let response = self .fallback_client .new_payload_v4(payload.clone(), versioned_hashes.clone(), parent_beacon_block_root, requests.clone()) @@ -509,6 +649,8 @@ impl EngineApiServer for PortalServer { let excess_blob_gas = payload.excess_blob_gas; debug!(block_number, %block_hash, gas_limit, gas_used, n_txs, n_withdrawals, blob_gas_used, excess_blob_gas, "new request"); + self.new_payload_block_number.store(block_number, Ordering::Relaxed); + *self.new_payload_block_hash.lock().await = block_hash; let response = self .fallback_client @@ -611,7 +753,6 @@ impl EngineApiServer for PortalServer { } let payload = gateway.or(fallback)?; - Ok(payload) } } @@ -658,11 +799,59 @@ impl PortalApiServer for PortalServer { } } +// TODO: Implement this properly +#[async_trait] +impl RegistryApiServer for PortalServer { + async fn get_future_gateway(&self, n_blocks_into_future: u64) -> RpcResult<(u64, Url, Address, B256)> { + match self.registry_client.get_future_gateway(n_blocks_into_future).await { + Ok(gateway) => Ok(gateway), + Err(err) => { + error!(%err, "Failed to get future gateway"); + Err(RpcError::Internal) + } + } + } + + async fn current_gateway(&self) -> RpcResult<(u64, Url, Address, B256)> { + match self.current_gateway.lock().await.as_ref() { + Some(gateway) => { + let block_number = self.current_block_number.load(Ordering::Relaxed); + let url = gateway.id.clone(); + let address = gateway.address; + let jwt_as_b256 = gateway.jwt.as_bytes().try_into().map_err(|_| RpcError::Internal)?; + + Ok((block_number, url, address, jwt_as_b256)) + } + None => Err(RpcError::Internal), + } + } + + async fn registered_gateways(&self) -> RpcResult> { + match self.registry_client.registered_gateways().await { + Ok(gateways) => Ok(gateways), + Err(err) => { + error!(%err, "Failed to get registered gateways"); + Err(RpcError::Internal) + } + } + } + + async fn register_gateway(&self, gateway: (Url, Address, B256)) -> RpcResult<()> { + match self.registry_client.register_gateway(gateway).await { + Ok(()) => Ok(()), + Err(err) => { + error!(%err, "Failed to register gateway"); + Err(RpcError::Internal) + } + } + } +} + fn create_client(url: Url, timeout: Duration) -> eyre::Result { let client = HttpClientBuilder::default() .max_request_size(u32::MAX) .max_response_size(u32::MAX) - .request_timeout(timeout) + .request_timeout(timeout.into()) .build(url)?; Ok(client) } @@ -675,14 +864,15 @@ fn create_auth_client(url: Url, jwt: JwtSecret, timeout: Duration) -> eyre::Resu .max_request_size(u32::MAX) .max_response_size(u32::MAX) .set_http_middleware(middleware) - .request_timeout(timeout) + .request_timeout(timeout.into()) .build(url)?; Ok(client) } -fn create_gateway_client(url: Url, jwt: JwtSecret, timeout: Duration) -> eyre::Result { +fn create_gateway_client(url: Url, jwt_str: String, address: Address, timeout: Duration) -> eyre::Result { + let jwt = JwtSecret::from_hex(&jwt_str).map_err(|_| eyre::eyre!("Invalid JWT secret"))?; let client = create_auth_client(url.clone(), jwt, timeout)?; - let gateway_client = Gateway { client, id: url }; + let gateway_client = Gateway::new(url, client, jwt_str, address); Ok(gateway_client) } diff --git a/based/crates/common/src/api.rs b/based/crates/common/src/api.rs index d3731c36f..a581ae56b 100644 --- a/based/crates/common/src/api.rs +++ b/based/crates/common/src/api.rs @@ -163,7 +163,6 @@ pub trait PortalApi { /// The network id of the l1 #[method(name = "l1ChainId")] async fn l1_chain_id(&self) -> RpcResult; - /// rollup.json file #[method(name = "fileRollup")] async fn file_rollup(&self) -> RpcResult; @@ -184,6 +183,15 @@ pub trait PortalApi { async fn op_geth_bootnode_enode(&self) -> RpcResult; } +#[rpc(client, server, namespace = "control")] +pub trait ControlApi { + // Heartbeat API + #[method(name = "heartbeat")] + async fn heartbeat(&self) -> RpcResult<()> { + Ok(()) + } +} + #[rpc(client, server, namespace = "optimism")] pub trait OpNodeApi { /// The rollup config of the op-node diff --git a/based/crates/common/src/config.rs b/based/crates/common/src/config.rs index 044ae5975..2a08e4713 100644 --- a/based/crates/common/src/config.rs +++ b/based/crates/common/src/config.rs @@ -37,6 +37,9 @@ pub struct GatewayArgs { pub rpc_port_no_auth: u16, #[arg(long = "rpc.jwt")] pub rpc_jwt: String, + /// PortalApi RPC URL + #[arg(long = "portal_rpc.url", default_value = "http://localhost:9998")] + pub portal_rpc_url: Url, /// Url to an L2 eth api rpc #[arg(long = "eth_client.url", default_value = "http://localhost:8545")] pub eth_client_url: Url, diff --git a/based/crates/common/src/utils.rs b/based/crates/common/src/utils.rs index 241ada868..1569fdc77 100644 --- a/based/crates/common/src/utils.rs +++ b/based/crates/common/src/utils.rs @@ -190,6 +190,10 @@ pub fn utcnow_sec() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() } +pub fn utcnow_ms() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis().try_into().unwrap() +} + #[macro_export] macro_rules! debug_panic { ($($arg:tt)*) => (if cfg!(debug_assertions) { panic!($($arg)*); } else {tracing::error!($($arg)*)}) diff --git a/based/crates/rpc/src/lib.rs b/based/crates/rpc/src/lib.rs index 3156d523b..b6a3d2d07 100644 --- a/based/crates/rpc/src/lib.rs +++ b/based/crates/rpc/src/lib.rs @@ -3,7 +3,7 @@ use std::{net::SocketAddr, sync::Arc}; use alloy_primitives::{B256, Bytes}; use alloy_rpc_types::engine::JwtSecret; use bop_common::{ - api::{EngineApiServer, MinimalEthApiServer}, + api::{ControlApiServer, EngineApiServer, MinimalEthApiServer}, communication::{ Producer, Sender, Spine, messages::{EngineApi, RpcResult}, @@ -83,7 +83,9 @@ impl RpcServer { .await .expect("failed to create eth RPC server"); let mut module = MinimalEthApiServer::into_rpc(self.clone()); - module.merge(EngineApiServer::into_rpc(self.clone())).expect("failed to merge modules"); + module.merge(EngineApiServer::into_rpc(self.clone().clone())).expect("failed to merge modules"); + module.merge(ControlApiServer::into_rpc(self.clone())).expect("failed to merge modules"); + let server_handle_auth = server_auth.start(module); let service_builder = tower::ServiceBuilder::new().timeout(std::time::Duration::from_secs(2)); @@ -109,9 +111,6 @@ impl RpcServer { } } -/// Note: this is a temporary RPC implementation that only serves the lastest state from the sequencer. -/// It doesn't adhere to the specific block number or hash requests. -/// This will ultimately be replaced by the RPC server in the EL when the full Frag handling is implemented. #[async_trait] impl MinimalEthApiServer for RpcServer { #[tracing::instrument(skip_all, err, ret(level = Level::TRACE))] @@ -126,3 +125,10 @@ impl MinimalEthApiServer for RpcServer { Ok(hash) } } + +#[async_trait] +impl ControlApiServer for RpcServer { + async fn heartbeat(&self) -> RpcResult<()> { + Ok(()) + } +} diff --git a/follower_node/compose.yml b/follower_node/compose.yml index 2632f47c3..e663eb456 100644 --- a/follower_node/compose.yml +++ b/follower_node/compose.yml @@ -44,7 +44,7 @@ services: --allow-insecure-unlock \ --discovery.port=$OP_GETH_GOSSIP_PORT \ --port=$OP_GETH_GOSSIP_PORT \ - --bootnodes=$MAIN_OP_GETH_ENODE,enode://3ce86ecae36ae4bc4197e555b0c518c642602596e66f851090fc7a2686d29924b7baa6c4fcc8ca886f00f51f6951c4b4251d1cb67f61dc491a70cec212fd0498@18.185.199.51:31303 \ + --bootnodes= \ --rollup.sequencerhttp=$PORTAL ports: