diff --git a/Node/src/ethereum_l1/execution_layer.rs b/Node/src/ethereum_l1/execution_layer.rs index 67a2229..f069713 100644 --- a/Node/src/ethereum_l1/execution_layer.rs +++ b/Node/src/ethereum_l1/execution_layer.rs @@ -1,10 +1,11 @@ use super::slot_clock::SlotClock; use crate::utils::{config, types::*}; use alloy::{ + consensus::TypedTransaction, contract::EventPoller, network::{Ethereum, EthereumWallet, NetworkWallet}, primitives::{Address, Bytes, FixedBytes, B256, U256}, - providers::ProviderBuilder, + providers::{Provider, ProviderBuilder}, signers::{ local::{LocalSigner, PrivateKeySigner}, Signature, SignerSync, @@ -29,6 +30,7 @@ pub struct ExecutionLayer { contract_addresses: ContractAddresses, slot_clock: Arc, preconf_registry_expiry_sec: u64, + chain_id: u64, } pub struct ContractAddresses { @@ -126,7 +128,7 @@ sol!( ); impl ExecutionLayer { - pub fn new( + pub async fn new( rpc_url: &str, avs_node_ecdsa_private_key: &str, contract_addresses: &config::ContractAddresses, @@ -144,6 +146,9 @@ impl ExecutionLayer { let contract_addresses = Self::parse_contract_addresses(contract_addresses) .map_err(|e| Error::msg(format!("Failed to parse contract addresses: {}", e)))?; + let provider = ProviderBuilder::new().on_http(rpc_url.parse()?); + let chain_id = provider.get_chain_id().await?; + Ok(Self { rpc_url: rpc_url.parse()?, signer, @@ -152,6 +157,7 @@ impl ExecutionLayer { contract_addresses, slot_clock, preconf_registry_expiry_sec, + chain_id, }) } @@ -179,17 +185,16 @@ impl ExecutionLayer { pub async fn propose_new_block( &self, + nonce: u64, tx_list: Vec, parent_meta_hash: [u8; 32], lookahead_set: Vec, - ) -> Result<(), Error> { - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .wallet(self.wallet.clone()) - .on_http(self.rpc_url.clone()); + send_to_contract: bool, + ) -> Result, Error> { + let provider = ProviderBuilder::new().on_http(self.rpc_url.clone()); let contract = - PreconfTaskManager::new(self.contract_addresses.avs.preconf_task_manager, provider); + PreconfTaskManager::new(self.contract_addresses.avs.preconf_task_manager, &provider); let block_params = BlockParams { assignedProver: Address::ZERO, @@ -207,6 +212,8 @@ impl ExecutionLayer { let encoded_block_params = Bytes::from(BlockParams::abi_encode_sequence(&block_params)); let tx_list = Bytes::from(tx_list); + + // create lookahead set let lookahead_set_param = lookahead_set .iter() .map(|duty| { @@ -217,17 +224,50 @@ impl ExecutionLayer { }) .collect::, Error>>()?; - let builder = contract.newBlockProposal( - encoded_block_params, - tx_list, - U256::from(0), //TODO: Replace it with the proper lookaheadPointer when the contract is ready. - lookahead_set_param, - ); + // TODO check gas parameters + let builder = contract + .newBlockProposal( + encoded_block_params, + tx_list, + U256::from(0), //TODO: Replace it with the proper lookaheadPointer when the contract is ready. + lookahead_set_param, + ) + .chain_id(self.chain_id) + .nonce(nonce) //TODO how to get it? + .gas(50_000) + .max_fee_per_gas(20_000_000_000) + .max_priority_fee_per_gas(1_000_000_000); + + // Build transaction + let tx = builder.as_ref().clone().build_typed_tx(); + let Ok(TypedTransaction::Eip1559(mut tx)) = tx else { + // TODO fix + panic!("Not EIP1559 transaction"); + }; - let tx_hash = builder.send().await?.watch().await?; - tracing::debug!("Proposed new block: {tx_hash}"); + // Sign transaction + let signature = self + .wallet + .default_signer() + .sign_transaction(&mut tx) + .await?; - Ok(()) + // Encode transaction + let mut buf = vec![]; + tx.encode_with_signature(&signature, &mut buf, false); + + // Send transaction + if send_to_contract { + let pending = provider + .send_raw_transaction(&buf) + .await? + .register() + .await?; + + tracing::debug!("Proposed new block, with hash {}", pending.tx_hash()); + } + + Ok(buf) } pub async fn register_preconfer(&self) -> Result<(), Error> { @@ -319,6 +359,15 @@ impl ExecutionLayer { Ok(address) } + pub async fn get_preconfer_nonce(&self) -> Result { + let provider = ProviderBuilder::new().on_http(self.rpc_url.clone()); + + let nonce = provider + .get_transaction_count(self.preconfer_address) + .await?; + Ok(nonce) + } + pub async fn prove_incorrect_preconfirmation( &self, block_id: u64, @@ -410,7 +459,7 @@ impl ExecutionLayer { let params = contract .getLookaheadParamsForEpoch( U256::from(epoch_begin_timestamp), - validator_bls_pub_keys.map(|key| Bytes::from(key)), + validator_bls_pub_keys.map(Bytes::from), ) .call() .await? @@ -437,7 +486,7 @@ impl ExecutionLayer { } #[cfg(test)] - pub fn new_from_pk( + pub async fn new_from_pk( rpc_url: reqwest::Url, private_key: elliptic_curve::SecretKey, ) -> Result { @@ -445,6 +494,9 @@ impl ExecutionLayer { let wallet = EthereumWallet::from(signer.clone()); let clock = SlotClock::new(0u64, 0u64, 12u64, 32u64); + let provider = ProviderBuilder::new().on_http(rpc_url.clone()); + let chain_id = provider.get_chain_id().await?; + Ok(Self { rpc_url, signer, @@ -465,6 +517,7 @@ impl ExecutionLayer { }, }, preconf_registry_expiry_sec: 120, + chain_id, }) } @@ -521,7 +574,9 @@ mod tests { let anvil = Anvil::new().try_spawn().unwrap(); let rpc_url: reqwest::Url = anvil.endpoint().parse().unwrap(); let private_key = anvil.keys()[0].clone(); - let el = ExecutionLayer::new_from_pk(rpc_url, private_key).unwrap(); + let el = ExecutionLayer::new_from_pk(rpc_url, private_key) + .await + .unwrap(); el.call_test_contract().await.unwrap(); } @@ -530,9 +585,11 @@ mod tests { let anvil = Anvil::new().try_spawn().unwrap(); let rpc_url: reqwest::Url = anvil.endpoint().parse().unwrap(); let private_key = anvil.keys()[0].clone(); - let el = ExecutionLayer::new_from_pk(rpc_url, private_key).unwrap(); + let el = ExecutionLayer::new_from_pk(rpc_url, private_key) + .await + .unwrap(); - el.propose_new_block(vec![0; 32], [0; 32], vec![]) + el.propose_new_block(0, vec![0; 32], [0; 32], vec![], true) .await .unwrap(); } @@ -541,7 +598,9 @@ mod tests { let anvil = Anvil::new().try_spawn().unwrap(); let rpc_url: reqwest::Url = anvil.endpoint().parse().unwrap(); let private_key = anvil.keys()[0].clone(); - let el = ExecutionLayer::new_from_pk(rpc_url, private_key).unwrap(); + let el = ExecutionLayer::new_from_pk(rpc_url, private_key) + .await + .unwrap(); let result = el.register_preconfer().await; assert!(result.is_ok(), "Register method failed: {:?}", result.err()); diff --git a/Node/src/ethereum_l1/mod.rs b/Node/src/ethereum_l1/mod.rs index d1be9f5..2d34719 100644 --- a/Node/src/ethereum_l1/mod.rs +++ b/Node/src/ethereum_l1/mod.rs @@ -39,7 +39,8 @@ impl EthereumL1 { contract_addresses, slot_clock.clone(), preconf_registry_expiry_sec, - )?; + ) + .await?; Ok(Self { slot_clock, @@ -64,9 +65,11 @@ mod tests { let anvil = Anvil::new().try_spawn().unwrap(); let rpc_url: reqwest::Url = anvil.endpoint().parse().unwrap(); let private_key = anvil.keys()[0].clone(); - let el = ExecutionLayer::new_from_pk(rpc_url, private_key).unwrap(); + let el = ExecutionLayer::new_from_pk(rpc_url, private_key) + .await + .unwrap(); - el.propose_new_block(vec![0; 32], [0; 32], duties) + el.propose_new_block(0, vec![0; 32], [0; 32], duties, true) .await .unwrap(); } diff --git a/Node/src/main.rs b/Node/src/main.rs index 3d91d4e..e7ea335 100644 --- a/Node/src/main.rs +++ b/Node/src/main.rs @@ -55,11 +55,12 @@ async fn main() -> Result<(), Error> { config.taiko_chain_id, )); - let mev_boost = mev_boost::MevBoost::new(&config.mev_boost_url); + let mev_boost = mev_boost::MevBoost::new(&config.mev_boost_url, config.validator_index); let block_proposed_event_checker = BlockProposedEventReceiver::new(taiko.clone(), node_tx.clone()); BlockProposedEventReceiver::start(block_proposed_event_checker).await; let ethereum_l1 = Arc::new(ethereum_l1); + let node = node::Node::new( node_rx, node_to_p2p_tx, diff --git a/Node/src/mev_boost/constraints.rs b/Node/src/mev_boost/constraints.rs new file mode 100644 index 0000000..3030c6c --- /dev/null +++ b/Node/src/mev_boost/constraints.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Constraint { + tx: String, + index: Option, +} + +impl Constraint { + pub fn new(tx: String, index: Option) -> Self { + Self { tx, index } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ConstraintsMessage { + validator_index: u64, + slot: u64, + constraints: Vec, +} + +impl ConstraintsMessage { + pub fn new(validator_index: u64, slot: u64, constraints: Vec) -> Self { + Self { + validator_index, + slot, + constraints, + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct SignedConstraints { + message: ConstraintsMessage, + signature: String, +} + +impl SignedConstraints { + pub fn new(message: ConstraintsMessage, signature: String) -> Self { + Self { message, signature } + } +} + +impl From for Vec { + fn from(val: ConstraintsMessage) -> Self { + bincode::serialize(&val).expect("MEV Boost message serialization failed") + } +} diff --git a/Node/src/mev_boost/mod.rs b/Node/src/mev_boost/mod.rs index 388d95c..c0bd630 100644 --- a/Node/src/mev_boost/mod.rs +++ b/Node/src/mev_boost/mod.rs @@ -1,20 +1,57 @@ -#![allow(unused)] //TODO remove after the EthereumL1 is used in release code - +use crate::ethereum_l1::EthereumL1; use crate::utils::rpc_client::RpcClient; +use anyhow::Error; +use std::sync::Arc; + +pub mod constraints; +use constraints::{Constraint, ConstraintsMessage, SignedConstraints}; pub struct MevBoost { - _rpc_client: RpcClient, + rpc_client: RpcClient, + validator_index: u64, } impl MevBoost { - pub fn new(rpc_url: &str) -> Self { + pub fn new(rpc_url: &str, validator_index: u64) -> Self { let rpc_client = RpcClient::new(rpc_url); Self { - _rpc_client: rpc_client, + rpc_client, + validator_index, } } - pub fn send_transaction(&self, _tx: &[u8], _validator_index: u64, _slot: u64) { - //TODO: implement + pub async fn force_inclusion( + &self, + constraints: Vec, + ethereum_l1: Arc, + ) -> Result<(), Error> { + // Prepare the message + // TODO check slot id value + let slot_id = ethereum_l1.slot_clock.get_current_slot()?; + + let message = ConstraintsMessage::new(self.validator_index, slot_id, constraints); + + let data_to_sign: Vec = message.clone().into(); + + // Sign the message + // TODO: Determine if the transaction data needs to be signed as a JSON string. + let signature = ethereum_l1 + .execution_layer + .sign_message_with_private_ecdsa_key(&data_to_sign)?; + + // Prepare data to send + let signed_constraints = + SignedConstraints::new(message, format!("0x{}", hex::encode(signature))); + let json_data = serde_json::to_value(&signed_constraints).unwrap(); + + // https://chainbound.github.io/bolt-docs/api/builder#ethv1builderconstraints + let method = "/eth/v1/builder/constraints"; + // Make rpc request + self.rpc_client + .call_method(method, vec![json_data]) + .await + .unwrap(); + + Ok(()) } } diff --git a/Node/src/node/mod.rs b/Node/src/node/mod.rs index 1067f00..1dd1122 100644 --- a/Node/src/node/mod.rs +++ b/Node/src/node/mod.rs @@ -1,6 +1,6 @@ use crate::{ ethereum_l1::{slot_clock::Epoch, EthereumL1}, - mev_boost::MevBoost, + mev_boost::{self, MevBoost}, taiko::{l2_tx_lists::RPCReplyL2TxLists, Taiko}, utils::{ block_proposed::BlockProposed, commit::L2TxListsCommit, @@ -8,10 +8,15 @@ use crate::{ preconfirmation_proof::PreconfirmationProof, }, }; +use alloy::hex; use anyhow::{anyhow as any_err, Error}; use beacon_api_client::ProposerDuty; use operator::{Operator, Status as OperatorStatus}; -use std::{collections::HashMap, sync::Arc}; +use std::sync::atomic::Ordering; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; use tokio::sync::{ mpsc::{Receiver, Sender}, Mutex, @@ -20,6 +25,10 @@ use tracing::info; pub mod block_proposed_receiver; mod operator; +mod preconfirmation_helper; +use preconfirmation_helper::PreconfirmationHelper; + +use mev_boost::constraints::Constraint; const OLDEST_BLOCK_DISTANCE: u64 = 256; @@ -30,12 +39,15 @@ pub struct Node { p2p_to_node_rx: Option>>, gas_used: u64, ethereum_l1: Arc, - _mev_boost: MevBoost, // temporary unused + mev_boost: MevBoost, epoch: Epoch, lookahead: Vec, l2_slot_duration_sec: u64, preconfirmed_blocks: Arc>>, + is_preconfer_now: Arc, + preconfirmation_txs: Arc>>>, // block_id -> tx operator: Operator, + preconfirmation_helper: PreconfirmationHelper, } impl Node { @@ -57,12 +69,15 @@ impl Node { p2p_to_node_rx: Some(p2p_to_node_rx), gas_used: 0, ethereum_l1, - _mev_boost: mev_boost, + mev_boost, epoch: current_epoch, lookahead: vec![], l2_slot_duration_sec, preconfirmed_blocks: Arc::new(Mutex::new(HashMap::new())), + is_preconfer_now: Arc::new(AtomicBool::new(false)), + preconfirmation_txs: Arc::new(Mutex::new(HashMap::new())), operator, + preconfirmation_helper: PreconfirmationHelper::new(), }) } @@ -79,6 +94,8 @@ impl Node { let preconfirmed_blocks = self.preconfirmed_blocks.clone(); let ethereum_l1 = self.ethereum_l1.clone(); let taiko = self.taiko.clone(); + let is_preconfer_now = self.is_preconfer_now.clone(); + let preconfirmation_txs = self.preconfirmation_txs.clone(); if let Some(node_rx) = self.node_rx.take() { let p2p_to_node_rx = self.p2p_to_node_rx.take().unwrap(); tokio::spawn(async move { @@ -88,6 +105,8 @@ impl Node { preconfirmed_blocks, ethereum_l1, taiko, + is_preconfer_now, + preconfirmation_txs, ) .await; }); @@ -102,22 +121,33 @@ impl Node { preconfirmed_blocks: Arc>>, ethereum_l1: Arc, taiko: Arc, + is_preconfer_now: Arc, + preconfirmation_txs: Arc>>>, ) { loop { tokio::select! { Some(block_proposed) = node_rx.recv() => { - tracing::debug!("Node received block proposed event: {:?}", block_proposed); - if let Err(e) = Self::check_preconfirmed_blocks_correctness(&preconfirmed_blocks, taiko.chain_id, &block_proposed, ethereum_l1.clone()).await { - tracing::error!("Failed to check preconfirmed blocks correctness: {}", e); - } - if let Err(e) = Self::clean_old_blocks(&preconfirmed_blocks, block_proposed.block_id).await { - tracing::error!("Failed to clean old blocks: {}", e); + if !is_preconfer_now.load(Ordering::Acquire) { + tracing::debug!("Node received block proposed event: {:?}", block_proposed); + if let Err(e) = Self::check_preconfirmed_blocks_correctness(&preconfirmed_blocks, taiko.chain_id, &block_proposed, ethereum_l1.clone()).await { + tracing::error!("Failed to check preconfirmed blocks correctness: {}", e); + } + if let Err(e) = Self::clean_old_blocks(&preconfirmed_blocks, block_proposed.block_id).await { + tracing::error!("Failed to clean old blocks: {}", e); + } + } else { + tracing::debug!("Node is Preconfer and received block proposed event: {:?}", block_proposed); + preconfirmation_txs.lock().await.remove(&block_proposed.block_id); } }, Some(p2p_message) = p2p_to_node_rx.recv() => { - let msg: PreconfirmationMessage = p2p_message.into(); - tracing::debug!("Node received message from p2p: {:?}", msg); - Self::check_preconfirmation_message(msg, &preconfirmed_blocks, ethereum_l1.clone(), taiko.clone()).await; + if !is_preconfer_now.load(Ordering::Acquire) { + let msg: PreconfirmationMessage = p2p_message.into(); + tracing::debug!("Node received message from p2p: {:?}", msg); + Self::check_preconfirmation_message(msg, &preconfirmed_blocks, ethereum_l1.clone(), taiko.clone()).await; + } else { + tracing::debug!("Node is Preconfer and received message from p2p: {:?}", p2p_message); + } } } } @@ -210,9 +240,9 @@ impl Node { } async fn preconfirmation_loop(&mut self) { + // TODO syncronize with slot clock let mut interval = tokio::time::interval(std::time::Duration::from_secs(self.l2_slot_duration_sec)); - loop { interval.tick().await; @@ -224,16 +254,17 @@ impl Node { async fn main_block_preconfirmation_step(&mut self) -> Result<(), Error> { let current_epoch = self.ethereum_l1.slot_clock.get_current_epoch()?; - let current_epoch_timestamp = self - .ethereum_l1 - .slot_clock - .get_epoch_begin_timestamp(current_epoch)?; if current_epoch != self.epoch { tracing::debug!( "Current epoch changed from {} to {}", self.epoch, current_epoch ); + let current_epoch_timestamp = self + .ethereum_l1 + .slot_clock + .get_epoch_begin_timestamp(current_epoch)?; + self.epoch = current_epoch; self.operator = Operator::new(self.ethereum_l1.clone()); @@ -252,25 +283,15 @@ impl Node { match self.operator.get_status(current_slot)? { OperatorStatus::PreconferAndProposer => { - if self - .operator - .should_post_lookahead(current_epoch_timestamp) - .await? - { - // TODO: post lookahead - } - // TODO: replace with mev-boost forced inclusion list - self.preconfirm_block().await?; + self.preconfirm_last_slot().await?; } OperatorStatus::Preconfer => { - if self - .operator - .should_post_lookahead(current_epoch_timestamp) - .await? - { - // TODO: post lookahead + if !self.is_preconfer_now.load(Ordering::Acquire) { + self.is_preconfer_now.store(true, Ordering::Release); + self.start_propose().await?; } - self.preconfirm_block().await?; + + self.preconfirm_block(true).await?; } OperatorStatus::None => { tracing::debug!("Not my slot to preconfirm: {}", current_slot); @@ -280,18 +301,77 @@ impl Node { Ok(()) } - async fn preconfirm_block(&mut self) -> Result<(), Error> { + async fn preconfirm_last_slot(&mut self) -> Result<(), Error> { + self.preconfirm_block(false).await?; + if self + .preconfirmation_helper + .is_last_final_slot_perconfirmation() + { + // Last(4th) perconfirmation when we are proposer and preconfer + self.is_preconfer_now.store(false, Ordering::Release); + + let mut preconfirmation_txs = self.preconfirmation_txs.lock().await; + if !preconfirmation_txs.is_empty() { + // Build constraints + let constraints: Vec = preconfirmation_txs + .iter() + .map(|(_, value)| Constraint::new(format!("0x{}", hex::encode(value)), None)) + .collect(); + + self.mev_boost + .force_inclusion(constraints, self.ethereum_l1.clone()) + .await?; + + preconfirmation_txs.clear(); + } + } else { + // Increment perconfirmations count when we are proposer and preconfer + self.preconfirmation_helper + .increment_final_slot_perconfirmation(); + } + + Ok(()) + } + + async fn start_propose(&mut self) -> Result<(), Error> { + // get L1 preconfer wallet nonce + let nonce = self + .ethereum_l1 + .execution_layer + .get_preconfer_nonce() + .await?; + + self.preconfirmation_helper.init(nonce); + Ok(()) + } + + async fn preconfirm_block(&mut self, send_to_contract: bool) -> Result<(), Error> { tracing::debug!( "Preconfirming for the slot: {:?}", self.ethereum_l1.slot_clock.get_current_slot()? ); + let current_epoch_timestamp = self + .ethereum_l1 + .slot_clock + .get_epoch_begin_timestamp(self.epoch)?; + + if self + .operator + .should_post_lookahead(current_epoch_timestamp) + .await? + { + // TODO: build lookahead + } + let pending_tx_lists = self.taiko.get_pending_l2_tx_lists().await?; if pending_tx_lists.tx_list_bytes.is_empty() { return Ok(()); } let new_block_height = pending_tx_lists.parent_block_id + 1; + let nonce = self.preconfirmation_helper.get_next_nonce(); + let (commit_hash, signature) = self.generate_commit_hash_and_signature(&pending_tx_lists, new_block_height)?; @@ -311,15 +391,24 @@ impl Node { self.taiko .advance_head_to_new_l2_block(pending_tx_lists.tx_lists, self.gas_used) .await?; - self.ethereum_l1 + let tx = self + .ethereum_l1 .execution_layer .propose_new_block( + nonce, pending_tx_lists.tx_list_bytes[0].clone(), //TODO: handle rest tx lists pending_tx_lists.parent_meta_hash, std::mem::take(&mut self.lookahead), + send_to_contract, ) .await?; + // insert transaction + self.preconfirmation_txs + .lock() + .await + .insert(new_block_height, tx); + self.preconfirmed_blocks .lock() .await diff --git a/Node/src/node/operator.rs b/Node/src/node/operator.rs index 2acf422..172da6c 100644 --- a/Node/src/node/operator.rs +++ b/Node/src/node/operator.rs @@ -72,7 +72,7 @@ impl Operator { return Ok(true); } } - return Ok(false); + Ok(false) } pub async fn find_slots_to_preconfirm( diff --git a/Node/src/node/preconfirmation_helper.rs b/Node/src/node/preconfirmation_helper.rs new file mode 100644 index 0000000..6eb3246 --- /dev/null +++ b/Node/src/node/preconfirmation_helper.rs @@ -0,0 +1,32 @@ +pub struct PreconfirmationHelper { + nonce: u64, + final_slot_perconfirmation_count: u8, +} + +impl PreconfirmationHelper { + pub fn new() -> Self { + Self { + nonce: 0, + final_slot_perconfirmation_count: 0, + } + } + + pub fn init(&mut self, nonce: u64) { + self.nonce = nonce; + self.final_slot_perconfirmation_count = 0; + } + + pub fn get_next_nonce(&mut self) -> u64 { + let nonce = self.nonce; + self.nonce += 1; + nonce + } + + pub fn increment_final_slot_perconfirmation(&mut self) { + self.final_slot_perconfirmation_count += 1; + } + + pub fn is_last_final_slot_perconfirmation(&self) -> bool { + self.final_slot_perconfirmation_count >= 3 + } +} diff --git a/Node/src/utils/config.rs b/Node/src/utils/config.rs index 34bcd79..4367811 100644 --- a/Node/src/utils/config.rs +++ b/Node/src/utils/config.rs @@ -17,6 +17,7 @@ pub struct Config { pub contract_addresses: ContractAddresses, pub p2p_network_config: P2PNetworkConfig, pub taiko_chain_id: u64, + pub validator_index: u64, } #[derive(Debug)] @@ -207,6 +208,11 @@ impl Config { }) .expect("TAIKO_CHAIN_ID must be a number"); + let validator_index = std::env::var("VALIDATOR_INDEX") + .expect("VALIDATOR_INDEX env variable must be set") + .parse::() + .expect("VALIDATOR_INDEX must be a number"); + let config = Self { taiko_proposer_url: std::env::var("TAIKO_PROPOSER_URL") .unwrap_or("http://127.0.0.1:1234".to_string()), @@ -227,6 +233,7 @@ impl Config { contract_addresses, p2p_network_config, taiko_chain_id, + validator_index, }; info!(