diff --git a/Cargo.lock b/Cargo.lock index 696a1d8d..538cd809 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3153,7 +3153,9 @@ dependencies = [ "figment", "futures", "getrandom 0.2.15", + "helios-consensus-core", "helios-core", + "helios-ethereum", "hex", "libp2p", "libp2p-identity", diff --git a/cli/src/main.rs b/cli/src/main.rs index 6263e1af..6bf93739 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,6 +7,7 @@ use std::{ sync::{Arc, Mutex}, }; +use alloy::primitives::hex; use alloy::primitives::B256; use clap::{Args, Parser, Subcommand}; use dirs::home_dir; @@ -14,10 +15,6 @@ use eyre::Result; use figment::providers::Serialized; use figment::value::Value; use futures::executor::block_on; -use tracing::{error, info}; -use tracing_subscriber::filter::{EnvFilter, LevelFilter}; -use tracing_subscriber::FmtSubscriber; - use helios_core::client::Client; use helios_core::consensus::Consensus; use helios_core::network_spec::NetworkSpec; @@ -25,6 +22,9 @@ use helios_ethereum::config::{cli::CliConfig, Config as EthereumConfig}; use helios_ethereum::database::FileDB; use helios_ethereum::{EthereumClient, EthereumClientBuilder}; use helios_opstack::{config::Config as OpStackConfig, OpStackClient, OpStackClientBuilder}; +use tracing::{error, info}; +use tracing_subscriber::filter::{EnvFilter, LevelFilter}; +use tracing_subscriber::FmtSubscriber; #[tokio::main] async fn main() -> Result<()> { @@ -191,6 +191,20 @@ struct OpStackArgs { execution_rpc: Option, #[clap(short, long, env)] consensus_rpc: Option, + #[clap( + short = 'w', + long = "ethereum-checkpoint", + env = "ETHEREUM_CHECKPOINT", + help = "Set custom weak subjectivity checkpoint for chosen Ethereum network. Helios uses this to sync and trustlessly fetch the correct unsafe signer address used by " + )] + checkpoint: Option, + #[clap( + short = 'l', + long = "ethereum-load-external-fallback", + env = "ETHEREUM_LOAD_EXTERNAL_FALLBACK", + help = "Enable fallback for weak subjectivity checkpoint. Use if --ethereum-checkpoint fails." + )] + load_external_fallback: bool, } impl OpStackArgs { @@ -232,6 +246,14 @@ impl OpStackArgs { user_dict.insert("rpc_port", Value::from(port)); } + if self.load_external_fallback { + user_dict.insert("load_external_fallback", Value::from(true)); + } + + if let Some(checkpoint) = self.checkpoint { + user_dict.insert("checkpoint", Value::from(hex::encode(checkpoint))); + } + Serialized::from(user_dict, &self.network) } } diff --git a/core/src/execution/mod.rs b/core/src/execution/mod.rs index bc8979d6..193fc466 100644 --- a/core/src/execution/mod.rs +++ b/core/src/execution/mod.rs @@ -22,12 +22,11 @@ use self::types::Account; pub mod constants; pub mod errors; pub mod evm; +pub mod proof; pub mod rpc; pub mod state; pub mod types; -mod proof; - #[derive(Clone)] pub struct ExecutionClient> { pub rpc: R, @@ -63,7 +62,7 @@ impl> ExecutionClient { let proof = self .rpc - .get_proof(address, slots, block.number.to()) + .get_proof(address, slots, block.number.into()) .await?; let account_path = keccak256(address).to_vec(); diff --git a/core/src/execution/rpc/http_rpc.rs b/core/src/execution/rpc/http_rpc.rs index 4eb52b96..8f77319d 100644 --- a/core/src/execution/rpc/http_rpc.rs +++ b/core/src/execution/rpc/http_rpc.rs @@ -53,12 +53,12 @@ impl ExecutionRpc for HttpRpc { &self, address: Address, slots: &[B256], - block: u64, + block: BlockId, ) -> Result { let proof_response = self .provider .get_proof(address, slots.to_vec()) - .block_id(block.into()) + .block_id(block) .await .map_err(|e| RpcError::new("get_proof", e))?; diff --git a/core/src/execution/rpc/mock_rpc.rs b/core/src/execution/rpc/mock_rpc.rs index 17c977bc..3da2d207 100644 --- a/core/src/execution/rpc/mock_rpc.rs +++ b/core/src/execution/rpc/mock_rpc.rs @@ -1,7 +1,9 @@ use std::{fs::read_to_string, path::PathBuf}; use alloy::primitives::{Address, B256, U256}; -use alloy::rpc::types::{AccessList, EIP1186AccountProofResponse, FeeHistory, Filter, Log}; +use alloy::rpc::types::{ + AccessList, BlockId, EIP1186AccountProofResponse, FeeHistory, Filter, Log, +}; use async_trait::async_trait; use eyre::{eyre, Result}; @@ -26,7 +28,7 @@ impl ExecutionRpc for MockRpc { &self, _address: Address, _slots: &[B256], - _block: u64, + _block: BlockId, ) -> Result { let proof = read_to_string(self.path.join("proof.json"))?; Ok(serde_json::from_str(&proof)?) diff --git a/core/src/execution/rpc/mod.rs b/core/src/execution/rpc/mod.rs index 56706dc4..e8a7d68a 100644 --- a/core/src/execution/rpc/mod.rs +++ b/core/src/execution/rpc/mod.rs @@ -1,5 +1,7 @@ use alloy::primitives::{Address, B256, U256}; -use alloy::rpc::types::{AccessList, EIP1186AccountProofResponse, FeeHistory, Filter, Log}; +use alloy::rpc::types::{ + AccessList, BlockId, EIP1186AccountProofResponse, FeeHistory, Filter, Log, +}; use async_trait::async_trait; use eyre::Result; @@ -20,7 +22,7 @@ pub trait ExecutionRpc: Send + Clone + Sync + 'static { &self, address: Address, slots: &[B256], - block: u64, + block: BlockId, ) -> Result; async fn create_access_list( diff --git a/core/src/lib.rs b/core/src/lib.rs index 91378f0a..1c24b63b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,8 +1,7 @@ pub mod client; pub mod consensus; pub mod errors; +pub mod execution; pub mod network_spec; pub mod time; pub mod types; - -mod execution; diff --git a/core/src/types.rs b/core/src/types.rs index b15390c2..e4868038 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -25,6 +25,7 @@ pub struct Block { pub size: U64, pub state_root: B256, pub timestamp: U64, + #[serde(default)] pub total_difficulty: U64, pub transactions: Transactions, pub transactions_root: B256, diff --git a/ethereum/src/config/mod.rs b/ethereum/src/config/mod.rs index 55af3e37..6a9dcb78 100644 --- a/ethereum/src/config/mod.rs +++ b/ethereum/src/config/mod.rs @@ -91,3 +91,24 @@ impl Config { } } } + +impl From for Config { + fn from(base: BaseConfig) -> Self { + Config { + rpc_bind_ip: Some(base.rpc_bind_ip), + rpc_port: Some(base.rpc_port), + consensus_rpc: base.consensus_rpc.unwrap_or_default(), + execution_rpc: String::new(), + checkpoint: None, + default_checkpoint: base.default_checkpoint, + chain: base.chain, + forks: base.forks, + max_checkpoint_age: base.max_checkpoint_age, + data_dir: base.data_dir, + fallback: None, + load_external_fallback: base.load_external_fallback, + strict_checkpoint_age: base.strict_checkpoint_age, + database_type: None, + } + } +} diff --git a/ethereum/src/consensus.rs b/ethereum/src/consensus.rs index 1b640ea0..c030a88f 100644 --- a/ethereum/src/consensus.rs +++ b/ethereum/src/consensus.rs @@ -42,6 +42,7 @@ pub struct ConsensusClient, DB: Database> { pub block_recv: Option>>, pub finalized_block_recv: Option>>>, pub checkpoint_recv: watch::Receiver>, + shutdown_send: watch::Sender, genesis_time: u64, config: Arc, phantom: PhantomData<(S, R, DB)>, @@ -79,6 +80,7 @@ impl, DB: Database> Consensus } fn shutdown(&self) -> Result<()> { + self.shutdown_send.send(true)?; Ok(()) } } @@ -88,6 +90,7 @@ impl, DB: Database> ConsensusClient, DB: Database> ConsensusClient::new( &rpc, @@ -138,28 +143,42 @@ impl, DB: Database> ConsensusClient { + if *shutdown_rx.borrow() { + info!(target: "helios::consensus", "shutting down consensus client"); + break; + } + } + _ = interval.tick() => { + let res = inner.advance().await; + if let Err(err) = res { + warn!(target: "helios::consensus", "advance error: {}", err); + continue; + } + + let res = inner.send_blocks().await; + if let Err(err) = res { + warn!(target: "helios::consensus", "send error: {}", err); + continue; + } + } } } }); - save_new_checkpoints(checkpoint_recv.clone(), db.clone(), initial_checkpoint); + save_new_checkpoints( + checkpoint_recv.clone(), + db.clone(), + initial_checkpoint, + shutdown_recv, + ); Ok(ConsensusClient { block_recv: Some(block_recv), finalized_block_recv: Some(finalized_block_recv), checkpoint_recv, + shutdown_send, genesis_time, config: config_clone, phantom: PhantomData, @@ -177,6 +196,7 @@ fn save_new_checkpoints( mut checkpoint_recv: watch::Receiver>, db: Arc, initial_checkpoint: B256, + mut shutdown_recv: watch::Receiver, ) { #[cfg(not(target_arch = "wasm32"))] let run = tokio::spawn; @@ -187,20 +207,28 @@ fn save_new_checkpoints( run(async move { let mut last_saved_checkpoint = initial_checkpoint; loop { - let new_checkpoint = *checkpoint_recv.borrow_and_update(); - if let Some(new_checkpoint) = new_checkpoint.as_ref() { - if *new_checkpoint != last_saved_checkpoint { - // There is a more recent checkpoint to save - if db.save_checkpoint(*new_checkpoint).is_err() { - warn!(target: "helios::consensus", "failed to save checkpoint"); - } else { - info!(target: "helios::consensus", "saved checkpoint to DB: 0x{}", hex::encode(*new_checkpoint)); - last_saved_checkpoint = *new_checkpoint; + tokio::select! { + _ = shutdown_recv.changed() => { + if *shutdown_recv.borrow() { + break; + } + } + checkpoint_result = checkpoint_recv.changed() => { + if checkpoint_result.is_err() { + break; + } + let new_checkpoint = *checkpoint_recv.borrow_and_update(); + if let Some(new_checkpoint) = new_checkpoint.as_ref() { + if *new_checkpoint != last_saved_checkpoint { + if db.save_checkpoint(*new_checkpoint).is_err() { + warn!(target: "helios::consensus", "failed to save checkpoint"); + } else { + info!(target: "helios::consensus", "saved checkpoint to DB: 0x{}", hex::encode(*new_checkpoint)); + last_saved_checkpoint = *new_checkpoint; + } + } } } - } - if checkpoint_recv.changed().await.is_err() { - break; } } }); diff --git a/helios-ts/src/opstack.rs b/helios-ts/src/opstack.rs index 47fe946b..2fd7d58b 100644 --- a/helios-ts/src/opstack.rs +++ b/helios-ts/src/opstack.rs @@ -43,6 +43,8 @@ impl OpStackClient { consensus_rpc, chain: network_config.chain, rpc_socket: None, + load_external_fallback: None, + checkpoint: None, }; let inner = map_err(OpStackClientBuilder::new().config(config).build())?; diff --git a/opstack/Cargo.toml b/opstack/Cargo.toml index 7affdca1..1315e96c 100644 --- a/opstack/Cargo.toml +++ b/opstack/Cargo.toml @@ -37,6 +37,8 @@ snap = "1" figment = { version = "0.10.7", features = ["toml", "env"] } helios-core = { path = "../core" } +helios-ethereum = { path = "../ethereum" } +helios-consensus-core = { path = "../ethereum/consensus-core" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # server diff --git a/opstack/bin/server.rs b/opstack/bin/server.rs index 636232f7..c1fc5944 100644 --- a/opstack/bin/server.rs +++ b/opstack/bin/server.rs @@ -21,16 +21,20 @@ async fn main() -> Result<()> { let chain_id = config.chain.chain_id; let unsafe_signer = config.chain.unsafe_signer; + let system_config_contract = config.chain.system_config_contract; let server_addr = cli.server_address; let gossip_addr = cli.gossip_address; let replica_urls = cli.replica_urls.unwrap_or_default(); + let execution_rpc = cli.execution_rpc; start_server( server_addr, gossip_addr, chain_id, unsafe_signer, + system_config_contract, replica_urls, + execution_rpc, ) .await?; @@ -67,4 +71,6 @@ struct Cli { gossip_address: SocketAddr, #[clap(short, long, value_delimiter = ',')] replica_urls: Option>, + #[clap(short, long)] + execution_rpc: Url, } diff --git a/opstack/src/builder.rs b/opstack/src/builder.rs index 0516075f..97a58eab 100644 --- a/opstack/src/builder.rs +++ b/opstack/src/builder.rs @@ -1,11 +1,11 @@ use std::net::SocketAddr; -use alloy::primitives::Address; use eyre::Result; use reqwest::{IntoUrl, Url}; use crate::{ - config::{ChainConfig, Config}, + config::Network, + config::{Config, NetworkConfig}, consensus::ConsensusClient, OpStackClient, }; @@ -13,8 +13,7 @@ use crate::{ #[derive(Default)] pub struct OpStackClientBuilder { config: Option, - chain_id: Option, - unsafe_signer: Option
, + network: Option, consensus_rpc: Option, execution_rpc: Option, rpc_socket: Option, @@ -30,16 +29,6 @@ impl OpStackClientBuilder { self } - pub fn chain_id(mut self, chain_id: u64) -> Self { - self.chain_id = Some(chain_id); - self - } - - pub fn unsafe_signer(mut self, signer: Address) -> Self { - self.unsafe_signer = Some(signer); - self - } - pub fn consensus_rpc(mut self, consensus_rpc: T) -> Self { self.consensus_rpc = Some(consensus_rpc.into_url().unwrap()); self @@ -55,16 +44,17 @@ impl OpStackClientBuilder { self } + pub fn network(mut self, network: Network) -> Self { + self.network = Some(network); + self + } + pub fn build(self) -> Result { let config = if let Some(config) = self.config { config } else { - let Some(chain_id) = self.chain_id else { - eyre::bail!("chain id required"); - }; - - let Some(unsafe_signer) = self.unsafe_signer else { - eyre::bail!("unsafe signer required"); + let Some(network) = self.network else { + eyre::bail!("network required"); }; let Some(consensus_rpc) = self.consensus_rpc else { @@ -79,13 +69,11 @@ impl OpStackClientBuilder { consensus_rpc, execution_rpc, rpc_socket: self.rpc_socket, - chain: ChainConfig { - chain_id, - unsafe_signer, - }, + chain: NetworkConfig::from(network).chain, + load_external_fallback: None, + checkpoint: None, } }; - let consensus = ConsensusClient::new(&config); OpStackClient::new( &config.execution_rpc.to_string(), diff --git a/opstack/src/config.rs b/opstack/src/config.rs index 547b33f0..76357307 100644 --- a/opstack/src/config.rs +++ b/opstack/src/config.rs @@ -2,28 +2,33 @@ use std::{ collections::HashMap, fmt::Display, net::SocketAddr, path::PathBuf, process::exit, str::FromStr, }; -use alloy::primitives::{address, Address}; +use alloy::primitives::{address, Address, B256}; use eyre::Result; use figment::{ providers::{Format, Serialized, Toml}, value::Value, Figment, }; +use helios_ethereum::config::networks::Network as EthNetwork; use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Config { pub consensus_rpc: Url, pub execution_rpc: Url, pub rpc_socket: Option, pub chain: ChainConfig, + pub load_external_fallback: Option, + pub checkpoint: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct ChainConfig { pub chain_id: u64, pub unsafe_signer: Address, + pub system_config_contract: Address, + pub eth_network: EthNetwork, } #[derive(Serialize, Deserialize)] @@ -73,6 +78,8 @@ impl From for NetworkConfig { chain: ChainConfig { chain_id: 10, unsafe_signer: address!("AAAA45d9549EDA09E70937013520214382Ffc4A2"), + system_config_contract: address!("229047fed2591dbec1eF1118d64F7aF3dB9EB290"), + eth_network: EthNetwork::MAINNET, }, }, Network::Base => NetworkConfig { @@ -80,6 +87,8 @@ impl From for NetworkConfig { chain: ChainConfig { chain_id: 8453, unsafe_signer: address!("Af6E19BE0F9cE7f8afd49a1824851023A8249e8a"), + system_config_contract: address!("73a79Fab69143498Ed3712e519A88a918e1f4072"), + eth_network: EthNetwork::MAINNET, }, }, Network::Worldchain => NetworkConfig { @@ -91,6 +100,8 @@ impl From for NetworkConfig { chain: ChainConfig { chain_id: 480, unsafe_signer: address!("2270d6eC8E760daA317DD978cFB98C8f144B1f3A"), + system_config_contract: address!("6ab0777fD0e609CE58F939a7F70Fe41F5Aa6300A"), + eth_network: EthNetwork::MAINNET, }, }, Network::Zora => NetworkConfig { @@ -98,6 +109,8 @@ impl From for NetworkConfig { chain: ChainConfig { chain_id: 7777777, unsafe_signer: address!("3Dc8Dfd0709C835cAd15a6A27e089FF4cF4C9228"), + system_config_contract: address!("A3cAB0126d5F504B071b81a3e8A2BBBF17930d86"), + eth_network: EthNetwork::MAINNET, }, }, } diff --git a/opstack/src/consensus.rs b/opstack/src/consensus.rs index 32bb4ebd..15fb5abb 100644 --- a/opstack/src/consensus.rs +++ b/opstack/src/consensus.rs @@ -1,12 +1,12 @@ -use std::time::Duration; - use alloy::consensus::Transaction as TxTrait; use alloy::primitives::{b256, fixed_bytes, keccak256, Address, B256, U256, U64}; use alloy::rlp::Decodable; -use alloy::rpc::types::{Parity, Signature, Transaction}; +use alloy::rpc::types::{EIP1186AccountProofResponse, Parity, Signature, Transaction}; use alloy_rlp::encode; -use eyre::Result; +use eyre::{eyre, OptionExt, Result}; use op_alloy_consensus::OpTxEnvelope; +use std::str::FromStr; +use std::time::Duration; use tokio::sync::mpsc::Sender; use tokio::sync::{ mpsc::{channel, Receiver}, @@ -14,12 +14,24 @@ use tokio::sync::{ }; use triehash_ethereum::ordered_trie_root; +use helios_consensus_core::consensus_spec::MainnetConsensusSpec; use helios_core::consensus::Consensus; use helios_core::time::{interval, SystemTime, UNIX_EPOCH}; use helios_core::types::{Block, Transactions}; +use helios_ethereum::consensus::ConsensusClient as EthConsensusClient; +use std::sync::{Arc, Mutex}; use crate::{config::Config, types::ExecutionPayload, SequencerCommitment}; +use helios_core::execution::proof::{encode_account, verify_proof}; +use helios_ethereum::database::ConfigDB; +use helios_ethereum::rpc::http_rpc::HttpRpc; +use tracing::{error, info, warn}; + +// Storage slot containing the unsafe signer address in all superchain system config contracts +const UNSAFE_SIGNER_SLOT: &str = + "0x65a7ed542fb37fe237fdfbdd70b31598523fe5b32879e307bae27a0bd9581c08"; + pub struct ConsensusClient { block_recv: Option>>, finalized_block_recv: Option>>>, @@ -33,13 +45,15 @@ impl ConsensusClient { let mut inner = Inner { server_url: config.consensus_rpc.to_string(), - unsafe_signer: config.chain.unsafe_signer, + unsafe_signer: Arc::new(Mutex::new(config.chain.unsafe_signer)), chain_id: config.chain.chain_id, latest_block: None, block_send, finalized_block_send, }; + verify_unsafe_signer(config.clone(), inner.unsafe_signer.clone()); + #[cfg(not(target_arch = "wasm32"))] let run = tokio::spawn; @@ -49,7 +63,9 @@ impl ConsensusClient { run(async move { let mut interval = interval(Duration::from_secs(1)); loop { - _ = inner.advance().await; + if let Err(e) = inner.advance().await { + error!(target: "helios::opstack", "failed to advance: {}", e); + } interval.tick().await; } }); @@ -87,7 +103,7 @@ impl Consensus for ConsensusClient { #[allow(dead_code)] struct Inner { server_url: String, - unsafe_signer: Address, + unsafe_signer: Arc>, chain_id: u64, latest_block: Option, block_send: Sender>, @@ -102,7 +118,11 @@ impl Inner { .json::() .await?; - if commitment.verify(self.unsafe_signer, self.chain_id).is_ok() { + let curr_signer = *self + .unsafe_signer + .lock() + .map_err(|_| eyre!("failed to lock signer"))?; + if commitment.verify(curr_signer, self.chain_id).is_ok() { let payload = ExecutionPayload::try_from(&commitment)?; if self .latest_block @@ -136,6 +156,87 @@ impl Inner { } } +fn verify_unsafe_signer(config: Config, signer: Arc>) { + #[cfg(not(target_arch = "wasm32"))] + let run = tokio::spawn; + + #[cfg(target_arch = "wasm32")] + let run = wasm_bindgen_futures::spawn_local; + + run(async move { + let mut eth_config = config.chain.eth_network.to_base_config(); + eth_config.load_external_fallback = config.load_external_fallback.unwrap_or(false); + if let Some(checkpoint) = config.checkpoint { + eth_config.default_checkpoint = checkpoint; + } + let mut eth_consensus = EthConsensusClient::::new( + ð_config + .consensus_rpc + .clone() + .ok_or_else(|| eyre!("missing consensus rpc"))?, + Arc::new(eth_config.into()), + )?; + let block = eth_consensus + .block_recv() + .unwrap() + .recv() + .await + .ok_or_eyre("failed to receive block")?; + // Query proof from op consensus server + let req = format!("{}unsafe_signer_proof/{}", config.consensus_rpc, block.hash); + let proof = reqwest::get(req) + .await? + .json::() + .await?; + + // Verify unsafe signer + // with account proof + let account_path = keccak256(proof.address).to_vec(); + let account_encoded = encode_account(&proof); + let is_valid = verify_proof( + &proof.account_proof, + block.state_root.as_slice(), + &account_path, + &account_encoded, + ); + if !is_valid { + warn!(target: "helios::opstack", "account proof invalid"); + return Err(eyre!("account proof invalid")); + } + // with storage proof + let storage_proof = proof.storage_proof[0].clone(); + let key = storage_proof.key.0; + if key != B256::from_str(UNSAFE_SIGNER_SLOT)? { + warn!(target: "helios::opstack", "account proof invalid"); + return Err(eyre!("account proof invalid")); + } + let key_hash = keccak256(key); + let value = encode(storage_proof.value); + let is_valid = verify_proof( + &storage_proof.proof, + proof.storage_hash.as_slice(), + key_hash.as_slice(), + &value, + ); + if !is_valid { + warn!(target: "helios::opstack", "storage proof invalid"); + return Err(eyre!("storage proof invalid")); + } + // Replace unsafe signer if different + let verified_signer = Address::from_slice(&storage_proof.value.to_be_bytes::<32>()[12..32]); + { + let mut curr_signer = signer.lock().map_err(|_| eyre!("failed to lock signer"))?; + if verified_signer != *curr_signer { + info!(target: "helios::opstack", "unsafe signer updated: {}", verified_signer); + *curr_signer = verified_signer; + } + } + // Shutdown eth consensus client + eth_consensus.shutdown()?; + Ok(()) + }); +} + fn payload_to_block(value: ExecutionPayload) -> Result> { let empty_nonce = fixed_bytes!("0000000000000000"); let empty_uncle_hash = diff --git a/opstack/src/server/mod.rs b/opstack/src/server/mod.rs index 6850be3a..651d1b8e 100644 --- a/opstack/src/server/mod.rs +++ b/opstack/src/server/mod.rs @@ -1,7 +1,15 @@ use std::{net::SocketAddr, sync::Arc, time::Duration}; -use alloy::primitives::Address; -use axum::{extract::State, routing::get, Json, Router}; +use alloy::{ + primitives::{Address, FixedBytes, B256}, + rpc::types::EIP1186AccountProofResponse, +}; +use axum::{ + extract::{Path, State}, + http::StatusCode, + routing::get, + Json, Router, +}; use eyre::Result; use tokio::{ sync::{ @@ -15,22 +23,33 @@ use url::Url; use crate::{types::ExecutionPayload, SequencerCommitment}; use self::net::{block_handler::BlockHandler, gossip::GossipService}; +use helios_core::execution::rpc::{http_rpc::HttpRpc, ExecutionRpc}; +use helios_ethereum::spec::Ethereum; +use std::str::FromStr; pub mod net; mod poller; +// Storage slot containing the unsafe signer address in all superchain system config contracts +const UNSAFE_SIGNER_SLOT: &str = + "0x65a7ed542fb37fe237fdfbdd70b31598523fe5b32879e307bae27a0bd9581c08"; + pub async fn start_server( server_addr: SocketAddr, gossip_addr: SocketAddr, chain_id: u64, signer: Address, + system_config_contract: Address, replica_urls: Vec, + execution_rpc: Url, ) -> Result<()> { let state = Arc::new(RwLock::new(ServerState::new( gossip_addr, chain_id, signer, + system_config_contract, replica_urls, + execution_rpc, )?)); let state_copy = state.clone(); @@ -44,6 +63,10 @@ pub async fn start_server( let router = Router::new() .route("/latest", get(latest_handler)) .route("/chain_id", get(chain_id_handler)) + .route( + "/unsafe_signer_proof/:block_hash", + get(unsafe_signer_proof_handler), + ) .with_state(state); let listener = tokio::net::TcpListener::bind(server_addr).await?; @@ -62,10 +85,30 @@ async fn chain_id_handler(State(state): State>>) -> Json Json(state.read().await.chain_id) } +async fn unsafe_signer_proof_handler( + State(state): State>>, + Path(block_hash): Path>, +) -> Result, StatusCode> { + let rpc = HttpRpc::::new(&state.read().await.execution_rpc.to_string()) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let signer_slot = + B256::from_str(UNSAFE_SIGNER_SLOT).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let proof = rpc + .get_proof( + state.read().await.system_config_contract, + &[signer_slot], + block_hash.into(), + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(proof)) +} struct ServerState { chain_id: u64, commitment_recv: Receiver, latest_commitment: Option<(SequencerCommitment, u64)>, + execution_rpc: Url, + system_config_contract: Address, } impl ServerState { @@ -73,7 +116,9 @@ impl ServerState { addr: SocketAddr, chain_id: u64, signer: Address, + system_config_contract: Address, replica_urls: Vec, + execution_rpc: Url, ) -> Result { let (send, commitment_recv) = channel(256); poller::start(replica_urls, signer, chain_id, send.clone()); @@ -85,6 +130,8 @@ impl ServerState { chain_id, commitment_recv, latest_commitment: None, + execution_rpc, + system_config_contract, }) }